From 87a822c9a7b04ee0b4aba96e0cc93f48027f159c Mon Sep 17 00:00:00 2001 From: argszero Date: Mon, 24 Aug 2026 11:05:07 +0800 Subject: [PATCH] fix(ui): real server-side pagination for transactions (rant 2026-08-24T10:51:57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fake pagination bug: loadTransactions hardcoded page=1&page_size=100 and the pager used the loaded data.length (=100) as the total, so with 7320 real records only the first 100 were ever visible. - loadTransactions now sends txTable.page / txTable.pageSize and records loadedPage/loadedPageSize after each fetch - renderTransactions refetches the requested page when local page/pageSize differs from the loaded one (page buttons, sort, filters, page-size all flow through this guard) - buildDataTable gains optional serverPaging.total: pager shows the backend total and page count from it; rows are the already-paginated page (no local slice); other tables unaffected - compact pager with ellipsis window (1 … p-1 p p+1 … N) when pages > 9 - scroll list into view after paging to a page > 1 - cache-bust 20260824-3 --- ui/css/style.css | 1 + ui/index.html | 10 +++++----- ui/js/app.js | 49 ++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/ui/css/style.css b/ui/css/style.css index 45521f4..8407912 100644 --- a/ui/css/style.css +++ b/ui/css/style.css @@ -505,6 +505,7 @@ mark { } .pager button.active { border-color: var(--accent); color: var(--accent-text); } .pager span { color: var(--text-mute); font-size: 12px; } +.pager .pager-ellipsis { min-width: 22px; text-align: center; user-select: none; } /* MRT 风格表格:表头排序 + 列筛选 + 每页行数 */ .th-sort { diff --git a/ui/index.html b/ui/index.html index d4112bd..1f812cb 100644 --- a/ui/index.html +++ b/ui/index.html @@ -5,7 +5,7 @@ AITokenPool - + @@ -683,9 +683,9 @@

使用模型

- - - - + + + + diff --git a/ui/js/app.js b/ui/js/app.js index 0c83f45..a3a0cb7 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -1385,6 +1385,11 @@ $("#tx-table").innerHTML = loadErrorHtml(T("tx.loadFail"), null, T("err.loadFail")); return; } + // 真后端分页(rant 2026-08-24T10:51:57):本地页码/每页行数与已加载页不一致 → 重新向后端拉取对应页 + if (Live.transactions && (txTable.loadedPage !== txTable.page || txTable.loadedPageSize !== txTable.pageSize)) { + loadTransactions(); + return; + } let list = Live.transactions ? txsToView(Live.transactions.items || []) : []; // 交易汇总条:与 tab + 列筛选联动,与表格可见行一致(rant 20:39:30 B) renderTxSummary(filterRows(list, TX_COLUMNS, txTable.filters)); @@ -1396,6 +1401,8 @@ rows: list, state: txTable, onState: renderTransactions, + // 真后端分页(rant 2026-08-24T10:51:57):总数显示后端 total;页码点击 → onState → renderTransactions 页码不一致自动重拉 + serverPaging: Live.transactions ? { total: Live.transactions.total || 0 } : null, }); } @@ -1417,14 +1424,19 @@ return p.join("&"); } - // P2-B:按 tab 拉取交易(后端分页;带时间段过滤) + // P2-B:按 tab 拉取交易(真后端分页 rant 2026-08-24T10:51:57:页码/每页行数随请求发出; + // loadedPage/loadedPageSize 记录已加载页,renderTransactions 发现页码不一致时自动重拉) async function loadTransactions() { if (!loggedIn()) return; const type = txTab === "all" ? "" : txTab; const range = txRangeParams(); - const q = "/api/transactions?type=" + type + "&page=1&page_size=100" + (range ? "&" + range : ""); + const page = Math.max(1, txTable.page || 1); + const pageSize = Math.min(100, Math.max(1, txTable.pageSize || 10)); + const q = "/api/transactions?type=" + type + "&page=" + page + "&page_size=" + pageSize + (range ? "&" + range : ""); try { await liveLoad("transactions", q); + txTable.loadedPage = page; + txTable.loadedPageSize = pageSize; } catch (e) { Live.transactions = null; /* 登录态降级空态 */ } // 趋势图数据(rant 2026-08-23T16:01:07 需求 2):独立拉取,失败不阻塞列表 const bucket = txTrendBucket(); @@ -1433,6 +1445,11 @@ if (Live.transactions) Live.transactions.trend = trend; } catch (e) { if (Live.transactions) Live.transactions.trend = null; } renderTransactions(); + // 翻页后滚动到列表顶部(rant 2026-08-24T10:51:57 需求 4) + if (page > 1) { + const tbl = $("#tx-table"); + if (tbl && tbl.scrollIntoView) tbl.scrollIntoView({ block: "start" }); + } } // 交易记录导出 CSV(rant 20:46:57 E:Blob + a[download],UTF-8 BOM,文件名 aitokenpool-transactions-YYYYMMDD.csv;导出当前筛选可见行) @@ -1540,8 +1557,20 @@ }); } + // 紧凑分页码(页数 > 9 时省略号收拢:1 … p-1 p p+1 … N;页数少则全量渲染) + function pagerButtons(page, pages) { + const out = []; + if (pages <= 9) { for (let i = 1; i <= pages; i++) out.push(i); return out; } + out.push(1); + if (page > 4) out.push("…"); + for (let i = Math.max(2, page - 1); i <= Math.min(pages - 1, page + 1); i++) out.push(i); + if (page < pages - 3) out.push("…"); + out.push(pages); + return out; + } + function buildDataTable(cfg) { - const { container, columns, rows, state, onState } = cfg; + const { container, columns, rows, state, onState, serverPaging } = cfg; // 1) 筛选 let data = filterRows(rows, columns, state.filters); @@ -1561,10 +1590,11 @@ }); } - // 3) 分页 - const pages = Math.max(1, Math.ceil(data.length / state.pageSize)); + // 3) 分页(serverPaging:总数取后端 total,行即当前页——服务端已翻页,不做本地 slice) + const totalRows = serverPaging ? serverPaging.total : data.length; + const pages = Math.max(1, Math.ceil(totalRows / state.pageSize)); if (state.page > pages) state.page = pages; - const pageRows = data.slice((state.page - 1) * state.pageSize, state.page * state.pageSize); + const pageRows = serverPaging ? data : data.slice((state.page - 1) * state.pageSize, state.page * state.pageSize); // 4) 渲染表头(排序按钮)+ 筛选行 let html = ''; @@ -1606,8 +1636,11 @@ // 5) 分页器 + 每页行数 if (pages > 1) { html += '
'; - for (let i = 1; i <= pages; i++) html += '"; - html += "" + state.page + " / " + pages + " · " + T("tx.pager.count", { n: data.length }) + "
"; + pagerButtons(state.page, pages).forEach((p) => { + if (p === "…") html += ''; + else html += '"; + }); + html += "" + state.page + " / " + pages + " · " + T("tx.pager.count", { n: totalRows }) + ""; } html += '
' + T("tx.pager.size") + ' ' + T("tx.pager.rows") + '
';