- 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
159 lines
4.8 KiB
Swift
159 lines
4.8 KiB
Swift
import SwiftUI
|
|
|
|
/// BricksView — SwiftBricks的主入口视图
|
|
/// 从JSON schema渲染完整的UI页面
|
|
///
|
|
/// 用法:
|
|
/// ```swift
|
|
/// BricksView(json: jsonString, engine: engine)
|
|
/// BricksView(url: "/api/page.dspy", engine: engine)
|
|
/// ```
|
|
public struct BricksView: View {
|
|
@ObservedObject var engine: BricksEngine
|
|
@State private var isLoaded: Bool = false
|
|
@State private var loadError: String?
|
|
|
|
private var jsonString: String?
|
|
private var loadURL: String?
|
|
|
|
/// 从JSON字符串创建
|
|
public init(json: String, engine: BricksEngine) {
|
|
self.jsonString = json
|
|
self.loadURL = nil
|
|
self._engine = ObservedObject(wrappedValue: engine)
|
|
}
|
|
|
|
/// 从URL加载
|
|
public init(url: String, engine: BricksEngine) {
|
|
self.jsonString = nil
|
|
self.loadURL = url
|
|
self._engine = ObservedObject(wrappedValue: engine)
|
|
}
|
|
|
|
/// 从已加载的schema创建
|
|
public init(schema: ControlSchema, engine: BricksEngine) {
|
|
self.jsonString = nil
|
|
self.loadURL = nil
|
|
self._engine = ObservedObject(wrappedValue: engine)
|
|
// 立即加载
|
|
engine.loadSchema(schema)
|
|
}
|
|
|
|
public var body: some View {
|
|
ZStack {
|
|
if let schema = engine.rootSchema {
|
|
// 主内容
|
|
ControlRenderer(schema: schema, engine: engine)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
|
// 弹窗层
|
|
if let popup = engine.activePopup {
|
|
popupOverlay(popup)
|
|
}
|
|
|
|
// 加载指示器
|
|
if engine.isLoading {
|
|
Color.black.opacity(0.01)
|
|
.overlay(ProgressView())
|
|
}
|
|
} else if let error = loadError {
|
|
errorView(error)
|
|
} else {
|
|
ProgressView("加载中...")
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|
|
.task {
|
|
await performLoad()
|
|
}
|
|
.alert("错误", isPresented: .init(
|
|
get: { engine.errorMessage != nil },
|
|
set: { if !$0 { engine.errorMessage = nil } }
|
|
)) {
|
|
Button("确定") { engine.errorMessage = nil }
|
|
} message: {
|
|
Text(engine.errorMessage ?? "")
|
|
}
|
|
}
|
|
|
|
// MARK: - 加载
|
|
|
|
private func performLoad() async {
|
|
if let json = jsonString {
|
|
do {
|
|
try engine.loadJSON(json)
|
|
isLoaded = true
|
|
} catch {
|
|
loadError = "JSON解析失败: \(error.localizedDescription)"
|
|
}
|
|
} else if let url = loadURL {
|
|
do {
|
|
try await engine.loadFromURL(url)
|
|
isLoaded = true
|
|
} catch {
|
|
loadError = error.localizedDescription
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 弹窗层
|
|
|
|
private func popupOverlay(_ popup: PopupInfo) -> some View {
|
|
ZStack {
|
|
// 遮罩
|
|
Color.black.opacity(0.3)
|
|
.ignoresSafeArea()
|
|
.onTapGesture {
|
|
engine.activePopup = nil
|
|
}
|
|
|
|
// 弹窗内容
|
|
PopupWindowControl(schema: popup.schema, engine: engine)
|
|
.frame(
|
|
maxWidth: popupWidth(popup.options),
|
|
maxHeight: popupHeight(popup.options)
|
|
)
|
|
.padding(20)
|
|
}
|
|
}
|
|
|
|
private func popupWidth(_ opts: PopupOptions) -> CGFloat? {
|
|
if let cw = opts.cwidth { return CGFloat(cw * 8) }
|
|
if let w = opts.width, let px = Double(w.replacingOccurrences(of: "px", with: "")) {
|
|
return CGFloat(px)
|
|
}
|
|
return 400
|
|
}
|
|
|
|
private func popupHeight(_ opts: PopupOptions) -> CGFloat? {
|
|
if let ch = opts.cheight { return CGFloat(ch * 16) }
|
|
if let h = opts.height {
|
|
if h.hasSuffix("%") { return nil } // 百分比用maxHeight
|
|
if let px = Double(h.replacingOccurrences(of: "px", with: "")) {
|
|
return CGFloat(px)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MARK: - 错误视图
|
|
|
|
private func errorView(_ error: String) -> some View {
|
|
VStack(spacing: 12) {
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
.font(.largeTitle)
|
|
.foregroundColor(.orange)
|
|
Text("加载失败")
|
|
.font(.headline)
|
|
Text(error)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
Button("重试") {
|
|
Task { await performLoad() }
|
|
}
|
|
}
|
|
.padding(40)
|
|
}
|
|
}
|