mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
Add UsageDashboard with summary cards, trend chart, and data tables. Implement model pricing configuration panel. Add request log viewer with filtering and detail panel.
165 lines
5.7 KiB
TypeScript
165 lines
5.7 KiB
TypeScript
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { useRequestLogs } from "@/lib/query/usage";
|
|
import { RequestDetailPanel } from "./RequestDetailPanel";
|
|
import type { LogFilters } from "@/types/usage";
|
|
|
|
export function RequestLogTable() {
|
|
const { t } = useTranslation();
|
|
const [filters] = useState<LogFilters>({});
|
|
const [page, setPage] = useState(0);
|
|
const [selectedRequestId, setSelectedRequestId] = useState<string | null>(
|
|
null,
|
|
);
|
|
const limit = 20;
|
|
|
|
const { data: logs, isLoading } = useRequestLogs(
|
|
filters,
|
|
limit,
|
|
page * limit,
|
|
);
|
|
|
|
if (isLoading) {
|
|
return <div className="h-[400px] animate-pulse rounded bg-gray-100" />;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="rounded-md bg-card/60 shadow-sm">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>{t("usage.time", "时间")}</TableHead>
|
|
<TableHead>{t("usage.provider", "供应商")}</TableHead>
|
|
<TableHead>{t("usage.billingModel", "计费模型")}</TableHead>
|
|
<TableHead className="text-right">
|
|
{t("usage.inputTokens", "输入")}
|
|
</TableHead>
|
|
<TableHead className="text-right">
|
|
{t("usage.outputTokens", "输出")}
|
|
</TableHead>
|
|
<TableHead className="text-right">
|
|
{t("usage.cacheCreationTokens", "缓存写入")}
|
|
</TableHead>
|
|
<TableHead className="text-right">
|
|
{t("usage.cacheReadTokens", "缓存读取")}
|
|
</TableHead>
|
|
<TableHead className="text-right">
|
|
{t("usage.totalCost", "成本")}
|
|
</TableHead>
|
|
<TableHead className="text-right">
|
|
{t("usage.latency", "耗时")}
|
|
</TableHead>
|
|
<TableHead>{t("usage.status", "状态")}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{logs?.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={7}
|
|
className="text-center text-muted-foreground"
|
|
>
|
|
{t("usage.noData", "暂无数据")}
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
logs?.map((log) => (
|
|
<TableRow
|
|
key={log.requestId}
|
|
className="cursor-pointer hover:bg-muted/50"
|
|
onClick={() => setSelectedRequestId(log.requestId)}
|
|
>
|
|
<TableCell>
|
|
{new Date(log.createdAt * 1000).toLocaleString("zh-CN")}
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex flex-col text-sm leading-tight">
|
|
<span className="font-medium">
|
|
{log.providerName ||
|
|
t("usage.unknownProvider", "未知供应商")}
|
|
</span>
|
|
<span className="font-mono text-xs text-muted-foreground">
|
|
{log.providerId}
|
|
</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="font-mono text-sm">
|
|
{log.model}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{log.inputTokens.toLocaleString()}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{log.outputTokens.toLocaleString()}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{log.cacheCreationTokens.toLocaleString()}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{log.cacheReadTokens.toLocaleString()}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
${parseFloat(log.totalCostUsd).toFixed(6)}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{log.latencyMs}ms
|
|
</TableCell>
|
|
<TableCell>
|
|
<span
|
|
className={`inline-flex rounded-full px-2 py-1 text-xs ${
|
|
log.statusCode >= 200 && log.statusCode < 300
|
|
? "bg-green-100 text-green-800"
|
|
: "bg-red-100 text-red-800"
|
|
}`}
|
|
>
|
|
{log.statusCode}
|
|
</span>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{logs && logs.length >= limit && (
|
|
<div className="flex justify-center gap-2">
|
|
<button
|
|
onClick={() => setPage(Math.max(0, page - 1))}
|
|
disabled={page === 0}
|
|
className="rounded border px-3 py-1 disabled:opacity-50"
|
|
>
|
|
{t("common.previous", "上一页")}
|
|
</button>
|
|
<span className="px-3 py-1">
|
|
{t("common.page", "第")} {page + 1} {t("common.pageUnit", "页")}
|
|
</span>
|
|
<button
|
|
onClick={() => setPage(page + 1)}
|
|
disabled={logs.length < limit}
|
|
className="rounded border px-3 py-1 disabled:opacity-50"
|
|
>
|
|
{t("common.next", "下一页")}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{selectedRequestId && (
|
|
<RequestDetailPanel
|
|
requestId={selectedRequestId}
|
|
onClose={() => setSelectedRequestId(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|