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 摘要)¶
- 内容优先: chrome(侧栏、页头)退后,数据区占主体;无大标题横幅、无 hero 区。
- 克制色彩:全 app 只有一个强调色
ACCENT(Apple Blue),只用于当前选中、主操作、进行中状态。语义色(成功/警告/危险/紫)只出现在状态徽章上,不做装饰。 - 发丝线代替阴影:层级靠
HAIRLINE1px 描边 + 留白表达,禁用投影(ft.Shadow/ elevation)。 - 层级靠字重与灰度:标题 bold → 正文 regular → 次要信息
TEXT_2,不靠彩色字区分。 - 4 的倍数间距阶梯:所有 padding/spacing/gap 取自
SPACE,禁止裸像素。 - 圆角双档:卡片
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/ 被两处以上复用后才算立住。