import { useState, useEffect, useMemo } from 'react'; import { useRouter } from 'next/router'; import { isLoggedIn, getRecords, getStats, createRecord, updateRecord, deleteRecord } from '../lib/api'; import AppShell from '../components/AppShell'; import { Icon } from '../components/icons'; import RecordModal from '../components/RecordModal'; import SwipeableItem from '../components/SwipeableItem'; import EmptyState from '../components/EmptyState'; import { useToast } from '../components/ToastProvider'; import { useConfirm } from '../components/ConfirmProvider'; import { useCategories } from '../hooks/useCategories'; import { getMonthStr, formatMoney, formatMonth, parseDateTime } from '../lib/utils'; export default function Home() { const router = useRouter(); const { showToast } = useToast(); const { confirm } = useConfirm(); const [currentMonth, setCurrentMonth] = useState(new Date()); const [typeFilter, setTypeFilter] = useState('all'); const [records, setRecords] = useState([]); const [stats, setStats] = useState({ income: 0, expense: 0, balance: 0 }); const [modalOpen, setModalOpen] = useState(false); const [editingRecord, setEditingRecord] = useState(null); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); const [deleting, setDeleting] = useState(false); const [formError, setFormError] = useState(''); const { categories, error: categoriesError } = useCategories(); // 显示分类加载错误 useEffect(() => { if (categoriesError) { showToast('分类加载失败', { tone: 'error' }); } }, [categoriesError]); const [formData, setFormData] = useState({ type: 'expense', amount: '', category: '', date: new Date().toISOString().split('T')[0], time: new Date().toTimeString().slice(0, 5), note: '', }); useEffect(() => { if (!isLoggedIn()) { router.push('/login'); return; } loadData(); }, [router, currentMonth, typeFilter]); const resetForm = () => ({ type: 'expense', amount: '', category: '', date: new Date().toISOString().split('T')[0], time: new Date().toTimeString().slice(0, 5), note: '', }); // Memoized category lookup map for O(1) icon retrieval 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 loadData = async () => { setLoading(true); try { const [recordsData, statsData] = await Promise.all([ getRecords(getMonthStr(currentMonth), typeFilter), getStats(getMonthStr(currentMonth)), ]); setRecords(recordsData); setStats(statsData); } catch (err) { console.error(err); showToast(err.message, { tone: 'error' }); } finally { setLoading(false); } }; const prevMonth = () => { const newDate = new Date(currentMonth); newDate.setMonth(newDate.getMonth() - 1); setCurrentMonth(newDate); }; const nextMonth = () => { const newDate = new Date(currentMonth); newDate.setMonth(newDate.getMonth() + 1); setCurrentMonth(newDate); }; const handleCardClick = (type) => { setTypeFilter(type === typeFilter ? 'all' : type); }; const openAddModal = () => { setEditingRecord(null); setFormData(resetForm()); setFormError(''); setModalOpen(true); }; 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, 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, }; if (editingRecord) { const updated = { ...editingRecord, ...data, amount: parseFloat(data.amount) }; await updateRecord(editingRecord.id, data); setRecords(prev => prev.map(r => r.id === editingRecord.id ? updated : r)); showToast('记录已更新', { tone: 'success' }); } else { const newRecord = await createRecord(data); setRecords(prev => [...prev, newRecord]); 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); setRecords(prev => prev.filter(r => r.id !== editingRecord.id)); closeModal(); showToast('记录已删除', { tone: 'success' }); } catch (err) { setFormError(err.message); showToast(err.message, { tone: 'error' }); } finally { setDeleting(false); } }; const handleQuickDelete = async (record) => { const shouldDelete = await confirm({ title: '删除这条记录?', description: `${record.category} ${record.type === 'income' ? '+' : '-'}${formatMoney(record.amount)}`, confirmText: '删除', }); if (!shouldDelete) return; try { await deleteRecord(record.id); setRecords(prev => prev.filter(r => r.id !== record.id)); showToast('记录已删除', { tone: 'success' }); } catch (err) { showToast(err.message, { tone: 'error' }); } }; const groupedRecords = useMemo(() => { return records.reduce((groups, record) => { const date = getDatePart(record.date); if (!groups[date]) groups[date] = []; groups[date].push(record); return groups; }, {}); }, [records]); const sortedDates = useMemo( () => Object.keys(groupedRecords).sort((a, b) => b.localeCompare(a)), [groupedRecords] ); const getDayExpense = (date) => { const dayRecords = groupedRecords[date] || []; return dayRecords .filter((record) => record.type === 'expense') .reduce((sum, record) => sum + record.amount, 0); }; const homeHeaderContent = (
{formatMonth(currentMonth)}
); const modalHeaderContent = (
{editingRecord ? '编辑记录' : '新增记录'}
); return (

记录列表

{typeFilter === 'all' ? '全部记录' : typeFilter === 'income' ? '仅看收入' : '仅看支出'}

{loading ? (
加载中...
) : records.length === 0 ? ( ) : (
{sortedDates.map((date) => (
{new Date(date).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', weekday: 'short' })} 支出 {formatMoney(getDayExpense(date))}
{groupedRecords[date].map((record) => { const icon = categoryIconMap[`${record.type}:${record.category}`] || '📝'; return ( openEditModal(record)} renderRightActions={(handleAction) => ( <> )} > ); })}
))}
)}
); }