- Core: Schema (Codable), Store, EventBus, RPC, Engine, I18n - Controls: Text, Title1-6, Label, Input, Textarea, Number, Date, Button, Select VBox, HBox, ScrollPanel, DynamicColumn, Tabular, Form, InlineForm, TabView, Menu, PopupWindow, Html, Image, UrlWidget - Renderer: BricksView (main entry), ControlRenderer (recursive dispatch) - 30+ widget types, 9 bind actiontypes - Form validation (required/minlength/maxlength/min/max/email/number/pattern) - i18n with multi-language JSON files - 18 tests covering schema parsing, store, i18n, events
153 lines
4.8 KiB
Swift
153 lines
4.8 KiB
Swift
import SwiftUI
|
||
|
||
// MARK: - Button Control
|
||
|
||
struct ButtonControl: View {
|
||
let schema: ControlSchema
|
||
@ObservedObject var engine: BricksEngine
|
||
@State private var isLoading: Bool = false
|
||
|
||
var body: some View {
|
||
Button(action: handleAction) {
|
||
HStack(spacing: 6) {
|
||
if isLoading {
|
||
ProgressView()
|
||
.scaleEffect(0.7)
|
||
}
|
||
if let icon = schema.options.icon {
|
||
Text(icon)
|
||
}
|
||
Text(displayLabel)
|
||
}
|
||
.padding(.horizontal, 12)
|
||
.padding(.vertical, 6)
|
||
}
|
||
.buttonStyle(buttonStyle)
|
||
.disabled(isLoading)
|
||
}
|
||
|
||
private var displayLabel: String {
|
||
let raw = schema.options.label ?? ""
|
||
return (schema.options.i18n != false) ? engine.i18n.t(raw) : raw
|
||
}
|
||
|
||
private var buttonStyle: BricksButtonStyle {
|
||
let css = schema.options.css ?? ""
|
||
if css.contains("primary") {
|
||
return BricksButtonStyle(variant: .primary)
|
||
} else if css.contains("danger") {
|
||
return BricksButtonStyle(variant: .danger)
|
||
} else if css.contains("text") {
|
||
return BricksButtonStyle(variant: .text)
|
||
}
|
||
return BricksButtonStyle(variant: .default)
|
||
}
|
||
|
||
private func handleAction() {
|
||
// 如果有binds,触发click事件
|
||
Task {
|
||
await engine.triggerEvent(widgetId: schema.effectiveId, event: "click")
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Button Style
|
||
|
||
struct BricksButtonStyle: ButtonStyle {
|
||
enum Variant { case `default`, primary, danger, text }
|
||
let variant: Variant
|
||
|
||
func makeBody(configuration: Configuration) -> some View {
|
||
configuration.label
|
||
.foregroundColor(textColor)
|
||
.background(backgroundColor(configuration.isPressed))
|
||
.cornerRadius(6)
|
||
.opacity(configuration.isPressed ? 0.8 : 1.0)
|
||
}
|
||
|
||
private var textColor: Color {
|
||
switch variant {
|
||
case .primary: return .white
|
||
case .danger: return .white
|
||
case .text: return .accentColor
|
||
case .default: return .primary
|
||
}
|
||
}
|
||
|
||
private func backgroundColor(_ pressed: Bool) -> Color {
|
||
switch variant {
|
||
case .primary: return pressed ? .blue.opacity(0.7) : .blue
|
||
case .danger: return pressed ? .red.opacity(0.7) : .red
|
||
case .text: return .clear
|
||
case .default: return pressed ? Color.secondary.opacity(0.2) : Color.secondary.opacity(0.1)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Select Control
|
||
|
||
struct SelectControl: View {
|
||
let schema: ControlSchema
|
||
@ObservedObject var engine: BricksEngine
|
||
@State private var selectedValue: String = ""
|
||
@State private var items: [CodeItem] = []
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
if let label = schema.options.label ?? schema.options.title {
|
||
Text(engine.i18n.t(label))
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
}
|
||
|
||
Picker("", selection: $selectedValue) {
|
||
ForEach(items, id: \.value) { item in
|
||
Text(engine.i18n.t(item.text)).tag(item.value)
|
||
}
|
||
}
|
||
.pickerStyle(.menu)
|
||
}
|
||
.onAppear {
|
||
loadItems()
|
||
selectedValue = schema.options.value ?? ""
|
||
}
|
||
.onChange(of: selectedValue) { _, newValue in
|
||
engine.store.setValue(id: schema.effectiveId, value: newValue)
|
||
Task {
|
||
await engine.triggerEvent(widgetId: schema.effectiveId, event: "change",
|
||
data: EventData(["value": newValue]))
|
||
}
|
||
}
|
||
.task {
|
||
await loadRemoteData()
|
||
}
|
||
}
|
||
|
||
private func loadItems() {
|
||
if let data = schema.options.data ?? schema.options.codes {
|
||
items = data
|
||
} else {
|
||
items = []
|
||
}
|
||
}
|
||
|
||
private func loadRemoteData() async {
|
||
guard let dataurl = schema.options.dataurl else { return }
|
||
do {
|
||
let response = try await engine.rpc.get(dataurl)
|
||
if response.isSuccess, let dict = response.dictionary {
|
||
if let rows = dict["rows"] as? [[String: Any]] {
|
||
let vf = schema.options.valueField ?? "value"
|
||
let tf = schema.options.textField ?? "text"
|
||
items = rows.compactMap { row in
|
||
guard let v = row[vf] as? String, let t = row[tf] as? String else { return nil }
|
||
return CodeItem(value: v, text: t)
|
||
}
|
||
}
|
||
}
|
||
} catch {
|
||
print("[SwiftBricks] Select data load failed: \(error)")
|
||
}
|
||
}
|
||
}
|