163 lines
6.7 KiB
TypeScript
163 lines
6.7 KiB
TypeScript
|
|
/**
|
||
|
|
* HealthScoreModal Component
|
||
|
|
* Displays detailed financial health analysis
|
||
|
|
* Phase 3 Requirement: Emotional interface & Smart feedback
|
||
|
|
*/
|
||
|
|
|
||
|
|
import React, { useEffect, useState } from 'react';
|
||
|
|
import { Icon } from '@iconify/react';
|
||
|
|
import { formatCurrency } from '../../../utils/format';
|
||
|
|
import './HealthScoreModal.css';
|
||
|
|
|
||
|
|
interface HealthScoreModalProps {
|
||
|
|
isOpen: boolean;
|
||
|
|
onClose: () => void;
|
||
|
|
score: number;
|
||
|
|
totalAssets: number;
|
||
|
|
totalLiabilities: number;
|
||
|
|
todaySpend: number;
|
||
|
|
yesterdaySpend: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const HealthScoreModal: React.FC<HealthScoreModalProps> = ({
|
||
|
|
isOpen,
|
||
|
|
onClose,
|
||
|
|
score,
|
||
|
|
totalAssets,
|
||
|
|
totalLiabilities,
|
||
|
|
todaySpend,
|
||
|
|
yesterdaySpend,
|
||
|
|
}) => {
|
||
|
|
const [animate, setAnimate] = useState(false);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (isOpen) {
|
||
|
|
setTimeout(() => setAnimate(true), 100);
|
||
|
|
} else {
|
||
|
|
setAnimate(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';
|
||
|
|
|
||
|
|
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' };
|
||
|
|
};
|
||
|
|
|
||
|
|
const level = getLevel(score);
|
||
|
|
|
||
|
|
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>
|
||
|
|
</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>
|
||
|
|
</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>
|
||
|
|
<span className={`metric-trend ${spendTrend}`}>
|
||
|
|
{spendTrend === 'up' ? '比昨天多' : '比昨天少'} {formatCurrency(Math.abs(spendDiff))}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</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}>
|
||
|
|
明白了
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|