feat: 新增健康评分弹窗组件,提供详细财务分析及AI智能财务建议服务。

This commit is contained in:
2026-01-30 16:18:00 +08:00
parent dea24a1297
commit 9443ef39f3
2 changed files with 60 additions and 30 deletions

View File

@@ -6,6 +6,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { formatCurrency } from '../../../utils/format'; import { formatCurrency } from '../../../utils/format';
import { getFinancialAdvice } from '../../../services/aiService';
import './HealthScoreModal.css'; import './HealthScoreModal.css';
interface HealthScoreModalProps { interface HealthScoreModalProps {
@@ -46,7 +47,8 @@ export const HealthScoreModal: React.FC<HealthScoreModalProps> = ({
}) => { }) => {
const [showRules, setShowRules] = useState(false); const [showRules, setShowRules] = useState(false);
const [animate, setAnimate] = useState(false); const [animate, setAnimate] = useState(false);
// AI Advice state can be removed if strictly using tips, or kept as fallback const [aiAdvice, setAiAdvice] = useState<string>('');
const [loadingAdvice, setLoadingAdvice] = useState(false);
// --- Logic & Calculations --- // --- Logic & Calculations ---
@@ -54,15 +56,36 @@ export const HealthScoreModal: React.FC<HealthScoreModalProps> = ({
// const spendDiff = todaySpend - yesterdaySpend; // Unused now // const spendDiff = todaySpend - yesterdaySpend; // Unused now
useEffect(() => { useEffect(() => {
let isMounted = true;
if (isOpen) { if (isOpen) {
setTimeout(() => setAnimate(true), 50); setTimeout(() => setAnimate(true), 50);
// Fetch AI Advice if not already fetched
if (!aiAdvice && !loadingAdvice) { // Only fetch if not already fetched or loading
setLoadingAdvice(true);
getFinancialAdvice({
score,
totalAssets,
totalLiabilities,
metrics,
tips
})
.then(advice => {
if (isMounted) setAiAdvice(advice);
})
.catch(err => {
console.error("Failed to load AI advice", err);
})
.finally(() => {
if (isMounted) setLoadingAdvice(false);
});
}
} else { } else {
setAnimate(false); setAnimate(false);
setShowRules(false); // Reset view on close setShowRules(false); // Reset view on close
} }
}, [isOpen]); return () => { isMounted = false; };
}, [isOpen, aiAdvice, loadingAdvice, score, totalAssets, totalLiabilities, metrics, tips]);
const aiAdvice = ''; // Fallback placeholder if needed
if (!isOpen) return null; if (!isOpen) return null;
@@ -79,13 +102,7 @@ export const HealthScoreModal: React.FC<HealthScoreModalProps> = ({
const level = getLevel(score); const level = getLevel(score);
// Dynamic Suggestion (Fallback)
const getStaticSuggestion = () => {
if (debtRatio > 50) return "负债率偏高(>50%)。建议优先偿还高息债务(如信用卡),避免产生不必要的利息支出。同时请审视非必要消费。";
if (score < 60) return "建议建立强制储蓄计划,每月发工资后先存下一笔钱。同时,坚持每日记账能帮助你发现隐形浪费。";
if (score >= 90) return "您的财务状况非常健康!目前的低负债率是很好的优势。建议考虑学习理财知识,让结余资金通过稳健投资实现增值。";
return "财务状况良好。建议检查是否有闲置资金可以转入储蓄账户赚取收益,并尝试为自己设定一个年度储蓄目标。";
};
// --- Render Helpers --- // --- Render Helpers ---
@@ -238,12 +255,21 @@ export const HealthScoreModal: React.FC<HealthScoreModalProps> = ({
<span>AI </span> <span>AI </span>
</div> </div>
<div className="suggestion-content"> <div className="suggestion-content">
{tips && tips.length > 0 ? ( {/* Static Tips */}
tips.map((tip, i) => ( {tips && tips.length > 0 && tips.map((tip, i) => (
<div key={i} style={{ marginBottom: '8px', fontSize: '0.9rem' }}>{tip}</div> <div key={i} style={{ marginBottom: '8px', fontSize: '0.9rem', color: 'rgba(255,255,255,0.9)' }}>{tip}</div>
)) ))}
) : (
<p>{aiAdvice || getStaticSuggestion()}</p> {/* AI Dynamic Advice */}
{loadingAdvice ? (
<div style={{ marginTop: '12px', fontSize: '0.85rem', opacity: 0.7, fontStyle: 'italic', display: 'flex', alignItems: 'center', gap: '6px' }}>
<span className="loading-dots">CFO ...</span>
</div>
) : aiAdvice && (
<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid rgba(255,255,255,0.1)', fontSize: '0.9rem', lineHeight: '1.6', color: '#60a5fa' }}>
<Icon icon="solar:chat-round-line-duotone" style={{ marginRight: '6px', verticalAlign: 'text-bottom' }} />
{aiAdvice}
</div>
)} )}
</div> </div>
</div> </div>

View File

@@ -342,8 +342,12 @@ export async function getFinancialAdvice(context: {
totalAssets: number; totalAssets: number;
totalLiabilities: number; totalLiabilities: number;
score: number; score: number;
todaySpend: number; metrics: {
yesterdaySpend: number; debtRatio: number;
survivalMonths: number;
budgetHealth: number;
};
tips: string[];
}): Promise<string> { }): Promise<string> {
// Generate a simple hash of the context to detect data changes // Generate a simple hash of the context to detect data changes
const currentHash = JSON.stringify(context); const currentHash = JSON.stringify(context);
@@ -360,25 +364,25 @@ export async function getFinancialAdvice(context: {
// Construct a prompt for the AI // Construct a prompt for the AI
const prompt = `System: 你是全能的首席财务官 (CFO) 兼个人财富导师。 const prompt = `System: 你是全能的首席财务官 (CFO) 兼个人财富导师。
Context: 用户希望获得深度、犀利且具有前瞻性的财务洞察。不要说废话,直击痛点或爽点 Context: 用户希望获得深度、犀利且具有前瞻性的财务洞察。基于用户的健康分数据和系统预生成的 Tips给予综合评价
User Financial Data: User Financial Data:
- 综合评分: ${context.score} (S级标准: 90+, 警戒线: 60) - 综合评分: ${context.score}
- 财务结构: 负债率 ${(context.metrics.debtRatio * 100).toFixed(1)}%, 生存期 ${context.metrics.survivalMonths.toFixed(1)} 个月
- 净资产: ¥${(context.totalAssets - context.totalLiabilities).toFixed(2)} - 净资产: ¥${(context.totalAssets - context.totalLiabilities).toFixed(2)}
- 资产/负债: ¥${context.totalAssets.toFixed(2)} / ¥${context.totalLiabilities.toFixed(2)} - 预算状态: ${context.metrics.budgetHealth > 0 ? '有结余 (优秀)' : context.metrics.budgetHealth < 0 ? '超支 (警告)' : '正常'}
- 负债杠杆: ${context.totalAssets > 0 ? ((context.totalLiabilities / context.totalAssets) * 100).toFixed(1) : 0}% - 系统 Tips: ${context.tips.join('; ')}
- 短期收支: 今日支出 ¥${context.todaySpend.toFixed(2)} (对比昨日: ¥${context.yesterdaySpend.toFixed(2)})
Instruction: Instruction:
请根据上述数据,运用"第一性原理"分析用户的核心财务健康度 请根据上述数据,生成一段简短有力的财务点评
1. 如果负债率过高(>30%),给出一条关于"债务雪崩法"或"债务雪球法"的具体行动建议 1. 甚至可以引用系统 Tips 中的关键数据来加强说服力
2. 如果资产健康但支出波动大,提示"拿铁因子"风险 2. 如果生存期 < 3个月必须强调建立应急储备的重要性
3. 如果状态完美,建议关注"被动收入"或"抗通胀"配置 3. 语气要像个严厉但负责任的导师
Output Requirements: Output Requirements:
- 限制字数: 120字以内。 - 限制字数: 100字以内。
- 风格: 睿智、冷静、一针见血。 - 风格: 睿智、冷静、一针见血。
- 格式: 纯文本,适当使用Emoji (💡, 🚀, 🛡️) 作为视觉锚点`; - 格式: 纯文本,不要 markdown 标题`;
try { try {
// 2. Request from AI // 2. Request from AI