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.
This commit is contained in:
Jason
2026-04-13 08:35:55 +08:00
parent 420f4c8c23
commit 3c8f9d1287
4 changed files with 49 additions and 15 deletions
+43 -15
View File
@@ -57,6 +57,7 @@ export function RequestLogTable({
const [appliedFilters, setAppliedFilters] = useState<LogFilters>({});
const [draftFilters, setDraftFilters] = useState<LogFilters>({});
const [page, setPage] = useState(0);
const [pageInput, setPageInput] = useState("");
const pageSize = 20;
const [validationError, setValidationError] = useState<string | null>(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({
>
<ChevronLeft className="h-4 w-4" />
</Button>
{/* 页码按钮 */}
{(() => {
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<number>();
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" ? (
<span
key={`ellipsis-${idx}`}
className="px-2 text-muted-foreground"
>
<span key={p} className="px-2 text-muted-foreground">
...
</span>
) : (
@@ -608,6 +621,21 @@ export function RequestLogTable({
>
<ChevronRight className="h-4 w-4" />
</Button>
<div className="flex items-center gap-1 ml-2">
<Input
type="text"
value={pageInput}
onChange={(e) => setPageInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleGoToPage();
}}
placeholder={t("usage.pageInputPlaceholder")}
className="h-8 w-16 text-center text-xs"
/>
<Button variant="outline" size="sm" onClick={handleGoToPage}>
{t("usage.goToPage")}
</Button>
</div>
</div>
</div>
)}
+2
View File
@@ -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",
+2
View File
@@ -1107,6 +1107,8 @@
"failed": "セッション同期に失敗しました"
},
"totalRecords": "全 {{total}} 件",
"goToPage": "移動",
"pageInputPlaceholder": "ページ",
"modelPricing": "モデル料金",
"loadPricingError": "料金データの読み込みに失敗しました",
"modelPricingDesc": "各モデルのトークンコストを設定",
+2
View File
@@ -1108,6 +1108,8 @@
"failed": "会话同步失败"
},
"totalRecords": "共 {{total}} 条记录",
"goToPage": "跳转",
"pageInputPlaceholder": "页码",
"modelPricing": "模型定价",
"loadPricingError": "加载定价数据失败",
"modelPricingDesc": "配置各模型的 Token 成本",