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

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

View File

@@ -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">

View File

@@ -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>
);
};

View File

@@ -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