From 9b2ac4e45f98975ffb9214a86a04d40c069151b6 Mon Sep 17 00:00:00 2001 From: Jeff Gu Date: Tue, 14 Apr 2026 20:05:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(usage-logs):=20=E4=BC=98=E5=8C=96=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=AF=A6=E6=83=85=E5=B1=95=E7=A4=BA=E4=B8=8E=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/UsageLogs.tsx | 531 +++++++++++++++++++------------ 1 file changed, 319 insertions(+), 212 deletions(-) diff --git a/frontend/src/pages/UsageLogs.tsx b/frontend/src/pages/UsageLogs.tsx index 5148b1c..b2221a4 100644 --- a/frontend/src/pages/UsageLogs.tsx +++ b/frontend/src/pages/UsageLogs.tsx @@ -1,10 +1,17 @@ import { startTransition, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { apiClient } from "@/api/client"; -import { AnalyticsEventItem, AnalyticsRange, UsageLogFilterDimension, UsageLogFilters, UsageLogSearchData } from "@/types"; +import { + AnalyticsEventItem, + AnalyticsRange, + UsageLogFilterDimension, + UsageLogFilters, + UsageLogSearchData, +} from "@/types"; import { PageContainer } from "@/components/ui/page-container"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; @@ -164,13 +171,63 @@ const ClientSummary = ({ item }: { item: AnalyticsEventItem }) => { ); }; -const DetailField = ({ label, value, mono = false }: { label: string; value: string | number; mono?: boolean }) => { - const text = String(value || "--"); +const formatDetailValue = (value: string | number | null | undefined): string => { + if (value === null || value === undefined) { + return "--"; + } + + if (typeof value === "string" && value.trim().length === 0) { + return "--"; + } + + return String(value); +}; + +const DetailMetric = ({ label, value }: { label: string; value: string | number }) => { + const text = formatDetailValue(value); return ( -
-
{label}
-
{text}
+
+
{label}
+
{text}
+
+ ); +}; + +const DetailSection = ({ title, children }: { title: string; children: React.ReactNode }) => { + return ( +
+
{title}
+ {children} +
+ ); +}; + +const DetailRow = ({ + label, + value, + mono = false, + tone = "default", +}: { + label: string; + value: string | number; + mono?: boolean; + tone?: "default" | "success" | "danger" | "subtle"; +}) => { + const text = formatDetailValue(value); + const toneClassName = + tone === "success" + ? "text-emerald-600 dark:text-emerald-400" + : tone === "danger" + ? "text-rose-600 dark:text-rose-400" + : tone === "subtle" + ? "text-muted-foreground" + : "text-foreground"; + + return ( +
+
{label}
+
{text}
); }; @@ -188,12 +245,8 @@ export function UsageLogs() { () => hydrateUsageLogFilters(cachedLogsSnapshot?.data.appliedFilters, defaultFilters), [cachedLogsSnapshot, defaultFilters], ); - const [draftFilters, setDraftFilters] = useState( - () => initialDraftFilters, - ); - const [appliedFilters, setAppliedFilters] = useState( - () => initialAppliedFilters, - ); + const [draftFilters, setDraftFilters] = useState(() => initialDraftFilters); + const [appliedFilters, setAppliedFilters] = useState(() => initialAppliedFilters); const [currentPage, setCurrentPage] = useState(() => cachedLogsSnapshot?.data.currentPage ?? 1); const [selectedItem, setSelectedItem] = useState(null); const initialLogsData = @@ -270,28 +323,6 @@ export function UsageLogs() { } >
- {/* - -
-
-
- - Diagnostics -
-

面向排障的请求日志视图

-

- 记录 request id、trace id、错误摘要、IP、UA 与地理位置,适合定位某一批异常设备或单个失败链路。 -

-
-
- - - -
-
-
-
*/} - {logsQuery.data?.compatibilityWarning && ( @@ -374,208 +405,284 @@ export function UsageLogs() { - - - 日志结果 - 日志可能存在 30 秒左右的延迟 - - - {logsQuery.isLoading ? ( -
日志加载中...
- ) : logsQuery.data?.items.length ? ( -
- - - - - - - - - - - - - - {logsQuery.data.items.map((item) => ( - - + + {logsQuery.isLoading ? ( +
日志加载中...
+ ) : logsQuery.data?.items.length ? ( +
+
时间模型 / 令牌端点 / 渠道结果 / 状态方式 / 用时资源消耗详情
-
{formatDateTime(item.timestamp)}
-
{item.requestId || "--"}
-
+ + + + + + + + + + + + + {logsQuery.data.items.map((item) => ( + + - - + + - - - - - - - - - ))} - -
时间模型 / 令牌端点 / 渠道结果 / 状态方式 / 用时资源消耗详情
+
{formatDateTime(item.timestamp)}
+
{item.requestId || "--"}
+
-
{item.requestedModel || "--"}
-
{item.tokenName || "未命名令牌"}
-
-
{item.providerType || "--"}
-
{item.channelKey || "--"}
-
+
{item.requestedModel || "--"}
+
{item.tokenName || "未命名令牌"}
+
+
{item.providerType || "--"}
+
{item.channelKey || "--"}
+
-
- - {item.result === "success" ? "成功" : "失败"} - -
-
- {item.upstreamStatus || "--"} · 重试 {item.retryCount} -
-
-
- {item.streamMode === "stream" ? "流式" : "非流式"} -
-
{formatDuration(item.latencyMs)}
-
- {/*
{formatCompactNumber(item.totalTokens)} tokens
*/} -
- 输入 {formatCompactNumber(item.promptTokens)} / 输出{" "} - {formatCompactNumber(item.completionTokens)} -
-
{formatCurrency(item.totalCost, displayDecimals)}
-
- +
+ - - -
-
-
- {totalItems > 0 ? `显示第 ${currentStart}-${currentEnd} 条,共 ${totalItems} 条` : "暂无记录"} -
-
+ {item.result === "success" ? "成功" : "失败"} + +
+
+ {item.upstreamStatus || "--"} · 重试 {item.retryCount} +
+ + + +
+ {item.streamMode === "stream" ? "流式" : "非流式"} +
+
{formatDuration(item.latencyMs)}
+ + + + {/*
{formatCompactNumber(item.totalTokens)} tokens
*/} +
+ 输入 {formatCompactNumber(item.promptTokens)} / 输出{" "} + {formatCompactNumber(item.completionTokens)} +
+
+ {formatCurrency(item.totalCost, displayDecimals)} +
+ + + + + + + ))} + + +
+
+ {totalItems > 0 ? `显示第 ${currentStart}-${currentEnd} 条,共 ${totalItems} 条` : "暂无记录"} +
+
+ + {visiblePages.map((page) => ( - {visiblePages.map((page) => ( - - ))} - -
+ ))} +
- ) : ( -
-

没有匹配的日志记录

-

调整时间范围、维度或关键字后重试。

-
- )} - +
+ ) : ( +
+

没有匹配的日志记录

+

调整时间范围、维度或关键字后重试。

+
+ )}
!open && setSelectedItem(null)}> - + - 使用日志详情 - 查看本次请求的完整上下文、客户端信息、错误摘要与资源消耗。 + 日志详情 {selectedItem && ( -
-
- - - - -
+
+
+
+
+
+ + {selectedItem.result === "success" ? "成功" : "失败"} + + + {selectedItem.streamMode === "stream" ? "流式" : "非流式"} + + + {formatDetailValue(selectedItem.routeId)} + +
-
- - - - -
+
+
+
+ {formatDetailValue(selectedItem.requestedModel || selectedItem.upstreamModel)} +
+
+
+
+ 令牌 +
+ {formatDetailValue(selectedItem.tokenName)} +
+
+ 端点 +
+ {formatDetailValue(selectedItem.providerType)} +
+
+ 渠道 +
+ {formatDetailValue(selectedItem.channelKey)} +
+
+
+
+
-
- - - - -
+
+ + + + +
+
+
-
- - -
+ +
+
+ + + + + +
+
+ + + + + +
+
+
-
- - - -
+ +
+
+ + + +
+
+ + + +
+
+
-
- -
+ +
+
+ + +
+
+ + +
+
-
- - -
+ {selectedItem.result !== "success" && selectedItem.errorSummary && ( +
+ {formatDetailValue(selectedItem.errorSummary)} +
+ )} +
-
- - - - -
+ +
+
+ + +
+
+ + +
+
+
)}