From 2133d96e186b5b74d7c76c42419f9a7952c2f015 Mon Sep 17 00:00:00 2001 From: zhangkunshi Date: Tue, 21 Apr 2026 10:36:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(s7.1.1):=20SettingsView=204=20tab=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=20+=20ContentView=20=E9=A1=B6=E6=A0=8F?= =?UTF-8?q?=E9=AA=A8=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sprint 7.1 第一轮 · 两个相对独立的文件改造,需 Xcode 编译验证。 ## SettingsView.swift 重构(249 → 452 行) 从 3 tab(外观/数据/认证)重构为 4 tab(按 Sprint 6 UI 设计稿): - 通用 — 合并原"外观"+"数据"+新增"运行环境" section - 供应商 — 原"认证"改名,内容保留(OAuth + Provider list + 添加按钮) - 卡片化展示:每个供应商一张卡,有图标/名字/地址/状态徽章/操作按钮 - 当前激活的加绿色 "✓ 当前使用" badge + 粗边框 - 快捷键 — 新增,只读展示三组:会话/导航/输入(编辑功能 S8 做) - 实验性 ⚗️ — 新增,4 个 feature flag 用 @AppStorage 持久化: - 命令面板模糊搜索(默认开) - AI 长上下文(默认关) - 多模态输入(默认关) - 调试面板(默认关) 左侧导航条(180px)替代原顶部 capsule 风格,更符合 macOS HIG。 ## ContentView.swift 重构(59 → 196 行) 保留 NavigationSplitView + HStack 三栏结构,toolbar 重排: - 左侧(navigation placement): - 项目切换器 · "🚀 默认项目 [A · 绿地 ▾]" · 点击弹占位提示 - 场景导航 · 💼业务/💻开发/🧪测试/🚀运维 · 点击弹占位提示 - 右侧(automatic placement): - 产出物面板开关(保留) - 收藏过滤(保留) - 刷新(保留) - 用户头像菜单 · Menu 组件 · 6 个 item(资料/角色/统计/设置/退出) "敬请期待" alert 统一封装,message 说明哪个 Sprint 实现。 ## 未做(留后续) - SidebarView 右键菜单 · S7.1.2 - ChatView 附件 chip + 工具条 · S7.1.3 - SetupWizardView 加步骤 3-4 · S7.1.3 - ProjectStore / LoginView · S7.3 - 场景工作流 WorkflowView · S7.4 ## 验证 需要 Xcode 编译。如有错误,告诉我具体哪一行,我修。 --- app/ClaudeCodeHistory/ContentView.swift | 161 ++++++- app/ClaudeCodeHistory/SettingsView.swift | 535 ++++++++++++++++------- 2 files changed, 518 insertions(+), 178 deletions(-) diff --git a/app/ClaudeCodeHistory/ContentView.swift b/app/ClaudeCodeHistory/ContentView.swift index 09357aa..cca9aa9 100644 --- a/app/ClaudeCodeHistory/ContentView.swift +++ b/app/ClaudeCodeHistory/ContentView.swift @@ -1,5 +1,14 @@ import SwiftUI +/// 主视图三栏布局(S7.1.1 改造) +/// +/// 结构: +/// - NavigationSplitView · 原生左侧栏(会话列表) +/// - detail: 中栏(ChatView)+ 右栏(ArtifactPanel,有产出物时) +/// - toolbar 顶栏: 项目切换器 · 场景导航(业务/开发/测试/运维)· 用户头像 +/// +/// 说明:项目切换器 / 场景导航 当前为 placeholder(点击显示"敬请期待"), +/// 真实 ProjectStore 在 S7.3 实现,场景工作流在 S7.4 实现。 struct ContentView: View { @EnvironmentObject var store: ConversationStore @EnvironmentObject var favorites: FavoritesManager @@ -7,23 +16,19 @@ struct ContentView: View { @EnvironmentObject var settings: AppSettings @StateObject private var artifactMgr = ArtifactManager.shared + @State private var showComingSoon = false + @State private var comingSoonMessage = "" + var body: some View { NavigationSplitView { SidebarView() .navigationSplitViewColumnWidth(min: 260, ideal: 290, max: 340) - .toolbar { - ToolbarItem(placement: .automatic) { Spacer() } - } } detail: { HStack(spacing: 0) { - // 对话区域 - ChatView() - .frame(maxWidth: .infinity) + ChatView().frame(maxWidth: .infinity) - // Artifact 面板(右侧分屏) if artifactMgr.isPanelVisible { Divider() - ArtifactPanel(manager: artifactMgr) .frame(minWidth: 320, idealWidth: 420, maxWidth: 600) .transition(.move(edge: .trailing).combined(with: .opacity)) @@ -31,29 +36,161 @@ struct ContentView: View { } .animation(.spring(response: 0.3), value: artifactMgr.isPanelVisible) .toolbar { + // 左侧:项目切换器 + 场景导航 + ToolbarItemGroup(placement: .navigation) { + projectSwitcher + scenarioNav + } + + // 右侧:Artifact / 收藏 / 刷新 / 用户菜单 ToolbarItemGroup(placement: .automatic) { Spacer() - // Artifact 面板切换 Button(action: { withAnimation { artifactMgr.isPanelVisible.toggle() } }) { Image(systemName: "rectangle.righthalf.inset.filled") .foregroundColor(artifactMgr.isPanelVisible ? settings.tc : .secondary) } - .help("显示/隐藏 Artifact 面板") + .help("显示/隐藏 产出物面板") .disabled(artifactMgr.artifacts.isEmpty) Button(action: { store.showFavoritesOnly.toggle() }) { Image(systemName: store.showFavoritesOnly ? "star.fill" : "star") .foregroundColor(store.showFavoritesOnly ? .yellow : .secondary) - } + }.help("收藏过滤 ⌘⇧F") + Button(action: { store.reload() }) { Image(systemName: "arrow.clockwise") - } + }.help("刷新 ⌘R") + + userChip } } } .environmentObject(artifactMgr) + .alert("敬请期待", isPresented: $showComingSoon) { + Button("好的", role: .cancel) {} + } message: { + Text(comingSoonMessage) + } + } + + // MARK: - 顶栏组件 + + /// 项目切换器(占位 · 真实 ProjectStore 在 S7.3 实现) + private var projectSwitcher: some View { + Button(action: { + comingSoonMessage = "项目列表 / 切换在 Sprint 7.3 实现。届时这里可切换多项目,每个项目带接入模式徽章。" + showComingSoon = true + }) { + HStack(spacing: 6) { + Text("🚀").font(.system(size: 12)) + Text("默认项目") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(.primary) + modeBadge(letter: "A", name: "绿地", tint: .green) + Image(systemName: "chevron.down") + .font(.system(size: 9, weight: .bold)) + .foregroundColor(.secondary) + } + .padding(.horizontal, 8).padding(.vertical, 4) + .background(RoundedRectangle(cornerRadius: 6).fill(Color.secondary.opacity(0.06))) + .overlay( + RoundedRectangle(cornerRadius: 6) + .strokeBorder(Color.secondary.opacity(0.12), lineWidth: 0.5) + ) + }.buttonStyle(.plain) + } + + /// 场景导航 · 业务 / 开发 / 测试 / 运维 + private var scenarioNav: some View { + HStack(spacing: 12) { + scenarioLink(icon: "💼", name: "业务", message: "业务场景工作流在 Sprint 7.4 实现") + scenarioLink(icon: "💻", name: "开发", message: "开发场景工作流在 Sprint 7.4 实现") + scenarioLink(icon: "🧪", name: "测试", message: "测试场景工作流在 Sprint 7.4 实现") + scenarioLink(icon: "🚀", name: "运维", message: "运维场景工作流在 Sprint 7.4 实现") + } + .padding(.leading, 8) + } + + private func scenarioLink(icon: String, name: String, message: String) -> some View { + Button(action: { + comingSoonMessage = message + showComingSoon = true + }) { + HStack(spacing: 4) { + Text(icon).font(.system(size: 12)) + Text(name).font(.system(size: 12, weight: .medium)) + } + .foregroundColor(.secondary) + }.buttonStyle(.plain) + } + + /// 模式徽章(A/B/C/D,4 种接入模式) + private func modeBadge(letter: String, name: String, tint: Color) -> some View { + Text("\(letter) · \(name)") + .font(.system(size: 9, weight: .bold)) + .foregroundColor(tint) + .padding(.horizontal, 5).padding(.vertical, 1) + .background(Capsule().fill(tint.opacity(0.12))) + .overlay(Capsule().strokeBorder(tint.opacity(0.4), lineWidth: 0.5)) + } + + /// 用户头像 chip · 点击弹菜单 + private var userChip: some View { + Menu { + Section("张工") { + Text("产品 · 基础架构组") + Text("zhang.san@company.com") + } + Divider() + Button(action: { + comingSoonMessage = "用户资料编辑在 Sprint 8 实现" + showComingSoon = true + }) { + Label("我的资料", systemImage: "person.crop.circle") + } + Button(action: { + comingSoonMessage = "切换角色 ⌘⇧R 在 Sprint 7.4 实现" + showComingSoon = true + }) { + Label("切换角色 ⌘⇧R", systemImage: "person.2") + } + Button(action: { + comingSoonMessage = "使用统计在 Sprint 8 实现" + showComingSoon = true + }) { + Label("我的使用统计", systemImage: "chart.bar") + } + Divider() + Button(action: { + NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) + }) { + Label("设置 ⌘,", systemImage: "gearshape") + } + Divider() + Button(role: .destructive, action: { + comingSoonMessage = "退出登录在 Sprint 7.3 实现(LoginView 就位后)" + showComingSoon = true + }) { + Label("退出登录", systemImage: "rectangle.portrait.and.arrow.right") + } + } label: { + HStack(spacing: 4) { + Circle() + .fill(settings.tc) + .frame(width: 22, height: 22) + .overlay( + Text("张") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.white) + ) + Text("张工").font(.system(size: 11, weight: .medium)) + } + } + .menuStyle(.borderlessButton) + .menuIndicator(.hidden) + .fixedSize() } } diff --git a/app/ClaudeCodeHistory/SettingsView.swift b/app/ClaudeCodeHistory/SettingsView.swift index 80874cc..962d70f 100644 --- a/app/ClaudeCodeHistory/SettingsView.swift +++ b/app/ClaudeCodeHistory/SettingsView.swift @@ -1,217 +1,314 @@ import SwiftUI +/// 设置页 · 4 tab(按 Sprint 6 UI 设计稿) +/// +/// 布局: +/// - 左侧导航条(180px) +/// - 右侧内容区(flex) +/// +/// 四个 tab: +/// 1. 通用 — 外观 / 数据 / 运行环境(合并原"外观" + "数据") +/// 2. 供应商 — 原"认证" 改名 +/// 3. 快捷键 — 新增(只读展示) +/// 4. 实验性 — 新增(beta feature flags) struct SettingsView: View { @ObservedObject var settings = AppSettings.shared - @StateObject private var checker = EnvironmentChecker.shared // v4 + @StateObject private var checker = EnvironmentChecker.shared @Environment(\.dismiss) var dismiss - @State private var selectedTab = 0 - // v4: 新增"认证"tab - private let tabs = [ - ("paintbrush", "外观"), - ("folder", "数据"), - ("key", "认证"), // v4 新增 - ] + @State private var selectedTab: SettingsTab = .general - var body: some View { - VStack(spacing: 0) { - ZStack { - HStack(spacing: 2) { - ForEach(0.. some View { + let isSelected = selectedTab == tab + return Button(action: { withAnimation(.easeInOut(duration: 0.15)) { selectedTab = tab } }) { + HStack(spacing: 10) { + Image(systemName: tab.iconName).font(.system(size: 12)).frame(width: 16) + Text(tab.rawValue).font(.system(size: 13, weight: isSelected ? .semibold : .regular)) + if let badge = tab.badge { Text(badge).font(.system(size: 10)) } + Spacer() } - settingsSection("侧边栏") { - settingsRow("对话列表字号") { + .foregroundColor(isSelected ? settings.tc : .primary) + .padding(.horizontal, 14).padding(.vertical, 7) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(isSelected ? settings.tcLight : Color.clear) + .padding(.horizontal, 8) + ) + .overlay( + Rectangle() + .fill(isSelected ? settings.tc : Color.clear) + .frame(width: 3), + alignment: .leading + ) + }.buttonStyle(.plain) + } + + // ─── 右侧内容 ─── + @ViewBuilder + private var content: some View { + switch selectedTab { + case .general: generalTab + case .provider: providerTab + case .shortcuts: shortcutsTab + case .experimental: experimentalTab + } + } + + // MARK: - 通用 tab + + private var generalTab: some View { + VStack(alignment: .leading, spacing: 24) { + section("外观") { + row("主题色") { HStack(spacing: 10) { - Text("A").font(.system(size: 10)).foregroundColor(.secondary) - Slider(value: $settings.sidebarFontSize, in: 11...18, step: 0.5).frame(width: 140).tint(settings.tc) - Text("A").font(.system(size: 16)).foregroundColor(.secondary) + ForEach(AppThemeColor.allCases) { theme in + Button(action: { settings.themeColor = theme }) { + Circle().fill(theme.color).frame(width: 26, height: 26) + .overlay( + Circle() + .strokeBorder(settings.themeColor == theme ? Color.primary : Color.clear, lineWidth: 2) + .frame(width: 32, height: 32) + ) + }.buttonStyle(.plain).help(theme.rawValue) + } + } + } + row("侧边栏字号") { + HStack(spacing: 8) { + Text("A").font(.system(size: 10)) + Slider(value: $settings.sidebarFontSize, in: 11...18, step: 0.5) + .frame(width: 140).tint(settings.tc) + Text("A").font(.system(size: 16)) Text(String(format: "%.0f", settings.sidebarFontSize)) - .font(.system(size: 12, design: .monospaced)).foregroundColor(settings.tc).frame(width: 24, alignment: .trailing) + .font(.system(size: 11, design: .monospaced)) + .foregroundColor(settings.tc).frame(width: 20, alignment: .trailing) } } } - } - } - // ─── 数据(保持 v3 不变)─── - private var dataTab: some View { - VStack(alignment: .leading, spacing: 20) { - settingsSection("数据目录") { - VStack(alignment: .leading, spacing: 8) { + section("数据") { + VStack(alignment: .leading, spacing: 10) { HStack(spacing: 8) { - Image(systemName: "folder.fill").foregroundColor(settings.tc).frame(width: 20) + Image(systemName: "folder.fill").foregroundColor(settings.tc) TextField("默认: ~/.claude/projects", text: $settings.customDataPath) .textFieldStyle(.roundedBorder).font(.system(size: 12)) Button("浏览...") { - let panel = NSOpenPanel(); panel.canChooseDirectories = true; panel.canChooseFiles = false - if panel.runModal() == .OK, let url = panel.url { settings.customDataPath = url.path } - }.font(.system(size: 12)) + let panel = NSOpenPanel() + panel.canChooseDirectories = true + panel.canChooseFiles = false + if panel.runModal() == .OK, let url = panel.url { + settings.customDataPath = url.path + } + }.font(.system(size: 11)) } HStack(spacing: 4) { - Image(systemName: "info.circle").font(.system(size: 10)).foregroundColor(.secondary.opacity(0.5)) + Image(systemName: "info.circle").font(.system(size: 10)) Text("当前: \(settings.effectiveDataPath)") - .font(.system(size: 10)).foregroundColor(.secondary.opacity(0.5)).lineLimit(1).truncationMode(.middle) - }.padding(.leading, 28) + .font(.system(size: 10)).lineLimit(1).truncationMode(.middle) + } + .foregroundColor(.secondary).padding(.leading, 24) if !settings.customDataPath.isEmpty { Button(action: { settings.customDataPath = "" }) { - HStack(spacing: 4) { Image(systemName: "arrow.uturn.backward"); Text("恢复默认路径") } - .font(.system(size: 11)).foregroundColor(settings.tc) - }.buttonStyle(.plain).padding(.leading, 28) + HStack(spacing: 4) { + Image(systemName: "arrow.uturn.backward") + Text("恢复默认路径") + } + .font(.system(size: 11)).foregroundColor(settings.tc) + }.buttonStyle(.plain).padding(.leading, 24) } } - } - settingsSection("历史保留") { - settingsRow("显示范围") { + row("历史保留") { Picker("", selection: $settings.historyRetentionDays) { - Text("全部").tag(0); Divider() - Text("最近 7 天").tag(7); Text("最近 30 天").tag(30) - Text("最近 90 天").tag(90); Text("最近 1 年").tag(365) + Text("全部").tag(0) + Divider() + Text("最近 7 天").tag(7) + Text("最近 30 天").tag(30) + Text("最近 90 天").tag(90) + Text("最近 1 年").tag(365) }.labelsHidden().frame(width: 160) } } - } - } - - // ─── v4 新增:Provider tab ─── - @StateObject private var providerMgr = ProviderManager.shared - @AppStorage("setupCompleted") private var setupCompleted = false - @State private var showAddProvider = false - private var authTab: some View { - VStack(alignment: .leading, spacing: 20) { - // 当前 Provider - settingsSection("API Provider") { - // OAuth - HStack(spacing: 10) { - Image(systemName: "person.crop.circle").font(.system(size: 14)).foregroundColor(settings.tc) - Text("Claude 账号登录").font(.system(size: 12)) + section("运行环境") { + envRow("Node.js", checker.nodeStatus) + envRow("Claude Code CLI", checker.claudeStatus) + HStack { Spacer() - if providerMgr.activeProvider == nil { - Text("当前").font(.system(size: 10, weight: .medium)).foregroundColor(settings.tc) - .padding(.horizontal, 7).padding(.vertical, 2) - .background(Capsule().fill(settings.tcLight)) - } else { - Button("切换") { providerMgr.activateOAuth() }.buttonStyle(.bordered).controlSize(.mini) - } - Button("登录") { - let home = FileManager.default.homeDirectoryForCurrentUser.path - runInExternalTerminal("\(home)/.local/bin/claude login") - }.buttonStyle(.bordered).controlSize(.mini) + Button("重新检查") { Task { await checker.checkAll() } } + .buttonStyle(.bordered).controlSize(.small) + Button("重新运行引导") { + UserDefaults.standard.set(false, forKey: "setupCompleted") + }.buttonStyle(.bordered).controlSize(.small) } + } + } + .onAppear { Task { await checker.checkAll() } } + } - Divider() + // MARK: - 供应商 tab - // Provider 列表 - ForEach(providerMgr.providers) { p in - HStack(spacing: 10) { - Image(systemName: "key.fill").font(.system(size: 12)).foregroundColor(settings.tc) - VStack(alignment: .leading, spacing: 1) { - Text(p.name).font(.system(size: 12, weight: .medium)) - if !p.baseURL.isEmpty { - Text(p.baseURL).font(.system(size: 9, design: .monospaced)).foregroundColor(.secondary).lineLimit(1) - } - } - Spacer() - if p.isActive { - Text("启用中").font(.system(size: 10, weight: .medium)).foregroundColor(settings.tc) - .padding(.horizontal, 7).padding(.vertical, 2) - .background(Capsule().fill(settings.tcLight)) - } else { - Button("启用") { providerMgr.activate(p.id) }.buttonStyle(.bordered).controlSize(.mini) - } - Button(action: { providerMgr.removeProvider(p.id) }) { - Image(systemName: "trash").font(.system(size: 10)).foregroundColor(.secondary.opacity(0.4)) - }.buttonStyle(.plain) - } - } + @StateObject private var providerMgr = ProviderManager.shared + @State private var showAddProvider = false + private var providerTab: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + let oauthCount = providerMgr.activeProvider == nil ? 1 : 0 + Text("已配置的供应商 (\(providerMgr.providers.count + oauthCount))") + .font(.system(size: 12)).foregroundColor(.secondary) + Spacer() Button(action: { showAddProvider = true }) { HStack(spacing: 4) { - Image(systemName: "plus.circle.fill").font(.system(size: 11)) - Text("添加 Provider").font(.system(size: 11, weight: .medium)) - }.foregroundColor(settings.tc) + Image(systemName: "plus").font(.system(size: 10, weight: .bold)) + Text("添加供应商").font(.system(size: 11, weight: .medium)) + } + .padding(.horizontal, 10).padding(.vertical, 5) + .background(Capsule().fill(settings.tc)) + .foregroundColor(.white) }.buttonStyle(.plain) } - // 环境状态 - settingsSection("运行环境") { - envRow("Node.js", checker.nodeStatus) - envRow("Claude Code CLI", checker.claudeStatus) - HStack { - Spacer() - Button("重新检查") { Task { await checker.checkAll() } }.buttonStyle(.bordered).controlSize(.small) - Button("重新运行引导") { setupCompleted = false }.buttonStyle(.bordered).controlSize(.small) - } + // OAuth(Claude 官方) + providerCard( + title: "Claude 官方账号(OAuth)", + subtitle: "通过 claude login 命令登录", + icon: "person.crop.circle", + isActive: providerMgr.activeProvider == nil, + isOAuth: true, + providerId: nil + ) + + ForEach(providerMgr.providers) { p in + providerCard( + title: p.name, + subtitle: p.baseURL.isEmpty ? "API Key 方式" : p.baseURL, + icon: "key.fill", + isActive: p.isActive, + isOAuth: false, + providerId: p.id + ) } } - .onAppear { - Task { await checker.checkAll() } - providerMgr.detectCurrentProvider() - } + .onAppear { providerMgr.detectCurrentProvider() } .sheet(isPresented: $showAddProvider) { AddProviderView(providerMgr: providerMgr, isPresented: $showAddProvider) .environmentObject(settings) } } + @ViewBuilder + private func providerCard(title: String, subtitle: String, icon: String, isActive: Bool, isOAuth: Bool, providerId: String?) -> some View { + HStack(spacing: 12) { + Image(systemName: icon).font(.system(size: 16)) + .foregroundColor(settings.tc) + .frame(width: 28, height: 28) + .background(Circle().fill(settings.tcLight)) + + VStack(alignment: .leading, spacing: 2) { + Text(title).font(.system(size: 13, weight: .semibold)) + Text(subtitle).font(.system(size: 11, design: .monospaced)) + .foregroundColor(.secondary).lineLimit(1) + } + Spacer() + + if isActive { + Label("当前使用", systemImage: "checkmark.circle.fill") + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.green) + .padding(.horizontal, 8).padding(.vertical, 3) + .background(Capsule().fill(Color.green.opacity(0.1))) + } else if isOAuth { + Button("登录") { + let home = FileManager.default.homeDirectoryForCurrentUser.path + runInExternalTerminal("\(home)/.local/bin/claude login") + }.buttonStyle(.bordered).controlSize(.small) + } else if let id = providerId { + Button("启用") { providerMgr.activate(id) } + .buttonStyle(.bordered).controlSize(.small) + } + + if let id = providerId { + Button(action: { providerMgr.removeProvider(id) }) { + Image(systemName: "trash") + .font(.system(size: 11)).foregroundColor(.secondary.opacity(0.5)) + }.buttonStyle(.plain) + } + } + .padding(12) + .background(RoundedRectangle(cornerRadius: 10).fill(Color.secondary.opacity(0.04))) + .overlay( + RoundedRectangle(cornerRadius: 10) + .strokeBorder(isActive ? settings.tc : Color.secondary.opacity(0.1), + lineWidth: isActive ? 1.5 : 0.5) + ) + } + private func envRow(_ name: String, _ status: DependencyStatus) -> some View { HStack { Text(name).font(.system(size: 12)) @@ -223,27 +320,133 @@ struct SettingsView: View { Image(systemName: "checkmark.circle.fill").foregroundColor(.green) Text(ver).font(.system(size: 11, design: .monospaced)).foregroundColor(.secondary) } - case .missing: Label("未安装", systemImage: "xmark.circle.fill").foregroundColor(.red).font(.system(size: 11)) + case .missing: + Label("未安装", systemImage: "xmark.circle.fill") + .foregroundColor(.red).font(.system(size: 11)) case .installing: - HStack(spacing: 4) { ProgressView().controlSize(.mini); Text("安装中").font(.caption).foregroundColor(.secondary) } + HStack(spacing: 4) { + ProgressView().controlSize(.mini) + Text("安装中").font(.caption).foregroundColor(.secondary) + } case .installFailed(let msg): - Label("失败", systemImage: "exclamationmark.triangle.fill").foregroundColor(.orange).font(.system(size: 11)).help(msg) + Label("失败", systemImage: "exclamationmark.triangle.fill") + .foregroundColor(.orange).font(.system(size: 11)).help(msg) + } + } + } + + // MARK: - 快捷键 tab + + private var shortcutsTab: some View { + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 6) { + Image(systemName: "info.circle").font(.system(size: 11)) + Text("快捷键当前为只读,编辑功能规划在 Sprint 8+") + .font(.system(size: 11)) + } + .foregroundColor(.secondary) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(RoundedRectangle(cornerRadius: 6).fill(Color.secondary.opacity(0.06))) + + section("会话") { + shortcutRow("新建对话", "⌘N") + shortcutRow("刷新列表", "⌘R") + shortcutRow("关闭窗口", "⌘W") + } + section("导航") { + shortcutRow("设置", "⌘,") + shortcutRow("切换收藏过滤", "⌘⇧F") + shortcutRow("停止运行", "⌘.") + } + section("输入") { + shortcutRow("命令面板", "/") + shortcutRow("发送消息", "⌘↵") + shortcutRow("换行", "⇧↵") } } } - // ─── 通用组件(保持 v3 不变)─── - private func settingsSection(_ title: String, @ViewBuilder content: () -> Content) -> some View { + private func shortcutRow(_ label: String, _ keys: String) -> some View { + HStack { + Text(label).font(.system(size: 12)) + Spacer() + Text(keys) + .font(.system(size: 11, design: .monospaced)) + .padding(.horizontal, 8).padding(.vertical, 2) + .background(RoundedRectangle(cornerRadius: 4).fill(Color.secondary.opacity(0.12))) + } + } + + // MARK: - 实验性 tab + + @AppStorage("experimentalFeatures.fuzzyCommand") private var expFuzzyCommand: Bool = true + @AppStorage("experimentalFeatures.longContext") private var expLongContext: Bool = false + @AppStorage("experimentalFeatures.multimodal") private var expMultimodal: Bool = false + @AppStorage("experimentalFeatures.debugPanel") private var expDebugPanel: Bool = false + + private var experimentalTab: some View { + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 6) { + Text("⚠️").font(.system(size: 12)) + Text("以下功能不稳定,启用前请知悉风险") + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.orange) + } + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(RoundedRectangle(cornerRadius: 6).fill(Color.orange.opacity(0.08))) + + experimentalToggle(title: "命令面板模糊搜索", + description: "beta · 已开启,如有问题可关", + isOn: $expFuzzyCommand) + experimentalToggle(title: "AI 长上下文优化(256k → 1M)", + description: "beta · 可能更慢、更贵", + isOn: $expLongContext) + experimentalToggle(title: "多模态输入(图片 / PDF)", + description: "alpha · 部分供应商不支持", + isOn: $expMultimodal) + experimentalToggle(title: "调试面板(F12)", + description: "开发者用", + isOn: $expDebugPanel) + } + } + + private func experimentalToggle(title: String, description: String, isOn: Binding) -> some View { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(title).font(.system(size: 13, weight: .medium)) + Text(description).font(.system(size: 11)).foregroundColor(.secondary) + } + Spacer() + Toggle("", isOn: isOn).labelsHidden() + } + .padding(12) + .background(RoundedRectangle(cornerRadius: 8).fill(Color.secondary.opacity(0.04))) + } + + // MARK: - 通用组件 + + private func section(_ title: String, @ViewBuilder content: () -> Content) -> some View { VStack(alignment: .leading, spacing: 10) { - Text(title).font(.system(size: 12, weight: .semibold)).foregroundColor(settings.tc).textCase(.uppercase).tracking(0.5) + Text(title) + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(.secondary) + .textCase(.uppercase) + .tracking(0.5) VStack(alignment: .leading, spacing: 10) { content() } - .padding(14).frame(maxWidth: .infinity, alignment: .leading) + .padding(14) + .frame(maxWidth: .infinity, alignment: .leading) .background(RoundedRectangle(cornerRadius: 10).fill(Color.secondary.opacity(0.04))) .overlay(RoundedRectangle(cornerRadius: 10).strokeBorder(Color.secondary.opacity(0.08), lineWidth: 0.5)) } } - private func settingsRow(_ label: String, @ViewBuilder content: () -> Content) -> some View { - HStack { Text(label).font(.system(size: 13)); Spacer(); content() } + private func row(_ label: String, @ViewBuilder content: () -> Content) -> some View { + HStack { + Text(label).font(.system(size: 13)) + Spacer() + content() + } } }