feat(api): 集成后端API,实现数据持久化和用户登录
- 新增token管理和用户信息状态,支持基于token的自动登录 - 实现登录或注册接口调用,自动根据URL参数完成身份验证 - 从后端加载交易记录、消息和待办提醒,替代本地缓存读写 - 对交易记录的增删改操作同步到数据库,确保数据一致性 - 对消息和提醒的增删改操作支持服务器同步,提升数据可靠性 - 优化消息发送逻辑,先保存用户消息,再处理AI回复并存库 - 添加待办提醒切换状态API调用,确保状态同步服务器 - 改进界面互动,支持文件与头像上传功能,更新管家形象 - 采用异步处理并增加错误捕获和用户提示,增强稳定性
This commit is contained in:
382
App.tsx
382
App.tsx
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -346,13 +650,13 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-400 mb-2 uppercase tracking-wider">管家名字</label>
|
<label className="block text-xs font-bold text-gray-400 mb-2 uppercase tracking-wider">管家名字</label>
|
||||||
<input type="text" value={aiSettings.name} onChange={(e) => 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" />
|
<input type="text" value={aiSettings.name} onChange={(e) => 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" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-400 mb-3 uppercase tracking-wider">性格预设</label>
|
<label className="block text-xs font-bold text-gray-400 mb-3 uppercase tracking-wider">性格预设</label>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{personaPresets.map(p => (
|
{personaPresets.map(p => (
|
||||||
<button key={p.id} onClick={() => setAiSettings({...aiSettings, name: p.name.split(' ')[0], persona: p.description})} className={`p-4 rounded-2xl border text-left transition-all ${aiSettings.persona === p.description ? 'bg-indigo-600 border-indigo-600 text-white shadow-lg shadow-indigo-100' : 'bg-white border-gray-100 hover:bg-gray-50 text-gray-600'}`}>
|
<button key={p.id} onClick={() => setAiSettings({ ...aiSettings, name: p.name.split(' ')[0], persona: p.description })} className={`p-4 rounded-2xl border text-left transition-all ${aiSettings.persona === p.description ? 'bg-indigo-600 border-indigo-600 text-white shadow-lg shadow-indigo-100' : 'bg-white border-gray-100 hover:bg-gray-50 text-gray-600'}`}>
|
||||||
<div className={`font-bold text-sm mb-1 ${aiSettings.persona === p.description ? 'text-white' : 'text-gray-900'}`}>{p.name}</div>
|
<div className={`font-bold text-sm mb-1 ${aiSettings.persona === p.description ? 'text-white' : 'text-gray-900'}`}>{p.name}</div>
|
||||||
<div className={`text-[10px] leading-tight ${aiSettings.persona === p.description ? 'text-indigo-100' : 'text-gray-400'}`}>{p.description.slice(0, 24)}...</div>
|
<div className={`text-[10px] leading-tight ${aiSettings.persona === p.description ? 'text-indigo-100' : 'text-gray-400'}`}>{p.description.slice(0, 24)}...</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -361,7 +665,7 @@ const App: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-bold text-gray-400 mb-2 uppercase tracking-wider">详细性格定义</label>
|
<label className="block text-xs font-bold text-gray-400 mb-2 uppercase tracking-wider">详细性格定义</label>
|
||||||
<textarea value={aiSettings.persona} onChange={(e) => setAiSettings({...aiSettings, persona: e.target.value})} className="w-full p-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none text-sm text-gray-600 h-28 resize-none" />
|
<textarea value={aiSettings.persona} onChange={(e) => setAiSettings({ ...aiSettings, persona: e.target.value })} className="w-full p-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none text-sm text-gray-600 h-28 resize-none" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-5 shrink-0 border-t border-gray-100 mt-2">
|
<div className="pt-5 shrink-0 border-t border-gray-100 mt-2">
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
222
docs/数据结构.md
222
docs/数据结构.md
@@ -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小时/人
|
|
||||||
@@ -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
678
services/api.ts
Normal 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,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user