feat: 新增预算详情模态框、交易表单、小猪罐功能,并支持生成预算洞察。
This commit is contained in:
144
src/components/home/DailyInsightCard/DailyInsightCard.tsx
Normal file
144
src/components/home/DailyInsightCard/DailyInsightCard.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
```typescript
|
||||
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;
|
||||
}
|
||||
|
||||
export const DailyInsightCard: React.FC<DailyInsightCardProps> = ({
|
||||
todaySpend,
|
||||
yesterdaySpend,
|
||||
monthlyBudget,
|
||||
monthlySpent,
|
||||
topCategory,
|
||||
maxTransaction,
|
||||
lastWeekSpend,
|
||||
streakDays
|
||||
}) => {
|
||||
const [aiData, setAiData] = useState<{spending: string; budget: 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 () => {
|
||||
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
|
||||
});
|
||||
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]);
|
||||
|
||||
const getSpendingInsight = (today: number, yesterday: number) => {
|
||||
if (aiData) return { text: <span>{aiData.spending}</span>, type: 'ai' };
|
||||
|
||||
if (today === 0) return { text: "今天还没有花钱,保持这种“零消费”状态就是最大的赚钱!", type: 'success' };
|
||||
|
||||
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' };
|
||||
|
||||
if (today < yesterday) {
|
||||
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 {
|
||||
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' };
|
||||
|
||||
const ratio = spent / total;
|
||||
const today = new Date().getDate();
|
||||
const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate();
|
||||
const timeRatio = today / daysInMonth;
|
||||
|
||||
if (ratio >= 1) return { text: <span className="daily-insight__highlight daily-insight__highlight--danger">本月预算已耗尽!接下来的每一天都需要极限生存挑战了。</span>, type: 'danger' };
|
||||
|
||||
if (ratio > timeRatio + 0.15) {
|
||||
return { text: <span>进度 <strong className="daily-insight__highlight daily-insight__highlight--warning">{Math.round(ratio*100)}%</strong> (时间 {Math.round(timeRatio*100)}%)。花钱速度有点快了,建议踩踩刹车。</span>, type: 'warning' };
|
||||
}
|
||||
|
||||
if (ratio < timeRatio - 0.1) {
|
||||
return { text: <span>进度 <strong className="daily-insight__highlight daily-insight__highlight--success">{Math.round(ratio*100)}%</strong> (时间 {Math.round(timeRatio*100)}%)。控制得非常完美,月底可能有惊喜结余!</span>, type: 'success' };
|
||||
}
|
||||
|
||||
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 (
|
||||
<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>
|
||||
{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>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
Reference in New Issue
Block a user