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-card ${aiData ? 'daily-insight-card--ai' : ''}`}>
|
||||||
<div className="daily-insight__header">
|
<div className="daily-insight__header">
|
||||||
<Icon icon={aiData ? "solar:magic-stick-3-bold-duotone" : "solar:stars-minimalistic-bold-duotone"} width="20" />
|
<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>}
|
{aiData?.emoji && <span className="daily-insight__emoji">{aiData.emoji}</span>}
|
||||||
{isAiLoading && !aiData && <span className="daily-insight__loading-badge">AI 思考中...</span>}
|
{isAiLoading && !aiData && <span className="daily-insight__loading-badge">AI 思考中...</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { HealthScoreModal } from '../../components/home/HealthScoreModal/HealthS
|
|||||||
import { ContributionModal } from '../../components/common/ContributionGraph/ContributionModal';
|
import { ContributionModal } from '../../components/common/ContributionGraph/ContributionModal';
|
||||||
import { DailyInsightCard } from '../../components/home/DailyInsightCard/DailyInsightCard'; // Import
|
import { DailyInsightCard } from '../../components/home/DailyInsightCard/DailyInsightCard'; // Import
|
||||||
import { getBudgets } from '../../services/budgetService'; // Import
|
import { getBudgets } from '../../services/budgetService'; // Import
|
||||||
|
import { toLocalDateString, isSameDay } from '../../utils/dateUtils';
|
||||||
import type { Account, Transaction, Category, Ledger, UserSettings } from '../../types';
|
import type { Account, Transaction, Category, Ledger, UserSettings } from '../../types';
|
||||||
|
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ function Home() {
|
|||||||
const [todayTransactions, setTodayTransactions] = useState<Transaction[]>([]);
|
const [todayTransactions, setTodayTransactions] = useState<Transaction[]>([]);
|
||||||
const [lastWeekSpend, setLastWeekSpend] = useState(0);
|
const [lastWeekSpend, setLastWeekSpend] = useState(0);
|
||||||
const [last7DaysSpend, setLast7DaysSpend] = useState<number[]>([]); // New: 最近7天支出
|
const [last7DaysSpend, setLast7DaysSpend] = useState<number[]>([]); // New: 最近7天支出
|
||||||
|
const [weekTransactions, setWeekTransactions] = useState<Transaction[]>([]); // New: 最近7天交易详情 (for AI)
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -69,20 +71,9 @@ function Home() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
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) => {
|
// Helper for local date string YYYY-MM-DD - REMOVED, using imported utils
|
||||||
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);
|
||||||
@@ -96,8 +87,8 @@ function Home() {
|
|||||||
const rangeEnd = new Date(today);
|
const rangeEnd = new Date(today);
|
||||||
rangeEnd.setDate(rangeEnd.getDate() + 1);
|
rangeEnd.setDate(rangeEnd.getDate() + 1);
|
||||||
|
|
||||||
const rangeStartStr = toLocalDate(rangeStart);
|
const rangeStartStr = toLocalDateString(rangeStart);
|
||||||
const rangeEndStr = toLocalDate(rangeEnd);
|
const rangeEndStr = toLocalDateString(rangeEnd);
|
||||||
|
|
||||||
// Parallel fetch optimized: Single request for transaction history
|
// Parallel fetch optimized: Single request for transaction history
|
||||||
const [accountsData, recentTxData, categoriesData, ledgersData, settingsData, budgetsData, streakData, rangeTxData] = await Promise.all([
|
const [accountsData, recentTxData, categoriesData, ledgersData, settingsData, budgetsData, streakData, rangeTxData] = await Promise.all([
|
||||||
@@ -136,13 +127,17 @@ function Home() {
|
|||||||
|
|
||||||
// Last 7 Days Array (for chart)
|
// Last 7 Days Array (for chart)
|
||||||
const last7DaysSpendArray = [];
|
const last7DaysSpendArray = [];
|
||||||
|
const weekTxList: Transaction[] = [];
|
||||||
|
|
||||||
for (let i = 6; i >= 0; i--) {
|
for (let i = 6; i >= 0; i--) {
|
||||||
const d = new Date(today);
|
const d = new Date(today);
|
||||||
d.setDate(d.getDate() - i);
|
d.setDate(d.getDate() - i);
|
||||||
const dayTx = allRangeTx.filter(t => isSameDay(new Date(t.transactionDate), d));
|
const dayTx = allRangeTx.filter(t => isSameDay(new Date(t.transactionDate), d));
|
||||||
last7DaysSpendArray.push(calculateTotalExpense(dayTx));
|
last7DaysSpendArray.push(calculateTotalExpense(dayTx));
|
||||||
|
weekTxList.push(...dayTx);
|
||||||
}
|
}
|
||||||
setLast7DaysSpend(last7DaysSpendArray);
|
setLast7DaysSpend(last7DaysSpendArray);
|
||||||
|
setWeekTransactions(weekTxList);
|
||||||
|
|
||||||
// Monthly Budget Stats
|
// Monthly Budget Stats
|
||||||
let mTotal = 0;
|
let mTotal = 0;
|
||||||
@@ -168,20 +163,17 @@ function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { maxTransaction, topCategory, top3Categories, todayTransactionCount, avgDailySpend } = useMemo(() => {
|
const { maxTransaction, topCategory, top3Categories, todayTransactionCount, avgDailySpend } = useMemo(() => {
|
||||||
if (todayTransactions.length === 0) return {
|
// Basic stats
|
||||||
maxTransaction: undefined,
|
const todayCount = todayTransactions.length;
|
||||||
topCategory: undefined,
|
|
||||||
top3Categories: undefined,
|
|
||||||
todayTransactionCount: 0,
|
|
||||||
avgDailySpend: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Max Transaction
|
// Max Transaction (Keep as Today's max for immediate context)
|
||||||
const maxTx = [...todayTransactions].sort((a, b) => b.amount - a.amount)[0];
|
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> = {};
|
const catMap: Record<number, number> = {};
|
||||||
todayTransactions.forEach((t) => {
|
sourceTransactions.forEach((t) => {
|
||||||
catMap[t.categoryId] = (catMap[t.categoryId] || 0) + t.amount;
|
catMap[t.categoryId] = (catMap[t.categoryId] || 0) + t.amount;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -210,10 +202,10 @@ function Home() {
|
|||||||
maxTransaction: maxTx,
|
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,
|
top3Categories: top3.length > 0 ? top3 : undefined,
|
||||||
todayTransactionCount: todayTransactions.length,
|
todayTransactionCount: todayCount,
|
||||||
avgDailySpend: avgDaily
|
avgDailySpend: avgDaily
|
||||||
};
|
};
|
||||||
}, [todayTransactions, categories, monthlyBudgetSpentTotal]);
|
}, [todayTransactions, weekTransactions, categories, monthlyBudgetSpentTotal]);
|
||||||
|
|
||||||
const handleQuickTransaction = () => {
|
const handleQuickTransaction = () => {
|
||||||
navigate('/transactions?action=new');
|
navigate('/transactions?action=new');
|
||||||
|
|||||||
Reference in New Issue
Block a user