- 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
193 lines
6.6 KiB
Swift
193 lines
6.6 KiB
Swift
import SwiftUI
|
||
|
||
// MARK: - Tabular Control (表格/数据列表)
|
||
|
||
struct TabularControl: View {
|
||
let schema: ControlSchema
|
||
@ObservedObject var engine: BricksEngine
|
||
@State private var rows: [[String: Any]] = []
|
||
@State private var selectedRow: Int? = nil
|
||
@State private var currentPage: Int = 0
|
||
@State private var isLoading: Bool = false
|
||
|
||
var body: some View {
|
||
VStack(spacing: 0) {
|
||
// 工具栏
|
||
if let tools = schema.options.toolbar?.tools, !tools.isEmpty {
|
||
toolbarView(tools: tools)
|
||
}
|
||
|
||
// 表头
|
||
headerRow
|
||
|
||
// 数据行
|
||
if isLoading {
|
||
ProgressView("加载中...")
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
} else if rows.isEmpty {
|
||
Text("暂无数据")
|
||
.foregroundColor(.secondary)
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
} else {
|
||
ScrollView {
|
||
LazyVStack(spacing: 0) {
|
||
ForEach(Array(pagedRows.enumerated()), id: \.offset) { idx, row in
|
||
dataRow(row, index: idx + currentPage * pageRows)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 分页
|
||
if totalPages > 1 {
|
||
paginationBar
|
||
}
|
||
}
|
||
.background(Color.secondary.opacity(0.05))
|
||
.cornerRadius(8)
|
||
.task {
|
||
await loadData()
|
||
}
|
||
}
|
||
|
||
// MARK: - 表头
|
||
|
||
private var headerRow: some View {
|
||
HStack(spacing: 0) {
|
||
ForEach(visibleFields, id: \.name) { field in
|
||
Text(field.label ?? field.title ?? field.name)
|
||
.font(.caption.bold())
|
||
.foregroundColor(.secondary)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.padding(.horizontal, 8)
|
||
.padding(.vertical, 8)
|
||
}
|
||
}
|
||
.background(Color.secondary.opacity(0.1))
|
||
}
|
||
|
||
// MARK: - 数据行
|
||
|
||
private func dataRow(_ row: [String: Any], index: Int) -> some View {
|
||
HStack(spacing: 0) {
|
||
ForEach(visibleFields, id: \.name) { field in
|
||
cellView(row: row, field: field)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.padding(.horizontal, 8)
|
||
.padding(.vertical, 6)
|
||
}
|
||
}
|
||
.background(selectedRow == index ? Color.accentColor.opacity(0.15) : Color.clear)
|
||
.border(Color.secondary.opacity(0.1), width: 0.5)
|
||
.onTapGesture {
|
||
selectedRow = index
|
||
engine.store.selectRow(tabularId: schema.effectiveId, index: index)
|
||
Task {
|
||
await engine.triggerEvent(widgetId: schema.effectiveId, event: "select",
|
||
data: EventData(["row": row, "index": index]))
|
||
}
|
||
}
|
||
}
|
||
|
||
private func cellView(row: [String: Any], field: FieldSchema) -> some View {
|
||
let value = row[field.name] as? String ?? "\(row[field.name] ?? "")"
|
||
|
||
// 检查alters(代码字段映射)
|
||
if let alter = schema.options.row_options?.browserfields?.alters?[field.name],
|
||
let codes = alter.codes ?? alter.data,
|
||
let match = codes.first(where: { $0.value == value }) {
|
||
return Text(engine.i18n.t(match.text))
|
||
.font(.body)
|
||
.lineLimit(1)
|
||
}
|
||
|
||
return Text(value)
|
||
.font(.body)
|
||
.lineLimit(1)
|
||
}
|
||
|
||
// MARK: - 工具栏
|
||
|
||
private func toolbarView(tools: [ToolbarTool]) -> some View {
|
||
HStack(spacing: 8) {
|
||
ForEach(tools, id: \.name) { tool in
|
||
Button(engine.i18n.t(tool.label ?? tool.name)) {
|
||
handleToolbarAction(tool)
|
||
}
|
||
.buttonStyle(.bordered)
|
||
.disabled(tool.selected_row == true && selectedRow == nil)
|
||
}
|
||
Spacer()
|
||
}
|
||
.padding(8)
|
||
.background(Color.secondary.opacity(0.05))
|
||
}
|
||
|
||
private func handleToolbarAction(_ tool: ToolbarTool) {
|
||
if tool.selected_row == true && selectedRow == nil {
|
||
engine.errorMessage = "请先选择一行"
|
||
return
|
||
}
|
||
Task {
|
||
var data = EventData()
|
||
if selectedRow != nil, let rowData = engine.store.getSelectedRowData(tabularId: schema.effectiveId) {
|
||
for (k, v) in rowData {
|
||
if let s = v as? String { data[k] = s }
|
||
}
|
||
}
|
||
await engine.triggerEvent(widgetId: schema.effectiveId, event: tool.name, data: data)
|
||
}
|
||
}
|
||
|
||
// MARK: - 分页
|
||
|
||
private var paginationBar: some View {
|
||
HStack {
|
||
Button("上一页") { currentPage -= 1 }
|
||
.disabled(currentPage <= 0)
|
||
Text("\(currentPage + 1) / \(totalPages)")
|
||
.font(.caption)
|
||
Button("下一页") { currentPage += 1 }
|
||
.disabled(currentPage >= totalPages - 1)
|
||
}
|
||
.padding(8)
|
||
}
|
||
|
||
// MARK: - 数据加载
|
||
|
||
private func loadData() async {
|
||
guard let url = schema.options.data_url else { return }
|
||
isLoading = true
|
||
let resolvedURL = engine.resolveTemplate(url, params: [:])
|
||
do {
|
||
let response = try await engine.rpc.get(resolvedURL)
|
||
if response.isSuccess, let dict = response.dictionary, let r = dict["rows"] as? [[String: Any]] {
|
||
rows = r
|
||
engine.store.setTableData(id: schema.effectiveId, rows: r)
|
||
}
|
||
} catch {
|
||
engine.errorMessage = "加载失败: \(error.localizedDescription)"
|
||
}
|
||
isLoading = false
|
||
}
|
||
|
||
// MARK: - 计算属性
|
||
|
||
private var visibleFields: [FieldSchema] {
|
||
guard let fields = schema.options.row_options?.fields else { return [] }
|
||
let excluded = schema.options.row_options?.browserfields?.exclouded ?? []
|
||
return fields.filter { !excluded.contains($0.name) }
|
||
}
|
||
|
||
private var pageRows: Int { schema.options.page_rows ?? 20 }
|
||
private var pagedRows: [[String: Any]] {
|
||
let start = currentPage * pageRows
|
||
let end = min(start + pageRows, rows.count)
|
||
guard start < rows.count else { return [] }
|
||
return Array(rows[start..<end])
|
||
}
|
||
private var totalPages: Int {
|
||
max(1, Int(ceil(Double(rows.count) / Double(pageRows))))
|
||
}
|
||
}
|