From c80f64346ee9cd3e263652e522eb94a22d946f79 Mon Sep 17 00:00:00 2001 From: 12975 <1297598740@qq.com> Date: Thu, 29 Jan 2026 00:11:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9C=A8=E9=A6=96=E9=A1=B5=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=AF=8F=E6=97=A5=E6=B4=9E=E5=AF=9F=E5=8D=A1=EF=BC=8C?= =?UTF-8?q?=E9=9B=86=E6=88=90AI=E6=9C=8D=E5=8A=A1=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E4=B8=AA=E6=80=A7=E5=8C=96=E8=B4=A2=E5=8A=A1=E5=88=86=E6=9E=90?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DailyInsightCard/DailyInsightCard.css | 77 +++++++++++++++++++ .../DailyInsightCard/DailyInsightCard.tsx | 66 +++++++++++++++- src/pages/Home/Home.tsx | 65 +++++++++++++--- src/services/aiService.ts | 22 +++++- 4 files changed, 213 insertions(+), 17 deletions(-) diff --git a/src/components/home/DailyInsightCard/DailyInsightCard.css b/src/components/home/DailyInsightCard/DailyInsightCard.css index 7d98834..6aaf16a 100644 --- a/src/components/home/DailyInsightCard/DailyInsightCard.css +++ b/src/components/home/DailyInsightCard/DailyInsightCard.css @@ -159,4 +159,81 @@ .week-diff-badge.red { background: rgba(239, 68, 68, 0.1); color: #EF4444; +} + +/* Emoji badge in header */ +.daily-insight__emoji { + font-size: 1.2rem; + margin-left: auto; + animation: bounce 1s ease-in-out; +} + +@keyframes bounce { + + 0%, + 100% { + transform: scale(1); + } + + 50% { + transform: scale(1.2); + } +} + +/* Tip section at bottom */ +.daily-insight__tip { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + background: linear-gradient(135deg, rgba(var(--color-primary-rgb), 0.08), rgba(var(--color-primary-rgb), 0.02)); + border-radius: var(--radius-lg); + font-size: 0.85rem; + color: var(--color-text-secondary); + grid-column: 1 / -1; +} + +.daily-insight__tip svg { + color: var(--color-primary); + flex-shrink: 0; +} + +/* Trend Chart */ +.trend-chart { + display: flex; + align-items: flex-end; + justify-content: space-between; + height: 60px; + padding-top: 10px; + gap: 4px; +} + +.trend-bar-wrapper { + flex: 1; + height: 100%; + display: flex; + align-items: flex-end; + justify-content: center; +} + +.trend-bar { + width: 60%; + background: rgba(var(--color-text-rgb), 0.1); + border-radius: 4px; + transition: all 0.3s ease; + min-height: 4px; +} + +.trend-bar--today { + background: var(--color-primary); + box-shadow: 0 0 8px rgba(var(--color-primary-rgb), 0.4); +} + +.trend-bar--max { + background: var(--color-warning, #F59E0B); +} + +/* If today is also max, give it a special gradient */ +.trend-bar--today.trend-bar--max { + background: linear-gradient(to top, var(--color-primary), var(--color-warning, #F59E0B)); } \ No newline at end of file diff --git a/src/components/home/DailyInsightCard/DailyInsightCard.tsx b/src/components/home/DailyInsightCard/DailyInsightCard.tsx index ec9400e..f1f1b61 100644 --- a/src/components/home/DailyInsightCard/DailyInsightCard.tsx +++ b/src/components/home/DailyInsightCard/DailyInsightCard.tsx @@ -13,6 +13,14 @@ interface DailyInsightCardProps { maxTransaction?: { note?: string; amount: number }; lastWeekSpend?: number; streakDays?: number; + // 新增字段 + budgetRemaining?: number; + daysRemaining?: number; + weeklyTotal?: number; + avgDailySpend?: number; + top3Categories?: { name: string; amount: number }[]; + todayTransactionCount?: number; + last7DaysSpend?: number[]; // 最近7天每日支出 } export const DailyInsightCard: React.FC = ({ @@ -23,9 +31,16 @@ export const DailyInsightCard: React.FC = ({ topCategory, maxTransaction, lastWeekSpend, - streakDays + streakDays, + budgetRemaining, + daysRemaining, + weeklyTotal, + avgDailySpend, + top3Categories, + todayTransactionCount, + last7DaysSpend, }) => { - const [aiData, setAiData] = useState<{ spending: string; budget: string } | null>(null); + const [aiData, setAiData] = useState<{ spending: string; budget: string; emoji?: string; tip?: string } | null>(null); const [isAiLoading, setIsAiLoading] = useState(false); useEffect(() => { @@ -50,7 +65,15 @@ export const DailyInsightCard: React.FC = ({ amount: maxTransaction.amount } : undefined, lastWeekSpend, - streakDays + streakDays, + // 新增数据 + budgetRemaining, + daysRemaining: daysRemaining ?? (daysInMonth - today), + weeklyTotal, + avgDailySpend, + top3Categories, + todayTransactionCount, + last7DaysSpend, }); setAiData(result); } catch (e) { @@ -62,7 +85,7 @@ export const DailyInsightCard: React.FC = ({ const timer = setTimeout(fetchAI, 500); return () => clearTimeout(timer); - }, [todaySpend, yesterdaySpend, monthlyBudget, monthlySpent, topCategory, maxTransaction, lastWeekSpend, streakDays]); + }, [todaySpend, yesterdaySpend, monthlyBudget, monthlySpent, topCategory, maxTransaction, lastWeekSpend, streakDays, budgetRemaining, daysRemaining, weeklyTotal, avgDailySpend, top3Categories, todayTransactionCount, last7DaysSpend]); const getSpendingInsight = (today: number, yesterday: number) => { if (aiData) return { text: {aiData.spending}, type: 'ai' }; @@ -114,6 +137,7 @@ export const DailyInsightCard: React.FC = ({
{aiData ? 'AI 每日简报' : '每日简报'} + {aiData?.emoji && {aiData.emoji}} {isAiLoading && !aiData && AI 思考中...}
@@ -130,12 +154,46 @@ export const DailyInsightCard: React.FC = ({

{spendingInsight.text}

+ {/* 7-Day Trend Chart */} + {last7DaysSpend && last7DaysSpend.length > 0 && ( +
+ 近7天趋势 +
+ {last7DaysSpend.map((amount, index) => { + const maxVal = Math.max(...last7DaysSpend, 1); + const heightPercent = Math.max((amount / maxVal) * 100, 10); // Min 10% height + const isToday = index === last7DaysSpend.length - 1; + const isMax = amount === maxVal && amount > 0; + + return ( +
+
+
+ ); + })} +
+
+ )} +
预算风向标

{budgetInsight.text}

+ + {aiData?.tip && ( + <> +
+
+ + {aiData.tip} +
+ + )}
); diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 0f68fab..1c23605 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -56,7 +56,8 @@ function Home() { const [monthlyBudgetTotal, setMonthlyBudgetTotal] = useState(0); const [monthlyBudgetSpentTotal, setMonthlyBudgetSpentTotal] = useState(0); const [todayTransactions, setTodayTransactions] = useState([]); - const [lastWeekSpend, setLastWeekSpend] = useState(0); // New State + const [lastWeekSpend, setLastWeekSpend] = useState(0); + const [last7DaysSpend, setLast7DaysSpend] = useState([]); // New: 最近7天支出 useEffect(() => { @@ -87,8 +88,16 @@ function Home() { const yesterdayStr = toLocalDate(yesterday); const lastWeekStr = toLocalDate(lastWeek); - // Load accounts, recent transactions, today/yesterday stats... AND last week - const [accountsData, transactionsData, categoriesData, ledgersData, settingsData, budgetsData, todayData, yesterdayData, lastWeekData, streakData] = await Promise.all([ + // Generate last 7 days date strings (from 6 days ago to today) + const last7Days: string[] = []; + 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 + const [accountsData, transactionsData, categoriesData, ledgersData, settingsData, budgetsData, todayData, yesterdayData, lastWeekData, streakData, ...last7DaysData] = await Promise.all([ getAccounts(), getTransactions({ page: 1, pageSize: 5 }), // Recent getCategories(), @@ -99,6 +108,10 @@ function Home() { 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), + // Fetch last 7 days data + ...last7Days.map(dateStr => + getTransactions({ startDate: dateStr, endDate: dateStr, type: 'expense', pageSize: 100 }) + ), ]); setAccounts(accountsData || []); @@ -133,6 +146,12 @@ function Home() { // Set streak info setStreakInfo(streakData); + + // Calculate last 7 days spending array + const last7DaysSpendArray = last7DaysData.map(dayData => + calculateTotalExpense(dayData?.items || []) + ); + setLast7DaysSpend(last7DaysSpendArray); } catch (err) { setError(err instanceof Error ? err.message : '加载数据失败'); console.error('Failed to load home page data:', err); @@ -141,34 +160,53 @@ function Home() { } }; - const { maxTransaction, topCategory } = useMemo(() => { - if (todayTransactions.length === 0) return { maxTransaction: undefined, topCategory: undefined }; + const { maxTransaction, topCategory, top3Categories, todayTransactionCount, avgDailySpend } = useMemo(() => { + if (todayTransactions.length === 0) return { + maxTransaction: undefined, + topCategory: undefined, + top3Categories: undefined, + todayTransactionCount: 0, + avgDailySpend: 0 + }; // Max Transaction const maxTx = [...todayTransactions].sort((a, b) => b.amount - a.amount)[0]; - // Top Category + // Category aggregation const catMap: Record = {}; todayTransactions.forEach((t) => { catMap[t.categoryId] = (catMap[t.categoryId] || 0) + t.amount; }); - // Sort categories + // Sort categories by amount const sortedCatIds = Object.keys(catMap).sort((a, b) => catMap[Number(b)] - catMap[Number(a)]); - // Find top one + // Top category let topCatName = undefined; if (sortedCatIds.length > 0) { const topCatId = Number(sortedCatIds[0]); topCatName = categories.find((c) => c.id === topCatId)?.name; - // Optimization: if category name not found, try to look up in transaction's populated data if available or fallback } + // Top 3 categories + const top3 = sortedCatIds.slice(0, 3).map(idStr => { + const id = Number(idStr); + const name = categories.find((c) => c.id === id)?.name || '未知'; + return { name, amount: catMap[id] }; + }); + + // Avg daily spend (简单计算: 本月已花 / 已过天数) + const dayOfMonth = new Date().getDate(); + const avgDaily = dayOfMonth > 0 ? monthlyBudgetSpentTotal / dayOfMonth : 0; + return { maxTransaction: maxTx, - topCategory: topCatName ? { name: topCatName, amount: catMap[Number(sortedCatIds[0])] } : undefined + topCategory: topCatName ? { name: topCatName, amount: catMap[Number(sortedCatIds[0])] } : undefined, + top3Categories: top3.length > 0 ? top3 : undefined, + todayTransactionCount: todayTransactions.length, + avgDailySpend: avgDaily }; - }, [todayTransactions, categories]); + }, [todayTransactions, categories, monthlyBudgetSpentTotal]); const handleQuickTransaction = () => { navigate('/transactions?action=new'); @@ -475,6 +513,11 @@ function Home() { topCategory={topCategory} lastWeekSpend={lastWeekSpend} streakDays={streakInfo?.currentStreak || 0} + budgetRemaining={monthlyBudgetTotal - monthlyBudgetSpentTotal} + avgDailySpend={avgDailySpend} + top3Categories={top3Categories} + todayTransactionCount={todayTransactionCount} + last7DaysSpend={last7DaysSpend} />
diff --git a/src/services/aiService.ts b/src/services/aiService.ts index e597723..3457066 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -299,21 +299,39 @@ let dailyInsightCache: { } | null = null; export interface DailyInsightContext { + // 今日数据 todaySpend: number; yesterdaySpend: number; + + // 预算数据 monthlyBudget: number; monthlySpent: number; monthProgress: number; // 0-1 + budgetRemaining?: number; // 剩余预算 + daysRemaining?: number; // 剩余天数 + + // 历史数据 + lastWeekSpend?: number; // 上周同日 + weeklyTotal?: number; // 本周累计 + last7DaysSpend?: number[];// 最近7天支出数组 + avgDailySpend?: number; // 日均支出 + + // 分类分析 topCategory?: { name: string; amount: number }; + top3Categories?: { name: string; amount: number }[]; // 本月前三分类 + + // 交易详情 maxTransaction?: { note: string; amount: number }; - lastWeekSpend?: number; + todayTransactionCount?: number; // 今日交易笔数 + + // 用户习惯 streakDays?: number; } /** * Get AI-powered daily insight */ -export async function getDailyInsight(context: DailyInsightContext): Promise<{ spending: string; budget: string }> { +export async function getDailyInsight(context: DailyInsightContext): Promise<{ spending: string; budget: string; emoji?: string; tip?: string }> { // Hash needs to include new fields const currentHash = JSON.stringify(context); const NOW = Date.now();