import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import { isLoggedIn, getRecurringBills, createRecurringBill, updateRecurringBill, deleteRecurringBill, applyRecurringBill } from '../lib/api'; import { DEFAULT_CATEGORIES } from '../lib/categories'; import AppShell from '../components/AppShell'; import { Icon } from '../components/icons'; import { useToast } from '../components/ToastProvider'; import { useConfirm } from '../components/ConfirmProvider'; export default function Recurring() { const router = useRouter(); const { showToast } = useToast(); const { confirm } = useConfirm(); const [bills, setBills] = useState([]); const [modalOpen, setModalOpen] = useState(false); const [editingBill, setEditingBill] = useState(null); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); const [deleting, setDeleting] = useState(false); const [applyingId, setApplyingId] = useState(null); const [formError, setFormError] = useState(''); const [formData, setFormData] = useState({ type: 'expense', amount: '', category: '', day: '1', note: '', is_active: true, }); useEffect(() => { if (!isLoggedIn()) { router.push('/login'); return; } loadBills(); }, [router]); const loadBills = async () => { setLoading(true); try { const data = await getRecurringBills(); setBills(data); } catch (err) { console.error(err); showToast(err.message, { tone: 'error' }); } finally { setLoading(false); } }; const openAddModal = () => { setEditingBill(null); setFormData({ type: 'expense', amount: '', category: '', day: '1', note: '', is_active: true, }); setFormError(''); setModalOpen(true); }; const openEditModal = (bill) => { setEditingBill(bill); setFormData({ type: bill.type, amount: bill.amount.toString(), category: bill.category, day: bill.day.toString(), note: bill.note || '', is_active: !!bill.is_active, }); setFormError(''); setModalOpen(true); }; const closeModal = () => { if (submitting || deleting) return; setModalOpen(false); setEditingBill(null); setFormError(''); }; const handleSubmit = async (e) => { e.preventDefault(); if (!formData.amount || !formData.category || !formData.day) { setFormError('请填写完整信息'); return; } setSubmitting(true); setFormError(''); try { const data = { type: formData.type, amount: parseFloat(formData.amount), category: formData.category, day: parseInt(formData.day), note: formData.note, is_active: formData.is_active ? 1 : 0, }; if (editingBill) { await updateRecurringBill(editingBill.id, data); showToast('周期账单已更新', { tone: 'success' }); } else { await createRecurringBill(data); showToast('周期账单已创建', { tone: 'success' }); } closeModal(); await loadBills(); } catch (err) { setFormError(err.message); showToast(err.message, { tone: 'error' }); } finally { setSubmitting(false); } }; const handleDelete = async () => { if (!editingBill) return; const shouldDelete = await confirm({ title: '删除这条周期账单?', description: '删除后将无法恢复,请确认是否继续。', confirmText: '删除', }); if (!shouldDelete) return; setDeleting(true); try { await deleteRecurringBill(editingBill.id); closeModal(); showToast('周期账单已删除', { tone: 'success' }); await loadBills(); } catch (err) { setFormError(err.message); showToast(err.message, { tone: 'error' }); } finally { setDeleting(false); } }; const handleApply = async (id) => { setApplyingId(id); try { await applyRecurringBill(id); showToast('已添加到账目', { tone: 'success' }); } catch (err) { showToast(err.message, { tone: 'error' }); } finally { setApplyingId(null); } }; const handleToggleActive = async (bill, event) => { event.stopPropagation(); try { await updateRecurringBill(bill.id, { ...bill, is_active: bill.is_active ? 0 : 1, }); showToast(bill.is_active ? '已停用周期账单' : '已启用周期账单', { tone: 'success' }); await loadBills(); } catch (err) { showToast(err.message, { tone: 'error' }); } }; const days = Array.from({ length: 28 }, (_, index) => index + 1); const actions = ( router.push('/settings')}> ); return ( {loading ? ( 加载中... ) : bills.length === 0 ? ( 暂无周期性账单 点击右下角新增定期收入或支出。 ) : ( 账单列表 {bills.length} 条周期账单 {bills.map((bill) => { const categories = DEFAULT_CATEGORIES[bill.type]; const cat = categories.find((item) => item.name === bill.category) || { icon: '📝' }; return ( openEditModal(bill)} > {cat.icon} {bill.category} {bill.is_active ? '启用中' : '已停用'} 每月 {bill.day} 日 {bill.note && {bill.note}} {bill.type === 'income' ? '+' : '-'}{bill.amount.toFixed(2)} { e.stopPropagation(); handleApply(bill.id); }} disabled={applyingId === bill.id} > {applyingId === bill.id ? '应用中...' : '应用'} handleToggleActive(bill, event)} > {bill.is_active ? '停用' : '启用'} ); })} )} {modalOpen && ( e.stopPropagation()}> {editingBill ? '编辑账单' : '新增账单'} {editingBill ? '更新周期账单' : '创建周期账单'} {formError && {formError}} setFormData({ ...formData, type: 'expense', category: '' })} disabled={submitting || deleting} > 支出 setFormData({ ...formData, type: 'income', category: '' })} disabled={submitting || deleting} > 收入 金额 setFormData({ ...formData, amount: e.target.value })} placeholder="0.00" step="0.01" required disabled={submitting || deleting} /> 分类 {DEFAULT_CATEGORIES[formData.type].map((cat) => ( setFormData({ ...formData, category: cat.name })} disabled={submitting || deleting} > {cat.icon} {cat.name} ))} 日期(每月几号) setFormData({ ...formData, day: e.target.value })} disabled={submitting || deleting}> {days.map((day) => ( {day} 日 ))} 备注(可选) setFormData({ ...formData, note: e.target.value })} placeholder="添加备注..." disabled={submitting || deleting} /> {editingBill && ( setFormData({ ...formData, is_active: e.target.checked })} disabled={submitting || deleting} /> 启用此账单 )} {submitting ? ( 保存中... ) : '保存'} {editingBill && ( {deleting ? '删除中...' : '删除'} )} )} ); }
暂无周期性账单
点击右下角新增定期收入或支出。
账单列表
{editingBill ? '编辑账单' : '新增账单'}