import React, { useEffect, useMemo, useRef } from 'react'; import { Pressable, ScrollView, StyleSheet, Text, View, Alert } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { useLedgerStore } from '../../store/ledgerStore'; import { useMetadataStore } from '../../store/metadataStore'; import { useTheme } from '../../theme'; import { useT } from '../../i18n'; import { Card } from '../../components/Card'; import { SpeedDial } from '../../components/SpeedDial'; import { TransactionCard } from '../../components/TransactionCard'; import { AccountTree } from '../../components/AccountTree'; import { TrendLine } from '../../components/charts/TrendLine'; import { calculateNetWorth } from '../../domain/netWorth'; import { groupByMonth } from '../../domain/chartStats'; import { buildAccountTree } from '../../domain/accountTree'; import { getDueRecurring, instantiateRecurring, calculateNextDueDate } from '../../domain/recurring'; export default function HomeScreen() { const { theme } = useTheme(); const t = useT(); const router = useRouter(); const ledger = useLedgerStore(s => s.ledger); const addTransaction = useLedgerStore(s => s.addTransaction); const recurringTransactions = useMetadataStore(s => s.recurringTransactions); const updateRecurringTransaction = useMetadataStore(s => s.updateRecurringTransaction); const transactions = useMemo(() => ledger?.transactions ?? [], [ledger]); const netWorth = useMemo(() => calculateNetWorth(transactions), [transactions]); const monthlyData = useMemo(() => groupByMonth(transactions), [transactions]); const currentMonth = useMemo(() => { if (monthlyData.length === 0) return null; return monthlyData[monthlyData.length - 1]; }, [monthlyData]); // 1. 账户树计算 const accountNodes = useMemo(() => { if (!ledger || !ledger.accounts) return []; return buildAccountTree(ledger.accounts, transactions, ledger); }, [ledger, transactions]); // 2. 周期性记账到期计算 const todayStr = useMemo(() => new Date().toISOString().slice(0, 10), []); const dueRecurring = useMemo(() => { return getDueRecurring(recurringTransactions, todayStr); }, [recurringTransactions, todayStr]); // 周期记账自动触发:启动时自动确认标记了 autoConfirm 的到期项 const autoConfirmed = useRef(false); useEffect(() => { let isMounted = true; if (autoConfirmed.current) return; const autoDue = dueRecurring.filter(r => r.autoConfirm); if (autoDue.length === 0) return; autoConfirmed.current = true; (async () => { for (const rec of autoDue) { if (!isMounted) break; try { const draft = instantiateRecurring(rec); await addTransaction(draft); const nextDueDate = calculateNextDueDate(rec.frequency, rec.interval, rec.nextDueDate); updateRecurringTransaction(rec.id, { nextDueDate }); } catch { // 静默失败,用户可在周期管理页手动处理 } } })(); return () => { isMounted = false; }; }, [dueRecurring, addTransaction, updateRecurringTransaction]); const handleConfirmRecurring = async (rec: any) => { try { const draft = instantiateRecurring(rec); await addTransaction(draft); const nextDueDate = calculateNextDueDate(rec.frequency, rec.interval, rec.nextDueDate); updateRecurringTransaction(rec.id, { nextDueDate }); Alert.alert(t('home.recurringSuccess'), t('home.recurringSuccessDesc', { name: rec.name })); } catch (e) { Alert.alert(t('home.recurringFail'), String(e)); } }; // 3. 千分位格式化金额 const fmtAmount = (s: string) => { if (!s || s === '0') return '¥0.00'; const num = parseFloat(s); if (isNaN(num)) return s; const formatted = num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); return num >= 0 ? `¥${formatted}` : `-¥${Math.abs(num).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }; return ( {t('app.name')} {/* 净资产 Hero 卡片 */} {t('home.netWorthTitle')} {fmtAmount(netWorth.netWorth)} {t('home.assets')} {fmtAmount(netWorth.assets)} {t('home.liabilities')} {fmtAmount(netWorth.liabilities)} {/* Bento 双格排列 */} {(dueRecurring.length > 0 || currentMonth) && ( {dueRecurring.length > 0 ? ( {dueRecurring.slice(0, 2).map(rec => ( {rec.name} handleConfirmRecurring(rec)} style={({ pressed }) => [ styles.confirmBtnMini, { backgroundColor: theme.colors.bgTertiary, borderColor: theme.colors.border, borderRadius: theme.radii.sm, opacity: pressed ? 0.7 : 1, }, ]} > {rec.draft.postings[0]?.amount} CNY ))} ) : null} {currentMonth ? ( {t('home.income')} +{fmtAmount(currentMonth.income.toString())} {t('home.expense')} -{fmtAmount(currentMonth.expense.toString())} ) : null} )} {/* 月度趋势 */} {monthlyData.length > 0 && ( )} {/* 账户与资产余额树 */} {accountNodes.length > 0 && ( )} router.push('/transaction/new') }, { key: 'ocr', icon: 'camera-outline', label: t('home.ocrScan'), onPress: () => router.push({ pathname: '/transaction/new', params: { mode: 'ocr' } }) }, { key: 'import', icon: 'download-outline', label: t('tab.import'), onPress: () => router.push('/import') }, ]} /> ); } const styles = StyleSheet.create({ page: { flex: 1 }, header: { paddingHorizontal: 16, paddingTop: 8, paddingBottom: 12 }, content: { padding: 16, paddingBottom: 96 }, heroFooter: { flexDirection: 'row', justifyContent: 'space-between', borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: 'rgba(255,255,255,0.15)', paddingTop: 10, marginTop: 4 }, bentoRow: { flexDirection: 'row', gap: 12 }, bentoCol: { flex: 1 }, recurringList: { gap: 8 }, recurringItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingBottom: 6, borderBottomWidth: StyleSheet.hairlineWidth }, confirmBtnMini: { paddingVertical: 4, paddingHorizontal: 8, borderWidth: StyleSheet.hairlineWidth }, bentoStats: { gap: 8 }, });