@@ -1,47 +1,24 @@
import { useEffect , useState } from 'react' ;
import { useNavigate } from 'react-router-dom' ;
import { motion , useScroll , useTransform } from 'framer-motion' ;
import './Home.css' ;
// ... rest of imports
// ... inside component
function Home() {
const navigate = useNavigate ( ) ;
// ... state declarations
// Scroll Parallax Effect for Header
const { scrollY } = useScroll ( ) ;
const headerOpacity = useTransform ( scrollY , [ 0 , 100 ] , [ 1 , 0.4 ] ) ;
const headerY = useTransform ( scrollY , [ 0 , 100 ] , [ 0 , 20 ] ) ;
const headerScale = useTransform ( scrollY , [ 0 , 100 ] , [ 1 , 0.95 ] ) ;
// ... rest of logic
return (
< div className = "home-page" >
< motion.header
className = "home-header"
style = { { opacity : headerOpacity , y : headerY , scale : headerScale } }
>
< div className = "home-greeting animate-slide-up" >
{ /* ... */ }
import { getAccounts , calculateTotalAssets , calculateTotalLiabilities } from '../../services/accountService' ;
import { getTransactions , calculateTotalExpense } from '../../services/transactionService' ;
import { getCategories } from '../../services/categoryService' ;
import { getLedgers , reorderLedgers } from '../../services/ledgerService' ;
import { getSettings , updateSettings } from '../../services/settingsService' ;
import { Icon } from '@iconify/react' ;
import { SpendingTrendChart } from '../../components/charts/SpendingTrendChart' ;
import { Skeleton } from '../../components/common/Skeleton/Skeleton' ;
import { LedgerSelector } from '../../components/ledger/LedgerSelector/LedgerSelector' ;
import VoiceInputModal from '../../components/ai/VoiceInputModal/VoiceInputModal' ;
import { CreateFirstAccountModal } from '../../components/account/CreateFirstAccountModal/CreateFirstAccountModal' ;
import { AccountForm } from '../../components/account/AccountForm/AccountForm' ;
import { createAccount } from '../../services/accountService' ;
import { Confetti } from '../../components/common/Confetti' ;
import { LikeButton } from '../../components/common/MicroInteraction/LikeButton' ;
import { HealthScoreModal } from '../../components/home/HealthScoreModal/HealthScoreModal' ;
import type { Account , Transaction , Category , Ledger , UserSettings } from '../../types' ;
import { getAccounts , calculateTotalAssets , calculateTotalLiabilities } from '../../services/accountService' ;
import { getTransactions , calculateTotalExpense } from '../../services/transactionService' ;
import { getCategories } from '../../services/categoryService' ;
import { getLedgers , reorderLedgers } from '../../services/ledgerService' ;
import { getSettings , updateSettings } from '../../services/settingsService' ;
import { Icon } from '@iconify/react' ;
import { SpendingTrendChart } from '../../components/charts/SpendingTrendChart' ;
import { Skeleton } from '../../components/common/Skeleton/Skeleton' ;
import { LedgerSelector } from '../../components/ledger/LedgerSelector/LedgerSelector' ;
import VoiceInputModal from '../../components/ai/VoiceInputModal/VoiceInputModal' ;
import { CreateFirstAccountModal } from '../../components/account/CreateFirstAccountModal/CreateFirstAccountModal' ;
import { AccountForm } from '../../components/account/AccountForm/AccountForm' ;
import { createAccount } from '../../services/accountService' ;
import { Confetti } from '../../components/common/Confetti' ;
import { LikeButton } from '../../components/common/MicroInteraction/LikeButton' ;
import { HealthScoreModal } from '../../components/home/HealthScoreModal/HealthScoreModal' ;
import type { Account , Transaction , Category , Ledger , UserSettings } from '../../types' ;
/**
@@ -52,498 +29,497 @@ function Home() {
* Requirements:
* - 8.1: Quick transaction entry (3 steps or less)
* - 8.2: Fast loading (< 2 seconds)
*/
function Home() {
*/
function Home() {
const navigate = useNavigate ( ) ;
const [ accounts , setAccounts ] = useState < Account [ ] > ( [ ] ) ;
const [ recentTransactions , setRecentTransactions ] = useState < Transaction [ ] > ( [ ] ) ;
const [ categories , setCategories ] = useState < Category [ ] > ( [ ] ) ;
const [ ledgers , setLedgers ] = useState < Ledger [ ] > ( [ ] ) ;
const [ settings , setSettings ] = useState < UserSettings | null > ( null ) ;
const [ ledgerSelectorOpen , setLedgerSelectorOpen ] = useState ( false ) ;
const [ voiceModalOpen , setVoiceModalOpen ] = useState ( false ) ;
const [ showAccountForm , setShowAccountForm ] = useState ( false ) ;
const [ showConfetti , setShowConfetti ] = useState ( false ) ;
const [ showHealthModal , setShowHealthModal ] = useState ( false ) ;
const [ todaySpend , setTodaySpend ] = useState ( 0 ) ;
const [ yesterdaySpend , setYesterdaySpend ] = useState ( 0 ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ error , setError ] = useState < string | null > ( null ) ;
const [ accounts , setAccounts ] = useState < Account [ ] > ( [ ] ) ;
const [ recentTransactions , setRecentTransactions ] = useState < Transaction [ ] > ( [ ] ) ;
const [ categories , setCategories ] = useState < Category [ ] > ( [ ] ) ;
const [ ledgers , setLedgers ] = useState < Ledger [ ] > ( [ ] ) ;
const [ settings , setSettings ] = useState < UserSettings | null > ( null ) ;
const [ ledgerSelectorOpen , setLedgerSelectorOpen ] = useState ( false ) ;
const [ voiceModalOpen , setVoiceModalOpen ] = useState ( false ) ;
const [ showAccountForm , setShowAccountForm ] = useState ( false ) ;
const [ showConfetti , setShowConfetti ] = useState ( false ) ;
const [ showHealthModal , setShowHealthModal ] = useState ( false ) ;
const [ todaySpend , setTodaySpend ] = useState ( 0 ) ;
const [ yesterdaySpend , setYesterdaySpend ] = useState ( 0 ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ error , setError ] = useState < string | null > ( null ) ;
useEffect ( ( ) = > {
loadData ( ) ;
loadData ( ) ;
} , [ ] ) ;
const loadData = async ( ) = > {
try {
setLoading ( true ) ;
setError ( null ) ;
setLoading ( true ) ;
setError ( null ) ;
// Calculate dates for today and yesterday
const today = new Date ( ) ;
const yesterday = new Date ( today ) ;
yesterday . setDate ( yesterday . getDate ( ) - 1 ) ;
// Calculate dates for today and yesterday
const today = new Date ( ) ;
const yesterday = new Date ( today ) ;
yesterday . setDate ( yesterday . getDate ( ) - 1 ) ;
const todayStr = today . toISOString ( ) . split ( 'T' ) [ 0 ] ;
const yesterdayStr = yesterday . toISOString ( ) . split ( 'T' ) [ 0 ] ;
const todayStr = today . toISOString ( ) . split ( 'T' ) [ 0 ] ;
const yesterdayStr = yesterday . toISOString ( ) . split ( 'T' ) [ 0 ] ;
// Load accounts, recent transactions, today/yesterday stats
const [ accountsData , transactionsData , categoriesData , ledgersData , settingsData , todayData , yesterdayData ] = await Promise . all ( [
getAccounts ( ) ,
getTransactions ( { page : 1 , pageSize : 5 } ) , // Recent transactions
getCategories ( ) ,
// Load accounts, recent transactions, today/yesterday stats
const [ accountsData , transactionsData , categoriesData , ledgersData , settingsData , todayData , yesterdayData ] = await Promise . all ( [
getAccounts ( ) ,
getTransactions ( { page : 1 , pageSize : 5 } ) , // Recent transactions
getCategories ( ) ,
getLedgers ( ) . catch ( ( ) = > [ ] ) ,
getSettings ( ) . catch ( ( ) = > null ) ,
getTransactions ( { startDate : todayStr , endDate : todayStr , type : 'expense' , pageSize : 100 } ) ,
getTransactions ( { startDate : yesterdayStr , endDate : yesterdayStr , type : 'expense' , pageSize : 100 } ) ,
] ) ;
getTransactions ( { startDate : todayStr , endDate : todayStr , type : 'expense' , pageSize : 100 } ) ,
getTransactions ( { startDate : yesterdayStr , endDate : yesterdayStr , type : 'expense' , pageSize : 100 } ) ,
] ) ;
setAccounts ( accountsData || [ ] ) ;
setRecentTransactions ( transactionsData ? . items || [ ] ) ;
setCategories ( categoriesData || [ ] ) ;
setLedgers ( ledgersData || [ ] ) ;
setSettings ( settingsData ) ;
setAccounts ( accountsData || [ ] ) ;
setRecentTransactions ( transactionsData ? . items || [ ] ) ;
setCategories ( categoriesData || [ ] ) ;
setLedgers ( ledgersData || [ ] ) ;
setSettings ( settingsData ) ;
// Calculate daily spends
setTodaySpend ( calculateTotalExpense ( todayData . items ) ) ;
setYesterdaySpend ( calculateTotalExpense ( yesterdayData . items ) ) ;
// Calculate daily spends
setTodaySpend ( calculateTotalExpense ( todayData . items ) ) ;
setYesterdaySpend ( calculateTotalExpense ( yesterdayData . items ) ) ;
} catch ( err ) {
setError ( err instanceof Error ? err . message : '加载数据失败' ) ;
console . error ( 'Failed to load home page data:' , err ) ;
setError ( err instanceof Error ? err . message : '加载数据失败' ) ;
console . error ( 'Failed to load home page data:' , err ) ;
} finally {
setLoading ( false ) ;
setLoading ( false ) ;
}
} ;
const handleQuickTransaction = ( ) = > {
navigate ( '/transactions?action=new' ) ;
navigate ( '/transactions?action=new' ) ;
} ;
const handleAIBookkeeping = ( ) = > {
setVoiceModalOpen ( true ) ;
setVoiceModalOpen ( true ) ;
} ;
const handleAIConfirm = ( ) = > {
// 确认后刷新数据
loadData ( ) ;
setVoiceModalOpen ( false ) ;
// 确认后刷新数据
loadData ( ) ;
setVoiceModalOpen ( false ) ;
} ;
const handleViewAllAccounts = ( ) = > {
navigate ( '/accounts' ) ;
navigate ( '/accounts' ) ;
} ;
const handleViewAllTransactions = ( ) = > {
navigate ( '/transactions' ) ;
navigate ( '/transactions' ) ;
} ;
const handleLedgerSelect = async ( ledgerId : number ) = > {
try {
// Update settings with new current ledger
if ( settings ) {
await updateSettings ( { . . . settings , currentLedgerId : ledgerId } ) ;
setSettings ( { . . . settings , currentLedgerId : ledgerId } ) ;
await updateSettings ( { . . . settings , currentLedgerId : ledgerId } ) ;
setSettings ( { . . . settings , currentLedgerId : ledgerId } ) ;
}
setLedgerSelectorOpen ( false ) ;
// Reload data to show transactions from selected ledger
await loadData ( ) ;
setLedgerSelectorOpen ( false ) ;
// Reload data to show transactions from selected ledger
await loadData ( ) ;
} catch ( err ) {
console . error ( 'Failed to switch ledger:' , err ) ;
setError ( '切换账本失败' ) ;
console . error ( 'Failed to switch ledger:' , err ) ;
setError ( '切换账本失败' ) ;
}
} ;
const handleLedgerReorder = async ( reorderedLedgers : Ledger [ ] ) = > {
try {
setLedgers ( reorderedLedgers ) ;
setLedgers ( reorderedLedgers ) ;
const ledgerIds = reorderedLedgers . map ( l = > l . id ) ;
await reorderLedgers ( ledgerIds ) ;
await reorderLedgers ( ledgerIds ) ;
} catch ( err ) {
console . error ( 'Failed to reorder ledgers:' , err ) ;
// Revert on error
await loadData ( ) ;
console . error ( 'Failed to reorder ledgers:' , err ) ;
// Revert on error
await loadData ( ) ;
}
} ;
const handleAddLedger = ( ) = > {
setLedgerSelectorOpen ( false ) ;
navigate ( '/ledgers/new' ) ;
setLedgerSelectorOpen ( false ) ;
navigate ( '/ledgers/new' ) ;
} ;
const handleManageLedgers = ( ) = > {
setLedgerSelectorOpen ( false ) ;
navigate ( '/ledgers' ) ;
setLedgerSelectorOpen ( false ) ;
navigate ( '/ledgers' ) ;
} ;
const currentLedger = ledgers . find ( l = > l . id === settings ? . currentLedgerId ) || ledgers . find ( l = > l . isDefault ) || ledgers [ 0 ] ;
const totalAssets = calculateTotalAssets ( accounts ) ;
const totalLiabilities = calculateTotalLiabilities ( accounts ) ;
const netWorth = totalAssets - totalLiabilities ;
const totalAssets = calculateTotalAssets ( accounts ) ;
const totalLiabilities = calculateTotalLiabilities ( accounts ) ;
const netWorth = totalAssets - totalLiabilities ;
const formatCurrency = ( amount : number ) : string = > {
return new Intl . NumberFormat ( 'zh-CN' , {
style : 'currency' ,
currency : 'CNY' ,
style : 'currency' ,
currency : 'CNY' ,
} ) . format ( amount ) ;
} ;
// Lock body scroll when modal is open
useEffect ( ( ) = > {
if ( showAccountForm ) {
document . body . style . overflow = 'hidden' ;
document . body . style . overflow = 'hidden' ;
} else {
document . body . style . overflow = '' ;
document . body . style . overflow = '' ;
}
return ( ) = > {
document . body . style . overflow = '' ;
document . body . style . overflow = '' ;
} ;
} , [ showAccountForm ] ) ;
const formatDate = ( dateString : string ) : string = > {
const date = new Date ( dateString ) ;
const today = new Date ( ) ;
const yesterday = new Date ( today ) ;
yesterday . setDate ( yesterday . getDate ( ) - 1 ) ;
const today = new Date ( ) ;
const yesterday = new Date ( today ) ;
yesterday . setDate ( yesterday . getDate ( ) - 1 ) ;
if ( date . toDateString ( ) === today . toDateString ( ) ) {
if ( date . toDateString ( ) === today . toDateString ( ) ) {
return '今天' ;
} else if ( date . toDateString ( ) === yesterday . toDateString ( ) ) {
return '昨天' ;
} else {
return date . toLocaleDateString ( 'zh-CN' , { month : '2-digit' , day : '2-digit' } ) ;
return date . toLocaleDateString ( 'zh-CN' , { month : '2-digit' , day : '2-digit' } ) ;
}
} ;
// Phase 3: Daily Briefing Logic
const getDailyBriefing = ( ) = > {
const hour = new Date ( ) . getHours ( ) ;
let greeting = '你好' ;
if ( hour < 5 ) greeting = '夜深了' ;
else if ( hour < 11 ) greeting = '早上好' ;
else if ( hour < 13 ) greeting = '中午好' ;
else if ( hour < 18 ) greeting = '下午好' ;
else greeting = '晚上好' ;
let greeting = '你好' ;
if ( hour < 5 ) greeting = '夜深了' ;
else if ( hour < 11 ) greeting = '早上好' ;
else if ( hour < 13 ) greeting = '中午好' ;
else if ( hour < 18 ) greeting = '下午好' ;
else greeting = '晚上好' ;
let insight = '今天还没有记账哦' ;
let insight = '今天还没有记账哦' ;
if ( todaySpend > 0 ) {
if ( yesterdaySpend > 0 ) {
const diff = todaySpend - yesterdaySpend ;
const diffPercent = Math . abs ( ( diff / yesterdaySpend ) * 100 ) ;
const diffPercent = Math . abs ( ( diff / yesterdaySpend ) * 100 ) ;
if ( Math . abs ( diff ) < 5 ) {
insight = ` 今日支出 ${ formatCurrency ( todaySpend ) } ,与昨天持平 ` ;
if ( Math . abs ( diff ) < 5 ) {
insight = ` 今日支出 ${ formatCurrency ( todaySpend ) } ,与昨天持平 ` ;
} else if ( diff < 0 ) {
insight = ` 今日支出 ${ formatCurrency ( todaySpend ) } ,比昨天节省了 ${ diffPercent . toFixed ( 0 ) } % ` ;
insight = ` 今日支出 ${ formatCurrency ( todaySpend ) } ,比昨天节省了 ${ diffPercent . toFixed ( 0 ) } % ` ;
} else {
insight = ` 今日支出 ${ formatCurrency ( todaySpend ) } ,比昨天多 ${ diffPercent . toFixed ( 0 ) } % ` ;
insight = ` 今日支出 ${ formatCurrency ( todaySpend ) } ,比昨天多 ${ diffPercent . toFixed ( 0 ) } % ` ;
}
} else {
insight = ` 今天已支出 ${ formatCurrency ( todaySpend ) } ` ;
insight = ` 今天已支出 ${ formatCurrency ( todaySpend ) } ` ;
}
} else {
if ( yesterdaySpend > 0 ) {
insight = '新的一天,保持理智消费' ;
insight = '新的一天,保持理智消费' ;
}
}
return { greeting , insight } ;
return { greeting , insight } ;
} ;
const { greeting , insight } = getDailyBriefing ( ) ;
const { greeting , insight } = getDailyBriefing ( ) ;
// Phase 3: Financial Health Score (Mock Logic)
// In a real app, this would be complex. Here we use a simple placeholder derived from net worth/assets ratio
const calculateHealthScore = ( ) = > {
if ( totalAssets === 0 ) return 60 ; // Baseline
const ratio = ( totalAssets - totalLiabilities ) / totalAssets ;
let score = Math . round ( ratio * 100 ) ;
if ( score < 40 ) score = 40 ;
const ratio = ( totalAssets - totalLiabilities ) / totalAssets ;
let score = Math . round ( ratio * 100 ) ;
if ( score < 40 ) score = 40 ;
if ( score > 98 ) score = 98 ;
return score ;
return score ;
} ;
const healthScore = calculateHealthScore ( ) ;
const healthScore = calculateHealthScore ( ) ;
if ( loading ) {
if ( loading ) {
return (
< div className = "home-page" >
< header className = "home-header" >
< Skeleton width = { 120 } height = { 32 } / >
< / header >
< main className = "home-content" >
< section className = "quick-actions" >
< Skeleton width = "100%" height = { 60 } variant = "rectangular" / >
< / section >
< section className = "account-overview" >
< div className = "section-header" >
< Skeleton width = { 100 } height = { 24 } / >
< Skeleton width = { 60 } height = { 24 } / >
< / div >
< div className = "balance-summary" >
< Skeleton width = "100%" height = { 120 } variant = "card" style = { { marginBottom : '1rem' } } / >
< div style = { { display : 'flex' , gap : '1rem' } } >
< Skeleton width = "50%" height = { 80 } variant = "card" / >
< Skeleton width = "50%" height = { 80 } variant = "card" / >
< / div >
< / div >
< / section >
< / main >
< / div >
) ;
< div className = "home-page" >
< header className = "home-header" >
< Skeleton width = { 120 } height = { 32 } / >
< / header >
< main className = "home-content" >
< section className = "quick-actions" >
< Skeleton width = "100%" height = { 60 } variant = "rectangular" / >
< / section >
< section className = "account-overview" >
< div className = "section-header" >
< Skeleton width = { 100 } height = { 24 } / >
< Skeleton width = { 60 } height = { 24 } / >
< / div >
< div className = "balance-summary" >
< Skeleton width = "100%" height = { 120 } variant = "card" style = { { marginBottom : '1rem' } } / >
< div style = { { display : 'flex' , gap : '1rem' } } >
< Skeleton width = "50%" height = { 80 } variant = "card" / >
< Skeleton width = "50%" height = { 80 } variant = "card" / >
< / div >
< / div >
< / section >
< / main >
< / div >
) ;
}
if ( error ) {
if ( error ) {
return (
< div className = "home-page" >
< div className = "error-state" >
< Icon icon = "solar:danger-circle-bold" width = "48" color = "#ef4444" / >
< p > { error } < / p >
< button className = "retry-btn" onClick = { loadData } > 重 试 < / button >
< div className = "home-page" >
< div className = "error-state" >
< Icon icon = "solar:danger-circle-bold" width = "48" color = "#ef4444" / >
< p > { error } < / p >
< button className = "retry-btn" onClick = { loadData } > 重 试 < / button >
< / div >
< / div >
) ;
}
return (
< div className = "home-page" >
< header className = "home-header" >
< div className = "home-greeting animate-slide-up" >
< div className = "greeting-top-row" >
< div className = "greeting-pill" onClick = { ( ) = > setLedgerSelectorOpen ( true ) } >
{ currentLedger && (
< >
< Icon icon = "solar:notebook-minimalistic-bold-duotone" width = "14" / >
< span > { currentLedger . name } < / span >
< Icon icon = "solar:alt-arrow-down-bold" width = "10" className = "chevron-icon" / >
< / >
) }
< / div >
< span className = "home-date" > { new Date ( ) . toLocaleDateString ( 'zh-CN' , { weekday : 'short' , month : 'long' , day : 'numeric' } ) } < / span >
< / div >
< h1 className = "greeting-text" >
{ greeting } , < span className = "greeting-highlight" > 保 持 节 奏 < / span >
< / h1 >
< p className = "greeting-insight animate-slide-up delay-100" >
< Icon icon = "solar:bell-bing-bold-duotone" width = "16" className = "insight-icon" / >
{ insight }
< div className = "insight-actions" style = { { marginLeft : 'auto' , display : 'flex' , alignItems : 'center' } } >
< LikeButton size = "sm" initialCount = { Math . floor ( Math . random ( ) * 20 ) } / >
< / div >
< / p >
< / div >
< div className = "header-actions animate-slide-up delay-200" >
< button className = "health-score-btn" onClick = { ( ) = > setShowHealthModal ( true ) } >
< div className = "health-ring" style = { { '--score' : ` ${ healthScore } % ` , '--color' : 'var(--accent-success)' } as any } >
< svg viewBox = "0 0 36 36" >
< path className = "ring-bg" d = "M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" / >
< path className = "ring-fill" strokeDasharray = { ` ${ healthScore } , 100 ` } d = "M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" / >
< / svg >
< span className = "health-val" > { healthScore } < / span >
< / div >
< span className = "health-label" > 健 康 分 < / span >
< / button >
< button className = "quick-action-btn-small" onClick = { handleQuickTransaction } >
< Icon icon = "solar:add-circle-bold-duotone" width = "20" / >
< span > 记 一 笔 < / span >
< / button >
< / div >
< / header >
< main className = "home-content" >
{ /* Asset Dashboard - Requirement 8.1 */ }
< section className = "dashboard-grid" >
{ /* Net Worth Card - Main Hero */ }
< div className = "dashboard-card home-net-worth-card" >
< div className = "card-content" >
< span className = "card-label" > 净 资 产 < / span >
< div className = "card-value-group" >
< span className = "currency-symbol" > ¥ < / span >
< span className = "card-value-main privacy-mask" > { formatCurrency ( netWorth ) . replace ( /[^0-9.,-]/g , '' ) } < / span >
< / div >
< div className = "card-footer" >
< span className = "trend-neutral" > 总 览 所 有 账 户 < / span >
< / div >
< / div >
< div className = "card-bg-decoration" > < / div >
< / div >
{ /* Assets Card */ }
< div className = "dashboard-card assets-card" onClick = { handleViewAllAccounts } >
< div className = "card-icon-wrapper income" >
< Icon icon = "solar:graph-up-bold-duotone" width = "20" / >
< / div >
< div className = "card-content" >
< span className = "card-label" > 总 资 产 < / span >
< span className = "card-value-sub privacy-mask" > { formatCurrency ( totalAssets ) } < / span >
< / div >
< / div >
) ;
}
return (
< div className = "home-page" >
< header className = "home-header " >
< div className = "home-greeting animate-slide-up" >
< div className = "greeting-top-row" >
< div className = "greeting-pill" onClick = { ( ) = > setLedgerSelectorOpen ( true ) } >
{ currentLedger && (
< >
< Icon icon = "solar:notebook-minimalistic-bold-duotone" width = "14" / >
< span > { currentLedger . name } < / span >
< Icon icon = "solar:alt-arrow-down-bold" width = "10" className = "chevron-icon" / >
< / >
) }
< / div >
< span className = "home-date" > { new Date ( ) . toLocaleDateString ( 'zh-CN' , { weekday : 'short' , month : 'long' , day : 'numeric' } ) } < / span >
< / div >
< h1 className = "greeting-text" >
{ greeting } , < span className = "greeting-highlight" > 保 持 节 奏 < / span >
< / h1 >
< p className = "greeting-insight animate-slide-up delay-100" >
< Icon icon = "solar:bell-bing-bold-duotone" width = "16" className = "insight-icon" / >
{ insight }
< div className = "insight-actions" style = { { marginLeft : 'auto' , display : 'flex' , alignItems : 'center' } } >
< LikeButton size = "small" initialCount = { Math . floor ( Math . random ( ) * 20 ) } / >
< / div >
< / p >
< / div >
< div className = "header-actions animate-slide-up delay-200" >
< button className = "health-score-btn" onClick = { ( ) = > setShowHealthModal ( true ) } >
< div className = "health-ring" style = { { '--score' : ` ${ healthScore } % ` , '--color' : 'var(--accent-success)' } as any } >
< svg viewBox = "0 0 36 36" >
< path className = "ring-bg" d = "M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" / >
< path className = "ring-fill" strokeDasharray = { ` ${ healthScore } , 100 ` } d = "M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" / >
< / svg >
< span className = "health-val" > { healthScore } < / span >
< / div >
< span className = "health-label" > 健 康 分 < / span >
< / button >
< button className = "quick-action-btn-small" onClick = { handleQuickTransaction } >
< Icon icon = "solar:add-circle-bold-duotone" width = "20" / >
< span > 记 一 笔 < / span >
< / button >
< / div >
< / header >
{ /* Liabilities Card */ }
< div className = "dashboard-card liabilities-card" onClick = { handleViewAllAccounts } >
< div className = "card-icon-wrapper expense " >
< Icon icon = "solar:graph-down-bold-duotone" width = "20" / >
< / div >
< div className = "card-content" >
< span className = "card-label" > 总 负 债 < / span >
< span className = "card-value-sub privacy-mask" > { formatCurrency ( totalLiabilities ) } < / span >
< /div >
< / div >
< / section >
{ /* Quick Actions Section */ }
< section className = "quick-actions-section" >
< button className = "action-card primary" onClick = { handleQuickTransaction } >
< div className = "action-icon blur-bg" >
< Icon icon = "solar:add-circle-bold-duotone" width = "24" / >
< / div >
< div className = "action-info" >
< span className = "action-title" > 快 速 记 账 < / span >
< span className = "action-desc" > 记 录 新 的 收 入 或 支 出 < / span >
< / div >
< / button >
< mai n className = "home-content" >
{ /* Asset Dashboard - Requirement 8.1 */ }
< section className = "dashboard-grid" >
{ /* Net Worth Card - Main Hero */ }
< div className = "dashboard-card home-net-worth-card " >
< div className = "card-content" >
< span className = "card-label " > 净 资 产 < / span >
< div className = "card-value-group" >
< span className = "currency-symbol" > ¥ < / spa n>
< span className = "card-value-main privacy-mask" > { formatCurrency ( netWorth ) . replace ( /[^0-9.,-]/g , '' ) } < / span >
< / div >
< div className = "card-footer" >
< span className = "trend-neutral" > 总 览 所 有 账 户 < / span >
< / div >
< / div >
< div className = "card-bg-decoration" > < / div >
< / div >
< butto n className = "action-card ai" onClick = { handleAIBookkeeping } >
< div className = "action-icon blur-bg" >
< Icon icon = "solar:microphone-3-bold-duotone" width = "24" / >
< / div >
< div className = "action-info " >
< span className = "action-title" > AI 记 账 < / span >
< span className = "action-desc " > 语 音 或 文 字 智 能 记 账 < / span >
< / div >
< / butto n>
{ /* Assets Card */ }
< div className = "dashboard-card assets-card" onClick = { handleViewAllAccounts } >
< div className = "card-icon-wrapper income" >
< Icon icon = "solar:graph-up-bold-duotone" width = "20" / >
< / div >
< div className = "card-content" >
< span className = "card-label " > 总 资 产 < / span >
< span className = "card-value-sub privacy-mask" > { formatCurrency ( totalAssets ) } < / span >
< / div >
< / div >
< button className = "action-card secondary" onClick = { handleViewAllAccounts } >
< div className = "action-icon blur-bg" >
< Icon icon = "solar:wallet-bold-duotone" width = "24" / >
< / div >
< div className = "action-info" >
< span className = "action-title" > 管 理 账 户 < / span >
< span className = "action-desc " > 查 看 { accounts . length } 个 账 户 详 情 < / span >
< / div >
< / button >
< / section >
{ /* Liabilities Card */ }
< div className = "dashboard-card liabilities-card" onClick = { handleViewAllAccounts } >
< div className = "card-icon-wrapper expense " >
< Icon ic on = "solar:graph-down-bold-duotone" width = "20" / >
< / div >
< div className = "card-content" >
< span className = "card-label" > 总 负 债 < / span >
< span className = "card-value-sub privacy-mask" > { formatCurrency ( totalLiabilities ) } < / span >
< / div >
< / div >
< / section >
{ /* Quick Actions Section */ }
< section className = "quick-actions-section" >
< button className = "action-card primary" onClick = { handleQuickTransaction } >
< div className = "action-icon blur-bg" >
< Icon icon = "solar:add-circle-bold-duotone" width = "24" / >
< / div >
< div className = "action-info" >
< span className = "action-title" > 快 速 记 账 < / span >
< span className = "action-desc" > 记 录 新 的 收 入 或 支 出 < / span >
< / div >
< / button >
< button className = "action-card ai" onClick = { handleAIBookkeeping } >
< div className = "action-icon blur-bg" >
< Icon icon = "solar:microphone-3-bold-duotone" width = "24" / >
< / div >
< div className = "action-info" >
< span className = "action-title" > AI 记 账 < / span >
< span className = "action-desc" > 语 音 或 文 字 智 能 记 账 < / span >
< / div >
< / button >
< button className = "action-card secondary" onClick = { handleViewAllAccounts } >
< div className = "action-icon blur-bg" >
< Icon icon = "solar:wallet-bold-duotone" width = "24" / >
< / div >
< div className = "action-info" >
< span className = "action-title" > 管 理 账 户 < / span >
< span className = "action-desc" > 查 看 { accounts . length } 个 账 户 详 情 < / span >
< / div >
< / button >
< / section >
< div className = "content-columns" >
{ /* Spending Trend Chart */ }
< div className = "chart-container privacy-mask" >
< SpendingTrendChart transactions = { recentTransactions } / >
< / div >
{ /* Recent Transactions List */ }
< section className = "recent-transactions-section" >
< div className = "section-header" >
< h2 > 最 近 交 易 < / h2 >
< button className = "view-all-link" onClick = { handleViewAllTransactions } >
查 看 全 部
< / button >
< / div >
{ recentTransactions . length > 0 ? (
< div className = "transaction-list-compact" >
{ recentTransactions . map ( ( transaction ) = > (
< div key = { transaction . id } className = "transaction-row" onClick = { ( ) = > navigate ( '/transactions' ) } >
< div className = { ` transaction-icon ${ transaction . type } ` } >
{ transaction . type === 'income' ? < Icon icon = "solar:graph-up-bold-duotone" width = "16" / > : < Icon icon = "solar:graph-down-bold-duotone" width = "16" / > }
< / div >
< div className = "transaction-details" >
< span className = "transaction-category" >
{ categories . find ( c = > c . id === transaction . categoryId ) ? . name || '无分类' }
< / span >
< span className = "transaction-note-compact" > { transaction . note || '无备注' } < / span >
< / div >
< div className = "transaction-meta" >
< span className = { ` transaction-amount-compact ${ transaction . type } privacy-mask ` } >
{ transaction . type === 'income' ? '+' : '-' } { formatCurrency ( Math . abs ( transaction . amount ) ) }
< / span >
< span className = "transaction-time" > { formatDate ( transaction . transactionDate ) } < / span >
< / div >
< / div >
) ) }
< / div >
) : (
< div className = "empty-state-compact" >
< Icon icon = "solar:document-text-bold-duotone" width = "32" / >
< p > 暂 无 交 易 记 录 < / p >
< / div >
) }
< / section >
< / div >
< / main >
{ /* Ledger Selector Modal - Requirements 3.2, 3.3 */ }
{ ledgers . length > 0 && (
< LedgerSelector
ledgers = { ledgers }
currentLedgerId = { currentLedger ? . id || 0 }
onSelect = { handleLedgerSelect }
onAdd = { handleAddLedger }
onManage = { handleManageLedgers }
onReorder = { handleLedgerReorder }
open = { ledgerSelectorOpen }
onClose = { ( ) = > setLedgerSelectorOpen ( false ) }
/ >
) }
{ /* AI Voice Input Modal */ }
< VoiceInputModal
isOpen = { voiceModalOpen }
onClose = { ( ) = > setVoiceModalOpen ( false ) }
onConfirm = { handleAIConfirm }
/ >
{ /* First Account Guide Modal */ }
< CreateFirstAccountModal
isOpen = { ! loading && ! error && accounts . length === 0 && ! showAccountForm }
onCreate = { ( ) = > setShowAccountForm ( true ) }
/ >
{ /* Account Creation Modal */ }
{ showAccountForm && (
< div className = "modal-overlay" >
< div className = "modal-content" >
< AccountForm
onSubmit = { async ( data ) = > {
try {
setLoading ( true ) ;
await createAccount ( data ) ;
setShowAccountForm ( false ) ;
await loadData ( ) ;
} catch ( err ) {
setError ( '创建账户失败,请重试' ) ;
console . error ( err ) ;
setLoading ( false ) ;
}
} }
onCancel = { ( ) = > {
// Should not allow cancel if it's the first account?
// Let's allow it but the Guide Modal will pop up again immediately because accounts.length is still 0
setShowAccountForm ( false ) ;
} }
loading = { loading }
/ >
< / div >
< / div >
) }
< Confetti active = { showConfetti } recycle = { false } onComplete = { ( ) = > setShowConfetti ( false ) } / >
< HealthScoreModal
isOpen = { showHealthModal }
onClose = { ( ) = > setShowHealthModal ( false ) }
score = { healthScore }
totalAssets = { totalAssets }
totalLiabilities = { totalLiabilities }
todaySpend = { todaySpend }
yesterdaySpend = { yesterdaySpend }
/ >
< div className = "content-columns" >
{ /* Spending Trend Chart */ }
< div className = "ch art-container privacy-mask " >
< SpendingTrendChart transacti ons = { recentTransactions } / >
< / div >
) ;
{ /* Recent Transactions List */ }
< section className = "recent-transactions-section" >
< div className = "section-header" >
< h2 > 最 近 交 易 < / h2 >
< button className = "view-all-link" onClick = { handleViewAllTransactions } >
查 看 全 部
< / button >
< / div >
{ recentTransactions . length > 0 ? (
< div className = "transaction-list-compact" >
{ recentTransactions . map ( ( transaction ) = > (
< div key = { transaction . id } className = "transaction-row" onClick = { ( ) = > navigate ( '/transactions' ) } >
< div className = { ` transaction-icon ${ transaction . type } ` } >
{ transaction . type === 'income' ? < Icon icon = "solar:graph-up-bold-duotone" width = "16" / > : < Icon icon = "solar:graph-down-bold-duotone" width = "16" / > }
< / div >
< div className = "transaction-details" >
< span className = "transaction-category" >
{ categories . find ( c = > c . id === transaction . categoryId ) ? . name || '无分类' }
< / span >
< span className = "transaction-note-compact" > { transaction . note || '无备注' } < / span >
< / div >
< div className = "transaction-meta" >
< span className = { ` transaction-amount-compact ${ transaction . type } privacy-mask ` } >
{ transaction . type === 'income' ? '+' : '-' } { formatCurrency ( Math . abs ( transaction . amount ) ) }
< / span >
< span className = "transaction-time" > { formatDate ( transaction . transactionDate ) } < / span >
< / div >
< / div >
) ) }
< / div >
) : (
< div className = "empty-state-compact" >
< Icon icon = "solar:document-text-bold-duotone" width = "32" / >
< p > 暂 无 交 易 记 录 < / p >
< / div >
) }
< / section >
< / div >
< / main >
{ /* Ledger Selector Modal - Requirements 3.2, 3.3 */ }
{ ledgers . length > 0 && (
< LedgerSelector
ledgers = { ledgers }
currentLedgerId = { currentLedger ? . id || 0 }
onSelect = { handleLedgerSelect }
onAdd = { handleAddLedger }
onManage = { handleManageLedgers }
onReorder = { handleLedgerReorder }
open = { ledgerSelectorOpen }
onClose = { ( ) = > setLedgerSelectorOpen ( false ) }
/ >
) }
{ /* AI Voice Input Modal */ }
< VoiceInputModal
isOpen = { voiceModalOpen }
onClose = { ( ) = > setVoiceModalOpen ( false ) }
onConfirm = { handleAIConfirm }
/ >
{ /* First Account Guide Modal */ }
< CreateFirstAccountModal
isOpen = { ! loading && ! error && accounts . length === 0 && ! showAccountForm }
onCreate = { ( ) = > setShowAccountForm ( true ) }
/ >
{ /* Account Creation Modal */ }
{ showAccountForm && (
< div className = "modal-overlay" >
< div className = "modal-content" >
< AccountForm
onSubmit = { async ( data ) = > {
try {
setLoading ( true ) ;
await createAccount ( data ) ;
setShowAccountForm ( false ) ;
await loadData ( ) ;
} catch ( err ) {
setError ( '创建账户失败,请重试' ) ;
console . error ( err ) ;
setLoading ( false ) ;
}
} }
onCancel = { ( ) = > {
// Should not allow cancel if it's the first account?
// Let's allow it but the Guide Modal will pop up again immediately because accounts.length is still 0
setShowAccountForm ( false ) ;
} }
loading = { loading }
/ >
< / div >
< / div >
) }
< Confetti active = { showConfetti } recycle = { false } onComplete = { ( ) = > setShowConfetti ( false ) } / >
< HealthScoreModal
isOpen = { showHealthModal }
onClose = { ( ) = > setShowHealthModal ( false ) }
score = { healthScore }
totalAssets = { totalAssets }
totalLiabilities = { totalLiabilities }
todaySpend = { todaySpend }
yesterdaySpend = { yesterdaySpend }
/ >
< / div >
) ;
}
export default Home ;
export default Home ;