feat(app): 实现完整的财务管家记账聊天与统计功能
- 新增App主组件,实现聊天、账本、统计三大功能模块切换 - 支持语音及文本输入,自动解析记账信息并生成交易记录 - 交易记录支持编辑、删除、提醒等操作,数据本地持久化 - 管家性格预设和自定义设定,包含头像上传和语气调整 - 会话展示支持关联交易显示,样式区分收入与支出 - 账本页添加日期分组交易列表及饼图统计展示 - 消息列表实现渐变背景和交互动画,提升用户体验 - 支持图片上传并同步发送给AI进行解析记账 - 底部导航实现三标签切换并同步显示对应内容 - 全局事务编辑弹窗支持直接调用和删除操作 - 增加环境变量示例和.gitignore规则,完善工程配置
This commit is contained in:
1686
docs/后端API接口设计文档.md
Normal file
1686
docs/后端API接口设计文档.md
Normal file
File diff suppressed because it is too large
Load Diff
907
docs/数据持久化接口文档.md
Normal file
907
docs/数据持久化接口文档.md
Normal file
@@ -0,0 +1,907 @@
|
||||
# 数据持久化接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档详细列出了智能账本应用中所有需要持久化数据的接口,包括存储键名、数据结构、操作方法和使用场景。应用采用浏览器 `localStorage` 作为持久化存储方案。
|
||||
|
||||
---
|
||||
|
||||
## 1. 交易记录持久化
|
||||
|
||||
### 存储键名
|
||||
```typescript
|
||||
'cui_transactions'
|
||||
```
|
||||
|
||||
### 数据类型
|
||||
```typescript
|
||||
Transaction[]
|
||||
```
|
||||
|
||||
### 数据结构
|
||||
```typescript
|
||||
interface Transaction {
|
||||
id: string; // 唯一标识符 (UUID)
|
||||
item: string; // 交易项目名称
|
||||
amount: number; // 交易金额
|
||||
type: TransactionType; // 交易类型: 'expense' | 'income'
|
||||
category: string; // 交易分类 (来自 ExpenseCategory 或 IncomeCategory)
|
||||
date: string; // 日期 (ISO格式 YYYY-MM-DD)
|
||||
timestamp: number; // 时间戳 (毫秒)
|
||||
}
|
||||
```
|
||||
|
||||
### 支持的分类
|
||||
|
||||
#### 支出分类 (ExpenseCategory)
|
||||
- 餐饮、购物、交通、住房、娱乐、医疗
|
||||
- 人情、学习、金融、孩子、宠物、汽车
|
||||
- 美容、经营、杂项
|
||||
|
||||
#### 收入分类 (IncomeCategory)
|
||||
- 工资、奖金、理财、兼职、红包、其他收入
|
||||
|
||||
### 操作接口
|
||||
|
||||
#### 读取
|
||||
```typescript
|
||||
// 位置: App.tsx L102
|
||||
const storedTransactions = localStorage.getItem('cui_transactions');
|
||||
if (storedTransactions) setTransactions(JSON.parse(storedTransactions));
|
||||
```
|
||||
|
||||
#### 写入
|
||||
```typescript
|
||||
// 位置: App.tsx L129
|
||||
useEffect(() => {
|
||||
localStorage.setItem('cui_transactions', JSON.stringify(transactions));
|
||||
}, [transactions]);
|
||||
```
|
||||
|
||||
#### 新增交易
|
||||
```typescript
|
||||
// 位置: App.tsx L192-204
|
||||
const newTransaction: Transaction = {
|
||||
id: uuidv4(),
|
||||
item: result.transaction.item,
|
||||
amount: result.transaction.amount,
|
||||
type: result.transaction.type,
|
||||
category: result.transaction.category,
|
||||
date: getLocalDateString(),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
setTransactions(prev => [...prev, newTransaction]);
|
||||
```
|
||||
|
||||
#### 更新交易
|
||||
```typescript
|
||||
// 位置: App.tsx L144-148
|
||||
const handleUpdateTransaction = (updated: Transaction) => {
|
||||
setTransactions(prev => prev.map(t => t.id === updated.id ? updated : t));
|
||||
};
|
||||
```
|
||||
|
||||
#### 删除交易
|
||||
```typescript
|
||||
// 位置: App.tsx L150-156
|
||||
const handleDeleteTransaction = (id: string) => {
|
||||
if (confirm('确定要删除这条记录吗?')) {
|
||||
setTransactions(prev => prev.filter(t => t.id !== id));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 使用场景
|
||||
- 聊天界面语音/文字输入记账
|
||||
- 图片识别账单自动记账
|
||||
- 账本页面手动编辑交易
|
||||
- 统计页面数据查询和分析
|
||||
- 日历视图显示每日交易
|
||||
|
||||
---
|
||||
|
||||
## 2. 聊天消息持久化
|
||||
|
||||
### 存储键名
|
||||
```typescript
|
||||
'cui_messages'
|
||||
```
|
||||
|
||||
### 数据类型
|
||||
```typescript
|
||||
Message[]
|
||||
```
|
||||
|
||||
### 数据结构
|
||||
```typescript
|
||||
interface Message {
|
||||
id: string; // 消息唯一标识符 (UUID)
|
||||
role: 'user' | 'assistant'; // 消息角色
|
||||
content: string; // 消息内容
|
||||
timestamp: number; // 时间戳 (毫秒)
|
||||
relatedTransactionId?: string; // 关联的交易ID (可选)
|
||||
}
|
||||
```
|
||||
|
||||
### 操作接口
|
||||
|
||||
#### 读取
|
||||
```typescript
|
||||
// 位置: App.tsx L103
|
||||
const storedMessages = localStorage.getItem('cui_messages');
|
||||
if (storedMessages) setMessages(JSON.parse(storedMessages));
|
||||
```
|
||||
|
||||
#### 写入
|
||||
```typescript
|
||||
// 位置: App.tsx L135-138
|
||||
useEffect(() => {
|
||||
localStorage.setItem('cui_messages', JSON.stringify(messages));
|
||||
if (activeTab === 'chat') scrollToBottom();
|
||||
}, [messages, activeTab]);
|
||||
```
|
||||
|
||||
#### 初始化欢迎消息
|
||||
```typescript
|
||||
// 位置: App.tsx L119-126
|
||||
if (!storedMessages) {
|
||||
setMessages([{
|
||||
id: 'init',
|
||||
role: 'assistant',
|
||||
content: `我是${aiSettings.name}。✨\n想记账?快说!🎤`,
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
}
|
||||
```
|
||||
|
||||
#### 新增用户消息
|
||||
```typescript
|
||||
// 位置: App.tsx L179-185
|
||||
const userMsg: Message = {
|
||||
id: uuidv4(),
|
||||
role: 'user',
|
||||
content: imageBase64 ? '已上传图片' : text,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
setMessages(prev => [...prev, userMsg]);
|
||||
```
|
||||
|
||||
#### 新增助手消息(含交易回执)
|
||||
```typescript
|
||||
// 位置: App.tsx L206-212
|
||||
setMessages(prev => [...prev, {
|
||||
id: uuidv4(),
|
||||
role: 'assistant',
|
||||
content: 'RECEIPT',
|
||||
timestamp: Date.now(),
|
||||
relatedTransactionId: newTransactionId
|
||||
}]);
|
||||
```
|
||||
|
||||
### 使用场景
|
||||
- 聊天界面显示对话历史
|
||||
- 显示交易回执卡片
|
||||
- 从聊天界面编辑/删除关联交易
|
||||
- 保留用户与AI的完整对话记录
|
||||
|
||||
---
|
||||
|
||||
## 3. 预算数据持久化
|
||||
|
||||
### 存储键名
|
||||
```typescript
|
||||
'cui_budget'
|
||||
```
|
||||
|
||||
### 数据类型
|
||||
```typescript
|
||||
Record<string, number>
|
||||
```
|
||||
|
||||
### 数据结构
|
||||
```typescript
|
||||
// 键值对形式,支持多预算项
|
||||
{
|
||||
'总预算': 3000,
|
||||
'餐饮预算': 1000,
|
||||
'购物预算': 500,
|
||||
// ... 更多预算项
|
||||
}
|
||||
```
|
||||
|
||||
### 操作接口
|
||||
|
||||
#### 读取(兼容旧版)
|
||||
```typescript
|
||||
// 位置: App.tsx L104, L111-114
|
||||
const storedBudget = localStorage.getItem('cui_budget');
|
||||
if (storedBudget) {
|
||||
const parsed = JSON.parse(storedBudget);
|
||||
setBudgets(typeof parsed === 'number' ? { '总预算': parsed } : parsed);
|
||||
}
|
||||
```
|
||||
|
||||
#### 写入
|
||||
```typescript
|
||||
// 位置: App.tsx L130
|
||||
useEffect(() => {
|
||||
localStorage.setItem('cui_budget', JSON.stringify(budgets));
|
||||
}, [budgets]);
|
||||
```
|
||||
|
||||
#### 更新预算
|
||||
```typescript
|
||||
// 通过 setBudgets 更新
|
||||
setBudgets({ ...budgets, '总预算': 5000 });
|
||||
```
|
||||
|
||||
### 使用场景
|
||||
- 账本页面显示月度预算对比
|
||||
- 预算超支提醒
|
||||
- 多项预算管理(未来扩展)
|
||||
|
||||
### 兼容性说明
|
||||
- 支持旧版单一预算值(纯数字)
|
||||
- 新版支持多预算项(对象格式)
|
||||
- 自动兼容转换
|
||||
|
||||
---
|
||||
|
||||
## 4. 账单提醒持久化
|
||||
|
||||
### 存储键名
|
||||
```typescript
|
||||
'cui_reminders'
|
||||
```
|
||||
|
||||
### 数据类型
|
||||
```typescript
|
||||
BillReminder[]
|
||||
```
|
||||
|
||||
### 数据结构
|
||||
```typescript
|
||||
interface BillReminder {
|
||||
id: string; // 唯一标识符 (UUID)
|
||||
title: string; // 提醒标题
|
||||
amount?: number; // 金额 (可选)
|
||||
frequency: 'monthly' | 'once'; // 频率: 每月 | 单次
|
||||
targetDate: string; // 目标日期: 'DD' (每月) 或 'YYYY-MM-DD' (单次)
|
||||
isCompleted: boolean; // 完成状态
|
||||
}
|
||||
```
|
||||
|
||||
### 操作接口
|
||||
|
||||
#### 读取
|
||||
```typescript
|
||||
// 位置: App.tsx L105
|
||||
const storedReminders = localStorage.getItem('cui_reminders');
|
||||
if (storedReminders) setReminders(JSON.parse(storedReminders));
|
||||
```
|
||||
|
||||
#### 写入
|
||||
```typescript
|
||||
// 位置: App.tsx L131
|
||||
useEffect(() => {
|
||||
localStorage.setItem('cui_reminders', JSON.stringify(reminders));
|
||||
}, [reminders]);
|
||||
```
|
||||
|
||||
#### 新增提醒
|
||||
```typescript
|
||||
// 位置: App.tsx L158-160
|
||||
const handleAddReminder = (reminder: BillReminder) => {
|
||||
setReminders(prev => [...prev, reminder]);
|
||||
};
|
||||
```
|
||||
|
||||
#### 更新提醒
|
||||
```typescript
|
||||
// 位置: App.tsx L162-164
|
||||
const handleUpdateReminder = (updated: BillReminder) => {
|
||||
setReminders(prev => prev.map(r => r.id === updated.id ? updated : r));
|
||||
};
|
||||
```
|
||||
|
||||
#### 删除提醒
|
||||
```typescript
|
||||
// 位置: App.tsx L166-170
|
||||
const handleDeleteReminder = (id: string) => {
|
||||
if (confirm('确定要删除这个提醒吗?')) {
|
||||
setReminders(prev => prev.filter(r => r.id !== id));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### 切换完成状态
|
||||
```typescript
|
||||
// 位置: App.tsx L172-174
|
||||
const handleToggleReminder = (id: string) => {
|
||||
setReminders(prev => prev.map(r =>
|
||||
r.id === id ? { ...r, isCompleted: !r.isCompleted } : r
|
||||
));
|
||||
};
|
||||
```
|
||||
|
||||
### 剩余天数计算
|
||||
```typescript
|
||||
// 位置: BillReminders.tsx L14-38
|
||||
export const getDaysRemaining = (r: BillReminder): number => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
let targetDate = new Date();
|
||||
targetDate.setHours(0, 0, 0, 0);
|
||||
|
||||
if (r.frequency === 'monthly') {
|
||||
const day = parseInt(r.targetDate);
|
||||
targetDate.setDate(day);
|
||||
|
||||
// 如果本月已过期,移至下月
|
||||
if (targetDate < today) {
|
||||
targetDate.setMonth(targetDate.getMonth() + 1);
|
||||
}
|
||||
} else {
|
||||
// 单次提醒
|
||||
targetDate = new Date(r.targetDate);
|
||||
targetDate.setHours(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
const diffTime = targetDate.getTime() - today.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
return diffDays;
|
||||
};
|
||||
```
|
||||
|
||||
### 使用场景
|
||||
- 月度账单还款提醒(房贷、信用卡等)
|
||||
- 一次性待办事项
|
||||
- 日历视图显示提醒日期
|
||||
- 账本页面待办列表
|
||||
- 逾期提醒和倒计时显示
|
||||
|
||||
---
|
||||
|
||||
## 5. AI助手设置持久化
|
||||
|
||||
### 存储键名
|
||||
```typescript
|
||||
'cui_ai_settings'
|
||||
```
|
||||
|
||||
### 数据类型
|
||||
```typescript
|
||||
AISettings
|
||||
```
|
||||
|
||||
### 数据结构
|
||||
```typescript
|
||||
interface AISettings {
|
||||
name: string; // AI助手名称
|
||||
persona: string; // AI性格描述
|
||||
avatar?: string; // 头像 (Base64编码图片,可选)
|
||||
}
|
||||
```
|
||||
|
||||
### 默认值
|
||||
```typescript
|
||||
// 位置: App.tsx L18-21
|
||||
const DEFAULT_AI_SETTINGS: AISettings = {
|
||||
name: "崔经理",
|
||||
persona: "一个精明、毒舌但实际上很负责任的财务经理。说话喜欢带刺,吐槽用户乱花钱,但关键时刻会给出靠谱建议。"
|
||||
};
|
||||
```
|
||||
|
||||
### 操作接口
|
||||
|
||||
#### 读取
|
||||
```typescript
|
||||
// 位置: App.tsx L106
|
||||
const storedAiSettings = localStorage.getItem('cui_ai_settings');
|
||||
if (storedAiSettings) setAiSettings(JSON.parse(storedAiSettings));
|
||||
```
|
||||
|
||||
#### 写入
|
||||
```typescript
|
||||
// 位置: App.tsx L132
|
||||
useEffect(() => {
|
||||
localStorage.setItem('cui_ai_settings', JSON.stringify(aiSettings));
|
||||
}, [aiSettings]);
|
||||
```
|
||||
|
||||
#### 更新设置
|
||||
```typescript
|
||||
// 修改名称
|
||||
setAiSettings({ ...aiSettings, name: '新名字' });
|
||||
|
||||
// 修改性格
|
||||
setAiSettings({ ...aiSettings, persona: '新的性格描述' });
|
||||
|
||||
// 更新头像
|
||||
setAiSettings(prev => ({ ...prev, avatar: base64Data }));
|
||||
```
|
||||
|
||||
### 预设性格列表
|
||||
```typescript
|
||||
// 位置: App.tsx L23-44
|
||||
[
|
||||
{
|
||||
id: 'toxic',
|
||||
name: '崔经理 (默认)',
|
||||
description: '精明、毒舌、傲娇。喜欢吐槽你乱花钱...'
|
||||
},
|
||||
{
|
||||
id: 'gentle',
|
||||
name: '温柔管家',
|
||||
description: '温柔、体贴、治愈。无论你怎么花钱都会温柔记录...'
|
||||
},
|
||||
{
|
||||
id: 'strict',
|
||||
name: '严厉会计',
|
||||
description: '一丝不苟、冷酷无情。只关心数字...'
|
||||
},
|
||||
{
|
||||
id: 'custom',
|
||||
name: '自定义',
|
||||
description: '可以根据你喜欢的方式设定助手的语气和风格'
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 使用场景
|
||||
- 聊天界面显示AI名称和头像
|
||||
- AI回复风格控制
|
||||
- Gemini API调用时的persona参数
|
||||
- 设置模态框显示和编辑
|
||||
|
||||
---
|
||||
|
||||
## 6. 自定义性格预设持久化
|
||||
|
||||
### 存储键名
|
||||
```typescript
|
||||
'cui_custom_preset_info'
|
||||
```
|
||||
|
||||
### 数据类型
|
||||
```typescript
|
||||
{ name: string; description: string }
|
||||
```
|
||||
|
||||
### 数据结构
|
||||
```typescript
|
||||
{
|
||||
name: '自定义', // 自定义预设名称
|
||||
description: '可以根据你喜欢的方式设定助手的语气和风格' // 描述
|
||||
}
|
||||
```
|
||||
|
||||
### 操作接口
|
||||
|
||||
#### 读取
|
||||
```typescript
|
||||
// 位置: App.tsx L107
|
||||
const storedCustom = localStorage.getItem('cui_custom_preset_info');
|
||||
if (storedCustom) setCustomPreset(JSON.parse(storedCustom));
|
||||
```
|
||||
|
||||
#### 写入
|
||||
```typescript
|
||||
// 位置: App.tsx L133
|
||||
useEffect(() => {
|
||||
localStorage.setItem('cui_custom_preset_info', JSON.stringify(customPreset));
|
||||
}, [customPreset]);
|
||||
```
|
||||
|
||||
#### 自动保存自定义预设
|
||||
```typescript
|
||||
// 位置: App.tsx L286-292
|
||||
const handleSaveSettings = () => {
|
||||
const currentPreset = INITIAL_PERSONA_PRESETS.find(
|
||||
p => p.description === aiSettings.persona && p.id !== 'custom'
|
||||
);
|
||||
if (!currentPreset) {
|
||||
setCustomPreset({ name: aiSettings.name, description: aiSettings.persona });
|
||||
}
|
||||
setIsSettingsOpen(false);
|
||||
};
|
||||
```
|
||||
|
||||
### 使用场景
|
||||
- 保存用户自定义的AI性格设置
|
||||
- 性格预设列表中显示自定义预设
|
||||
- 区分预设性格和自定义性格
|
||||
|
||||
---
|
||||
|
||||
## 7. 主题设置持久化
|
||||
|
||||
### 存储键名
|
||||
```typescript
|
||||
'cui_card_theme'
|
||||
```
|
||||
|
||||
### 数据类型
|
||||
```typescript
|
||||
string (数字索引)
|
||||
```
|
||||
|
||||
### 数据值
|
||||
```typescript
|
||||
'0' | '1' | '2' | '3' | '4' // 对应5个主题的索引
|
||||
```
|
||||
|
||||
### 主题列表
|
||||
```typescript
|
||||
// 位置: LedgerPage.tsx L20-26
|
||||
const CARD_THEMES = [
|
||||
{ id: 'dark-indigo', class: 'from-gray-900 via-indigo-950 to-black', themeColor: 'bg-indigo-600', name: '暗夜霓虹' },
|
||||
{ id: 'sunset', class: 'from-orange-500 via-rose-600 to-pink-700', themeColor: 'bg-rose-600', name: '落日熔金' },
|
||||
{ id: 'emerald', class: 'from-emerald-600 via-teal-700 to-cyan-900', themeColor: 'bg-emerald-600', name: '薄荷森林' },
|
||||
{ id: 'royal', class: 'from-blue-700 via-indigo-800 to-indigo-950', themeColor: 'bg-blue-700', name: '皇家礼赞' },
|
||||
{ id: 'rose', class: 'from-rose-500 via-pink-600 to-purple-800', themeColor: 'bg-purple-600', name: '浪漫巴黎' },
|
||||
];
|
||||
```
|
||||
|
||||
### 操作接口
|
||||
|
||||
#### 读取
|
||||
```typescript
|
||||
// 位置: LedgerPage.tsx L41
|
||||
const [themeIndex, setThemeIndex] = useState(() =>
|
||||
Number(localStorage.getItem('cui_card_theme')) || 0
|
||||
);
|
||||
```
|
||||
|
||||
#### 写入
|
||||
```typescript
|
||||
// 位置: LedgerPage.tsx L43-45
|
||||
useEffect(() => {
|
||||
localStorage.setItem('cui_card_theme', themeIndex.toString());
|
||||
}, [themeIndex]);
|
||||
```
|
||||
|
||||
#### 切换主题
|
||||
```typescript
|
||||
// 位置: LedgerPage.tsx L56
|
||||
const cycleTheme = () => setThemeIndex((prev) => (prev + 1) % CARD_THEMES.length);
|
||||
```
|
||||
|
||||
### 使用场景
|
||||
- 账本页面顶部卡片主题
|
||||
- 标签页按钮颜色主题
|
||||
- 用户个性化界面体验
|
||||
|
||||
---
|
||||
|
||||
## 8. AI服务接口(非持久化,但涉及数据处理)
|
||||
|
||||
### Gemini API 调用接口
|
||||
|
||||
#### 服务文件
|
||||
```typescript
|
||||
services/geminiService.ts
|
||||
```
|
||||
|
||||
#### 处理用户消息
|
||||
```typescript
|
||||
// 位置: geminiService.ts L67-115
|
||||
export const processUserMessage = async (
|
||||
message: string,
|
||||
aiName: string,
|
||||
aiPersona: string,
|
||||
imageBase64?: string,
|
||||
mimeType?: string
|
||||
): Promise<ParseResult>
|
||||
```
|
||||
|
||||
#### 返回结果
|
||||
```typescript
|
||||
// 位置: geminiService.ts L57-65
|
||||
interface ParseResult {
|
||||
transaction: {
|
||||
item: string;
|
||||
amount: number;
|
||||
type: TransactionType;
|
||||
category: string;
|
||||
} | null;
|
||||
reply: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### API模型配置
|
||||
```typescript
|
||||
// 位置: geminiService.ts L4-7
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
const MODEL_NAME = "gemini-3-flash-preview";
|
||||
```
|
||||
|
||||
#### 系统指令模板
|
||||
```typescript
|
||||
// 位置: geminiService.ts L9-29
|
||||
const getSystemInstruction = (aiName: string, aiPersona: string) => `
|
||||
你现在的身份是"${aiName}"。
|
||||
你的性格设定是:${aiPersona}
|
||||
|
||||
你的任务是:
|
||||
1. 从用户的输入中提取交易信息(内容、金额、类别、交易类型)。
|
||||
2. 判断是【支出】(expense) 还是【收入】(income)。
|
||||
3. **严格回复规范**:
|
||||
- 回复必须**极度简练**。
|
||||
- 每一段话的字数必须控制在 **30字左右**(越短越好)。
|
||||
- 全部的回复**最多只能分成 2 段**(中间用一个 \n 换行符隔开)。
|
||||
- 必须在每一段中都加入**生动的表情包 (Emoji)** 🌈。
|
||||
- 说话风格要完全符合"${aiName}"的设定。
|
||||
`;
|
||||
```
|
||||
|
||||
### 使用场景
|
||||
- 语音/文字输入转交易记录
|
||||
- 图片识别账单信息
|
||||
- AI对话回复生成
|
||||
- 自动分类和金额提取
|
||||
|
||||
---
|
||||
|
||||
## 数据流架构图
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph 浏览器存储层
|
||||
LS[localStorage]
|
||||
LS --> T[cui_transactions]
|
||||
LS --> M[cui_messages]
|
||||
LS --> B[cui_budget]
|
||||
LS --> R[cui_reminders]
|
||||
LS --> AS[cui_ai_settings]
|
||||
LS --> CP[cui_custom_preset_info]
|
||||
LS --> TH[cui_card_theme]
|
||||
end
|
||||
|
||||
subgraph React状态层
|
||||
ST[State Management]
|
||||
ST --> transactions[transactions]
|
||||
ST --> messages[messages]
|
||||
ST --> budgets[budgets]
|
||||
ST --> reminders[reminders]
|
||||
ST --> aiSettings[aiSettings]
|
||||
ST --> customPreset[customPreset]
|
||||
ST --> themeIndex[themeIndex]
|
||||
end
|
||||
|
||||
subgraph 组件层
|
||||
APP[App.tsx]
|
||||
LEDGER[LedgerPage.tsx]
|
||||
EDIT[EditTransactionModal.tsx]
|
||||
REMIND[BillReminders.tsx]
|
||||
CAL[CalendarView.tsx]
|
||||
STATS[StatisticsPage.tsx]
|
||||
end
|
||||
|
||||
subgraph 外部服务
|
||||
GEMINI[Gemini API]
|
||||
end
|
||||
|
||||
T <--> transactions
|
||||
M <--> messages
|
||||
B <--> budgets
|
||||
R <--> reminders
|
||||
AS <--> aiSettings
|
||||
CP <--> customPreset
|
||||
TH <--> themeIndex
|
||||
|
||||
transactions --> APP
|
||||
transactions --> LEDGER
|
||||
transactions --> STATS
|
||||
transactions --> CAL
|
||||
|
||||
messages --> APP
|
||||
reminders --> LEDGER
|
||||
reminders --> REMIND
|
||||
reminders --> CAL
|
||||
|
||||
APP --> GEMINI
|
||||
GEMINI --> APP
|
||||
|
||||
APP --> EDIT
|
||||
LEDGER --> EDIT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据大小估算
|
||||
|
||||
### 单条记录大小
|
||||
- **Transaction**: ~150 bytes
|
||||
- **Message**: ~100 bytes
|
||||
- **BillReminder**: ~120 bytes
|
||||
- **AISettings**: ~200 bytes (不含头像) / ~5KB (含头像)
|
||||
|
||||
### 容量预估(localStorage限制 5-10MB)
|
||||
- **1000条交易**: ~150KB
|
||||
- **500条消息**: ~50KB
|
||||
- **50个提醒**: ~6KB
|
||||
- **AI设置**: ~5KB (含头像)
|
||||
|
||||
**总计**: 约 211KB (不含聊天历史图片)
|
||||
|
||||
### 容量优化建议
|
||||
1. 定期归档历史数据(超过1年)
|
||||
2. 限制聊天历史记录数量(如保留最近500条)
|
||||
3. 图片压缩后再转Base64
|
||||
4. 实现数据导出/导入功能
|
||||
|
||||
---
|
||||
|
||||
## 错误处理与兼容性
|
||||
|
||||
### JSON解析容错
|
||||
```typescript
|
||||
try {
|
||||
const data = JSON.parse(localStorage.getItem('cui_transactions'));
|
||||
setTransactions(data);
|
||||
} catch (error) {
|
||||
console.error('数据解析失败', error);
|
||||
setTransactions([]); // 回退到默认值
|
||||
}
|
||||
```
|
||||
|
||||
### 版本兼容
|
||||
- **预算数据**: 自动兼容旧版纯数字格式
|
||||
- **新增字段**: 使用可选字段(如 BillReminder.amount?)
|
||||
- **默认值**: 所有状态都有合理的初始值
|
||||
|
||||
### 数据校验建议
|
||||
```typescript
|
||||
// 类型守卫示例
|
||||
function isValidTransaction(obj: any): obj is Transaction {
|
||||
return (
|
||||
typeof obj.id === 'string' &&
|
||||
typeof obj.item === 'string' &&
|
||||
typeof obj.amount === 'number' &&
|
||||
['expense', 'income'].includes(obj.type) &&
|
||||
typeof obj.date === 'string'
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据备份与恢复
|
||||
|
||||
### 导出数据(建议实现)
|
||||
```typescript
|
||||
function exportAllData() {
|
||||
const data = {
|
||||
transactions: JSON.parse(localStorage.getItem('cui_transactions') || '[]'),
|
||||
messages: JSON.parse(localStorage.getItem('cui_messages') || '[]'),
|
||||
budgets: JSON.parse(localStorage.getItem('cui_budget') || '{}'),
|
||||
reminders: JSON.parse(localStorage.getItem('cui_reminders') || '[]'),
|
||||
aiSettings: JSON.parse(localStorage.getItem('cui_ai_settings') || '{}'),
|
||||
customPreset: JSON.parse(localStorage.getItem('cui_custom_preset_info') || '{}'),
|
||||
themeIndex: localStorage.getItem('cui_card_theme') || '0',
|
||||
exportDate: new Date().toISOString()
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `账本数据备份_${new Date().toISOString().slice(0,10)}.json`;
|
||||
a.click();
|
||||
}
|
||||
```
|
||||
|
||||
### 导入数据(建议实现)
|
||||
```typescript
|
||||
function importAllData(file: File) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.target?.result as string);
|
||||
localStorage.setItem('cui_transactions', JSON.stringify(data.transactions));
|
||||
localStorage.setItem('cui_messages', JSON.stringify(data.messages));
|
||||
localStorage.setItem('cui_budget', JSON.stringify(data.budgets));
|
||||
localStorage.setItem('cui_reminders', JSON.stringify(data.reminders));
|
||||
localStorage.setItem('cui_ai_settings', JSON.stringify(data.aiSettings));
|
||||
localStorage.setItem('cui_custom_preset_info', JSON.stringify(data.customPreset));
|
||||
localStorage.setItem('cui_card_theme', data.themeIndex);
|
||||
|
||||
alert('数据导入成功!请刷新页面');
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
alert('数据导入失败:' + error.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安全性考虑
|
||||
|
||||
### 敏感数据
|
||||
- **交易记录**: 包含个人财务信息
|
||||
- **AI头像**: Base64图片数据
|
||||
- **聊天记录**: 可能包含敏感对话
|
||||
|
||||
### 安全建议
|
||||
1. **不存储**: Gemini API Key(使用环境变量)
|
||||
2. **加密存储**: 考虑对敏感数据加密(如使用Web Crypto API)
|
||||
3. **定期清理**: 提供清除所有数据的功能
|
||||
4. **隐私模式**: 提示用户不要在公共设备上使用
|
||||
|
||||
### 清除数据示例
|
||||
```typescript
|
||||
function clearAllData() {
|
||||
if (confirm('确定要清除所有数据吗?此操作不可恢复!')) {
|
||||
localStorage.removeItem('cui_transactions');
|
||||
localStorage.removeItem('cui_messages');
|
||||
localStorage.removeItem('cui_budget');
|
||||
localStorage.removeItem('cui_reminders');
|
||||
localStorage.removeItem('cui_ai_settings');
|
||||
localStorage.removeItem('cui_custom_preset_info');
|
||||
localStorage.removeItem('cui_card_theme');
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 批量写入优化
|
||||
```typescript
|
||||
// 避免频繁写入,使用防抖
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const debouncedSave = debounce((key: string, data: any) => {
|
||||
localStorage.setItem(key, JSON.stringify(data));
|
||||
}, 500);
|
||||
```
|
||||
|
||||
### 2. 数据分页加载
|
||||
```typescript
|
||||
// 仅加载当前月份数据到UI
|
||||
const currentMonthTransactions = transactions.filter(
|
||||
t => t.date.startsWith(selectedMonth)
|
||||
);
|
||||
```
|
||||
|
||||
### 3. 索引优化
|
||||
```typescript
|
||||
// 创建日期索引,加速查询
|
||||
const transactionsByDate = useMemo(() => {
|
||||
return transactions.reduce((acc, t) => {
|
||||
if (!acc[t.date]) acc[t.date] = [];
|
||||
acc[t.date].push(t);
|
||||
return acc;
|
||||
}, {} as Record<string, Transaction[]>);
|
||||
}, [transactions]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
本文档详细列出了应用中所有7个持久化数据接口:
|
||||
|
||||
1. ✅ **交易记录** (cui_transactions) - 核心财务数据
|
||||
2. ✅ **聊天消息** (cui_messages) - AI对话历史
|
||||
3. ✅ **预算数据** (cui_budget) - 预算管理
|
||||
4. ✅ **账单提醒** (cui_reminders) - 待办事项
|
||||
5. ✅ **AI设置** (cui_ai_settings) - AI助手配置
|
||||
6. ✅ **自定义预设** (cui_custom_preset_info) - 性格预设
|
||||
7. ✅ **主题设置** (cui_card_theme) - 界面主题
|
||||
|
||||
所有接口均采用 **localStorage** 存储,使用 **JSON** 序列化,并通过 **React useEffect** 实现自动持久化。
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v1.0
|
||||
**生成时间**: 2026-01-19
|
||||
**最后更新**: 2026-01-19
|
||||
**维护者**: 开发团队
|
||||
104
docs/数据结构.md
Normal file
104
docs/数据结构.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# App_02_记账
|
||||
- 应用信息
|
||||
- 应用名称
|
||||
- 记账
|
||||
- 英文标识
|
||||
- bill
|
||||
- 应用编号
|
||||
- app_02
|
||||
- 应用描述
|
||||
- 一个精明、毒舌但实际上很负责任的财务经理。说话喜欢带刺,吐槽用户乱花钱,但关键时刻会给出靠谱建议。
|
||||
- 数据结构
|
||||
- Project 项目分类
|
||||
- name 名称(name)
|
||||
- desc 背景(context)
|
||||
- target 目标(goal)
|
||||
- value 价值(value)
|
||||
- creator 创建人
|
||||
- Member
|
||||
- Task 任务项目
|
||||
- title 标题(title)
|
||||
- project 分类(projectId)
|
||||
- Project
|
||||
- desc 描述(notes)
|
||||
- startDate 开始日期(date)
|
||||
- startTime 开始时间(startTime)
|
||||
- isCompleted 是否完成(isCompleted)
|
||||
- created_at 创建时间(createdAt)
|
||||
- creator 创建人
|
||||
- Member
|
||||
- API接口
|
||||
- auth 授权
|
||||
- POST /mp/auth/get_token
|
||||
- 获取token
|
||||
- GET /mp/auth/profile
|
||||
- 获取个人信息
|
||||
- 项目
|
||||
- GET /mp/todo/projects
|
||||
- 项目列表
|
||||
- GET /mp/todo/projects/{project_id}
|
||||
- 获取项目详细
|
||||
- POST /mp/todo/projects
|
||||
- 创建项目
|
||||
- PUT /mp/todo/projects/{project_id}
|
||||
- 更新项目
|
||||
- DELETE /mp/todo/projects/{project_id}
|
||||
- 删除项目
|
||||
- 任务
|
||||
- GET /mp/todo/tasks
|
||||
- 任务列表
|
||||
- GET /mp/todo/tasks/{task_id}
|
||||
- 获取任务详细
|
||||
- POST /mp/todo/tasks
|
||||
- 创建任务
|
||||
- PUT /mp/todo/tasks/{task_id}
|
||||
- 更新任务
|
||||
- DELETE /mp/todo/tasks/{task_id}
|
||||
- 删除项目
|
||||
- PUT /mp/todo/tasks/{task_id}/completed
|
||||
- 设置为已完成
|
||||
- PUT /mp/todo/tasks/{task_id}/uncompleted
|
||||
- 设置为未完成
|
||||
- 问题/需求
|
||||
- 单用户转换为多用户模式
|
||||
- 增加登录验证
|
||||
- 增加creator字段
|
||||
- 登录问题
|
||||
- 小程序跳转到H5, 需要检测登录状态
|
||||
- 如果已经登录, 需要把登录信息传递到H5首页
|
||||
- 如果未登录跳转至小程序登录页
|
||||
- URL传递参数openid和mobile
|
||||
- H5端首页先检测token状态
|
||||
- 如果存在并且未过期, 则携带token获取用户信息
|
||||
- 如果不存在则通过openid和mobile获取token, 并获取用户信息
|
||||
- 新建项目问题
|
||||
- 目前是实时更新模式, 保存到本地缓存没问题, 但是要持久化到数据库就会报错
|
||||
- 增加保存按钮, 点击保存后提交接口, 并同步本地数据
|
||||
- 分别处理本地新建项目和提交接口新建项目的逻辑, 并处理错误提示, 例如项目名称重复
|
||||
- 任务过滤查询
|
||||
- 日期等于今天的任务
|
||||
- 日期小于今日的任务
|
||||
- 查询所有任务匹配到日历中
|
||||
- 今日任务和日历中任务的匹配调节
|
||||
- 测试任务2没有显示在今日任务中
|
||||
- 最可能原因是日期转换过程中的时区差异。当API返回的日期时间字符串 "2026-01-14T00:00:00" 在客户端进行时区转换时,可能会因为客户端的时区设置导致日期变成 2026-01-13 或其他日期,从而不满足 t.date === systemDate 的条件。
|
||||
- 多个在增删改查操作中存在的时区转换问题
|
||||
- 开发任务
|
||||
- 小程序跳转时增加登录判断和URL传参(openid和mobile)
|
||||
- 1小时/人
|
||||
- H5首页接收openid和mobile参数
|
||||
- 0.5小时/人
|
||||
- H5首页token检测并获取, 通过接口获取用户信息
|
||||
- 1.5小时/人
|
||||
- 开发H5端获取token接口
|
||||
- 2小时/人
|
||||
- 开发H5端获取用户信息接口
|
||||
- 1.5小时/人
|
||||
- 开发项目增删改查接口
|
||||
- 4小时/人
|
||||
- 开发任务增删改查接口和已完成/未完成接口
|
||||
- 5小时/人
|
||||
- 开发任务按时间过滤列表接口
|
||||
- 2小时/人
|
||||
- 调试H5页面交互, 调用项目和任务接口
|
||||
- 6~10小时/人
|
||||
Reference in New Issue
Block a user