From e692d47b6a8bea0b001d5161ce894a6d59926e73 Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Sat, 2 May 2026 15:19:48 +0800 Subject: [PATCH] =?UTF-8?q?style(portfolio):=20=E4=BC=98=E5=8C=96=20CSV=20?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E5=AD=97=E6=AE=B5=E6=A0=BC=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?=E7=B2=BE=E5=87=86=E5=89=A5=E7=A6=BB=E4=BB=B7=E6=A0=BC=E6=9C=AB?= =?UTF-8?q?=E5=B0=BE=E7=9A=84=E6=97=A0=E6=84=8F=E4=B9=89=E9=9B=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Memory.md | 5 +++++ app/dashboard/page.tsx | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Memory.md b/Memory.md index 34d16a6..15b3767 100644 --- a/Memory.md +++ b/Memory.md @@ -1,5 +1,10 @@ # Omniledger 架构与开发记忆 (Memory) +## 优化 exportToCSV 数据净洗逻辑,利用防科学计数法的纯正则处理去除现价/成本价末尾的无意义零 (Task 69) +- 在 `app/dashboard/page.tsx` 的 `exportToCSV()` 函数顶部注入 `stripTrailingZeros` 纯字符串去零工具函数。 +- 该函数内置防御性设计:对 null/undefined/空值返回 `"0"`;仅对包含小数点的字符串执行正则处理(`/0+$/` 剥离尾随零 → `/\.$/` 剥离末尾小数点),彻底杜绝极小数值(如 `0.00000001`)被 `String()` 转换后触发科学计数法(`1e-8`)。 +- 将 CSV 映射层中的"成本价"字段(`avgCostNative`)和"现价"字段(`latestPrice`)包裹 `stripTrailingZeros()`,使小米 `29.020` 输出为 `29.02`、整数价格 `29.00` 输出为 `29`,而"总市值""浮动盈亏""累计盈亏"等法币资产字段保留 `.toFixed(2)` 的两位小数格式以维持表格对齐。 + ## 新增持仓明细的已清仓资产显示开关(基于 1e-8 精度容差过滤),并实装注入 UTF-8 BOM 的客户端 CSV 导出功能 (Task 68) - 在 `src/actions/portfolio.ts` 的 `getPortfolioPositions()` 函数中新增 `includeCleared: boolean = false` 参数,将清仓判定从 `quantity === 0` 升级为 `Big.js` 的 `1e-8` 精度容差过滤(`holding.quantity.gt('1e-8')`),杜绝浮点数灰尘资产被错误保留或隐藏。 - 当 `!includeCleared` 时,自动过滤掉持仓量 ≤ 1e-8 的已清仓资产;当 `includeCleared` 为 true 时,已清仓资产会被返回,其 `marketValue` 和 `floatingPnl` 为 0,但**保留真实计算出的 `accumulatedPnl`(累计盈亏)字段**,确保历史盈亏数据不丢失。 diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index a3cd9ae..13cfc94 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -56,14 +56,24 @@ function formatNative(value: string, baseCurrency: string): string { } function exportToCSV(positions: any[]) { + const stripTrailingZeros = (val: any): string => { + if (val === null || val === undefined || val === '') return "0"; + let str = String(val); + if (str.includes('.')) { + str = str.replace(/0+$/, ''); + str = str.replace(/\.$/, ''); + } + return str; + }; + const headers = ["资产名称", "代码", "持仓量", "成本价", "现价", "总市值", "浮动盈亏", "累计盈亏"]; const rows = positions.map(item => [ item.name || item.symbol, item.symbol, item.quantity || '0', - new Big(item.avgCostNative || '0').toFixed(2), - item.latestPrice || '0', + stripTrailingZeros(new Big(item.avgCostNative || '0').toFixed(2)), + stripTrailingZeros(item.latestPrice || '0'), new Big(item.marketValueNative || '0').toFixed(2), new Big(item.floatingPnlNative || '0').toFixed(2), new Big(item.cumulativePnlNative || '0').toFixed(2),