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
679 lines
22 KiB
Markdown
679 lines
22 KiB
Markdown
# 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<string, ThemeTokens> = {
|
||
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<string, string> = {
|
||
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 管理页模板化 + 清零审计**
|
||
|
||
每阶段完成后基于实际代码编写下一阶段计划。
|