diff --git a/src/components/usage/RequestLogTable.tsx b/src/components/usage/RequestLogTable.tsx index 1a667bb82..26dbc66b9 100644 --- a/src/components/usage/RequestLogTable.tsx +++ b/src/components/usage/RequestLogTable.tsx @@ -8,156 +8,392 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { useRequestLogs } from "@/lib/query/usage"; -import { RequestDetailPanel } from "./RequestDetailPanel"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useRequestLogs, usageKeys } from "@/lib/query/usage"; +import { useQueryClient } from "@tanstack/react-query"; import type { LogFilters } from "@/types/usage"; +import { ChevronLeft, ChevronRight, RefreshCw, Search, X } from "lucide-react"; export function RequestLogTable() { const { t } = useTranslation(); - const [filters] = useState({}); + const queryClient = useQueryClient(); + + // 默认时间范围:过去24小时 + const getDefaultFilters = (): LogFilters => { + const now = Math.floor(Date.now() / 1000); + const oneDayAgo = now - 24 * 60 * 60; + return { startDate: oneDayAgo, endDate: now }; + }; + + const [filters, setFilters] = useState(getDefaultFilters); + const [tempFilters, setTempFilters] = useState(getDefaultFilters); const [page, setPage] = useState(0); - const [selectedRequestId, setSelectedRequestId] = useState( - null, - ); - const limit = 20; + const pageSize = 20; - const { data: logs, isLoading } = useRequestLogs( - filters, - limit, - page * limit, - ); + const { data: result, isLoading } = useRequestLogs(filters, page, pageSize); - if (isLoading) { - return
; - } + const logs = result?.data ?? []; + const total = result?.total ?? 0; + const totalPages = Math.ceil(total / pageSize); + + const handleSearch = () => { + setFilters(tempFilters); + setPage(0); + }; + + const handleReset = () => { + const defaults = getDefaultFilters(); + setTempFilters(defaults); + setFilters(defaults); + setPage(0); + }; + + const handleRefresh = () => { + queryClient.invalidateQueries({ + queryKey: usageKeys.logs(filters, page, pageSize), + }); + }; return (
-
- - - - {t("usage.time", "时间")} - {t("usage.provider", "供应商")} - {t("usage.billingModel", "计费模型")} - - {t("usage.inputTokens", "输入")} - - - {t("usage.outputTokens", "输出")} - - - {t("usage.cacheCreationTokens", "缓存写入")} - - - {t("usage.cacheReadTokens", "缓存读取")} - - - {t("usage.totalCost", "成本")} - - - {t("usage.latency", "耗时")} - - {t("usage.status", "状态")} - - - - {logs?.length === 0 ? ( - - - {t("usage.noData", "暂无数据")} - - - ) : ( - logs?.map((log) => ( - setSelectedRequestId(log.requestId)} - > - - {new Date(log.createdAt * 1000).toLocaleString("zh-CN")} - - -
- - {log.providerName || - t("usage.unknownProvider", "未知供应商")} - - - {log.providerId} - -
-
- - {log.model} - - - {log.inputTokens.toLocaleString()} - - - {log.outputTokens.toLocaleString()} - - - {log.cacheCreationTokens.toLocaleString()} - - - {log.cacheReadTokens.toLocaleString()} - - - ${parseFloat(log.totalCostUsd).toFixed(6)} - - - {log.latencyMs}ms - - - = 200 && log.statusCode < 300 - ? "bg-green-100 text-green-800" - : "bg-red-100 text-red-800" - }`} - > - {log.statusCode} - - -
- )) - )} -
-
+ {/* 筛选栏 */} +
+ + + + + + setTempFilters({ + ...tempFilters, + providerName: e.target.value || undefined, + }) + } + /> + + + setTempFilters({ + ...tempFilters, + model: e.target.value || undefined, + }) + } + /> + + + setTempFilters({ + ...tempFilters, + startDate: e.target.value + ? Math.floor(new Date(e.target.value).getTime() / 1000) + : undefined, + }) + } + /> + + + setTempFilters({ + ...tempFilters, + endDate: e.target.value + ? Math.floor(new Date(e.target.value).getTime() / 1000) + : undefined, + }) + } + /> + +
+ + + +
- {logs && logs.length >= limit && ( -
- - - {t("common.page", "第")} {page + 1} {t("common.pageUnit", "页")} - - -
- )} + {isLoading ? ( +
+ ) : ( + <> +
+ + + + {t("usage.time", "时间")} + {t("usage.provider", "供应商")} + + {t("usage.billingModel", "计费模型")} + + + {t("usage.inputTokens", "输入")} + + + {t("usage.outputTokens", "输出")} + + + {t("usage.cacheCreationTokens", "缓存写入")} + + + {t("usage.cacheReadTokens", "缓存读取")} + + + {t("usage.totalCost", "成本")} + + + {t("usage.timingInfo", "用时/首字")} + + {t("usage.status", "状态")} + + + + {logs.length === 0 ? ( + + + {t("usage.noData", "暂无数据")} + + + ) : ( + logs.map((log) => ( + + + {new Date(log.createdAt * 1000).toLocaleString("zh-CN")} + + + {log.providerName || + t("usage.unknownProvider", "未知供应商")} + + + {log.model} + + + {log.inputTokens.toLocaleString()} + + + {log.outputTokens.toLocaleString()} + + + {log.cacheCreationTokens.toLocaleString()} + + + {log.cacheReadTokens.toLocaleString()} + + + ${parseFloat(log.totalCostUsd).toFixed(6)} + + +
+ {(() => { + const durationSec = + (log.durationMs ?? log.latencyMs) / 1000; + const durationColor = + durationSec <= 5 + ? "bg-green-100 text-green-800" + : durationSec <= 120 + ? "bg-orange-100 text-orange-800" + : "bg-red-200 text-red-900"; + return ( + + {Math.round(durationSec)}s + + ); + })()} + {log.isStreaming && + log.firstTokenMs != null && + (() => { + const firstSec = log.firstTokenMs / 1000; + const firstColor = + firstSec <= 5 + ? "bg-green-100 text-green-800" + : firstSec <= 120 + ? "bg-orange-100 text-orange-800" + : "bg-red-200 text-red-900"; + return ( + + {firstSec.toFixed(1)}s + + ); + })()} + + {log.isStreaming + ? t("usage.stream", "流") + : t("usage.nonStream", "非流")} + +
+
+ + = 200 && log.statusCode < 300 + ? "bg-green-100 text-green-800" + : "bg-red-100 text-red-800" + }`} + > + {log.statusCode} + + +
+ )) + )} +
+
+
- {selectedRequestId && ( - setSelectedRequestId(null)} - /> + {/* 分页控件 */} + {total > 0 && ( +
+ + {t("usage.totalRecords", "共 {{total}} 条记录", { total })} + +
+ + {/* 页码按钮 */} + {(() => { + const pages: (number | string)[] = []; + if (totalPages <= 7) { + for (let i = 0; i < totalPages; i++) pages.push(i); + } else { + pages.push(0); + if (page > 2) pages.push("..."); + for ( + let i = Math.max(1, page - 1); + i <= Math.min(totalPages - 2, page + 1); + i++ + ) { + pages.push(i); + } + if (page < totalPages - 3) pages.push("..."); + pages.push(totalPages - 1); + } + return pages.map((p, idx) => + typeof p === "string" ? ( + + ... + + ) : ( + + ), + ); + })()} + +
+
+ )} + )}
);