From 975503a651ae625e6a35d8a855c39371961f0054 Mon Sep 17 00:00:00 2001 From: jdjingdian Date: Tue, 24 Mar 2026 11:54:37 +0800 Subject: [PATCH 1/2] fix(ui): unify settings entry and align route edit sheets unify settings entry behind SettingsNavigator so views rely on one interface across macOS versions. use SettingsLink for macOS 14+ settings scene while keeping legacy selector fallback for macOS 12-13. align RouteEditSheet and LegacyRouteEditSheet visual hierarchy with the style guide to improve consistency and readability. add openspec change artifacts for the macOS 13 settings issue and route config UI alignment. --- StaticRouteHelper.xcodeproj/project.pbxproj | 16 ++-- StaticRouter/Services/SettingsNavigator.swift | 61 +++++++++++++ .../View/Customized/StatusBanner.swift | 4 +- StaticRouter/View/LegacyRouteListView.swift | 76 ++++++++++++---- StaticRouter/View/MainWindow.swift | 14 ++- StaticRouter/View/RouteEditSheet.swift | 14 +-- StaticRouter/View/SidebarView.swift | 2 +- .../.openspec.yaml | 2 + .../design.md | 91 +++++++++++++++++++ .../proposal.md | 27 ++++++ .../specs/main-navigation/spec.md | 24 +++++ .../specs/route-crud/spec.md | 52 +++++++++++ .../specs/status-banner/spec.md | 34 +++++++ .../tasks.md | 20 ++++ 14 files changed, 393 insertions(+), 44 deletions(-) create mode 100644 StaticRouter/Services/SettingsNavigator.swift create mode 100644 openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/.openspec.yaml create mode 100644 openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/design.md create mode 100644 openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/proposal.md create mode 100644 openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/main-navigation/spec.md create mode 100644 openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/route-crud/spec.md create mode 100644 openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/status-banner/spec.md create mode 100644 openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/tasks.md diff --git a/StaticRouteHelper.xcodeproj/project.pbxproj b/StaticRouteHelper.xcodeproj/project.pbxproj index 37db997..a9e976f 100644 --- a/StaticRouteHelper.xcodeproj/project.pbxproj +++ b/StaticRouteHelper.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ AA000004000000000000BB01 /* cn.magicdian.staticrouter.service.plist in CopyFiles */ = {isa = PBXBuildFile; fileRef = AA000004000000000000BB02 /* cn.magicdian.staticrouter.service.plist */; }; AA000001000000000000BB02 /* InstallMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000000000000BB02 /* InstallMethod.swift */; }; AA000001000000000000BB03 /* PrivilegedHelperManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000000000000BB03 /* PrivilegedHelperManager.swift */; }; + AA900001000000000000CC01 /* SettingsNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA900002000000000000CC01 /* SettingsNavigator.swift */; }; /* End PBXBuildFile section */ /* Begin PBXShellScriptBuildPhase section */ @@ -177,6 +178,7 @@ AA000002000000000000AA33 /* StaticRouteLegacy.xcdatamodeld */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodeld; path = StaticRouteLegacy.xcdatamodeld; sourceTree = ""; }; AA000002000000000000BB02 /* InstallMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallMethod.swift; sourceTree = ""; }; AA000002000000000000BB03 /* PrivilegedHelperManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivilegedHelperManager.swift; sourceTree = ""; }; + AA900002000000000000CC01 /* SettingsNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNavigator.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -376,6 +378,7 @@ AA000002000000000000AA21 /* SystemRouteReader.swift */, AA000002000000000000BB02 /* InstallMethod.swift */, AA000002000000000000BB03 /* PrivilegedHelperManager.swift */, + AA900002000000000000CC01 /* SettingsNavigator.swift */, ); path = Services; sourceTree = ""; @@ -542,6 +545,7 @@ 3962561B296EE01B00DB6000 /* RouterCommand.swift in Sources */, AA000001000000000000BB02 /* InstallMethod.swift in Sources */, AA000001000000000000BB03 /* PrivilegedHelperManager.swift in Sources */, + AA900001000000000000CC01 /* SettingsNavigator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -567,7 +571,7 @@ CODE_SIGN_IDENTITY = "-"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 68; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; @@ -598,7 +602,7 @@ CODE_SIGN_IDENTITY = "-"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 68; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; @@ -760,9 +764,9 @@ "$(inherited)", "@executable_path/../Frameworks", ); - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 68; MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.2.2; PRODUCT_BUNDLE_IDENTIFIER = cn.magicdian.staticrouter; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ROUTER; @@ -790,9 +794,9 @@ "$(inherited)", "@executable_path/../Frameworks", ); - CURRENT_PROJECT_VERSION = 66; + CURRENT_PROJECT_VERSION = 68; MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 2.2.1; + MARKETING_VERSION = 2.2.2; PRODUCT_BUNDLE_IDENTIFIER = cn.magicdian.staticrouter; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ROUTER; diff --git a/StaticRouter/Services/SettingsNavigator.swift b/StaticRouter/Services/SettingsNavigator.swift new file mode 100644 index 0000000..7d679d4 --- /dev/null +++ b/StaticRouter/Services/SettingsNavigator.swift @@ -0,0 +1,61 @@ +// +// SettingsNavigator.swift +// StaticRouteHelper +// +// Legacy settings navigation helper (macOS 12-13). +// + +import AppKit +import Foundation +import os +import SwiftUI + +enum SettingsNavigator { + private static let logger = Logger(subsystem: "cn.magicdian.staticrouter", category: "settings-navigation") + + /// Opens the app settings window via selector fallbacks for macOS 12-13. + /// Returns true when an action was successfully dispatched. + @MainActor + @discardableResult + static func openAppSettings() -> Bool { + let settingsWindowSelector = Selector(("showSettingsWindow:")) + let preferencesWindowSelector = Selector(("showPreferencesWindow:")) + + if NSApp.sendAction(settingsWindowSelector, to: nil, from: nil) { + logger.info("Opened settings via showSettingsWindow: fallback") + NSApp.activate(ignoringOtherApps: true) + return true + } + + if NSApp.sendAction(preferencesWindowSelector, to: nil, from: nil) { + logger.info("Opened settings via showPreferencesWindow: fallback") + NSApp.activate(ignoringOtherApps: true) + return true + } + + logger.error("Failed to open settings window using all known legacy actions") + NSApp.activate(ignoringOtherApps: true) + return false + } + + /// Unified settings entry for SwiftUI views. + /// - macOS 14+: uses SettingsLink to open Settings scene. + /// - macOS 12-13: falls back to selector-based window opening. + struct Entry: View { + @ViewBuilder let label: () -> Label + + var body: some View { + if #available(macOS 14, *) { + SettingsLink { + label() + } + } else { + Button { + SettingsNavigator.openAppSettings() + } label: { + label() + } + } + } + } +} diff --git a/StaticRouter/View/Customized/StatusBanner.swift b/StaticRouter/View/Customized/StatusBanner.swift index 92a6dce..f34c9fb 100644 --- a/StaticRouter/View/Customized/StatusBanner.swift +++ b/StaticRouter/View/Customized/StatusBanner.swift @@ -41,8 +41,8 @@ enum BannerStyle { // MARK: - StatusBanner /// Generic banner with a style-driven icon/background and an arbitrary action button. -/// Use the `@ViewBuilder actionButton` parameter to supply any button type (plain Button -/// or SettingsLink) so callers control the navigation target. +/// Use the `@ViewBuilder actionButton` parameter to supply any button type so callers +/// control the navigation target. struct StatusBanner: View { let style: BannerStyle let message: LocalizedStringKey diff --git a/StaticRouter/View/LegacyRouteListView.swift b/StaticRouter/View/LegacyRouteListView.swift index 9a727ff..b595320 100644 --- a/StaticRouter/View/LegacyRouteListView.swift +++ b/StaticRouter/View/LegacyRouteListView.swift @@ -305,78 +305,114 @@ struct LegacyRouteEditSheet: View { } var body: some View { - VStack(alignment: .leading, spacing: 20) { - Text(isEditing ? String(localized: "route.edit.title.edit") : String(localized: "route.edit.title.add")) - .font(.headline) + VStack(alignment: .leading, spacing: 14) { + VStack(alignment: .leading, spacing: 4) { + Text(isEditing ? String(localized: "route.edit.title.edit") : String(localized: "route.edit.title.add")) + .font(.title2.bold()) + Text("Network / Gateway") + .font(.footnote) + .foregroundStyle(.secondary) + } // Network + Prefix - VStack(alignment: .leading, spacing: 6) { - Text(String(localized: "route.edit.field.destination.label")) - .font(.subheadline).foregroundStyle(.secondary) + VStack(alignment: .leading, spacing: 10) { + Label(String(localized: "route.edit.field.destination.label"), systemImage: "dot.scope") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.secondary) HStack(spacing: 8) { TextField("192.168.4.0", text: $network) .textFieldStyle(.roundedBorder) .overlay( RoundedRectangle(cornerRadius: 5) - .stroke(networkError != nil ? Color.red : Color.clear, lineWidth: 1) + .stroke(networkError != nil ? RouterTheme.danger : Color.clear, lineWidth: 1) ) .onChange(of: network) { _ in validateNetwork() } Text("/").foregroundStyle(.secondary) TextField("24", value: $prefixLength, format: .number) + .multilineTextAlignment(.center) .textFieldStyle(.roundedBorder) - .frame(width: 50) + .frame(width: 68) .overlay( RoundedRectangle(cornerRadius: 5) - .stroke(prefixLengthError != nil ? Color.red : Color.clear, lineWidth: 1) + .stroke(prefixLengthError != nil ? RouterTheme.danger : Color.clear, lineWidth: 1) ) } + + Text("= \(subnetMaskPreview)") + .font(.caption) + .foregroundStyle(.secondary) + if let err = networkError { - Text(err).font(.caption).foregroundStyle(.red) + Text(err).font(.caption).foregroundStyle(RouterTheme.danger) + } + if let err = prefixLengthError { + Text(err).font(.caption).foregroundStyle(RouterTheme.danger) } } + .padding(14) + .background(Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .stroke(RouterTheme.subtleBorder, lineWidth: 0.6) + ) // Gateway Type + Gateway - VStack(alignment: .leading, spacing: 6) { - Text(String(localized: "route.edit.field.gateway.label")) - .font(.subheadline).foregroundStyle(.secondary) + VStack(alignment: .leading, spacing: 10) { + Label(String(localized: "route.edit.field.gateway.label"), systemImage: "arrow.triangle.turn.up.right.diamond") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.secondary) Picker(String(localized: "route.edit.field.gateway.label"), selection: $gatewayTypeStr) { Text(String(localized: "route.edit.gateway.ip_option")).tag("ipAddress") Text(String(localized: "route.edit.gateway.interface_option")).tag("interface") } - .pickerStyle(.radioGroup) + .pickerStyle(.segmented) .onChange(of: gatewayTypeStr) { _ in gateway = ""; validateGateway() } TextField(gatewayTypeStr == "ipAddress" ? "10.0.0.1" : "utun3", text: $gateway) .textFieldStyle(.roundedBorder) .overlay( RoundedRectangle(cornerRadius: 5) - .stroke(gatewayError != nil ? Color.red : Color.clear, lineWidth: 1) + .stroke(gatewayError != nil ? RouterTheme.danger : Color.clear, lineWidth: 1) ) .onChange(of: gateway) { _ in validateGateway() } if let err = gatewayError { - Text(err).font(.caption).foregroundStyle(.red) + Text(err).font(.caption).foregroundStyle(RouterTheme.danger) } } - - Divider() + .padding(14) + .background(Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: 10, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .stroke(RouterTheme.subtleBorder, lineWidth: 0.6) + ) HStack { Spacer() Button(String(localized: "route.edit.button.cancel")) { dismiss() } .keyboardShortcut(.cancelAction) + .buttonStyle(.bordered) Button(isEditing ? String(localized: "route.edit.button.save") : String(localized: "route.edit.button.add")) { save() } .keyboardShortcut(.defaultAction) + .buttonStyle(.borderedProminent) .disabled(!isFormValid) } + .padding(.top, 4) + .padding(.horizontal, 2) } - .padding(24) - .frame(width: 360) + .padding(20) + .frame(width: 460) .onAppear { populate() } } + private var subnetMaskPreview: String { + guard prefixLength >= 0, prefixLength <= 32 else { return String(localized: "route.edit.mask_preview.invalid") } + let mask: UInt32 = prefixLength == 0 ? 0 : (~UInt32(0) << (32 - prefixLength)) + return "\((mask>>24)&0xFF).\((mask>>16)&0xFF).\((mask>>8)&0xFF).\(mask&0xFF)" + } + private func populate() { guard let mo = existingMO else { return } network = mo.network diff --git a/StaticRouter/View/MainWindow.swift b/StaticRouter/View/MainWindow.swift index f6dd149..edb6082 100644 --- a/StaticRouter/View/MainWindow.swift +++ b/StaticRouter/View/MainWindow.swift @@ -88,21 +88,21 @@ struct MainWindow14: View { if routerService.helperStatus == .pendingActivation { // Priority 1: installed but background switch is off — needs user approval StatusBanner(style: .warning, message: "helper.banner.pending_approval.message") { - SettingsLink { + SettingsNavigator.Entry { Text(String(localized: "helper.banner.goto_settings")) } } } else if routerService.helperStatus != .installed { // Priority 2: helper not installed / needs upgrade / not compatible StatusBanner(style: .warning, message: "helper.banner.message") { - SettingsLink { + SettingsNavigator.Entry { Text(String(localized: "helper.banner.goto_settings")) } } } else if routerService.helperManager.activeMethod == .smJobBless { // Priority 3 (macOS 14+ only): installed via bless, modern method available StatusBanner(style: .info, message: "helper.banner.bless_upgrade.message") { - SettingsLink { + SettingsNavigator.Entry { Text(String(localized: "helper.banner.goto_settings")) } } @@ -142,8 +142,8 @@ struct LegacyMainWindow: View { // macOS 12–13: only show warning banner (no SMAppService upgrade suggestion) if routerService.helperStatus != .installed { StatusBanner(style: .warning, message: "helper.banner.message") { - Button(String(localized: "helper.banner.goto_settings")) { - NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) + SettingsNavigator.Entry { + Text(String(localized: "helper.banner.goto_settings")) } } } @@ -176,9 +176,7 @@ struct LegacySidebarView: View { .safeAreaInset(edge: .bottom) { HStack { Spacer() - Button { - NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) - } label: { + SettingsNavigator.Entry { Image(systemName: "gearshape") .font(.system(size: 13, weight: .semibold)) .frame(width: 26, height: 22) diff --git a/StaticRouter/View/RouteEditSheet.swift b/StaticRouter/View/RouteEditSheet.swift index af70192..cc7aaad 100644 --- a/StaticRouter/View/RouteEditSheet.swift +++ b/StaticRouter/View/RouteEditSheet.swift @@ -47,18 +47,18 @@ struct RouteEditSheet: View { // MARK: - Body var body: some View { - VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 14) { VStack(alignment: .leading, spacing: 4) { Text(isEditing ? String(localized: "route.edit.title.edit") : String(localized: "route.edit.title.add")) .font(.title2.bold()) - Text("CIDR / Gateway / Group") + Text("Network / Gateway / Group") .font(.footnote) .foregroundStyle(.secondary) } // Network Address + Prefix Length VStack(alignment: .leading, spacing: 10) { - Text(String(localized: "route.edit.field.destination.label")) + Label(String(localized: "route.edit.field.destination.label"), systemImage: "dot.scope") .font(.subheadline.weight(.semibold)) .foregroundStyle(.secondary) HStack(spacing: 8) { @@ -103,7 +103,7 @@ struct RouteEditSheet: View { // Gateway Type Picker + Gateway Input VStack(alignment: .leading, spacing: 10) { - Text(String(localized: "route.edit.field.gateway.label")) + Label(String(localized: "route.edit.field.gateway.label"), systemImage: "arrow.triangle.turn.up.right.diamond") .font(.subheadline.weight(.semibold)) .foregroundStyle(.secondary) @@ -136,7 +136,7 @@ struct RouteEditSheet: View { // Group Multi-Select if !allGroups.isEmpty { VStack(alignment: .leading, spacing: 10) { - Text(String(localized: "route.edit.field.groups.label")) + Label(String(localized: "route.edit.field.groups.label"), systemImage: "folder.badge.plus") .font(.subheadline.weight(.semibold)) .foregroundStyle(.secondary) LazyVGrid(columns: [GridItem(.adaptive(minimum: 130), spacing: 8)], spacing: 8) { @@ -175,8 +175,6 @@ struct RouteEditSheet: View { ) } - Divider() - HStack { Spacer() Button(String(localized: "route.edit.button.cancel")) { dismiss() } @@ -187,6 +185,8 @@ struct RouteEditSheet: View { .buttonStyle(.borderedProminent) .disabled(!isFormValid) } + .padding(.top, 4) + .padding(.horizontal, 2) } .padding(20) .frame(width: 460) diff --git a/StaticRouter/View/SidebarView.swift b/StaticRouter/View/SidebarView.swift index adcd382..f7bae05 100644 --- a/StaticRouter/View/SidebarView.swift +++ b/StaticRouter/View/SidebarView.swift @@ -115,7 +115,7 @@ struct SidebarView: View { Spacer() - SettingsLink { + SettingsNavigator.Entry { Image(systemName: "gearshape") .font(.system(size: 13, weight: .semibold)) .frame(width: 26, height: 22) diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/.openspec.yaml b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/.openspec.yaml new file mode 100644 index 0000000..2ca4bc8 --- /dev/null +++ b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-24 diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/design.md b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/design.md new file mode 100644 index 0000000..3fd2635 --- /dev/null +++ b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/design.md @@ -0,0 +1,91 @@ +## 上下文 + +当前设置入口在代码中存在两套实现: +- macOS 14+ 主要使用 `SettingsLink` +- macOS 12–13 主要使用 `NSApp.sendAction(Selector(("showPreferencesWindow:")), ...)` + +实际反馈显示 macOS 13 上“设置按钮”和 Banner 的“前往设置”入口存在点击无反应,说明现有方案在部分系统版本/窗口场景下不稳定。与此同时,路由添加/编辑界面在新旧分支(`RouteEditSheet` vs `LegacyRouteEditSheet`)视觉和交互差异较大,不满足 `design/STYLE_GUIDE.md` 对分区、控件一致性和错误反馈的规范要求。 + +## 目标 / 非目标 + +**目标:** +- 在 macOS 12–14+ 上为“打开应用设置”提供稳定、可回退的统一调用路径。 +- 让侧边栏齿轮按钮与 Banner CTA 使用同一设置跳转逻辑,行为一致。 +- 让添加/编辑路由弹窗在 14+ 与 12–13 上尽可能对齐样式基线(卡片分区、分段选择、语义化错误样式、操作区层级)。 +- 保持现有功能语义不变(字段、保存逻辑、分组能力差异仍按系统版本保留)。 + +**非目标:** +- 不修改 Helper 安装/激活核心业务逻辑。 +- 不新增数据模型字段,不调整 SwiftData/Core Data 存储结构。 +- 不重做整个主窗口视觉,仅修复设置入口与路由配置弹窗。 + +## 决策 + +### 决策 1:引入统一的设置跳转封装(版本感知 + 回退) + +**选择:** +新增一个统一设置导航方法(例如 `SettingsNavigator.openAppSettings()`),并让 Banner/Sidebar 入口全部调用它。该方法采用回退链: +1. 优先使用当前系统推荐入口(14+ 可用的设置打开动作) +2. 失败时尝试 `showSettingsWindow:` +3. 再失败时尝试 `showPreferencesWindow:` +4. 最后记录日志并确保应用激活到前台,避免“无反馈”感知 + +**替代方案:** +- 继续使用 `SettingsLink` + 局部 legacy selector(维持现状) +- 所有版本直接硬编码单一 selector + +**理由:** +- 当前问题本质是入口分叉且缺少回退机制。 +- 统一封装可减少未来回归风险,也便于在多入口复用。 + +### 决策 2:将设置入口从“组件差异”改为“行为一致” + +**选择:** +Banner 与 Sidebar 的设置入口都使用显式 `Button` + 统一跳转动作,不再由各处自行决定 `SettingsLink` 或 selector。 + +**替代方案:** +- 仅修复 macOS 13 legacy 分支,不动 14+ `SettingsLink` + +**理由:** +- 多实现并存容易导致跨版本行为不一致。 +- 将“如何打开设置”收敛到单点后,后续改动只需要调整一个地方。 + +### 决策 3:路由配置弹窗采用统一的“卡片分区 + 语义样式”结构 + +**选择:** +对 `RouteEditSheet` 与 `LegacyRouteEditSheet` 统一采用: +- 三段式信息组织(目标网络、路由方式与网关、分组/说明) +- 网关类型使用 segmented 选择(legacy 从 radioGroup 切换) +- 错误态统一为语义红色边框 + 文本提示 +- 统一按钮区层级和控件尺寸,沿用 `RouterTheme` token + +**替代方案:** +- 仅改 14+,保留 legacy 旧样式 + +**理由:** +- 用户反馈聚焦“添加路由配置界面美观一致”,只改 14+ 无法覆盖 macOS 13 实际使用路径。 +- 该改造仅影响表现层,不改变保存逻辑,回归风险可控。 + +## 风险 / 权衡 + +- [风险] 部分系统版本对 selector 支持差异导致仍可能出现设置窗口未弹出 + → 缓解:使用多级回退 + 日志记录 + 真机验证(至少 macOS 13 与 14) + +- [风险] Legacy 弹窗视觉改造引入交互细节回归(键盘快捷键、禁用态) + → 缓解:保持原有保存/取消快捷键与校验逻辑,仅替换布局与样式层 + +- [权衡] 为一致性将 14+ 的 `SettingsLink` 改为统一动作,失去部分声明式简洁性 + → 收益:跨版本行为可控、可诊断、可回退 + +## Migration Plan + +1. 先引入统一设置跳转封装并替换入口调用(Sidebar + Banner)。 +2. 对 14+ 路由弹窗微调视觉细节,保持字段与数据流不变。 +3. 对 12–13 Legacy 路由弹窗同步样式升级(含 segmented 网关类型)。 +4. 在 macOS 13 与 14+ 上做手动回归:设置入口可达、表单校验、保存/取消、键盘快捷键。 +5. 如发现设置入口异常,可快速回滚到上一入口实现(功能层无数据迁移影响)。 + +## Open Questions + +- 是否需要在设置跳转失败时增加用户可见提示(例如轻量 Alert),还是仅记录日志即可? +- Legacy 弹窗在较小窗口宽度下,是否需要进一步压缩卡片内边距以保证无换行拥挤? diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/proposal.md b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/proposal.md new file mode 100644 index 0000000..9fd9fdb --- /dev/null +++ b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/proposal.md @@ -0,0 +1,27 @@ +## 为什么 + +当前在 macOS 13 上,侧边栏设置按钮与 Banner 中“前往设置”入口存在点击无响应问题,导致用户无法从主界面进入设置页完成 Helper 安装与排障。同时,添加/编辑路由弹窗在不同系统分支上的视觉与交互不一致,和 `design/STYLE_GUIDE.md` 里定义的卡片分区、控件样式与错误反馈规范存在偏差。 + +## 变更内容 + +- 修复设置入口在 macOS 12–14+ 的可达性问题:统一设置跳转策略,按系统版本选择可用方案并提供回退链路,确保设置按钮与 Banner CTA 均可打开设置窗口。 +- 将主界面中的设置入口(侧边栏齿轮、顶部 Banner“前往设置”)收敛到同一跳转实现,避免多处分叉逻辑导致行为不一致。 +- 对齐添加/编辑路由弹窗视觉规范:按“目标网络 / 路由方式与网关 / 分组”分区,统一控件尺寸与层级,使用语义化错误样式与更一致的操作区布局。 +- macOS 12–13 的 Legacy 路由编辑弹窗同步采用新样式语言(在能力范围内保持与 14+ 体验接近),减少跨版本体验割裂。 + +## 功能 (Capabilities) + +### 新增功能 +- (无) + +### 修改功能 +- `main-navigation`: 侧边栏设置入口在所有支持系统版本上必须稳定打开设置窗口,并采用版本感知的回退策略。 +- `status-banner`: Banner 内“前往设置”动作必须与侧边栏设置入口一致,避免 macOS 13 点击无响应。 +- `route-crud`: 添加/编辑路由弹窗的结构与样式要求更新,以对齐 `design/STYLE_GUIDE.md` 的组件和交互规范。 + +## 影响 + +- 视图层:`MainWindow`、`SidebarView` 的设置跳转入口将调整为统一策略;`RouteEditSheet` 与 `LegacyRouteEditSheet` 的布局和交互样式将更新。 +- 交互一致性:设置入口行为在 Banner 与工具栏之间统一,减少用户路径失败。 +- 设计规范对齐:本次 UI 调整以 `design/STYLE_GUIDE.md` 为基线,不引入新的视觉 token。 +- 测试与验证:需要在 macOS 13 与 macOS 14+ 分别验证设置跳转路径、弹窗可用性与表单校验反馈。 diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/main-navigation/spec.md b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/main-navigation/spec.md new file mode 100644 index 0000000..c4533c8 --- /dev/null +++ b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/main-navigation/spec.md @@ -0,0 +1,24 @@ +## MODIFIED Requirements + +### Requirement: Sidebar 底部工具栏 +Sidebar 底部 SHALL 显示工具栏,包含: +- "+" 按钮:创建新的路由分组 +- 设置按钮(齿轮图标):打开应用设置窗口 + +设置按钮 SHALL 通过统一的设置导航策略执行,确保 macOS 12–14+ 均可打开设置窗口。该策略必须包含版本感知和失败回退(例如首选现代入口,失败后尝试 selector 回退链路)。 + +#### Scenario: 点击添加分组按钮 +- **WHEN** 用户点击 Sidebar 底部的 "+" 按钮 +- **THEN** 弹出分组创建界面(输入名称的弹窗或 Sheet) + +#### Scenario: macOS 14+ 点击设置按钮 +- **WHEN** 用户在 macOS 14+ 点击齿轮图标 +- **THEN** 应用设置窗口被打开,且当前窗口保持可交互 + +#### Scenario: macOS 13 点击设置按钮 +- **WHEN** 用户在 macOS 13 点击齿轮图标 +- **THEN** 应用通过回退策略成功打开设置窗口,不得出现无响应 + +#### Scenario: 首选设置入口不可用时回退 +- **WHEN** 当前首选设置入口调用失败或不可用 +- **THEN** 系统自动尝试后续回退入口,直到设置窗口成功打开或记录失败日志 diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/route-crud/spec.md b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/route-crud/spec.md new file mode 100644 index 0000000..e847156 --- /dev/null +++ b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/route-crud/spec.md @@ -0,0 +1,52 @@ +## MODIFIED Requirements + +### Requirement: 添加路由规则 +用户 SHALL 能够通过路由列表视图中的 "添加路由" 按钮打开 Sheet 表单,输入路由信息并保存。 + +在 macOS 14+ 上,Sheet 表单 SHALL 包含以下输入项: +- 网络地址(IPv4 文本输入) +- 前缀长度(数字输入或下拉选择,范围 0-32) +- 路由方式(分段选择器:网关 IP / 网络接口) +- 网关地址或接口名称(根据路由方式切换) +- 所属分组(多选分组标签卡片,列出所有已有分组) + +在 macOS 12–13 上,Sheet 表单 SHALL 包含除分组多选以外的所有字段,分组选择区域不显示。 + +添加路由表单在所有支持版本上 SHALL 对齐 `design/STYLE_GUIDE.md`: +- 使用“目标网络 / 路由方式与网关 / 分组(如适用)”卡片分区 +- 网关类型使用 segmented 选择,不使用 radio 纵向堆叠 +- 输入错误使用语义红色边框和错误文案提示 +- 底部操作区保留取消/保存主次层级,并保持按钮尺寸与间距一致 + +表单 SHALL 在前缀长度下方自动显示对应的子网掩码(只读)。 + +#### Scenario: 添加路由弹窗遵循分区样式(macOS 14+) +- **WHEN** 用户在 macOS 14+ 打开“添加路由”Sheet +- **THEN** 弹窗按目标网络、路由方式与网关、分组三个区块展示,分组以可点击标签卡片形式呈现 + +#### Scenario: 添加路由弹窗遵循分区样式(macOS 12–13) +- **WHEN** 用户在 macOS 13 打开“添加路由”Sheet +- **THEN** 弹窗使用与 14+ 一致的卡片与错误样式语言,不显示分组区块,网关类型为 segmented 选择 + +#### Scenario: 输入无效时显示统一错误反馈 +- **WHEN** 用户输入非法 IP 或非法前缀长度 +- **THEN** 对应字段显示语义红色边框与错误文本,保存按钮禁用 + +#### Scenario: 添加使用 IP 网关的路由(macOS 14+,含分组) +- **WHEN** 用户在 macOS 14+ 上点击"添加路由",输入网络地址 "192.168.5.0",前缀长度 24,选择"网关 IP",输入 "10.0.0.1",勾选 "Office VPN" 分组,点击"保存" +- **THEN** 系统创建新路由规则(isActive 为 false),在 "All Routes" 和 "Office VPN" 视图中可见 + +#### Scenario: 添加使用 IP 网关的路由(macOS 12–13,无分组) +- **WHEN** 用户在 macOS 13 上点击"添加路由",输入网络地址 "192.168.5.0",前缀长度 24,选择"网关 IP",输入 "10.0.0.1",点击"保存" +- **THEN** 系统创建 RouteRuleMO 并保存到 Core Data,在路由列表中可见,不显示分组信息 + +### Requirement: 编辑路由规则 +用户 SHALL 能够编辑已有的路由规则。编辑操作通过双击路由行或右键上下文菜单中的"编辑"选项触发,打开与添加相同视觉规范的 Sheet 表单,预填充当前值。在 macOS 12–13 上,编辑操作从 Core Data `RouteRuleMO` 读取并保存数据。 + +#### Scenario: 编辑路由弹窗沿用添加弹窗视觉规范 +- **WHEN** 用户在任意支持版本打开“编辑路由”Sheet +- **THEN** 弹窗布局、控件样式和错误反馈规则与“添加路由”一致,并预填充原有路由字段 + +#### Scenario: 编辑路由后保存(所有版本) +- **WHEN** 用户将网关从 "10.0.0.1" 修改为 "10.0.0.2",点击"保存" +- **THEN** 路由规则更新,若该路由当前 isActive 为 true,系统 SHALL 先删除旧路由再添加新路由(即自动重新激活) diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/status-banner/spec.md b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/status-banner/spec.md new file mode 100644 index 0000000..b75f594 --- /dev/null +++ b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/status-banner/spec.md @@ -0,0 +1,34 @@ +## MODIFIED Requirements + +### Requirement: 主窗口横幅显示逻辑 +主窗口详情区域顶部 SHALL 根据以下条件按优先级显示一个横幅(互斥,只显示最高优先级): + +1. **优先级 1** — `helperStatus == .pendingActivation`:显示 `.warning` 横幅,消息提示 Helper 已安装但待生效(需开启后台运行开关),按钮引导到设置页 +2. **优先级 2** — `helperStatus != .installed` 且 `helperStatus != .pendingActivation`:显示 `.warning` 横幅,消息提示 Helper 未安装,按钮引导到设置页 +3. **优先级 3** — macOS 14+ 且 `activeMethod == .smJobBless` 且 `helperStatus == .installed`:显示 `.info` 横幅,消息提示有更现代的部署方式可用,按钮引导到设置页(卸载后重新安装) + +横幅中的“前往设置”按钮 SHALL 与 Sidebar 设置按钮复用同一设置导航策略,确保在 macOS 12–14+ 上均可用,且行为一致。 + +#### Scenario: 待生效时显示 warning 横幅 +- **WHEN** `helperStatus` 为 `.pendingActivation` +- **THEN** 主窗口顶部显示浅黄色 warning 横幅,图标为黄色感叹号,并提示需要开启后台运行开关 + +#### Scenario: Helper 未安装时显示 warning 横幅 +- **WHEN** `helperStatus` 为 `.notInstalled`、`.needUpgrade` 或 `.notCompatible` +- **THEN** 主窗口顶部显示浅黄色 warning 横幅,附带"前往设置"按钮 + +#### Scenario: SMJobBless 已安装时显示 info 横幅 +- **WHEN** macOS 14+ 上 `activeMethod` 为 `.smJobBless` 且 `helperStatus` 为 `.installed` +- **THEN** 主窗口顶部显示浅蓝色 info 横幅,提示可升级到 SMAppService,附带"前往设置"按钮 + +#### Scenario: macOS 13 点击横幅“前往设置” +- **WHEN** 用户在 macOS 13 点击横幅中的“前往设置” +- **THEN** 设置窗口被成功打开,不得出现点击无响应 + +#### Scenario: SMAppService 已安装时不显示横幅 +- **WHEN** `activeMethod` 为 `.smAppService` 且 `helperStatus` 为 `.installed` +- **THEN** 主窗口顶部不显示任何横幅 + +#### Scenario: macOS 12–13 上 SMJobBless 已安装时不显示 info 横幅 +- **WHEN** macOS 12–13 上 `activeMethod` 为 `.smJobBless` 且 `helperStatus` 为 `.installed` +- **THEN** 不显示 info 横幅(macOS 12–13 无 SMAppService 可用) diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/tasks.md b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/tasks.md new file mode 100644 index 0000000..49a841f --- /dev/null +++ b/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/tasks.md @@ -0,0 +1,20 @@ +## 1. 设置入口跨版本兼容修复 + +- [x] 1.1 新增统一设置跳转封装(单点 API),实现版本感知与回退链路(现代入口 -> `showSettingsWindow:` -> `showPreferencesWindow:`)。 +- [x] 1.2 将 `MainWindow` 中 Banner 的“前往设置”动作改为调用统一设置跳转封装,移除分散的直接 selector 调用。 +- [x] 1.3 将侧边栏工具区设置按钮(含 legacy 分支)统一改为同一跳转实现,确保行为一致。 +- [x] 1.4 为设置跳转失败路径补充日志与最小可观测信息,便于定位“点击无响应”问题。 + +## 2. 添加/编辑路由弹窗样式对齐 + +- [x] 2.1 按 `design/STYLE_GUIDE.md` 重构 `RouteEditSheet` 区块结构与间距层级,确保“目标网络 / 路由方式与网关 / 分组”分区一致。 +- [x] 2.2 重构 `LegacyRouteEditSheet` 视觉与交互样式,使其与 14+ 风格对齐(卡片化区块、统一按钮区、语义错误样式)。 +- [x] 2.3 将 legacy 网关类型选择从 `radioGroup` 调整为 segmented 选择器,并保持原有验证逻辑与保存逻辑不变。 +- [x] 2.4 统一两套弹窗的错误反馈表现(红色边框 + 文案)与保存按钮禁用规则。 + +## 3. 验证与回归 + +- [ ] 3.1 在 macOS 13 验证设置按钮与 Banner“前往设置”均可打开设置窗口。 +- [ ] 3.2 在 macOS 14+ 验证设置入口行为与 13 一致,且不影响现有设置窗口打开体验。 +- [ ] 3.3 回归添加/编辑路由流程(含 IP 网关、接口网关、无效输入、已激活路由编辑后重激活)。 +- [x] 3.4 检查 UI 与 `design/STYLE_GUIDE.md` 一致性(控件尺寸、分区层级、语义色与错误态)。 From 66856e68fed8d6665b91972d12a6bae70f384610 Mon Sep 17 00:00:00 2001 From: jdjingdian Date: Tue, 24 Mar 2026 12:07:41 +0800 Subject: [PATCH 2/2] chore(openspec): archive macos13 settings nav and route ui change mark all tasks done for settings nav and route ui polish sync delta specs into main-navigation, status-banner, route-crud archive change to 2026-03-24-fix-macos13-settings-navigation-and-route-config-ui --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/main-navigation/spec.md | 0 .../specs/route-crud/spec.md | 0 .../specs/status-banner/spec.md | 0 .../tasks.md | 6 +++--- openspec/specs/main-navigation/spec.md | 16 ++++++++++++--- openspec/specs/route-crud/spec.md | 20 ++++++++++++++++++- openspec/specs/status-banner/spec.md | 6 ++++++ 10 files changed, 41 insertions(+), 7 deletions(-) rename openspec/changes/{fix-macos13-settings-navigation-and-route-config-ui => archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui}/.openspec.yaml (100%) rename openspec/changes/{fix-macos13-settings-navigation-and-route-config-ui => archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui}/design.md (100%) rename openspec/changes/{fix-macos13-settings-navigation-and-route-config-ui => archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui}/proposal.md (100%) rename openspec/changes/{fix-macos13-settings-navigation-and-route-config-ui => archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui}/specs/main-navigation/spec.md (100%) rename openspec/changes/{fix-macos13-settings-navigation-and-route-config-ui => archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui}/specs/route-crud/spec.md (100%) rename openspec/changes/{fix-macos13-settings-navigation-and-route-config-ui => archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui}/specs/status-banner/spec.md (100%) rename openspec/changes/{fix-macos13-settings-navigation-and-route-config-ui => archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui}/tasks.md (88%) diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/.openspec.yaml b/openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/.openspec.yaml similarity index 100% rename from openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/.openspec.yaml rename to openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/.openspec.yaml diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/design.md b/openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/design.md similarity index 100% rename from openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/design.md rename to openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/design.md diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/proposal.md b/openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/proposal.md similarity index 100% rename from openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/proposal.md rename to openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/proposal.md diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/main-navigation/spec.md b/openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/specs/main-navigation/spec.md similarity index 100% rename from openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/main-navigation/spec.md rename to openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/specs/main-navigation/spec.md diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/route-crud/spec.md b/openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/specs/route-crud/spec.md similarity index 100% rename from openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/route-crud/spec.md rename to openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/specs/route-crud/spec.md diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/status-banner/spec.md b/openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/specs/status-banner/spec.md similarity index 100% rename from openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/specs/status-banner/spec.md rename to openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/specs/status-banner/spec.md diff --git a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/tasks.md b/openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/tasks.md similarity index 88% rename from openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/tasks.md rename to openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/tasks.md index 49a841f..99f28a8 100644 --- a/openspec/changes/fix-macos13-settings-navigation-and-route-config-ui/tasks.md +++ b/openspec/changes/archive/2026-03-24-fix-macos13-settings-navigation-and-route-config-ui/tasks.md @@ -14,7 +14,7 @@ ## 3. 验证与回归 -- [ ] 3.1 在 macOS 13 验证设置按钮与 Banner“前往设置”均可打开设置窗口。 -- [ ] 3.2 在 macOS 14+ 验证设置入口行为与 13 一致,且不影响现有设置窗口打开体验。 -- [ ] 3.3 回归添加/编辑路由流程(含 IP 网关、接口网关、无效输入、已激活路由编辑后重激活)。 +- [x] 3.1 在 macOS 13 验证设置按钮与 Banner“前往设置”均可打开设置窗口。 +- [x] 3.2 在 macOS 14+ 验证设置入口行为与 13 一致,且不影响现有设置窗口打开体验。 +- [x] 3.3 回归添加/编辑路由流程(含 IP 网关、接口网关、无效输入、已激活路由编辑后重激活)。 - [x] 3.4 检查 UI 与 `design/STYLE_GUIDE.md` 一致性(控件尺寸、分区层级、语义色与错误态)。 diff --git a/openspec/specs/main-navigation/spec.md b/openspec/specs/main-navigation/spec.md index 9d18f96..575c8fe 100644 --- a/openspec/specs/main-navigation/spec.md +++ b/openspec/specs/main-navigation/spec.md @@ -53,13 +53,23 @@ Sidebar 底部 SHALL 显示工具栏,包含: - "+" 按钮:创建新的路由分组 - 设置按钮(齿轮图标):打开应用设置窗口 +设置按钮 SHALL 通过统一的设置导航策略执行,确保 macOS 12–14+ 均可打开设置窗口。该策略必须包含版本感知和失败回退(例如首选现代入口,失败后尝试 selector 回退链路)。 + #### Scenario: 点击添加分组按钮 - **WHEN** 用户点击 Sidebar 底部的 "+" 按钮 - **THEN** 弹出分组创建界面(输入名称的弹窗或 Sheet) -#### Scenario: 点击设置按钮 -- **WHEN** 用户点击齿轮图标 -- **THEN** 打开 Settings 窗口 +#### Scenario: macOS 14+ 点击设置按钮 +- **WHEN** 用户在 macOS 14+ 点击齿轮图标 +- **THEN** 应用设置窗口被打开,且当前窗口保持可交互 + +#### Scenario: macOS 13 点击设置按钮 +- **WHEN** 用户在 macOS 13 点击齿轮图标 +- **THEN** 应用通过回退策略成功打开设置窗口,不得出现无响应 + +#### Scenario: 首选设置入口不可用时回退 +- **WHEN** 当前首选设置入口调用失败或不可用 +- **THEN** 系统自动尝试后续回退入口,直到设置窗口成功打开或记录失败日志 ### Requirement: Helper 未安装横幅提示 当 Privileged Helper 未安装时,主界面顶部 SHALL 显示醒目的横幅提示,告知用户需要安装 Helper 才能执行路由操作,并提供跳转到设置页面的按钮。 diff --git a/openspec/specs/route-crud/spec.md b/openspec/specs/route-crud/spec.md index 3300c0a..4bdd182 100644 --- a/openspec/specs/route-crud/spec.md +++ b/openspec/specs/route-crud/spec.md @@ -8,12 +8,26 @@ - 前缀长度(数字输入或下拉选择,范围 0-32) - 路由方式(分段选择器:网关 IP / 网络接口) - 网关地址或接口名称(根据路由方式切换) -- 所属分组(多选复选框列表,列出所有已有分组) +- 所属分组(多选分组标签卡片,列出所有已有分组) 在 macOS 12–13 上,Sheet 表单 SHALL 包含除分组多选以外的所有字段,分组选择区域不显示。 +添加路由表单在所有支持版本上 SHALL 对齐 `design/STYLE_GUIDE.md`: +- 使用“目标网络 / 路由方式与网关 / 分组(如适用)”卡片分区 +- 网关类型使用 segmented 选择,不使用 radio 纵向堆叠 +- 输入错误使用语义红色边框和错误文案提示 +- 底部操作区保留取消/保存主次层级,并保持按钮尺寸与间距一致 + 表单 SHALL 在前缀长度下方自动显示对应的子网掩码(只读)。 +#### Scenario: 添加路由弹窗遵循分区样式(macOS 14+) +- **WHEN** 用户在 macOS 14+ 打开“添加路由”Sheet +- **THEN** 弹窗按目标网络、路由方式与网关、分组三个区块展示,分组以可点击标签卡片形式呈现 + +#### Scenario: 添加路由弹窗遵循分区样式(macOS 12–13) +- **WHEN** 用户在 macOS 13 打开“添加路由”Sheet +- **THEN** 弹窗使用与 14+ 一致的卡片与错误样式语言,不显示分组区块,网关类型为 segmented 选择 + #### Scenario: 添加使用 IP 网关的路由(macOS 14+,含分组) - **WHEN** 用户在 macOS 14+ 上点击"添加路由",输入网络地址 "192.168.5.0",前缀长度 24,选择"网关 IP",输入 "10.0.0.1",勾选 "Office VPN" 分组,点击"保存" - **THEN** 系统创建新路由规则(isActive 为 false),在 "All Routes" 和 "Office VPN" 视图中可见 @@ -33,6 +47,10 @@ ### Requirement: 编辑路由规则 用户 SHALL 能够编辑已有的路由规则。编辑操作通过双击路由行或右键上下文菜单中的"编辑"选项触发,打开与添加相同的 Sheet 表单,预填充当前值。在 macOS 12–13 上,编辑操作从 Core Data `RouteRuleMO` 读取并保存数据。 +#### Scenario: 编辑路由弹窗沿用添加弹窗视觉规范 +- **WHEN** 用户在任意支持版本打开“编辑路由”Sheet +- **THEN** 弹窗布局、控件样式和错误反馈规则与“添加路由”一致,并预填充原有路由字段 + #### Scenario: 编辑路由规则(macOS 14+) - **WHEN** 用户在 macOS 14+ 上双击 "192.168.4.0/24" 路由行 - **THEN** 打开 Sheet 表单,预填充该路由的当前网络地址、前缀长度、网关信息和分组关联 diff --git a/openspec/specs/status-banner/spec.md b/openspec/specs/status-banner/spec.md index 1b6c2b9..8f4e89a 100644 --- a/openspec/specs/status-banner/spec.md +++ b/openspec/specs/status-banner/spec.md @@ -24,6 +24,8 @@ 2. **优先级 2** — `helperStatus != .installed` 且 `helperStatus != .pendingActivation`:显示 `.warning` 横幅,消息提示 Helper 未安装,按钮引导到设置页 3. **优先级 3** — macOS 14+ 且 `activeMethod == .smJobBless` 且 `helperStatus == .installed`:显示 `.info` 横幅,消息提示有更现代的部署方式可用,按钮引导到设置页(卸载后重新安装) +横幅中的“前往设置”按钮 SHALL 与 Sidebar 设置按钮复用同一设置导航策略,确保在 macOS 12–14+ 上均可用,且行为一致。 + #### Scenario: 待生效时显示 warning 横幅 - **WHEN** `helperStatus` 为 `.pendingActivation` - **THEN** 主窗口顶部显示浅黄色 warning 横幅,图标为黄色感叹号,并提示需要开启后台运行开关 @@ -36,6 +38,10 @@ - **WHEN** macOS 14+ 上 `activeMethod` 为 `.smJobBless` 且 `helperStatus` 为 `.installed` - **THEN** 主窗口顶部显示浅蓝色 info 横幅,提示可升级到 SMAppService,附带"前往设置"按钮 +#### Scenario: macOS 13 点击横幅“前往设置” +- **WHEN** 用户在 macOS 13 点击横幅中的“前往设置” +- **THEN** 设置窗口被成功打开,不得出现点击无响应 + #### Scenario: SMAppService 已安装时不显示横幅 - **WHEN** `activeMethod` 为 `.smAppService` 且 `helperStatus` 为 `.installed` - **THEN** 主窗口顶部不显示任何横幅