From b2d214958f5e71411b0170da8efe89afd9baa84a Mon Sep 17 00:00:00 2001 From: argszero Date: Mon, 14 Sep 2026 07:57:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(ui):=20export=20the=20transaction=20time=20?= =?UTF-8?q?column=20in=20the=20same=20=E5=8F=A3=E5=BE=84=20as=20the=20cell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `exportTxCsv` wrote the view row's raw `t.time` — the UTC string the API returns, which `txsToView` does not convert — while the table cell renders that same field through `fmtPrecise()` (local time, #139 / rant 2026-08-24T12:38:44). In a UTC+8 session one and the same row therefore reads `23:04` in the table and `15:04` in the exported file (west of Greenwich the sign flips). This is the class of divergence C2054/C2055 already closed for the 点数 column: "the export covers the currently visible rows, so the column must use the same 口径 as the cell — otherwise the screen says -3.7 and the file says 3.7". Fix: print the time column with the very helper that renders the cell (`fmtPrecise(t.time)`) instead of keeping a second copy of "time to string". Verified with a jsdom probe (real ui/index.html + the four scripts, only `fetch` stubbed) that boots three transactions, reads the rendered cell and the bytes of the exported Blob: pre-change red on the time-column assertion only; fixed tree 6/6, in Asia/Shanghai and America/New_York. The probe also asserts the process TZ is not UTC, since under TZ=UTC the two 口径 coincide and the defect is invisible. --- ui/README.md | 1 + ui/index.html | 2 +- ui/js/app.js | 8 ++++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ui/README.md b/ui/README.md index c3a1075..793066d 100644 --- a/ui/README.md +++ b/ui/README.md @@ -268,6 +268,7 @@ ui/ - 入口:交易页 page-head 右上 **`#tx-export-btn`「导出 CSV」**(`.btn.btn-secondary.btn-sm`),`bindEvents` 绑 `exportTxCsv`; - 数据范围:**当前筛选可见行** = tab(全部/消费/收益)→ `filterRows(list, TX_COLUMNS, txTable.filters)`(与表格、汇总条同一数据源);无数据 → toast info 不导出; +- **各列与表格单元格同口径**(C2054 点数 / C2111 时间):时间列走渲染单元格的同一个 `fmtPrecise(t.time)`(本地精确时间),**不**直接写视图行的 `t.time`(库内 UTC 串,`txsToView` 不转换)——否则同一行在表里是 `23:04`、在导出文件里却是 `15:04`(东八区;西半球反向)。`#139`(rant 2026-08-24T12:38:44)把单元格改成当地时间展示时,漏了导出这个消费者; - 格式:**UTF-8 BOM**(`"\uFEFF"` 前缀)+ `\r\n` 换行 + 表头 `时间,类型,模型 / Key,Token 用量,点数,状态`;类型用 `TX_TYPE` 中文映射;点数正负号原值;字段含 `,`/`"`/换行按 RFC4180 双引号转义(`cell()` 助手); - 下载:`Blob(type="text/csv;charset=utf-8")` → `URL.createObjectURL` → 临时 `` click → `remove()` → `setTimeout 1s` revoke;文件名 **`aitokenpool-transactions-YYYYMMDD.csv`**(`new Date()` 本地日期); - 冒烟测试注意:stub 需给 `document.createElement("a")` 返回带 `click()`/`remove()` 的元素并捕获 `href`/`download`,`URL.createObjectURL` 捕获 Blob(`arrayBuffer()` 首 3 字节 EF BB BF 验证 BOM——`blob.text()` 会按规范剥掉 BOM);列筛选联动可注入 `#tx-table` 的 `querySelectorAll(".th-filter")`/`querySelector('[data-filter-key=…]')` 假输入并 fire `input`。 diff --git a/ui/index.html b/ui/index.html index 68b7e0c..169a853 100644 --- a/ui/index.html +++ b/ui/index.html @@ -845,6 +845,6 @@

使用模型

- + diff --git a/ui/js/app.js b/ui/js/app.js index 84605dd..0f60994 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -1652,9 +1652,13 @@ if (!list.length) { toast(T("tx.export.none"), "info"); return; } const cell = (v) => { const s = String(v == null ? "" : v); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }; const headers = [T("tx.col.time"), T("tx.col.type"), T("tx.col.user"), T("tx.col.model"), T("tx.col.apiKeyName"), T("tx.col.input"), T("tx.col.cached"), T("tx.col.output"), T("tx.col.tokens"), T("tx.col.pts"), T("tx.col.status")]; - // C2054:导出的是「当前筛选可见行」,故「点数」列与表格单元格同口径(有符号:收入正/支出负), + // C2054:导出的是「当前筛选可见行」,故各列与表格单元格**同口径** —— 点数写有符号值, // 否则屏幕上写着 -3.7、导出的文件里却是 3.7。 - const lines = list.map((t) => [t.time, txType(t.type), t.user, t.model, t.key, t.inputTokens, t.cachedTokens, t.outputTokens, t.tokens, signedPts(t.type, t.pts), txStatus(t.status)].map(cell).join(",")); + // C2111:时间列同理。单元格渲染的是**本地**精确时间(`fmtPrecise`,时间列口径见 #139 + // rant 2026-08-24T12:38:44),而导出一直直接写 `t.time`(库内 UTC 串,`txsToView` 未转换) + // ⇒ 同一行在表里是 23:04、在文件里却是 15:04(东八区;西半球反向)。两处必须同源: + // 导出直接用渲染该单元格的同一个 helper,而不是再抄一份「时间转字符串」的口径。 + const lines = list.map((t) => [fmtPrecise(t.time), txType(t.type), t.user, t.model, t.key, t.inputTokens, t.cachedTokens, t.outputTokens, t.tokens, signedPts(t.type, t.pts), txStatus(t.status)].map(cell).join(",")); const csv = "\uFEFF" + [headers.join(","), ...lines].join("\r\n"); // UTF-8 BOM,Excel 中文不乱码 const blob = new Blob([csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob);