- 新增App主组件,实现聊天、账本、统计三大功能模块切换 - 支持语音及文本输入,自动解析记账信息并生成交易记录 - 交易记录支持编辑、删除、提醒等操作,数据本地持久化 - 管家性格预设和自定义设定,包含头像上传和语气调整 - 会话展示支持关联交易显示,样式区分收入与支出 - 账本页添加日期分组交易列表及饼图统计展示 - 消息列表实现渐变背景和交互动画,提升用户体验 - 支持图片上传并同步发送给AI进行解析记账 - 底部导航实现三标签切换并同步显示对应内容 - 全局事务编辑弹窗支持直接调用和删除操作 - 增加环境变量示例和.gitignore规则,完善工程配置
22 KiB
22 KiB
数据持久化接口文档
概述
本文档详细列出了智能账本应用中所有需要持久化数据的接口,包括存储键名、数据结构、操作方法和使用场景。应用采用浏览器 localStorage 作为持久化存储方案。
1. 交易记录持久化
存储键名
'cui_transactions'
数据类型
Transaction[]
数据结构
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)
- 工资、奖金、理财、兼职、红包、其他收入
操作接口
读取
// 位置: App.tsx L102
const storedTransactions = localStorage.getItem('cui_transactions');
if (storedTransactions) setTransactions(JSON.parse(storedTransactions));
写入
// 位置: App.tsx L129
useEffect(() => {
localStorage.setItem('cui_transactions', JSON.stringify(transactions));
}, [transactions]);
新增交易
// 位置: 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]);
更新交易
// 位置: App.tsx L144-148
const handleUpdateTransaction = (updated: Transaction) => {
setTransactions(prev => prev.map(t => t.id === updated.id ? updated : t));
};
删除交易
// 位置: App.tsx L150-156
const handleDeleteTransaction = (id: string) => {
if (confirm('确定要删除这条记录吗?')) {
setTransactions(prev => prev.filter(t => t.id !== id));
}
};
使用场景
- 聊天界面语音/文字输入记账
- 图片识别账单自动记账
- 账本页面手动编辑交易
- 统计页面数据查询和分析
- 日历视图显示每日交易
2. 聊天消息持久化
存储键名
'cui_messages'
数据类型
Message[]
数据结构
interface Message {
id: string; // 消息唯一标识符 (UUID)
role: 'user' | 'assistant'; // 消息角色
content: string; // 消息内容
timestamp: number; // 时间戳 (毫秒)
relatedTransactionId?: string; // 关联的交易ID (可选)
}
操作接口
读取
// 位置: App.tsx L103
const storedMessages = localStorage.getItem('cui_messages');
if (storedMessages) setMessages(JSON.parse(storedMessages));
写入
// 位置: App.tsx L135-138
useEffect(() => {
localStorage.setItem('cui_messages', JSON.stringify(messages));
if (activeTab === 'chat') scrollToBottom();
}, [messages, activeTab]);
初始化欢迎消息
// 位置: App.tsx L119-126
if (!storedMessages) {
setMessages([{
id: 'init',
role: 'assistant',
content: `我是${aiSettings.name}。✨\n想记账?快说!🎤`,
timestamp: Date.now()
}]);
}
新增用户消息
// 位置: App.tsx L179-185
const userMsg: Message = {
id: uuidv4(),
role: 'user',
content: imageBase64 ? '已上传图片' : text,
timestamp: Date.now()
};
setMessages(prev => [...prev, userMsg]);
新增助手消息(含交易回执)
// 位置: App.tsx L206-212
setMessages(prev => [...prev, {
id: uuidv4(),
role: 'assistant',
content: 'RECEIPT',
timestamp: Date.now(),
relatedTransactionId: newTransactionId
}]);
使用场景
- 聊天界面显示对话历史
- 显示交易回执卡片
- 从聊天界面编辑/删除关联交易
- 保留用户与AI的完整对话记录
3. 预算数据持久化
存储键名
'cui_budget'
数据类型
Record<string, number>
数据结构
// 键值对形式,支持多预算项
{
'总预算': 3000,
'餐饮预算': 1000,
'购物预算': 500,
// ... 更多预算项
}
操作接口
读取(兼容旧版)
// 位置: App.tsx L104, L111-114
const storedBudget = localStorage.getItem('cui_budget');
if (storedBudget) {
const parsed = JSON.parse(storedBudget);
setBudgets(typeof parsed === 'number' ? { '总预算': parsed } : parsed);
}
写入
// 位置: App.tsx L130
useEffect(() => {
localStorage.setItem('cui_budget', JSON.stringify(budgets));
}, [budgets]);
更新预算
// 通过 setBudgets 更新
setBudgets({ ...budgets, '总预算': 5000 });
使用场景
- 账本页面显示月度预算对比
- 预算超支提醒
- 多项预算管理(未来扩展)
兼容性说明
- 支持旧版单一预算值(纯数字)
- 新版支持多预算项(对象格式)
- 自动兼容转换
4. 账单提醒持久化
存储键名
'cui_reminders'
数据类型
BillReminder[]
数据结构
interface BillReminder {
id: string; // 唯一标识符 (UUID)
title: string; // 提醒标题
amount?: number; // 金额 (可选)
frequency: 'monthly' | 'once'; // 频率: 每月 | 单次
targetDate: string; // 目标日期: 'DD' (每月) 或 'YYYY-MM-DD' (单次)
isCompleted: boolean; // 完成状态
}
操作接口
读取
// 位置: App.tsx L105
const storedReminders = localStorage.getItem('cui_reminders');
if (storedReminders) setReminders(JSON.parse(storedReminders));
写入
// 位置: App.tsx L131
useEffect(() => {
localStorage.setItem('cui_reminders', JSON.stringify(reminders));
}, [reminders]);
新增提醒
// 位置: App.tsx L158-160
const handleAddReminder = (reminder: BillReminder) => {
setReminders(prev => [...prev, reminder]);
};
更新提醒
// 位置: App.tsx L162-164
const handleUpdateReminder = (updated: BillReminder) => {
setReminders(prev => prev.map(r => r.id === updated.id ? updated : r));
};
删除提醒
// 位置: App.tsx L166-170
const handleDeleteReminder = (id: string) => {
if (confirm('确定要删除这个提醒吗?')) {
setReminders(prev => prev.filter(r => r.id !== id));
}
};
切换完成状态
// 位置: App.tsx L172-174
const handleToggleReminder = (id: string) => {
setReminders(prev => prev.map(r =>
r.id === id ? { ...r, isCompleted: !r.isCompleted } : r
));
};
剩余天数计算
// 位置: 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助手设置持久化
存储键名
'cui_ai_settings'
数据类型
AISettings
数据结构
interface AISettings {
name: string; // AI助手名称
persona: string; // AI性格描述
avatar?: string; // 头像 (Base64编码图片,可选)
}
默认值
// 位置: App.tsx L18-21
const DEFAULT_AI_SETTINGS: AISettings = {
name: "崔经理",
persona: "一个精明、毒舌但实际上很负责任的财务经理。说话喜欢带刺,吐槽用户乱花钱,但关键时刻会给出靠谱建议。"
};
操作接口
读取
// 位置: App.tsx L106
const storedAiSettings = localStorage.getItem('cui_ai_settings');
if (storedAiSettings) setAiSettings(JSON.parse(storedAiSettings));
写入
// 位置: App.tsx L132
useEffect(() => {
localStorage.setItem('cui_ai_settings', JSON.stringify(aiSettings));
}, [aiSettings]);
更新设置
// 修改名称
setAiSettings({ ...aiSettings, name: '新名字' });
// 修改性格
setAiSettings({ ...aiSettings, persona: '新的性格描述' });
// 更新头像
setAiSettings(prev => ({ ...prev, avatar: base64Data }));
预设性格列表
// 位置: 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. 自定义性格预设持久化
存储键名
'cui_custom_preset_info'
数据类型
{ name: string; description: string }
数据结构
{
name: '自定义', // 自定义预设名称
description: '可以根据你喜欢的方式设定助手的语气和风格' // 描述
}
操作接口
读取
// 位置: App.tsx L107
const storedCustom = localStorage.getItem('cui_custom_preset_info');
if (storedCustom) setCustomPreset(JSON.parse(storedCustom));
写入
// 位置: App.tsx L133
useEffect(() => {
localStorage.setItem('cui_custom_preset_info', JSON.stringify(customPreset));
}, [customPreset]);
自动保存自定义预设
// 位置: 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. 主题设置持久化
存储键名
'cui_card_theme'
数据类型
string (数字索引)
数据值
'0' | '1' | '2' | '3' | '4' // 对应5个主题的索引
主题列表
// 位置: 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: '浪漫巴黎' },
];
操作接口
读取
// 位置: LedgerPage.tsx L41
const [themeIndex, setThemeIndex] = useState(() =>
Number(localStorage.getItem('cui_card_theme')) || 0
);
写入
// 位置: LedgerPage.tsx L43-45
useEffect(() => {
localStorage.setItem('cui_card_theme', themeIndex.toString());
}, [themeIndex]);
切换主题
// 位置: LedgerPage.tsx L56
const cycleTheme = () => setThemeIndex((prev) => (prev + 1) % CARD_THEMES.length);
使用场景
- 账本页面顶部卡片主题
- 标签页按钮颜色主题
- 用户个性化界面体验
8. AI服务接口(非持久化,但涉及数据处理)
Gemini API 调用接口
服务文件
services/geminiService.ts
处理用户消息
// 位置: geminiService.ts L67-115
export const processUserMessage = async (
message: string,
aiName: string,
aiPersona: string,
imageBase64?: string,
mimeType?: string
): Promise<ParseResult>
返回结果
// 位置: geminiService.ts L57-65
interface ParseResult {
transaction: {
item: string;
amount: number;
type: TransactionType;
category: string;
} | null;
reply: string;
}
API模型配置
// 位置: geminiService.ts L4-7
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
const MODEL_NAME = "gemini-3-flash-preview";
系统指令模板
// 位置: geminiService.ts L9-29
const getSystemInstruction = (aiName: string, aiPersona: string) => `
你现在的身份是"${aiName}"。
你的性格设定是:${aiPersona}
你的任务是:
1. 从用户的输入中提取交易信息(内容、金额、类别、交易类型)。
2. 判断是【支出】(expense) 还是【收入】(income)。
3. **严格回复规范**:
- 回复必须**极度简练**。
- 每一段话的字数必须控制在 **30字左右**(越短越好)。
- 全部的回复**最多只能分成 2 段**(中间用一个 \n 换行符隔开)。
- 必须在每一段中都加入**生动的表情包 (Emoji)** 🌈。
- 说话风格要完全符合"${aiName}"的设定。
`;
使用场景
- 语音/文字输入转交易记录
- 图片识别账单信息
- AI对话回复生成
- 自动分类和金额提取
数据流架构图
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年)
- 限制聊天历史记录数量(如保留最近500条)
- 图片压缩后再转Base64
- 实现数据导出/导入功能
错误处理与兼容性
JSON解析容错
try {
const data = JSON.parse(localStorage.getItem('cui_transactions'));
setTransactions(data);
} catch (error) {
console.error('数据解析失败', error);
setTransactions([]); // 回退到默认值
}
版本兼容
- 预算数据: 自动兼容旧版纯数字格式
- 新增字段: 使用可选字段(如 BillReminder.amount?)
- 默认值: 所有状态都有合理的初始值
数据校验建议
// 类型守卫示例
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'
);
}
数据备份与恢复
导出数据(建议实现)
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();
}
导入数据(建议实现)
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图片数据
- 聊天记录: 可能包含敏感对话
安全建议
- 不存储: Gemini API Key(使用环境变量)
- 加密存储: 考虑对敏感数据加密(如使用Web Crypto API)
- 定期清理: 提供清除所有数据的功能
- 隐私模式: 提示用户不要在公共设备上使用
清除数据示例
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. 批量写入优化
// 避免频繁写入,使用防抖
import { debounce } from 'lodash';
const debouncedSave = debounce((key: string, data: any) => {
localStorage.setItem(key, JSON.stringify(data));
}, 500);
2. 数据分页加载
// 仅加载当前月份数据到UI
const currentMonthTransactions = transactions.filter(
t => t.date.startsWith(selectedMonth)
);
3. 索引优化
// 创建日期索引,加速查询
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个持久化数据接口:
- ✅ 交易记录 (cui_transactions) - 核心财务数据
- ✅ 聊天消息 (cui_messages) - AI对话历史
- ✅ 预算数据 (cui_budget) - 预算管理
- ✅ 账单提醒 (cui_reminders) - 待办事项
- ✅ AI设置 (cui_ai_settings) - AI助手配置
- ✅ 自定义预设 (cui_custom_preset_info) - 性格预设
- ✅ 主题设置 (cui_card_theme) - 界面主题
所有接口均采用 localStorage 存储,使用 JSON 序列化,并通过 React useEffect 实现自动持久化。
文档版本: v1.0
生成时间: 2026-01-19
最后更新: 2026-01-19
维护者: 开发团队