fix(ui): 暴力重构净值图表数据流,彻底对齐法币本金渲染
This commit is contained in:
parent
8b76ec9a6d
commit
89b40a72bb
@ -410,3 +410,11 @@
|
||||
- 在 `app/dashboard/page.tsx` 的 `loadSnapshots()` 函数中,删除 `new Big(summary.totalCnyValue).minus(new Big(summary.totalPnlCny))` 的反向推导代码,改为直接使用 `summary.totalCostCny` 作为快照的本金值。
|
||||
- 净盈亏在 `NetWorthChart` 的 `CustomTooltip` 中通过 `pnl = totalValue - totalCost` 顺向派生,符合"盈亏 = 市值 - 本金"的金融计算规范。
|
||||
- **验收标准**:鼠标悬浮在图表上,投入本金显示底层原汁原味的成本值(如 ¥5094.59),净盈亏自动修正为真实值(如 +¥472.12)。
|
||||
|
||||
## 暴力重构 NetWorthChart 数据绑定逻辑,添加对后端字段名 (snake_case vs camelCase) 的强力兼容,彻底消除前端 Tooltip 的旧账残影 (Task 82)
|
||||
- **根因分析**:`src/components/dashboard/net-worth-chart.tsx` 的 `CustomTooltip` 和 `chartData` 映射层仅依赖驼峰字段 (`totalCostCny`),但后端 Drizzle ORM 返回的原始数据可能包含蛇形字段 (`total_cost_cny`),导致 Tooltip 中本金显示错误值(如 704 而非真实的 5094)。
|
||||
- **强制调试日志**:在组件入口注入 `console.log("【CHART DATA DEBUG】", snapshots[0])`,通过浏览器控制台 F12 直接查看原始数据结构,作为排查字段映射问题的终极武器。
|
||||
- **数据映射层蛇形/驼峰双重兼容**:在 `chartData` 的 `map` 函数中,采用 `parseFloat(s.totalCostCny) || parseFloat(s.total_cost_cny || 0)` 的强制 fallback 逻辑,确保无论后端返回哪种命名风格都能正确解析。
|
||||
- **Tooltip 防御性解构**:`CustomTooltip` 中的值读取改为 `Number(dataNode.totalValueCny || dataNode._raw?.totalValueCny || 0) || 0`,通过 `_raw` 快照兜底读取,确保 Tooltip 永远能拿到本金和现值的真实数据。
|
||||
- **Snapshot 接口扩展**:新增 `total_value_cny?: string` 和 `total_cost_cny?: string` 可选字段,`ChartDatum` 接口新增 `_raw: Snapshot` 字段用于 Tooltip 层 fallback。
|
||||
- **验收标准**:控制台 `【CHART DATA DEBUG】` 打印出带真实本金(如 5094)的字段;Tooltip 中投入本金显示真实法币数字,彻底消除 704 旧账残影。
|
||||
@ -6,19 +6,30 @@ interface Snapshot {
|
||||
date: string;
|
||||
totalValueCny: string;
|
||||
totalCostCny: string;
|
||||
total_value_cny?: string;
|
||||
total_cost_cny?: string;
|
||||
}
|
||||
|
||||
interface ChartDatum {
|
||||
date: string;
|
||||
totalValueCny: number;
|
||||
totalCostCny: number;
|
||||
_raw: Snapshot;
|
||||
}
|
||||
|
||||
interface NetWorthChartProps {
|
||||
snapshots: Snapshot[];
|
||||
}
|
||||
|
||||
function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: string; payload: Snapshot }>; label?: string }) {
|
||||
function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: string; payload: any }>; label?: string }) {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
const totalValue = parseFloat(data.totalValueCny);
|
||||
const totalCost = parseFloat(data.totalCostCny);
|
||||
const pnl = totalValue - totalCost;
|
||||
const pnlPercent = totalCost > 0 ? (pnl / totalCost) * 100 : 0;
|
||||
const dataNode = payload[0].payload;
|
||||
|
||||
const value = Number(dataNode.totalValueCny || dataNode._raw?.totalValueCny || 0) || 0;
|
||||
const cost = Number(dataNode.totalCostCny || dataNode._raw?.totalCostCny || 0) || 0;
|
||||
|
||||
const pnl = value - cost;
|
||||
const pnlPercent = cost > 0 ? (pnl / cost) * 100 : 0;
|
||||
const isPositive = pnl >= 0;
|
||||
|
||||
const formattedDate = (label || '').replace(/-/g, '/');
|
||||
@ -42,7 +53,7 @@ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?:
|
||||
总市值
|
||||
</span>
|
||||
<span style={{ fontWeight: '700', color: '#f59e0b' }}>
|
||||
¥{totalValue.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
¥{value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '24px', marginBottom: '4px' }}>
|
||||
@ -51,7 +62,7 @@ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?:
|
||||
投入本金
|
||||
</span>
|
||||
<span style={{ fontWeight: '600', color: 'hsl(var(--muted-foreground))' }}>
|
||||
¥{totalCost.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
¥{cost.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '24px', paddingTop: '6px', borderTop: '1px solid hsl(var(--border))' }}>
|
||||
@ -79,11 +90,18 @@ export default function NetWorthChart({ snapshots }: NetWorthChartProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const chartData = snapshots.map(s => ({
|
||||
date: s.date.replace(/-/g, '/'),
|
||||
totalValueCny: parseFloat(s.totalValueCny),
|
||||
totalCostCny: parseFloat(s.totalCostCny),
|
||||
}));
|
||||
console.log("【CHART DATA DEBUG】", snapshots[0]);
|
||||
|
||||
const chartData = snapshots.map(s => {
|
||||
const totalValueCny = parseFloat(s.totalValueCny) || parseFloat((s as any).total_value_cny || 0);
|
||||
const totalCostCny = parseFloat(s.totalCostCny) || parseFloat((s as any).total_cost_cny || 0);
|
||||
return {
|
||||
date: s.date.replace(/-/g, '/'),
|
||||
totalValueCny,
|
||||
totalCostCny,
|
||||
_raw: s,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-[320px] w-full">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user