Files
Novault-Frontend-web/src/components/home/DailyInsightCard/DailyInsightCard.tsx

176 lines
7.6 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import { Icon } from '@iconify/react';
import { formatCurrency } from '../../../utils/format';
import { getDailyInsight } from '../../../services/aiService';
import './DailyInsightCard.css';
interface DailyInsightCardProps {
todaySpend: number;
yesterdaySpend: number;
monthlyBudget: number;
monthlySpent: number;
topCategory?: { name: string; amount: number };
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<DailyInsightCardProps> = ({
todaySpend,
yesterdaySpend,
monthlyBudget,
monthlySpent,
topCategory,
maxTransaction,
lastWeekSpend,
streakDays,
budgetRemaining,
daysRemaining,
weeklyTotal,
avgDailySpend,
top3Categories,
todayTransactionCount,
last7DaysSpend,
}) => {
const [aiData, setAiData] = useState<{ spending: string; budget: string; emoji?: string; tip?: string } | null>(null);
const [isAiLoading, setIsAiLoading] = useState(false);
useEffect(() => {
// Only fetch if has meaningful data or at least budget is set
if (monthlyBudget === 0 && todaySpend === 0 && yesterdaySpend === 0) return;
const fetchAI = async () => {
2026-01-28 21:48:50 +08:00
setIsAiLoading(true);
try {
const today = new Date().getDate();
const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate();
const result = await getDailyInsight({
todaySpend,
yesterdaySpend,
monthlyBudget,
monthlySpent,
monthProgress: today / daysInMonth,
topCategory,
maxTransaction: maxTransaction ? {
note: maxTransaction.note || '',
amount: maxTransaction.amount
} : undefined,
lastWeekSpend,
streakDays,
// 新增数据
budgetRemaining,
daysRemaining: daysRemaining ?? (daysInMonth - today),
weeklyTotal,
avgDailySpend,
top3Categories,
todayTransactionCount,
last7DaysSpend,
2026-01-28 21:48:50 +08:00
});
setAiData(result);
} catch (e) {
console.warn('AI insight fetch failed, sticking to local logic');
} finally {
setIsAiLoading(false);
}
};
const timer = setTimeout(fetchAI, 500);
return () => clearTimeout(timer);
}, [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: <span>{aiData.spending}</span>, type: 'ai' };
if (today === 0) return { text: "今天还没有花钱,保持这种“零消费”状态就是最大的赚钱!", type: 'success' };
2026-01-28 21:48:50 +08:00
if (yesterday === 0) return { text: <span> <strong className="daily-insight__highlight">{formatCurrency(today)}</strong></span>, type: 'normal' };
const diff = today - yesterday;
if (Math.abs(diff) < 10) return { text: <span> <strong className="daily-insight__highlight">{formatCurrency(today)}</strong></span>, type: 'normal' };
2026-01-28 21:48:50 +08:00
if (today < yesterday) {
2026-01-28 21:48:50 +08:00
const percent = Math.round(((yesterday - today) / yesterday) * 100);
return { text: <span> <strong className="daily-insight__highlight daily-insight__highlight--success">{percent}%</strong></span>, type: 'success' };
} else {
2026-01-28 21:48:50 +08:00
const percent = Math.round(((today - yesterday) / yesterday) * 100);
return { text: <span> <strong className="daily-insight__highlight daily-insight__highlight--warning">{percent}%</strong></span>, type: 'warning' };
}
};
const getBudgetInsight = (spent: number, total: number) => {
if (aiData) return { text: <span>{aiData.budget}</span>, type: 'ai' };
if (total === 0) return { text: "您还没有设置月度预算,建议去设置一个。", type: 'normal' };
2026-01-28 21:48:50 +08:00
const ratio = spent / total;
const today = new Date().getDate();
const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate();
2026-01-28 21:48:50 +08:00
const timeRatio = today / daysInMonth;
if (ratio >= 1) return { text: <span className="daily-insight__highlight daily-insight__highlight--danger"></span>, type: 'danger' };
2026-01-28 21:48:50 +08:00
if (ratio > timeRatio + 0.15) {
2026-01-28 21:48:50 +08:00
return { text: <span> <strong className="daily-insight__highlight daily-insight__highlight--warning">{Math.round(ratio * 100)}%</strong> ( {Math.round(timeRatio * 100)}%)</span>, type: 'warning' };
}
2026-01-28 21:48:50 +08:00
if (ratio < timeRatio - 0.1) {
2026-01-28 21:48:50 +08:00
return { text: <span> <strong className="daily-insight__highlight daily-insight__highlight--success">{Math.round(ratio * 100)}%</strong> ( {Math.round(timeRatio * 100)}%)</span>, type: 'success' };
}
2026-01-28 21:48:50 +08:00
return { text: <span> <strong>{Math.round(ratio * 100)}%</strong> ( {Math.round(timeRatio * 100)}%)</span>, type: 'normal' };
};
const spendingInsight = getSpendingInsight(todaySpend, yesterdaySpend);
const budgetInsight = getBudgetInsight(monthlySpent, monthlyBudget);
return (
2026-01-28 21:48:50 +08:00
<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>
{aiData?.emoji && <span className="daily-insight__emoji">{aiData.emoji}</span>}
2026-01-28 21:48:50 +08:00
{isAiLoading && !aiData && <span className="daily-insight__loading-badge">AI ...</span>}
</div>
<div className="daily-insight__content">
<div className="daily-insight__section">
<div className="section-header-row">
<span className="daily-insight__title"></span>
{lastWeekSpend !== undefined && lastWeekSpend > 0 && (
<span className={`week-diff-badge ${todaySpend <= lastWeekSpend ? 'green' : 'red'}`}>
{todaySpend <= lastWeekSpend ? '↓' : '↑'}{Math.abs(todaySpend - lastWeekSpend).toFixed(0)}
</span>
)}
</div>
2026-01-28 21:48:50 +08:00
<p className="daily-insight__text animate-fade-in">{spendingInsight.text}</p>
</div>
<div className="daily-insight__divider" />
<div className="daily-insight__section">
<span className="daily-insight__title"></span>
<p className="daily-insight__text animate-fade-in">{budgetInsight.text}</p>
</div>
</div>
{aiData?.tip && (
<div className="daily-insight__footer">
<div className="daily-insight__tip">
<Icon icon="solar:lightbulb-bolt-bold-duotone" width="16" />
<span>{aiData.tip}</span>
</div>
</div>
)}
</div>
);
};