import SwiftUI // MARK: - Form Control struct FormControl: View { let schema: ControlSchema @ObservedObject var engine: BricksEngine @State private var fieldValues: [String: String] = [:] @State private var errors: [String: String] = [:] @State private var isSubmitting: Bool = false @State private var submitMessage: String = "" var body: some View { VStack(alignment: .leading, spacing: 16) { // 标题 if let title = schema.options.title { Text(engine.i18n.t(title)) .font(.title3.bold()) } // 字段 ForEach(fields, id: \.name) { field in formField(field) } // 错误信息 if !submitMessage.isEmpty { Text(submitMessage) .font(.caption) .foregroundColor(.red) } // 按钮栏 HStack { Spacer() Button("重置") { resetForm() } .buttonStyle(.bordered) Button("提交") { submitForm() } .buttonStyle(.borderedProminent) .disabled(isSubmitting) if isSubmitting { ProgressView().scaleEffect(0.7) } } } .padding(16) .onAppear { initializeValues() } } // MARK: - 表单字段渲染 @ViewBuilder private func formField(_ field: FieldSchema) -> some View { VStack(alignment: .leading, spacing: 4) { HStack(spacing: 4) { Text(field.effectiveLabel) .font(.subheadline) if field.required == true { Text("*").foregroundColor(.red) } } switch field.effectiveUitype { case "text": TextEditor(text: Binding( get: { fieldValues[field.name] ?? "" }, set: { fieldValues[field.name] = $0 } )) .frame(minHeight: 80) .border(Color.secondary.opacity(0.3)) case "code", "select": Picker("", selection: Binding( get: { fieldValues[field.name] ?? "" }, set: { fieldValues[field.name] = $0 } )) { Text("--请选择--").tag("") if let codes = field.codes ?? field.data { ForEach(codes, id: \.value) { item in Text(item.text).tag(item.value) } } } .pickerStyle(.menu) case "number": TextField("", text: Binding( get: { fieldValues[field.name] ?? "" }, set: { fieldValues[field.name] = $0 } )) .textFieldStyle(.roundedBorder) #if os(iOS) .keyboardType(.decimalPad) #endif case "date": DatePicker("", selection: Binding( get: { parseDate(fieldValues[field.name] ?? "") }, set: { fieldValues[field.name] = formatDate($0) } ), displayedComponents: [.date]) .labelsHidden() default: // str TextField(field.placeholder ?? field.effectiveLabel, text: Binding( get: { fieldValues[field.name] ?? "" }, set: { fieldValues[field.name] = $0 } )) .textFieldStyle(.roundedBorder) } if let error = errors[field.name] { Text(error) .font(.caption2) .foregroundColor(.red) } } } // MARK: - 字段列表 private var fields: [FieldSchema] { schema.options.fields ?? [] } // MARK: - 初始化 private func initializeValues() { for field in fields { fieldValues[field.name] = field.value ?? field.defaultvalue ?? "" } engine.store.setFormValues(formId: schema.effectiveId, data: fieldValues) } // MARK: - 验证 private func validate() -> Bool { errors = [:] for field in fields { let value = fieldValues[field.name] ?? "" if field.required == true && value.isEmpty { errors[field.name] = "\(field.effectiveLabel)不能为空" continue } if let rules = field.rules { for rule in rules { if !validateRule(rule, value: value) { errors[field.name] = rule.message ?? "验证失败" break } } } } return errors.isEmpty } private func validateRule(_ rule: ValidationRule, value: String) -> Bool { switch rule.type { case "required": return !value.isEmpty case "minlength": return value.count >= (Int(rule.value ?? "0") ?? 0) case "maxlength": return value.count <= (Int(rule.value ?? "999999") ?? 999999) case "min": return (Double(value) ?? -999999) >= (Double(rule.value ?? "0") ?? 0) case "max": return (Double(value) ?? 999999) <= (Double(rule.value ?? "999999") ?? 999999) case "email": return value.range(of: #"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"#, options: .regularExpression) != nil case "number": return Double(value) != nil case "pattern": return value.range(of: rule.value ?? "", options: .regularExpression) != nil default: return true } } // MARK: - 提交 private func submitForm() { guard validate() else { return } guard let url = schema.options.submit_url else { return } isSubmitting = true submitMessage = "" engine.store.setFormValues(formId: schema.effectiveId, data: fieldValues) Task { do { let response = try await engine.rpc.postForm(url, fields: fieldValues) isSubmitting = false if response.isSuccess { submitMessage = "提交成功" await engine.triggerEvent(widgetId: schema.effectiveId, event: "submit", data: EventData(fieldValues as [String: Any])) } else { submitMessage = "提交失败: HTTP \(response.statusCode)" } } catch { isSubmitting = false submitMessage = "提交失败: \(error.localizedDescription)" } } } private func resetForm() { fieldValues = [:] errors = [:] submitMessage = "" engine.store.clearForm(formId: schema.effectiveId) } // MARK: - 日期工具 private func parseDate(_ str: String) -> Date { let f = DateFormatter() f.dateFormat = "yyyy-MM-dd" return f.date(from: str) ?? Date() } private func formatDate(_ date: Date) -> String { let f = DateFormatter() f.dateFormat = "yyyy-MM-dd" return f.string(from: date) } } // MARK: - InlineForm Control struct InlineFormControl: View { let schema: ControlSchema @ObservedObject var engine: BricksEngine @State private var fieldValues: [String: String] = [:] var body: some View { HStack(spacing: 8) { ForEach(fields, id: \.name) { field in inlineField(field) } Button(submitLabel) { engine.store.setFormValues(formId: schema.effectiveId, data: fieldValues) Task { await engine.triggerEvent(widgetId: schema.effectiveId, event: "submit", data: EventData(fieldValues as [String: Any])) } } .buttonStyle(.borderedProminent) } .padding(8) .background(Color.secondary.opacity(0.05)) .cornerRadius(8) } @ViewBuilder private func inlineField(_ field: FieldSchema) -> some View { switch field.effectiveUitype { case "code", "select": Picker("", selection: Binding( get: { fieldValues[field.name] ?? "" }, set: { fieldValues[field.name] = $0 } )) { Text(field.placeholder ?? "全部").tag("") if let codes = field.codes ?? field.data { ForEach(codes, id: \.value) { item in Text(item.text).tag(item.value) } } } .pickerStyle(.menu) .fixedSize() default: TextField(field.placeholder ?? field.name, text: Binding( get: { fieldValues[field.name] ?? "" }, set: { fieldValues[field.name] = $0 } )) .textFieldStyle(.roundedBorder) .frame(width: 120) } } private var fields: [FieldSchema] { schema.options.fields ?? [] } private var submitLabel: String { let label = schema.options.submit_label ?? "提交" return (schema.options.i18n != false) ? engine.i18n.t(label) : label } }