feat: 添加首页、交易页面及相关组件,并引入Confetti效果。
This commit is contained in:
@@ -14,8 +14,10 @@ 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 type { Account, Transaction, Category, Ledger, UserSettings } from '../../types';
|
||||
|
||||
|
||||
/**
|
||||
* Home Page Component
|
||||
* Displays account balance overview and recent transactions
|
||||
@@ -35,9 +37,11 @@ function Home() {
|
||||
const [ledgerSelectorOpen, setLedgerSelectorOpen] = useState(false);
|
||||
const [voiceModalOpen, setVoiceModalOpen] = useState(false);
|
||||
const [showAccountForm, setShowAccountForm] = useState(false);
|
||||
const [showConfetti, setShowConfetti] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
@@ -169,15 +173,50 @@ function Home() {
|
||||
}
|
||||
};
|
||||
|
||||
const getGreeting = () => {
|
||||
// Phase 3: Daily Briefing Logic
|
||||
const getDailyBriefing = () => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 5) return '夜深了';
|
||||
if (hour < 11) return '早上好';
|
||||
if (hour < 13) return '中午好';
|
||||
if (hour < 18) return '下午好';
|
||||
return '晚上好';
|
||||
const todaySpend = recentTransactions
|
||||
.filter(t => {
|
||||
const tDate = new Date(t.transactionDate);
|
||||
const today = new Date();
|
||||
return tDate.getDate() === today.getDate() &&
|
||||
tDate.getMonth() === today.getMonth() &&
|
||||
tDate.getFullYear() === today.getFullYear() &&
|
||||
t.type === 'expense';
|
||||
})
|
||||
.reduce((sum, t) => sum + Math.abs(t.amount), 0);
|
||||
|
||||
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 = '今天还没有记账哦';
|
||||
if (todaySpend > 0) {
|
||||
insight = `今天已支出 ${formatCurrency(todaySpend)}`;
|
||||
}
|
||||
|
||||
return { greeting, insight };
|
||||
};
|
||||
|
||||
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;
|
||||
if (score > 98) score = 98;
|
||||
return score;
|
||||
};
|
||||
const healthScore = calculateHealthScore();
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="home-page">
|
||||
@@ -221,23 +260,46 @@ function Home() {
|
||||
return (
|
||||
<div className="home-page">
|
||||
<header className="home-header">
|
||||
<div className="home-greeting">
|
||||
<div className="greeting-title-wrapper" onClick={() => setLedgerSelectorOpen(true)}>
|
||||
<h1 className="greeting-text">{getGreeting()}</h1>
|
||||
{currentLedger && (
|
||||
<div className="current-ledger-badge">
|
||||
<Icon icon="solar: notebook-minimalistic-bold-duotone" width="14" />
|
||||
<span>{currentLedger.name}</span>
|
||||
<Icon icon="solar:alt-arrow-down-bold" width="12" className="chevron-icon" />
|
||||
<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}
|
||||
</p>
|
||||
</div>
|
||||
<p className="home-date">{new Date().toLocaleDateString('zh-CN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</p>
|
||||
</div>
|
||||
<button className="quick-action-btn-small" onClick={handleQuickTransaction}>
|
||||
<Icon icon="solar:add-circle-bold-duotone" width="20" />
|
||||
<span>记一笔</span>
|
||||
</button>
|
||||
<div className="header-actions animate-slide-up delay-200">
|
||||
<button className="health-score-btn" onClick={() => setShowConfetti(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>
|
||||
|
||||
</header>
|
||||
|
||||
|
||||
@@ -418,7 +480,10 @@ function Home() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Confetti active={showConfetti} recycle={false} onComplete={() => setShowConfetti(false)} />
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user