import SwiftUI // MARK: - TabView Control struct TabViewControl: View { let schema: ControlSchema @ObservedObject var engine: BricksEngine @State private var selectedIndex: Int = 0 var body: some View { VStack(spacing: 0) { // Tab按钮栏 HStack(spacing: 0) { ForEach(Array(tabs.enumerated()), id: \.offset) { idx, tab in Button(action: { selectedIndex = idx }) { HStack(spacing: 4) { if let icon = tab.icon { Text(icon) } Text(engine.i18n.t(tab.label)) .font(.subheadline) } .padding(.horizontal, 16) .padding(.vertical, 10) .foregroundColor(selectedIndex == idx ? .accentColor : .secondary) .background(selectedIndex == idx ? Color.accentColor.opacity(0.1) : Color.clear) } .buttonStyle(.plain) .overlay(alignment: .bottom) { Rectangle() .fill(selectedIndex == idx ? Color.accentColor : Color.clear) .frame(height: 2) } } Spacer() } .background(Color.secondary.opacity(0.05)) // 内容区 if selectedIndex < tabs.count { let tab = tabs[selectedIndex] if let content = tab.content { ControlRenderer(schema: content, engine: engine) .frame(maxWidth: .infinity, maxHeight: .infinity) } } } .onAppear { selectedIndex = schema.options.activeTab ?? 0 } } private var tabs: [TabItem] { schema.options.tabs ?? [] } } // MARK: - Menu Control (侧边栏菜单) struct MenuControl: View { let schema: ControlSchema @ObservedObject var engine: BricksEngine @State private var expandedItems: Set = [] @State private var selectedItem: String = "" var body: some View { let isCollapsed = engine.store.collapsedMenus.contains(schema.effectiveId) ScrollView { VStack(alignment: .leading, spacing: 2) { if let items = schema.options.items { ForEach(items, id: \.name) { item in menuItemView(item, depth: 0) } } } .padding(4) } .frame(width: isCollapsed ? 50 : nil) .background(Color.secondary.opacity(0.05)) .animation(.easeInOut(duration: 0.2), value: isCollapsed) } @ViewBuilder private func menuItemView(_ item: MenuItemSchema, depth: Int) -> AnyView { let hasChildren = !(item.submenu?.isEmpty ?? true) let isExpanded = expandedItems.contains(item.name) let isSelected = selectedItem == item.name return AnyView( VStack(alignment: .leading, spacing: 0) { Button(action: { handleMenuClick(item, hasChildren: hasChildren) }) { HStack(spacing: 8) { if let icon = item.icon { Text(icon) .frame(width: 20) } Text(engine.i18n.t(item.label)) .font(.subheadline) .lineLimit(1) Spacer() if hasChildren { Image(systemName: isExpanded ? "chevron.down" : "chevron.right") .font(.caption) .foregroundColor(.secondary) } } .padding(.horizontal, 12 + CGFloat(depth * 16)) .padding(.vertical, 8) .foregroundColor(isSelected ? .accentColor : .primary) .background(isSelected ? Color.accentColor.opacity(0.1) : Color.clear) .cornerRadius(6) } .buttonStyle(.plain) // 子菜单 if hasChildren && isExpanded { if let subitems = item.submenu { ForEach(subitems, id: \.name) { sub in menuItemView(sub, depth: depth + 1) } } } } ) } private func handleMenuClick(_ item: MenuItemSchema, hasChildren: Bool) { if hasChildren { if expandedItems.contains(item.name) { expandedItems.remove(item.name) } else { expandedItems.insert(item.name) } } else { selectedItem = item.name // 触发binds Task { if let binds = item.binds { for bind in binds { if bind.event == "click" { await engine.handleBind(bind, sourceId: schema.effectiveId) } } } } } } } // MARK: - PopupWindow Control struct PopupWindowControl: View { let schema: ControlSchema @ObservedObject var engine: BricksEngine var body: some View { VStack(alignment: .leading, spacing: 0) { // 标题栏 HStack { Text(engine.i18n.t(schema.options.title ?? "")) .font(.headline) Spacer() Button(action: close) { Image(systemName: "xmark") .font(.caption.bold()) .foregroundColor(.secondary) } .buttonStyle(.plain) } .padding(12) .background(Color.secondary.opacity(0.1)) // 内容 if let subwidgets = schema.subwidgets { VStack(alignment: .leading, spacing: 8) { SubWidgetsView(subwidgets: subwidgets, engine: engine) } .padding(12) } } #if os(iOS) .background(Color(.systemBackground)) #else .background(Color(nsColor: .windowBackgroundColor)) #endif .cornerRadius(12) .shadow(radius: 8) } private func close() { engine.activePopup = nil } } // MARK: - HTML Control struct HtmlControl: View { let schema: ControlSchema @ObservedObject var engine: BricksEngine var body: some View { let html = schema.options.html ?? "" #if os(iOS) Text(html) // iOS不支持AttributedString from HTML直接渲染 .font(.body) #else if let data = html.data(using: .utf8), let attrStr = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) { Text(AttributedString(attrStr)) } else { Text(html) } #endif } } // MARK: - Image Control struct ImageControl: View { let schema: ControlSchema @ObservedObject var engine: BricksEngine @State private var image: Image? = nil @State private var isLoading: Bool = false var body: some View { Group { if let image { image.resizable().aspectRatio(contentMode: .fit) } else if isLoading { ProgressView() } else { Rectangle() .fill(Color.secondary.opacity(0.1)) .overlay(Text(schema.options.alt ?? "🖼️")) } } .task { await loadImage() } } private func loadImage() async { guard let src = schema.options.src, let url = URL(string: src) else { return } isLoading = true do { let (data, _) = try await URLSession.shared.data(from: url) #if os(iOS) if let uiImage = UIImage(data: data) { image = Image(uiImage: uiImage) } #else if let nsImage = NSImage(data: data) { image = Image(nsImage: nsImage) } #endif } catch { print("[SwiftBricks] Image load failed: \(error)") } isLoading = false } } // MARK: - UrlWidget Control (加载远程widget内容) struct UrlWidgetControl: View { let schema: ControlSchema @ObservedObject var engine: BricksEngine @State private var loadedSchema: ControlSchema? = nil @State private var isLoading: Bool = true @State private var errorMessage: String? = nil var body: some View { Group { if isLoading { ProgressView("加载中...") .frame(maxWidth: .infinity, maxHeight: .infinity) } else if let error = errorMessage { VStack { Image(systemName: "exclamationmark.triangle") .foregroundColor(.orange) Text(error).font(.caption).foregroundColor(.secondary) } } else if let schema = loadedSchema { ControlRenderer(schema: schema, engine: engine) } } .task { await loadContent() } } private func loadContent() async { guard let url = schema.options.url else { errorMessage = "缺少url" isLoading = false return } do { let response = try await engine.rpc.get(url) if response.isSuccess { loadedSchema = try JSONDecoder().decode(ControlSchema.self, from: response.data) } else { errorMessage = "HTTP \(response.statusCode)" } } catch { errorMessage = error.localizedDescription } isLoading = false } }