/** * 预算管理页面(plan.md「1.1 budget/index」+ 决策 1 双轨制)。 * * 功能:预算列表(含进度条) + 添加/编辑/删除。 * 进度计算调用 calculateBudgetProgress 纯函数,展示已用/剩余/百分比。 */ import React, { useState } from 'react'; import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useTheme } from '../../theme'; import { useLedgerStore } from '../../store/ledgerStore'; import { useMetadataStore, generateId } from '../../store/metadataStore'; import { useT } from '../../i18n'; import { Card } from '../../components/Card'; import { FormModal, type FormField } from '../../components/FormModal'; import { calculateBudgetProgress } from '../../domain/budgets'; import type { Budget } from '../../domain/budgets'; type ModalMode = { type: 'add' } | { type: 'edit'; budget: Budget } | null; export default function BudgetScreen() { const { theme } = useTheme(); const t = useT(); const router = useRouter(); const budgets = useMetadataStore(s => s.budgets); const addBudget = useMetadataStore(s => s.addBudget); const updateBudget = useMetadataStore(s => s.updateBudget); const removeBudget = useMetadataStore(s => s.removeBudget); const ledger = useLedgerStore(s => s.ledger); const [modal, setModal] = useState(null); const transactions = ledger?.transactions ?? []; const today = new Date().toISOString().slice(0, 10); const handleDelete = (budget: Budget) => { Alert.alert(t('budget.deleteTitle'), t('budget.deleteConfirm', { name: budget.name }), [ { text: t('common.cancel'), style: 'cancel' }, { text: t('common.delete'), style: 'destructive', onPress: () => removeBudget(budget.id) }, ]); }; const handleConfirm = (values: Record) => { const name = values.name?.trim() || t('budget.unnamed'); const amount = values.amount?.trim() || '0'; const period = (values.period as Budget['period']) || 'monthly'; const categoryAccount = values.categoryAccount?.trim() || undefined; const startDate = values.startDate?.trim() || today; const amtNum = parseFloat(amount); if (isNaN(amtNum) || amtNum <= 0) { Alert.alert(t('budget.invalidAmountTitle'), t('budget.invalidAmountDesc')); return; } if (modal?.type === 'add') { addBudget({ id: generateId('bud'), name, amount, period, categoryAccount, startDate }); } else if (modal?.type === 'edit') { updateBudget(modal.budget.id, { name, amount, period, categoryAccount, startDate }); } setModal(null); }; const periodLabel = (p: string) => p === 'monthly' ? t('budget.periodMonthly') : p === 'weekly' ? t('budget.periodWeekly') : t('budget.periodYearly'); const editFields: FormField[] = modal?.type === 'edit' ? [ { key: 'name', label: t('budget.fieldName'), placeholder: t('budget.fieldNamePlaceholder'), defaultValue: modal.budget.name }, { key: 'amount', label: t('budget.fieldAmount'), placeholder: t('budget.fieldAmountPlaceholder'), defaultValue: modal.budget.amount, keyboardType: 'decimal-pad' }, { key: 'period', label: t('budget.fieldPeriod'), placeholder: 'monthly', defaultValue: modal.budget.period }, { key: 'categoryAccount', label: t('budget.fieldCategoryAccount'), placeholder: t('budget.fieldCategoryPlaceholder'), defaultValue: modal.budget.categoryAccount ?? '' }, { key: 'startDate', label: t('budget.fieldStartDate'), placeholder: '2026-01-01', defaultValue: modal.budget.startDate }, ] : []; return ( router.back()}> {t('budget.title')} setModal({ type: 'add' })} style={styles.addBtn}> {t('budget.add')} {budgets.map(budget => { const progress = calculateBudgetProgress(budget, transactions, today); return ( setModal({ type: 'edit', budget })} onLongPress={() => handleDelete(budget)} > {t('budget.yuanPer', { amount: budget.amount, period: periodLabel(budget.period) })} {t('budget.used', { spent: progress.spent, pct: progress.percentage.toFixed(0) })} {/* 进度条 */} {budget.categoryAccount && ( {budget.categoryAccount} )} ); })} {budgets.length === 0 && ( {t('budget.empty')} )} {t('common.clickEditLongDelete')} setModal(null)} /> ); } const styles = StyleSheet.create({ page: { flex: 1 }, header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingTop: 8, paddingBottom: 12 }, content: { padding: 16, gap: 12 }, addBtn: { flexDirection: 'row', alignItems: 'center' }, budgetRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, barWrap: { height: 8, borderRadius: 4, overflow: 'hidden', marginTop: 8 }, bar: { height: '100%', borderRadius: 4 }, });