# UI 重设计 P1:设计系统 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 将主题系统切换到「明亮 Bento 现代风」新设计令牌(双主题),移除自定义字体,硬编码颜色收归 palette,并重写 MASTER.md 使设计文档与实现一致。 **Architecture:** 只改 `src/theme/`、`src/app/_layout.tsx`(字体加载)、`src/domain/channelConfig.ts`(品牌色引用)、`src/components/CategoryPicker.tsx`(色板引用)、`design-system/`。Token 名不变只改值,组件接口向后兼容,现有页面自动"粗换皮"。 **Tech Stack:** React Native + Expo + TypeScript + Vitest。 **Spec:** `docs/ui-redesign-design.md` §3(视觉语言)、§10(P1 验收)。 **验收标准:** `npm test` 全绿;`npm run typecheck` 通过;`src/` 下不再引用 `@expo-google-fonts/*`;`presets.ts` 的 typography 无 `fontFamily`。 --- ### Task 1: 新设计令牌的失败测试 **Files:** - Modify: `tests/theme.test.ts` - Create: `tests/palette.test.ts` - [ ] **Step 1: 重写 tests/theme.test.ts 的断言以匹配新令牌** 完整替换文件内容为: ```typescript import { describe, expect, it } from 'vitest'; import { lightTheme, darkTheme, presetThemes } from '../src/theme/presets'; import { createTheme } from '../src/theme/createTheme'; import type { ThemeTokens } from '../src/theme/tokens'; describe('预置主题(明亮 Bento 设计系统)', () => { it('lightTheme 与 darkTheme 关键色彩不同', () => { expect(lightTheme.colors.bgPrimary).not.toBe(darkTheme.colors.bgPrimary); expect(lightTheme.colors.fgPrimary).not.toBe(darkTheme.colors.fgPrimary); }); it('浅色背景为 #F6F7F9,暗色为 OLED 纯黑 #040508', () => { expect(lightTheme.colors.bgPrimary).toBe('#F6F7F9'); expect(darkTheme.colors.bgPrimary).toBe('#040508'); }); it('accent 为近黑白单色:浅色 #111318,暗色反转为 #F3F4F6', () => { expect(lightTheme.colors.accent).toBe('#111318'); expect(darkTheme.colors.accent).toBe('#F3F4F6'); }); it('accentLight 为 accent 的半透明底色(不再是靛蓝族)', () => { expect(lightTheme.colors.accentLight).toBe('rgba(17,19,24,0.08)'); expect(darkTheme.colors.accentLight).toBe('rgba(243,244,246,0.10)'); }); it('财务语义色:暗色整体提亮一档', () => { expect(lightTheme.colors.financial.income).toBe('#10B981'); expect(lightTheme.colors.financial.expense).toBe('#EF4444'); expect(lightTheme.colors.financial.transfer).toBe('#3B82F6'); expect(darkTheme.colors.financial.income).toBe('#34D399'); expect(darkTheme.colors.financial.expense).toBe('#F87171'); expect(darkTheme.colors.financial.transfer).toBe('#60A5FA'); }); it('圆角含 xl(24),卡片默认大圆角', () => { for (const theme of [lightTheme, darkTheme]) { expect(theme.radii.sm).toBe(8); expect(theme.radii.md).toBe(12); expect(theme.radii.lg).toBe(16); expect(theme.radii.xl).toBe(24); expect(theme.radii.full).toBe(9999); } }); it('字阶含 display(34),且不指定 fontFamily(系统字体)', () => { for (const theme of [lightTheme, darkTheme] as ThemeTokens[]) { expect(theme.typography.display.fontSize).toBe(34); expect(theme.typography.h1.fontSize).toBe(28); expect(theme.typography.h2.fontSize).toBe(22); expect(theme.typography.h3.fontSize).toBe(17); for (const key of ['display', 'h1', 'h2', 'h3', 'body', 'bodySmall', 'caption'] as const) { expect(theme.typography[key].fontFamily).toBeUndefined(); } } }); it('presetThemes 注册表含 light 与 dark', () => { expect(presetThemes.light).toBe(lightTheme); expect(presetThemes.dark).toBe(darkTheme); }); it('spacing/shadows 结构完整', () => { for (const theme of [lightTheme, darkTheme] as ThemeTokens[]) { expect(theme.spacing).toHaveProperty('xs'); expect(theme.spacing).toHaveProperty('xl'); expect(theme.shadows).toHaveProperty('sm'); expect(theme.shadows).toHaveProperty('lg'); } }); }); describe('createTheme 自定义主题工厂', () => { it('未覆盖时等价于 lightTheme', () => { const custom = createTheme({}); expect(custom.colors.bgPrimary).toBe(lightTheme.colors.bgPrimary); expect(custom.spacing).toEqual(lightTheme.spacing); }); it('覆盖 accent 颜色时其他颜色保留', () => { const custom = createTheme({ colors: { ...lightTheme.colors, accent: '#FF5722' } }); expect(custom.colors.accent).toBe('#FF5722'); expect(custom.colors.bgPrimary).toBe(lightTheme.colors.bgPrimary); expect(custom.colors.success).toBe(lightTheme.colors.success); }); it('覆盖 financial 子对象时深度合并', () => { const custom = createTheme({ colors: { ...lightTheme.colors, financial: { ...lightTheme.colors.financial, income: '#000' } } }); expect(custom.colors.financial.income).toBe('#000'); expect(custom.colors.financial.expense).toBe(lightTheme.colors.financial.expense); }); it('覆盖 spacing 部分键时其他键保留', () => { const custom = createTheme({ spacing: { ...lightTheme.spacing, lg: 32 } }); expect(custom.spacing.lg).toBe(32); expect(custom.spacing.md).toBe(lightTheme.spacing.md); }); }); ``` - [ ] **Step 2: 新建 tests/palette.test.ts(分类色板 + 渠道品牌色)** ```typescript import { describe, expect, it } from 'vitest'; import { CATEGORY_PALETTE, getCategoryColor, CHANNEL_BRAND_COLORS } from '../src/theme/palette'; describe('categoryPalette', () => { it('色板为 12 色循环', () => { expect(CATEGORY_PALETTE).toHaveLength(12); for (const c of CATEGORY_PALETTE) expect(c).toMatch(/^#[0-9A-Fa-f]{6}$/); }); it('内置分类有具名颜色(与原 CategoryPicker 一致)', () => { expect(getCategoryColor('food')).toBe('#F59E0B'); expect(getCategoryColor('transport')).toBe('#3B82F6'); expect(getCategoryColor('salary')).toBe('#22C55E'); }); it('未知分类 id 走哈希循环,结果确定且在色板内', () => { const a = getCategoryColor('user_custom_abc'); expect(CATEGORY_PALETTE).toContain(a); expect(getCategoryColor('user_custom_abc')).toBe(a); // 幂等 }); it('空字符串 id 不崩溃', () => { expect(CATEGORY_PALETTE).toContain(getCategoryColor('')); }); it('渠道品牌色常量', () => { expect(CHANNEL_BRAND_COLORS.alipay).toBe('#1677FF'); expect(CHANNEL_BRAND_COLORS.wechat).toBe('#07C160'); expect(CHANNEL_BRAND_COLORS.bank).toBe('#E53935'); }); }); ``` - [ ] **Step 3: 运行测试确认失败** Run: `npx vitest run tests/theme.test.ts tests/palette.test.ts` Expected: FAIL —— `Cannot find module '../src/theme/palette'`,且 theme 断言多项不通过(旧色板)。 - [ ] **Step 4: Commit** ```bash git add tests/theme.test.ts tests/palette.test.ts git commit -m "test: P1 设计系统新令牌与色板的失败测试" ``` --- ### Task 2: tokens.ts — display 字阶 + radii.xl **Files:** - Modify: `src/theme/tokens.ts` - [ ] **Step 1: 修改 ThemeRadii 与 ThemeTypography** `ThemeRadii` 改为(加 `xl`): ```typescript export interface ThemeRadii { sm: number; md: number; lg: number; xl: number; // 卡片默认大圆角(24) full: number; } ``` `ThemeTypographyEntry` 的 `fontFamily` 标记废弃(保留字段以兼容现存 `theme.typography.X.fontFamily` 引用,后续阶段逐页清理): ```typescript export interface ThemeTypographyEntry { fontSize: number; fontWeight: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; lineHeight: number; /** @deprecated 设计系统已切换为系统字体,预置主题不再设置该字段;引用处将在 P4/P5 逐页移除。 */ fontFamily?: string; } ``` `ThemeTypography` 加 `display`: ```typescript export interface ThemeTypography { display: ThemeTypographyEntry; // 34/800,净资产等大数字 h1: ThemeTypographyEntry; h2: ThemeTypographyEntry; h3: ThemeTypographyEntry; body: ThemeTypographyEntry; bodySmall: ThemeTypographyEntry; caption: ThemeTypographyEntry; } ``` - [ ] **Step 2: 运行 typecheck 确认报错点** Run: `npm run typecheck` Expected: FAIL —— `presets.ts` 缺少 `display` 与 `radii.xl`(Task 3 修复)。 - [ ] **Step 3: 暂不 commit(与 Task 3 一起)** --- ### Task 3: presets.ts — 新双主题色板 **Files:** - Modify: `src/theme/presets.ts` - [ ] **Step 1: 完整替换 presets.ts** ```typescript import type { ThemeTokens } from './tokens'; /** * 预置主题 —— 「明亮 Bento 现代风」(docs/ui-redesign-design.md §3)。 * 浅色为默认;暗色为 OLED 纯黑完整对等主题。 * accent 为近黑白单色:品牌感靠排版与财务语义色表达。 */ export const lightTheme: ThemeTokens = { colors: { bgPrimary: '#F6F7F9', bgSecondary: '#FFFFFF', bgTertiary: '#EFF1F4', fgPrimary: '#111318', fgSecondary: '#6B7280', fgInverse: '#FFFFFF', accent: '#111318', // 近黑:按钮/选中态 accentLight: 'rgba(17,19,24,0.08)', // accent 8% 底色(选中高亮) accentDark: '#000000', // 按压加深态 success: '#10B981', warning: '#F59E0B', error: '#EF4444', info: '#3B82F6', financial: { income: '#10B981', expense: '#EF4444', transfer: '#3B82F6', }, border: '#E5E7EB', divider: '#F0F1F3', overlay: 'rgba(17,19,24,0.4)', skeleton: '#E5E7EB', progressBg: 'rgba(17,19,24,0.06)', }, spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 }, radii: { sm: 8, md: 12, lg: 16, xl: 24, full: 9999 }, typography: { display: { fontSize: 34, fontWeight: '800', lineHeight: 42 }, h1: { fontSize: 28, fontWeight: '700', lineHeight: 36 }, h2: { fontSize: 22, fontWeight: '700', lineHeight: 30 }, h3: { fontSize: 17, fontWeight: '600', lineHeight: 24 }, body: { fontSize: 16, fontWeight: '400', lineHeight: 24 }, bodySmall: { fontSize: 14, fontWeight: '400', lineHeight: 20 }, caption: { fontSize: 12, fontWeight: '400', lineHeight: 16 }, }, shadows: { sm: { shadowColor: '#111318', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.04, shadowRadius: 4, elevation: 1 }, md: { shadowColor: '#111318', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.06, shadowRadius: 8, elevation: 2 }, lg: { shadowColor: '#111318', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.10, shadowRadius: 12, elevation: 4 }, }, }; export const darkTheme: ThemeTokens = { ...lightTheme, colors: { bgPrimary: '#040508', // OLED 纯黑 bgSecondary: '#101218', bgTertiary: '#1A1D24', fgPrimary: '#F3F4F6', fgSecondary: '#9CA3AF', fgInverse: '#111318', accent: '#F3F4F6', // 反转为白 accentLight: 'rgba(243,244,246,0.10)', accentDark: '#FFFFFF', success: '#34D399', warning: '#FBBF24', error: '#F87171', info: '#60A5FA', financial: { income: '#34D399', // 提亮一档保证对比度 expense: '#F87171', transfer: '#60A5FA', }, border: 'rgba(255,255,255,0.08)', divider: 'rgba(255,255,255,0.04)', overlay: 'rgba(0,0,0,0.7)', skeleton: '#1A1D24', progressBg: 'rgba(243,244,246,0.08)', }, shadows: { sm: { shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.4, shadowRadius: 2, elevation: 1 }, md: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.5, shadowRadius: 4, elevation: 3 }, lg: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.6, shadowRadius: 8, elevation: 5 }, }, }; /** 预置主题注册表。 */ export const presetThemes: Record = { light: lightTheme, dark: darkTheme, }; ``` - [ ] **Step 2: 运行 Task 1 的测试** Run: `npx vitest run tests/theme.test.ts` Expected: PASS(palette.test.ts 仍 FAIL,Task 4 修复)。 - [ ] **Step 3: 运行 typecheck** Run: `npm run typecheck` Expected: PASS。 - [ ] **Step 4: Commit** ```bash git add src/theme/tokens.ts src/theme/presets.ts git commit -m "feat(theme): P1 明亮 Bento 令牌 —— 近黑 accent、xl 圆角、display 字阶、系统字体" ``` --- ### Task 4: theme/palette.ts — 分类色板 + 渠道品牌色 **Files:** - Create: `src/theme/palette.ts` - Modify: `src/domain/channelConfig.ts:32,40,48` - Modify: `src/components/CategoryPicker.tsx:31-48,59` - [ ] **Step 1: 创建 src/theme/palette.ts(纯 TS,无 RN/domain 依赖,可被双方向引用)** ```typescript /** * 分类/渠道颜色统一色板(docs/ui-redesign-design.md §3.2)。 * * 纯 TS 模块,不依赖 React Native 与 domain 层,因此: * - components/(CategoryPicker 等)用它给分类配色; * - domain/channelConfig.ts 用它引用渠道品牌色; * 全工程的颜色字面量只允许存在于 presets.ts 与本文件(P5 grep 审计)。 */ /** 12 色循环色板:未知/用户自定义分类按 id 哈希取色。 */ export const CATEGORY_PALETTE = [ '#F59E0B', // Amber '#3B82F6', // Blue '#EC4899', // Pink '#06B6D4', // Cyan '#6366F1', // Indigo '#8B5CF6', // Purple '#10B981', // Emerald '#64748B', // Slate '#F43F5E', // Rose '#14B8A6', // Teal '#EF4444', // Red '#84CC16', // Lime ] as const; /** 内置分类的具名颜色(沿用原 CategoryPicker CATEGORY_COLORS 的映射,视觉不变)。 */ const NAMED_CATEGORY_COLORS: Record = { food: '#F59E0B', transport: '#3B82F6', shopping: '#EC4899', housing_utility: '#06B6D4', housing_rent: '#6366F1', housing_communication: '#8B5CF6', entertainment: '#10B981', services: '#64748B', personal_care: '#F43F5E', clothing: '#14B8A6', health: '#EF4444', learning: '#84CC16', salary: '#22C55E', income_activity: '#EF4444', income_investment: '#F59E0B', }; /** * 本地 FNV-1a 哈希。 * 注:domain/ledger.ts 也有 hash(),但 theme 层不反向依赖 domain 解析器,故保留这份 8 行实现。 */ function fnv1a(input: string): number { let h = 0x811c9dc5; for (let i = 0; i < input.length; i++) { h ^= input.charCodeAt(i); h = Math.imul(h, 0x01000193); } return h >>> 0; } /** 取分类颜色:具名映射优先,否则按 id 哈希在 12 色板内确定性循环。 */ export function getCategoryColor(categoryId: string): string { const named = NAMED_CATEGORY_COLORS[categoryId]; if (named) return named; return CATEGORY_PALETTE[fnv1a(categoryId) % CATEGORY_PALETTE.length]; } /** 渠道品牌色(供 domain/channelConfig.ts 引用)。 */ export const CHANNEL_BRAND_COLORS = { alipay: '#1677FF', wechat: '#07C160', bank: '#E53935', } as const; ``` - [ ] **Step 2: channelConfig.ts 改用品牌色常量** 文件顶部加 import: ```typescript import { CHANNEL_BRAND_COLORS } from '../theme/palette'; ``` 三处字面量替换:`color: '#1677FF'` → `color: CHANNEL_BRAND_COLORS.alipay`,`color: '#07C160'` → `color: CHANNEL_BRAND_COLORS.wechat`,`color: '#E53935'` → `color: CHANNEL_BRAND_COLORS.bank`。 - [ ] **Step 3: CategoryPicker.tsx 改用 getCategoryColor** 删除第 31–48 行的 `CATEGORY_COLORS` 常量(含 `fallback`),文件顶部加: ```typescript import { getCategoryColor } from '../theme/palette'; ``` 第 59 行 `const color = CATEGORY_COLORS[cat.id] || CATEGORY_COLORS.fallback;` 改为: ```typescript const color = getCategoryColor(cat.id); ``` - [ ] **Step 4: 运行测试** Run: `npx vitest run tests/palette.test.ts tests/channelConfig.test.ts` Expected: PASS(渠道色值未变,channelConfig 测试不受影响)。 - [ ] **Step 5: Commit** ```bash git add src/theme/palette.ts src/domain/channelConfig.ts src/components/CategoryPicker.tsx git commit -m "feat(theme): 分类/渠道颜色收归 theme/palette,消除组件与 domain 硬编码色" ``` --- ### Task 5: 移除自定义字体(Caveat/Quicksand) **Files:** - Modify: `src/app/_layout.tsx:27-39,70-79,370` - Modify: `package.json:14-15` - [ ] **Step 1: 删除 _layout.tsx 的字体导入** 删除第 27–39 行两个 `import { useFonts, ... } from '@expo-google-fonts/...'` 块。 - [ ] **Step 2: 删除 useFonts 调用** 删除第 70–79 行的 `const [fontsLoaded, fontError] = useFonts({...});`。 - [ ] **Step 3: 简化加载门禁** 第 370 行: ```typescript if (phase === 'loading' || (!fontsLoaded && !fontError)) { ``` 改为: ```typescript if (phase === 'loading') { ``` - [ ] **Step 4: 移除 package.json 依赖并重装** 删除 `package.json` 中 `"@expo-google-fonts/caveat"` 与 `"@expo-google-fonts/quicksand"` 两行,然后: Run: `npm install --legacy-peer-deps` Expected: lockfile 更新,无字体包残留。 - [ ] **Step 5: 验证无残留引用** Run: `grep -rn "expo-google-fonts\|Caveat_\|Quicksand_" src/ package.json` Expected: 无输出(typography 中 `theme.typography.X.fontFamily` 的运行时引用返回 undefined,RN 回退系统字体,无需在 P1 清理)。 - [ ] **Step 6: 全量测试 + typecheck** Run: `npm test && npm run typecheck` Expected: 全绿。 - [ ] **Step 7: Commit** ```bash git add src/app/_layout.tsx package.json package-lock.json git commit -m "feat(theme): 移除 Caveat/Quicksand 自定义字体,切换系统字体" ``` --- ### Task 6: commonStyles 适配新圆角 **Files:** - Modify: `src/theme/commonStyles.ts` - [ ] **Step 1: modalCard 圆角 lg → xl** `modalCard` 中 `borderRadius: theme.radii.lg, // 弹窗使用大圆角 lg (16px)` 改为: ```typescript borderRadius: theme.radii.xl, // 弹窗使用卡片级大圆角 xl (24px) ``` (`input` 的 `theme.radii.md` 现在即 12px,无需改动,仅更新注释 `// 输入框标准 radii 为 md (12px)`。) - [ ] **Step 2: 验证** Run: `npm run typecheck` Expected: PASS。 - [ ] **Step 3: Commit** ```bash git add src/theme/commonStyles.ts git commit -m "feat(theme): commonStyles 弹窗圆角升级为 xl(24)" ``` --- ### Task 7: 重写 MASTER.md 使其与实现一致 **Files:** - Modify: `design-system/beancount-mobile/MASTER.md` - [ ] **Step 1: 完整替换 MASTER.md** ```markdown # Beancount Mobile 设计系统(MASTER) > 本文件与 `src/theme/presets.ts` 一一对应,是实现的事实描述而非平行标准。 > 修改配色/字阶/圆角时必须先改 presets.ts,再同步本文件。 ## 设计方向:明亮 Bento 现代风 浅色为主、大圆角卡片(Bento)、近黑白单色 + 财务语义色点缀。 品牌感靠排版与财务色表达,不依赖彩色 accent。暗色为 OLED 纯黑完整对等主题。 ## 色板(= presets.ts) ### 浅色(默认) | Token | 值 | 用途 | |---|---|---| | bgPrimary | `#F6F7F9` | 页面背景 | | bgSecondary | `#FFFFFF` | 卡片 | | bgTertiary | `#EFF1F4` | 输入框 / chip | | fgPrimary | `#111318` | 主文字 | | fgSecondary | `#6B7280` | 次要文字 | | fgInverse | `#FFFFFF` | 反色文字 | | accent | `#111318` | 按钮 / 选中态(近黑) | | accentLight | `rgba(17,19,24,0.08)` | 选中高亮底色 | | accentDark | `#000000` | 按压态 | | financial.income | `#10B981` | 收入 | | financial.expense | `#EF4444` | 支出 | | financial.transfer | `#3B82F6` | 转账 | | border | `#E5E7EB` | 边框 | | overlay | `rgba(17,19,24,0.4)` | 遮罩 | ### 暗色(OLED) | Token | 值 | |---|---| | bgPrimary | `#040508` | | bgSecondary | `#101218` | | bgTertiary | `#1A1D24` | | fgPrimary / accent | `#F3F4F6` | | fgSecondary | `#9CA3AF` | | fgInverse | `#111318` | | financial.income | `#34D399`(提亮) | | financial.expense | `#F87171`(提亮) | | financial.transfer | `#60A5FA`(提亮) | | border | `rgba(255,255,255,0.08)` | 分类/渠道颜色见 `src/theme/palette.ts`(12 色循环 + 具名映射 + 渠道品牌色)。 全工程颜色字面量只允许存在于 `presets.ts` 与 `palette.ts`。 ## 字体 系统字体(iOS SF / Android Roboto),不加载自定义字体。 金额数字一律 `fontVariant: ['tabular-nums']` 等宽对齐。 字阶:display 34/800 · h1 28/700 · h2 22/700 · h3 17/600 · body 16/400 · bodySmall 14/400 · caption 12/400。 ## 圆角 / 间距 / 阴影 - 圆角:sm 8 · md 12 · lg 16 · **xl 24(卡片与弹窗默认)** · full - 间距:xs 4 · sm 8 · md 16 · lg 24 · xl 32 - 阴影:浅色 4–12px 弥散轻阴影;暗色以 1px 半透边框代替阴影 ## 图标 统一 Ionicons(`@expo/vector-icons`)。禁止用 emoji 充当图标; 分类图标 = Ionicon + 圆形彩色底(CategoryIcon 组件,P2 落地)。 ## 反模式 - 禁止在组件中写颜色字面量(`#xxxxxx` / `rgba(...)`),必须走 token - 禁止低对比文字(< 4.5:1) - 禁止瞬间状态变化,交互反馈 150–300ms - 禁止绕过 commonStyles 重复造 input/chip/modal 样式 ``` - [ ] **Step 2: Commit** ```bash git add design-system/beancount-mobile/MASTER.md git commit -m "docs: MASTER.md 重写为与实现一致的明亮 Bento 设计系统" ``` --- ### Task 8: P1 全量验收 - [ ] **Step 1: 全量测试** Run: `npm test` Expected: 全部通过(30+ 文件,含新 theme/palette 测试)。 - [ ] **Step 2: typecheck** Run: `npm run typecheck` Expected: 无错误。 - [ ] **Step 3: 审计** Run: `grep -rn "expo-google-fonts" src/ package.json` → 无输出 Run: `grep -rn "4F46E5\|EEF2FF\|3730A3" src/` → 无输出(靛蓝族清零) 已知遗留(不在 P1 处理,已在后续阶段计划内):`src/app/transaction/new.tsx:219` 的 `#2196F3`(P3 重写 new.tsx 时消除);`src/app/(tabs)/index.tsx` Hero 卡硬编码白字(P4 首页重写时消除);组件内 `fontFamily: theme.typography.X.fontFamily` 引用(P4/P5 逐页清理)。 - [ ] **Step 4: 手工走查(需要设备/模拟器)** Run: `npm run android`(或 expo start) 走查清单:浅色/暗色各过一遍四个 Tab —— 页面背景为米白/纯黑;chip 选中态为黑底白字/白底黑字;卡片圆角变大;无字体加载等待。 --- ## 后续计划(不在本文件) - **P2 组件库**:ScreenHeader / StatCard / AppTabBar / CategoryIcon / DatePickerField / FilterSheet / ManagementScreen - **P3 录入闭环**:NumpadSheet + 双腿账户选择 + 全局+ + transaction/new 重写 + SpeedDial 退役 - **P4 四个 Tab 页**:首页待办条 / 交易时间线 / 报表 anchor 统一 / 我的 4 分组 - **P5 管理页模板化 + 清零审计** 每阶段完成后基于实际代码编写下一阶段计划。