feat: 新增 CommandPalette 和 HealthScoreModal 组件,优化应用布局并引入 Budget、Home 页面。
This commit is contained in:
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import './Home.css';
|
||||
import { getAccounts, calculateTotalAssets, calculateTotalLiabilities } from '../../services/accountService';
|
||||
import { getTransactions } from '../../services/transactionService';
|
||||
import { getTransactions, calculateTotalExpense } from '../../services/transactionService';
|
||||
import { getCategories } from '../../services/categoryService';
|
||||
import { getLedgers, reorderLedgers } from '../../services/ledgerService';
|
||||
import { getSettings, updateSettings } from '../../services/settingsService';
|
||||
@@ -15,6 +15,7 @@ import { CreateFirstAccountModal } from '../../components/account/CreateFirstAcc
|
||||
import { AccountForm } from '../../components/account/AccountForm/AccountForm';
|
||||
import { createAccount } from '../../services/accountService';
|
||||
import { Confetti } from '../../components/common/Confetti';
|
||||
import { HealthScoreModal } from '../../components/home/HealthScoreModal/HealthScoreModal';
|
||||
import type { Account, Transaction, Category, Ledger, UserSettings } from '../../types';
|
||||
|
||||
|
||||
@@ -38,6 +39,9 @@ function Home() {
|
||||
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);
|
||||
|
||||
@@ -51,13 +55,23 @@ function Home() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Load accounts, recent transactions, categories, ledgers, and settings in parallel
|
||||
const [accountsData, transactionsData, categoriesData, ledgersData, settingsData] = await Promise.all([
|
||||
// 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];
|
||||
|
||||
// Load accounts, recent transactions, today/yesterday stats
|
||||
const [accountsData, transactionsData, categoriesData, ledgersData, settingsData, todayData, yesterdayData] = await Promise.all([
|
||||
getAccounts(),
|
||||
getTransactions({ page: 1, pageSize: 5 }), // Get 5 most recent transactions
|
||||
getTransactions({ page: 1, pageSize: 5 }), // Recent transactions
|
||||
getCategories(),
|
||||
getLedgers().catch(() => []), // Gracefully handle if ledgers not available
|
||||
getSettings().catch(() => null), // Gracefully handle if settings not available
|
||||
getLedgers().catch(() => []),
|
||||
getSettings().catch(() => null),
|
||||
getTransactions({ startDate: todayStr, endDate: todayStr, type: 'expense', pageSize: 100 }),
|
||||
getTransactions({ startDate: yesterdayStr, endDate: yesterdayStr, type: 'expense', pageSize: 100 }),
|
||||
]);
|
||||
|
||||
setAccounts(accountsData || []);
|
||||
@@ -65,6 +79,10 @@ function Home() {
|
||||
setCategories(categoriesData || []);
|
||||
setLedgers(ledgersData || []);
|
||||
setSettings(settingsData);
|
||||
|
||||
// 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);
|
||||
@@ -176,17 +194,6 @@ function Home() {
|
||||
// Phase 3: Daily Briefing Logic
|
||||
const getDailyBriefing = () => {
|
||||
const hour = new Date().getHours();
|
||||
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 = '早上好';
|
||||
@@ -196,7 +203,24 @@ function Home() {
|
||||
|
||||
let insight = '今天还没有记账哦';
|
||||
if (todaySpend > 0) {
|
||||
insight = `今天已支出 ${formatCurrency(todaySpend)}`;
|
||||
if (yesterdaySpend > 0) {
|
||||
const diff = todaySpend - yesterdaySpend;
|
||||
const diffPercent = Math.abs((diff / yesterdaySpend) * 100);
|
||||
|
||||
if (Math.abs(diff) < 5) {
|
||||
insight = `今日支出 ${formatCurrency(todaySpend)},与昨天持平`;
|
||||
} else if (diff < 0) {
|
||||
insight = `今日支出 ${formatCurrency(todaySpend)},比昨天节省了 ${diffPercent.toFixed(0)}%`;
|
||||
} else {
|
||||
insight = `今日支出 ${formatCurrency(todaySpend)},比昨天多 ${diffPercent.toFixed(0)}%`;
|
||||
}
|
||||
} else {
|
||||
insight = `今天已支出 ${formatCurrency(todaySpend)}`;
|
||||
}
|
||||
} else {
|
||||
if (yesterdaySpend > 0) {
|
||||
insight = '新的一天,保持理智消费';
|
||||
}
|
||||
}
|
||||
|
||||
return { greeting, insight };
|
||||
@@ -282,7 +306,7 @@ function Home() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="header-actions animate-slide-up delay-200">
|
||||
<button className="health-score-btn" onClick={() => setShowConfetti(true)}>
|
||||
<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" />
|
||||
@@ -479,6 +503,16 @@ function Home() {
|
||||
)}
|
||||
|
||||
<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>
|
||||
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user