feat: 新增 CommandPalette 和 HealthScoreModal 组件,优化应用布局并引入 Budget、Home 页面。
This commit is contained in:
162
src/components/home/HealthScoreModal/HealthScoreModal.tsx
Normal file
162
src/components/home/HealthScoreModal/HealthScoreModal.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user