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
15 KiB
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.ts);fontFamily 残留 14 处;emoji 5 处(monthlySummary.ts 4 + ai/chat.tsx 1);tag 页已是模板试点。
与 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: 管理页模板化批次 1(budget / rules / remark-template)
Files:
-
Modify:
src/app/budget/index.tsx、src/app/rules/index.tsx、src/app/remark-template/index.tsx -
Step 1: 三页套 ManagementScreen(参照试点
src/app/tag/index.tsx与模板src/components/ManagementScreen.tsx)
通用映射:页面 state 消失(modal/editing 由模板持有);手写 header/addBtn/Alert/FormModal 全删;footer 传 common.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 偏移) - formFields:5 字段(name/amount decimal-pad/period/categoryAccount/startDate),defaultValue 从 editing 取
- onSubmit:保留校验(amount parseFloat ≤ 0 → Alert + return false);add/edit 分支调 addBudget/updateBudget
- onDelete:
removeBudget(budget.id);deleteConfirmText:t('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(审计项) - formFields:7 字段(priority numeric/counterpartyContains/memoContains/sourceAccount/categoryAccount/narration/tags)
- onSubmit:保留现有 Rule 组装逻辑;addRule
{...rule, id: generateId('rule'), hits: 0}/ updateRule
remark-template/index.tsx:
-
2 字段(name/template);renderItem 的模板文本
fontFamily: 'monospace'改fontVariant: ['tabular-nums'](审计项) -
onSubmit:name 空回退
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: 管理页模板化批次 2(category / recurring / credit-card)
Files:
-
Modify:
src/app/category/index.tsx、src/app/recurring/index.tsx、src/app/credit-card/index.tsx -
Step 1: category/index.tsx — headerContent 类型 chips
-
保留一个本地 state:
const [catType, setCatType] = useState<'expense' | 'income'>('expense') -
headerContent:两个 chip(支出/收入,commonStyles.chip/chipActive),切换 setCatType -
items={categories.filter(c => c.type === catType)} -
formTitle:editing ?t('category.editTitle'):t('category.addTitle', { type: catType === 'income' ? t('category.income') : t('category.expense') })(插值语法以现有键为准,先 grep category.addTitle) -
formFields:3 字段(name/linkedAccount/keywords),编辑时带 defaultValue
-
onSubmit:add 时
type: catType,linkedAccount 空回退按类型(Income:/Expenses:Uncategorized),keywords 拆分逻辑保留;edit 保留原逻辑 -
renderItem:保留名称/linkedAccount/关键词行
-
Step 2: recurring/index.tsx — 显式按钮用 handlers
-
renderItem:保留信息行 Card;卡片底部两个按钮(编辑/删除)分别调
handlers.openEdit()/handlers.confirmDelete()(不用长按,交互更显式);外层 Pressable 去掉 -
formFields:6 字段;
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}
-
formFields:8 字段;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.tsx、src/app/settings/sync.tsx、src/app/settings/ai.tsx、src/app/settings/preferences.tsx -
Step 1: account/index.tsx
手写 header(arrow-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 与不再使用的 import(Ionicons/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.tsx、src/components/charts/TrendLine.tsx、src/components/charts/NetWorthChart.tsx、src/components/CalendarView.tsx、src/components/charts/CalendarHeatmap.tsx、src/components/DatePickerField.tsx、src/components/NumpadKeyboard.tsx、src/components/NumpadSheet.tsx、src/ai/monthlySummary.ts、src/app/ai/chat.tsx、src/app/transaction/[id].tsx、src/app/_error.tsx、src/app/import/index.tsx、src/components/PostingEditor.tsx、src/i18n/zh.ts、src/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:391)→
fontVariant: ['tabular-nums'] - 纯文本场景(_error.tsx:70 堆栈文本)→ 保留 monospace(堆栈对齐需要)或删除——保留,加注释说明
- 金额/数字场景(CategoryPie:35、transaction/[id].tsx:314、PostingEditor:63、import/index.tsx:391)→
-
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: 星期头 i18n(3 处硬编码中文)
-
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.tsx、CalendarView.tsx、CalendarHeatmap.tsx的 WEEKDAYS 常量改为t('datepicker.weekdays').split(',')/t('calendar.weekdays').split(',')(组件内 useT;注意保持周一/周日起始顺序与各自网格一致,不要换序) -
Step 4: NumpadKeyboard backspace 标签 i18n
-
i18n 加键:
'numpad.backspace': '退格'/'Backspace' -
NumpadKeyboard props 加
backspaceLabel: string,backspace 键 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.ts、src/components/NumpadSheet.tsx、src/components/NumpadSheetHost.tsx、src/domain/budgets.ts、src/app/(tabs)/transactions.tsx、tests/budgets.test.ts(或现有 budget 测试文件) -
Delete:
src/components/charts/MonthlyReport.tsx -
Step 1: numpadGlobalEntry 默认翻 true
src/store/settingsStore.ts DEFAULTS 中 numpadGlobalEntry: false → true。注意:已持久化 false 的用户不受影响(hydrate 合并优先级),仅新安装生效——在代码注释中写明。
-
Step 2: modal 模式未保存守卫(翻默认的前置)
-
NumpadSheet.tsx:加可选 proponDirtyChange?: (dirty: boolean) => void;把 beforeRemove 守卫里的 dirty 计算抽成computeDirty()(读 formRef),加useEffect(() => { onDirtyChange?.(computeDirty()); })(每次渲染后上报,轻量) -
NumpadSheetHost.tsx:const 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 import;common.discard 键先 grep 确认存在。)
- Step 3: 删除 MonthlyReport.tsx(P4 确认无引用的死文件)
Run: rm src/components/charts/MonthlyReport.tsx,再 grep -rn "MonthlyReport" src/ → 无输出
- Step 4: budgets.ts getPeriodRange 本地时区修复
读 src/domain/budgets.ts 的 getPeriodRange(约 93 行):new Date('YYYY-MM-DD') 是 UTC 解析,负时区会把边界日算到前一天。改为 split 构造本地 Date(const [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
走查清单(浅/暗双主题):
- 6 个管理页(分类/预算/规则/周期/信用卡/备注模板):+新增、点按编辑、删除(recurring 为显式按钮,其余长按)、空态文案
- account 页:类型 Tab、开户、调余额、关户
- settings/sync、ai、preferences:header 统一有返回箭头
- +按钮默认开全局面板(新装/清数据后):任意页面弹 modal;输入金额后点遮罩/Android 返回 → 弹「放弃修改」确认;无输入直接关
- 设置→偏好里开关关闭 → +回到跳整页
- 交易页左滑复制带标签的交易 → 标签预填
- 报表月 Tab 日历、年报节奏图、TrendLine/CategoryPie 显示正常
- 英文语言:日期选择器/日历星期头为英文缩写
完成后的项目状态
P1–P5 全部交付,spec §8 验收标准全达成。剩余已知非阻塞项(不修,记录):TransactionCard memo 被内联 onPress 击穿(性能);todayStr useMemo 跨午夜不刷新;NumpadSheet page 模式无可见返回按钮(spec 允许);_error.tsx monospace 保留(堆栈对齐)。