From 8b76ec9a6df4146102d402c5059e40f316e28851 Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Sat, 2 May 2026 21:57:19 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ui):=20=E9=87=8D=E5=A1=91=20Dashboard?= =?UTF-8?q?=20=E6=95=B0=E6=8D=AE=E6=B5=81=EF=BC=8C=E5=89=94=E9=99=A4?= =?UTF-8?q?=E8=BF=9D=E8=A7=84=E6=9C=AC=E9=87=91=E5=8F=8D=E6=8E=A8=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E5=BA=94=E7=94=A8=E9=A1=BA=E5=90=91=E7=9B=88?= =?UTF-8?q?=E4=BA=8F=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Memory.md | 11 ++++++++++- app/dashboard/page.tsx | 7 +++---- src/actions/portfolio.ts | 3 +++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Memory.md b/Memory.md index 8cf36a4..9d77d4a 100644 --- a/Memory.md +++ b/Memory.md @@ -400,4 +400,13 @@ ## 精确定位 Client Component,修复 net-worth-chart.tsx 中的 dataKey 与 Tooltip 绑定错误,彻底解决视图层与数据层本金单位不统一的问题 (Task 80) - **根因分析**:在 `src/components/dashboard/net-worth-chart.tsx`(Client Component)中,净值走势图的投入本金曲线和 Tooltip 需要读取经过汇率折算后的法币本金字段(`totalCostCny`),而非原币种或未经折算的字段。 - **数据链路验证**:从数据库 `portfolio_snapshots.total_cost_cny` → Drizzle ORM 映射为 `totalCostCny` → `getSnapshots()` 返回 → `page.tsx` 的 `loadSnapshots()` 中计算 `totalCostCny = totalCnyValue - totalPnlCny` → 通过 props 传入 `NetWorthChart` → `chartData` 映射为 `totalCostCny` → `` 渲染 + `CustomTooltip` 从 `payload[0].payload.totalCostCny` 读取。 -- **修复验证**:`Snapshot` 接口定义 `totalValueCny` / `totalCostCny`,`chartData` 映射使用 `totalValueCny` / `totalCostCny`(均 `parseFloat`),`` 的 `dataKey` 分别为 `totalValueCny` 和 `totalCostCny`,`CustomTooltip` 从 `data.totalValueCny` / `data.totalCostCny` 解构计算净盈亏。全链路字段名严格一致,确保投入本金曲线显示真实的累计投入成本(CNY 折算后)。 \ No newline at end of file +- **修复验证**:`Snapshot` 接口定义 `totalValueCny` / `totalCostCny`,`chartData` 映射使用 `totalValueCny` / `totalCostCny`(均 `parseFloat`),`` 的 `dataKey` 分别为 `totalValueCny` 和 `totalCostCny`,`CustomTooltip` 从 `data.totalValueCny` / `data.totalCostCny` 解构计算净盈亏。全链路字段名严格一致,确保投入本金曲线显示真实的累计投入成本(CNY 折算后)。 + +## 剔除 page.tsx 中违规的 `本金 = 市值 - 盈亏` 反向派生逻辑,确立 `盈亏 = 市值 - 本金` 的顺向金融计算流,彻底修复了走势图本金显示被旧 PnL 污染的架构 Bug (Task 81) +- **根因分析**:在 `app/dashboard/page.tsx` 的 `loadSnapshots()` 函数中(第 178 行),今日快照的 `totalCostCny` 被错误地通过 `new Big(summary.totalCnyValue).minus(new Big(summary.totalPnlCny)).toString()` 反向推导得出。这导致:1) 本金被旧 PnL 数据污染,失去底层真实性;2) 违反了金融计算中"本金优先"的架构红线。 +- **架构红线**:绝对禁止反推本金。本金 (`totalCostCny`) 必须直接读取数据库 snapshot 表或从 position 层级的 `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` 作为快照的本金值。 + - 净盈亏在 `NetWorthChart` 的 `CustomTooltip` 中通过 `pnl = totalValue - totalCost` 顺向派生,符合"盈亏 = 市值 - 本金"的金融计算规范。 +- **验收标准**:鼠标悬浮在图表上,投入本金显示底层原汁原味的成本值(如 ¥5094.59),净盈亏自动修正为真实值(如 +¥472.12)。 \ No newline at end of file diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index bbd6d8c..09e4b5f 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -171,20 +171,19 @@ export default function DashboardPage() { useEffect(() => { async function loadSnapshots() { - await recordDailySnapshot(); const summary = await getPortfolioSummary(); + await recordDailySnapshot(); const data = await getSnapshots(); const todayStr = new Date().toISOString().slice(0, 10); - const totalCostCny = new Big(summary.totalCnyValue).minus(new Big(summary.totalPnlCny)).toString(); const lastSnapshot = data[data.length - 1]; if (lastSnapshot && lastSnapshot.date === todayStr) { lastSnapshot.totalValueCny = summary.totalCnyValue; - lastSnapshot.totalCostCny = totalCostCny; + lastSnapshot.totalCostCny = summary.totalCostCny; } else { data.push({ date: todayStr, totalValueCny: summary.totalCnyValue, - totalCostCny, + totalCostCny: summary.totalCostCny, }); } setSnapshots(data); diff --git a/src/actions/portfolio.ts b/src/actions/portfolio.ts index 1489176..ac58435 100644 --- a/src/actions/portfolio.ts +++ b/src/actions/portfolio.ts @@ -468,11 +468,13 @@ export async function getPortfolioSummary(includeCleared: boolean = false) { let totalCnyValue = new Big('0'); let totalPnlCny = new Big('0'); let totalFloatingPnlCny = new Big('0'); + let totalCostCny = new Big('0'); for (const pos of positions) { totalCnyValue = totalCnyValue.plus(new Big(pos.marketValueCny || '0')); totalPnlCny = totalPnlCny.plus(new Big(pos.accumulatedPnlCny || '0')); totalFloatingPnlCny = totalFloatingPnlCny.plus(new Big(pos.floatingPnlCny || '0')); + totalCostCny = totalCostCny.plus(new Big(pos.totalCostCny || '0')); } const chartData = positions.map((pos, index) => ({ @@ -531,6 +533,7 @@ export async function getPortfolioSummary(includeCleared: boolean = false) { return { positions, totalCnyValue: totalCnyValue.toString(), + totalCostCny: totalCostCny.toString(), totalPnlCny: totalPnlCny.toString(), unrealizedPnlCny: totalFloatingPnlCny.toString(), chartData,