DriftLedger/docs/ui-redesign-p5-plan.md
fengmengqi 2e73b5a2c6 feat: UI 全面重设计(Bento 明亮风)+ 记一笔 NumpadSheet + 管理页模板 + OCR v5→v6 升级
UI 重设计(P1-P5):
- 主题从靛蓝 accent 切换为近黑白单色(#111318),品牌感靠排版与财务语义色
- 暗色模式适配 OLED 纯黑,财务色整体提亮一档保证对比度
- 圆角体系新增 xl(24),卡片/弹窗统一大圆角
- 移除 Caveat/Quicksand 自定义字体,切换为系统字体
- 新增 display(34/800) 字阶用于净资产等大数字展示
- 分类/标签/渠道颜色统一到 palette.ts 色板(12 色循环 + FNV 哈希)
记一笔 NumpadSheet(P3 录入闭环):
- 全新计算器风格录入面板:方向 chip → 大金额 → 账户 chip → 分类快捷行 → 数字键盘
- 支持表达式输入(20+15-5.5)与字符串十进制求值,避免浮点精度问题
- 高级模式保留多行分录编辑器,简单模式自动推断双腿
- postingLegs.ts 实现 buildPostings 的逆操作(编辑/草稿预填)
- 未保存守卫:整页 beforeRemove + modal 脏状态上报
- 全局弹层(NumpadSheetHost)由+按钮唤起,可通过设置开关
Tab 栏重构:
- AppTabBar 替换默认 tab bar,中央+按钮唤起全局记账面板
- Tab 从 5 个精简为 4 个(首页/交易/报表/设置),+按钮独立
管理页模板 ManagementScreen:
- 统一「列表 + 新增 + 点按编辑 + 长按删除 + FormModal」模式
- 预算/分类/标签/信用卡等管理页消除手写样板
其他:
- OCR 模型从 PP-OCRv5 升级到 v6(det/rec/dict 全部替换)
- GBK 解码器 import 修复(text-encoding-gbk ESM 兼容)
- 批量确认失败数计算修正(用 failedPayees.length 替代 store 快照差值)
- 新增 diagnostics 诊断页、TodoStrip 首页待办条、SwipeableTransactionCard 左滑操作
- periodNav.ts 报表周期导航(anchor+period 统一状态管理,本地时区运算)
- 测试覆盖 numpadExpression/postingLegs/periodNav/palette/search/transactionGroups/categoryIcons/dateGrid
2026-07-22 13:33:29 +08:00

15 KiB
Raw Blame History

UI 重设计 P5管理页模板化 + 清零收尾 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax. 不执行任何 git add/commit(用户要求,改动留工作区)。不创建额外任务清单。

Goal: 7 个管理页套 ManagementScreen 模板、account 页与 3 个设置二级页统一 ScreenHeader、图表/fontFamily/emoji/星期头清理、numpadGlobalEntry 翻默认(含 modal 未保存守卫)、清零审计验收。

Spec: docs/ui-redesign-design.md §7.5/§8。已确认现状2026-07-22 审计hex 硬编码已清零(#[0-9A-Fa-f]{6} 在 src/ 仅剩 presets.ts/palette.tsfontFamily 残留 14 处emoji 5 处monthlySummary.ts 4 + ai/chat.tsx 1tag 页已是模板试点。

与 spec 的偏差①account 页是「账户浏览器」(类型 Tab + 开户/调余额/关户三个交互),不套 ManagementScreen只做 ScreenHeader 统一;②numpadGlobalEntry 翻默认只影响新安装(已持久化的 false 不覆盖,尊重用户选择)。

模板适配决策(已通读 7 页源码):

  • budget/rules/remark-template → 直套T1
  • category → headerContent 放支出/收入 chips新增按当前类型T2
  • recurring → 卡片保留显式编辑/删除按钮(用 handlers.openEdit/confirmDelete不用长按T2
  • credit-card → 富 renderItem账单盒直套T2

Task 1: 管理页模板化批次 1budget / rules / remark-template

Files:

  • Modify: src/app/budget/index.tsxsrc/app/rules/index.tsxsrc/app/remark-template/index.tsx

  • Step 1: 三页套 ManagementScreen(参照试点 src/app/tag/index.tsx 与模板 src/components/ManagementScreen.tsx

通用映射:页面 state 消失modal/editing 由模板持有);手写 header/addBtn/Alert/FormModal 全删;footercommon.clickEditLongDelete 提示;deleteConfirmTitle 传各页 t('xxx.deleteTitle')

budget/index.tsx

  • items={budgets}keyExtractor={b => b.id}addLabel={t('budget.add')}emptyText={t('budget.empty')}
  • renderItem保留现有 Card + 进度条结构Pressable onPress={openEdit} onLongPress={confirmDelete} 包裹)。today 改用 toDateString(new Date())(修掉 toISOString().slice(0,10) 的 UTC 偏移)
  • formFields5 字段name/amount decimal-pad/period/categoryAccount/startDatedefaultValue 从 editing 取
  • onSubmit保留校验amount parseFloat ≤ 0 → Alert + return falseadd/edit 分支调 addBudget/updateBudget
  • onDeleteremoveBudget(budget.id)deleteConfirmTextt('budget.deleteConfirm', { name: budget.name })

rules/index.tsx

  • items={[...rules].sort((a, b) => b.priority - a.priority)}
  • renderItem保留规则行narration/P 徽标/条件/目标账户+命中次数);删除 fontFamily: theme.typography.caption.fontFamily(审计项)
  • formFields7 字段priority numeric/counterpartyContains/memoContains/sourceAccount/categoryAccount/narration/tags
  • onSubmit保留现有 Rule 组装逻辑addRule {...rule, id: generateId('rule'), hits: 0} / updateRule

remark-template/index.tsx

  • 2 字段name/templaterenderItem 的模板文本 fontFamily: 'monospace'fontVariant: ['tabular-nums'](审计项)

  • onSubmitname 空回退 t('common.untitled')

  • Step 2: 验证

Run: npm run typecheck && npm test → 全绿 Run: grep -ln "FormModal\|arrow-back" src/app/budget/index.tsx src/app/rules/index.tsx src/app/remark-template/index.tsx → 无输出


Task 2: 管理页模板化批次 2category / recurring / credit-card

Files:

  • Modify: src/app/category/index.tsxsrc/app/recurring/index.tsxsrc/app/credit-card/index.tsx

  • Step 1: category/index.tsx — headerContent 类型 chips

  • 保留一个本地 stateconst [catType, setCatType] = useState<'expense' | 'income'>('expense')

  • headerContent:两个 chip支出/收入commonStyles.chip/chipActive切换 setCatType

  • items={categories.filter(c => c.type === catType)}

  • formTitleediting ? t('category.editTitle') : t('category.addTitle', { type: catType === 'income' ? t('category.income') : t('category.expense') })(插值语法以现有键为准,先 grep category.addTitle

  • formFields3 字段name/linkedAccount/keywords编辑时带 defaultValue

  • onSubmitadd 时 type: catTypelinkedAccount 空回退按类型Income:/Expenses:Uncategorizedkeywords 拆分逻辑保留edit 保留原逻辑

  • renderItem保留名称/linkedAccount/关键词行

  • Step 2: recurring/index.tsx — 显式按钮用 handlers

  • renderItem保留信息行 Card卡片底部两个按钮编辑/删除)分别调 handlers.openEdit() / handlers.confirmDelete()用长按,交互更显式);外层 Pressable 去掉

  • formFields6 字段;new Date().toISOString().slice(0, 10) 两处 → toDateString(new Date())

  • onSubmit保留校验/draft 组装/成功 Alert返回 true校验失败 Alert 后 return false

  • 成功 Alert 在 onSubmit 内setModal(null) 由模板接管——删除手动 setModal 调用)

  • Step 3: credit-card/index.tsx — 富 renderItem 直套

  • accountBalances useMemo 保留在页面顶层

  • renderItem保留整张卡银行/账单日/还款日/额度/账单盒Pressable onPress={openEdit} onLongPress={confirmDelete}

  • formFields8 字段onSubmit 保留组装逻辑

  • Step 4: 验证

Run: npm run typecheck && npm test → 全绿 Run: grep -ln "FormModal\|arrow-back" src/app/category/index.tsx src/app/recurring/index.tsx src/app/credit-card/index.tsx → 无输出


Task 3: ScreenHeader 统一account + settings 三页)

Files:

  • Modify: src/app/account/index.tsxsrc/app/settings/sync.tsxsrc/app/settings/ai.tsxsrc/app/settings/preferences.tsx

  • Step 1: account/index.tsx

手写 headerarrow-back + title替换为 <ScreenHeader title={t('account.title')} />(组件保持其余逻辑不变:类型 Tab/开户 FormModal/调余额 FormModal/关户 Alert删除 styles.header。调余额表单的 date 默认值 new Date().toISOString().slice(0, 10)toDateString(new Date())

  • Step 2: settings/sync.tsx、ai.tsx、preferences.tsx

三页手写 header约在 sync.tsx:460、ai.tsx:51、preferences.tsx:44替换为 ScreenHeader标题沿用原文案键删除对应 styles.header 与不再使用的 importIonicons/router 若仅 header 使用。sync.tsx 546 行,只动 header 块,其余不动。

  • Step 3: 验证

Run: npm run typecheck && npm test → 全绿 Run: grep -rn "arrow-back" src/app/ | grep -v ScreenHeader → 无输出


Task 4: 图表与杂项清理fontFamily / emoji / 星期头 i18n

Files:

  • Modify: src/components/charts/CategoryPie.tsxsrc/components/charts/TrendLine.tsxsrc/components/charts/NetWorthChart.tsxsrc/components/CalendarView.tsxsrc/components/charts/CalendarHeatmap.tsxsrc/components/DatePickerField.tsxsrc/components/NumpadKeyboard.tsxsrc/components/NumpadSheet.tsxsrc/ai/monthlySummary.tssrc/app/ai/chat.tsxsrc/app/transaction/[id].tsxsrc/app/_error.tsxsrc/app/import/index.tsxsrc/components/PostingEditor.tsxsrc/i18n/zh.tssrc/i18n/en.ts

  • Step 1: fontFamily 清零

  • theme.typography.X.fontFamily 引用CategoryPie 2 处、TrendLine 4 处、ai/chat.tsx 1 处直接删除该属性P1 后 typography 无 fontFamily 字段,这些是无效引用)

  • fontFamily: 'monospace'

    • 金额/数字场景CategoryPie:35、transaction/[id].tsx:314、PostingEditor:63、import/index.tsx:391fontVariant: ['tabular-nums']
    • 纯文本场景_error.tsx:70 堆栈文本)→ 保留 monospace堆栈对齐需要或删除——保留加注释说明
  • NetWorthChart/CalendarHeatmap 顺便检查一遍grep 未命中但过一眼 token 合规)

  • Step 2: emoji 清零

  • src/ai/monthlySummary.ts:81-85:剥离 📊/💰/💸/📝 前缀,保留纯文本行(如 '总收入:...'

  • src/app/ai/chat.tsx:123:方向 emoji💰/🔄/💸替换为符号文本income ''、transfer '⇄'、expense ''(或直接金额上色——读上下文选与周边一致的处理;金额色用 theme.colors.financial.*

  • Step 3: 星期头 i18n3 处硬编码中文)

  • i18n 加键zh/en 对等,单键逗号分隔):'datepicker.weekdays': '一,二,三,四,五,六,日' / 'Mo,Tu,We,Th,Fr,Sa,Su'周一起DatePickerField 用);'calendar.weekdays': '日,一,二,三,四,五,六' / 'Su,Mo,Tu,We,Th,Fr,Sa'周日起CalendarView/CalendarHeatmap 用)

  • DatePickerField.tsxCalendarView.tsxCalendarHeatmap.tsx 的 WEEKDAYS 常量改为 t('datepicker.weekdays').split(',') / t('calendar.weekdays').split(',')(组件内 useT注意保持周一/周日起始顺序与各自网格一致,不要换序)

  • Step 4: NumpadKeyboard backspace 标签 i18n

  • i18n 加键:'numpad.backspace': '退格' / 'Backspace'

  • NumpadKeyboard props 加 backspaceLabel: stringbackspace 键 accessibilityLabel 用它;NumpadSheet.tsx 调用处传 t('numpad.backspace')

  • Step 5: 验证

Run: npm run typecheck && npm test → 全绿i18n 对等校验覆盖新键) Run: grep -rn "fontFamily" src/ --include="*.tsx" | grep -v "_error.tsx" → 无输出


Task 5: 收尾(翻默认 + modal 守卫 + MonthlyReport 删除 + budgets 时区修复 + duplicate 增强)

Files:

  • Modify: src/store/settingsStore.tssrc/components/NumpadSheet.tsxsrc/components/NumpadSheetHost.tsxsrc/domain/budgets.tssrc/app/(tabs)/transactions.tsxtests/budgets.test.ts(或现有 budget 测试文件)

  • Delete: src/components/charts/MonthlyReport.tsx

  • Step 1: numpadGlobalEntry 默认翻 true

src/store/settingsStore.ts DEFAULTS 中 numpadGlobalEntry: falsetrue注意:已持久化 false 的用户不受影响hydrate 合并优先级),仅新安装生效——在代码注释中写明。

  • Step 2: modal 模式未保存守卫(翻默认的前置)

  • NumpadSheet.tsx:加可选 prop onDirtyChange?: (dirty: boolean) => void;把 beforeRemove 守卫里的 dirty 计算抽成 computeDirty()(读 formRefuseEffect(() => { onDirtyChange?.(computeDirty()); })(每次渲染后上报,轻量)

  • NumpadSheetHost.tsxconst dirtyRef = useRef(false)NumpadSheet 传 onDirtyChange={d => { dirtyRef.current = d; }}overlay onPress 与 Modal onRequestClose 改调 requestClose

const requestClose = () => {
  if (!dirtyRef.current) { close(); return; }
  Alert.alert(t('transaction.unsavedTitle'), t('transaction.unsavedMessage'), [
    { text: t('common.cancel'), style: 'cancel' },
    { text: t('common.discard'), style: 'destructive', onPress: close },
  ]);
};

Host 需 useT + Alert importcommon.discard 键先 grep 确认存在。)

  • Step 3: 删除 MonthlyReport.tsxP4 确认无引用的死文件)

Run: rm src/components/charts/MonthlyReport.tsx,再 grep -rn "MonthlyReport" src/ → 无输出

  • Step 4: budgets.ts getPeriodRange 本地时区修复

src/domain/budgets.tsgetPeriodRange(约 93 行):new Date('YYYY-MM-DD') 是 UTC 解析,负时区会把边界日算到前一天。改为 split 构造本地 Dateconst [y, m, d] = dateStr.split('-').map(Number); new Date(y, m - 1, d)),与 periodNav 的 parse 一致。在现有 budget 测试文件(ls tests/ | grep -i budget)追加边界用例:getPeriodRange('monthly', '2026-07-01') 的 start 必须是 '2026-07-01'、end '2026-07-31'getPeriodRange('weekly', ...)/yearly 各补一例(先读现有测试风格与函数实际行为写期望值——若修复改变了现有行为导致旧测试失败,停止报告 BLOCKED

  • Step 5: handleDuplicate 带 tags

src/app/(tabs)/transactions.tsx 的 handleDuplicate draftJson 加 tags: tx.tags;同时检查 NumpadSheet.tsx 的 draftJson 预填 effect——目前不解析 tags参照 editId 预填的 tags 回填逻辑tags store 查名 → fallback TAG_COLORS[3])补上 draft.tags 解析(Array.isArray(draft.tags)。cost/price 不带hasExtras 逻辑已保证含 cost 交易从编辑入口进高级模式;复制是新建,丢 cost 属可接受简化,注释说明)。

  • Step 6: 验证

Run: npm run typecheck && npm test → 全绿


Task 6: P5 清零审计 + 全项目验收

  • Step 1: 清零审计(全部应无输出或仅剩允许项)
grep -rn "#[0-9A-Fa-f]\{6\}" src/ --include="*.ts" --include="*.tsx" | grep -v "theme/presets.ts\|theme/palette.ts"   # 无输出
grep -rnE "#[0-9A-Fa-f]{3}\b" src/ --include="*.ts" --include="*.tsx" | grep -v "theme/presets.ts\|theme/palette.ts" # 无输出
grep -rn "fontFamily" src/ --include="*.tsx"                                                                       # 仅 _error.tsx monospace
grep -rn "📊\|💰\|💸\|📝\|🔄\|🎉\|✨" src/ --include="*.ts" --include="*.tsx"                                       # 无输出
grep -rn "MonthlyReport\|SpeedDial" src/                                                                            # 无输出
grep -rn "arrow-back" src/app/                                                                                      # 无输出ScreenHeader 内部除外)
grep -rn "WEEKDAYS" src/components/                                                                                 # 无输出(全部走 i18n
  • Step 2: 全量测试 + typecheck

Run: npm test → 全绿 Run: npm run typecheck → 无错误

  • Step 3: 手工走查(需设备/模拟器)

Run: npm run android 走查清单(浅/暗双主题):

  1. 6 个管理页(分类/预算/规则/周期/信用卡/备注模板新增、点按编辑、删除recurring 为显式按钮,其余长按)、空态文案
  2. account 页:类型 Tab、开户、调余额、关户
  3. settings/sync、ai、preferencesheader 统一有返回箭头
  4. +按钮默认开全局面板(新装/清数据后):任意页面弹 modal输入金额后点遮罩/Android 返回 → 弹「放弃修改」确认;无输入直接关
  5. 设置→偏好里开关关闭 → +回到跳整页
  6. 交易页左滑复制带标签的交易 → 标签预填
  7. 报表月 Tab 日历、年报节奏图、TrendLine/CategoryPie 显示正常
  8. 英文语言:日期选择器/日历星期头为英文缩写

完成后的项目状态

P1P5 全部交付spec §8 验收标准全达成。剩余已知非阻塞项不修记录TransactionCard memo 被内联 onPress 击穿性能todayStr useMemo 跨午夜不刷新NumpadSheet page 模式无可见返回按钮spec 允许_error.tsx monospace 保留(堆栈对齐)。