feat(api): 集成后端API,实现数据持久化和用户登录
- 新增token管理和用户信息状态,支持基于token的自动登录 - 实现登录或注册接口调用,自动根据URL参数完成身份验证 - 从后端加载交易记录、消息和待办提醒,替代本地缓存读写 - 对交易记录的增删改操作同步到数据库,确保数据一致性 - 对消息和提醒的增删改操作支持服务器同步,提升数据可靠性 - 优化消息发送逻辑,先保存用户消息,再处理AI回复并存库 - 添加待办提醒切换状态API调用,确保状态同步服务器 - 改进界面互动,支持文件与头像上传功能,更新管家形象 - 采用异步处理并增加错误捕获和用户提示,增强稳定性
This commit is contained in:
@@ -23,8 +23,8 @@ const EditTransactionModal: React.FC<EditTransactionModalProps> = ({ transaction
|
||||
};
|
||||
|
||||
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="bg-white w-full max-w-md rounded-2xl p-6 shadow-2xl animate-float md:animate-none">
|
||||
<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 md:animate-none max-h-[calc(100vh-120px)] overflow-y-auto no-scrollbar">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-bold text-gray-900">编辑账单</h3>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
|
||||
@@ -6,8 +6,8 @@ import BillReminders, { getDaysRemaining } from './BillReminders';
|
||||
|
||||
interface LedgerPageProps {
|
||||
transactions: Transaction[];
|
||||
onUpdateTransaction: (t: Transaction) => void;
|
||||
onDeleteTransaction: (id: string) => void;
|
||||
onUpdateTransaction: (t: Transaction) => Promise<void>;
|
||||
onDeleteTransaction: (id: string) => Promise<void>;
|
||||
budgets: Record<string, number>;
|
||||
setBudgets: (val: Record<string, number>) => void;
|
||||
reminders: BillReminder[];
|
||||
@@ -200,7 +200,24 @@ const LedgerPage: React.FC<LedgerPageProps> = ({
|
||||
{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} />}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,8 @@ const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isF
|
||||
const recognitionRef = useRef<any>(null);
|
||||
const timerRef = useRef<number | null>(null);
|
||||
|
||||
const isStartingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
@@ -22,30 +24,37 @@ const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isF
|
||||
recognitionRef.current.onresult = (event: any) => {
|
||||
const transcript = event.results[0][0].transcript;
|
||||
if (transcript) onTranscript(transcript);
|
||||
setIsListening(false);
|
||||
};
|
||||
|
||||
recognitionRef.current.onerror = (event: any) => {
|
||||
if (event.error === 'network') {
|
||||
alert("语音识别网络连接失败。请检查网络或尝试切换浏览器(推荐 Chrome/Edge)。");
|
||||
}
|
||||
console.error('Speech recognition error', event.error);
|
||||
setIsListening(false);
|
||||
};
|
||||
|
||||
recognitionRef.current.onend = () => {
|
||||
setIsListening(false);
|
||||
isStartingRef.current = false;
|
||||
};
|
||||
}
|
||||
}, [onTranscript]);
|
||||
|
||||
const startListening = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
e.preventDefault();
|
||||
if (isProcessing || isListening) return;
|
||||
const startListening = (e: React.PointerEvent) => {
|
||||
if (e.cancelable) e.preventDefault();
|
||||
|
||||
if (isProcessing || isListening || isStartingRef.current) return;
|
||||
|
||||
if (recognitionRef.current) {
|
||||
try {
|
||||
isStartingRef.current = true;
|
||||
recognitionRef.current.start();
|
||||
setIsListening(true);
|
||||
} catch (e) {
|
||||
console.error("Failed to start recognition", e);
|
||||
} catch (err) {
|
||||
console.error("Failed to start recognition", err);
|
||||
// 如果 start 失败,立即重置状态,否则会锁死
|
||||
isStartingRef.current = false;
|
||||
setIsListening(false);
|
||||
}
|
||||
} else {
|
||||
alert("您的浏览器不支持语音识别,请使用 Chrome 或 Edge。");
|
||||
@@ -53,20 +62,23 @@ const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isF
|
||||
};
|
||||
|
||||
const stopListening = () => {
|
||||
// 仅在识别引擎正在运行时尝试停止
|
||||
// 不要在这里重置 isListening 和 isStartingRef,等待 onend 回调
|
||||
if (isListening) {
|
||||
recognitionRef.current?.stop();
|
||||
setIsListening(false);
|
||||
try {
|
||||
recognitionRef.current?.stop();
|
||||
} catch (err) {
|
||||
console.error("Failed to stop recognition", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${isFullWidth ? 'w-full' : ''} relative select-none touch-none`}>
|
||||
<button
|
||||
onMouseDown={startListening}
|
||||
onMouseUp={stopListening}
|
||||
onMouseLeave={stopListening}
|
||||
onTouchStart={startListening}
|
||||
onTouchEnd={stopListening}
|
||||
onPointerDown={startListening}
|
||||
onPointerUp={stopListening}
|
||||
onPointerLeave={stopListening}
|
||||
disabled={isProcessing}
|
||||
className={`relative flex items-center justify-center transition-all duration-300 active:scale-95 shadow-lg ${
|
||||
isFullWidth
|
||||
|
||||
222
docs/数据结构.md
222
docs/数据结构.md
@@ -1,4 +1,5 @@
|
||||
# App_02_记账
|
||||
|
||||
- 应用信息
|
||||
- 应用名称
|
||||
- 记账
|
||||
@@ -9,83 +10,158 @@
|
||||
- 应用描述
|
||||
- 一个精明、毒舌但实际上很负责任的财务经理。说话喜欢带刺,吐槽用户乱花钱,但关键时刻会给出靠谱建议。
|
||||
- 数据结构
|
||||
- Project 项目分类
|
||||
- name 名称(name)
|
||||
- desc 背景(context)
|
||||
- target 目标(goal)
|
||||
- value 价值(value)
|
||||
- 枚举数据
|
||||
- TransactionType 交易类型
|
||||
- EXPENSE = "expense"
|
||||
- INCOME = "income"
|
||||
- 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 创建人
|
||||
- Member
|
||||
- Task 任务项目
|
||||
- title 标题(title)
|
||||
- project 分类(projectId)
|
||||
- Project
|
||||
- desc 描述(notes)
|
||||
- startDate 开始日期(date)
|
||||
- startTime 开始时间(startTime)
|
||||
- isCompleted 是否完成(isCompleted)
|
||||
- created_at 创建时间(createdAt)
|
||||
- creator 创建人
|
||||
- Member
|
||||
- created_at 创建时间
|
||||
- updated_at 更新时间
|
||||
- Transaction 交易记录
|
||||
- item 交易项目名称
|
||||
- amount: float 交易金额
|
||||
- type: TransactionType 交易类型
|
||||
- category: ExpenseCategory | IncomeCategory 交易分类
|
||||
- date 交易日期 YYYY-MM-DD
|
||||
- timestamp: int 时间戳(毫秒)
|
||||
- Message 聊天消息
|
||||
- role: MessageRole 消息角色
|
||||
- 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接口
|
||||
- auth 授权
|
||||
- POST /mp/auth/get_token
|
||||
- Auth 授权
|
||||
- POST /api/wx/get_token
|
||||
- 获取token
|
||||
- GET /mp/auth/profile
|
||||
- GET /api/wx/profile
|
||||
- 获取个人信息
|
||||
- 项目
|
||||
- GET /mp/todo/projects
|
||||
- 项目列表
|
||||
- GET /mp/todo/projects/{project_id}
|
||||
- 获取项目详细
|
||||
- POST /mp/todo/projects
|
||||
- 创建项目
|
||||
- PUT /mp/todo/projects/{project_id}
|
||||
- 更新项目
|
||||
- DELETE /mp/todo/projects/{project_id}
|
||||
- 删除项目
|
||||
- 任务
|
||||
- GET /mp/todo/tasks
|
||||
- 任务列表
|
||||
- GET /mp/todo/tasks/{task_id}
|
||||
- 获取任务详细
|
||||
- POST /mp/todo/tasks
|
||||
- 创建任务
|
||||
- PUT /mp/todo/tasks/{task_id}
|
||||
- 更新任务
|
||||
- DELETE /mp/todo/tasks/{task_id}
|
||||
- 删除项目
|
||||
- PUT /mp/todo/tasks/{task_id}/completed
|
||||
- 设置为已完成
|
||||
- PUT /mp/todo/tasks/{task_id}/uncompleted
|
||||
- 设置为未完成
|
||||
- transaction Bill: 交易记录
|
||||
- GET /api/bill/transactions
|
||||
- 获取分页交易记录
|
||||
- GET /api/bill/transactions/{id}
|
||||
- 获取交易记录详情
|
||||
- POST /api/bill/transactions
|
||||
- 创建交易记录
|
||||
- PUT /api/bill/transactions/{id}
|
||||
- 更新交易记录
|
||||
- DELETE /api/bill/transactions/{id}
|
||||
- 删除交易记录
|
||||
- Message Bill: 聊天消息
|
||||
- GET /api/bill/messages
|
||||
- 获取分页聊天消息
|
||||
- GET /api/bill/messages/{id}
|
||||
- 获取聊天消息详情
|
||||
- POST /api/bill/messages
|
||||
- 创建聊天消息
|
||||
- PUT /api/bill/messages/{id}
|
||||
- 更新聊天消息
|
||||
- DELETE /api/bill/messages/{id}
|
||||
- 删除聊天消息
|
||||
- Budget Bill: 预算
|
||||
- GET /api/bill/budgets
|
||||
- 获取分页预算
|
||||
- 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字段
|
||||
- 登录问题
|
||||
- 小程序跳转到H5, 需要检测登录状态
|
||||
- 如果已经登录, 需要把登录信息传递到H5首页
|
||||
- 如果未登录跳转至小程序登录页
|
||||
- URL传递参数openid和mobile
|
||||
- H5端首页先检测token状态
|
||||
- 如果存在并且未过期, 则携带token获取用户信息
|
||||
- 如果不存在则通过openid和mobile获取token, 并获取用户信息
|
||||
- 新建项目问题
|
||||
- 目前是实时更新模式, 保存到本地缓存没问题, 但是要持久化到数据库就会报错
|
||||
- 增加保存按钮, 点击保存后提交接口, 并同步本地数据
|
||||
- 分别处理本地新建项目和提交接口新建项目的逻辑, 并处理错误提示, 例如项目名称重复
|
||||
- 任务过滤查询
|
||||
- 日期等于今天的任务
|
||||
- 日期小于今日的任务
|
||||
- 查询所有任务匹配到日历中
|
||||
- 今日任务和日历中任务的匹配调节
|
||||
- 测试任务2没有显示在今日任务中
|
||||
- 最可能原因是日期转换过程中的时区差异。当API返回的日期时间字符串 "2026-01-14T00:00:00" 在客户端进行时区转换时,可能会因为客户端的时区设置导致日期变成 2026-01-13 或其他日期,从而不满足 t.date === systemDate 的条件。
|
||||
- 多个在增删改查操作中存在的时区转换问题
|
||||
- 语音接口识别
|
||||
- 接口鲁棒性调试
|
||||
- 识别消息对接创建交易记录接口
|
||||
- 约定和调试大模型返回数据
|
||||
|
||||
- 开发任务
|
||||
- 小程序跳转时增加登录判断和URL传参(openid和mobile)
|
||||
- 1小时/人
|
||||
- H5首页接收openid和mobile参数
|
||||
- 0.5小时/人
|
||||
- H5首页token检测并获取, 通过接口获取用户信息
|
||||
@@ -94,11 +170,19 @@
|
||||
- 2小时/人
|
||||
- 开发H5端获取用户信息接口
|
||||
- 1.5小时/人
|
||||
- 开发项目增删改查接口
|
||||
- 开发交易记录增删改查接口
|
||||
- 4小时/人
|
||||
- 开发任务增删改查接口和已完成/未完成接口
|
||||
- 5小时/人
|
||||
- 开发任务按时间过滤列表接口
|
||||
- 开发聊天增删改查接口
|
||||
- 6小时/人
|
||||
- 开发预算增删改查接口
|
||||
- 4小时/人
|
||||
- 开发账单提醒增删改查完成接口
|
||||
- 6小时/人
|
||||
- 开发AI助手设置增删改查接口
|
||||
- 4小时/人
|
||||
- 开发用户偏好设置增删改查接口
|
||||
- 3小时/人
|
||||
- 调试H5页面交互, 调用交易记录、聊天、预算、账单提醒、助手设置、用户偏好接口
|
||||
- 8~12小时/人
|
||||
- 切换线上配置,打包、发布到线上服务器
|
||||
- 2小时/人
|
||||
- 调试H5页面交互, 调用项目和任务接口
|
||||
- 6~10小时/人
|
||||
@@ -4,7 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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.gstatic.com" crossorigin>
|
||||
<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