Skip to content

Latest commit

 

History

History
402 lines (293 loc) · 36.2 KB

DESIGN.md

File metadata and controls

402 lines (293 loc) · 36.2 KB

Pace iOS 视觉与交互规范(design.md)

状态:规范第一批 + 第二批的合并稿,与 iosApp/iosApp 当前实现对齐。 本文件是 iOS 端唯一的设计事实来源:颜色、字体、间距、导航、逐屏层级、状态、无障碍。 代码与本文件不一致时,代码是偏差——但第一次构建(本轮)不改界面,偏差集中列在文末第 12 节,等 app 在模拟器里跑起来后按优先级排。 文案不在本文件里定义:所有用户可见文字来自 Copy.swift(Camo 的文案稿),本文件只规定它们出现在哪里、用多大的字、什么颜色。


1. 方向

一句话:把每天的钱账做成一张干净的纸——近黑与白,靠排版的轻重和留白分层次,颜色只在非它不可的地方出现。

三条原则,按优先级:

  1. 层次靠排版,不靠颜色。 一屏里只允许一个视觉主角(今天剩多少),其余全部退到次级灰。不用渐变、不用彩色卡片、不用阴影堆叠。
  2. 颜色只有四份工作,多一份都不用:
    • 近黑:正文、选中、主按钮。
    • 绿:钱进来(收入、退款)。
    • 红:出问题(超支、错误、不可逆操作)。
    • 紫:只标记「一句话记账」这一个入口,别处不出现。
  3. 状态要诚实。 没有预算就是「还没设预算」,不是 0;没有账单就是「还没有账单」,不是留白;金额缺失就是破折号,不是猜一个数。

情绪词:安静、克制、可以信任。不做「记账软件」的账本感(没有横线格、没有印章、没有卡通吉祥物),也不做金融 App 的冷硬(不用蓝、不用数字仪表盘)。

参考对象:Apple 的「钱包 / 健康 / 健身」三套 App 的留白与数字排版;不动点在于我们把「剩多少」放大到接近海报的比例,别处一律压平。


2. 颜色

全部走 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 上时先算对比度再放。


3. 字体

系统字体(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),不截断。


4. 间距与圆角

只有这几个值,新页面不许自创(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 看起来「像一块被磨过的塑料」而不是「像网页」的关键。


5. 组件

组件 规格 用在哪
实底卡片 .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 唯一允许的玻璃质感是浮起的「记一笔」和账单撤销浮条——它们浮在内容之上,需要和底下的东西区分。


6. 导航与手势

  • 结构: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。
  • 返回:系统返回手势,不改。所有二级页(设置→一句话记账)进入后隐藏 tab 栏。
  • 删除:账单行左滑删除(禁止全滑直接删),删完给撤销浮条。
  • 搜索:只在账单页,.searchable,占位「搜索备注、分类或账户」;搜索跨全部时间范围,不受范围胶囊约束(这一点必须在 UI 上让用户看得出来,见 §12-21)。
  • 动效:只用来交代变化。卡片入场 8pt 上移 + 淡入,spring 0.38/0.86;数字变化用 .numericText();滚动收起「记一笔」0.22 easeOut。开启「减弱动态效果」时,全部换成 0.15 秒淡入,不做位移与缩放。

7. 逐屏规范

7.1 今天(HomeView)

目的:三秒内回答「今天还能花多少」,顺带给今天的账一个落点。

层级(从上到下)

  1. 预算卡(主角,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%」。
  2. 今日账单卡(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 元」。

状态:前 300ms 不闪骨架;300ms 后出两张骨架卡;读完后 spring 入场。读取失败时保留上一次的数据,不显示错误页(本地库读取,失败即视为空账本,见 §12-27)。

7.2 账单(BillsView)

目的:按时间翻账,改一笔账,删一笔账。

层级

  1. 范围胶囊(本周 / 本月 / 全部),segmented,固定在列表上方不随内容滚动,左右 20、上 4、下 12;无障碍标签「范围」。
  2. 列表(plain,露出 canvas):
    • 日期头:「10月26日 · 周日」(12 semibold,ink2,作为 header trait)+ 右侧当日净额(12,monospacedDigit,ink2,支出为负、收入/退款为正、转账不计);上 16 / 下 6。
    • 当日账单卡:一张连续卡片,只有首行上圆角、末行下圆角(list 半径),填充 card,必须有 0.5pt 描边。
    • 行:图标圆 34 → 标题 / 副标题 → 金额,规则同今天页;副标题里带「不计入预算」。
    • 行间 1pt line,左侧缩进 66。
    • 左滑「删除」(红色,trash,禁止全滑直删)。
  3. 空态:居中「这段时间还没有账单。」+「换个范围看看,或者记一笔。」;搜索无结果时换成「没找到相关的账单。」+「换个词试试。」。
  4. 撤销浮条:删除后从底部浮出,「已删除「午饭」」+「撤销」,8 秒后自动消失;点撤销恢复并显示「已恢复」(2 秒,无按钮)。

状态:骨架 3 行(同卡片形状);空态;搜索无结果;删除中;撤销成功/失败;金额缺失不适用(账单必有金额)。

7.3 统计(StatsView)

目的:这一周 / 这个月钱去哪了。

层级

  1. 范围 segmented(本周 / 本月),跟随内容滚动(与账单页的不同,见 §12-8);无障碍标签「范围」。
  2. 汇总卡(card):日期区间(11,ink2)→「支出」15 semibold ink2 + 汇总金额 38 heavy rounded → 2×2 小格(card2 底,block 圆角,12/10 内边距):计入预算 / 退款抵掉 / 收入 / 超支 N 天;小格标签 11 ink2,数值 15 bold(收入绿、超支红)。
  3. 花在哪了(card):标题 headline → 最多 6 行「分类 + 比例条 + 金额 · 占比」(分类 15 ink,金额 13 monospaced ink2),超出显示「还有 N 个分类」(13,ink2)。
  4. 每天花了多少(card):标题 headline → 每行「日期 44 宽 + 比例条 + 金额 128 宽」(日期 12,超支那天用红;金额 11 monospaced ink2,有预算时显示「已花 / 预算」)→ 脚注「每天预算从生效日起算;今天还剩 = 预算 − 计入预算的支出 + 退款。」(11,ink2)。

状态:骨架(一张汇总形状的卡 + 两行);两张卡各自的空态文案「这段时间没有支出。」(只显示一次,见 §12-13);超支日红;没预算时不显示「已花 / 预算」只显示已花。

7.4 记一笔 / 编辑账单(AddView,sheet)

层级(从上到下,间距 16,左右 20,上 8,下 32)

  1. 一句话记账入口(仅新建):胶囊,高 44,aiSoft 底;sparkles 14 + 「一句话记账」14 semibold(ai)+ 右侧示例「昨天午饭 28」13。
  2. 金额卡(card 圆角 / 20 内边距):字段标签「金额」12 semibold ink2 → ¥ 28 semibold ink2 + 输入 48 heavy rounded(数字键盘);错误信息 13 danger 出现在这张卡底部。
  3. 类型卡:segmented 支出 / 收入 / 转账 / 退款。
  4. 分类卡:字段标签「分类」+ 横向滚动的分类胶囊(选中实底)。
  5. 日期与账户卡(list 圆角,行高 52):日期行(今天 / 昨天 / 日期 + chevron.up.chevron.down,点整行出系统日期选择器)→ 1pt 分隔线 → 账户行(现金 / 微信 / 支付宝 / 银行卡 / 自定义…)。
  6. 备注卡:备注 标签 + 文本输入,占位「例如:午饭」。
  7. 计入预算卡(仅支出与退款):Toggle 15 medium + 说明「关闭后仍计入总支出统计」13 ink2。
  8. 退款关联卡(仅退款):字段标签「这笔退的是哪一笔」+ 选择原账单;没有候选时提示「还没有可以关联的支出账单。」(13,ink2)。

状态:新建 / 编辑(编辑时不显示一句话入口,标题变「编辑账单」);保存不可用(金额空或非法)时工具栏按钮 card2 底 ink3 字;保存错误显示在金额卡里;重复账单弹确认框;保存成功直接收起,不弹提示。

7.5 一句话记账(QuickAddView,sheet)

目的:打一句话,出几笔待确认的草稿。

层级:输入卡(card 圆角,18 内边距)→ 例句胶囊(点一下填入)→ 说明一行(按开关状态换三种文案)→ 主按钮「整理成草稿」/「正在整理…」→ 次按钮「改成手动记账」→ 失败卡(标题 + 说明 + 两个按钮,按失败类型换动作)。

状态:本机规则 / 你的服务(状态小字);整理中(按钮 spinner,禁点);成功(push 到草稿确认);失败(失败卡,主按钮按类型:重试远端 / 用本机规则 / 改成手动记账);退出到手动记账时把原句带进备注。

7.6 草稿确认(DraftReviewView)

目的: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)。

7.7 设置(SettingsView)

层级(Form 分组,系统行样式):

  1. 每日预算:当前预算一行 11 ink2(「现在是 ¥200.00,从 10 月 1 日开始」或「还没设预算」)→「每日预算」输入行(数字键盘)→「生效日期」行 →「保存预算」按钮(保存后右侧「已保存」12 出现 2 秒)→ 错误 12 danger。
  2. 一句话记账:「现在用」+ 右侧「本机规则 / 你的服务」,点进二级页。
  3. 导出与恢复:导出表格(CSV)/ 导出数据文件(JSON)/ 从文件恢复…,底部说明导出文件由用户自行保管;导入完成后一行结果 12 ink2。
  4. 数据:清空整个账本…(系统红)。
  5. 版本脚注:Pace 0.1.0 (1),11 ink2,居中。

状态:有/没有预算;保存中(无);保存成功(「已保存」2 秒);保存失败(红字);导入预览(有效 / 校验失败);替换需二次确认;清空需二次确认。

7.8 设置 → 一句话记账(QuickAddSettingsView)

层级:开关卡(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 二次确认;保存即时生效,无保存按钮。

7.9 隐私说明(PrivacyNoteView,sheet)

层级:标题 26 bold ink(「开启前,先说清楚一件事」)→ 四条 checkmark 12 bold ink2 + 15 ink 说明 → 脚注 12 ink2 → 主按钮「同意并开启」→ 次按钮「先不用」→ 底部提示 11 ink2(不是 ink3,见 §12-3)。

状态:只在第一次开启时出现;同意 = 记下已读 + 打开开关;拒绝 = 开关保持关闭;下滑关闭等同没选(下次再问)。

7.10 恢复预览(SettingsView 内 sheet)

层级:大标题「恢复预览」+ 左上「取消」→ 有效时三行(账单 N 笔(起 ~ 止)/ 分类 N · 预算规则 N / 其中 N 笔账本里已经有了)→ 两个操作「合并到现有账本」「替换当前账本…」(后者二次确认)→ 说明;校验失败时改成分组头「文件校验未通过,不会写入任何数据」+ 逐条错误 .footnote。


8. 通用状态规范

状态 规则
加载 本地读取通常 < 80ms:先留空让第一帧画出来,超过 300ms 才出骨架,避免闪一下。骨架用卡片形状 + redacted,不用转圈。
空 居中,两行:一行说「没有什么」(subheadline semibold),一行说「接下来能做什么」(footnote ink2)。空态不给按钮(主操作已经是浮起的「记一笔」)。
错误 就地显示,挨着出错的那个控件:红字 12–13,只有一行,说清楚「哪错了、怎么改」。不弹全屏错误页、不用 toast 代替(toast 只用于成功与瞬时结果)。
成功 能看得出结果的地方就别提示(记一笔保存后直接收起);需要确认的用 toast 或 2 秒的小字(「已保存」)。成功不用绿色,绿只代表钱进来。
超支 数字与进度条转 danger,状态胶囊变「已超支」,不改版式、不加弹窗。
不可用 主按钮 card2 底 ink3 字,可点但无效果不接受(禁用态不算对比度不合格)。

9. 无障碍

  • 每个列表行是一个 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)。

10. SF Symbols 清单

只用系统符号,不引图形资源。

用途 符号
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),不自成一套粗细。


11. 资源

不需要任何外部素材:图标全用 SF Symbols,颜色全用 token,字体用系统字体。已就位的是 App Icon(三行账,含深色与 tinted 变体)。新增图形前先问:能不能用排版或一个系统符号解决。


12. 当前实现与规范的偏差(编号,可测量)

本轮(第一次构建)不改界面。以下按「用户能不能感知」排序,等 app 在模拟器里跑起来后一起排优先级。P0 = 影响读数正确性或可读性;P1 = 一致性/无障碍缺陷;P2 = 打磨。带「偏好」的是我的判断,不是缺陷。

P0

  1. PaceColor.income 放在 card2 上只有 4.40:1(统计汇总卡「收入」数值,StatsView.swift:229)。浅色下 15pt bold 不算大字,差 0.1 不过 AA。改法:浅色 income 从 #12805C 调到 #107050(对 card2 5.44:1,对 card 6.08:1),深色不动。
  2. 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。
  3. 失败路径没有任何提示:账单页撤销失败静默返回(BillsView.swift:440-467)、草稿保存的非重复失败被吞掉(DraftReviewView.swift:277-287)、导出写文件失败 try? 吞掉(SettingsView.swift)。用户会以为记上了。三处都要给一行红字。
  4. 草稿确认可以空着保存(DraftReviewView.swift:260):0 笔草稿时按钮是「保存 0 笔」且可点,点完弹「已保存 0 笔」。空列表不该进得去,进去了也不该给保存。
  5. 「先补金额」在缺日期/分类时也说金额(DraftReviewView.swift:98-99 + Copy.swift:181):缺字段统一文案,按钮与实际缺的东西对不上。
  6. 草稿里缺「类型」永远补不上(DraftReviewView.swift:203-239 没有类型编辑):需要类型 一旦出现,页面就卡死。

P1

  1. 103 处固定字号 vs 19 处语义字号(9 个视图文件):动态字体开到最大,卡片里的字不跟着长。优先补:行标题、字段标签、说明、toast。
  2. 卡片描边不一致:账单页按天卡(BillsView.swift:339-347)和统计骨架小格(StatsView.swift:171-174)没有 0.5pt line,深色下边界消失;其余卡片都有。
  3. 圆角有六种:30/26/22/20/18/14(QuickAddView.swift:85,156、DraftReviewView.swift:177、QuickAddSettingsView.swift:266、BillsView.swift:614)。规范只有四种,20/18 要归到 22/26/14。
  4. 同一件事两种颜色结论:退款在账单页是绿(BillsView.swift:528-531),在统计「退款抵掉」是 ink(StatsView.swift:219-224);死代码 TxnRowView 里还是旧的青绿渐变(TxnUI.swift:90,GlassUI.swift 的 PaceGradients 只剩这一处引用)。统一为绿,删掉 TxnRowView 与 GlassUI.swift。
  5. 红色越界:「超支 0 天」也红(StatsView.swift:231);保存成功、连接成功用绿(SettingsView.swift:155、QuickAddSettingsView.swift:231)。按 §2 收回:绿=钱进来,红=超支/错误/不可逆。
  6. 两个黑:进度条用 accent #1C1C1E,文字用 ink #0E0E0F(PaceStyle.swift:26,30,160)。视觉上几乎无差,但两套 token 会导致后续改动漂移。选一个当「实底」,另一个只做文字。
  7. 统计两张卡的空态文案相同(StatsView.swift:285-290、:343-348):零数据时同屏出现两遍「这段时间没有支出。」
  8. 统计底部留 100pt 是给已经不存在的系统附件条(StatsView.swift:38-39 注释与实现不符,该屏也没有「记一笔」)。
  9. 账单页范围胶囊在搜索时变成假控件:搜索跨全部时间,胶囊仍显示「本周」(BillsView.swift:79,356-358)。搜索时隐藏或置灰胶囊,或让搜索遵守范围。
  10. 删除没有二次确认,左滑即删(BillsView.swift:329-335),而 Copy.Delete 里备好了确认文案。在「撤销」已经兜底的前提下,可以保留即删;但全滑必须继续禁止。
  11. 撤销浮条与「记一笔」可能叠在一起:撤销条距底 safeArea+8,浮按钮距底 99、高 44(顶部在 143),列表底 inset 只有 76,最后一行会被浮按钮盖住(BillsView.swift:117,245;FloatingAddButton.swift:34)。列表底 inset 至少 96。
  12. 无障碍覆盖不均:草稿确认和隐私说明两个页面一条无障碍修饰都没有;PrivacyNoteView.swift:25 四个装饰 checkmark 会被读出来;全 App 没有一处 accessibilityHint;toast、错误、「已保存」都没做动态朗读。
  13. 「清空 Key」是 12pt 纯文字(QuickAddSettingsView.swift:147-150),点击区不足 44。
  14. 草稿确认出现双层导航:从一句话记账 push 进来时,它内部又套了一个 NavigationStack(DraftReviewView.swift:34 + QuickAddView.swift:54)。
  15. 日期输入有三种交互(记一笔/设置用隐形的 compact picker、草稿页用系统 DatePicker),显示格式也有四种(今天 · 10月26日 / 10月26日 / 10月26日 · 周日 / 10-26)。统一为:行内显示「今天 · 10月26日」/「10月26日」/「2025年10月26日」,点行出选择器。
  16. 系统玻璃控件混进设计系统:记一笔左上关闭按钮用 .glass(AddView.swift:118),草稿页右上保存用 .glassProminent(DraftReviewView.swift:51),而记一笔的保存是自绘的扁胶囊。同一层级两个按钮两套外观。
  17. 设置页整套是系统 Form(SettingsView.swift:29),行高、分组头、分隔线、圆角都不在设计系统里。这是本轮最大的版式不一致;偏好上我倾向保留 Form(系统表单在这里更好用),但那就要明确它是「系统表单区」,并把卡片区与表单区的边界做得干净。
  18. 文案散落在代码里:标题、账户预设、草稿页的「改」「删除」「改好了」、设置里的分组头、恢复预览的每一行都不在 Copy.swift(如 AddView.swift:51,107,333,350、DraftReviewView.swift:144,155,164,237、SettingsView.swift:226,296,314-326)。文案改一遍要翻五个文件。

P2(打磨 / 偏好)

  1. 账单页日期头会随列表滚走(普通行,不吸顶)。偏好:吸顶更省事,但不吸顶更安静——我倾向维持现状。
  2. 统计范围胶囊跟着滚,账单范围胶囊固定。两屏行为不同,偏好上我倾向统一成「固定」,因为范围是这一屏的读法,不是内容的一部分。
  3. 本地库读取失败没有兜底:账单页/统计页读取失败会被当成空数据(BillsView.swift:356-366、StatsView.swift:85-98)。偏好:本地 SQLite 失败几乎等于坏库,值得给一行「账本读取失败」而不是说「没有账单」。
  4. 「今天」卡片标题与 tab 名都叫「今天」,点进去是同屏,不算错,只是重复。偏好:卡片标题可改成「今日明细」。
  5. 动态日期的「今天/昨天」前缀只在记一笔里做,账单页日期头没有;偏好上日期头保持不带前缀(它会滚出「今天」的范围)。
  6. TxnRowView 是死代码,且与账单行规则冲突(圆角字体、· 不计入预算 逻辑不同)。随第 10 条一起删。

追加:两击这一单的实拍复查(2026-10-26)

两击路径本身按规格落了。以下两条是看实拍图看出来的,编号接在后面,不动上面的。

  1. P1 · 草稿卡出现后输入框那句掉在 ink2(QuickAddView.swift:107,判据 busy || isConfirming):实测 #68676C,白底 5.5:1;同屏草稿卡里的金额和值是近黑。规格 §5 只允许「正在整理」那一段降级,草稿卡一出现,这句话就是被确认的对象,该回到 ink。改法:判据只留 busy。
  2. 待确认 · 「昨天」没进草稿:实拍图里那句话是「昨天午饭 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