feat: 实现首页组件,展示账户概览、交易趋势、预算信息及提供快捷操作。
This commit is contained in:
@@ -69,7 +69,7 @@ function Home() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Helper for local date string YYYY-MM-DD to fix timezone issues
|
// Helper for local date string YYYY-MM-DD
|
||||||
const toLocalDate = (d: Date) => {
|
const toLocalDate = (d: Date) => {
|
||||||
const year = d.getFullYear();
|
const year = d.getFullYear();
|
||||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
@@ -77,56 +77,74 @@ function Home() {
|
|||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate dates for today, yesterday, and last week same day
|
// Helper to check if two dates are the same day (using local time)
|
||||||
|
const isSameDay = (d1: Date, d2: Date) => {
|
||||||
|
return d1.getFullYear() === d2.getFullYear() &&
|
||||||
|
d1.getMonth() === d2.getMonth() &&
|
||||||
|
d1.getDate() === d2.getDate();
|
||||||
|
};
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const yesterday = new Date(today);
|
const yesterday = new Date(today);
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
const lastWeek = new Date(today);
|
const lastWeekSameDay = new Date(today);
|
||||||
lastWeek.setDate(lastWeek.getDate() - 7);
|
lastWeekSameDay.setDate(lastWeekSameDay.getDate() - 7);
|
||||||
|
|
||||||
const todayStr = toLocalDate(today);
|
// Calculate range for fetching: 8 days ago to Tomorrow (to cover timezone shifts)
|
||||||
const yesterdayStr = toLocalDate(yesterday);
|
const rangeStart = new Date(today);
|
||||||
const lastWeekStr = toLocalDate(lastWeek);
|
rangeStart.setDate(rangeStart.getDate() - 8);
|
||||||
|
const rangeEnd = new Date(today);
|
||||||
|
rangeEnd.setDate(rangeEnd.getDate() + 1);
|
||||||
|
|
||||||
// Generate last 7 days date strings (from 6 days ago to today)
|
const rangeStartStr = toLocalDate(rangeStart);
|
||||||
const last7Days: string[] = [];
|
const rangeEndStr = toLocalDate(rangeEnd);
|
||||||
for (let i = 6; i >= 0; i--) {
|
|
||||||
const d = new Date(today);
|
|
||||||
d.setDate(d.getDate() - i);
|
|
||||||
last7Days.push(toLocalDate(d));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load accounts, recent transactions, today/yesterday stats... AND last 7 days
|
// Parallel fetch optimized: Single request for transaction history
|
||||||
const [accountsData, transactionsData, categoriesData, ledgersData, settingsData, budgetsData, todayData, yesterdayData, lastWeekData, streakData, ...last7DaysData] = await Promise.all([
|
const [accountsData, recentTxData, categoriesData, ledgersData, settingsData, budgetsData, streakData, rangeTxData] = await Promise.all([
|
||||||
getAccounts(),
|
getAccounts(),
|
||||||
getTransactions({ page: 1, pageSize: 5 }), // Recent
|
getTransactions({ page: 1, pageSize: 5 }), // Recent list (server sort)
|
||||||
getCategories(),
|
getCategories(),
|
||||||
getLedgers().catch(() => []),
|
getLedgers().catch(() => []),
|
||||||
getSettings().catch(() => null),
|
getSettings().catch(() => null),
|
||||||
getBudgets().catch(() => []),
|
getBudgets().catch(() => []),
|
||||||
getTransactions({ startDate: todayStr, endDate: todayStr, type: 'expense', pageSize: 100 }),
|
|
||||||
getTransactions({ startDate: yesterdayStr, endDate: yesterdayStr, type: 'expense', pageSize: 100 }),
|
|
||||||
getTransactions({ startDate: lastWeekStr, endDate: lastWeekStr, type: 'expense', pageSize: 100 }), // Last week same day
|
|
||||||
getStreakInfo().catch(() => null),
|
getStreakInfo().catch(() => null),
|
||||||
// Fetch last 7 days data
|
getTransactions({ startDate: rangeStartStr, endDate: rangeEndStr, type: 'expense', pageSize: 1000 }), // Bulk fetch
|
||||||
...last7Days.map(dateStr =>
|
|
||||||
getTransactions({ startDate: dateStr, endDate: dateStr, type: 'expense', pageSize: 100 })
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setAccounts(accountsData || []);
|
setAccounts(accountsData || []);
|
||||||
setRecentTransactions(transactionsData?.items || []);
|
setRecentTransactions(recentTxData?.items || []);
|
||||||
setCategories(categoriesData || []);
|
setCategories(categoriesData || []);
|
||||||
setLedgers(ledgersData || []);
|
setLedgers(ledgersData || []);
|
||||||
setSettings(settingsData);
|
setSettings(settingsData);
|
||||||
|
setStreakInfo(streakData);
|
||||||
|
|
||||||
// Calculate daily spends
|
// In-Memory Aggregation for Daily Spend (Fixes Timezone Issues)
|
||||||
setTodaySpend(calculateTotalExpense(todayData.items));
|
const allRangeTx = rangeTxData?.items || [];
|
||||||
setTodayTransactions(todayData.items || []);
|
|
||||||
setYesterdaySpend(calculateTotalExpense(yesterdayData.items));
|
|
||||||
setLastWeekSpend(calculateTotalExpense(lastWeekData.items));
|
|
||||||
|
|
||||||
// Calculate monthly budget stats (Normalized)
|
// Today
|
||||||
|
const todayTxItems = allRangeTx.filter(t => isSameDay(new Date(t.transactionDate), today));
|
||||||
|
setTodayTransactions(todayTxItems);
|
||||||
|
setTodaySpend(calculateTotalExpense(todayTxItems));
|
||||||
|
|
||||||
|
// Yesterday
|
||||||
|
const yesterdayTxItems = allRangeTx.filter(t => isSameDay(new Date(t.transactionDate), yesterday));
|
||||||
|
setYesterdaySpend(calculateTotalExpense(yesterdayTxItems));
|
||||||
|
|
||||||
|
// Last Week Same Day
|
||||||
|
const lastWeekTxItems = allRangeTx.filter(t => isSameDay(new Date(t.transactionDate), lastWeekSameDay));
|
||||||
|
setLastWeekSpend(calculateTotalExpense(lastWeekTxItems));
|
||||||
|
|
||||||
|
// Last 7 Days Array (for chart)
|
||||||
|
const last7DaysSpendArray = [];
|
||||||
|
for (let i = 6; i >= 0; i--) {
|
||||||
|
const d = new Date(today);
|
||||||
|
d.setDate(d.getDate() - i);
|
||||||
|
const dayTx = allRangeTx.filter(t => isSameDay(new Date(t.transactionDate), d));
|
||||||
|
last7DaysSpendArray.push(calculateTotalExpense(dayTx));
|
||||||
|
}
|
||||||
|
setLast7DaysSpend(last7DaysSpendArray);
|
||||||
|
|
||||||
|
// Monthly Budget Stats
|
||||||
let mTotal = 0;
|
let mTotal = 0;
|
||||||
let mSpent = 0;
|
let mSpent = 0;
|
||||||
(budgetsData || []).forEach((b: any) => {
|
(budgetsData || []).forEach((b: any) => {
|
||||||
@@ -136,22 +154,11 @@ function Home() {
|
|||||||
else if (b.periodType === 'yearly') multiplier = 1 / 12;
|
else if (b.periodType === 'yearly') multiplier = 1 / 12;
|
||||||
|
|
||||||
mTotal += b.amount * multiplier;
|
mTotal += b.amount * multiplier;
|
||||||
// We add the actual spent amount regardless of period, as it contributes to "Monthly Spending" in a general sense
|
|
||||||
// But strictly speaking, Daily spent resets daily.
|
|
||||||
// For the purpose of "Monthly Overview", we just want to know if there IS a budget.
|
|
||||||
mSpent += (b.spent || 0);
|
mSpent += (b.spent || 0);
|
||||||
});
|
});
|
||||||
setMonthlyBudgetTotal(mTotal);
|
setMonthlyBudgetTotal(mTotal);
|
||||||
setMonthlyBudgetSpentTotal(mSpent);
|
setMonthlyBudgetSpentTotal(mSpent);
|
||||||
|
|
||||||
// Set streak info
|
|
||||||
setStreakInfo(streakData);
|
|
||||||
|
|
||||||
// Calculate last 7 days spending array
|
|
||||||
const last7DaysSpendArray = last7DaysData.map(dayData =>
|
|
||||||
calculateTotalExpense(dayData?.items || [])
|
|
||||||
);
|
|
||||||
setLast7DaysSpend(last7DaysSpendArray);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : '加载数据失败');
|
setError(err instanceof Error ? err.message : '加载数据失败');
|
||||||
console.error('Failed to load home page data:', err);
|
console.error('Failed to load home page data:', err);
|
||||||
|
|||||||
Reference in New Issue
Block a user