diff --git a/Memory.md b/Memory.md index 9d77d4a..024e30f 100644 --- a/Memory.md +++ b/Memory.md @@ -409,4 +409,12 @@ - 在 `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` 作为快照的本金值。 - 净盈亏在 `NetWorthChart` 的 `CustomTooltip` 中通过 `pnl = totalValue - totalCost` 顺向派生,符合"盈亏 = 市值 - 本金"的金融计算规范。 -- **验收标准**:鼠标悬浮在图表上,投入本金显示底层原汁原味的成本值(如 ¥5094.59),净盈亏自动修正为真实值(如 +¥472.12)。 \ No newline at end of file +- **验收标准**:鼠标悬浮在图表上,投入本金显示底层原汁原味的成本值(如 ¥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 旧账残影。 \ No newline at end of file diff --git a/src/components/dashboard/net-worth-chart.tsx b/src/components/dashboard/net-worth-chart.tsx index d1bc128..72683fa 100644 --- a/src/components/dashboard/net-worth-chart.tsx +++ b/src/components/dashboard/net-worth-chart.tsx @@ -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?: 总市值 - ¥{totalValue.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + ¥{value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
@@ -51,7 +62,7 @@ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: 投入本金 - ¥{totalCost.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + ¥{cost.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
@@ -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 (