swiftbricks/Sources/SwiftBricks/Controls/TabularControl.swift
Hermes Agent 36967370fd SwiftBricks: Bricks JSON UI framework native Swift/SwiftUI implementation
- 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
2026-06-21 12:12:03 +08:00

193 lines
6.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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))))
}
}