import { useState, useEffect, useMemo } from 'react'; import { useRouter } from 'next/router'; import { isLoggedIn, searchRecords, updateRecord, deleteRecord } from '../lib/api'; import { DEFAULT_CATEGORIES } from '../lib/categories'; import AppShell from '../components/AppShell'; import { Icon } from '../components/icons'; import RecordModal from '../components/RecordModal'; import { useToast } from '../components/ToastProvider'; import { useConfirm } from '../components/ConfirmProvider'; export default function Search() { const router = useRouter(); const { showToast } = useToast(); const confirm = useConfirm(); const [keyword, setKeyword] = useState(''); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [editingRecord, setEditingRecord] = useState(null); const [formData, setFormData] = useState({ type: 'expense', amount: '', category: '', date: '', time: '00:00', note: '', }); const [formError, setFormError] = useState(''); const [submitting, setSubmitting] = useState(false); const [deleting, setDeleting] = useState(false); useEffect(() => { if (!isLoggedIn()) { router.push('/login'); } }, [router]); const handleSearch = async (e) => { e.preventDefault(); if (!keyword.trim()) { showToast('请输入搜索关键词', { tone: 'info' }); return; } setLoading(true); try { const data = await searchRecords(keyword); setResults(data); } catch (err) { console.error(err); showToast(err.message, { tone: 'error' }); } finally { setLoading(false); } }; const openEditModal = (record) => { const [datePart, timePart] = record.date.split(' '); setEditingRecord(record); setFormData({ type: record.type, amount: record.amount.toString(), category: record.category, date: datePart, time: timePart || '00:00', note: record.note || '', }); setFormError(''); setModalOpen(true); }; const closeModal = () => { if (submitting || deleting) return; setModalOpen(false); setEditingRecord(null); setFormError(''); }; const handleSubmit = async (e) => { e.preventDefault(); if (!formData.amount || !formData.category || !formData.date) { setFormError('请填写完整信息'); return; } setSubmitting(true); setFormError(''); try { const data = { type: formData.type, amount: parseFloat(formData.amount), category: formData.category, date: `${formData.date} ${formData.time || '00:00'}`, note: formData.note, }; // 先调用 API 保存到数据库 await updateRecord(editingRecord.id, data); // 直接更新本地状态,避免重新获取 const updatedResults = results.map(r => r.id === editingRecord.id ? { ...r, ...data, amount: parseFloat(data.amount), date: data.date } : r ); setResults(updatedResults); showToast('记录已更新', { tone: 'success' }); closeModal(); } catch (err) { setFormError(err.message); showToast(err.message, { tone: 'error' }); } finally { setSubmitting(false); } }; const handleDelete = async () => { if (!editingRecord) return; const shouldDelete = await confirm({ title: '删除这条记录?', description: '删除后将无法恢复,请确认是否继续。', confirmText: '删除', }); if (!shouldDelete) return; setDeleting(true); try { await deleteRecord(editingRecord.id); // 直接从本地状态移除,避免重新获取 const filteredResults = results.filter(r => r.id !== editingRecord.id); setResults(filteredResults); showToast('记录已删除', { tone: 'success' }); closeModal(); } catch (err) { setFormError(err.message); showToast(err.message, { tone: 'error' }); } finally { setDeleting(false); } }; const formatMoney = (num) => (num || 0).toFixed(2); const groupedResults = useMemo(() => { return results.reduce((groups, record) => { const date = record.date.split(' ')[0]; if (!groups[date]) groups[date] = []; groups[date].push(record); return groups; }, {}); }, [results]); const sortedDates = useMemo( () => Object.keys(groupedResults).sort((a, b) => b.localeCompare(a)), [groupedResults] ); const { totalIncome, totalExpense, totalBalance } = useMemo(() => { let income = 0; let expense = 0; for (const record of results) { if (record.type === 'income') income += record.amount; else expense += record.amount; } return { totalIncome: income, totalExpense: expense, totalBalance: income - expense }; }, [results]); // O(1) category icon lookup map const categoryIconMap = useMemo(() => { const icons = {}; for (const type of ['expense', 'income']) { for (const cat of DEFAULT_CATEGORIES[type]) { icons[`${type}:${cat.name}`] = cat.icon; } } return icons; }, []); const actions = ( router.push('/settings')}> ); return ( setKeyword(e.target.value)} placeholder="搜索分类或备注..." autoFocus disabled={loading} /> {loading ? ( 搜索中... ) : '搜索'} {results.length > 0 ? ( <> 收入 +{formatMoney(totalIncome)} 支出 -{formatMoney(totalExpense)} 结余 {formatMoney(totalBalance)} 找到 {results.length} 条记录 > ) : null} {loading ? ( 搜索中... ) : results.length > 0 ? ( {sortedDates.map((date) => ( {new Date(date).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', weekday: 'short' })} {groupedResults[date].map((record) => { const icon = categoryIconMap[`${record.type}:${record.category}`] || '📝'; return ( openEditModal(record)}> {icon} {record.category} {record.note && {record.note}} {record.type === 'income' ? '+' : '-'} {formatMoney(record.amount)} ); })} ))} ) : keyword ? ( 未找到相关记录 ) : ( 输入关键词开始搜索 )} ); }
未找到相关记录
输入关键词开始搜索