import Foundation import SwiftUI import Combine /// Bricks引擎 — 核心控制器 /// 负责加载JSON schema、管理状态、处理binds事件 @MainActor public final class BricksEngine: ObservableObject { /// 当前渲染的根schema @Published public var rootSchema: ControlSchema? /// 数据存储 public let store = BricksStore() /// 事件总线 public let eventBus = BricksEventBus() /// RPC网络层 public let rpc = BricksRPC() /// 国际化 public let i18n = BricksI18n() /// Widget注册表: widgettype → factory private var widgetRegistry: [String: WidgetFactory] = [:] /// Widget实例索引: id → schema @Published public var widgetIndex: [String: ControlSchema] = [:] /// 当前弹窗 @Published public var activePopup: PopupInfo? /// 导航栈(urlwidget加载的内容) @Published public var navigationStack: [String: ControlSchema] = [:] /// 加载状态 @Published public var isLoading: Bool = false /// 错误信息 @Published public var errorMessage: String? public init() { registerBuiltInWidgets() setupBuiltinEvents() } // MARK: - 加载JSON /// 从JSON字符串加载 public func loadJSON(_ jsonString: String) throws { let data = jsonString.data(using: .utf8)! let schema = try JSONDecoder().decode(ControlSchema.self, from: data) loadSchema(schema) } /// 从URL加载JSON public func loadFromURL(_ urlString: String) async throws { isLoading = true defer { isLoading = false } let response = try await rpc.get(urlString) guard response.isSuccess else { throw BricksRPCError.httpError(response.statusCode, response.text) } let schema = try JSONDecoder().decode(ControlSchema.self, from: response.data) loadSchema(schema) } /// 从schema对象加载 public func loadSchema(_ schema: ControlSchema) { rootSchema = schema indexWidgets(schema) setupBinds(schema) } // MARK: - Widget注册 /// 注册自定义widget public func registerWidget(type: String, factory: @escaping WidgetFactory) { widgetRegistry[type] = factory } /// 获取widget工厂 public func getWidgetFactory(type: String) -> WidgetFactory? { widgetRegistry[type] } // MARK: - Widget索引 /// 递归索引所有widget private func indexWidgets(_ schema: ControlSchema) { let id = schema.effectiveId widgetIndex[id] = schema if let subwidgets = schema.subwidgets { for sub in subwidgets { indexWidgets(sub) } } } /// 按ID查找widget schema public func findWidget(id: String) -> ControlSchema? { // 支持 "app.content" 点链语法 if id.contains(".") { let parts = id.split(separator: ".") if let lastPart = parts.last { return widgetIndex[String(lastPart)] } } // 支持 "-" 前缀向上查找 let cleanId = id.hasPrefix("-") ? String(id.dropFirst()) : id return widgetIndex[cleanId] } // MARK: - Binds处理 /// 递归设置所有binds private func setupBinds(_ schema: ControlSchema) { if let binds = schema.binds { for bind in binds { setupBind(bind, sourceId: schema.effectiveId) } } if let subwidgets = schema.subwidgets { for sub in subwidgets { setupBinds(sub) } } } /// 设置单个bind private func setupBind(_ bind: BindSchema, sourceId: String) { let wid = bind.wid == "self" ? sourceId : bind.wid eventBus.on("\(wid).\(bind.event)") { [weak self] data in guard let self else { return } await self.handleBind(bind, sourceId: sourceId, eventData: data) } } /// 执行bind动作 public func handleBind(_ bind: BindSchema, sourceId: String, eventData: EventData = EventData()) async { guard let actionType = ActionType(rawValue: bind.actiontype) else { print("[SwiftBricks] Invalid actiontype: \(bind.actiontype)") return } switch actionType { case .urlwidget: await handleUrlWidget(bind, sourceId: sourceId, eventData: eventData) case .urldata: await handleUrlData(bind, sourceId: sourceId) case .method: await handleMethod(bind, sourceId: sourceId, eventData: eventData) case .event: await handleEvent(bind, eventData: eventData) case .newwindow: if let url = bind.options?.url { #if os(macOS) NSWorkspace.shared.open(URL(string: url)!) #elseif os(iOS) await UIApplication.shared.open(URL(string: url)!) #endif } case .bricks: await handleBricksAction(bind) case .script: // SwiftBricks不支持内联脚本(符合Bricks无代码哲学) print("[SwiftBricks] actiontype 'script' not supported in SwiftBricks") case .iframe, .registerfunction: // 不适用于Swift环境 break } } // MARK: - 动作处理器 private func handleUrlWidget(_ bind: BindSchema, sourceId: String, eventData: EventData) async { guard let url = bind.options?.url else { return } // 构建参数 var params: [String: String] = bind.options?.params ?? [:] // 合并事件数据(如表单值、选中行数据) for (key, value) in eventData.asDictionary { if let strVal = value as? String { params[key] = strVal } else if let intVal = value as? Int { params[key] = "\(intVal)" } } // 处理 ${field}$ 变量替换 let resolvedURL = resolveTemplate(url, params: params) // 获取选中行数据(如果是toolbar按钮) if let selectedData = store.getSelectedRowData(tabularId: sourceId) { for (key, value) in selectedData { if let strVal = value as? String { params[key] = strVal } } } do { isLoading = true let response = try await rpc.get(resolvedURL, params: params) isLoading = false if response.isSuccess { if let schema = try? JSONDecoder().decode(ControlSchema.self, from: response.data) { if let target = bind.target { if target == "PopupWindow" || target == "Popup" { let opts = bind.popup_options ?? PopupOptions() activePopup = PopupInfo(schema: schema, options: opts) } else { navigationStack[target] = schema } } } } } catch { isLoading = false errorMessage = error.localizedDescription } } private func handleUrlData(_ bind: BindSchema, sourceId: String) async { guard let url = bind.options?.url else { return } let params = bind.options?.params ?? [:] let resolvedURL = resolveTemplate(url, params: params) do { let response = try await rpc.get(resolvedURL, params: params) if response.isSuccess, let target = bind.target { store.setValue(id: target, value: response.text) } } catch { errorMessage = error.localizedDescription } } private func handleMethod(_ bind: BindSchema, sourceId: String, eventData: EventData) async { guard let methodName = bind.method, let target = bind.target else { return } // 内置方法 switch methodName { case "render": // 重新加载Tabular数据 if let schema = findWidget(id: target), let url = schema.options.data_url { await reloadTabular(id: target, url: url, params: eventData.asDictionary as? [String: String] ?? [:]) } case "toggle_collapse": if store.collapsedMenus.contains(target) { store.collapsedMenus.remove(target) } else { store.collapsedMenus.insert(target) } case "setValue": if let value = eventData["value"] as? String { store.setValue(id: target, value: value) } case "reset": store.clearForm(formId: target) default: await eventBus.dispatch("\(target).\(methodName)", data: eventData) } } private func handleEvent(_ bind: BindSchema, eventData: EventData) async { if let target = bind.target { await eventBus.dispatch(target, data: eventData) } } private func handleBricksAction(_ bind: BindSchema) async { // bricks actiontype: 从配置实例化widget(暂不实现远程加载) print("[SwiftBricks] actiontype 'bricks' - remote widget instantiation not yet supported") } // MARK: - Tabular数据加载 public func reloadTabular(id: String, url: String, params: [String: String] = [:]) async { let resolvedURL = resolveTemplate(url, params: params) do { let response = try await rpc.get(resolvedURL, params: params) if response.isSuccess, let dict = response.dictionary, let rows = dict["rows"] as? [[String: Any]] { store.setTableData(id: id, rows: rows) } } catch { errorMessage = "加载数据失败: \(error.localizedDescription)" } } // MARK: - 模板变量替换 public func resolveTemplate(_ template: String, params: [String: String]) -> String { var result = template for (key, value) in params { result = result.replacingOccurrences(of: "${\(key)}$", with: value) } return result } // MARK: - 触发事件 /// 触发widget事件 public func triggerEvent(widgetId: String, event: String, data: EventData = EventData()) async { await eventBus.dispatch("\(widgetId).\(event)", data: data) } // MARK: - 表单提交 public func submitForm(formId: String, submitURL: String) async -> Bool { let values = store.getFormValues(formId: formId) // 验证 if let schema = findWidget(id: formId), let fields = schema.options.fields { for field in fields { if let rules = field.rules { let value = values[field.name] ?? "" for rule in rules { if !validateRule(rule, value: value) { errorMessage = rule.message ?? "验证失败: \(field.effectiveLabel)" return false } } } if field.required == true { let value = values[field.name] ?? "" if value.isEmpty { errorMessage = "\(field.effectiveLabel)不能为空" return false } } } } // 提交 do { let response = try await rpc.postForm(submitURL, fields: values) if response.isSuccess { return true } else { errorMessage = "提交失败: HTTP \(response.statusCode)" return false } } catch { errorMessage = error.localizedDescription return false } } // MARK: - 表单验证 private func validateRule(_ rule: ValidationRule, value: String) -> Bool { switch rule.type { case "required": return !value.isEmpty case "minlength": guard let minLen = Int(rule.value ?? "0") else { return true } return value.count >= minLen case "maxlength": guard let maxLen = Int(rule.value ?? "0") else { return true } return value.count <= maxLen case "min": guard let minVal = Double(rule.value ?? "0"), let val = Double(value) else { return true } return val >= minVal case "max": guard let maxVal = Double(rule.value ?? "0"), let val = Double(value) else { return true } return val <= maxVal case "email": let emailRegex = #"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"# return value.range(of: emailRegex, options: .regularExpression) != nil case "number": return Double(value) != nil case "pattern": guard let pattern = rule.value else { return true } return value.range(of: pattern, options: .regularExpression) != nil default: return true } } // MARK: - 内置事件 private func setupBuiltinEvents() { // 可在此注册全局事件 } // MARK: - 内置Widget注册 private func registerBuiltInWidgets() { // 所有widget类型都通过ControlRenderer处理 // 这里可以注册自定义widget工厂 } } /// Widget工厂函数类型 public typealias WidgetFactory = (ControlSchema, BricksEngine) -> AnyView /// 弹窗信息 public struct PopupInfo: Identifiable { public let id = UUID() public let schema: ControlSchema public let options: PopupOptions }