# 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（堆栈对齐需要）或删除——保留，加注释说明
- 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 保留（堆栈对齐）。
