feat: 新增健康评分弹窗组件,提供详细财务分析及AI智能财务建议服务。
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user