跳转至

07 - UI 设计规范(Apple HIG · 极简)

Flet 界面的设计与实现规范。目标:界面像一款原生 Mac 工具——安静、克制、内容优先。

哲学(对标 shadcn/ui 之于 React):不引入第三方组件库,自持一层薄组件层(ui/components/)——Flet 生态的组件库(fletmint / fluentflet 等)均不成熟且锁版本,用自己的 200 行换长期可控。规范 = token(ui/theme.py)+ 组件约定(ui/components/)+ 页面结构(ui/pages/)。

1. 定位与红线

  • UI 是 daemon 的遥控器:只读写数据库与触发导出,不承载监控/解析逻辑(AGENTS 红线 12)。数据入口统一走 core/queries.py 的只读查询,页面不直接写 SQLAlchemy 查询。
  • 凭据(API Key、邮箱授权码、加签密钥)永不展示明文,设置页只显示"已配置/未配置"(红线 5)。
  • 工作区入口:侧栏仪表盘导航项右侧的文件夹图标,点击用系统文件管理器打开工作区根目录,失败走 SnackBar。
  • 凭据配置:页头状态 chip(圆点 + API 已配置/未配置,components/credentials_chip.py),点击弹出 AlertDialog——表单 password + can_reveal_password,已配置项以掩码回显(前 4 位 + ****)证明存在性,保存时输入值等于掩码视为未修改、不写回钥匙串,保存后关弹窗并 SnackBar 确认。不单独设设置页。
  • 金额展示层格式化:分 → ¥x,xxx.xx,一律在 UI 边界除以 100(红线 1)。

2. 设计原则(Apple HIG 摘要)

  1. 内容优先: chrome(侧栏、页头)退后,数据区占主体;无大标题横幅、无 hero 区。
  2. 克制色彩:全 app 只有一个强调色 ACCENT(Apple Blue),只用于当前选中、主操作、进行中状态。语义色(成功/警告/危险/紫)只出现在状态徽章上,不做装饰。
  3. 发丝线代替阴影:层级靠 HAIRLINE 1px 描边 + 留白表达,禁用投影(ft.Shadow / elevation)。
  4. 层级靠字重与灰度:标题 bold → 正文 regular → 次要信息 TEXT_2,不靠彩色字区分。
  5. 4 的倍数间距阶梯:所有 padding/spacing/gap 取自 SPACE,禁止裸像素。
  6. 圆角双档:卡片 12、交互控件 8,不出现第三种圆角。

3. 设计 Token(唯一来源:ui/theme.py)

页面与组件禁止裸色值/裸像素,一律 import token。等价于 tailwind.config + globals.css 的角色。

3.1 色彩(浅色)

Token 值 用途
BG #F5F5F7 窗口底色(apple.com 浅灰)
SURFACE #FFFFFF 卡片、侧栏
TEXT #1D1D1F 主文本
TEXT_2 #86868B 次要文本、未选中导航
HAIRLINE #E5E5EA 发丝分隔线、卡片描边
SELECTED #EDEDF0 侧栏选中底色、中性徽章底
ACCENT / ACCENT_BG #007AFF / #E5F1FF 唯一强调色 / 其浅底
SUCCESS / SUCCESS_BG #34C759 / #E9F8EF 已报销
WARNING / WARNING_BG #FF9500 / #FFF3E1 失败、重复、未配置
DANGER / DANGER_BG #FF3B30 / #FFEBEA 出错、破坏性操作
PURPLE / PURPLE_BG #AF52C5 / #F5EEF9 已入批(区别于 ACCENT 的"进行中")

3.2 间距 / 圆角 / 字号

Token 值 说明
SPACE xs=4 sm=8 md=12 lg=16 xl=24 2xl=32 4px 阶梯
RADIUS control=8 card=12 控件 / 卡片
FONT caption=12 body=13 headline=17 title=22 large=28 五档字号
WEIGHT medium(W_500) semibold(W_600) bold(W_700) 三档字重

全局主题由 apply_theme(page) 一次装配(浅色 + ColorScheme 映射),窗口基准 1080×720。

4. 布局与导航

macOS「源列表」式双栏(ui/app.py):

┌───────────┬────────────────────────────────────────┐
│ 侧栏 208  │ 页头:待报销 ¥xx  已解析 xx 张  未匹配 xx │  ← 侧栏白底、右发丝线;
│           │       …… [API●] [刷新]                 │     页头无底色,指标随刷新重读
│ 报销助手   ├────────────────────────────────────────┤
│ 仪表盘 ●📂│   内容区(BG 浅灰底)                     │
└───────────┴────────────────────────────────────────┘
  • 页头左侧三指标:label caption TEXT_2 + 数值 headline bold;仅待报销金额用 ACCENT,其余 TEXT。
  • 侧栏仪表盘项右侧的 📂 为工作区入口(IconButton 不参与选中态着色,点击不触发导航)。
  • 侧栏选中态:TEXT 8% 透明底 + ACCENT 图标(macOS 源列表行为),不用填充色块。
  • 导航注册表 _NAV: [(icon, 页名, builder)],builder 统一签名 (engine, config) -> ft.Control;新增页面 = 在 ui/pages/ 写 build() + _NAV 加一行。
  • 刷新按钮语义:重新读库(重建当前页控件),不是重载窗口。

5. 组件约定(ui/components/)

每个组件一个文件、一个工厂函数、零外部依赖,只 import theme token。

组件 签名 规则
StatusChip (status) 胶囊徽章(radius 100、caption 字号);色由状态映射表决定(§6),不得手动传色
EmptyState (icon, title, hint) 空列表占位;大图标 + 一句话标题 + 一句引导,居中
drop_zone (on_files) 虚线投递框:点击打开系统文件选择器(pdf/jpg/jpeg/png),选中即回调 (page, files) 由页面落地 Inbox。虚线四边由 Canvas 按实际尺寸自绘(Flet 无 dash 属性);Flet 1.0 的 DragTarget 仅支持应用内控件拖放、不接收系统文件拖入,故投递方式是点击而非拖拽。内容居中用 Stack(fit=EXPAND) + Column(mainAxisAlignment=CENTER)

DataTable 规则:表头 caption 字号 TEXT_2 无底色;行高 40;金额列右对齐 ¥ 千分位格式化;状态列一律 StatusChip;空值显示 —(不用"无"/空串)。

明细列表规则(仪表盘明细区 ui/pages/invoices.py,固定高度内嵌、列表内滚): - 单栏列表(不做 Master-Detail 预览窗格):行是两行式(主体标题 + caption 元信息),不用 DataTable。 - 行主体 = 物品名称(goods_first,多物品显示"首项 等 N 项",无商品行回退销售方);元信息行 = 日期 · 分类 · 附件计数;金额右对齐 + StatusChip 收尾。 - 点击行 → 展开物品明细与附件(SELECTED 底色区,展开时才查询——lazy);展开数据来自 core/goods.py 重建与 attachment 表,UI 不解析 raw json。 - 行内元信息用 📎 图标 + 计数(附件)表达存在性,零值不显示(极简)。 - 原件查看走系统文件管理器(归档目录在设置页可点开),不为预览引入图片/PDF 渲染依赖。

6. 状态色映射(StatusChip 唯一出处)

status 文案 前景 / 底色 语义
parsed 已解析 ACCENT / ACCENT_BG 进行中(待归档确认)
archived 已归档 TEXT_2 / SELECTED 中性稳态
in_batch 已入批 PURPLE / PURPLE_BG 进入报销流程
reimbursed 已报销 SUCCESS / SUCCESS_BG 终态·闭环
failed 失败 WARNING / WARNING_BG 可重试
duplicate 重复 WARNING / WARNING_BG 防重拦截,非错误
error 出错 WARNING / DANGER_BG 需人工介入

新增状态必须先在此表登记再使用;页头指标仅待报销金额用 ACCENT、其余用 TEXT,不得自造颜色。

7. 文案与交互

  • 全中文、简体;空态文案 = 标题(发生了什么)+ 提示(接下来做什么),如"还没有发票记录 / 拖入 Inbox 的发票解析成功后会出现在这里"。
  • 数字格式:金额 ¥1,234.56,计数直接阿拉伯数字;0 值也如实展示。
  • 破坏性操作(删除附件、拆批)配 DANGER 色 + 二次确认;其余操作不做确认弹窗(极简)。
  • 长路径/长名称:max_lines=1 截断或 selectable=True 复制,不换行撑破布局。

8. Flet 1.0 已知坑(踩过必记)

  • Row 禁用 CrossAxisAlignment.BASELINE:Flet 协议不传 text_baseline,Flutter 端 baseline 对齐缺 textBaseline 会导致整棵子树静默不渲染(无 console 报错)。需要底对齐视觉时用 END 近似。
  • ft.Center 不存在:居中用 Column(mainAxisAlignment=CENTER)(配合 Stack(fit=EXPAND) 做铺满容器内的居中)。
  • DragTarget 只收应用内拖放:系统文件拖入不支持,投递一律点击选择。
  • 事件处理器类型注解精确到控件类型(ft.Event[ft.IconButton]),mypy 才通过。

9. 暗色模式

v1 固定浅色(ThemeMode.LIGHT)。token 已按"前景/底色成对"组织,M2 扩展为双套 token + ThemeMode.SYSTEM 跟随系统;届时只需在 theme.py 内切换色值,组件层零改动。现在不要在页面里出现任何只考虑浅色的裸色值,就是为这一步留的余地。

10. 代码结构

smart_invoice/ui/
├── theme.py        # ★ 全部设计 token + apply_theme(规范 §3 的代码化身)
├── components/     # 薄组件层:status_chip / empty_state / credentials_chip / drop_zone
├── pages/          # 每页一个 build() -> ft.Control,数据来自 core/queries.py
└── app.py          # 壳:侧栏 + 页头 + 路由(_NAV 注册表)

改动纪律:调色彩/间距/字号 → 只改 theme.py;新状态/新徽章 → 只改 status_chip.py 映射表;新组件先进 components/ 被两处以上复用后才算立住。