import React, { useState, useEffect, useRef } from 'react'; import { processUserMessage } from './services/geminiService'; import VoiceInput from './components/VoiceInput'; import LedgerPage from './components/LedgerPage'; import StatisticsPage from './components/StatisticsPage'; import EditTransactionModal from './components/EditTransactionModal'; import { Transaction, TransactionType, Message, ExpenseCategory, IncomeCategory, BillReminder } from './types'; import { deleteMessage, deleteTransaction, updateTransaction, createTransaction, createMessage, getMessages, getTransactions, loginOrRegister, getUserProfile, createReminder, getReminders, updateReminder, toggleReminder, deleteReminder } from './services/api'; type Tab = 'chat' | 'ledger' | 'stats'; interface AISettings { name: string; persona: string; avatar?: string; } const DEFAULT_AI_SETTINGS: AISettings = { name: "崔经理", persona: "一个精明、毒舌但实际上很负责任的财务经理。说话喜欢带刺,吐槽用户乱花钱,但关键时刻会给出靠谱建议。" }; const INITIAL_PERSONA_PRESETS = [ { id: 'toxic', name: '崔经理 (默认)', description: '精明、毒舌、傲娇。喜欢吐槽你乱花钱,"恨铁不成钢",但内心其实是为你好。' }, { id: 'gentle', name: '温柔管家', description: '温柔、体贴、治愈。无论你怎么花钱都会温柔记录,并在超支时委婉地提醒你注意身体和钱包。' }, { id: 'strict', name: '严厉会计', description: '一丝不苟、冷酷无情。只关心数字,对任何非必要支出都会严厉批评,没有任何废话。' }, { id: 'custom', name: '自定义', description: '可以根据你喜欢的方式设定助手的语气和风格' } ]; const getLocalDateString = (date = new Date()) => { const offset = date.getTimezoneOffset(); const localDate = new Date(date.getTime() - (offset * 60 * 1000)); return localDate.toISOString().split('T')[0]; }; const getCategoryIcon = (category: string, type: TransactionType) => { if (type === TransactionType.INCOME) { switch (category) { case IncomeCategory.SALARY: return '💰'; case IncomeCategory.BONUS: return '🧧'; case IncomeCategory.INVESTMENT: return '📈'; default: return '💵'; } } switch (category) { case ExpenseCategory.FOOD: return '🍔'; case ExpenseCategory.SHOPPING: return '🛍️'; case ExpenseCategory.TRANSPORT: return '🚗'; case ExpenseCategory.HOUSING: return '🏠'; case ExpenseCategory.ENTERTAINMENT: return '🎬'; case ExpenseCategory.MEDICAL: return '💊'; case ExpenseCategory.SOCIAL: return '🤝'; case ExpenseCategory.EDUCATION: return '📚'; case ExpenseCategory.FINANCE: return '🏦'; case ExpenseCategory.CHILD: return '👶'; case ExpenseCategory.PET: return '🐱'; case ExpenseCategory.CAR: return '🚘'; case ExpenseCategory.BEAUTY: return '💄'; case ExpenseCategory.BUSINESS: return '💼'; case ExpenseCategory.MISC: return '🧺'; default: return '🧾'; } }; const App: React.FC = () => { const [activeTab, setActiveTab] = useState('chat'); const [messages, setMessages] = useState(() => { try { const saved = localStorage.getItem('cui_messages'); return saved ? JSON.parse(saved) : []; } catch (e) { return []; } }); const [transactions, setTransactions] = useState(() => { try { const saved = localStorage.getItem('cui_transactions'); return saved ? JSON.parse(saved) : []; } catch (e) { return []; } }); const [budgets, setBudgets] = useState>(() => { try { const saved = localStorage.getItem('cui_budget'); if (saved) { const parsed = JSON.parse(saved); return typeof parsed === 'number' ? { '总预算': parsed } : parsed; } } catch (e) {} return { '总预算': 3000 }; }); const [reminders, setReminders] = useState(() => { try { const saved = localStorage.getItem('cui_reminders'); return saved ? JSON.parse(saved) : []; } catch (e) { return []; } }); const [inputText, setInputText] = useState(''); const [isProcessing, setIsProcessing] = useState(false); const [editingTransaction, setEditingTransaction] = useState(null); const [isEditingFromChat, setIsEditingFromChat] = useState(false); const [inputMode, setInputMode] = useState<'voice' | 'keyboard'>('voice'); const [token, setToken] = useState(null); const [userInfo, setUserInfo] = useState(null); const [aiSettings, setAiSettings] = useState(() => { try { const saved = localStorage.getItem('cui_ai_settings'); return saved ? JSON.parse(saved) : DEFAULT_AI_SETTINGS; } catch (e) { return DEFAULT_AI_SETTINGS; } }); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [customPreset, setCustomPreset] = useState(() => { try { const saved = localStorage.getItem('cui_custom_preset_info'); return saved ? JSON.parse(saved) : { name: '自定义', description: '可以根据你喜欢的方式设定助手的语气和风格' }; } catch (e) { return { name: '自定义', description: '可以根据你喜欢的方式设定助手的语气和风格' }; } }); const fileInputRef = useRef(null); const avatarInputRef = useRef(null); const messagesEndRef = useRef(null); // 在首页加载后打印当前URL并检查token useEffect(() => { console.log('当前URL:', window.location.href); // 检查是否存在token,如果不存在则调用登录接口 checkAndLogin().then((userInfo) => { // 登录成功后加载数据 if (userInfo || localStorage.getItem('auth_token')) { loadInitialData(); } }); }, []); // 检查token并登录的函数 const checkAndLogin = async (): Promise => { // 从localStorage检查是否存在token const storedToken = localStorage.getItem('auth_token'); if (storedToken) { setToken(storedToken); // 如果有token,携带token访问 GET /api/wx/profile 获取用户信息 try { const userInfo = await getUserProfile(); // 更新本地缓存 localStorage.setItem('user_info', JSON.stringify(userInfo)); setUserInfo(userInfo); console.log('通过token获取用户信息成功:', userInfo); return userInfo; } catch (error) { console.error('获取用户信息失败:', error); // 如果token失效,清除本地存储 localStorage.removeItem('auth_token'); localStorage.removeItem('user_info'); setToken(null); setUserInfo(null); // 如果请求失败,尝试使用本地缓存 try { const cachedUserInfo = JSON.parse(localStorage.getItem('user_info') || '{}'); if (cachedUserInfo && Object.keys(cachedUserInfo).length > 0) { setUserInfo(cachedUserInfo); return cachedUserInfo; } } catch (e) { console.error('解析本地用户信息失败:', e); } } return null; } // 如果没有token,则检查URL参数并调用登录接口 const urlParams = new URLSearchParams(window.location.search); const openid = urlParams.get('openid'); const mobile = urlParams.get('mobile'); if (openid && mobile) { try { const result = await loginOrRegister(openid, mobile); // 存储token和用户信息 localStorage.setItem('user_info', JSON.stringify(result.user)); setToken(result.token); setUserInfo(result.user); console.log('登录成功:', result.user); return result.user; } catch (error) { console.error('登录请求出错:', error); return null; } } else { console.log('缺少必需的URL参数: openid 或 mobile'); return null; } }; const loadInitialData = async () => { // 从数据库获取交易列表 try { const response = await getTransactions(); if (response.items && response.items.length > 0) { // 将数据库返回的交易转换为本地格式 const transactionsFromDB: Transaction[] = response.items.map(tx => ({ id: tx.id, item: tx.item, amount: tx.amount, type: tx.type, category: tx.category, date: tx.date, timestamp: tx.timestamp })); // 更新本地缓存 setTransactions(transactionsFromDB); console.log('交易列表已从数据库加载:', transactionsFromDB.length); } else { console.log('数据库中暂无交易记录'); } } catch (error) { console.error('加载交易列表失败:', error); // 失败时尝试从本地存储加载 const storedTransactions = localStorage.getItem('cui_transactions'); if (storedTransactions) { setTransactions(JSON.parse(storedTransactions)); console.log('已从本地缓存加载交易记录'); } } // 从数据库获取消息列表 try { const response = await getMessages(); if (response.items && response.items.length > 0) { // 将数据库返回的消息转换为本地格式 const messagesFromDB: Message[] = response.items.map((msg: any) => ({ id: msg.id, role: msg.role, content: msg.content, timestamp: msg.timestamp, // API返回的是transaction_id,需要映射为relatedTransactionId relatedTransactionId: msg.transaction_id || msg.relatedTransactionId })); // 更新本地缓存 setMessages(messagesFromDB); console.log('消息列表已从数据库加载:', messagesFromDB.length); } else { // 如果数据库中没有消息,显示初始欢迎消息 const aiName = aiSettings.name; setMessages([{ id: 'init', role: 'assistant', content: `我是${aiName}。✨\n想记账?快说!🎤`, timestamp: Date.now() }]); } } catch (error) { console.error('加载消息列表失败:', error); // 失败时尝试从本地存储加载 const storedMessages = localStorage.getItem('cui_messages'); if (storedMessages) { setMessages(JSON.parse(storedMessages)); } else { // 显示初始欢迎消息 const aiName = aiSettings.name; setMessages([{ id: 'init', role: 'assistant', content: `我是${aiName}。✨\n想记账?快说!🎤`, timestamp: Date.now() }]); } } // 从数据库获取待办提醒 try { const response = await getReminders(); if (response.items && response.items.length > 0) { const remindersFromDB: BillReminder[] = response.items.map((r: any) => ({ id: r.id, title: r.title, amount: r.amount, frequency: r.frequency, targetDate: r.target_date || r.targetDate, isCompleted: r.is_completed ?? r.isCompleted ?? false })); setReminders(remindersFromDB); console.log('待办提醒已从数据库加载:', remindersFromDB.length); } } catch (error) { console.error('加载待办提醒失败:', error); } }; useEffect(() => { localStorage.setItem('cui_transactions', JSON.stringify(transactions)); }, [transactions]); useEffect(() => { localStorage.setItem('cui_budget', JSON.stringify(budgets)); }, [budgets]); useEffect(() => { localStorage.setItem('cui_reminders', JSON.stringify(reminders)); }, [reminders]); useEffect(() => { localStorage.setItem('cui_ai_settings', JSON.stringify(aiSettings)); }, [aiSettings]); useEffect(() => { localStorage.setItem('cui_custom_preset_info', JSON.stringify(customPreset)); }, [customPreset]); useEffect(() => { localStorage.setItem('cui_messages', JSON.stringify(messages)); if (activeTab === 'chat') scrollToBottom(); }, [messages, activeTab]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; const handleUpdateTransaction = async (updated: Transaction) => { try { // 调用 API 更新交易记录 const result = await updateTransaction(updated.id, { item: updated.item, amount: updated.amount, type: updated.type, category: updated.category, date: updated.date, timestamp: updated.timestamp }); console.log('交易记录已在数据库中更新:', result); // 更新成功后,更新本地缓存 setTransactions(prev => prev.map(t => t.id === updated.id ? result : t)); setEditingTransaction(null); setIsEditingFromChat(false); } catch (error) { console.error('更新交易记录失败:', error); alert('更新失败,请检查网络连接后重试。'); throw error; } }; const handleDeleteTransaction = async (id: string) => { if (confirm('确定要删除这条记录吗?')) { try { // 先尝试查找关联的消息(卡片) const associatedMessage = messages.find(m => m.relatedTransactionId === id); // 调用 API 删除交易记录 await deleteTransaction(id); console.log('交易记录已从数据库中删除:', id); // 如果有对应消息,也从数据库删除 if (associatedMessage) { try { await deleteMessage(associatedMessage.id); console.log('关联消息已从数据库中删除:', associatedMessage.id); } catch (msgErr) { console.warn('删除关联消息失败:', msgErr); } } // 删除成功后,更新本地缓存 setTransactions(prev => prev.filter(t => t.id !== id)); setMessages(prev => prev.filter(m => m.relatedTransactionId !== id)); setEditingTransaction(null); setIsEditingFromChat(false); } catch (error) { console.error('删除交易记录失败:', error); alert('删除失败,请检查网络连接后重试。'); } } }; const handleAddReminder = async (reminder: BillReminder) => { try { const result: any = await createReminder({ title: reminder.title, amount: reminder.amount, frequency: reminder.frequency, target_date: reminder.targetDate }); const newReminder: BillReminder = { id: result.id, title: result.title, amount: result.amount, frequency: result.frequency, targetDate: result.target_date || result.targetDate, isCompleted: result.is_completed ?? result.isCompleted ?? false }; setReminders(prev => [...prev, newReminder]); } catch (error) { console.error('创建待办提醒失败:', error); alert('创建失败,请稍后重试'); } }; const handleUpdateReminder = async (updated: BillReminder) => { try { const result: any = await updateReminder(updated.id, { title: updated.title, amount: updated.amount, frequency: updated.frequency, target_date: updated.targetDate }); const mappedResult: BillReminder = { id: result.id, title: result.title, amount: result.amount, frequency: result.frequency, targetDate: result.target_date || result.targetDate, isCompleted: result.is_completed ?? result.isCompleted ?? false }; setReminders(prev => prev.map(r => r.id === updated.id ? mappedResult : r)); } catch (error) { console.error('更新待办提醒失败:', error); alert('更新失败,请稍后重试'); } }; const handleDeleteReminder = async (id: string) => { if (confirm('确定要删除这个提醒吗?')) { try { await deleteReminder(id); setReminders(prev => prev.filter(r => r.id !== id)); } catch (error) { console.error('删除待办提醒失败:', error); alert('删除失败,请稍后重试'); } } }; const handleToggleReminder = async (id: string) => { try { const result = await toggleReminder(id); setReminders(prev => prev.map(r => r.id === id ? { ...r, isCompleted: result.is_completed } : r)); } catch (error) { console.error('切换待办状态失败:', error); alert('操作失败,请稍后重试'); } }; // 辅助函数:先保存消息到数据库,然后更新本地 const addMessage = async (messageData: Omit & { id?: string }): Promise => { try { const savedMessage = await createMessage({ role: messageData.role, content: messageData.content, timestamp: messageData.timestamp, transaction_id: messageData.relatedTransactionId }); console.log('消息已保存到数据库:', savedMessage); // 数据库保存成功后,更新本地缓存 // API返回的是transaction_id,需要映射为relatedTransactionId const newMessage: Message = { id: savedMessage.id, role: savedMessage.role, content: savedMessage.content, timestamp: savedMessage.timestamp, relatedTransactionId: (savedMessage as any).transaction_id || savedMessage.relatedTransactionId }; setMessages(prev => [...prev, newMessage]); return newMessage; } catch (error) { console.error('保存消息到数据库失败:', error); return null; } }; const handleSendMessage = React.useCallback(async (text: string, imageBase64?: string, mimeType?: string) => { if (!text.trim() && !imageBase64) return; // 先保存用户消息到数据库 const userMessage = await addMessage({ role: 'user', content: imageBase64 ? '已上传图片' : text, timestamp: Date.now() }); if (!userMessage) { // 如果保存失败,显示错误提示 console.error('用户消息保存失败'); return; } setInputText(''); setIsProcessing(true); try { const result = await processUserMessage(text, aiSettings.name, aiSettings.persona, imageBase64, mimeType); if (result.transaction) { const transactionData = { item: result.transaction.item, amount: result.transaction.amount, type: result.transaction.type, category: result.transaction.category, date: getLocalDateString(), timestamp: Date.now() }; // 先提交到数据库 try { const savedTransaction = await createTransaction(transactionData); console.log('交易记录已保存到数据库:', savedTransaction); // 数据库保存成功后,更新本地缓存 const newTransaction: Transaction = { id: savedTransaction.id, // 使用数据库返回的ID ...transactionData }; setTransactions(prev => [...prev, newTransaction]); // 显示交易卡片和回复消息 await addMessage({ role: 'assistant', content: 'RECEIPT', timestamp: Date.now(), relatedTransactionId: savedTransaction.id }); const replySegments = result.reply.split('\n').filter(s => s.trim()).slice(0, 2); for (let i = 0; i < replySegments.length; i++) { if (i > 0) await new Promise(r => setTimeout(r, 450)); await addMessage({ role: 'assistant', content: replySegments[i].trim(), timestamp: Date.now() }); } } catch (dbError) { console.error('保存交易记录到数据库失败:', dbError); // 数据库保存失败时,显示错误提示 await addMessage({ role: 'assistant', content: '抱歉,保存交易记录失败,请检查网络连接或稍后重试。', timestamp: Date.now() }); } } else { const replySegments = result.reply.split('\n').filter(s => s.trim()).slice(0, 2); for (let i = 0; i < replySegments.length; i++) { if (i > 0) await new Promise(r => setTimeout(r, 450)); await addMessage({ role: 'assistant', content: replySegments[i].trim(), timestamp: Date.now() }); } } } catch (error) { await addMessage({ role: 'assistant', content: '系统走神了... 🫥\n等会儿再说。', timestamp: Date.now() }); } finally { setIsProcessing(false); } }, [aiSettings, transactions, messages]); const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(inputText); } }; const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onloadend = () => { const base64Data = (reader.result as string).split(',')[1]; handleSendMessage("", base64Data, file.type); }; reader.readAsDataURL(file); if (fileInputRef.current) fileInputRef.current.value = ''; } const handleAvatarUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onloadend = () => { const base64Data = reader.result as string; setAiSettings(prev => ({ ...prev, avatar: base64Data })); }; reader.readAsDataURL(file); if (avatarInputRef.current) avatarInputRef.current.value = ''; } const todayStr = getLocalDateString(); const todaySpending = transactions .filter(t => t.date === todayStr && t.type === TransactionType.EXPENSE) .reduce((a, b) => a + b.amount, 0); const personaPresets = INITIAL_PERSONA_PRESETS.map(p => { if (p.id === 'custom') { return { ...p, name: customPreset.name, description: customPreset.description }; } return p; }); 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); }; return (
{/* Chat Tab */}
setIsSettingsOpen(true)}>
{aiSettings.avatar ? avatar : aiSettings.name.charAt(0)}

{aiSettings.name}

{aiSettings.persona.split(/[。!]/)[0] + '...'}
今日支出 ¥{todaySpending.toFixed(0)}
{/* AI Settings Modal */} {isSettingsOpen && (

管家设定

{aiSettings.avatar ? avatar : aiSettings.name.charAt(0)}
更改管家形象
setAiSettings({ ...aiSettings, name: e.target.value })} className="w-full p-3.5 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none font-bold text-gray-800" />
{personaPresets.map(p => ( ))}