- 新增token管理和用户信息状态,支持基于token的自动登录 - 实现登录或注册接口调用,自动根据URL参数完成身份验证 - 从后端加载交易记录、消息和待办提醒,替代本地缓存读写 - 对交易记录的增删改操作同步到数据库,确保数据一致性 - 对消息和提醒的增删改操作支持服务器同步,提升数据可靠性 - 优化消息发送逻辑,先保存用户消息,再处理AI回复并存库 - 添加待办提醒切换状态API调用,确保状态同步服务器 - 改进界面互动,支持文件与头像上传功能,更新管家形象 - 采用异步处理并增加错误捕获和用户提示,增强稳定性
679 lines
14 KiB
TypeScript
679 lines
14 KiB
TypeScript
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,
|
|
};
|