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` 作为快照的本金值。
|
- 在 `app/dashboard/page.tsx` 的 `loadSnapshots()` 函数中,删除 `new Big(summary.totalCnyValue).minus(new Big(summary.totalPnlCny))` 的反向推导代码,改为直接使用 `summary.totalCostCny` 作为快照的本金值。
|
||||||
- 净盈亏在 `NetWorthChart` 的 `CustomTooltip` 中通过 `pnl = totalValue - totalCost` 顺向派生,符合"盈亏 = 市值 - 本金"的金融计算规范。
|
- 净盈亏在 `NetWorthChart` 的 `CustomTooltip` 中通过 `pnl = totalValue - totalCost` 顺向派生,符合"盈亏 = 市值 - 本金"的金融计算规范。
|
||||||
- **验收标准**:鼠标悬浮在图表上,投入本金显示底层原汁原味的成本值(如 ¥5094.59),净盈亏自动修正为真实值(如 +¥472.12)。
|
- **验收标准**:鼠标悬浮在图表上,投入本金显示底层原汁原味的成本值(如 ¥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;
|
date: string;
|
||||||
totalValueCny: string;
|
totalValueCny: string;
|
||||||
totalCostCny: string;
|
totalCostCny: string;
|
||||||
|
total_value_cny?: string;
|
||||||
|
total_cost_cny?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartDatum {
|
||||||
|
date: string;
|
||||||
|
totalValueCny: number;
|
||||||
|
totalCostCny: number;
|
||||||
|
_raw: Snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NetWorthChartProps {
|
interface NetWorthChartProps {
|
||||||
snapshots: Snapshot[];
|
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) {
|
if (active && payload && payload.length) {
|
||||||
const data = payload[0].payload;
|
const dataNode = payload[0].payload;
|
||||||
const totalValue = parseFloat(data.totalValueCny);
|
|
||||||
const totalCost = parseFloat(data.totalCostCny);
|
const value = Number(dataNode.totalValueCny || dataNode._raw?.totalValueCny || 0) || 0;
|
||||||
const pnl = totalValue - totalCost;
|
const cost = Number(dataNode.totalCostCny || dataNode._raw?.totalCostCny || 0) || 0;
|
||||||
const pnlPercent = totalCost > 0 ? (pnl / totalCost) * 100 : 0;
|
|
||||||
|
const pnl = value - cost;
|
||||||
|
const pnlPercent = cost > 0 ? (pnl / cost) * 100 : 0;
|
||||||
const isPositive = pnl >= 0;
|
const isPositive = pnl >= 0;
|
||||||
|
|
||||||
const formattedDate = (label || '').replace(/-/g, '/');
|
const formattedDate = (label || '').replace(/-/g, '/');
|
||||||
@ -42,7 +53,7 @@ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?:
|
|||||||
总市值
|
总市值
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontWeight: '700', color: '#f59e0b' }}>
|
<span style={{ fontWeight: '700', color: '#f59e0b' }}>
|
||||||
¥{totalValue.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
¥{value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '24px', marginBottom: '4px' }}>
|
<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>
|
||||||
<span style={{ fontWeight: '600', color: 'hsl(var(--muted-foreground))' }}>
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '24px', paddingTop: '6px', borderTop: '1px solid hsl(var(--border))' }}>
|
<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 => ({
|
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, '/'),
|
date: s.date.replace(/-/g, '/'),
|
||||||
totalValueCny: parseFloat(s.totalValueCny),
|
totalValueCny,
|
||||||
totalCostCny: parseFloat(s.totalCostCny),
|
totalCostCny,
|
||||||
}));
|
_raw: s,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[320px] w-full">
|
<div className="h-[320px] w-full">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user