feat: 新增每日洞察卡片组件,提供消费和预算分析并集成AI洞察功能
This commit is contained in:
@@ -136,7 +136,7 @@ export const DailyInsightCard: React.FC<DailyInsightCardProps> = ({
|
||||
<div className={`daily-insight-card ${aiData ? 'daily-insight-card--ai' : ''}`}>
|
||||
<div className="daily-insight__header">
|
||||
<Icon icon={aiData ? "solar:magic-stick-3-bold-duotone" : "solar:stars-minimalistic-bold-duotone"} width="20" />
|
||||
<span>{aiData ? 'AI 每日简报' : '每日简报'}</span>
|
||||
<span>{aiData ? 'AI 消费简报' : '消费简报'}</span>
|
||||
{aiData?.emoji && <span className="daily-insight__emoji">{aiData.emoji}</span>}
|
||||
{isAiLoading && !aiData && <span className="daily-insight__loading-badge">AI 思考中...</span>}
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { HealthScoreModal } from '../../components/home/HealthScoreModal/HealthS
|
||||
import { ContributionModal } from '../../components/common/ContributionGraph/ContributionModal';
|
||||
import { DailyInsightCard } from '../../components/home/DailyInsightCard/DailyInsightCard'; // Import
|
||||
import { getBudgets } from '../../services/budgetService'; // Import
|
||||
import { toLocalDateString, isSameDay } from '../../utils/dateUtils';
|
||||
import type { Account, Transaction, Category, Ledger, UserSettings } from '../../types';
|
||||
|
||||
|
||||
@@ -58,6 +59,7 @@ function Home() {
|
||||
const [todayTransactions, setTodayTransactions] = useState<Transaction[]>([]);
|
||||
const [lastWeekSpend, setLastWeekSpend] = useState(0);
|
||||
const [last7DaysSpend, setLast7DaysSpend] = useState<number[]>([]); // New: 最近7天支出
|
||||
const [weekTransactions, setWeekTransactions] = useState<Transaction[]>([]); // New: 最近7天交易详情 (for AI)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@@ -69,20 +71,9 @@ function Home() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Helper for local date string YYYY-MM-DD
|
||||
const toLocalDate = (d: Date) => {
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${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();
|
||||
};
|
||||
|
||||
// Helper for local date string YYYY-MM-DD - REMOVED, using imported utils
|
||||
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
@@ -96,8 +87,8 @@ function Home() {
|
||||
const rangeEnd = new Date(today);
|
||||
rangeEnd.setDate(rangeEnd.getDate() + 1);
|
||||
|
||||
const rangeStartStr = toLocalDate(rangeStart);
|
||||
const rangeEndStr = toLocalDate(rangeEnd);
|
||||
const rangeStartStr = toLocalDateString(rangeStart);
|
||||
const rangeEndStr = toLocalDateString(rangeEnd);
|
||||
|
||||
// Parallel fetch optimized: Single request for transaction history
|
||||
const [accountsData, recentTxData, categoriesData, ledgersData, settingsData, budgetsData, streakData, rangeTxData] = await Promise.all([
|
||||
@@ -136,13 +127,17 @@ function Home() {
|
||||
|
||||
// Last 7 Days Array (for chart)
|
||||
const last7DaysSpendArray = [];
|
||||
const weekTxList: Transaction[] = [];
|
||||
|
||||
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));
|
||||
weekTxList.push(...dayTx);
|
||||
}
|
||||
setLast7DaysSpend(last7DaysSpendArray);
|
||||
setWeekTransactions(weekTxList);
|
||||
|
||||
// Monthly Budget Stats
|
||||
let mTotal = 0;
|
||||
@@ -168,20 +163,17 @@ function Home() {
|
||||
};
|
||||
|
||||
const { maxTransaction, topCategory, top3Categories, todayTransactionCount, avgDailySpend } = useMemo(() => {
|
||||
if (todayTransactions.length === 0) return {
|
||||
maxTransaction: undefined,
|
||||
topCategory: undefined,
|
||||
top3Categories: undefined,
|
||||
todayTransactionCount: 0,
|
||||
avgDailySpend: 0
|
||||
};
|
||||
// Basic stats
|
||||
const todayCount = todayTransactions.length;
|
||||
|
||||
// Max Transaction
|
||||
// Max Transaction (Keep as Today's max for immediate context)
|
||||
const maxTx = [...todayTransactions].sort((a, b) => b.amount - a.amount)[0];
|
||||
|
||||
// Category aggregation
|
||||
// Category aggregation - Use Week Transactions for better AI context
|
||||
const sourceTransactions = weekTransactions.length > 0 ? weekTransactions : todayTransactions;
|
||||
|
||||
const catMap: Record<number, number> = {};
|
||||
todayTransactions.forEach((t) => {
|
||||
sourceTransactions.forEach((t) => {
|
||||
catMap[t.categoryId] = (catMap[t.categoryId] || 0) + t.amount;
|
||||
});
|
||||
|
||||
@@ -210,10 +202,10 @@ function Home() {
|
||||
maxTransaction: maxTx,
|
||||
topCategory: topCatName ? { name: topCatName, amount: catMap[Number(sortedCatIds[0])] } : undefined,
|
||||
top3Categories: top3.length > 0 ? top3 : undefined,
|
||||
todayTransactionCount: todayTransactions.length,
|
||||
todayTransactionCount: todayCount,
|
||||
avgDailySpend: avgDaily
|
||||
};
|
||||
}, [todayTransactions, categories, monthlyBudgetSpentTotal]);
|
||||
}, [todayTransactions, weekTransactions, categories, monthlyBudgetSpentTotal]);
|
||||
|
||||
const handleQuickTransaction = () => {
|
||||
navigate('/transactions?action=new');
|
||||
|
||||
Reference in New Issue
Block a user