From 3c8f9d1287ce15d8f6fbec85d447199914a44454 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 13 Apr 2026 08:35:55 +0800 Subject: [PATCH] feat(usage): improve pagination with first/last 3 pages and page jump input Show first 3 and last 3 page buttons instead of just first/last, with Set-based deduplication for clean edge merging. Add a page number input field with Go button for direct page navigation. --- src/components/usage/RequestLogTable.tsx | 58 ++++++++++++++++++------ src/i18n/locales/en.json | 2 + src/i18n/locales/ja.json | 2 + src/i18n/locales/zh.json | 2 + 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/components/usage/RequestLogTable.tsx b/src/components/usage/RequestLogTable.tsx index fe485ec20..7519a39d0 100644 --- a/src/components/usage/RequestLogTable.tsx +++ b/src/components/usage/RequestLogTable.tsx @@ -57,6 +57,7 @@ export function RequestLogTable({ const [appliedFilters, setAppliedFilters] = useState({}); const [draftFilters, setDraftFilters] = useState({}); const [page, setPage] = useState(0); + const [pageInput, setPageInput] = useState(""); const pageSize = 20; const [validationError, setValidationError] = useState(null); @@ -131,6 +132,15 @@ export function RequestLogTable({ setPage(0); }; + const handleGoToPage = () => { + const trimmed = pageInput.trim(); + if (!/^\d+$/.test(trimmed)) return; + const parsed = Number(trimmed); + if (parsed < 1 || parsed > totalPages) return; + setPage(parsed - 1); + setPageInput(""); + }; + const handleRefresh = () => { const key = { timeMode: appliedTimeMode, @@ -561,30 +571,33 @@ export function RequestLogTable({ > - {/* 页码按钮 */} {(() => { const pages: (number | string)[] = []; - if (totalPages <= 7) { + // 3 head + 3 tail + 3 neighborhood = 9 max distinct pages + if (totalPages <= 9) { for (let i = 0; i < totalPages; i++) pages.push(i); } else { - pages.push(0); - if (page > 2) pages.push("..."); + const pageSet = new Set(); + for (let i = 0; i < 3; i++) pageSet.add(i); + for (let i = totalPages - 3; i < totalPages; i++) + pageSet.add(i); for ( - let i = Math.max(1, page - 1); - i <= Math.min(totalPages - 2, page + 1); + let i = Math.max(0, page - 1); + i <= Math.min(totalPages - 1, page + 1); i++ - ) { - pages.push(i); + ) + pageSet.add(i); + const sorted = Array.from(pageSet).sort((a, b) => a - b); + for (let i = 0; i < sorted.length; i++) { + if (i > 0 && sorted[i] - sorted[i - 1] > 1) { + pages.push(`ellipsis-${i}`); + } + pages.push(sorted[i]); } - if (page < totalPages - 3) pages.push("..."); - pages.push(totalPages - 1); } - return pages.map((p, idx) => + return pages.map((p) => typeof p === "string" ? ( - + ... ) : ( @@ -608,6 +621,21 @@ export function RequestLogTable({ > +
+ setPageInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleGoToPage(); + }} + placeholder={t("usage.pageInputPlaceholder")} + className="h-8 w-16 text-center text-xs" + /> + +
)} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8dbb44b59..597f10d38 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1107,6 +1107,8 @@ "failed": "Session sync failed" }, "totalRecords": "{{total}} records total", + "goToPage": "Go", + "pageInputPlaceholder": "Page", "modelPricing": "Model Pricing", "loadPricingError": "Failed to load pricing data", "modelPricingDesc": "Configure token costs for each model", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 43e87c067..707497f21 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1107,6 +1107,8 @@ "failed": "セッション同期に失敗しました" }, "totalRecords": "全 {{total}} 件", + "goToPage": "移動", + "pageInputPlaceholder": "ページ", "modelPricing": "モデル料金", "loadPricingError": "料金データの読み込みに失敗しました", "modelPricingDesc": "各モデルのトークンコストを設定", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index bf4b35766..7659c6d0e 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1108,6 +1108,8 @@ "failed": "会话同步失败" }, "totalRecords": "共 {{total}} 条记录", + "goToPage": "跳转", + "pageInputPlaceholder": "页码", "modelPricing": "模型定价", "loadPricingError": "加载定价数据失败", "modelPricingDesc": "配置各模型的 Token 成本",