OCR 模型按需下载(P8 瘦身): - 移除 plugins/ppocr/assets/ 内置模型(det 9.8MB + rec 21MB + dict),APK 减包 ~31MB - 新增 modelDownloader.ts:优先从 HuggingFace/CDN 下载,兜底从 APK assets 拷贝 - OcrModule.kt 新增 setModelDir,支持从 filesystem 加载模型,回退 assets 兼容旧用户 - settingsStore 持久化 ocrModelVersion / ocrModelDir - 自动化页集成模型状态检查与一键下载 UI OCR 三层独立控制: - OcrProcessorConfig 从单一 aiVisionEnabled 拆分为 layer1/2/3 三个独立开关 - 设置页可按层启停(L1 正则规则 / L2 本地 OCR / L3 AI Vision) - OCR 处理增加耗时与字符数日志 日志系统升级: - logger.ts 新增 LogFileBackend 抽象,支持磁盘持久化(按日期 app-YYYY-MM-DD.log) - 新增 logBackend.ts(ExpoLogFileBackend)+ 日志中心页 settings/logs.tsx - 日志中心:实时缓冲 + 历史文件、4 级过滤、Tag/关键词搜索、JSON 展开、分享导出、7 天过期清理 - _layout.tsx 启动时初始化文件后端 + 日志脱敏(验证码/卡号) 原生浮层 UI 主题同步(P6): - 新增 floatingUiConfig.ts:JS 侧从 theme tokens + i18n 构建 FloatingUiConfig 推送原生 - 新增 FloatingUiConfigStore.kt:SharedPreferences 存储,三浮层组件读取 - FloatingBillView 重设计:颜色/文案走配置、新增币种 chip、金额校验改 BigDecimal - FloatingHelper / FloatingTip 同步适配 - _layout.tsx 新增 FloatingUiConfigSyncer,主题/语言切换自动推送 UI 与组件增强: - FormModal 新增 select/dropdown 控件、行内布局(row/flex)、联动回调 onValuesChange - 新增 Touchable 通用触摸组件、AccountCreateModal 快速建账弹窗 - 信用卡页展示账单周期/到期还款日/本期应还/剩余可用额度,关联账户改下拉选择 - AI 设置页重做:OpenAI/Gemini/DeepSeek 预设 + 默认 URL/模型 - 引导页新增 Android 权限检查步骤(无障碍/通知/短信/存储/悬浮窗) 去重优化: - 对手方匹配改为模糊包含(includes),双方均无对手方时判定低置信度重复 - DedupResult 新增 matchedItem 返回匹配对比项 文档重构: - README.md 重写为入口索引(品牌更新 + 模块概览 + 文档导航表) - 新增 AGENTS.md(AI 助手贡献指南)、docs/architecture.md(Mermaid 数据流/分层/OCR 级联图) - 新增 docs/development.md(环境/命令/编码规范/测试/提交规范)、plugins/README.md - UI 重设计文档(design spec + p1-p8)移入 docs/design/ 其他: - i18n 新增权限/信用卡详情/日志中心/AI 设置等翻译键 - ppocr Config Plugin 修复 import 注入去重;size-optimization 增强 - 新增测试:logger.test.ts、floating-ui-config.test.ts
234 lines
15 KiB
Markdown
234 lines
15 KiB
Markdown
# 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 保留(堆栈对齐)。
|