状态:规范第一批 + 第二批的合并稿,与
iosApp/iosApp当前实现对齐。 本文件是 iOS 端唯一的设计事实来源:颜色、字体、间距、导航、逐屏层级、状态、无障碍。 代码与本文件不一致时,代码是偏差——但第一次构建(本轮)不改界面,偏差集中列在文末第 12 节,等 app 在模拟器里跑起来后按优先级排。 文案不在本文件里定义:所有用户可见文字来自Copy.swift(Camo 的文案稿),本文件只规定它们出现在哪里、用多大的字、什么颜色。
一句话:把每天的钱账做成一张干净的纸——近黑与白,靠排版的轻重和留白分层次,颜色只在非它不可的地方出现。
三条原则,按优先级:
- 层次靠排版,不靠颜色。 一屏里只允许一个视觉主角(今天剩多少),其余全部退到次级灰。不用渐变、不用彩色卡片、不用阴影堆叠。
- 颜色只有四份工作,多一份都不用:
- 近黑:正文、选中、主按钮。
- 绿:钱进来(收入、退款)。
- 红:出问题(超支、错误、不可逆操作)。
- 紫:只标记「一句话记账」这一个入口,别处不出现。
- 状态要诚实。 没有预算就是「还没设预算」,不是 0;没有账单就是「还没有账单」,不是留白;金额缺失就是破折号,不是猜一个数。
情绪词:安静、克制、可以信任。不做「记账软件」的账本感(没有横线格、没有印章、没有卡通吉祥物),也不做金融 App 的冷硬(不用蓝、不用数字仪表盘)。
参考对象:Apple 的「钱包 / 健康 / 健身」三套 App 的留白与数字排版;不动点在于我们把「剩多少」放大到接近海报的比例,别处一律压平。
全部走 PaceColor(PaceStyle.swift),浅色/深色两套,代码里不写死十六进制。
| 语义 token | 浅色 | 深色 | 用在哪 | 绝不用在哪 |
|---|---|---|---|---|
canvas |
#F7F7F7 |
#0C0C0D |
屏幕底色 | 卡片内部 |
card |
#FFFFFF |
#1A1A1C |
实底卡片 | 页面底色 |
card2 |
#F2F2F4 |
#242426 |
卡片里的次级块(统计小格、未选中的胶囊、安全提示) | 整屏背景 |
ink |
#0E0E0F |
#F5F5F7 |
正文、标题、主数字 | 大面积填充 |
ink2 |
#6E6E73 |
#A6A6AD |
次级文字、说明、副标题 | 主数字 |
ink3 |
#8E8E93 |
#7A7A80 |
只用于禁用态、占位符、装饰性破折号 | 任何需要读的正文 |
line |
黑 10% | 白 10% | 0.5pt 卡片描边、1pt 分隔线、进度条底 | 文字 |
accent |
#1C1C1E |
#F2F2F4 |
主按钮底、进度条已用段、选中胶囊 | 文字 |
onAccent |
#FFFFFF |
#0C0C0D |
压在 accent 上的字和图标 | 别的底 |
income |
#12805C |
#4ED4A0 |
收入金额、退款金额 | 成功提示、测试通过 |
danger |
#C0392B |
#FF8578 |
超支金额、错误、删除类操作 | 普通强调 |
ai |
#5B3FD9 |
#B9A6FF |
「一句话记账」入口的字与图标 | 其他任何地方 |
aiSoft |
ai 12% | ai 12% | 上述入口的胶囊底 | 普通卡片 |
softFill |
黑 6% | 白 10% | 状态胶囊底、列表图标圆底 | 主按钮 |
实际对比度(按 WCAG 公式算的,不是估的;正文要求 ≥ 4.5:1,大字 ≥ 3:1):
| 组合 | 浅色 | 深色 | 结论 |
|---|---|---|---|
| ink / canvas | 18.0 | 18.0 | 通过 |
| ink2 / canvas | 4.73 | 8.08 | 通过(浅色余量薄) |
| ink2 / card | 5.07 | 7.18 | 通过 |
| ink2 / card2 | 4.54 | 6.40 | 勉强通过 |
| ink3 / card | 3.26 | 4.07 | 只允许装饰与禁用 |
| ink3 / card2 | 2.92 | 3.63 | 不允许放文字 |
| ink3 / aiSoft | 2.71 | — | 不允许放文字 |
| income / card | 4.92 | 9.31 | 通过 |
| income / card2 | 4.40 | 8.30 | 浅色不足 4.5,见 §12-3 |
| danger / card | 5.44 | 7.35 | 通过 |
| danger / card2 | 4.86 | 6.55 | 通过 |
| ai / aiSoft | 5.53 | — | 通过 |
| onAccent / accent | 17.0 | 17.5 | 通过 |
| canvas / card | 1.07 | 1.13 | 边界必须靠 0.5pt line 描边(明度差不足以分界) |
推论(必须遵守):卡片与底色之间永远画 0.5pt line 描边,这是深色模式唯一能看清卡片边界的手段;正文不用 ink3;income 放在 card2 上时先算对比度再放。
系统字体(SF / SF Rounded),不引第三方字体,不打包字体文件。
两套数字,一套文字:
- 金额一律
.rounded+.monospacedDigit():圆角数字更友善,等宽保证金额变化时不跳动。 - 文字一律默认 SF(不圆角):圆角只属于钱。
- 中文用系统默认(PingFang SC),不指定字重以外的字形。
| 角色 | 写法 | 用在哪 |
|---|---|---|
| 主数字(今天还剩) | .system(size: 56, weight: .heavy, design: .rounded),配 @ScaledMetric(relativeTo: .largeTitle),lineLimit(1),minimumScaleFactor(0.6) |
今天页预算卡 |
| 输入金额 | .system(size: 48, weight: .heavy, design: .rounded) + monospacedDigit,minScale 0.6 |
记一笔·金额 |
| 次级大数字 | .system(size: 38, weight: .heavy, design: .rounded) |
统计·汇总卡 |
| 列表金额 | .system(.subheadline, design: .rounded).weight(.semibold) + monospacedDigit,minScale 0.8 |
账单行、今天清单行 |
| 小格数字 | .system(size: 15, weight: .bold) |
统计四格 |
| 卡片标题 | .headline(17 semibold,跟随动态字体) |
今天·今日账单、统计两张卡 |
| 行标题 | .subheadline.weight(.medium) |
账单行、今天清单行 |
| 正文字号 | .system(size: 15, weight: .medium) |
表单行标签、设置里的值 |
| 字段标签 | .system(size: 12, weight: .semibold),ink2 |
记一笔各卡的字段名 |
| 说明 / 副标题 | .footnote 或 .system(size: 13),ink2 |
预算说明、空态副标题、统计脚注 |
| 最小字号 | .system(size: 11),ink2 |
只用于脚注性质的一行(版本号、公式说明) |
| 状态胶囊 | .caption.weight(.semibold),图标 11 semibold |
在预算内 / 已超支 / 还没设预算 |
| 按钮 | 主按钮 .system(size: 16, weight: .semibold);次按钮 .system(size: 15, weight: .medium);胶囊 14 medium |
全局 |
动态字体规则:正文、说明、卡片标题走语义样式(.headline .subheadline .footnote .caption);所有写死 size: 的位置只允许是「海报数字」和紧凑控件,且海报数字必须用 @ScaledMetric 起一个基准值。文字不得小于 11pt,11pt 不得承载用户必须读的信息。
行高与截断:列表行标题 / 副标题 lineLimit(1);金额 lineLimit(1) + minimumScaleFactor;说明段落 fixedSize(horizontal: false, vertical: true),不截断。
只有这几个值,新页面不许自创(PaceStyle.swift 里叫 PaceSpace / PaceRadius):
| token | 值 | 用途 |
|---|---|---|
screen |
20 | 屏幕左右边距 |
cardPadding |
20 | 普通卡片内边距 |
heroPadding |
22 | 主角卡片(今天预算卡)内边距 |
cardGap |
16 | 卡片与卡片之间 |
inner |
12 | 卡片内部块与块之间 |
| 圆角 token | 值 | 用途 |
|---|---|---|
hero |
30 | 今天预算卡 |
card |
26 | 内容卡片 |
list |
22 | 按天分组的账单卡、日期/账户两行卡 |
block |
14 | 卡片里的小格、主按钮、输入块 |
补充固定值:进度条高 8;列表图标圆 34;行最小高 44(实际内容行约 54);主按钮高 50;工具栏胶囊按钮高 34;「记一笔」高 44;撤销浮条最小高 48。
圆角必须 .continuous(不是默认的圆形圆角),这是全 App 看起来「像一块被磨过的塑料」而不是「像网页」的关键。
| 组件 | 规格 | 用在哪 |
|---|---|---|
实底卡片 .paceCard(radius:padding:) |
填充 card + 0.5pt line 描边 + 连续圆角;没有阴影 |
除账单按天卡外的所有卡片 |
| 主角卡 | .paceCard(radius: 30, padding: 22) |
今天页预算卡 |
| 按天账单卡 | UnevenRoundedRectangle 只给首行/末行上圆角,list 半径,填充 card;行间 1pt line 分隔,左侧缩进 66 = 20 + 46 |
账单页 |
状态胶囊 StatusPill |
.caption.semibold,图标 11,左右 10 / 上下 5,胶囊底;在预算内 = ink/softFill + checkmark.seal.fill;已超支 = danger/dangerSoft + exclamationmark.triangle.fill;没预算 = ink/softFill + gearshape |
今天页右上 |
进度条 PaceBar |
高 8,胶囊,底 line,已用 accent,超支 danger;比例夹在 0…1 |
今天页、统计分类/每日 |
主按钮 PrimaryButtonStyle |
满宽,高 50,圆角 14,accent 底 onAccent 字;按下 0.85 透明度 + 0.97 缩放,spring 0.25/0.8 |
内容区主操作(设置预算、测试连接、同意并开启) |
次按钮 PlainButtonStylePace |
满宽,高 44,纯文字 ink2 15 medium;按下 0.6 |
「先不用」「改成手动记账」 |
工具栏胶囊 SaveToolbarButtonStyle |
高 34,圆角胶囊,左右 14;可用 = accent/onAccent,不可用 = card2/ink3 |
记一笔·保存 |
分类胶囊 PillChipStyle |
14 medium,左右 14 / 上下 7,最小高 44,胶囊;选中 accent 底 onAccent 字,未选 card2 底 ink 字;按下 0.8 |
记一笔·分类、草稿·改/删除 |
| 列表行 | 图标圆 34(softFill 底 + 15 semibold 图标)+ 标题/副标题 + 右侧金额;行内间距 12,上下 8,最小高 44;行间 1pt line,左侧缩进 46 |
今天页、账单页 |
| 浮起「记一笔」 | 玻璃胶囊,高 44,plus 15 semibold + 「记一笔」15 semibold,onAccent 字,accent 底;距右 20、距底 99;向下滚过 24pt 淡出并缩到 0.92、下移 8,停手 0.4s 或回滚 24pt 复原 |
今天页、账单页 |
| 撤销浮条 | 胶囊,最小高 48,左右 16,card 底 + line 描边;文字 14 semibold ink,「撤销」14 semibold;出现 spring 0.35/0.9,停留 8s(「已恢复」2s) |
账单页删除后 |
| 骨架 | card 填充 + redacted(.placeholder);只有在读取超过 300ms 时才出现,之前留空 |
今天 / 账单 / 统计 |
| 顶部 toast | 胶囊,card 底,14 semibold ink,左右 16 / 上下 10,距顶 8,阴影黑 12% r12 y6 |
草稿保存、设置里关掉开关 |
禁止:渐变、彩色阴影、玻璃叠玻璃、描边高光、装饰性圆点或徽章。全 App 唯一允许的玻璃质感是浮起的「记一笔」和账单撤销浮条——它们浮在内容之上,需要和底下的东西区分。
- 结构:
TabView四个 tab——今天(house.fill)、账单(list.bullet)、统计(chart.bar.fill)、设置(gearshape)。每个 tab 自带NavigationStack。tab 栏随下滑收起(tabBarMinimizeBehavior(.onScrollDown))。 - 标题:今天页 inline 标题
Pace(全 App 唯一出现名字的地方);账单、统计、设置用大标题;所有弹层用 inline 标题。 - 主操作只有一个:浮起的「记一笔」。只在今天、账单两屏出现;统计和设置没有;任何弹层打开时消失。
- 弹层:
- 记一笔 / 编辑账单:sheet,
.large,左上xmark关闭,右上「保存」。 - 一句话记账:sheet,
.large,显示拖拽指示条,没有关闭按钮(下滑关闭,或走「改成手动记账」)。 - 草稿确认:从一句话记账 push 进来;也可从别处 sheet 呈现。
- 隐私说明:sheet,
.large,首开一次。 - 恢复预览:sheet。
- 记一笔 / 编辑账单:sheet,
- 返回:系统返回手势,不改。所有二级页(设置→一句话记账)进入后隐藏 tab 栏。
- 删除:账单行左滑删除(禁止全滑直接删),删完给撤销浮条。
- 搜索:只在账单页,
.searchable,占位「搜索备注、分类或账户」;搜索跨全部时间范围,不受范围胶囊约束(这一点必须在 UI 上让用户看得出来,见 §12-21)。 - 动效:只用来交代变化。卡片入场 8pt 上移 + 淡入,spring 0.38/0.86;数字变化用
.numericText();滚动收起「记一笔」0.22 easeOut。开启「减弱动态效果」时,全部换成 0.15 秒淡入,不做位移与缩放。
目的:三秒内回答「今天还能花多少」,顺带给今天的账一个落点。
层级(从上到下)
-
预算卡(主角,
hero圆角 / 22 内边距)- 第一行:左边
今天 · 10月26日(footnote,ink2),右边状态胶囊(在预算内 / 已超支 / 还没设预算)。- 这一行用
ViewThatFits:放得下就并排;放不下就分两行,日期一行、胶囊一行,日期永远不换行、不截、不缩小。minimumScaleFactor在这里禁用——字号是用户选的,缩字等于在他最需要看清的时候把字变小,而且会让人以为胶囊比日期重要。两行也放不下时才换短写法10/26,「今天」不能丢。 - 断行示例(accessibility-large 实测,2026-10-26):
今天 · 10月/26日,就是没约束的Text被胶囊挤出来的。
- 这一行用
- 第二行:标签「今天还剩」(subheadline semibold,
ink2)——超支时换成「今天超支」;下面主数字 56 heavy rounded,前缀货币符号,去掉负号,只显示绝对值。 - 进度条:已用比例 = 计入预算的净支出 ÷ 预算,夹在 0…1;超支转红。
- 最后一行:
预算 ¥200.00 · 已花 ¥68.00(footnote,ink2)。 - 没有预算时:主数字位置放一个 44pt heavy 的破折号(
ink3,非圆角字体),下面一行「设一个每日预算,Pace 就开始算你还剩多少。」(footnote,ink2),再加一颗满宽主按钮「设置预算」,点了跳设置页。不显示 0,不显示空进度条。 - 整张卡对 VoiceOver 是一个元素:值读「今天还剩 132 元,预算 200 元,已花 68 元」,进度读「已用 34%」。
- 第一行:左边
-
今日账单卡(
card圆角 / 20 内边距)- 标题行:左「今天」(headline,
ink),右「N 笔」(footnote,ink2,0 笔时不显示数字)。 - 行:图标圆 34(分类图标,
softFill底,15 semibold,ink)→ 标题(有备注用备注,否则用分类名,subheadline medium)→ 副标题(分类(标题已是分类名时省略)+ 时间 + 账户 + 「不计入预算」,footnote,ink2,中间用「 · 」)→ 右侧金额(列表金额字体,收入/退款绿,其余ink)。 - 行间 1pt
line分隔,左侧缩进 46。 - 空态:居中两行——「还没有账单」(subheadline semibold)+「点下面的「记一笔」,或者打一句话。」(footnote,
ink2),上下各 24。 - 每行对 VoiceOver 是一个元素:「午饭,餐饮,支出 32 元」。
- 标题行:左「今天」(headline,
状态:前 300ms 不闪骨架;300ms 后出两张骨架卡;读完后 spring 入场。读取失败时保留上一次的数据,不显示错误页(本地库读取,失败即视为空账本,见 §12-27)。
目的:按时间翻账,改一笔账,删一笔账。
层级
- 范围胶囊(本周 / 本月 / 全部),segmented,固定在列表上方不随内容滚动,左右 20、上 4、下 12;无障碍标签「范围」。
- 列表(plain,露出
canvas):- 日期头:「10月26日 · 周日」(12 semibold,
ink2,作为 header trait)+ 右侧当日净额(12,monospacedDigit,ink2,支出为负、收入/退款为正、转账不计);上 16 / 下 6。 - 当日账单卡:一张连续卡片,只有首行上圆角、末行下圆角(
list半径),填充card,必须有 0.5pt 描边。 - 行:图标圆 34 → 标题 / 副标题 → 金额,规则同今天页;副标题里带「不计入预算」。
- 行间 1pt
line,左侧缩进 66。 - 左滑「删除」(红色,
trash,禁止全滑直删)。
- 日期头:「10月26日 · 周日」(12 semibold,
- 空态:居中「这段时间还没有账单。」+「换个范围看看,或者记一笔。」;搜索无结果时换成「没找到相关的账单。」+「换个词试试。」。
- 撤销浮条:删除后从底部浮出,「已删除「午饭」」+「撤销」,8 秒后自动消失;点撤销恢复并显示「已恢复」(2 秒,无按钮)。
状态:骨架 3 行(同卡片形状);空态;搜索无结果;删除中;撤销成功/失败;金额缺失不适用(账单必有金额)。
目的:这一周 / 这个月钱去哪了。
层级
- 范围 segmented(本周 / 本月),跟随内容滚动(与账单页的不同,见 §12-8);无障碍标签「范围」。
- 汇总卡(
card):日期区间(11,ink2)→「支出」15 semiboldink2+ 汇总金额 38 heavy rounded → 2×2 小格(card2底,block圆角,12/10 内边距):计入预算 / 退款抵掉 / 收入 / 超支 N 天;小格标签 11ink2,数值 15 bold(收入绿、超支红)。 - 花在哪了(
card):标题 headline → 最多 6 行「分类 + 比例条 + 金额 · 占比」(分类 15ink,金额 13 monospacedink2),超出显示「还有 N 个分类」(13,ink2)。 - 每天花了多少(
card):标题 headline → 每行「日期 44 宽 + 比例条 + 金额 128 宽」(日期 12,超支那天用红;金额 11 monospacedink2,有预算时显示「已花 / 预算」)→ 脚注「每天预算从生效日起算;今天还剩 = 预算 − 计入预算的支出 + 退款。」(11,ink2)。
状态:骨架(一张汇总形状的卡 + 两行);两张卡各自的空态文案「这段时间没有支出。」(只显示一次,见 §12-13);超支日红;没预算时不显示「已花 / 预算」只显示已花。
层级(从上到下,间距 16,左右 20,上 8,下 32)
- 一句话记账入口(仅新建):胶囊,高 44,
aiSoft底;sparkles14 + 「一句话记账」14 semibold(ai)+ 右侧示例「昨天午饭 28」13。 - 金额卡(
card圆角 / 20 内边距):字段标签「金额」12 semiboldink2→¥28 semiboldink2+ 输入 48 heavy rounded(数字键盘);错误信息 13danger出现在这张卡底部。 - 类型卡:segmented 支出 / 收入 / 转账 / 退款。
- 分类卡:字段标签「分类」+ 横向滚动的分类胶囊(选中实底)。
- 日期与账户卡(
list圆角,行高 52):日期行(今天 / 昨天 / 日期 +chevron.up.chevron.down,点整行出系统日期选择器)→ 1pt 分隔线 → 账户行(现金 / 微信 / 支付宝 / 银行卡 / 自定义…)。 - 备注卡:
备注标签 + 文本输入,占位「例如:午饭」。 - 计入预算卡(仅支出与退款):Toggle 15 medium + 说明「关闭后仍计入总支出统计」13
ink2。 - 退款关联卡(仅退款):字段标签「这笔退的是哪一笔」+ 选择原账单;没有候选时提示「还没有可以关联的支出账单。」(13,
ink2)。
状态:新建 / 编辑(编辑时不显示一句话入口,标题变「编辑账单」);保存不可用(金额空或非法)时工具栏按钮 card2 底 ink3 字;保存错误显示在金额卡里;重复账单弹确认框;保存成功直接收起,不弹提示。
目的:打一句话,出几笔待确认的草稿。
层级:输入卡(card 圆角,18 内边距)→ 例句胶囊(点一下填入)→ 说明一行(按开关状态换三种文案)→ 主按钮「整理成草稿」/「正在整理…」→ 次按钮「改成手动记账」→ 失败卡(标题 + 说明 + 两个按钮,按失败类型换动作)。
状态:本机规则 / 你的服务(状态小字);整理中(按钮 spinner,禁点);成功(push 到草稿确认);失败(失败卡,主按钮按类型:重试远端 / 用本机规则 / 改成手动记账);退出到手动记账时把原句带进备注。
目的:AI 拆出来的每一笔,用户看过、补齐、确认后才进账本。
层级:标题「草稿 N 笔」+ 右上「保存 N 笔 / 先补金额」→ 头部两行(左「确认后才进账本」13 semibold,右「逐笔可改」13 ink2;缺字段时补一行「还有 N 笔要补。」)→ 每笔一张草稿卡(16 内边距):标题 16 semibold + 金额 16 semibold rounded(缺失显示破折号)→ 元信息行 13 ink2 → 展开编辑(金额 / 日期 / 分类 / 备注)→ 收起时「改」「删除」两个胶囊 + 缺什么挂什么状态胶囊 → 底部固定脚注(12,ink2)。
状态:可保存 / 缺字段(按钮禁用,标题指明缺什么);保存中(按钮禁用);成功 toast「已保存 N 笔」后 1.4s 收起;重复账单(全部重复 / 部分重复)弹窗,「照样记」强制入账;空草稿不得出现「保存 0 笔」可点(§12-24)。
层级(Form 分组,系统行样式):
- 每日预算:当前预算一行 11
ink2(「现在是 ¥200.00,从 10 月 1 日开始」或「还没设预算」)→「每日预算」输入行(数字键盘)→「生效日期」行 →「保存预算」按钮(保存后右侧「已保存」12 出现 2 秒)→ 错误 12danger。 - 一句话记账:「现在用」+ 右侧「本机规则 / 你的服务」,点进二级页。
- 导出与恢复:导出表格(CSV)/ 导出数据文件(JSON)/ 从文件恢复…,底部说明导出文件由用户自行保管;导入完成后一行结果 12
ink2。 - 数据:清空整个账本…(系统红)。
- 版本脚注:
Pace 0.1.0 (1),11ink2,居中。
状态:有/没有预算;保存中(无);保存成功(「已保存」2 秒);保存失败(红字);导入预览(有效 / 校验失败);替换需二次确认;清空需二次确认。
层级:开关卡(card 圆角,16 内边距):「用自己填的模型」15 semibold + 说明 12 ink2 → 字段卡:接口地址 / 模型名 / Key(每项标签 15 ink,占位在字段里,下面 11 ink2 的说明)→ 主按钮「测试连接」/「正在连接…」→ 结果行(checkmark.circle.fill 绿 / exclamationmark.triangle.fill 红 / info.circle 灰,14 semibold)→ 安全提示卡(block 圆角,card2 底,13 semibold + 12 ink2)→「清空 Key」(15 semibold danger)。
状态:开关关 / 开;首次开启先过隐私说明;测试中(spinner,按钮禁用);四种结果;清空 Key 二次确认;保存即时生效,无保存按钮。
层级:标题 26 bold ink(「开启前,先说清楚一件事」)→ 四条 checkmark 12 bold ink2 + 15 ink 说明 → 脚注 12 ink2 → 主按钮「同意并开启」→ 次按钮「先不用」→ 底部提示 11 ink2(不是 ink3,见 §12-3)。
状态:只在第一次开启时出现;同意 = 记下已读 + 打开开关;拒绝 = 开关保持关闭;下滑关闭等同没选(下次再问)。
层级:大标题「恢复预览」+ 左上「取消」→ 有效时三行(账单 N 笔(起 ~ 止)/ 分类 N · 预算规则 N / 其中 N 笔账本里已经有了)→ 两个操作「合并到现有账本」「替换当前账本…」(后者二次确认)→ 说明;校验失败时改成分组头「文件校验未通过,不会写入任何数据」+ 逐条错误 .footnote。
| 状态 | 规则 |
|---|---|
| 加载 | 本地读取通常 < 80ms:先留空让第一帧画出来,超过 300ms 才出骨架,避免闪一下。骨架用卡片形状 + redacted,不用转圈。 |
| 空 | 居中,两行:一行说「没有什么」(subheadline semibold),一行说「接下来能做什么」(footnote ink2)。空态不给按钮(主操作已经是浮起的「记一笔」)。 |
| 错误 | 就地显示,挨着出错的那个控件:红字 12–13,只有一行,说清楚「哪错了、怎么改」。不弹全屏错误页、不用 toast 代替(toast 只用于成功与瞬时结果)。 |
| 成功 | 能看得出结果的地方就别提示(记一笔保存后直接收起);需要确认的用 toast 或 2 秒的小字(「已保存」)。成功不用绿色,绿只代表钱进来。 |
| 超支 | 数字与进度条转 danger,状态胶囊变「已超支」,不改版式、不加弹窗。 |
| 不可用 | 主按钮 card2 底 ink3 字,可点但无效果不接受(禁用态不算对比度不合格)。 |
- 每个列表行是一个 VoiceOver 元素,
accessibilityLabel顺序读「标题,分类,类型 金额」,金额读成「32 元」「12 元 5 角」「12 元 5 角 3 分」,不读「¥32.00」。 - 主角卡整体一个元素:label「今天还剩 132 元,预算 200 元,已花 68 元」,value「已用 34%」。
- 进度条单独给
accessibilityValue(「已用 N%」),不要把视觉条当装饰藏掉。 - 状态胶囊、错误文案、toast、保存结果是动态更新的信息:加
.updatesFrequently或用.accessibilityLiveRegion(.polite)(iOS 26)让 VoiceOver 主动读。 - 装饰元素必须隐藏:图标、分隔线、
checkmark项目符号、骨架、破折号占位(accessibilityHidden(true))。 - 点击区域 ≥ 44×44:文字按钮(撤销、清空 Key、改/删除胶囊)必须补
frame(minHeight: 44)或contentShape。 - 对比度:正文 ≥ 4.5:1,大字(≥ 24pt 或 ≥ 18.5pt bold)≥ 3:1;
ink3不得承载正文(见 §2 实测表)。 - 减弱动态效果:位移、缩放、数字滚动全部退化成 0.15s 淡入。
- 深色模式:每个页面都要单独看一遍;卡片边界靠 0.5pt
line,不能靠阴影。 - 全屏截图:账单、统计两张长页面在 VoiceOver 下必须能顺序读完,不能有读不到的卡片标题(值得加
.isHeader)。
只用系统符号,不引图形资源。
| 用途 | 符号 |
|---|---|
| tab | house.fill、list.bullet、chart.bar.fill、gearshape |
| 主操作 | plus |
| 状态 | checkmark.seal.fill(在预算内)、exclamationmark.triangle.fill(超支/出错)、gearshape(没预算)、info.circle(待补全) |
| 分类 | fork.knife 餐饮、car.fill 交通、bag.fill 购物、house.fill 住房、party.popper.fill 娱乐、cross.case.fill 医疗、book.fill 教育、gift.fill 人情社交、banknote.fill 工资、chart.line.uptrend.xyaxis 理财;未识别分类 circle.fill |
| 类型 | arrow.down.left 收入、arrow.uturn.backward 退款、arrow.left.arrow.right 转账 |
| 控件 | xmark(关闭)、sparkles(一句话记账)、chevron.up.chevron.down(日期/账户/原账单)、checkmark(选中)、trash(删除) |
符号字重跟文字一致(正文旁 15 semibold,胶囊里 11 semibold),不自成一套粗细。
不需要任何外部素材:图标全用 SF Symbols,颜色全用 token,字体用系统字体。已就位的是 App Icon(三行账,含深色与 tinted 变体)。新增图形前先问:能不能用排版或一个系统符号解决。
本轮(第一次构建)不改界面。以下按「用户能不能感知」排序,等 app 在模拟器里跑起来后一起排优先级。P0 = 影响读数正确性或可读性;P1 = 一致性/无障碍缺陷;P2 = 打磨。带「偏好」的是我的判断,不是缺陷。
PaceColor.income放在card2上只有 4.40:1(统计汇总卡「收入」数值,StatsView.swift:229)。浅色下 15pt bold 不算大字,差 0.1 不过 AA。改法:浅色income从#12805C调到#107050(对card25.44:1,对card6.08:1),深色不动。ink3被当成正文用(三处,全部 < 4.5:1):记一笔里「账户」空占位 15pt(AddView.swift:344,3.26:1)、「选择原账单」占位(:423)、一句话入口里的示例「昨天午饭 28」13pt 压在aiSoft上(:179,2.71:1)、隐私说明底部那行 11pt(PrivacyNoteView.swift:62,3.04:1)。占位符也是要读的字:改用ink2;禁用态才允许ink3。- 失败路径没有任何提示:账单页撤销失败静默返回(
BillsView.swift:440-467)、草稿保存的非重复失败被吞掉(DraftReviewView.swift:277-287)、导出写文件失败try?吞掉(SettingsView.swift)。用户会以为记上了。三处都要给一行红字。 - 草稿确认可以空着保存(
DraftReviewView.swift:260):0 笔草稿时按钮是「保存 0 笔」且可点,点完弹「已保存 0 笔」。空列表不该进得去,进去了也不该给保存。 - 「先补金额」在缺日期/分类时也说金额(
DraftReviewView.swift:98-99+Copy.swift:181):缺字段统一文案,按钮与实际缺的东西对不上。 - 草稿里缺「类型」永远补不上(
DraftReviewView.swift:203-239没有类型编辑):需要类型一旦出现,页面就卡死。
- 103 处固定字号 vs 19 处语义字号(9 个视图文件):动态字体开到最大,卡片里的字不跟着长。优先补:行标题、字段标签、说明、toast。
- 卡片描边不一致:账单页按天卡(
BillsView.swift:339-347)和统计骨架小格(StatsView.swift:171-174)没有 0.5ptline,深色下边界消失;其余卡片都有。 - 圆角有六种:30/26/22/20/18/14(
QuickAddView.swift:85,156、DraftReviewView.swift:177、QuickAddSettingsView.swift:266、BillsView.swift:614)。规范只有四种,20/18 要归到 22/26/14。 - 同一件事两种颜色结论:退款在账单页是绿(
BillsView.swift:528-531),在统计「退款抵掉」是ink(StatsView.swift:219-224);死代码TxnRowView里还是旧的青绿渐变(TxnUI.swift:90,GlassUI.swift的PaceGradients只剩这一处引用)。统一为绿,删掉TxnRowView与GlassUI.swift。 - 红色越界:「超支 0 天」也红(
StatsView.swift:231);保存成功、连接成功用绿(SettingsView.swift:155、QuickAddSettingsView.swift:231)。按 §2 收回:绿=钱进来,红=超支/错误/不可逆。 - 两个黑:进度条用
accent #1C1C1E,文字用ink #0E0E0F(PaceStyle.swift:26,30,160)。视觉上几乎无差,但两套 token 会导致后续改动漂移。选一个当「实底」,另一个只做文字。 - 统计两张卡的空态文案相同(
StatsView.swift:285-290、:343-348):零数据时同屏出现两遍「这段时间没有支出。」 - 统计底部留 100pt 是给已经不存在的系统附件条(
StatsView.swift:38-39注释与实现不符,该屏也没有「记一笔」)。 - 账单页范围胶囊在搜索时变成假控件:搜索跨全部时间,胶囊仍显示「本周」(
BillsView.swift:79,356-358)。搜索时隐藏或置灰胶囊,或让搜索遵守范围。 - 删除没有二次确认,左滑即删(
BillsView.swift:329-335),而Copy.Delete里备好了确认文案。在「撤销」已经兜底的前提下,可以保留即删;但全滑必须继续禁止。 - 撤销浮条与「记一笔」可能叠在一起:撤销条距底
safeArea+8,浮按钮距底 99、高 44(顶部在 143),列表底 inset 只有 76,最后一行会被浮按钮盖住(BillsView.swift:117,245;FloatingAddButton.swift:34)。列表底 inset 至少 96。 - 无障碍覆盖不均:草稿确认和隐私说明两个页面一条无障碍修饰都没有;
PrivacyNoteView.swift:25四个装饰checkmark会被读出来;全 App 没有一处accessibilityHint;toast、错误、「已保存」都没做动态朗读。 - 「清空 Key」是 12pt 纯文字(
QuickAddSettingsView.swift:147-150),点击区不足 44。 - 草稿确认出现双层导航:从一句话记账 push 进来时,它内部又套了一个
NavigationStack(DraftReviewView.swift:34+QuickAddView.swift:54)。 - 日期输入有三种交互(记一笔/设置用隐形的 compact picker、草稿页用系统 DatePicker),显示格式也有四种(
今天 · 10月26日/10月26日/10月26日 · 周日/10-26)。统一为:行内显示「今天 · 10月26日」/「10月26日」/「2025年10月26日」,点行出选择器。 - 系统玻璃控件混进设计系统:记一笔左上关闭按钮用
.glass(AddView.swift:118),草稿页右上保存用.glassProminent(DraftReviewView.swift:51),而记一笔的保存是自绘的扁胶囊。同一层级两个按钮两套外观。 - 设置页整套是系统 Form(
SettingsView.swift:29),行高、分组头、分隔线、圆角都不在设计系统里。这是本轮最大的版式不一致;偏好上我倾向保留 Form(系统表单在这里更好用),但那就要明确它是「系统表单区」,并把卡片区与表单区的边界做得干净。 - 文案散落在代码里:标题、账户预设、草稿页的「改」「删除」「改好了」、设置里的分组头、恢复预览的每一行都不在
Copy.swift(如AddView.swift:51,107,333,350、DraftReviewView.swift:144,155,164,237、SettingsView.swift:226,296,314-326)。文案改一遍要翻五个文件。
- 账单页日期头会随列表滚走(普通行,不吸顶)。偏好:吸顶更省事,但不吸顶更安静——我倾向维持现状。
- 统计范围胶囊跟着滚,账单范围胶囊固定。两屏行为不同,偏好上我倾向统一成「固定」,因为范围是这一屏的读法,不是内容的一部分。
- 本地库读取失败没有兜底:账单页/统计页读取失败会被当成空数据(
BillsView.swift:356-366、StatsView.swift:85-98)。偏好:本地 SQLite 失败几乎等于坏库,值得给一行「账本读取失败」而不是说「没有账单」。 - 「今天」卡片标题与 tab 名都叫「今天」,点进去是同屏,不算错,只是重复。偏好:卡片标题可改成「今日明细」。
- 动态日期的「今天/昨天」前缀只在记一笔里做,账单页日期头没有;偏好上日期头保持不带前缀(它会滚出「今天」的范围)。
TxnRowView是死代码,且与账单行规则冲突(圆角字体、· 不计入预算逻辑不同)。随第 10 条一起删。
两击路径本身按规格落了。以下两条是看实拍图看出来的,编号接在后面,不动上面的。
- P1 · 草稿卡出现后输入框那句掉在
ink2(QuickAddView.swift:107,判据busy || isConfirming):实测#68676C,白底 5.5:1;同屏草稿卡里的金额和值是近黑。规格 §5 只允许「正在整理」那一段降级,草稿卡一出现,这句话就是被确认的对象,该回到ink。改法:判据只留busy。 - 待确认 · 「昨天」没进草稿:实拍图里那句话是「昨天午饭 28」,草稿卡的日期行是「今天」,而 Pluso 的截图说明写的是「昨天」。图上为准就是解析把相对日期丢了,这是账本正确性问题;以说明为准就是截图与说明对不上。先让 Pluso 复跑一句确认,再决定归哪一类。
窗口下三分之二空白、成功回执用 ai 紫——这两条是我的偏好,已当面说过,不进缺陷清单。
- 设计 token:
iosApp/iosApp/PaceStyle.swift - 旧玻璃系统(待删):
iosApp/iosApp/GlassUI.swift - 文案:
iosApp/iosApp/Copy.swift - 业务规则与验收案例:
docs/BUDGET_RULES.md、docs/EDGE_CASES.md - 产品范围:
PLAN.md