import SwiftUI struct SettingsView: View { @StateObject private var model: SettingsViewModel @ObservedObject private var themeStore = ThemeStore.shared @State private var showsClearDialog = false init(model: SettingsViewModel? = nil) { _model = StateObject(wrappedValue: model ?? DefaultContainer.makeSettingsModel()) } var body: some View { ScrollView { LazyVStack(spacing: 12) { if let errorMessage = model.errorMessage { AIStatusBanner( errorMessage, tint: AITheme.danger, systemImage: "exclamationmark.triangle") } if let notice = model.notice { AIStatusBanner(notice, tint: AITheme.success, systemImage: "checkmark.circle") } themeCard runtimeCard storageCard securityCard dangerZoneCard } .padding(16) .padding(.bottom, 8) } .background(AITheme.canvas.ignoresSafeArea()) .navigationTitle("设置") .toolbar { ToolbarItemGroup(placement: .primaryAction) { AIIconButton("刷新", systemImage: "arrow.clockwise") { Task { await model.refresh() } } .disabled(model.isLoading) } } .refreshable { await model.refresh() } .confirmationDialog( "确认清除运行日志?此操作不可恢复。", isPresented: $showsClearDialog, titleVisibility: .visible ) { Button("清除运行日志", role: .destructive) { Task { await model.clearRuntimeLogs() } } Button("取消", role: .cancel) {} } .task { await model.refresh() } } private var runtimeCard: some View { AICard { VStack(alignment: .leading, spacing: 12) { AISectionTitle("应用信息", systemImage: "info.circle") if let runtime = model.runtime { AIKeyValueRow("版本", value: "\(runtime.marketingVersion) (\(runtime.buildVersion))") AIKeyValueRow("系统", value: runtime.systemVersion) } else { Text("正在读取应用信息…") .font(.subheadline) .foregroundStyle(AITheme.muted) } } } } private var themeCard: some View { AICard { VStack(alignment: .leading, spacing: 12) { // 三个选项块本身已经标出当前模式,标题右侧再重复一次是冗余信息。 AISectionTitle("主题外观", systemImage: "circle.lefthalf.filled") Grid(alignment: .leading, horizontalSpacing: 8, verticalSpacing: 8) { GridRow { ForEach(ThemeMode.allCases) { mode in themeOption(mode) } } } } } } private func themeOption(_ mode: ThemeMode) -> some View { let isSelected = themeStore.mode == mode return Button { themeStore.set(mode) } label: { VStack(spacing: 7) { Image(systemName: mode.systemImage) .font(.system(size: 18, weight: .semibold)) .foregroundStyle(isSelected ? AITheme.primary : AITheme.muted) .accessibilityHidden(true) Text(mode.label) .font(.caption.weight(isSelected ? .semibold : .medium)) .foregroundStyle(isSelected ? AITheme.ink : AITheme.muted) .lineLimit(1) .minimumScaleFactor(0.8) } .frame(maxWidth: .infinity, minHeight: 68) .background(isSelected ? AITheme.primary.opacity(0.10) : AITheme.control) .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) .overlay { RoundedRectangle(cornerRadius: 14, style: .continuous) .strokeBorder(isSelected ? AITheme.primary.opacity(0.42) : AITheme.line) } .contentShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) } .buttonStyle(.plain) .accessibilityLabel("主题:\(mode.label)") .accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : .isButton) } private var storageCard: some View { AICard { NavigationLink { LogsView() } label: { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 10) { AISectionTitle("本地数据", systemImage: "externaldrive") .frame(maxWidth: .infinity, alignment: .leading) Image(systemName: "chevron.right") .font(.footnote.weight(.semibold)) .foregroundStyle(AITheme.muted) .accessibilityHidden(true) } AIKeyValueRow("日志记录", value: model.storageLabel) } } .buttonStyle(.plain) .accessibilityHint("进入日志与诊断") } } private var securityCard: some View { AICard { VStack(alignment: .leading, spacing: 12) { AISectionTitle("隐私与数据", systemImage: "lock.shield") AIKeyValueRow("节点与凭据", value: "仅写入本机存储") AIKeyValueRow("网络请求", value: "只访问你添加的节点") // 卡内再套一个描边框会变成"卡中卡",这里退成一段脚注。 HStack(alignment: .top, spacing: 7) { Image(systemName: "checkmark.seal") .font(.system(size: 12, weight: .semibold)) .foregroundStyle(AITheme.success) .padding(.top, 2) .accessibilityHidden(true) Text("不经过中转服务器,也不上传遥测数据;节点地址、密码与日志只保存在这台 iPhone 上,删除应用即彻底清除。") .font(.caption) .foregroundStyle(AITheme.muted) .fixedSize(horizontal: false, vertical: true) } .padding(.top, 2) } } } private var dangerZoneCard: some View { AICard { VStack(alignment: .leading, spacing: 12) { AISectionTitle("数据清理", systemImage: "trash.square", detail: "不可恢复") // 整块实心红是这页最大的色块,会把"读设置"的视线抢走;退成一行危险色入口。 Button { showsClearDialog = true } label: { HStack(spacing: 8) { Image(systemName: "trash") .font(.system(size: 14, weight: .semibold)) .accessibilityHidden(true) Text("清除运行日志") .font(.subheadline.weight(.semibold)) Spacer(minLength: 8) Image(systemName: "chevron.right") .font(.footnote.weight(.semibold)) .opacity(0.7) .accessibilityHidden(true) } .foregroundStyle(AITheme.danger) .padding(.horizontal, 12) .frame(minHeight: AITheme.compactControlHeight) .frame(maxWidth: .infinity, alignment: .leading) .background(AITheme.danger.opacity(0.07)) .clipShape(RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous)) .overlay { RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous) .strokeBorder(AITheme.danger.opacity(0.20)) } .contentShape(RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous)) } .buttonStyle(.plain) } } } }