feat(ui): add usage dashboard components

Add UsageDashboard with summary cards, trend chart, and data tables.
Implement model pricing configuration panel.
Add request log viewer with filtering and detail panel.
This commit is contained in:
YoVinchen
2025-12-03 00:25:41 +08:00
parent 38e8949152
commit 434ffd559d
9 changed files with 863 additions and 290 deletions
+94 -27
View File
@@ -1,16 +1,31 @@
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 type { LogFilters } from '@/types/usage';
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);
const { data: logs, isLoading } = useRequestLogs(
filters,
limit,
page * limit,
);
if (isLoading) {
return <div className="h-[400px] animate-pulse rounded bg-gray-100" />;
@@ -18,47 +33,92 @@ export function RequestLogTable() {
return (
<div className="space-y-4">
<div className="rounded-md border">
<div className="rounded-md bg-card/60 shadow-sm">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t('usage.time', '时间')}</TableHead>
<TableHead>{t('usage.provider', 'Provider')}</TableHead>
<TableHead>{t('usage.model', '模型')}</TableHead>
<TableHead className="text-right">{t('usage.tokens', 'Tokens')}</TableHead>
<TableHead className="text-right">{t('usage.cost', '成本')}</TableHead>
<TableHead className="text-right">{t('usage.latency', '延迟')}</TableHead>
<TableHead>{t('usage.status', '状态')}</TableHead>
<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
colSpan={7}
className="text-center text-muted-foreground"
>
{t("usage.noData", "暂无数据")}
</TableCell>
</TableRow>
) : (
logs?.map((log) => (
<TableRow key={log.requestId}>
<TableRow
key={log.requestId}
className="cursor-pointer hover:bg-muted/50"
onClick={() => setSelectedRequestId(log.requestId)}
>
<TableCell>
{new Date(log.createdAt * 1000).toLocaleString('zh-CN')}
{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="font-mono text-sm">{log.providerId}</TableCell>
<TableCell className="font-mono text-sm">{log.model}</TableCell>
<TableCell className="text-right">
{(log.inputTokens + log.outputTokens).toLocaleString()}
{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 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'
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{log.statusCode}
@@ -78,20 +138,27 @@ export function RequestLogTable() {
disabled={page === 0}
className="rounded border px-3 py-1 disabled:opacity-50"
>
{t('common.previous', '上一页')}
{t("common.previous", "上一页")}
</button>
<span className="px-3 py-1">
{t('common.page', '第')} {page + 1} {t('common.pageUnit', '页')}
{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', '下一页')}
{t("common.next", "下一页")}
</button>
</div>
)}
{selectedRequestId && (
<RequestDetailPanel
requestId={selectedRequestId}
onClose={() => setSelectedRequestId(null)}
/>
)}
</div>
);
}