docs: UI 重设计方案 — Apple 浅深双主题
- 设计令牌为单一真相源(theme.py 中 Python dict) - Apple Blue 主色统一 macOS/Windows - 监听 styleHints colorSchemeChanged 实现自动跟随系统 - 范围:外观 + 关键 layout 修复(按钮分级、卡片化、sidebar 头部) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Showing
2 changed files
with
422 additions
and
0 deletions
| 1 | # UI 重设计:Apple 高级浅色主题 + 浅深双模式 | ||
| 2 | |||
| 3 | **日期**:2026-05-09 | ||
| 4 | **分支**:`feat/ui-redesign-apple-theme` | ||
| 5 | **状态**:设计已定,实施进行中 | ||
| 6 | |||
| 7 | --- | ||
| 8 | |||
| 9 | ## 1. 背景与目标 | ||
| 10 | |||
| 11 | ### 1.1 问题陈述 | ||
| 12 | 当前 UI 是 PySide6 默认皮肤 + 65+ 处零散 inline `setStyleSheet`(image_generator.py:65 处、task_queue.py:4 处),没有统一设计语言。具体表现: | ||
| 13 | |||
| 14 | - 全灰白默认 Qt 风格,缺现代感 | ||
| 15 | - "生成图片"和"下载图片"主次按钮视觉权重一样 | ||
| 16 | - GroupBox 边框过重(Win95 观感) | ||
| 17 | - "任务队列"sidebar 标题在视觉上飘到主区 tab bar 旁边 | ||
| 18 | - 字号/间距/圆角不统一 | ||
| 19 | - macOS 系统切换深色主题时整个应用观感断裂 | ||
| 20 | |||
| 21 | ### 1.2 目标 | ||
| 22 | 1. **统一设计语言**:Apple Human Interface Guidelines 浅色风格 + Apple Blue (#0071E3) 主色 | ||
| 23 | 2. **跨平台一致**:macOS 和 Windows 11 视觉一致(用户原话:"统一两端的设计语言") | ||
| 24 | 3. **支持系统深浅主题自动切换**:跟随 `QGuiApplication.styleHints().colorScheme()` | ||
| 25 | 4. **修复关键 layout 问题**:按钮主次分级、sidebar 头部、卡片化 GroupBox | ||
| 26 | 5. **保持业务逻辑不变**:纯视觉层重构,不动生成 / 队列 / 历史功能 | ||
| 27 | |||
| 28 | ### 1.3 非目标(YAGNI) | ||
| 29 | - 不引第三方 UI 库(qfluentwidgets、qt-material 等) | ||
| 30 | - 不重做信息架构(tabs、表单字段) | ||
| 31 | - 不增加新功能 | ||
| 32 | - 不做主题色自定义(用户改色等高级功能) | ||
| 33 | |||
| 34 | --- | ||
| 35 | |||
| 36 | ## 2. 已确认的设计决策 | ||
| 37 | |||
| 38 | | 决策点 | 结果 | 用户原话/理由 | | ||
| 39 | |---|---|---| | ||
| 40 | | 设计方向 | C · Apple 高级感浅色 | "可以统一 MacOS 与 Windows 的设计语言" | | ||
| 41 | | 主色调 | A · Apple Blue `#0071E3` | "跟我们的品牌元素的颜色契合" | | ||
| 42 | | 重写范围 | B · 外观 + 关键 layout 修复 | (选择推荐项)| | ||
| 43 | | 深色模式 | 完整双套主题 | "macos 会让软件跟随时间自由转换深浅主题,不做就巨丑巨难看" | | ||
| 44 | | 二级窗口 | 一并重做(登录 + 所有 dialog)| (选择推荐项)| | ||
| 45 | | 实现路径 | B · 设计令牌生成器(Python tokens → build_qss)| Python dict 比双 .qss 文件不容易飘,dark mode 90% 是规则反转 | | ||
| 46 | |||
| 47 | --- | ||
| 48 | |||
| 49 | ## 3. 设计令牌(Design Tokens) | ||
| 50 | |||
| 51 | 所有数值在 `theme.py` 中以 Python dict 定义,单一真相源。 | ||
| 52 | |||
| 53 | ### 3.1 颜色 — 浅色(Light) | ||
| 54 | |||
| 55 | ``` | ||
| 56 | # 表面层级 | ||
| 57 | bg_canvas #fbfbfd 主窗口背景(极浅冷灰) | ||
| 58 | bg_surface #ffffff 卡片 / dialog 背景 | ||
| 59 | bg_elevated #ffffff 带投影的浮起容器 | ||
| 60 | bg_subtle #f4f4f7 GroupBox / 输入框 hover 底色 | ||
| 61 | |||
| 62 | # 文字层级 | ||
| 63 | text_primary #1d1d1f 主文字 | ||
| 64 | text_secondary #6e6e73 次要文字 / 标签 | ||
| 65 | text_tertiary #86868b 占位符 / 禁用文字 | ||
| 66 | text_on_accent #ffffff 主按钮上的白字 | ||
| 67 | |||
| 68 | # 边框 / 分隔 | ||
| 69 | border_default rgba(0,0,0,0.10) 默认边框(投影替代式) | ||
| 70 | border_strong rgba(0,0,0,0.15) 焦点 / 选中 | ||
| 71 | divider rgba(0,0,0,0.06) 分隔线 | ||
| 72 | |||
| 73 | # 主色 | ||
| 74 | accent #0071e3 Apple Blue | ||
| 75 | accent_hover #0077ed 悬停(略亮) | ||
| 76 | accent_pressed #0062c4 按下 | ||
| 77 | accent_subtle rgba(0,113,227,0.08) 主色淡背景(hover 卡片等) | ||
| 78 | |||
| 79 | # 状态色 | ||
| 80 | success #34c759 成功 / 在线 | ||
| 81 | warning #ff9500 警告 | ||
| 82 | danger #ff3b30 危险 / 错误 | ||
| 83 | ``` | ||
| 84 | |||
| 85 | ### 3.2 颜色 — 深色(Dark) | ||
| 86 | |||
| 87 | ``` | ||
| 88 | bg_canvas #1a1a1c | ||
| 89 | bg_surface #2c2c2e | ||
| 90 | bg_elevated #3a3a3c | ||
| 91 | bg_subtle #242426 | ||
| 92 | |||
| 93 | text_primary #ffffff | ||
| 94 | text_secondary #98989d | ||
| 95 | text_tertiary #6e6e73 | ||
| 96 | text_on_accent #ffffff | ||
| 97 | |||
| 98 | border_default rgba(255,255,255,0.10) | ||
| 99 | border_strong rgba(255,255,255,0.18) | ||
| 100 | divider rgba(255,255,255,0.06) | ||
| 101 | |||
| 102 | accent #0a84ff (Dark mode 下 Apple Blue 略亮) | ||
| 103 | accent_hover #1d8cff | ||
| 104 | accent_pressed #006edc | ||
| 105 | accent_subtle rgba(10,132,255,0.16) | ||
| 106 | |||
| 107 | success #30d158 | ||
| 108 | warning #ff9f0a | ||
| 109 | danger #ff453a | ||
| 110 | ``` | ||
| 111 | |||
| 112 | **设计原则**:浅深对照不是简单灰阶反转,而是参考 Apple SF Symbols / SwiftUI 的语义对照表。`bg_canvas` 比 `bg_surface` 低半级(浅色:surface 比 canvas 亮;深色:surface 比 canvas 亮)—— 卡片永远比底板"凸"。 | ||
| 113 | |||
| 114 | ### 3.3 间距与尺寸 | ||
| 115 | |||
| 116 | ``` | ||
| 117 | # 圆角 | ||
| 118 | radius_sm 4px 标签 / chip | ||
| 119 | radius_md 8px 输入框 / 普通按钮 / 卡片 | ||
| 120 | radius_lg 12px 大卡片 / dialog | ||
| 121 | radius_pill 980px 胶囊主按钮(Apple HIG 标志性) | ||
| 122 | |||
| 123 | # 间距(4px grid) | ||
| 124 | space_1 4px | ||
| 125 | space_2 8px | ||
| 126 | space_3 12px | ||
| 127 | space_4 16px | ||
| 128 | space_5 20px | ||
| 129 | space_6 24px | ||
| 130 | space_8 32px | ||
| 131 | |||
| 132 | # 字号(pt - Qt 跨平台) | ||
| 133 | font_xs 10pt 提示 / 标签 | ||
| 134 | font_sm 11pt 次要文字 / 按钮文字 | ||
| 135 | font_base 12pt 正文(Windows)/ 13pt(macOS, 系统检测加 1) | ||
| 136 | font_lg 14pt 小标题 | ||
| 137 | font_xl 17pt 主标题 | ||
| 138 | |||
| 139 | # 控件高度 | ||
| 140 | control_h_sm 28px 小按钮 / 紧凑输入框 | ||
| 141 | control_h_md 32px 常规按钮 / 输入框(默认) | ||
| 142 | control_h_lg 40px 主操作按钮 / 大输入框 | ||
| 143 | ``` | ||
| 144 | |||
| 145 | ### 3.4 字体栈 | ||
| 146 | |||
| 147 | ``` | ||
| 148 | font_family = "-apple-system, 'SF Pro Text', 'PingFang SC', | ||
| 149 | 'Microsoft YaHei UI', 'Segoe UI Variable', | ||
| 150 | 'Segoe UI', sans-serif" | ||
| 151 | ``` | ||
| 152 | |||
| 153 | 策略:让操作系统选最合适的字体——macOS 走 SF Pro / 苹方,Windows 走 Segoe UI Variable / 微软雅黑。Qt 会自动 fallback。 | ||
| 154 | |||
| 155 | --- | ||
| 156 | |||
| 157 | ## 4. 模块架构 | ||
| 158 | |||
| 159 | ### 4.1 `theme.py`(新文件,约 300 行) | ||
| 160 | |||
| 161 | ```python | ||
| 162 | # 公共 API | ||
| 163 | def apply_theme(app: QApplication) -> ThemeManager | ||
| 164 | """应用主题。在 main() 创建 QApplication 后立即调用。返回 ThemeManager | ||
| 165 | 实例(持有引用以保持信号连接)。""" | ||
| 166 | |||
| 167 | class ThemeManager(QObject): | ||
| 168 | theme_changed = Signal(str) # 'light' | 'dark' | ||
| 169 | def current_mode() -> str | ||
| 170 | def force_mode(mode: str | None) # None = 跟随系统 | ||
| 171 | # 内部:监听 QGuiApplication.styleHints().colorSchemeChanged | ||
| 172 | |||
| 173 | # 令牌 | ||
| 174 | TOKENS_LIGHT: dict[str, str] | ||
| 175 | TOKENS_DARK: dict[str, str] | ||
| 176 | |||
| 177 | def build_qss(mode: str) -> str | ||
| 178 | """根据 mode(light/dark)生成完整 QSS 字符串。""" | ||
| 179 | ``` | ||
| 180 | |||
| 181 | ### 4.2 集成点 | ||
| 182 | |||
| 183 | **main 入口** `image_generator.py:4940`: | ||
| 184 | ```python | ||
| 185 | app = QApplication(sys.argv) | ||
| 186 | from theme import apply_theme | ||
| 187 | theme_manager = apply_theme(app) # 必须在创建任何窗口前 | ||
| 188 | ``` | ||
| 189 | |||
| 190 | **preflight 入口** `preflight.py:215, 218, 256, 258`:同上 | ||
| 191 | |||
| 192 | **所有 widget**:通过 `setObjectName()` 或 `setProperty()` 来命中 QSS 选择器,**禁止再用 inline setStyleSheet**(除少数语义性状态色,见 §6.2)。 | ||
| 193 | |||
| 194 | ### 4.3 数据流 | ||
| 195 | |||
| 196 | ``` | ||
| 197 | 系统切换深色 → Qt 触发 colorSchemeChanged | ||
| 198 | → ThemeManager 接收信号 | ||
| 199 | → build_qss(new_mode) | ||
| 200 | → app.setStyleSheet(new_qss) | ||
| 201 | → 所有 widget 自动重绘(Qt 内置) | ||
| 202 | → 发射 theme_changed 信号(业务代码可监听,目前不需要) | ||
| 203 | ``` | ||
| 204 | |||
| 205 | --- | ||
| 206 | |||
| 207 | ## 5. 组件样式映射 | ||
| 208 | |||
| 209 | ### 5.1 按钮分级 | ||
| 210 | |||
| 211 | ``` | ||
| 212 | QPushButton 默认次按钮(白底蓝边/字) | ||
| 213 | QPushButton[variant="primary"] 主按钮(实色 accent 背景,白字,pill 圆角) | ||
| 214 | QPushButton[variant="ghost"] 幽灵按钮(透明背景,hover 出 accent_subtle) | ||
| 215 | QPushButton[variant="danger"] 危险按钮(红字描边) | ||
| 216 | ``` | ||
| 217 | |||
| 218 | **应用清单**: | ||
| 219 | - `生成图片` → `variant="primary"`(pill 圆角 + accent 背景) | ||
| 220 | - `下载图片` / `添加图片` / `粘贴图片` → 默认次按钮 | ||
| 221 | - `删除` / `清空历史` → `variant="danger"` | ||
| 222 | - `收藏` 之类的图标按钮 → `variant="ghost"` | ||
| 223 | |||
| 224 | ### 5.2 GroupBox → 卡片化 | ||
| 225 | |||
| 226 | 去掉边框 + Win95 风格标题,改为: | ||
| 227 | - 白色底(`bg_surface`) | ||
| 228 | - 极淡的投影(`box-shadow: 0 1px 2px rgba(0,0,0,0.04)`,Qt QSS 不直接支持 box-shadow,用 `border: 1px solid border_default` 模拟) | ||
| 229 | - 标题不再镶嵌在边框上,改为顶部独立 label(uppercase 小字 + `text_secondary`) | ||
| 230 | |||
| 231 | ### 5.3 Tab 栏 | ||
| 232 | |||
| 233 | ``` | ||
| 234 | QTabBar::tab 默认状态:透明背景 + text_secondary 文字 | ||
| 235 | QTabBar::tab:selected 选中:accent 文字 + 底部 2px accent 横线 | ||
| 236 | QTabBar::tab:hover 悬停:text_primary 文字 | ||
| 237 | ``` | ||
| 238 | |||
| 239 | 去掉默认 Qt tab 的"凸起带边框"外观,改为下划线式(更现代,Notion/VSCode/Chrome 都这风格)。 | ||
| 240 | |||
| 241 | ### 5.4 输入框 / 下拉 | ||
| 242 | |||
| 243 | ``` | ||
| 244 | QLineEdit, QTextEdit, QComboBox | ||
| 245 | 默认:bg_surface + 1px border_default + radius_md | ||
| 246 | 焦点:1px border_strong + 1px accent 外发光(QSS 不支持 outline,用双层 border 实现) | ||
| 247 | 禁用:bg_subtle + text_tertiary | ||
| 248 | ``` | ||
| 249 | |||
| 250 | ### 5.5 状态色(保留 inline,但走 property) | ||
| 251 | |||
| 252 | ```python | ||
| 253 | # 之前 | ||
| 254 | self.status_label.setStyleSheet("QLabel { color: #34C759; }") | ||
| 255 | # 之后 | ||
| 256 | self.status_label.setProperty("status", "success") | ||
| 257 | self.status_label.style().polish(self.status_label) # 重绘 | ||
| 258 | ``` | ||
| 259 | |||
| 260 | QSS 里: | ||
| 261 | ```css | ||
| 262 | QLabel[status="success"] { color: palette(success); } | ||
| 263 | QLabel[status="error"] { color: palette(danger); } | ||
| 264 | QLabel[status="muted"] { color: palette(text_secondary); } | ||
| 265 | ``` | ||
| 266 | |||
| 267 | ### 5.6 任务队列 sidebar header 修复 | ||
| 268 | |||
| 269 | 当前 sidebar 顶部"任务队列"label 在视觉上飘到主区 tab bar 右侧。修复:sidebar 顶部加一个 32px 高的 header bar,使 sidebar 头部与主区 tab bar 在同一基线。 | ||
| 270 | |||
| 271 | --- | ||
| 272 | |||
| 273 | ## 6. 迁移策略 | ||
| 274 | |||
| 275 | ### 6.1 全量删除清单 | ||
| 276 | |||
| 277 | ``` | ||
| 278 | image_generator.py: 65 处 setStyleSheet | ||
| 279 | task_queue.py: 4 处 setStyleSheet | ||
| 280 | 合计 69 处 | ||
| 281 | ``` | ||
| 282 | |||
| 283 | 按性质分类: | ||
| 284 | |||
| 285 | | 类型 | 数量估算 | 处理方式 | | ||
| 286 | |---|---|---| | ||
| 287 | | 全局 QSS 块(line 1331/1602/1644/1659/1676/2427)| 6 | **删除**,迁移到 theme.py | | ||
| 288 | | 装饰色(按钮背景、容器边框、字体大小) | ~50 | **删除**,依赖全局主题 | | ||
| 289 | | 状态色(红/绿/灰)| ~10 | **改 setProperty + style().polish()** | | ||
| 290 | | 平台兼容 hack(Mac 输入可见性等) | ~3 | 保留并在 design doc 标注 | | ||
| 291 | |||
| 292 | ### 6.2 保留的 inline setStyleSheet | ||
| 293 | |||
| 294 | 只有以下场景允许 inline: | ||
| 295 | 1. 平台 hack(如 macOS 输入框可见性 fix,line 1285) | ||
| 296 | 2. 临时高亮反馈(鼠标悬停后 1 秒消失这种) | ||
| 297 | |||
| 298 | 其他一律走 QSS。 | ||
| 299 | |||
| 300 | ### 6.3 OBJECT_NAME 命名约定 | ||
| 301 | |||
| 302 | ``` | ||
| 303 | loginDialog 登录对话框 | ||
| 304 | mainWindow 主窗口 | ||
| 305 | generationTab 生成 tab | ||
| 306 | styleDesignerTab 款式设计 tab | ||
| 307 | historyTab 历史记录 tab | ||
| 308 | taskQueueSidebar 任务队列侧边栏 | ||
| 309 | sidebarHeader 侧边栏头部 32px | ||
| 310 | previewArea 预览区 | ||
| 311 | referenceImageDrop 参考图拖拽区 | ||
| 312 | ``` | ||
| 313 | |||
| 314 | QSS 通过 `#objectName` 命中。 | ||
| 315 | |||
| 316 | --- | ||
| 317 | |||
| 318 | ## 7. Layout 修复清单(Scope B 范围内) | ||
| 319 | |||
| 320 | | 问题 | 修复方案 | | ||
| 321 | |---|---| | ||
| 322 | | "任务队列"飘右上角 | sidebar 顶部加 32px header bar 与 tab bar 对齐 | | ||
| 323 | | 主次按钮不分 | "生成图片"加 `variant="primary"` | | ||
| 324 | | GroupBox 边框过重 | 卡片化(白底 + 极淡边框 + 圆角)| | ||
| 325 | | 控件间距不一 | 统一改用 `space_3 (12px)` 间距 | | ||
| 326 | | 状态文字颜色 inline | 改 property | | ||
| 327 | | 登录页和主窗口风格断裂 | 全部走全局主题 | | ||
| 328 | |||
| 329 | **不做的 layout 改动**(避免范围蔓延): | ||
| 330 | - 不重排 tab 顺序 | ||
| 331 | - 不动 70/30 主区与 sidebar 比例 | ||
| 332 | - 不改字段顺序 | ||
| 333 | - 不改"款式设计"tab 的 9 个 GroupBox 信息架构 | ||
| 334 | |||
| 335 | --- | ||
| 336 | |||
| 337 | ## 8. 测试策略 | ||
| 338 | |||
| 339 | ### 8.1 自动化测试 | ||
| 340 | 项目目前没有 GUI 测试框架。**不为本次重构新增测试**(避免引入 pytest-qt 等依赖)。 | ||
| 341 | 保留现有 `test_config_loading.py` 跑通即可(QSS 重构不影响配置)。 | ||
| 342 | |||
| 343 | ### 8.2 手工验收清单 | ||
| 344 | |||
| 345 | 启动后按顺序检查: | ||
| 346 | |||
| 347 | - [ ] 登录页能正常显示和登录 | ||
| 348 | - [ ] 主窗口三个 tab 切换正常 | ||
| 349 | - [ ] "生成图片"按钮视觉权重明显高于"下载图片" | ||
| 350 | - [ ] GroupBox 看起来是"卡片"而不是"边框" | ||
| 351 | - [ ] 任务队列侧边栏头部不再飘到主区 tab 上 | ||
| 352 | - [ ] 输入框有焦点蓝边 | ||
| 353 | - [ ] 系统切换深色(Mac: 系统设置 → 外观 / Win11: 设置 → 个性化 → 颜色)→ 应用自动跟随 | ||
| 354 | - [ ] QMessageBox 弹窗也跟主题(Qt 默认会,确认一下) | ||
| 355 | - [ ] 所有"删除"按钮有 danger 视觉 | ||
| 356 | - [ ] 业务功能完全没动(生成、队列、历史) | ||
| 357 | |||
| 358 | ### 8.3 打包验证 | ||
| 359 | PyInstaller 打包跑一次,确保: | ||
| 360 | - exe 能启动到登录页 | ||
| 361 | - 启动到主窗口 | ||
| 362 | - 主题资源(如有)被打包进去 | ||
| 363 | |||
| 364 | --- | ||
| 365 | |||
| 366 | ## 9. 风险与回退 | ||
| 367 | |||
| 368 | ### 9.1 已知风险 | ||
| 369 | |||
| 370 | | 风险 | 缓解 | | ||
| 371 | |---|---| | ||
| 372 | | Qt QSS 不支持 box-shadow,圆角阴影效果有限 | 用双层 border 模拟,效果接近但不等同 web | | ||
| 373 | | StyleHint colorSchemeChanged 在某些 Linux DE 不发射 | 项目实际部署只在 macOS + Windows,无影响 | | ||
| 374 | | 删 inline setStyleSheet 时漏改某个 widget → 视觉断层 | 每次 commit 单独跑一次启动验证 | | ||
| 375 | | 全局 palette 改变后 QMessageBox 系统 dialog 可能不跟随 | 全局 setStyleSheet 会覆盖;如不行再单独处理 | | ||
| 376 | |||
| 377 | ### 9.2 回退方案 | ||
| 378 | |||
| 379 | 每次 commit 颗粒度小,单点回退: | ||
| 380 | ```bash | ||
| 381 | git revert <commit-sha> | ||
| 382 | ``` | ||
| 383 | |||
| 384 | 最坏情况整分支扔掉: | ||
| 385 | ```bash | ||
| 386 | git checkout master | ||
| 387 | git branch -D feat/ui-redesign-apple-theme | ||
| 388 | ``` | ||
| 389 | |||
| 390 | master 完全不动。 | ||
| 391 | |||
| 392 | --- | ||
| 393 | |||
| 394 | ## 10. 实施步骤(参考 TaskList) | ||
| 395 | |||
| 396 | 1. ✅ 创建分支 | ||
| 397 | 2. ✅ 写本设计文档 | ||
| 398 | 3. 实现 `theme.py` | ||
| 399 | 4. 主入口接入 | ||
| 400 | 5. 迁移 ImageGeneratorWindow | ||
| 401 | 6. 迁移 LoginDialog | ||
| 402 | 7. 迁移 StyleDesignerTab + DraggableThumbnail | ||
| 403 | 8. 迁移 TaskQueueWidget | ||
| 404 | 9. 打包验证 | ||
| 405 | 10. 推送 + 写 OVERNIGHT_SUMMARY.md | ||
| 406 | |||
| 407 | 每步 1-2 个 commit,每步完成后启动跑一次冒烟(`python image_generator.py` 能进登录页即可)。 | ||
| 408 | |||
| 409 | --- | ||
| 410 | |||
| 411 | ## 11. 给明天的你 | ||
| 412 | |||
| 413 | 醒来先看仓库根目录的 `OVERNIGHT_SUMMARY.md`,里面有完整进度、commit 列表、怎么运行、怎么验收、已知问题。 | ||
| 414 | |||
| 415 | 如果某处方向不对,直接 `git checkout master` 当无事发生。 |
-
Please register or sign in to post a comment