feat(api): 集成后端API,实现数据持久化和用户登录

- 新增token管理和用户信息状态,支持基于token的自动登录
- 实现登录或注册接口调用,自动根据URL参数完成身份验证
- 从后端加载交易记录、消息和待办提醒,替代本地缓存读写
- 对交易记录的增删改操作同步到数据库,确保数据一致性
- 对消息和提醒的增删改操作支持服务器同步,提升数据可靠性
- 优化消息发送逻辑,先保存用户消息,再处理AI回复并存库
- 添加待办提醒切换状态API调用,确保状态同步服务器
- 改进界面互动,支持文件与头像上传功能,更新管家形象
- 采用异步处理并增加错误捕获和用户提示,增强稳定性
This commit is contained in:
2026-01-22 18:25:42 +08:00
parent c06e74bd01
commit 194de6ec06
7 changed files with 1595 additions and 499 deletions

376
App.tsx
View File

@@ -6,6 +6,10 @@ import StatisticsPage from './components/StatisticsPage';
import EditTransactionModal from './components/EditTransactionModal'; import EditTransactionModal from './components/EditTransactionModal';
import { Transaction, TransactionType, Message, ExpenseCategory, IncomeCategory, BillReminder } from './types'; import { Transaction, TransactionType, Message, ExpenseCategory, IncomeCategory, BillReminder } from './types';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import {
deleteMessage, deleteTransaction, updateTransaction, createTransaction, createMessage, getMessages, getTransactions, loginOrRegister, getUserProfile,
createReminder, getReminders, updateReminder, toggleReminder, deleteReminder
} from './services/api';
type Tab = 'chat' | 'ledger' | 'stats'; type Tab = 'chat' | 'ledger' | 'stats';
@@ -89,6 +93,8 @@ const App: React.FC = () => {
const [editingTransaction, setEditingTransaction] = useState<Transaction | null>(null); const [editingTransaction, setEditingTransaction] = useState<Transaction | null>(null);
const [isEditingFromChat, setIsEditingFromChat] = useState(false); const [isEditingFromChat, setIsEditingFromChat] = useState(false);
const [inputMode, setInputMode] = useState<'voice' | 'keyboard'>('voice'); const [inputMode, setInputMode] = useState<'voice' | 'keyboard'>('voice');
const [token, setToken] = useState<string | null>(null);
const [userInfo, setUserInfo] = useState<any>(null);
const [aiSettings, setAiSettings] = useState<AISettings>(DEFAULT_AI_SETTINGS); const [aiSettings, setAiSettings] = useState<AISettings>(DEFAULT_AI_SETTINGS);
const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false);
@@ -98,16 +104,84 @@ const App: React.FC = () => {
const avatarInputRef = useRef<HTMLInputElement>(null); const avatarInputRef = useRef<HTMLInputElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
// 在首页加载后打印当前URL并检查token
useEffect(() => { useEffect(() => {
const storedTransactions = localStorage.getItem('cui_transactions'); console.log('当前URL:', window.location.href);
const storedMessages = localStorage.getItem('cui_messages');
// 检查是否存在token,如果不存在则调用登录接口
checkAndLogin().then((userInfo) => {
// 登录成功后加载数据
if (userInfo || localStorage.getItem('auth_token')) {
loadInitialData();
}
});
}, []);
// 检查token并登录的函数
const checkAndLogin = async (): Promise<any | null> => {
// 从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 () => {
const storedBudget = localStorage.getItem('cui_budget'); const storedBudget = localStorage.getItem('cui_budget');
const storedReminders = localStorage.getItem('cui_reminders'); const storedReminders = localStorage.getItem('cui_reminders');
const storedAiSettings = localStorage.getItem('cui_ai_settings'); const storedAiSettings = localStorage.getItem('cui_ai_settings');
const storedCustom = localStorage.getItem('cui_custom_preset_info'); const storedCustom = localStorage.getItem('cui_custom_preset_info');
if (storedTransactions) setTransactions(JSON.parse(storedTransactions));
if (storedMessages) setMessages(JSON.parse(storedMessages));
if (storedBudget) { if (storedBudget) {
const parsed = JSON.parse(storedBudget); const parsed = JSON.parse(storedBudget);
setBudgets(typeof parsed === 'number' ? { '总预算': parsed } : parsed); setBudgets(typeof parsed === 'number' ? { '总预算': parsed } : parsed);
@@ -116,15 +190,99 @@ const App: React.FC = () => {
if (storedAiSettings) setAiSettings(JSON.parse(storedAiSettings)); if (storedAiSettings) setAiSettings(JSON.parse(storedAiSettings));
if (storedCustom) setCustomPreset(JSON.parse(storedCustom)); if (storedCustom) setCustomPreset(JSON.parse(storedCustom));
if (!storedMessages) { // 从数据库获取交易列表
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 = storedAiSettings ? JSON.parse(storedAiSettings).name : '崔经理';
setMessages([{ setMessages([{
id: 'init', id: 'init',
role: 'assistant', role: 'assistant',
content: `我是${storedAiSettings ? JSON.parse(storedAiSettings).name : '崔经理'}。✨\n想记账?快说!🎤`, content: `我是${aiName}。✨\n想记账?快说!🎤`,
timestamp: Date.now() timestamp: Date.now()
}]); }]);
} }
}, []); } catch (error) {
console.error('加载消息列表失败:', error);
// 失败时尝试从本地存储加载
const storedMessages = localStorage.getItem('cui_messages');
if (storedMessages) {
setMessages(JSON.parse(storedMessages));
} else {
// 显示初始欢迎消息
const aiName = storedAiSettings ? JSON.parse(storedAiSettings).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_transactions', JSON.stringify(transactions)); }, [transactions]);
useEffect(() => { localStorage.setItem('cui_budget', JSON.stringify(budgets)); }, [budgets]); useEffect(() => { localStorage.setItem('cui_budget', JSON.stringify(budgets)); }, [budgets]);
@@ -141,48 +299,175 @@ const App: React.FC = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}; };
const handleUpdateTransaction = (updated: Transaction) => { const handleUpdateTransaction = async (updated: Transaction) => {
setTransactions(prev => prev.map(t => t.id === updated.id ? updated : t)); 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); setEditingTransaction(null);
setIsEditingFromChat(false); setIsEditingFromChat(false);
} catch (error) {
console.error('更新交易记录失败:', error);
alert('更新失败,请检查网络连接后重试。');
throw error;
}
}; };
const handleDeleteTransaction = (id: string) => { const handleDeleteTransaction = async (id: string) => {
if (confirm('确定要删除这条记录吗?')) { 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)); setTransactions(prev => prev.filter(t => t.id !== id));
setMessages(prev => prev.filter(m => m.relatedTransactionId !== id));
setEditingTransaction(null); setEditingTransaction(null);
setIsEditingFromChat(false); setIsEditingFromChat(false);
} catch (error) {
console.error('删除交易记录失败:', error);
alert('删除失败,请检查网络连接后重试。');
}
} }
}; };
const handleAddReminder = (reminder: BillReminder) => { const handleAddReminder = async (reminder: BillReminder) => {
setReminders(prev => [...prev, reminder]); 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 = (updated: BillReminder) => { const handleUpdateReminder = async (updated: BillReminder) => {
setReminders(prev => prev.map(r => r.id === updated.id ? updated : r)); 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 = (id: string) => { const handleDeleteReminder = async (id: string) => {
if (confirm('确定要删除这个提醒吗?')) { if (confirm('确定要删除这个提醒吗?')) {
try {
await deleteReminder(id);
setReminders(prev => prev.filter(r => r.id !== id)); setReminders(prev => prev.filter(r => r.id !== id));
} catch (error) {
console.error('删除待办提醒失败:', error);
alert('删除失败,请稍后重试');
}
} }
}; };
const handleToggleReminder = (id: string) => { const handleToggleReminder = async (id: string) => {
setReminders(prev => prev.map(r => r.id === id ? { ...r, isCompleted: !r.isCompleted } : r)); 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<Message, 'id'> & { id?: string }): Promise<Message | null> => {
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 = async (text: string, imageBase64?: string, mimeType?: string) => { const handleSendMessage = async (text: string, imageBase64?: string, mimeType?: string) => {
if (!text.trim() && !imageBase64) return; if (!text.trim() && !imageBase64) return;
const userMsg: Message = { // 先保存用户消息到数据库
id: uuidv4(), const userMessage = await addMessage({
role: 'user', role: 'user',
content: imageBase64 ? '已上传图片' : text, content: imageBase64 ? '已上传图片' : text,
timestamp: Date.now() timestamp: Date.now()
}; });
setMessages(prev => [...prev, userMsg]);
if (!userMessage) {
// 如果保存失败,显示错误提示
console.error('用户消息保存失败');
return;
}
setInputText(''); setInputText('');
setIsProcessing(true); setIsProcessing(true);
@@ -190,9 +475,7 @@ const App: React.FC = () => {
const result = await processUserMessage(text, aiSettings.name, aiSettings.persona, imageBase64, mimeType); const result = await processUserMessage(text, aiSettings.name, aiSettings.persona, imageBase64, mimeType);
if (result.transaction) { if (result.transaction) {
const newTransactionId = uuidv4(); const transactionData = {
const newTransaction: Transaction = {
id: newTransactionId,
item: result.transaction.item, item: result.transaction.item,
amount: result.transaction.amount, amount: result.transaction.amount,
type: result.transaction.type, type: result.transaction.type,
@@ -201,40 +484,61 @@ const App: React.FC = () => {
timestamp: Date.now() timestamp: Date.now()
}; };
// 先提交到数据库
try {
const savedTransaction = await createTransaction(transactionData);
console.log('交易记录已保存到数据库:', savedTransaction);
// 数据库保存成功后,更新本地缓存
const newTransaction: Transaction = {
id: savedTransaction.id, // 使用数据库返回的ID
...transactionData
};
setTransactions(prev => [...prev, newTransaction]); setTransactions(prev => [...prev, newTransaction]);
setMessages(prev => [...prev, { // 显示交易卡片和回复消息
id: uuidv4(), await addMessage({
role: 'assistant', role: 'assistant',
content: 'RECEIPT', content: 'RECEIPT',
timestamp: Date.now(), timestamp: Date.now(),
relatedTransactionId: newTransactionId relatedTransactionId: savedTransaction.id
}]); });
const replySegments = result.reply.split('\n').filter(s => s.trim()).slice(0, 2); const replySegments = result.reply.split('\n').filter(s => s.trim()).slice(0, 2);
for (let i = 0; i < replySegments.length; i++) { for (let i = 0; i < replySegments.length; i++) {
if (i > 0) await new Promise(r => setTimeout(r, 450)); if (i > 0) await new Promise(r => setTimeout(r, 450));
setMessages(prev => [...prev, { await addMessage({
id: uuidv4(),
role: 'assistant', role: 'assistant',
content: replySegments[i].trim(), content: replySegments[i].trim(),
timestamp: Date.now() timestamp: Date.now()
}]); });
}
} catch (dbError) {
console.error('保存交易记录到数据库失败:', dbError);
// 数据库保存失败时,显示错误提示
await addMessage({
role: 'assistant',
content: '抱歉,保存交易记录失败,请检查网络连接或稍后重试。',
timestamp: Date.now()
});
} }
} else { } else {
const replySegments = result.reply.split('\n').filter(s => s.trim()).slice(0, 2); const replySegments = result.reply.split('\n').filter(s => s.trim()).slice(0, 2);
for (let i = 0; i < replySegments.length; i++) { for (let i = 0; i < replySegments.length; i++) {
if (i > 0) await new Promise(r => setTimeout(r, 450)); if (i > 0) await new Promise(r => setTimeout(r, 450));
setMessages(prev => [...prev, { await addMessage({
id: uuidv4(),
role: 'assistant', role: 'assistant',
content: replySegments[i].trim(), content: replySegments[i].trim(),
timestamp: Date.now() timestamp: Date.now()
}]); });
} }
} }
} catch (error) { } catch (error) {
setMessages(prev => [...prev, { id: uuidv4(), role: 'assistant', content: '系统走神了... 🫥\n等会儿再说。', timestamp: Date.now() }]); await addMessage({
role: 'assistant',
content: '系统走神了... 🫥\n等会儿再说。',
timestamp: Date.now()
});
} finally { } finally {
setIsProcessing(false); setIsProcessing(false);
} }
@@ -372,7 +676,7 @@ const App: React.FC = () => {
)} )}
{/* Messages - Added Gradient Background */} {/* Messages - Added Gradient Background */}
<div className="flex-1 overflow-y-auto p-4 pb-32 space-y-6 bg-gradient-to-b from-indigo-50/40 via-white to-gray-50"> <div className="flex-1 overflow-y-auto p-4 pb-40 space-y-6 bg-gradient-to-b from-indigo-50/40 via-white to-gray-50">
{messages.map((msg) => { {messages.map((msg) => {
const isUser = msg.role === 'user'; const isUser = msg.role === 'user';
const relatedTx = msg.relatedTransactionId ? transactions.find(t => t.id === msg.relatedTransactionId) : null; const relatedTx = msg.relatedTransactionId ? transactions.find(t => t.id === msg.relatedTransactionId) : null;

View File

@@ -23,8 +23,8 @@ const EditTransactionModal: React.FC<EditTransactionModalProps> = ({ transaction
}; };
return ( return (
<div className="fixed inset-0 z-[60] flex items-end md:items-center justify-center bg-black/50 backdrop-blur-sm p-4"> <div className="fixed inset-0 z-[60] flex items-end md:items-center justify-center bg-black/50 backdrop-blur-sm p-4 pb-28 md:pb-4">
<div className="bg-white w-full max-w-md rounded-2xl p-6 shadow-2xl animate-float md:animate-none"> <div className="bg-white w-full max-w-md rounded-2xl p-6 shadow-2xl md:animate-none max-h-[calc(100vh-120px)] overflow-y-auto no-scrollbar">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold text-gray-900">编辑账单</h3> <h3 className="text-lg font-bold text-gray-900">编辑账单</h3>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"> <button onClick={onClose} className="text-gray-400 hover:text-gray-600">

View File

@@ -6,8 +6,8 @@ import BillReminders, { getDaysRemaining } from './BillReminders';
interface LedgerPageProps { interface LedgerPageProps {
transactions: Transaction[]; transactions: Transaction[];
onUpdateTransaction: (t: Transaction) => void; onUpdateTransaction: (t: Transaction) => Promise<void>;
onDeleteTransaction: (id: string) => void; onDeleteTransaction: (id: string) => Promise<void>;
budgets: Record<string, number>; budgets: Record<string, number>;
setBudgets: (val: Record<string, number>) => void; setBudgets: (val: Record<string, number>) => void;
reminders: BillReminder[]; reminders: BillReminder[];
@@ -200,7 +200,24 @@ const LedgerPage: React.FC<LedgerPageProps> = ({
{subTab === 'calendar' && <CalendarView transactions={transactions} reminders={reminders} viewDate={selectedMonth} />} {subTab === 'calendar' && <CalendarView transactions={transactions} reminders={reminders} viewDate={selectedMonth} />}
{subTab === 'bills' && <BillReminders reminders={reminders.filter(r => r.frequency === 'monthly' || r.targetDate.startsWith(selectedMonth))} onAdd={onAddReminder} onUpdate={onUpdateReminder} onDelete={onDeleteReminder} onToggle={onToggleReminder} />} {subTab === 'bills' && <BillReminders reminders={reminders.filter(r => r.frequency === 'monthly' || r.targetDate.startsWith(selectedMonth))} onAdd={onAddReminder} onUpdate={onUpdateReminder} onDelete={onDeleteReminder} onToggle={onToggleReminder} />}
</div> </div>
{editingTransaction && <EditTransactionModal transaction={editingTransaction} onSave={(updated) => { onUpdateTransaction(updated); setEditingTransaction(null); }} onDelete={(id) => { onDeleteTransaction(id); setEditingTransaction(null); }} onClose={() => setEditingTransaction(null)} />} {editingTransaction && (
<EditTransactionModal
transaction={editingTransaction}
onSave={async (updated) => {
try {
await onUpdateTransaction(updated);
setEditingTransaction(null);
} catch (e) {
// Error handled in App.tsx
}
}}
onDelete={async (id) => {
await onDeleteTransaction(id);
setEditingTransaction(null);
}}
onClose={() => setEditingTransaction(null)}
/>
)}
</div> </div>
); );
}; };

View File

@@ -11,6 +11,8 @@ const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isF
const recognitionRef = useRef<any>(null); const recognitionRef = useRef<any>(null);
const timerRef = useRef<number | null>(null); const timerRef = useRef<number | null>(null);
const isStartingRef = useRef(false);
useEffect(() => { useEffect(() => {
if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) { if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
@@ -22,30 +24,37 @@ const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isF
recognitionRef.current.onresult = (event: any) => { recognitionRef.current.onresult = (event: any) => {
const transcript = event.results[0][0].transcript; const transcript = event.results[0][0].transcript;
if (transcript) onTranscript(transcript); if (transcript) onTranscript(transcript);
setIsListening(false);
}; };
recognitionRef.current.onerror = (event: any) => { recognitionRef.current.onerror = (event: any) => {
if (event.error === 'network') {
alert("语音识别网络连接失败。请检查网络或尝试切换浏览器(推荐 Chrome/Edge)。");
}
console.error('Speech recognition error', event.error); console.error('Speech recognition error', event.error);
setIsListening(false);
}; };
recognitionRef.current.onend = () => { recognitionRef.current.onend = () => {
setIsListening(false); setIsListening(false);
isStartingRef.current = false;
}; };
} }
}, [onTranscript]); }, [onTranscript]);
const startListening = (e: React.MouseEvent | React.TouchEvent) => { const startListening = (e: React.PointerEvent) => {
e.preventDefault(); if (e.cancelable) e.preventDefault();
if (isProcessing || isListening) return;
if (isProcessing || isListening || isStartingRef.current) return;
if (recognitionRef.current) { if (recognitionRef.current) {
try { try {
isStartingRef.current = true;
recognitionRef.current.start(); recognitionRef.current.start();
setIsListening(true); setIsListening(true);
} catch (e) { } catch (err) {
console.error("Failed to start recognition", e); console.error("Failed to start recognition", err);
// 如果 start 失败,立即重置状态,否则会锁死
isStartingRef.current = false;
setIsListening(false);
} }
} else { } else {
alert("您的浏览器不支持语音识别,请使用 Chrome 或 Edge。"); alert("您的浏览器不支持语音识别,请使用 Chrome 或 Edge。");
@@ -53,20 +62,23 @@ const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isF
}; };
const stopListening = () => { const stopListening = () => {
// 仅在识别引擎正在运行时尝试停止
// 不要在这里重置 isListening 和 isStartingRef,等待 onend 回调
if (isListening) { if (isListening) {
try {
recognitionRef.current?.stop(); recognitionRef.current?.stop();
setIsListening(false); } catch (err) {
console.error("Failed to stop recognition", err);
}
} }
}; };
return ( return (
<div className={`${isFullWidth ? 'w-full' : ''} relative select-none touch-none`}> <div className={`${isFullWidth ? 'w-full' : ''} relative select-none touch-none`}>
<button <button
onMouseDown={startListening} onPointerDown={startListening}
onMouseUp={stopListening} onPointerUp={stopListening}
onMouseLeave={stopListening} onPointerLeave={stopListening}
onTouchStart={startListening}
onTouchEnd={stopListening}
disabled={isProcessing} disabled={isProcessing}
className={`relative flex items-center justify-center transition-all duration-300 active:scale-95 shadow-lg ${ className={`relative flex items-center justify-center transition-all duration-300 active:scale-95 shadow-lg ${
isFullWidth isFullWidth

View File

@@ -1,4 +1,5 @@
# App_02_记账 # App_02_记账
- 应用信息 - 应用信息
- 应用名称 - 应用名称
- 记账 - 记账
@@ -9,83 +10,158 @@
- 应用描述 - 应用描述
- 一个精明、毒舌但实际上很负责任的财务经理。说话喜欢带刺,吐槽用户乱花钱,但关键时刻会给出靠谱建议。 - 一个精明、毒舌但实际上很负责任的财务经理。说话喜欢带刺,吐槽用户乱花钱,但关键时刻会给出靠谱建议。
- 数据结构 - 数据结构
- Project 项目分类 - 枚举数据
- name 名称(name) - TransactionType 交易类型
- desc 背景(context) - EXPENSE = "expense"
- target 目标(goal) - INCOME = "income"
- value 价值(value) - ExpenseCategory 支出分类
- FOOD = "餐饮"
- SHOPPING = "购物"
- TRANSPORT = "交通"
- HOUSING = "住房"
- ENTERTAINMENT = "娱乐"
- MEDICAL = "医疗"
- SOCIAL = "人情"
- EDUCATION = "学习"
- FINANCE = "金融"
- CHILD = "孩子"
- PET = "宠物"
- CAR = "汽车"
- BEAUTY = "美容"
- BUSINESS = "经营"
- MISC = "杂项"
- IncomeCategory 收入分类
- SALARY = "工资"
- BONUS = "奖金"
- INVESTMENT = "理财"
- PARTTIME = "兼职"
- GIFT = "红包"
- OTHER = "其他收入"
- MessageRole 消息角色
- USER = "user"
- ASSISTANT = "assistant"
- ReminderFrequency 提醒频率
- MONTHLY = "monthly"
- ONCE = "once"
- Common 通用属性
- seq 排序
- isEnabled 是否启用
- creator 创建人 - creator 创建人
- Member - created_at 创建时间
- Task 任务项目 - updated_at 更新时间
- title 标题(title) - Transaction 交易记录
- project 分类(projectId) - item 交易项目名称
- Project - amount: float 交易金额
- desc 描述(notes) - type: TransactionType 交易类型
- startDate 开始日期(date) - category: ExpenseCategory | IncomeCategory 交易分类
- startTime 开始时间(startTime) - date 交易日期 YYYY-MM-DD
- isCompleted 是否完成(isCompleted) - timestamp: int 时间戳(毫秒)
- created_at 创建时间(createdAt) - Message 聊天消息
- creator 创建人 - role: MessageRole 消息角色
- Member - content 消息内容
- timestamp: int 时间戳(毫秒)
- transaction_id 关联的交易ID
- Budget 预算
- member_id 会员ID
- budgets: dict 预算字典 {名称: 金额}
- month 预算月份 YYYY-MM
- Reminder 账单提醒
- member_id 会员ID
- title 提醒标题
- amount: float 金额(可选)
- frequency: ReminderFrequency 频率
- target_date 目标日期: DD(每月) 或 YYYY-MM-DD(单次)
- is_completed: bool 完成状态
- AISettings AI助手设置
- member_id 会员ID
- name AI助手名称
- persona AI性格描述
- avatar 头像Base64编码
- UserPreference 用户偏好设置
- member_id 会员ID
- custom_preset_name 自定义预设名称
- custom_preset_description 自定义预设描述
- theme_index: int 主题索引
- API接口 - API接口
- auth 授权 - Auth 授权
- POST /mp/auth/get_token - POST /api/wx/get_token
- 获取token - 获取token
- GET /mp/auth/profile - GET /api/wx/profile
- 获取个人信息 - 获取个人信息
- 项目 - transaction Bill: 交易记录
- GET /mp/todo/projects - GET /api/bill/transactions
- 项目列表 - 获取分页交易记录
- GET /mp/todo/projects/{project_id} - GET /api/bill/transactions/{id}
- 获取项目详细 - 获取交易记录详情
- POST /mp/todo/projects - POST /api/bill/transactions
- 创建项目 - 创建交易记录
- PUT /mp/todo/projects/{project_id} - PUT /api/bill/transactions/{id}
- 更新项目 - 更新交易记录
- DELETE /mp/todo/projects/{project_id} - DELETE /api/bill/transactions/{id}
- 删除项目 - 删除交易记录
- 任务 - Message Bill: 聊天消息
- GET /mp/todo/tasks - GET /api/bill/messages
- 任务列表 - 获取分页聊天消息
- GET /mp/todo/tasks/{task_id} - GET /api/bill/messages/{id}
- 获取任务详细 - 获取聊天消息详情
- POST /mp/todo/tasks - POST /api/bill/messages
- 创建任务 - 创建聊天消息
- PUT /mp/todo/tasks/{task_id} - PUT /api/bill/messages/{id}
- 更新任务 - 更新聊天消息
- DELETE /mp/todo/tasks/{task_id} - DELETE /api/bill/messages/{id}
- 删除项目 - 删除聊天消息
- PUT /mp/todo/tasks/{task_id}/completed - Budget Bill: 预算
- 设置为已完成 - GET /api/bill/budgets
- PUT /mp/todo/tasks/{task_id}/uncompleted - 获取分页预算
- 设置为未完成 - GET /api/bill/budgets/{id}
- 获取预算详情
- POST /api/bill/transactions
- 创建交易记录
- PUT /api/bill/transactions/{id}
- 更新交易记录
- DELETE /api/bill/transactions/{id}
- 删除交易记录
- BillReminder Bill: 账单提醒
- GET /api/bill/bill_reminders
- 获取分页账单提醒
- GET /api/bill/bill_reminders/{id}
- 获取账单提醒详情
- POST /api/bill/transactions
- 创建交易记录
- PUT /api/bill/transactions/{id}
- 更新交易记录
- DELETE /api/bill/transactions/{id}
- 删除交易记录
- AISettings AI助手设置
- GET /api/h5/ai_settings
- 获取AI助手设置
- PUT /api/h5/ai_settings
- 更新AI助手设置
- POST /api/h5/ai_settings/reset
- 重置AI助手设置
- UserPreference 用户偏好设置
- GET /api/h5/user_preference
- 获取用户偏好设置
- PUT /api/h5/user_preference
- 更新用户偏好设置
- 问题/需求 - 问题/需求
- 单用户转换为多用户模式 - 单用户转换为多用户模式
- 增加登录验证 - 增加登录验证
- 增加creator字段 - 增加creator字段
- 登录问题 - 登录问题
- 小程序跳转到H5, 需要检测登录状态
- 如果已经登录, 需要把登录信息传递到H5首页
- 如果未登录跳转至小程序登录页
- URL传递参数openid和mobile - URL传递参数openid和mobile
- H5端首页先检测token状态 - H5端首页先检测token状态
- 如果存在并且未过期, 则携带token获取用户信息 - 如果存在并且未过期, 则携带token获取用户信息
- 如果不存在则通过openid和mobile获取token, 并获取用户信息 - 如果不存在则通过openid和mobile获取token, 并获取用户信息
- 新建项目问题 - 语音接口识别
- 目前是实时更新模式, 保存到本地缓存没问题, 但是要持久化到数据库就会报错 - 接口鲁棒性调试
- 增加保存按钮, 点击保存后提交接口, 并同步本地数据 - 识别消息对接创建交易记录接口
- 分别处理本地新建项目和提交接口新建项目的逻辑, 并处理错误提示, 例如项目名称重复 - 约定和调试大模型返回数据
- 任务过滤查询
- 日期等于今天的任务
- 日期小于今日的任务
- 查询所有任务匹配到日历中
- 今日任务和日历中任务的匹配调节
- 测试任务2没有显示在今日任务中
- 最可能原因是日期转换过程中的时区差异。当API返回的日期时间字符串 "2026-01-14T00:00:00" 在客户端进行时区转换时,可能会因为客户端的时区设置导致日期变成 2026-01-13 或其他日期,从而不满足 t.date === systemDate 的条件。
- 多个在增删改查操作中存在的时区转换问题
- 开发任务 - 开发任务
- 小程序跳转时增加登录判断和URL传参(openid和mobile)
- 1小时/人
- H5首页接收openid和mobile参数 - H5首页接收openid和mobile参数
- 0.5小时/人 - 0.5小时/人
- H5首页token检测并获取, 通过接口获取用户信息 - H5首页token检测并获取, 通过接口获取用户信息
@@ -94,11 +170,19 @@
- 2小时/人 - 2小时/人
- 开发H5端获取用户信息接口 - 开发H5端获取用户信息接口
- 1.5小时/人 - 1.5小时/人
- 开发项目增删改查接口 - 开发交易记录增删改查接口
- 4小时/人 - 4小时/人
- 开发任务增删改查接口和已完成/未完成接口 - 开发聊天增删改查接口
- 5小时/人 - 6小时/人
- 开发任务按时间过滤列表接口 - 开发预算增删改查接口
- 4小时/人
- 开发账单提醒增删改查完成接口
- 6小时/人
- 开发AI助手设置增删改查接口
- 4小时/人
- 开发用户偏好设置增删改查接口
- 3小时/人
- 调试H5页面交互, 调用交易记录、聊天、预算、账单提醒、助手设置、用户偏好接口
- 8~12小时/人
- 切换线上配置,打包、发布到线上服务器
- 2小时/人 - 2小时/人
- 调试H5页面交互, 调用项目和任务接口
- 6~10小时/人

View File

@@ -4,7 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>崔经理记账</title> <title>崔经理记账</title>
<script src="https://cdn.tailwindcss.com"></script> <!-- <script src="https://cdn.tailwindcss.com"></script> -->
<script src="https://mp.yizhezhe.com/static/tailwindcss-3.4.17.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;900&display=swap" rel="stylesheet">

678
services/api.ts Normal file
View File

@@ -0,0 +1,678 @@
import { Transaction, Message, BillReminder } from '../types';
// API基础配置
const getApiUrl = (): string => {
return process.env.API_URL || 'http://localhost:8000';
};
const getAuthToken = (): string | null => {
return localStorage.getItem('auth_token');
};
// 统一响应格式
interface ApiResponse<T = any> {
code: number;
msg?: string;
message?: string;
result?: T;
}
// 分页响应格式
interface PaginatedResponse<T> {
items: T[];
skip: number;
total: number;
}
// 统计数据响应格式
interface TransactionStats {
month: string;
total_income: number;
total_expense: number;
balance: number;
expense_by_category: Record<string, number>;
income_by_category: Record<string, number>;
daily_expense: Array<{ date: string; amount: number }>;
}
// 预算响应格式
interface Budget {
id: string;
user_id?: string;
member_id?: string;
budgets: Record<string, number>;
month: string;
created_at?: string;
updated_at?: string;
}
// 预算执行情况
interface BudgetExecution {
month: string;
budgets: Record<string, {
budget: number;
spent: number;
remaining: number;
percentage: number;
}>;
total_budget: number;
total_spent: number;
total_remaining: number;
is_over_budget: boolean;
}
// AI设置
interface AISettings {
id?: string;
user_id?: string;
member_id?: string;
name: string;
persona: string;
avatar?: string;
created_at?: string;
updated_at?: string;
}
// 通用请求函数
async function request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const token = getAuthToken();
const apiUrl = getApiUrl();
const url = `${apiUrl}${endpoint}`;
console.log(`请求地址: ${url}`);
const defaultHeaders: HeadersInit = {
'Content-Type': 'application/json',
};
if (token) {
defaultHeaders['Authorization'] = `Bearer ${token}`;
}
const config: RequestInit = {
...options,
headers: {
...defaultHeaders,
...options.headers,
},
};
try {
const response = await fetch(url, config);
const data: ApiResponse<T> = await response.json();
if (!response.ok || (data.code && data.code !== 200)) {
const errorMsg = data.msg || data.message || `HTTP error! status: ${response.status}`;
throw new Error(errorMsg);
}
return data.result as T;
} catch (error) {
console.error('API请求错误:', error);
throw error;
}
}
// ==================== 用户认证接口 ====================
/**
* 登录或注册
*/
export async function loginOrRegister(openid: string, mobile: string): Promise<{
token: string;
user: any;
}> {
const apiUrl = getApiUrl();
console.log(`请求地址: ${apiUrl}/wx/loginOrRegister`);
const response = await fetch(`${apiUrl}/wx/loginOrRegister`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ openid, mobile }),
});
const data = await response.json();
if (data.code === 200 && data.result?.token) {
// 保存token到localStorage
localStorage.setItem('auth_token', data.result.token);
}
return data.result;
}
/**
* 获取用户信息
*/
export async function getUserProfile(): Promise<any> {
return request<any>('/wx/profile', {
method: 'GET',
});
}
/**
* 退出登录
*/
export function logout(): void {
localStorage.removeItem('auth_token');
}
// ==================== 交易记录接口 ====================
/**
* 创建交易记录
*/
export async function createTransaction(data: {
item: string;
amount: number;
type: 'expense' | 'income';
category: string;
date: string;
timestamp: number;
}): Promise<Transaction> {
return request<Transaction>('/bill/transactions/', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* 查询交易记录列表(分页)
*/
export async function getTransactions(params?: {
skip?: number;
limit?: number;
type?: 'expense' | 'income';
category?: string;
start_date?: string;
end_date?: string;
month?: string;
}): Promise<PaginatedResponse<Transaction>> {
const queryString = new URLSearchParams(
Object.entries(params || {}).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = String(value);
}
return acc;
}, {} as Record<string, string>)
).toString();
return request<PaginatedResponse<Transaction>>(
`/bill/transactions/${queryString ? `?${queryString}` : ''}`
);
}
/**
* 获取单个交易记录
*/
export async function getTransaction(id: string): Promise<Transaction> {
return request<Transaction>(`/bill/transactions/${id}`);
}
/**
* 更新交易记录
*/
export async function updateTransaction(
id: string,
data: Partial<{
item: string;
amount: number;
type: 'expense' | 'income';
category: string;
date: string;
timestamp: number;
}>
): Promise<Transaction> {
return request<Transaction>(`/bill/transactions/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
/**
* 删除交易记录
*/
export async function deleteTransaction(id: string): Promise<void> {
return request<void>(`/bill/transactions/${id}`, {
method: 'DELETE',
});
}
/**
* 批量删除交易记录
*/
export async function batchDeleteTransactions(
transactionIds: string[]
): Promise<{ deleted_count: number }> {
return request<{ deleted_count: number }>('/bill/transactions/batch', {
method: 'DELETE',
body: JSON.stringify({ transaction_ids: transactionIds }),
});
}
/**
* 获取交易统计数据
*/
export async function getTransactionStats(
month: string
): Promise<TransactionStats> {
return request<TransactionStats>(`/bill/transactions/stats/summary?month=${month}`);
}
// ==================== 聊天消息接口 ====================
/**
* 创建聊天消息
*/
export async function createMessage(data: {
role: 'user' | 'assistant';
content: string;
timestamp: number;
transaction_id?: string;
}): Promise<Message> {
return request<Message>('/bill/messages/', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* 查询聊天消息列表
*/
export async function getMessages(params?: {
skip?: number;
limit?: number;
role?: 'user' | 'assistant';
start_timestamp?: number;
end_timestamp?: number;
}): Promise<PaginatedResponse<Message>> {
const queryString = new URLSearchParams(
Object.entries(params || {}).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = String(value);
}
return acc;
}, {} as Record<string, string>)
).toString();
return request<PaginatedResponse<Message>>(
`/bill/messages/${queryString ? `?${queryString}` : ''}`
);
}
/**
* 获取单个消息
*/
export async function getMessage(id: string): Promise<Message> {
return request<Message>(`/bill/messages/${id}`);
}
/**
* 删除消息
*/
export async function deleteMessage(id: string): Promise<void> {
return request<void>(`/bill/messages/${id}`, {
method: 'DELETE',
});
}
/**
* 清空聊天历史
*/
export async function clearMessages(): Promise<{ deleted_count: number }> {
return request<{ deleted_count: number }>('/bill/messages/clear', {
method: 'DELETE',
});
}
// ==================== 预算管理接口 ====================
/**
* 获取月度预算
*/
export async function getMonthlyBudget(month: string): Promise<Budget> {
return request<Budget>(`/bill/budgets/${month}`);
}
/**
* 设置/更新月度预算
*/
export async function updateMonthlyBudget(
month: string,
budgets: Record<string, number>
): Promise<Budget> {
return request<Budget>(`/bill/budgets/${month}`, {
method: 'PUT',
body: JSON.stringify({ budgets }),
});
}
/**
* 获取预算执行情况
*/
export async function getBudgetExecution(
month: string
): Promise<BudgetExecution> {
return request<BudgetExecution>(`/bill/budgets/${month}/execution`);
}
// ==================== 账单提醒接口 ====================
/**
* 创建账单提醒
*/
export async function createReminder(data: {
title: string;
amount?: number;
frequency: 'monthly' | 'once';
target_date: string;
}): Promise<BillReminder> {
return request<BillReminder>('/bill/reminders/', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* 查询账单提醒列表
*/
export async function getReminders(params?: {
skip?: number;
limit?: number;
frequency?: 'monthly' | 'once';
is_completed?: boolean;
month?: string;
}): Promise<PaginatedResponse<BillReminder>> {
const queryString = new URLSearchParams(
Object.entries(params || {}).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = String(value);
}
return acc;
}, {} as Record<string, string>)
).toString();
return request<PaginatedResponse<BillReminder>>(
`/bill/reminders/${queryString ? `?${queryString}` : ''}`
);
}
/**
* 获取单个提醒
*/
export async function getReminder(id: string): Promise<BillReminder> {
return request<BillReminder>(`/bill/reminders/${id}`);
}
/**
* 更新账单提醒
*/
export async function updateReminder(
id: string,
data: Partial<{
title: string;
amount?: number;
frequency: 'monthly' | 'once';
target_date: string;
}>
): Promise<BillReminder> {
return request<BillReminder>(`/bill/reminders/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
/**
* 切换提醒完成状态
*/
export async function toggleReminder(
id: string
): Promise<{ id: string; is_completed: boolean }> {
return request<{ id: string; is_completed: boolean }>(
`/bill/reminders/${id}/toggle`,
{
method: 'PATCH',
}
);
}
/**
* 删除账单提醒
*/
export async function deleteReminder(id: string): Promise<void> {
return request<void>(`/bill/reminders/${id}`, {
method: 'DELETE',
});
}
/**
* 获取即将到期的提醒
*/
export async function getUpcomingReminders(
days: number = 7
): Promise<PaginatedResponse<BillReminder>> {
return request<PaginatedResponse<BillReminder>>(
`/bill/reminders/upcoming?days=${days}`
);
}
// ==================== AI设置接口 ====================
/**
* 获取AI设置
*/
export async function getAISettings(): Promise<AISettings> {
return request<AISettings>('/h5/ai-settings');
}
/**
* 更新AI设置
*/
export async function updateAISettings(data: {
name?: string;
persona?: string;
avatar?: string;
}): Promise<AISettings> {
return request<AISettings>('/h5/ai-settings', {
method: 'PUT',
body: JSON.stringify(data),
});
}
/**
* 重置AI设置为默认
*/
export async function resetAISettings(): Promise<AISettings> {
return request<AISettings>('/h5/ai-settings/reset', {
method: 'POST',
});
}
// ==================== 用户偏好接口 ====================
interface UserPreferences {
id?: string;
user_id?: string;
custom_preset_name?: string;
custom_preset_description?: string;
theme_index?: number;
created_at?: string;
updated_at?: string;
}
/**
* 获取用户偏好
*/
export async function getUserPreferences(): Promise<UserPreferences> {
return request<UserPreferences>('/h5/preferences');
}
/**
* 更新用户偏好
*/
export async function updateUserPreferences(
data: Partial<UserPreferences>
): Promise<UserPreferences> {
return request<UserPreferences>('/h5/preferences', {
method: 'PUT',
body: JSON.stringify(data),
});
}
// ==================== 数据同步接口 ====================
interface SyncData {
transactions?: Transaction[];
messages?: Message[];
budgets?: Record<string, number>;
reminders?: BillReminder[];
ai_settings?: AISettings;
preferences?: UserPreferences;
}
/**
* 全量数据同步(上传)
*/
export async function uploadSyncData(data: SyncData): Promise<{
synced_at: string;
summary: {
transactions_count: number;
messages_count: number;
reminders_count: number;
};
}> {
return request('/bill/sync/upload', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* 全量数据同步(下载)
*/
export async function downloadSyncData(): Promise<SyncData> {
return request<SyncData>('/bill/sync/download');
}
/**
* 增量数据同步
*/
export async function incrementalSync(data: {
last_sync_at: string;
changes: SyncData;
}): Promise<{
synced_at: string;
server_changes: SyncData;
}> {
return request('/bill/sync/incremental', {
method: 'POST',
body: JSON.stringify(data),
});
}
// ==================== AI处理接口 ====================
interface AIProcessResult {
transaction: {
item: string;
amount: number;
type: 'expense' | 'income';
category: string;
} | null;
reply: string;
created_transaction_id?: string;
created_message_ids?: string[];
}
/**
* AI处理用户消息
*/
export async function processAIMessage(data: {
message: string;
image_base64?: string;
mime_type?: string;
}): Promise<AIProcessResult> {
return request<AIProcessResult>('/ai/process', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* AI识别账单图片
*/
export async function recognizeReceipt(
imageFile: File
): Promise<AIProcessResult> {
const formData = new FormData();
formData.append('image', imageFile);
// 注意: FormData需要特殊处理,不设置Content-Type让浏览器自动设置
return request<AIProcessResult>('/ai/recognize-receipt', {
method: 'POST',
body: formData,
headers: {}, // 清空默认headers,让fetch自动处理multipart/form-data
});
}
// ==================== 导出所有API ====================
export default {
// 用户认证
loginOrRegister,
getUserProfile,
logout,
// 交易记录
createTransaction,
getTransactions,
getTransaction,
updateTransaction,
deleteTransaction,
batchDeleteTransactions,
getTransactionStats,
// 聊天消息
createMessage,
getMessages,
getMessage,
deleteMessage,
clearMessages,
// 预算管理
getMonthlyBudget,
updateMonthlyBudget,
getBudgetExecution,
// 账单提醒
createReminder,
getReminders,
getReminder,
updateReminder,
toggleReminder,
deleteReminder,
getUpcomingReminders,
// AI设置
getAISettings,
updateAISettings,
resetAISettings,
// 用户偏好
getUserPreferences,
updateUserPreferences,
// 数据同步
uploadSyncData,
downloadSyncData,
incrementalSync,
// AI处理
processAIMessage,
recognizeReceipt,
};