fix(ui): 暴力重构净值图表数据流,彻底对齐法币本金渲染

This commit is contained in:
kennethcheng 2026-05-02 22:10:01 +08:00
parent 8b76ec9a6d
commit 89b40a72bb
2 changed files with 40 additions and 14 deletions

View File

@ -409,4 +409,12 @@
- 在 `src/actions/portfolio.ts``getPortfolioSummary()` 函数中新增 `totalCostCny` 的逐项累加计算:`totalCostCny = sum(pos.totalCostCny)`,并在返回值中暴露 `totalCostCny` 字段。 - 在 `src/actions/portfolio.ts``getPortfolioSummary()` 函数中新增 `totalCostCny` 的逐项累加计算:`totalCostCny = sum(pos.totalCostCny)`,并在返回值中暴露 `totalCostCny` 字段。
- 在 `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 旧账残影。

View File

@ -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]);
date: s.date.replace(/-/g, '/'),
totalValueCny: parseFloat(s.totalValueCny), const chartData = snapshots.map(s => {
totalCostCny: parseFloat(s.totalCostCny), 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 ( return (
<div className="h-[320px] w-full"> <div className="h-[320px] w-full">