311 lines
10 KiB
Swift
311 lines
10 KiB
Swift
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<String> = []
|
|
@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
|
|
}
|
|
}
|