import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; 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, i18n } = useTranslation(); 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 pageSize = 20; const { data: result, isLoading } = useRequestLogs(filters, page, pageSize); 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), }); }; // 将 Unix 时间戳转换为本地时间的 datetime-local 格式 const timestampToLocalDatetime = (timestamp: number): string => { const date = new Date(timestamp * 1000); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); return `${year}-${month}-${day}T${hours}:${minutes}`; }; // 将 datetime-local 格式转换为 Unix 时间戳 const localDatetimeToTimestamp = (datetime: string): number | undefined => { if (!datetime) return undefined; // 验证格式是否完整 (YYYY-MM-DDTHH:mm) if (datetime.length < 16) return undefined; const timestamp = new Date(datetime).getTime(); // 验证是否为有效日期 if (isNaN(timestamp)) return undefined; return Math.floor(timestamp / 1000); }; const dateLocale = i18n.language === "zh" ? "zh-CN" : i18n.language === "ja" ? "ja-JP" : "en-US"; return (
{/* 筛选栏 */}
setTempFilters({ ...tempFilters, providerName: e.target.value || undefined, }) } />
setTempFilters({ ...tempFilters, model: e.target.value || undefined, }) } />
{t("usage.timeRange")}: { const timestamp = localDatetimeToTimestamp(e.target.value); setTempFilters({ ...tempFilters, startDate: timestamp, }); }} /> - { const timestamp = localDatetimeToTimestamp(e.target.value); setTempFilters({ ...tempFilters, endDate: timestamp, }); }} />
{isLoading ? (
) : ( <>
{t("usage.time")} {t("usage.provider")} {t("usage.billingModel")} {t("usage.inputTokens")} {t("usage.outputTokens")} {t("usage.cacheReadTokens")} {t("usage.cacheCreationTokens")} {t("usage.multiplier")} {t("usage.totalCost")} {t("usage.timingInfo")} {t("usage.status")} {logs.length === 0 ? ( {t("usage.noData")} ) : ( logs.map((log) => ( {new Date(log.createdAt * 1000).toLocaleString( dateLocale, )} {log.providerName || t("usage.unknownProvider")}
{log.model}
{log.requestModel && log.requestModel !== log.model && (
← {log.requestModel}
)}
{log.inputTokens.toLocaleString()} {log.outputTokens.toLocaleString()} {log.cacheReadTokens.toLocaleString()} {log.cacheCreationTokens.toLocaleString()} {parseFloat(log.costMultiplier) !== 1 ? ( ×{log.costMultiplier} ) : ( ×1 )} ${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}
)) )}
{/* 分页控件 */} {total > 0 && (
{t("usage.totalRecords", { 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" ? ( ... ) : ( ), ); })()}
)} )}
); }