diff --git a/crates/wb-switch-core/src/modules/official_usage.rs b/crates/wb-switch-core/src/modules/official_usage.rs index ce7763d..3c4ad75 100644 --- a/crates/wb-switch-core/src/modules/official_usage.rs +++ b/crates/wb-switch-core/src/modules/official_usage.rs @@ -37,7 +37,13 @@ fn official_usage_url_for(account: &Value) -> &'static str { } } pub const OFFICIAL_USAGE_PAGE_SIZE: usize = 3_000; -pub const OFFICIAL_USAGE_DETAIL_LIMIT: usize = 100; +/// 每个账号落进 `requests` 明细的条数上限。 +/// +/// 官方接口已全量扫回(见 [`OFFICIAL_USAGE_PAGE_SIZE`] 的分页扫描),这里只是 +/// 「明细对外暴露多少条」的闸门:定得太低,用户排查高消耗请求时看不到更早的 +/// 记录;不设上限,重度账号会把明细撑到几万条,缓存文件与前端渲染一起变慢。 +/// 1000 条配合前端分页与按消耗排序,足够覆盖排查场景,同时保持有界。 +pub const OFFICIAL_USAGE_DETAIL_LIMIT: usize = 1_000; /// 连续扫描的轮数上限:每轮最多 [`OFFICIAL_USAGE_PAGE_SIZE`] 条,100 轮足够覆盖 /// 单账号单窗口的任何真实量级,同时兜住「服务端异常回同样一页」的死循环。 const OFFICIAL_USAGE_MAX_ROUNDS: usize = 100; diff --git a/src/pages/CreditStatsPage.tsx b/src/pages/CreditStatsPage.tsx index 60e33f7..28a5016 100644 --- a/src/pages/CreditStatsPage.tsx +++ b/src/pages/CreditStatsPage.tsx @@ -1,9 +1,12 @@ import { useCallback, useEffect, useMemo, useState, type ComponentProps } from "react"; import { Bar, BarChart, CartesianGrid, Rectangle, XAxis, YAxis } from "recharts"; import { + ArrowUpDown, CalendarDays, CalendarRange, Check, + ChevronLeft, + ChevronRight, CircleAlert, CircleCheck, Loader2, @@ -1236,6 +1239,76 @@ function OfficialRequestRow({ ); } +/** 请求用量明细每页条数;明细由后端一次性返回,分页只控制单页渲染量 */ +const REQUEST_PAGE_SIZE = 100; + +/** 请求用量明细的排序维度:请求时间(默认倒序)与消耗 */ +type RequestSortKey = "time" | "credit"; +type SortDirection = "desc" | "asc"; + +/** 明细行的时间戳:requestTime 是「YYYY-MM-DD HH:mm:ss」文本,按字典序即时间序 */ +function requestSortValue(request: CreditOfficialUsageRequest, key: RequestSortKey): number | string { + if (key === "credit") return request.credit ?? 0; + return request.requestTime ?? ""; +} + +function compareRequests( + left: CreditOfficialUsageRequest, + right: CreditOfficialUsageRequest, + key: RequestSortKey, + direction: SortDirection, +): number { + const leftValue = requestSortValue(left, key); + const rightValue = requestSortValue(right, key); + let order: number; + if (typeof leftValue === "number" && typeof rightValue === "number") { + order = leftValue - rightValue; + } else { + order = String(leftValue).localeCompare(String(rightValue)); + } + if (order === 0) { + // 同值时用请求 ID 兜底,保证排序稳定(数组排序在 V8 中稳定,但仍显式保证) + order = (left.requestId ?? "").localeCompare(right.requestId ?? ""); + } + return direction === "desc" ? -order : order; +} + +function SortableHeader({ + label, + sortKey, + activeKey, + direction, + onSort, + align = "left", +}: { + label: string; + sortKey: RequestSortKey; + activeKey: RequestSortKey; + direction: SortDirection; + onSort: (key: RequestSortKey) => void; + align?: "left" | "right"; +}) { + const active = activeKey === sortKey; + return ( +
| 请求时间 | +账号 | } -消耗 | +模型 | 客户端 | 请求 ID |
|---|