# 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)替换为 ``(组件保持其余逻辑不变:类型 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(堆栈对齐需要)或删除——保留,加注释说明 - 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`:加可选 prop `onDirtyChange?: (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`: ```typescript 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: 清零审计(全部应无输出或仅剩允许项)** ```bash 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、preferences:header 统一有返回箭头 4. **+按钮默认开全局面板**(新装/清数据后):任意页面弹 modal;输入金额后点遮罩/Android 返回 → 弹「放弃修改」确认;无输入直接关 5. 设置→偏好里开关关闭 → +回到跳整页 6. 交易页左滑复制带标签的交易 → 标签预填 7. 报表月 Tab 日历、年报节奏图、TrendLine/CategoryPie 显示正常 8. 英文语言:日期选择器/日历星期头为英文缩写 --- ## 完成后的项目状态 P1–P5 全部交付,spec §8 验收标准全达成。剩余已知非阻塞项(不修,记录):TransactionCard memo 被内联 onPress 击穿(性能);todayStr useMemo 跨午夜不刷新;NumpadSheet page 模式无可见返回按钮(spec 允许);_error.tsx monospace 保留(堆栈对齐)。