- 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
175 lines
5.5 KiB
Swift
175 lines
5.5 KiB
Swift
import SwiftUI
|
||
|
||
// MARK: - Input Control
|
||
|
||
struct InputControl: View {
|
||
let schema: ControlSchema
|
||
@ObservedObject var engine: BricksEngine
|
||
@State private var text: String = ""
|
||
@State private var isFocused: Bool = false
|
||
@State private var errorMessage: String = ""
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
if showLabel {
|
||
Text(effectiveLabel)
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
}
|
||
|
||
TextField(placeholder, text: $text)
|
||
.textFieldStyle(.roundedBorder)
|
||
.onChange(of: text) { _, newValue in
|
||
let formId = findParentFormId()
|
||
if let formId, let name = schema.options.name {
|
||
engine.store.setFormField(formId: formId, field: name, value: newValue)
|
||
}
|
||
engine.store.setValue(id: schema.effectiveId, value: newValue)
|
||
if isFocused { clearError() }
|
||
}
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 6)
|
||
.stroke(errorMessage.isEmpty ? Color.clear : Color.red, lineWidth: 1)
|
||
)
|
||
|
||
if !errorMessage.isEmpty {
|
||
Text(errorMessage)
|
||
.font(.caption2)
|
||
.foregroundColor(.red)
|
||
}
|
||
}
|
||
.onAppear {
|
||
text = schema.options.value ?? schema.options.defaultvalue ?? ""
|
||
let formId = findParentFormId()
|
||
if let formId, let name = schema.options.name {
|
||
text = engine.store.getFormField(formId: formId, field: name)
|
||
if text.isEmpty {
|
||
text = schema.options.value ?? schema.options.defaultvalue ?? ""
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private var effectiveLabel: String {
|
||
schema.options.label ?? schema.options.title ?? schema.options.name ?? ""
|
||
}
|
||
|
||
private var placeholder: String {
|
||
let raw = schema.options.placeholder ?? ""
|
||
return raw.isEmpty ? effectiveLabel : engine.i18n.t(raw)
|
||
}
|
||
|
||
private var showLabel: Bool { true }
|
||
|
||
private func findParentFormId() -> String? {
|
||
// 简化实现:使用schema id作为form标识
|
||
schema.options.name
|
||
}
|
||
|
||
private func clearError() {
|
||
errorMessage = ""
|
||
}
|
||
}
|
||
|
||
// MARK: - Textarea Control
|
||
|
||
struct TextareaControl: View {
|
||
let schema: ControlSchema
|
||
@ObservedObject var engine: BricksEngine
|
||
@State private var text: String = ""
|
||
|
||
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)
|
||
}
|
||
|
||
#if os(iOS)
|
||
TextEditor(text: $text)
|
||
.frame(minHeight: 100)
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 6)
|
||
.stroke(Color.secondary.opacity(0.3), lineWidth: 1)
|
||
)
|
||
#else
|
||
TextEditor(text: $text)
|
||
.frame(minHeight: 100)
|
||
.border(Color.secondary.opacity(0.3))
|
||
#endif
|
||
}
|
||
.onAppear {
|
||
text = schema.options.value ?? ""
|
||
}
|
||
.onChange(of: text) { _, newValue in
|
||
engine.store.setValue(id: schema.effectiveId, value: newValue)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Number Control
|
||
|
||
struct NumberControl: View {
|
||
let schema: ControlSchema
|
||
@ObservedObject var engine: BricksEngine
|
||
@State private var value: Double = 0
|
||
|
||
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)
|
||
}
|
||
|
||
HStack {
|
||
Button("−") { value -= step }
|
||
.buttonStyle(.bordered)
|
||
|
||
Text(String(format: format, value))
|
||
.frame(minWidth: 60)
|
||
.multilineTextAlignment(.center)
|
||
|
||
Button("+") { value += step }
|
||
.buttonStyle(.bordered)
|
||
}
|
||
}
|
||
.onAppear {
|
||
value = Double(schema.options.value ?? "0") ?? 0
|
||
}
|
||
.onChange(of: value) { _, newValue in
|
||
engine.store.setValue(id: schema.effectiveId, value: "\(newValue)")
|
||
}
|
||
}
|
||
|
||
private var step: Double { 1 }
|
||
private var format: String { "%g" }
|
||
}
|
||
|
||
// MARK: - Date Control
|
||
|
||
struct DateControl: View {
|
||
let schema: ControlSchema
|
||
@ObservedObject var engine: BricksEngine
|
||
@State private var date: Date = Date()
|
||
|
||
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)
|
||
}
|
||
|
||
DatePicker("", selection: $date, displayedComponents: [.date])
|
||
.labelsHidden()
|
||
}
|
||
.onChange(of: date) { _, newValue in
|
||
let formatter = DateFormatter()
|
||
formatter.dateFormat = "yyyy-MM-dd"
|
||
engine.store.setValue(id: schema.effectiveId, value: formatter.string(from: newValue))
|
||
}
|
||
}
|
||
}
|