- 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
161 lines
5.1 KiB
Swift
161 lines
5.1 KiB
Swift
import SwiftUI
|
||
|
||
/// 控件渲染器 — 递归将JSON schema渲染为SwiftUI视图
|
||
/// 这是SwiftBricks的核心,对应Bricks的widget实例化链
|
||
public struct ControlRenderer: View {
|
||
let schema: ControlSchema
|
||
@ObservedObject var engine: BricksEngine
|
||
|
||
public init(schema: ControlSchema, engine: BricksEngine) {
|
||
self.schema = schema
|
||
self.engine = engine
|
||
}
|
||
|
||
public var body: some View {
|
||
renderWidget(schema)
|
||
.frame(
|
||
maxWidth: resolveWidth(schema.options),
|
||
maxHeight: resolveHeight(schema.options),
|
||
alignment: resolveAlignment(schema.options)
|
||
)
|
||
}
|
||
|
||
// MARK: - Widget分发
|
||
|
||
@ViewBuilder
|
||
func renderWidget(_ schema: ControlSchema) -> some View {
|
||
let type = schema.widgettype
|
||
|
||
switch type {
|
||
// 文本类
|
||
case "Text":
|
||
TextControl(schema: schema, engine: engine)
|
||
case "Title1":
|
||
TitleControl(schema: schema, engine: engine, level: 1)
|
||
case "Title2":
|
||
TitleControl(schema: schema, engine: engine, level: 2)
|
||
case "Title3":
|
||
TitleControl(schema: schema, engine: engine, level: 3)
|
||
case "Title4":
|
||
TitleControl(schema: schema, engine: engine, level: 4)
|
||
case "Title5":
|
||
TitleControl(schema: schema, engine: engine, level: 5)
|
||
case "Title6":
|
||
TitleControl(schema: schema, engine: engine, level: 6)
|
||
case "Label":
|
||
LabelControl(schema: schema, engine: engine)
|
||
|
||
// 输入类
|
||
case "Input", "UiStr":
|
||
InputControl(schema: schema, engine: engine)
|
||
case "Textarea", "UiText":
|
||
TextareaControl(schema: schema, engine: engine)
|
||
case "UiNumber":
|
||
NumberControl(schema: schema, engine: engine)
|
||
case "UiDate":
|
||
DateControl(schema: schema, engine: engine)
|
||
case "Select", "UiCode":
|
||
SelectControl(schema: schema, engine: engine)
|
||
|
||
// 按钮
|
||
case "Button":
|
||
ButtonControl(schema: schema, engine: engine)
|
||
|
||
// 布局
|
||
case "VBox", "Card":
|
||
VBoxControl(schema: schema, engine: engine)
|
||
case "HBox":
|
||
HBoxControl(schema: schema, engine: engine)
|
||
case "VScrollPanel", "HScrollPanel":
|
||
ScrollPanelControl(schema: schema, engine: engine)
|
||
case "Filler":
|
||
Spacer().frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
case "DynamicColumn":
|
||
DynamicColumnControl(schema: schema, engine: engine)
|
||
|
||
// 数据展示
|
||
case "Tabular", "DataViewer":
|
||
TabularControl(schema: schema, engine: engine)
|
||
case "Form":
|
||
FormControl(schema: schema, engine: engine)
|
||
case "InlineForm":
|
||
InlineFormControl(schema: schema, engine: engine)
|
||
|
||
// 导航
|
||
case "TabView":
|
||
TabViewControl(schema: schema, engine: engine)
|
||
case "Menu":
|
||
MenuControl(schema: schema, engine: engine)
|
||
|
||
// 弹窗
|
||
case "PopupWindow":
|
||
PopupWindowControl(schema: schema, engine: engine)
|
||
|
||
// HTML
|
||
case "Html":
|
||
HtmlControl(schema: schema, engine: engine)
|
||
|
||
// Image
|
||
case "Image":
|
||
ImageControl(schema: schema, engine: engine)
|
||
|
||
// urlwidget(直接加载远程内容)
|
||
case "urlwidget":
|
||
UrlWidgetControl(schema: schema, engine: engine)
|
||
|
||
default:
|
||
// 未知widgettype — 渲染占位符
|
||
VStack {
|
||
Text("⚠️ Unknown: \(type)")
|
||
.font(.caption)
|
||
.foregroundColor(.secondary)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 尺寸解析
|
||
|
||
private func resolveWidth(_ opts: ControlOptions) -> CGFloat? {
|
||
if let w = opts.width {
|
||
if w == "100%" { return nil } // 使用frame的maxWidth
|
||
if let px = parsePixels(w) { return px }
|
||
}
|
||
return nil
|
||
}
|
||
|
||
private func resolveHeight(_ opts: ControlOptions) -> CGFloat? {
|
||
if let h = opts.height {
|
||
if h == "100%" { return nil }
|
||
if let px = parsePixels(h) { return px }
|
||
}
|
||
return nil
|
||
}
|
||
|
||
private func resolveAlignment(_ opts: ControlOptions) -> Alignment {
|
||
switch opts.alignItems {
|
||
case "center": return .center
|
||
case "flex-end", "end": return .trailing
|
||
case "flex-start", "start": return .leading
|
||
default: return .topLeading
|
||
}
|
||
}
|
||
|
||
private func parsePixels(_ str: String) -> CGFloat? {
|
||
let cleaned = str.replacingOccurrences(of: "px", with: "")
|
||
return CGFloat(Double(cleaned) ?? 0)
|
||
}
|
||
}
|
||
|
||
// MARK: - 渲染子控件列表
|
||
|
||
struct SubWidgetsView: View {
|
||
let subwidgets: [ControlSchema]
|
||
@ObservedObject var engine: BricksEngine
|
||
|
||
var body: some View {
|
||
ForEach(Array(subwidgets.enumerated()), id: \.offset) { _, sub in
|
||
ControlRenderer(schema: sub, engine: engine)
|
||
}
|
||
}
|
||
}
|