feat: 新增首页,并集成财务健康评分功能、相关组件和计算逻辑
This commit is contained in:
@@ -6,7 +6,6 @@
|
|||||||
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 {
|
||||||
@@ -24,6 +23,12 @@ interface HealthScoreModalProps {
|
|||||||
activity: number;
|
activity: number;
|
||||||
};
|
};
|
||||||
streakDays: number;
|
streakDays: number;
|
||||||
|
metrics: {
|
||||||
|
debtRatio: number;
|
||||||
|
survivalMonths: number;
|
||||||
|
budgetHealth: number;
|
||||||
|
};
|
||||||
|
tips: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HealthScoreModal: React.FC<HealthScoreModalProps> = ({
|
export const HealthScoreModal: React.FC<HealthScoreModalProps> = ({
|
||||||
@@ -32,53 +37,34 @@ export const HealthScoreModal: React.FC<HealthScoreModalProps> = ({
|
|||||||
score,
|
score,
|
||||||
totalAssets,
|
totalAssets,
|
||||||
totalLiabilities,
|
totalLiabilities,
|
||||||
todaySpend,
|
// todaySpend, // Unused
|
||||||
yesterdaySpend,
|
// yesterdaySpend, // Unused
|
||||||
breakdown,
|
breakdown,
|
||||||
streakDays,
|
streakDays,
|
||||||
|
metrics,
|
||||||
|
tips
|
||||||
}) => {
|
}) => {
|
||||||
const [showRules, setShowRules] = useState(false);
|
const [showRules, setShowRules] = useState(false);
|
||||||
const [animate, setAnimate] = useState(false);
|
const [animate, setAnimate] = useState(false);
|
||||||
const [aiAdvice, setAiAdvice] = useState<string>('');
|
// AI Advice state can be removed if strictly using tips, or kept as fallback
|
||||||
const [loadingAdvice, setLoadingAdvice] = useState(false);
|
|
||||||
|
|
||||||
// --- Logic & Calculations ---
|
// --- Logic & Calculations ---
|
||||||
|
|
||||||
const debtRatio = totalAssets > 0 ? (totalLiabilities / totalAssets) * 100 : 0;
|
const debtRatio = totalAssets > 0 ? (totalLiabilities / totalAssets) * 100 : 0;
|
||||||
const spendDiff = todaySpend - yesterdaySpend;
|
// 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) {
|
|
||||||
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 {
|
} else {
|
||||||
setAnimate(false);
|
setAnimate(false);
|
||||||
setShowRules(false); // Reset view on close
|
setShowRules(false); // Reset view on close
|
||||||
}
|
}
|
||||||
return () => { isMounted = false; };
|
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const aiAdvice = ''; // Fallback placeholder if needed
|
||||||
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
// Status Levels
|
// Status Levels
|
||||||
@@ -135,8 +121,8 @@ export const HealthScoreModal: React.FC<HealthScoreModalProps> = ({
|
|||||||
<div className="rule-item">
|
<div className="rule-item">
|
||||||
<div className="rule-score rule-score--blue">{breakdown.solvency}/40</div>
|
<div className="rule-score rule-score--blue">{breakdown.solvency}/40</div>
|
||||||
<div className="rule-desc-group">
|
<div className="rule-desc-group">
|
||||||
<h4>偿债能力 (40分)</h4>
|
<h4>财务结构 (40分)</h4>
|
||||||
<p>核心指标。计算公式:1 - (总负债 / 总资产)。负债率越低,得分越高。<br />{debtRatio > 50 ? '当前负债率过高,严重影响得分。' : '当前资产结构健康。'}</p>
|
<p>由“负债率”与“应急流动性”共同决定。不仅关注负债水平,更看重现金流能否支撑至少3-6个月的生活开支。<br />这是抵御风险的基石。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -227,17 +213,20 @@ export const HealthScoreModal: React.FC<HealthScoreModalProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Spending Card */}
|
{/* Survival Time Card (Replacing Spend) */}
|
||||||
<div className="health-metric-card">
|
<div className="health-metric-card">
|
||||||
<div className="metric-icon-wrapper" style={{ background: 'rgba(245, 158, 11, 0.15)' }}>
|
<div className="metric-icon-wrapper" style={{ background: 'rgba(16, 185, 129, 0.15)' }}>
|
||||||
<Icon icon="solar:wallet-money-bold-duotone" width="20" color="#f59e0b" />
|
<Icon icon="solar:shield-check-bold-duotone" width="20" color="#10b981" />
|
||||||
</div>
|
</div>
|
||||||
<span className="metric-title">今日消费</span>
|
<span className="metric-title">生存期</span>
|
||||||
<div className="metric-value-group">
|
<div className="metric-value-group">
|
||||||
<span className="metric-number">{formatCurrency(todaySpend)}</span>
|
<span className="metric-number">
|
||||||
|
{metrics?.survivalMonths > 99 ? '>99' : (metrics?.survivalMonths || 0).toFixed(1)}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: '1rem', fontWeight: 600, marginLeft: '2px' }}>个月</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="metric-subtitle" style={{ color: spendDiff > 0 ? '#ef4444' : '#10b981' }}>
|
<span className="metric-subtitle" style={{ color: (metrics?.survivalMonths || 0) < 3 ? '#ef4444' : '#10b981' }}>
|
||||||
比昨日 {spendDiff > 0 ? '多' : '少'} {formatCurrency(Math.abs(spendDiff))}
|
{(metrics?.survivalMonths || 0) >= 6 ? '资金充裕' : (metrics?.survivalMonths || 0) >= 3 ? '健康' : '急需储备'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,15 +236,16 @@ export const HealthScoreModal: React.FC<HealthScoreModalProps> = ({
|
|||||||
<div className="suggestion-header">
|
<div className="suggestion-header">
|
||||||
<Icon icon="solar:lightbulb-bolt-bold-duotone" width="20" />
|
<Icon icon="solar:lightbulb-bolt-bold-duotone" width="20" />
|
||||||
<span>AI 智能建议</span>
|
<span>AI 智能建议</span>
|
||||||
{loadingAdvice && <span style={{ fontSize: '0.8rem', opacity: 0.7, marginLeft: 'auto' }}>思考中...</span>}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="suggestion-content">
|
<div className="suggestion-content">
|
||||||
{loadingAdvice ? (
|
{tips && tips.length > 0 ? (
|
||||||
<span className="loading-dots">正在生成个性化理财建议...</span>
|
tips.map((tip, i) => (
|
||||||
|
<div key={i} style={{ marginBottom: '8px', fontSize: '0.9rem' }}>{tip}</div>
|
||||||
|
))
|
||||||
) : (
|
) : (
|
||||||
aiAdvice || getStaticSuggestion()
|
<p>{aiAdvice || getStaticSuggestion()}</p>
|
||||||
)}
|
)}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -257,7 +257,8 @@ function Home() {
|
|||||||
monthlyBudget: monthlyBudgetTotal,
|
monthlyBudget: monthlyBudgetTotal,
|
||||||
monthlySpent: monthlyBudgetSpentTotal,
|
monthlySpent: monthlyBudgetSpentTotal,
|
||||||
streakDays: streakInfo?.currentStreak || 0,
|
streakDays: streakInfo?.currentStreak || 0,
|
||||||
hasRecentActivity: todaySpend > 0 || yesterdaySpend > 0
|
hasRecentActivity: todaySpend > 0 || yesterdaySpend > 0,
|
||||||
|
accounts: accounts || []
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const healthStatus = calculateHealthScore();
|
const healthStatus = calculateHealthScore();
|
||||||
@@ -430,6 +431,8 @@ function Home() {
|
|||||||
yesterdaySpend={yesterdaySpend}
|
yesterdaySpend={yesterdaySpend}
|
||||||
breakdown={healthStatus.breakdown}
|
breakdown={healthStatus.breakdown}
|
||||||
streakDays={streakInfo?.currentStreak || 0}
|
streakDays={streakInfo?.currentStreak || 0}
|
||||||
|
metrics={healthStatus.metrics}
|
||||||
|
tips={healthStatus.tips}
|
||||||
/>
|
/>
|
||||||
<button className="fab-voice-btn" onClick={handleVoiceBookkeeping} title="语音记账">
|
<button className="fab-voice-btn" onClick={handleVoiceBookkeeping} title="语音记账">
|
||||||
<Icon icon="solar:microphone-bold-duotone" width="28" />
|
<Icon icon="solar:microphone-bold-duotone" width="28" />
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
* 4. Recent Activity (10%): Engagement
|
* 4. Recent Activity (10%): Engagement
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { Account } from '../types';
|
||||||
|
|
||||||
interface HealthScoreDetails {
|
interface HealthScoreDetails {
|
||||||
totalAssets: number;
|
totalAssets: number;
|
||||||
totalLiabilities: number;
|
totalLiabilities: number;
|
||||||
@@ -15,6 +17,7 @@ interface HealthScoreDetails {
|
|||||||
monthlySpent: number;
|
monthlySpent: number;
|
||||||
streakDays: number;
|
streakDays: number;
|
||||||
hasRecentActivity: boolean; // Spent today or yesterday
|
hasRecentActivity: boolean; // Spent today or yesterday
|
||||||
|
accounts: Account[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FinancialStatus {
|
interface FinancialStatus {
|
||||||
@@ -28,6 +31,12 @@ interface FinancialStatus {
|
|||||||
habit: number;
|
habit: number;
|
||||||
activity: number;
|
activity: number;
|
||||||
};
|
};
|
||||||
|
metrics: {
|
||||||
|
debtRatio: number;
|
||||||
|
survivalMonths: number;
|
||||||
|
budgetHealth: number; // -1 (Over), 0 (On Track), 1 (Saving)
|
||||||
|
};
|
||||||
|
tips: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const calculateFinancialHealth = (data: HealthScoreDetails): FinancialStatus => {
|
export const calculateFinancialHealth = (data: HealthScoreDetails): FinancialStatus => {
|
||||||
@@ -36,52 +45,91 @@ export const calculateFinancialHealth = (data: HealthScoreDetails): FinancialSta
|
|||||||
let habitScore = 0;
|
let habitScore = 0;
|
||||||
let activityScore = 0;
|
let activityScore = 0;
|
||||||
|
|
||||||
// 1. Solvency (Max 40)
|
// --- 1. SOLVENCY (Max 40) ---
|
||||||
|
// A. Debt Ratio (20 pts)
|
||||||
|
let debtScore = 0;
|
||||||
|
let debtRatio = 0;
|
||||||
if (data.totalAssets > 0) {
|
if (data.totalAssets > 0) {
|
||||||
const debtRatio = data.totalLiabilities / data.totalAssets;
|
debtRatio = data.totalLiabilities / data.totalAssets;
|
||||||
if (debtRatio === 0) solvencyScore = 40;
|
if (debtRatio === 0) debtScore = 20;
|
||||||
else if (debtRatio < 0.3) solvencyScore = 35; // < 30% debt
|
else if (debtRatio < 0.3) debtScore = 18;
|
||||||
else if (debtRatio < 0.5) solvencyScore = 25; // < 50% debt
|
else if (debtRatio < 0.5) debtScore = 12;
|
||||||
else if (debtRatio < 0.8) solvencyScore = 10; // < 80% debt
|
else if (debtRatio < 0.8) debtScore = 5;
|
||||||
else solvencyScore = 0; // High debt
|
else debtScore = 0;
|
||||||
} else {
|
} else {
|
||||||
// No assets
|
debtScore = data.totalLiabilities === 0 ? 10 : 0;
|
||||||
solvencyScore = data.totalLiabilities === 0 ? 20 : 0; // Blank slate is better than debt only
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Budget Adherence (Max 30)
|
// B. Liquidity / Runway (20 pts)
|
||||||
|
// Liquid Assets: Cash, Debit Card, E-Wallet
|
||||||
|
const liquidAssets = data.accounts
|
||||||
|
.filter(a => ['cash', 'debit_card', 'e_wallet'].includes(a.type))
|
||||||
|
.reduce((sum, a) => sum + (a.balance > 0 ? a.balance : 0), 0);
|
||||||
|
|
||||||
|
let liquidityScore = 0;
|
||||||
|
// Estimate monthly burn: Use Budget if set, otherwise assume Spent is typical (use fallback min 2000 to avoid divide by zero)
|
||||||
|
const monthlyBurn = data.monthlyBudget > 0 ? data.monthlyBudget : Math.max(data.monthlySpent, 2000);
|
||||||
|
const monthsRunway = liquidAssets / monthlyBurn;
|
||||||
|
|
||||||
|
if (monthsRunway >= 6) liquidityScore = 20; // Excellent safety net
|
||||||
|
else if (monthsRunway >= 3) liquidityScore = 15; // Healthy
|
||||||
|
else if (monthsRunway >= 1) liquidityScore = 8; // Basic suvival
|
||||||
|
else liquidityScore = 0; // Living paycheck to paycheck
|
||||||
|
|
||||||
|
solvencyScore = Math.round(debtScore + liquidityScore);
|
||||||
|
|
||||||
|
|
||||||
|
// --- 2. BUDGET CONTROL (Max 30) ---
|
||||||
|
// A. Adherence (20 pts) - Tracking against time
|
||||||
if (data.monthlyBudget > 0) {
|
if (data.monthlyBudget > 0) {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const dayOfMonth = today.getDate();
|
const dayOfMonth = today.getDate();
|
||||||
const daysInMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
|
const daysInMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
|
||||||
const progress = dayOfMonth / daysInMonth; // 0.1 to 1.0
|
const timeProgress = dayOfMonth / daysInMonth;
|
||||||
|
const budgetUsage = data.monthlySpent / data.monthlyBudget;
|
||||||
|
|
||||||
// Ideal usage matches time progress
|
// Allowed deviation: +/- 10% is fine.
|
||||||
// Example: Day 15/30 (50%), Spent 50% -> Good. Spent 80% -> Bad.
|
// Usage should be roughly equal to TimeProgress.
|
||||||
const usage = data.monthlySpent / data.monthlyBudget;
|
// If Usage << TimeProgress, it's SAVINGS (Bonus).
|
||||||
|
// If Usage >> TimeProgress, it's DANGER (Penalty).
|
||||||
|
|
||||||
if (usage <= progress) budgetScore = 30; // Under spending curve
|
const diff = budgetUsage - timeProgress;
|
||||||
else if (usage <= progress * 1.2) budgetScore = 20; // Slightly over (+20%)
|
|
||||||
else if (usage <= progress * 1.5) budgetScore = 10; // Over (+50%)
|
if (diff <= 0) budgetScore += 20; // Spending less than time passed -> Great
|
||||||
else budgetScore = 0; // Severely over
|
else if (diff <= 0.1) budgetScore += 15; // Slightly over (+10% ahead of schedule)
|
||||||
|
else if (diff <= 0.2) budgetScore += 10; // Moderately over (+20%)
|
||||||
|
else if (diff <= 0.3) budgetScore += 5;
|
||||||
|
else budgetScore += 0;
|
||||||
|
|
||||||
|
// B. Savings Bonus (Max 10 pts)
|
||||||
|
// If usage is significantly lower than time progress (e.g. usage is 50% of time), reward saving behavior
|
||||||
|
if (budgetUsage < timeProgress * 0.8) budgetScore += 10;
|
||||||
|
else if (budgetUsage < timeProgress * 0.9) budgetScore += 5;
|
||||||
|
|
||||||
|
// Cap Budget score at 30
|
||||||
|
budgetScore = Math.min(budgetScore, 30);
|
||||||
|
|
||||||
|
// Hard fail: Over 100% budget is 0 points regardless
|
||||||
|
if (budgetUsage > 1.0) budgetScore = 0;
|
||||||
|
|
||||||
// Hard cap: If total over 100%, score 0 regardless of day
|
|
||||||
if (usage > 1) budgetScore = 0;
|
|
||||||
} else {
|
} else {
|
||||||
budgetScore = 15; // Neutral if no budget set
|
// No Budget Set - Neutral Score
|
||||||
|
budgetScore = 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Habit Consistency (Max 20)
|
// --- 3. HABIT CONSISTENCY (Max 20) ---
|
||||||
if (data.streakDays >= 30) habitScore = 20;
|
// Curved grading for streaks
|
||||||
|
if (data.streakDays >= 21) habitScore = 20; // 3 weeks -> Habit formed
|
||||||
else if (data.streakDays >= 14) habitScore = 15;
|
else if (data.streakDays >= 14) habitScore = 15;
|
||||||
else if (data.streakDays >= 7) habitScore = 10;
|
else if (data.streakDays >= 7) habitScore = 10;
|
||||||
else if (data.streakDays >= 3) habitScore = 5;
|
else if (data.streakDays >= 3) habitScore = 5;
|
||||||
else habitScore = 0;
|
else habitScore = 0;
|
||||||
|
|
||||||
// 4. Activity (Max 10)
|
// --- 4. RECENT ACTIVITY (Max 10) ---
|
||||||
if (data.hasRecentActivity) activityScore = 10;
|
if (data.hasRecentActivity) activityScore = 10;
|
||||||
|
|
||||||
const totalScore = Math.round(solvencyScore + budgetScore + habitScore + activityScore);
|
// --- TOTAL ---
|
||||||
|
const totalScore = Math.min(Math.round(solvencyScore + budgetScore + habitScore + activityScore), 100);
|
||||||
|
|
||||||
// Determine Status
|
// Determine Status
|
||||||
let label = '';
|
let label = '';
|
||||||
@@ -90,24 +138,66 @@ export const calculateFinancialHealth = (data: HealthScoreDetails): FinancialSta
|
|||||||
|
|
||||||
if (totalScore >= 90) {
|
if (totalScore >= 90) {
|
||||||
label = '财务卓越';
|
label = '财务卓越';
|
||||||
color = '#10B981'; // Emerald 500
|
color = '#10B981';
|
||||||
description = '资产极其健康,习惯优秀';
|
description = '资产结构完美,抗风险能力极强';
|
||||||
} else if (totalScore >= 75) {
|
} else if (totalScore >= 75) {
|
||||||
label = '非常健康';
|
label = '非常健康';
|
||||||
color = '#3B82F6'; // Blue 500
|
color = '#3B82F6';
|
||||||
description = '财务状况良好,继续保持';
|
description = '现金流充足,债务控制良好';
|
||||||
} else if (totalScore >= 60) {
|
} else if (totalScore >= 60) {
|
||||||
label = '亚健康';
|
label = '亚健康';
|
||||||
color = '#F59E0B'; // Amber 500
|
color = '#F59E0B';
|
||||||
description = '存在潜在风险,需注意收支';
|
description = '需关注应急资金储备与消费控制';
|
||||||
} else if (totalScore >= 40) {
|
} else if (totalScore >= 40) {
|
||||||
label = '脆弱';
|
label = '脆弱';
|
||||||
color = '#F97316'; // Orange 500
|
color = '#F97316';
|
||||||
description = '抗风险能力弱,建议储蓄';
|
description = '高风险!建议优先建立应急储备金';
|
||||||
} else {
|
} else {
|
||||||
label = '危机';
|
label = '危机';
|
||||||
color = '#EF4444'; // Red 500
|
color = '#EF4444';
|
||||||
description = '财务状况堪忧,急需调整';
|
description = '严重的财务风险,请立即停止非必要支出';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TIPS GENERATION ---
|
||||||
|
const tips: string[] = [];
|
||||||
|
|
||||||
|
// Solvency Tips
|
||||||
|
if (monthsRunway < 3) {
|
||||||
|
tips.push(`⚠️ 应急资金不足 (仅够维持 ${monthsRunway.toFixed(1)} 个月)。建议储备至少 3-6 个月的生活费。`);
|
||||||
|
} else if (monthsRunway > 12) {
|
||||||
|
tips.push(`💡 现金储备充足 (${monthsRunway.toFixed(1)} 个月)。建议将多余资金进行投资以跑赢通胀。`);
|
||||||
|
} else {
|
||||||
|
tips.push(`✅ 应急储备健康 (${monthsRunway.toFixed(1)} 个月)。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debtRatio > 0.5) {
|
||||||
|
tips.push(`⚠️ 负债率偏高 (${(debtRatio * 100).toFixed(0)}%)。请优先偿还高息债务。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Budget Tips
|
||||||
|
let budgetHealth = 0;
|
||||||
|
if (data.monthlyBudget > 0) {
|
||||||
|
const today = new Date();
|
||||||
|
const daysInMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
|
||||||
|
const timeProgress = today.getDate() / daysInMonth;
|
||||||
|
const budgetUsage = data.monthlySpent / data.monthlyBudget;
|
||||||
|
|
||||||
|
if (budgetUsage > timeProgress * 1.2) {
|
||||||
|
tips.push(`⚠️ 消费速度过快 (超前 ${((budgetUsage - timeProgress) * 100).toFixed(0)}%)。请控制非必要支出。`);
|
||||||
|
budgetHealth = -1;
|
||||||
|
} else if (budgetUsage < timeProgress * 0.8) {
|
||||||
|
tips.push(`👏 预算控制极佳 (结余 ${((timeProgress - budgetUsage) * 100).toFixed(0)}%)。继续保持!`);
|
||||||
|
budgetHealth = 1;
|
||||||
|
} else {
|
||||||
|
budgetHealth = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tips.push(`💡 尚未设置预算。建议设定月度预算以获得更准确的健康评估。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Habit Tips
|
||||||
|
if (data.streakDays < 3) {
|
||||||
|
tips.push(`🔥 坚持记账是理财的第一步。加油!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -120,6 +210,12 @@ export const calculateFinancialHealth = (data: HealthScoreDetails): FinancialSta
|
|||||||
budget: budgetScore,
|
budget: budgetScore,
|
||||||
habit: habitScore,
|
habit: habitScore,
|
||||||
activity: activityScore
|
activity: activityScore
|
||||||
}
|
},
|
||||||
|
metrics: {
|
||||||
|
debtRatio,
|
||||||
|
survivalMonths: monthsRunway,
|
||||||
|
budgetHealth
|
||||||
|
},
|
||||||
|
tips
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user