refactor(api): 精简并移除多余的API接口和类型定义
- 移除多余的统计、预算、AI设置、用户偏好、数据同步及AI处理相关类型定义 - 删除大量未使用的API请求函数,简化导出接口 - 保留核心的用户认证、交易记录、聊天消息和账单提醒相关接口 - 优化App组件状态初始化,改为从localStorage加载持久化数据,避免重复请求 - VoiceInput组件重构,改用持续监听及更健壮的权限和错误处理逻辑 - 优化语音识别启动和停止机制,提升用户体验和错误恢复能力 - 更新依赖顺序,无功能性变更
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -24,4 +24,6 @@ dist-ssr
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
.qoder
|
.qoder
|
||||||
.trae
|
.trae
|
||||||
|
|
||||||
|
*.tar
|
||||||
78
App.tsx
78
App.tsx
@@ -5,7 +5,6 @@ import LedgerPage from './components/LedgerPage';
|
|||||||
import StatisticsPage from './components/StatisticsPage';
|
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 {
|
import {
|
||||||
deleteMessage, deleteTransaction, updateTransaction, createTransaction, createMessage, getMessages, getTransactions, loginOrRegister, getUserProfile,
|
deleteMessage, deleteTransaction, updateTransaction, createTransaction, createMessage, getMessages, getTransactions, loginOrRegister, getUserProfile,
|
||||||
createReminder, getReminders, updateReminder, toggleReminder, deleteReminder
|
createReminder, getReminders, updateReminder, toggleReminder, deleteReminder
|
||||||
@@ -84,10 +83,40 @@ const getCategoryIcon = (category: string, type: TransactionType) => {
|
|||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('chat');
|
const [activeTab, setActiveTab] = useState<Tab>('chat');
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>(() => {
|
||||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
try {
|
||||||
const [budgets, setBudgets] = useState<Record<string, number>>({ '总预算': 3000 });
|
const saved = localStorage.getItem('cui_messages');
|
||||||
const [reminders, setReminders] = useState<BillReminder[]>([]);
|
return saved ? JSON.parse(saved) : [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const [transactions, setTransactions] = useState<Transaction[]>(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('cui_transactions');
|
||||||
|
return saved ? JSON.parse(saved) : [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const [budgets, setBudgets] = useState<Record<string, number>>(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('cui_budget');
|
||||||
|
if (saved) {
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
return typeof parsed === 'number' ? { '总预算': parsed } : parsed;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
return { '总预算': 3000 };
|
||||||
|
});
|
||||||
|
const [reminders, setReminders] = useState<BillReminder[]>(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('cui_reminders');
|
||||||
|
return saved ? JSON.parse(saved) : [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
const [inputText, setInputText] = useState('');
|
const [inputText, setInputText] = useState('');
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [editingTransaction, setEditingTransaction] = useState<Transaction | null>(null);
|
const [editingTransaction, setEditingTransaction] = useState<Transaction | null>(null);
|
||||||
@@ -96,9 +125,23 @@ const App: React.FC = () => {
|
|||||||
const [token, setToken] = useState<string | null>(null);
|
const [token, setToken] = useState<string | null>(null);
|
||||||
const [userInfo, setUserInfo] = useState<any>(null);
|
const [userInfo, setUserInfo] = useState<any>(null);
|
||||||
|
|
||||||
const [aiSettings, setAiSettings] = useState<AISettings>(DEFAULT_AI_SETTINGS);
|
const [aiSettings, setAiSettings] = useState<AISettings>(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('cui_ai_settings');
|
||||||
|
return saved ? JSON.parse(saved) : DEFAULT_AI_SETTINGS;
|
||||||
|
} catch (e) {
|
||||||
|
return DEFAULT_AI_SETTINGS;
|
||||||
|
}
|
||||||
|
});
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
const [customPreset, setCustomPreset] = useState({ name: '自定义', description: '可以根据你喜欢的方式设定助手的语气和风格' });
|
const [customPreset, setCustomPreset] = useState(() => {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('cui_custom_preset_info');
|
||||||
|
return saved ? JSON.parse(saved) : { name: '自定义', description: '可以根据你喜欢的方式设定助手的语气和风格' };
|
||||||
|
} catch (e) {
|
||||||
|
return { name: '自定义', description: '可以根据你喜欢的方式设定助手的语气和风格' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const avatarInputRef = useRef<HTMLInputElement>(null);
|
const avatarInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -177,19 +220,6 @@ const App: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadInitialData = async () => {
|
const loadInitialData = async () => {
|
||||||
const storedBudget = localStorage.getItem('cui_budget');
|
|
||||||
const storedReminders = localStorage.getItem('cui_reminders');
|
|
||||||
const storedAiSettings = localStorage.getItem('cui_ai_settings');
|
|
||||||
const storedCustom = localStorage.getItem('cui_custom_preset_info');
|
|
||||||
|
|
||||||
if (storedBudget) {
|
|
||||||
const parsed = JSON.parse(storedBudget);
|
|
||||||
setBudgets(typeof parsed === 'number' ? { '总预算': parsed } : parsed);
|
|
||||||
}
|
|
||||||
if (storedReminders) setReminders(JSON.parse(storedReminders));
|
|
||||||
if (storedAiSettings) setAiSettings(JSON.parse(storedAiSettings));
|
|
||||||
if (storedCustom) setCustomPreset(JSON.parse(storedCustom));
|
|
||||||
|
|
||||||
// 从数据库获取交易列表
|
// 从数据库获取交易列表
|
||||||
try {
|
try {
|
||||||
const response = await getTransactions();
|
const response = await getTransactions();
|
||||||
@@ -238,7 +268,7 @@ const App: React.FC = () => {
|
|||||||
console.log('消息列表已从数据库加载:', messagesFromDB.length);
|
console.log('消息列表已从数据库加载:', messagesFromDB.length);
|
||||||
} else {
|
} else {
|
||||||
// 如果数据库中没有消息,显示初始欢迎消息
|
// 如果数据库中没有消息,显示初始欢迎消息
|
||||||
const aiName = storedAiSettings ? JSON.parse(storedAiSettings).name : '崔经理';
|
const aiName = aiSettings.name;
|
||||||
setMessages([{
|
setMessages([{
|
||||||
id: 'init',
|
id: 'init',
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@@ -254,7 +284,7 @@ const App: React.FC = () => {
|
|||||||
setMessages(JSON.parse(storedMessages));
|
setMessages(JSON.parse(storedMessages));
|
||||||
} else {
|
} else {
|
||||||
// 显示初始欢迎消息
|
// 显示初始欢迎消息
|
||||||
const aiName = storedAiSettings ? JSON.parse(storedAiSettings).name : '崔经理';
|
const aiName = aiSettings.name;
|
||||||
setMessages([{
|
setMessages([{
|
||||||
id: 'init',
|
id: 'init',
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@@ -452,7 +482,7 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendMessage = async (text: string, imageBase64?: string, mimeType?: string) => {
|
const handleSendMessage = React.useCallback(async (text: string, imageBase64?: string, mimeType?: string) => {
|
||||||
if (!text.trim() && !imageBase64) return;
|
if (!text.trim() && !imageBase64) return;
|
||||||
|
|
||||||
// 先保存用户消息到数据库
|
// 先保存用户消息到数据库
|
||||||
@@ -542,7 +572,7 @@ const App: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
}
|
}
|
||||||
};
|
}, [aiSettings, transactions, messages]);
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
|||||||
@@ -9,66 +9,160 @@ interface VoiceInputProps {
|
|||||||
const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isFullWidth = false }) => {
|
const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isFullWidth = false }) => {
|
||||||
const [isListening, setIsListening] = useState(false);
|
const [isListening, setIsListening] = useState(false);
|
||||||
const recognitionRef = useRef<any>(null);
|
const recognitionRef = useRef<any>(null);
|
||||||
const timerRef = useRef<number | null>(null);
|
|
||||||
|
|
||||||
const isStartingRef = useRef(false);
|
const isStartingRef = useRef(false);
|
||||||
|
const onTranscriptRef = useRef(onTranscript);
|
||||||
|
|
||||||
|
// 始终保持 ref 为最新,避免在 useEffect 中依赖 onTranscript 导致重复初始化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
|
onTranscriptRef.current = onTranscript;
|
||||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
||||||
recognitionRef.current = new SpeechRecognition();
|
|
||||||
recognitionRef.current.continuous = false;
|
|
||||||
recognitionRef.current.lang = 'zh-CN';
|
|
||||||
recognitionRef.current.interimResults = false;
|
|
||||||
|
|
||||||
recognitionRef.current.onresult = (event: any) => {
|
|
||||||
const transcript = event.results[0][0].transcript;
|
|
||||||
if (transcript) onTranscript(transcript);
|
|
||||||
};
|
|
||||||
|
|
||||||
recognitionRef.current.onerror = (event: any) => {
|
|
||||||
if (event.error === 'network') {
|
|
||||||
alert("语音识别网络连接失败。请检查网络或尝试切换浏览器(推荐 Chrome/Edge)。");
|
|
||||||
}
|
|
||||||
console.error('Speech recognition error', event.error);
|
|
||||||
};
|
|
||||||
|
|
||||||
recognitionRef.current.onend = () => {
|
|
||||||
setIsListening(false);
|
|
||||||
isStartingRef.current = false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [onTranscript]);
|
}, [onTranscript]);
|
||||||
|
|
||||||
|
const initRecognition = () => {
|
||||||
|
if (!('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)) return;
|
||||||
|
|
||||||
|
// 如果已有实例,先清理
|
||||||
|
if (recognitionRef.current) {
|
||||||
|
try {
|
||||||
|
recognitionRef.current.abort();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
|
||||||
|
const recognition = new SpeechRecognition();
|
||||||
|
|
||||||
|
recognition.continuous = true;
|
||||||
|
recognition.lang = 'zh-CN';
|
||||||
|
recognition.interimResults = true;
|
||||||
|
|
||||||
|
recognition.onstart = () => {
|
||||||
|
setIsListening(true);
|
||||||
|
isStartingRef.current = false;
|
||||||
|
console.log('Speech recognition started');
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onresult = (event: any) => {
|
||||||
|
const results = event.results;
|
||||||
|
for (let i = event.resultIndex; i < results.length; i++) {
|
||||||
|
if (results[i].isFinal) {
|
||||||
|
const transcript = results[i][0].transcript;
|
||||||
|
if (transcript) onTranscriptRef.current(transcript);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onerror = async (event: any) => {
|
||||||
|
console.error('Speech recognition error', event.error);
|
||||||
|
|
||||||
|
let message = '';
|
||||||
|
switch (event.error) {
|
||||||
|
case 'network':
|
||||||
|
message = "语音识别网络连接失败。请检查网络或尝试切换浏览器(推荐 Chrome/Edge)。";
|
||||||
|
break;
|
||||||
|
case 'not-allowed':
|
||||||
|
case 'service-not-allowed':
|
||||||
|
// 移动端初次授权时可能触发 race condition:用户点击“允许”但 start() 已超时失败
|
||||||
|
// 此时我们通过 getUserMedia 探测真实权限状态
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
// 如果能获取到流,说明权限其实是有的,只是刚才 start() 冲突了
|
||||||
|
stream.getTracks().forEach(t => t.stop());
|
||||||
|
console.log('Permission probe successful, refreshing recognition instance...');
|
||||||
|
isStartingRef.current = false;
|
||||||
|
setIsListening(false);
|
||||||
|
initRecognition(); // 静默重置实例
|
||||||
|
return; // 不弹窗,用户再次点击就会成功
|
||||||
|
} catch (e) {
|
||||||
|
// 探测也失败,说明是真的被拒绝了
|
||||||
|
message = "麦克风访问被拒绝。\n\n解决办法:\n1. 点击地址栏左侧的“锁”或“设置”图标\n2. 找到“麦克风”并设为“允许”\n3. 刷新页面后重试";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'no-speech':
|
||||||
|
return;
|
||||||
|
case 'aborted':
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message) alert(message);
|
||||||
|
|
||||||
|
setIsListening(false);
|
||||||
|
isStartingRef.current = false;
|
||||||
|
// 出错后重新初始化,确保下次点击可用
|
||||||
|
initRecognition();
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onend = () => {
|
||||||
|
console.log('Speech recognition ended');
|
||||||
|
setIsListening(false);
|
||||||
|
isStartingRef.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
recognitionRef.current = recognition;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initRecognition();
|
||||||
|
return () => {
|
||||||
|
if (recognitionRef.current) {
|
||||||
|
try {
|
||||||
|
recognitionRef.current.abort();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const startListening = (e: React.PointerEvent) => {
|
const startListening = (e: React.PointerEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
if (e.cancelable) e.preventDefault();
|
if (e.cancelable) e.preventDefault();
|
||||||
|
|
||||||
|
// 如果已经在处理中、正在监听或正在启动,则忽略
|
||||||
if (isProcessing || isListening || isStartingRef.current) return;
|
if (isProcessing || isListening || isStartingRef.current) return;
|
||||||
|
|
||||||
|
// 获取指针捕获,确保即使手指移出按钮范围也能触发 onPointerUp
|
||||||
|
try {
|
||||||
|
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to set pointer capture", err);
|
||||||
|
}
|
||||||
|
|
||||||
if (recognitionRef.current) {
|
if (recognitionRef.current) {
|
||||||
|
// 检查是否为安全上下文 (HTTPS 或 localhost)
|
||||||
|
if (!window.isSecureContext) {
|
||||||
|
alert("语音识别失败:浏览器限制在非 HTTPS 环境下使用麦克风。请确保使用 HTTPS 访问,或在 PC 端 Chrome 的 'Insecure origins treated as secure' 中设置白名单。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isStartingRef.current = true;
|
isStartingRef.current = true;
|
||||||
recognitionRef.current.start();
|
recognitionRef.current.start();
|
||||||
setIsListening(true);
|
// UI 状态会在 onstart 回调中更新,以保证真实感
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to start recognition", err);
|
console.error("Failed to start recognition", err);
|
||||||
// 如果 start 失败,立即重置状态,否则会锁死
|
|
||||||
isStartingRef.current = false;
|
isStartingRef.current = false;
|
||||||
setIsListening(false);
|
setIsListening(false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert("您的浏览器不支持语音识别,请使用 Chrome 或 Edge。");
|
if (!window.isSecureContext) {
|
||||||
|
alert("浏览器不支持语音识别:当前环境不是安全上下文 (HTTPS)。");
|
||||||
|
} else {
|
||||||
|
alert("您的浏览器不支持语音识别,请使用 Chrome 或 Edge。");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopListening = () => {
|
const stopListening = (e: React.PointerEvent) => {
|
||||||
// 仅在识别引擎正在运行时尝试停止
|
e.stopPropagation();
|
||||||
// 不要在这里重置 isListening 和 isStartingRef,等待 onend 回调
|
|
||||||
if (isListening) {
|
// 只有在正在运行或正在启动时才尝试停止
|
||||||
|
if (isListening || isStartingRef.current) {
|
||||||
try {
|
try {
|
||||||
recognitionRef.current?.stop();
|
recognitionRef.current?.stop();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to stop recognition", err);
|
console.error("Failed to stop recognition", err);
|
||||||
|
// 发生错误时强制重置本地状态
|
||||||
|
setIsListening(false);
|
||||||
|
isStartingRef.current = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -78,15 +172,18 @@ const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isF
|
|||||||
<button
|
<button
|
||||||
onPointerDown={startListening}
|
onPointerDown={startListening}
|
||||||
onPointerUp={stopListening}
|
onPointerUp={stopListening}
|
||||||
onPointerLeave={stopListening}
|
onPointerCancel={stopListening}
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
|
style={{ WebkitTouchCallout: 'none' }}
|
||||||
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
|
||||||
? 'w-full py-4 rounded-2xl font-bold text-sm tracking-widest'
|
? 'w-full py-4 rounded-2xl font-bold text-sm tracking-widest'
|
||||||
: 'w-14 h-14 rounded-full'
|
: 'w-14 h-14 rounded-full'
|
||||||
} ${
|
} ${
|
||||||
isListening
|
isListening
|
||||||
? 'bg-rose-500 text-white shadow-rose-200'
|
? 'bg-rose-500 text-white shadow-rose-200 scale-105'
|
||||||
: 'bg-indigo-600 text-white shadow-indigo-100'
|
: 'bg-indigo-600 text-white shadow-indigo-100'
|
||||||
} ${isProcessing ? 'opacity-50 grayscale cursor-not-allowed' : ''}`}
|
} ${isProcessing ? 'opacity-50 grayscale cursor-not-allowed' : ''}`}
|
||||||
>
|
>
|
||||||
@@ -113,4 +210,4 @@ const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isF
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VoiceInput;
|
export default VoiceInput;
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@google/genai": "^1.33.0",
|
||||||
|
"lunar-javascript": "1.6.12",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"@google/genai": "^1.33.0",
|
|
||||||
"uuid": "^13.0.0",
|
|
||||||
"recharts": "^2.12.0",
|
"recharts": "^2.12.0",
|
||||||
"lunar-javascript": "1.6.12"
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
|
|||||||
338
services/api.ts
338
services/api.ts
@@ -24,55 +24,6 @@ interface PaginatedResponse<T> {
|
|||||||
total: 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>(
|
async function request<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
@@ -156,13 +107,6 @@ export async function getUserProfile(): Promise<any> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 退出登录
|
|
||||||
*/
|
|
||||||
export function logout(): void {
|
|
||||||
localStorage.removeItem('auth_token');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 交易记录接口 ====================
|
// ==================== 交易记录接口 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,13 +152,6 @@ export async function getTransactions(params?: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单个交易记录
|
|
||||||
*/
|
|
||||||
export async function getTransaction(id: string): Promise<Transaction> {
|
|
||||||
return request<Transaction>(`/bill/transactions/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新交易记录
|
* 更新交易记录
|
||||||
*/
|
*/
|
||||||
@@ -244,27 +181,6 @@ export async function deleteTransaction(id: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量删除交易记录
|
|
||||||
*/
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 聊天消息接口 ====================
|
// ==================== 聊天消息接口 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -306,13 +222,6 @@ export async function getMessages(params?: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单个消息
|
|
||||||
*/
|
|
||||||
export async function getMessage(id: string): Promise<Message> {
|
|
||||||
return request<Message>(`/bill/messages/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除消息
|
* 删除消息
|
||||||
*/
|
*/
|
||||||
@@ -322,46 +231,6 @@ export async function deleteMessage(id: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空聊天历史
|
|
||||||
*/
|
|
||||||
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`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 账单提醒接口 ====================
|
// ==================== 账单提醒接口 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -403,13 +272,6 @@ export async function getReminders(params?: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单个提醒
|
|
||||||
*/
|
|
||||||
export async function getReminder(id: string): Promise<BillReminder> {
|
|
||||||
return request<BillReminder>(`/bill/reminders/${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新账单提醒
|
* 更新账单提醒
|
||||||
*/
|
*/
|
||||||
@@ -451,228 +313,28 @@ export async function deleteReminder(id: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取即将到期的提醒
|
|
||||||
*/
|
|
||||||
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 ====================
|
// ==================== 导出所有API ====================
|
||||||
export default {
|
export default {
|
||||||
// 用户认证
|
// 用户认证
|
||||||
loginOrRegister,
|
loginOrRegister,
|
||||||
getUserProfile,
|
getUserProfile,
|
||||||
logout,
|
|
||||||
|
|
||||||
// 交易记录
|
// 交易记录
|
||||||
createTransaction,
|
createTransaction,
|
||||||
getTransactions,
|
getTransactions,
|
||||||
getTransaction,
|
|
||||||
updateTransaction,
|
updateTransaction,
|
||||||
deleteTransaction,
|
deleteTransaction,
|
||||||
batchDeleteTransactions,
|
|
||||||
getTransactionStats,
|
|
||||||
|
|
||||||
// 聊天消息
|
// 聊天消息
|
||||||
createMessage,
|
createMessage,
|
||||||
getMessages,
|
getMessages,
|
||||||
getMessage,
|
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
clearMessages,
|
|
||||||
|
|
||||||
// 预算管理
|
|
||||||
getMonthlyBudget,
|
|
||||||
updateMonthlyBudget,
|
|
||||||
getBudgetExecution,
|
|
||||||
|
|
||||||
// 账单提醒
|
// 账单提醒
|
||||||
createReminder,
|
createReminder,
|
||||||
getReminders,
|
getReminders,
|
||||||
getReminder,
|
|
||||||
updateReminder,
|
updateReminder,
|
||||||
toggleReminder,
|
toggleReminder,
|
||||||
deleteReminder,
|
deleteReminder,
|
||||||
getUpcomingReminders,
|
|
||||||
|
|
||||||
// AI设置
|
|
||||||
getAISettings,
|
|
||||||
updateAISettings,
|
|
||||||
resetAISettings,
|
|
||||||
|
|
||||||
// 用户偏好
|
|
||||||
getUserPreferences,
|
|
||||||
updateUserPreferences,
|
|
||||||
|
|
||||||
// 数据同步
|
|
||||||
uploadSyncData,
|
|
||||||
downloadSyncData,
|
|
||||||
incrementalSync,
|
|
||||||
|
|
||||||
// AI处理
|
|
||||||
processAIMessage,
|
|
||||||
recognizeReceipt,
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user