import { useState, useEffect, useMemo } from 'react'; import { useRouter } from 'next/router'; import { isLoggedIn, searchRecords, updateRecord, deleteRecord } from '../lib/api'; 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'; import { useCategories } from '../hooks/useCategories'; import { formatMoney, parseDateTime, getDatePart } from '../lib/utils'; export default function Search() { const router = useRouter(); const { showToast } = useToast(); const confirm = useConfirm(); const { categories, error: categoriesError } = useCategories(); // 显示分类加载错误 useEffect(() => { if (categoriesError) { showToast('分类加载失败', { tone: 'error' }); } }, [categoriesError]); 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]); // 检查 URL 参数,自动搜索 useEffect(() => { if (router.isReady && router.query.q) { const q = decodeURIComponent(router.query.q); // Skip if already have results for this keyword (prevents double API call) if (keyword === q && results.length > 0) return; setKeyword(q); // 自动搜索 const doSearch = async () => { setLoading(true); try { const data = await searchRecords(q); setResults(data); } catch (err) { console.error(err); showToast(err.message, { tone: 'error' }); } finally { setLoading(false); } }; doSearch(); } }, [router.isReady, router.query.q, keyword, results]); const handleSearch = async (e) => { e.preventDefault(); if (!keyword.trim()) { showToast('请输入搜索关键词', { tone: 'info' }); return; } // 更新 URL router.push(`/search?q=${encodeURIComponent(keyword)}`, undefined, { shallow: true }); 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 } = parseDateTime(record.date); 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 groupedResults = useMemo(() => { if (!Array.isArray(results)) return {}; return results.reduce((groups, record) => { const date = getDatePart(record.date); 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(() => { if (!Array.isArray(results)) return { totalIncome: 0, totalExpense: 0, totalBalance: 0 }; 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 (includes custom categories) const categoryIconMap = useMemo(() => { const icons = {}; for (const type of ['expense', 'income']) { for (const cat of categories[type]) { icons[`${type}:${cat.name}`] = cat.icon; } } return icons; }, [categories]); 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 ? ( 未找到相关记录 ) : ( 输入关键词开始搜索 )} ); }
未找到相关记录
输入关键词开始搜索