feat: 新增了周期性交易列表、账户卡片、分类选择器等多个核心组件和页面,并引入了AI服务。
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* HealthScoreModal Component
|
||||
* Displays detailed financial health analysis
|
||||
* Phase 3 Requirement: Emotional interface & Smart feedback
|
||||
* Displays detailed financial health analysis with premium glassmorphism design
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { formatCurrency } from '../../../utils/format';
|
||||
import { getFinancialAdvice } from '../../../services/aiService';
|
||||
import './HealthScoreModal.css';
|
||||
|
||||
interface HealthScoreModalProps {
|
||||
@@ -28,132 +28,233 @@ export const HealthScoreModal: React.FC<HealthScoreModalProps> = ({
|
||||
todaySpend,
|
||||
yesterdaySpend,
|
||||
}) => {
|
||||
const [showRules, setShowRules] = useState(false);
|
||||
const [animate, setAnimate] = useState(false);
|
||||
const [aiAdvice, setAiAdvice] = useState<string>('');
|
||||
const [loadingAdvice, setLoadingAdvice] = useState(false);
|
||||
|
||||
// --- Logic & Calculations ---
|
||||
const netAssets = totalAssets - totalLiabilities;
|
||||
const debtRatio = totalAssets > 0 ? (totalLiabilities / totalAssets) * 100 : 0;
|
||||
const spendDiff = todaySpend - yesterdaySpend;
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
if (isOpen) {
|
||||
setTimeout(() => setAnimate(true), 100);
|
||||
setTimeout(() => setAnimate(true), 50);
|
||||
|
||||
// Fetch AI Advice if not already fetched
|
||||
if (!aiAdvice) {
|
||||
setLoadingAdvice(true);
|
||||
getFinancialAdvice({
|
||||
score,
|
||||
totalAssets,
|
||||
totalLiabilities,
|
||||
todaySpend,
|
||||
yesterdaySpend
|
||||
})
|
||||
.then(advice => {
|
||||
if (isMounted) setAiAdvice(advice);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to load AI advice", err);
|
||||
})
|
||||
.finally(() => {
|
||||
if (isMounted) setLoadingAdvice(false);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setAnimate(false);
|
||||
setShowRules(false); // Reset view on close
|
||||
}
|
||||
return () => { isMounted = false; };
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
// Analysis Logic
|
||||
const debtRatio = totalAssets > 0 ? (totalLiabilities / totalAssets) * 100 : 0;
|
||||
let debtLevel = '优秀';
|
||||
let debtColor = 'var(--color-success)';
|
||||
if (debtRatio > 30) {
|
||||
debtLevel = '一般';
|
||||
debtColor = 'var(--color-warning)';
|
||||
}
|
||||
if (debtRatio > 60) {
|
||||
debtLevel = '危险';
|
||||
debtColor = 'var(--color-error)';
|
||||
}
|
||||
|
||||
const spendDiff = todaySpend - yesterdaySpend;
|
||||
const spendTrend = spendDiff > 0 ? 'up' : 'down';
|
||||
|
||||
// Status Levels
|
||||
const getLevel = (s: number) => {
|
||||
if (s >= 90) return { label: '卓越', color: '#10b981', icon: 'solar:cup-star-bold-duotone' };
|
||||
if (s >= 80) return { label: '优秀', color: '#3b82f6', icon: 'solar:medal-star-bold-duotone' };
|
||||
if (s >= 60) return { label: '良好', color: '#f59e0b', icon: 'solar:check-circle-bold-duotone' };
|
||||
return { label: '需努力', color: '#ef4444', icon: 'solar:danger-circle-bold-duotone' };
|
||||
if (s >= 95) return { label: 'SSS', title: '财务自由', color: '#10b981', icon: 'solar:crown-star-bold-duotone', desc: '完美无瑕的资产结构' };
|
||||
if (s >= 90) return { label: 'S', title: '卓越', color: '#34d399', icon: 'solar:cup-star-bold-duotone', desc: '财务状况极佳' };
|
||||
if (s >= 80) return { label: 'A', title: '优秀', color: '#3b82f6', icon: 'solar:medal-star-bold-duotone', desc: '资产配置健康' };
|
||||
if (s >= 70) return { label: 'B', title: '良好', color: '#6366f1', icon: 'solar:check-circle-bold-duotone', desc: '继续保持记账习惯' };
|
||||
if (s >= 60) return { label: 'C', title: '及格', color: '#f59e0b', icon: 'solar:info-circle-bold-duotone', desc: '需注意控制负债' };
|
||||
return { label: 'D', title: '危险', color: '#ef4444', icon: 'solar:danger-circle-bold-duotone', desc: '建议立即调整开支' };
|
||||
};
|
||||
|
||||
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 ---
|
||||
|
||||
return (
|
||||
<div className="health-modal-overlay" onClick={onClose}>
|
||||
<div
|
||||
className={`health-modal-content ${animate ? 'animate-in' : ''}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button className="health-modal-close" onClick={onClose}>
|
||||
<Icon icon="solar:close-circle-bold-duotone" width="24" />
|
||||
</button>
|
||||
|
||||
<div className="health-modal-header">
|
||||
<div className="health-score-ring-large">
|
||||
<svg viewBox="0 0 100 100">
|
||||
<circle
|
||||
cx="50" cy="50" r="45"
|
||||
fill="none"
|
||||
stroke="var(--bg-tertiary)"
|
||||
strokeWidth="8"
|
||||
/>
|
||||
<circle
|
||||
cx="50" cy="50" r="45"
|
||||
fill="none"
|
||||
stroke={level.color}
|
||||
strokeWidth="8"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={`${283 * (score / 100)} 283`}
|
||||
transform="rotate(-90 50 50)"
|
||||
className="health-ring-progress"
|
||||
/>
|
||||
</svg>
|
||||
<div className="health-score-value-container">
|
||||
<span className="health-score-value" style={{ color: level.color }}>{score}</span>
|
||||
<span className="health-score-label">健康分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="health-level-badge" style={{ backgroundColor: `${level.color}20`, color: level.color }}>
|
||||
<Icon icon={level.icon} width="20" />
|
||||
<span>{level.label}状态</span>
|
||||
</div>
|
||||
{/* Header Controls */}
|
||||
<div className="health-modal-controls">
|
||||
<button
|
||||
className="health-icon-btn"
|
||||
onClick={() => setShowRules(!showRules)}
|
||||
title={showRules ? "返回评分" : "查看规则"}
|
||||
>
|
||||
<Icon icon={showRules ? "solar:arrow-left-bold-duotone" : "solar:question-circle-bold-duotone"} width="20" />
|
||||
</button>
|
||||
<button className="health-icon-btn" onClick={onClose}>
|
||||
<Icon icon="solar:close-circle-bold-duotone" width="20" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="health-metrics-grid">
|
||||
<div className="health-metric-card">
|
||||
<div className="metric-icon debt">
|
||||
<Icon icon="solar:card-2-bold-duotone" width="24" />
|
||||
</div>
|
||||
<div className="metric-info">
|
||||
<span className="metric-label">负债率</span>
|
||||
<div className="metric-value-row">
|
||||
<span className="metric-value">{debtRatio.toFixed(1)}%</span>
|
||||
<span className="metric-status" style={{ color: debtColor }}>{debtLevel}</span>
|
||||
<div className="health-scroll-container">
|
||||
{showRules ? (
|
||||
// --- Rules View ---
|
||||
<div className="health-rules-panel">
|
||||
<h3 style={{ fontSize: '1.25rem', fontWeight: 700, margin: '0 0 1.5rem', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Icon icon="solar:ruler-pen-bold-duotone" className="text-primary" />
|
||||
评分规则说明
|
||||
</h3>
|
||||
|
||||
<div className="rule-item">
|
||||
<div className="rule-score">40+</div>
|
||||
<div className="rule-desc-group">
|
||||
<h4>基础资产分</h4>
|
||||
<p>基于净资产率 (净资产/总资产) 计算。这是健康分的基石,资产越多负债越少,得分越高。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rule-item">
|
||||
<div className="rule-score">+5</div>
|
||||
<div className="rule-desc-group">
|
||||
<h4>连续记账奖励</h4>
|
||||
<p>连续记账超过 3 天,奖励坚持好习惯。记账是理财的第一步。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rule-item">
|
||||
<div className="rule-score">+5</div>
|
||||
<div className="rule-desc-group">
|
||||
<h4>活跃度奖励</h4>
|
||||
<p>近两天内有记账行为。保持对财务状况的关注有助于及时调整策略。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rule-item">
|
||||
<div className="rule-score">Lv</div>
|
||||
<div className="rule-desc-group">
|
||||
<h4>等级划分</h4>
|
||||
<p>SSS (95+), S (90-94), A (80-89), B (70-79), C (60-69), D (60以下)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// --- Score View ---
|
||||
<>
|
||||
<div className="health-score-section">
|
||||
<div className="health-score-ring-container">
|
||||
<svg className="health-score-ring-svg" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<linearGradient id="scoreGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="#4ade80" />
|
||||
<stop offset="100%" stopColor="#22c55e" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* Background Ring */}
|
||||
<circle
|
||||
cx="50" cy="50" r="45"
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
strokeWidth="8"
|
||||
/>
|
||||
{/* Progress Ring */}
|
||||
<circle
|
||||
cx="50" cy="50" r="45"
|
||||
fill="none"
|
||||
stroke={level.color} /* Use level color or gradient */
|
||||
strokeWidth="8"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={`${283 * (score / 100)} 283`}
|
||||
className="health-ring-progress"
|
||||
/>
|
||||
</svg>
|
||||
<div className="health-score-content">
|
||||
<span className="health-score-number">{score}</span>
|
||||
<span className="health-score-label">健康分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="health-metric-card">
|
||||
<div className="metric-icon spend">
|
||||
<Icon icon="solar:wallet-money-bold-duotone" width="24" />
|
||||
</div>
|
||||
<div className="metric-info">
|
||||
<span className="metric-label">今日消费</span>
|
||||
<div className="metric-value-row">
|
||||
<span className="metric-value">{formatCurrency(todaySpend)}</span>
|
||||
<div className="health-level-badge" style={{ borderColor: `${level.color}40`, background: `${level.color}15` }}>
|
||||
<Icon icon={level.icon} width="20" color={level.color} />
|
||||
<span style={{ color: level.color }}>{level.title} · {level.desc}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`metric-trend ${spendTrend}`}>
|
||||
{spendTrend === 'up' ? '比昨天多' : '比昨天少'} {formatCurrency(Math.abs(spendDiff))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="health-metrics-grid">
|
||||
{/* Debt Card */}
|
||||
<div className="health-metric-card">
|
||||
<div className="metric-icon-wrapper" style={{ background: 'rgba(239, 68, 68, 0.15)' }}>
|
||||
<Icon icon="solar:card-2-bold-duotone" width="20" color="#ef4444" />
|
||||
</div>
|
||||
<span className="metric-title">负债率</span>
|
||||
<div className="metric-value-group">
|
||||
<span className="metric-number">{debtRatio.toFixed(1)}%</span>
|
||||
<span className="metric-tag" style={{
|
||||
backgroundColor: debtRatio > 30 ? 'rgba(239, 68, 68, 0.2)' : 'rgba(16, 185, 129, 0.2)',
|
||||
color: debtRatio > 30 ? '#ef4444' : '#10b981'
|
||||
}}>
|
||||
{debtRatio > 30 ? '一般' : '优秀'}
|
||||
</span>
|
||||
</div>
|
||||
<span className="metric-subtitle">
|
||||
{debtRatio === 0 ? '无负债一身轻' : `负债 ${formatCurrency(totalLiabilities)}`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Spending Card */}
|
||||
<div className="health-metric-card">
|
||||
<div className="metric-icon-wrapper" style={{ background: 'rgba(245, 158, 11, 0.15)' }}>
|
||||
<Icon icon="solar:wallet-money-bold-duotone" width="20" color="#f59e0b" />
|
||||
</div>
|
||||
<span className="metric-title">今日消费</span>
|
||||
<div className="metric-value-group">
|
||||
<span className="metric-number">{formatCurrency(todaySpend)}</span>
|
||||
</div>
|
||||
<span className="metric-subtitle" style={{ color: spendDiff > 0 ? '#ef4444' : '#10b981' }}>
|
||||
比昨日 {spendDiff > 0 ? '多' : '少'} {formatCurrency(Math.abs(spendDiff))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Suggestion Box */}
|
||||
<div className="health-suggestion-box">
|
||||
<div className="suggestion-header">
|
||||
<Icon icon="solar:lightbulb-bolt-bold-duotone" width="20" />
|
||||
<span>AI 智能建议</span>
|
||||
{loadingAdvice && <span style={{ fontSize: '0.8rem', opacity: 0.7, marginLeft: 'auto' }}>思考中...</span>}
|
||||
</div>
|
||||
<p className="suggestion-content">
|
||||
{loadingAdvice ? (
|
||||
<span className="loading-dots">正在生成个性化理财建议...</span>
|
||||
) : (
|
||||
aiAdvice || getStaticSuggestion()
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="health-suggestion-box">
|
||||
<h4 className="suggestion-title">
|
||||
<Icon icon="solar:lightbulb-bolt-bold-duotone" width="20" className="text-primary" />
|
||||
智能建议
|
||||
</h4>
|
||||
<p className="suggestion-text">
|
||||
{score >= 80
|
||||
? '您的财务状况非常健康!建议继续保持低负债率,并考虑适当增加投资比例以抵抗通胀。'
|
||||
: score >= 60
|
||||
? '财务状况良好,但还有提升空间。试着控制非必要支出,提高每月的储蓄比例。'
|
||||
: '请注意控制支出!建议优先偿还高息债务,并审视近期的消费习惯。'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="health-actions">
|
||||
<button className="health-action-btn primary" onClick={onClose}>
|
||||
明白了
|
||||
<div className="health-actions-bottom">
|
||||
<button className="health-confirm-btn" onClick={onClose}>
|
||||
{showRules ? '返回' : '明白了'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user