From 87b80c66b2767fec9f0a62e3f62ab4afee80567f Mon Sep 17 00:00:00 2001 From: Dex Miller Date: Fri, 6 Feb 2026 22:00:33 +0800 Subject: [PATCH] feat(usage): enhance dashboard with auto-refresh control and robust formatting (#942) * style: format code and apply clippy lint fixes * feat(usage): enhance dashboard with auto-refresh control and robust formatting - Add configurable auto-refresh interval toggle (off/5s/10s/30s/60s) to usage dashboard - Extract shared format utilities (fmtUsd, fmtInt, parseFiniteNumber, getLocaleFromLanguage) - Refactor request log time filtering to rolling vs fixed mode with validation - Use stable serializable query keys instead of filter objects - Handle NaN/Infinity safely in number formatting across all usage components - Use RFC 3339 date format in backend trend data --- src-tauri/src/services/usage_stats.rs | 2 +- src/components/usage/ModelStatsTable.tsx | 15 +- src/components/usage/ProviderStatsTable.tsx | 15 +- src/components/usage/RequestLogTable.tsx | 217 ++++++++++++++------ src/components/usage/UsageDashboard.tsx | 80 +++++--- src/components/usage/UsageSummaryCards.tsx | 15 +- src/components/usage/UsageTrendChart.tsx | 33 +-- src/components/usage/format.ts | 39 ++++ src/lib/query/usage.ts | 115 ++++++++--- 9 files changed, 401 insertions(+), 130 deletions(-) create mode 100644 src/components/usage/format.ts diff --git a/src-tauri/src/services/usage_stats.rs b/src-tauri/src/services/usage_stats.rs index 759f8f2c..d050b864 100644 --- a/src-tauri/src/services/usage_stats.rs +++ b/src-tauri/src/services/usage_stats.rs @@ -272,7 +272,7 @@ impl Database { .single() .unwrap_or_else(Local::now); - let date = bucket_start.format("%Y-%m-%dT%H:%M:%S").to_string(); + let date = bucket_start.to_rfc3339(); if let Some(mut stat) = map.remove(&i) { stat.date = date; diff --git a/src/components/usage/ModelStatsTable.tsx b/src/components/usage/ModelStatsTable.tsx index 171fffd0..20f83e62 100644 --- a/src/components/usage/ModelStatsTable.tsx +++ b/src/components/usage/ModelStatsTable.tsx @@ -8,10 +8,17 @@ import { TableRow, } from "@/components/ui/table"; import { useModelStats } from "@/lib/query/usage"; +import { fmtUsd } from "./format"; -export function ModelStatsTable() { +interface ModelStatsTableProps { + refreshIntervalMs: number; +} + +export function ModelStatsTable({ refreshIntervalMs }: ModelStatsTableProps) { const { t } = useTranslation(); - const { data: stats, isLoading } = useModelStats(); + const { data: stats, isLoading } = useModelStats({ + refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false, + }); if (isLoading) { return
; @@ -60,10 +67,10 @@ export function ModelStatsTable() { {stat.totalTokens.toLocaleString()} - ${parseFloat(stat.totalCost).toFixed(4)} + {fmtUsd(stat.totalCost, 4)} - ${parseFloat(stat.avgCostPerRequest).toFixed(6)} + {fmtUsd(stat.avgCostPerRequest, 6)} )) diff --git a/src/components/usage/ProviderStatsTable.tsx b/src/components/usage/ProviderStatsTable.tsx index be6e72f5..e21e1f6c 100644 --- a/src/components/usage/ProviderStatsTable.tsx +++ b/src/components/usage/ProviderStatsTable.tsx @@ -8,10 +8,19 @@ import { TableRow, } from "@/components/ui/table"; import { useProviderStats } from "@/lib/query/usage"; +import { fmtUsd } from "./format"; -export function ProviderStatsTable() { +interface ProviderStatsTableProps { + refreshIntervalMs: number; +} + +export function ProviderStatsTable({ + refreshIntervalMs, +}: ProviderStatsTableProps) { const { t } = useTranslation(); - const { data: stats, isLoading } = useProviderStats(); + const { data: stats, isLoading } = useProviderStats({ + refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false, + }); if (isLoading) { return
; @@ -63,7 +72,7 @@ export function ProviderStatsTable() { {stat.totalTokens.toLocaleString()} - ${parseFloat(stat.totalCost).toFixed(4)} + {fmtUsd(stat.totalCost, 4)} {stat.successRate.toFixed(1)}% diff --git a/src/components/usage/RequestLogTable.tsx b/src/components/usage/RequestLogTable.tsx index e069d1b6..691a8e07 100644 --- a/src/components/usage/RequestLogTable.tsx +++ b/src/components/usage/RequestLogTable.tsx @@ -21,44 +21,122 @@ 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"; +import { + fmtInt, + fmtUsd, + getLocaleFromLanguage, + parseFiniteNumber, +} from "./format"; -export function RequestLogTable() { +interface RequestLogTableProps { + refreshIntervalMs: number; +} + +const ONE_DAY_SECONDS = 24 * 60 * 60; +const MAX_FIXED_RANGE_SECONDS = 30 * ONE_DAY_SECONDS; + +type TimeMode = "rolling" | "fixed"; + +export function RequestLogTable({ refreshIntervalMs }: RequestLogTableProps) { const { t, i18n } = useTranslation(); const queryClient = useQueryClient(); - // 默认时间范围:过去24小时 - const getDefaultFilters = (): LogFilters => { + const getRollingRange = () => { const now = Math.floor(Date.now() / 1000); - const oneDayAgo = now - 24 * 60 * 60; + const oneDayAgo = now - ONE_DAY_SECONDS; return { startDate: oneDayAgo, endDate: now }; }; - const [filters, setFilters] = useState(getDefaultFilters); - const [tempFilters, setTempFilters] = useState(getDefaultFilters); + const [appliedTimeMode, setAppliedTimeMode] = useState("rolling"); + const [draftTimeMode, setDraftTimeMode] = useState("rolling"); + + const [appliedFilters, setAppliedFilters] = useState({}); + const [draftFilters, setDraftFilters] = useState({}); const [page, setPage] = useState(0); const pageSize = 20; + const [validationError, setValidationError] = useState(null); - const { data: result, isLoading } = useRequestLogs(filters, page, pageSize); + const { data: result, isLoading } = useRequestLogs({ + filters: appliedFilters, + timeMode: appliedTimeMode, + rollingWindowSeconds: ONE_DAY_SECONDS, + page, + pageSize, + options: { + refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false, + }, + }); const logs = result?.data ?? []; const total = result?.total ?? 0; const totalPages = Math.ceil(total / pageSize); const handleSearch = () => { - setFilters(tempFilters); + setValidationError(null); + + if (draftTimeMode === "fixed") { + const start = draftFilters.startDate; + const end = draftFilters.endDate; + + if (typeof start !== "number" || typeof end !== "number") { + setValidationError( + t("usage.invalidTimeRange", "请选择完整的开始/结束时间"), + ); + return; + } + + if (start > end) { + setValidationError( + t("usage.invalidTimeRangeOrder", "开始时间不能晚于结束时间"), + ); + return; + } + + if (end - start > MAX_FIXED_RANGE_SECONDS) { + setValidationError( + t("usage.timeRangeTooLarge", "时间范围过大,请缩小范围"), + ); + return; + } + } + + setAppliedTimeMode(draftTimeMode); + setAppliedFilters((prev) => { + const next = { ...prev, ...draftFilters }; + if (draftTimeMode === "rolling") { + delete next.startDate; + delete next.endDate; + } + return next; + }); setPage(0); }; const handleReset = () => { - const defaults = getDefaultFilters(); - setTempFilters(defaults); - setFilters(defaults); + setValidationError(null); + setAppliedTimeMode("rolling"); + setDraftTimeMode("rolling"); + setDraftFilters({}); + setAppliedFilters({}); setPage(0); }; const handleRefresh = () => { + const key = { + timeMode: appliedTimeMode, + rollingWindowSeconds: + appliedTimeMode === "rolling" ? ONE_DAY_SECONDS : undefined, + appType: appliedFilters.appType, + providerName: appliedFilters.providerName, + model: appliedFilters.model, + statusCode: appliedFilters.statusCode, + startDate: + appliedTimeMode === "fixed" ? appliedFilters.startDate : undefined, + endDate: appliedTimeMode === "fixed" ? appliedFilters.endDate : undefined, + }; + queryClient.invalidateQueries({ - queryKey: usageKeys.logs(filters, page, pageSize), + queryKey: usageKeys.logs(key, page, pageSize), }); }; @@ -84,12 +162,11 @@ export function RequestLogTable() { return Math.floor(timestamp / 1000); }; - const dateLocale = - i18n.language === "zh" - ? "zh-CN" - : i18n.language === "ja" - ? "ja-JP" - : "en-US"; + const language = i18n.resolvedLanguage || i18n.language || "en"; + const locale = getLocaleFromLanguage(language); + + const rollingRangeForDisplay = + draftTimeMode === "rolling" ? getRollingRange() : null; return (
@@ -97,10 +174,10 @@ export function RequestLogTable() {
- setTempFilters({ - ...tempFilters, + setDraftFilters({ + ...draftFilters, providerName: e.target.value || undefined, }) } @@ -156,10 +238,10 @@ export function RequestLogTable() { - setTempFilters({ - ...tempFilters, + setDraftFilters({ + ...draftFilters, model: e.target.value || undefined, }) } @@ -174,14 +256,18 @@ export function RequestLogTable() { type="datetime-local" className="h-8 w-[200px] bg-background" value={ - tempFilters.startDate - ? timestampToLocalDatetime(tempFilters.startDate) + (rollingRangeForDisplay?.startDate ?? draftFilters.startDate) + ? timestampToLocalDatetime( + (rollingRangeForDisplay?.startDate ?? + draftFilters.startDate) as number, + ) : "" } onChange={(e) => { const timestamp = localDatetimeToTimestamp(e.target.value); - setTempFilters({ - ...tempFilters, + setDraftTimeMode("fixed"); + setDraftFilters({ + ...draftFilters, startDate: timestamp, }); }} @@ -191,14 +277,18 @@ export function RequestLogTable() { type="datetime-local" className="h-8 w-[200px] bg-background" value={ - tempFilters.endDate - ? timestampToLocalDatetime(tempFilters.endDate) + (rollingRangeForDisplay?.endDate ?? draftFilters.endDate) + ? timestampToLocalDatetime( + (rollingRangeForDisplay?.endDate ?? + draftFilters.endDate) as number, + ) : "" } onChange={(e) => { const timestamp = localDatetimeToTimestamp(e.target.value); - setTempFilters({ - ...tempFilters, + setDraftTimeMode("fixed"); + setDraftFilters({ + ...draftFilters, endDate: timestamp, }); }} @@ -234,6 +324,10 @@ export function RequestLogTable() {
+ + {validationError && ( +
{validationError}
+ )}
{isLoading ? ( @@ -293,9 +387,7 @@ export function RequestLogTable() { logs.map((log) => ( - {new Date(log.createdAt * 1000).toLocaleString( - dateLocale, - )} + {new Date(log.createdAt * 1000).toLocaleString(locale)} {log.providerName || t("usage.unknownProvider")} @@ -321,19 +413,19 @@ export function RequestLogTable() { )} - {log.inputTokens.toLocaleString()} + {fmtInt(log.inputTokens, locale)} - {log.outputTokens.toLocaleString()} + {fmtInt(log.outputTokens, locale)} - {log.cacheReadTokens.toLocaleString()} + {fmtInt(log.cacheReadTokens, locale)} - {log.cacheCreationTokens.toLocaleString()} + {fmtInt(log.cacheCreationTokens, locale)} - {parseFloat(log.costMultiplier) !== 1 ? ( + {(parseFiniteNumber(log.costMultiplier) ?? 1) !== 1 ? ( ×{log.costMultiplier} @@ -342,24 +434,30 @@ export function RequestLogTable() { )} - ${parseFloat(log.totalCostUsd).toFixed(6)} + {fmtUsd(log.totalCostUsd, 6)}
{(() => { - const durationSec = - (log.durationMs ?? log.latencyMs) / 1000; - const durationColor = - durationSec <= 5 + const durationMs = + typeof log.durationMs === "number" + ? log.durationMs + : log.latencyMs; + const durationSec = durationMs / 1000; + const durationColor = Number.isFinite(durationSec) + ? durationSec <= 5 ? "bg-green-100 text-green-800" : durationSec <= 120 ? "bg-orange-100 text-orange-800" - : "bg-red-200 text-red-900"; + : "bg-red-200 text-red-900" + : "bg-gray-100 text-gray-700"; return ( - {Math.round(durationSec)}s + {Number.isFinite(durationSec) + ? `${Math.round(durationSec)}s` + : "--"} ); })()} @@ -367,17 +465,20 @@ export function RequestLogTable() { log.firstTokenMs != null && (() => { const firstSec = log.firstTokenMs / 1000; - const firstColor = - firstSec <= 5 + const firstColor = Number.isFinite(firstSec) + ? firstSec <= 5 ? "bg-green-100 text-green-800" : firstSec <= 120 ? "bg-orange-100 text-orange-800" - : "bg-red-200 text-red-900"; + : "bg-red-200 text-red-900" + : "bg-gray-100 text-gray-700"; return ( - {firstSec.toFixed(1)}s + {Number.isFinite(firstSec) + ? `${firstSec.toFixed(1)}s` + : "--"} ); })()} diff --git a/src/components/usage/UsageDashboard.tsx b/src/components/usage/UsageDashboard.tsx index 49974ae7..01dd3764 100644 --- a/src/components/usage/UsageDashboard.tsx +++ b/src/components/usage/UsageDashboard.tsx @@ -8,11 +8,28 @@ import { ProviderStatsTable } from "./ProviderStatsTable"; import { ModelStatsTable } from "./ModelStatsTable"; import type { TimeRange } from "@/types/usage"; import { motion } from "framer-motion"; -import { BarChart3, ListFilter, Activity } from "lucide-react"; +import { BarChart3, ListFilter, Activity, RefreshCw } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useQueryClient } from "@tanstack/react-query"; +import { usageKeys } from "@/lib/query/usage"; export function UsageDashboard() { const { t } = useTranslation(); + const queryClient = useQueryClient(); const [timeRange, setTimeRange] = useState("1d"); + const [refreshIntervalMs, setRefreshIntervalMs] = useState(30000); + + const refreshIntervalOptionsMs = [0, 5000, 10000, 30000, 60000] as const; + const changeRefreshInterval = () => { + const currentIndex = refreshIntervalOptionsMs.indexOf( + refreshIntervalMs as (typeof refreshIntervalOptionsMs)[number], + ); + const safeIndex = currentIndex >= 0 ? currentIndex : 3; // default 30s + const nextIndex = (safeIndex + 1) % refreshIntervalOptionsMs.length; + const next = refreshIntervalOptionsMs[nextIndex]; + setRefreshIntervalMs(next); + queryClient.invalidateQueries({ queryKey: usageKeys.all }); + }; const days = timeRange === "1d" ? 1 : timeRange === "7d" ? 7 : 30; @@ -34,32 +51,45 @@ export function UsageDashboard() { onValueChange={(v) => setTimeRange(v as TimeRange)} className="w-full sm:w-auto" > - - + + + + {t("usage.today")} + + + {t("usage.last7days")} + + + {t("usage.last30days")} + + +
- + - +
@@ -86,15 +116,15 @@ export function UsageDashboard() { transition={{ delay: 0.2 }} > - + - + - + diff --git a/src/components/usage/UsageSummaryCards.tsx b/src/components/usage/UsageSummaryCards.tsx index f3a6176b..a281b2f2 100644 --- a/src/components/usage/UsageSummaryCards.tsx +++ b/src/components/usage/UsageSummaryCards.tsx @@ -4,19 +4,26 @@ import { Card, CardContent } from "@/components/ui/card"; import { useUsageSummary } from "@/lib/query/usage"; import { Activity, DollarSign, Layers, Database, Loader2 } from "lucide-react"; import { motion } from "framer-motion"; +import { fmtUsd, parseFiniteNumber } from "./format"; interface UsageSummaryCardsProps { days: number; + refreshIntervalMs: number; } -export function UsageSummaryCards({ days }: UsageSummaryCardsProps) { +export function UsageSummaryCards({ + days, + refreshIntervalMs, +}: UsageSummaryCardsProps) { const { t } = useTranslation(); - const { data: summary, isLoading } = useUsageSummary(days); + const { data: summary, isLoading } = useUsageSummary(days, { + refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false, + }); const stats = useMemo(() => { const totalRequests = summary?.totalRequests ?? 0; - const totalCost = parseFloat(summary?.totalCost || "0"); + const totalCost = parseFiniteNumber(summary?.totalCost); const inputTokens = summary?.totalInputTokens ?? 0; const outputTokens = summary?.totalOutputTokens ?? 0; @@ -37,7 +44,7 @@ export function UsageSummaryCards({ days }: UsageSummaryCardsProps) { }, { title: t("usage.totalCost"), - value: `$${totalCost.toFixed(4)}`, + value: totalCost == null ? "--" : fmtUsd(totalCost, 4), icon: DollarSign, color: "text-green-500", bg: "bg-green-500/10", diff --git a/src/components/usage/UsageTrendChart.tsx b/src/components/usage/UsageTrendChart.tsx index 376014e0..ba66a83f 100644 --- a/src/components/usage/UsageTrendChart.tsx +++ b/src/components/usage/UsageTrendChart.tsx @@ -11,14 +11,26 @@ import { } from "recharts"; import { useUsageTrends } from "@/lib/query/usage"; import { Loader2 } from "lucide-react"; +import { + fmtInt, + fmtUsd, + getLocaleFromLanguage, + parseFiniteNumber, +} from "./format"; interface UsageTrendChartProps { days: number; + refreshIntervalMs: number; } -export function UsageTrendChart({ days }: UsageTrendChartProps) { +export function UsageTrendChart({ + days, + refreshIntervalMs, +}: UsageTrendChartProps) { const { t, i18n } = useTranslation(); - const { data: trends, isLoading } = useUsageTrends(days); + const { data: trends, isLoading } = useUsageTrends(days, { + refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false, + }); if (isLoading) { return ( @@ -29,15 +41,12 @@ export function UsageTrendChart({ days }: UsageTrendChartProps) { } const isToday = days === 1; - const dateLocale = - i18n.language === "zh" - ? "zh-CN" - : i18n.language === "ja" - ? "ja-JP" - : "en-US"; + const language = i18n.resolvedLanguage || i18n.language || "en"; + const dateLocale = getLocaleFromLanguage(language); const chartData = trends?.map((stat) => { const pointDate = new Date(stat.date); + const cost = parseFiniteNumber(stat.totalCost); return { rawDate: stat.date, label: isToday @@ -56,7 +65,7 @@ export function UsageTrendChart({ days }: UsageTrendChartProps) { outputTokens: stat.totalOutputTokens, cacheCreationTokens: stat.totalCacheCreationTokens, cacheReadTokens: stat.totalCacheReadTokens, - cost: parseFloat(stat.totalCost), + cost: cost ?? null, }; }) || []; @@ -79,9 +88,9 @@ export function UsageTrendChart({ days }: UsageTrendChartProps) { /> {entry.name}: - {entry.name.includes(t("usage.cost", "成本")) - ? `$${typeof entry.value === "number" ? entry.value.toFixed(6) : entry.value}` - : entry.value.toLocaleString()} + {entry.dataKey === "cost" + ? fmtUsd(entry.value, 6) + : fmtInt(entry.value, dateLocale)}
))} diff --git a/src/components/usage/format.ts b/src/components/usage/format.ts new file mode 100644 index 00000000..e6df4223 --- /dev/null +++ b/src/components/usage/format.ts @@ -0,0 +1,39 @@ +export function parseFiniteNumber(value: unknown): number | null { + if (typeof value === "number") { + return Number.isFinite(value) ? value : null; + } + + if (typeof value === "string") { + const parsed = Number.parseFloat(value); + return Number.isFinite(parsed) ? parsed : null; + } + + return null; +} + +export function fmtInt( + value: unknown, + locale?: string, + fallback: string = "--", +): string { + const num = parseFiniteNumber(value); + if (num == null) return fallback; + return new Intl.NumberFormat(locale).format(Math.trunc(num)); +} + +export function fmtUsd( + value: unknown, + digits: number, + fallback: string = "--", +): string { + const num = parseFiniteNumber(value); + if (num == null) return fallback; + return `$${num.toFixed(digits)}`; +} + +export function getLocaleFromLanguage(language: string): string { + if (!language) return "en-US"; + if (language.startsWith("zh")) return "zh-CN"; + if (language.startsWith("ja")) return "ja-JP"; + return "en-US"; +} diff --git a/src/lib/query/usage.ts b/src/lib/query/usage.ts index bf220c54..180c264d 100644 --- a/src/lib/query/usage.ts +++ b/src/lib/query/usage.ts @@ -2,6 +2,35 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { usageApi } from "@/lib/api/usage"; import type { LogFilters } from "@/types/usage"; +const DEFAULT_REFETCH_INTERVAL_MS = 30000; + +type UsageQueryOptions = { + refetchInterval?: number | false; + refetchIntervalInBackground?: boolean; +}; + +type RequestLogsTimeMode = "rolling" | "fixed"; + +type RequestLogsQueryArgs = { + filters: LogFilters; + timeMode: RequestLogsTimeMode; + page?: number; + pageSize?: number; + rollingWindowSeconds?: number; + options?: UsageQueryOptions; +}; + +type RequestLogsKey = { + timeMode: RequestLogsTimeMode; + rollingWindowSeconds?: number; + appType?: string; + providerName?: string; + model?: string; + statusCode?: number; + startDate?: number; + endDate?: number; +}; + // Query keys export const usageKeys = { all: ["usage"] as const, @@ -9,8 +38,21 @@ export const usageKeys = { trends: (days: number) => [...usageKeys.all, "trends", days] as const, providerStats: () => [...usageKeys.all, "provider-stats"] as const, modelStats: () => [...usageKeys.all, "model-stats"] as const, - logs: (filters: LogFilters, page: number, pageSize: number) => - [...usageKeys.all, "logs", filters, page, pageSize] as const, + logs: (key: RequestLogsKey, page: number, pageSize: number) => + [ + ...usageKeys.all, + "logs", + key.timeMode, + key.rollingWindowSeconds ?? 0, + key.appType ?? "", + key.providerName ?? "", + key.model ?? "", + key.statusCode ?? -1, + key.startDate ?? 0, + key.endDate ?? 0, + page, + pageSize, + ] as const, detail: (requestId: string) => [...usageKeys.all, "detail", requestId] as const, pricing: () => [...usageKeys.all, "pricing"] as const, @@ -25,58 +67,85 @@ const getWindow = (days: number) => { }; // Hooks -export function useUsageSummary(days: number) { +export function useUsageSummary(days: number, options?: UsageQueryOptions) { return useQuery({ queryKey: usageKeys.summary(days), queryFn: () => { const { startDate, endDate } = getWindow(days); return usageApi.getUsageSummary(startDate, endDate); }, - refetchInterval: 30000, // 每30秒自动刷新 - refetchIntervalInBackground: false, // 后台不刷新 + refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新 + refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false, // 后台不刷新 }); } -export function useUsageTrends(days: number) { +export function useUsageTrends(days: number, options?: UsageQueryOptions) { return useQuery({ queryKey: usageKeys.trends(days), queryFn: () => { const { startDate, endDate } = getWindow(days); return usageApi.getUsageTrends(startDate, endDate); }, - refetchInterval: 30000, // 每30秒自动刷新 - refetchIntervalInBackground: false, + refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新 + refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false, }); } -export function useProviderStats() { +export function useProviderStats(options?: UsageQueryOptions) { return useQuery({ queryKey: usageKeys.providerStats(), queryFn: usageApi.getProviderStats, - refetchInterval: 30000, // 每30秒自动刷新 - refetchIntervalInBackground: false, + refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新 + refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false, }); } -export function useModelStats() { +export function useModelStats(options?: UsageQueryOptions) { return useQuery({ queryKey: usageKeys.modelStats(), queryFn: usageApi.getModelStats, - refetchInterval: 30000, // 每30秒自动刷新 - refetchIntervalInBackground: false, + refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新 + refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false, }); } -export function useRequestLogs( - filters: LogFilters, - page: number = 0, - pageSize: number = 20, -) { +const getRollingRange = (windowSeconds: number) => { + const endDate = Math.floor(Date.now() / 1000); + const startDate = endDate - windowSeconds; + return { startDate, endDate }; +}; + +export function useRequestLogs({ + filters, + timeMode, + page = 0, + pageSize = 20, + rollingWindowSeconds = 24 * 60 * 60, + options, +}: RequestLogsQueryArgs) { + const key: RequestLogsKey = { + timeMode, + rollingWindowSeconds: + timeMode === "rolling" ? rollingWindowSeconds : undefined, + appType: filters.appType, + providerName: filters.providerName, + model: filters.model, + statusCode: filters.statusCode, + startDate: timeMode === "fixed" ? filters.startDate : undefined, + endDate: timeMode === "fixed" ? filters.endDate : undefined, + }; + return useQuery({ - queryKey: usageKeys.logs(filters, page, pageSize), - queryFn: () => usageApi.getRequestLogs(filters, page, pageSize), - refetchInterval: 30000, // 每30秒自动刷新 - refetchIntervalInBackground: false, + queryKey: usageKeys.logs(key, page, pageSize), + queryFn: () => { + const effectiveFilters = + timeMode === "rolling" + ? { ...filters, ...getRollingRange(rollingWindowSeconds) } + : filters; + return usageApi.getRequestLogs(effectiveFilters, page, pageSize); + }, + refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新 + refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false, }); }