mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-04 01:52:00 +08:00
Compare commits
1 Commits
style/code
...
feat/usage
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b16650036 |
@@ -272,7 +272,7 @@ impl Database {
|
|||||||
.single()
|
.single()
|
||||||
.unwrap_or_else(Local::now);
|
.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) {
|
if let Some(mut stat) = map.remove(&i) {
|
||||||
stat.date = date;
|
stat.date = date;
|
||||||
|
|||||||
@@ -8,10 +8,17 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { useModelStats } from "@/lib/query/usage";
|
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 { t } = useTranslation();
|
||||||
const { data: stats, isLoading } = useModelStats();
|
const { data: stats, isLoading } = useModelStats({
|
||||||
|
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
||||||
|
});
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div className="h-[400px] animate-pulse rounded bg-gray-100" />;
|
return <div className="h-[400px] animate-pulse rounded bg-gray-100" />;
|
||||||
@@ -60,10 +67,10 @@ export function ModelStatsTable() {
|
|||||||
{stat.totalTokens.toLocaleString()}
|
{stat.totalTokens.toLocaleString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
${parseFloat(stat.totalCost).toFixed(4)}
|
{fmtUsd(stat.totalCost, 4)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
${parseFloat(stat.avgCostPerRequest).toFixed(6)}
|
{fmtUsd(stat.avgCostPerRequest, 6)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -8,10 +8,19 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { useProviderStats } from "@/lib/query/usage";
|
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 { t } = useTranslation();
|
||||||
const { data: stats, isLoading } = useProviderStats();
|
const { data: stats, isLoading } = useProviderStats({
|
||||||
|
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
||||||
|
});
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div className="h-[400px] animate-pulse rounded bg-gray-100" />;
|
return <div className="h-[400px] animate-pulse rounded bg-gray-100" />;
|
||||||
@@ -63,7 +72,7 @@ export function ProviderStatsTable() {
|
|||||||
{stat.totalTokens.toLocaleString()}
|
{stat.totalTokens.toLocaleString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
${parseFloat(stat.totalCost).toFixed(4)}
|
{fmtUsd(stat.totalCost, 4)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{stat.successRate.toFixed(1)}%
|
{stat.successRate.toFixed(1)}%
|
||||||
|
|||||||
@@ -21,44 +21,122 @@ import { useRequestLogs, usageKeys } from "@/lib/query/usage";
|
|||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import type { LogFilters } from "@/types/usage";
|
import type { LogFilters } from "@/types/usage";
|
||||||
import { ChevronLeft, ChevronRight, RefreshCw, Search, X } from "lucide-react";
|
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 { t, i18n } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// 默认时间范围:过去24小时
|
const getRollingRange = () => {
|
||||||
const getDefaultFilters = (): LogFilters => {
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const oneDayAgo = now - 24 * 60 * 60;
|
const oneDayAgo = now - ONE_DAY_SECONDS;
|
||||||
return { startDate: oneDayAgo, endDate: now };
|
return { startDate: oneDayAgo, endDate: now };
|
||||||
};
|
};
|
||||||
|
|
||||||
const [filters, setFilters] = useState<LogFilters>(getDefaultFilters);
|
const [appliedTimeMode, setAppliedTimeMode] = useState<TimeMode>("rolling");
|
||||||
const [tempFilters, setTempFilters] = useState<LogFilters>(getDefaultFilters);
|
const [draftTimeMode, setDraftTimeMode] = useState<TimeMode>("rolling");
|
||||||
|
|
||||||
|
const [appliedFilters, setAppliedFilters] = useState<LogFilters>({});
|
||||||
|
const [draftFilters, setDraftFilters] = useState<LogFilters>({});
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
|
const [validationError, setValidationError] = useState<string | null>(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 logs = result?.data ?? [];
|
||||||
const total = result?.total ?? 0;
|
const total = result?.total ?? 0;
|
||||||
const totalPages = Math.ceil(total / pageSize);
|
const totalPages = Math.ceil(total / pageSize);
|
||||||
|
|
||||||
const handleSearch = () => {
|
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);
|
setPage(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
const defaults = getDefaultFilters();
|
setValidationError(null);
|
||||||
setTempFilters(defaults);
|
setAppliedTimeMode("rolling");
|
||||||
setFilters(defaults);
|
setDraftTimeMode("rolling");
|
||||||
|
setDraftFilters({});
|
||||||
|
setAppliedFilters({});
|
||||||
setPage(0);
|
setPage(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
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({
|
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);
|
return Math.floor(timestamp / 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dateLocale =
|
const language = i18n.resolvedLanguage || i18n.language || "en";
|
||||||
i18n.language === "zh"
|
const locale = getLocaleFromLanguage(language);
|
||||||
? "zh-CN"
|
|
||||||
: i18n.language === "ja"
|
const rollingRangeForDisplay =
|
||||||
? "ja-JP"
|
draftTimeMode === "rolling" ? getRollingRange() : null;
|
||||||
: "en-US";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -97,10 +174,10 @@ export function RequestLogTable() {
|
|||||||
<div className="flex flex-col gap-4 rounded-lg border bg-card/50 p-4 backdrop-blur-sm">
|
<div className="flex flex-col gap-4 rounded-lg border bg-card/50 p-4 backdrop-blur-sm">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<Select
|
<Select
|
||||||
value={tempFilters.appType || "all"}
|
value={draftFilters.appType || "all"}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
setTempFilters({
|
setDraftFilters({
|
||||||
...tempFilters,
|
...draftFilters,
|
||||||
appType: v === "all" ? undefined : v,
|
appType: v === "all" ? undefined : v,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -117,11 +194,16 @@ export function RequestLogTable() {
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={tempFilters.statusCode?.toString() || "all"}
|
value={draftFilters.statusCode?.toString() || "all"}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
setTempFilters({
|
setDraftFilters({
|
||||||
...tempFilters,
|
...draftFilters,
|
||||||
statusCode: v === "all" ? undefined : parseInt(v),
|
statusCode:
|
||||||
|
v === "all"
|
||||||
|
? undefined
|
||||||
|
: Number.isFinite(Number.parseInt(v, 10))
|
||||||
|
? Number.parseInt(v, 10)
|
||||||
|
: undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -144,10 +226,10 @@ export function RequestLogTable() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder={t("usage.searchProviderPlaceholder")}
|
placeholder={t("usage.searchProviderPlaceholder")}
|
||||||
className="pl-9 bg-background"
|
className="pl-9 bg-background"
|
||||||
value={tempFilters.providerName || ""}
|
value={draftFilters.providerName || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setTempFilters({
|
setDraftFilters({
|
||||||
...tempFilters,
|
...draftFilters,
|
||||||
providerName: e.target.value || undefined,
|
providerName: e.target.value || undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -156,10 +238,10 @@ export function RequestLogTable() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder={t("usage.searchModelPlaceholder")}
|
placeholder={t("usage.searchModelPlaceholder")}
|
||||||
className="w-[180px] bg-background"
|
className="w-[180px] bg-background"
|
||||||
value={tempFilters.model || ""}
|
value={draftFilters.model || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setTempFilters({
|
setDraftFilters({
|
||||||
...tempFilters,
|
...draftFilters,
|
||||||
model: e.target.value || undefined,
|
model: e.target.value || undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -174,14 +256,18 @@ export function RequestLogTable() {
|
|||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
className="h-8 w-[200px] bg-background"
|
className="h-8 w-[200px] bg-background"
|
||||||
value={
|
value={
|
||||||
tempFilters.startDate
|
(rollingRangeForDisplay?.startDate ?? draftFilters.startDate)
|
||||||
? timestampToLocalDatetime(tempFilters.startDate)
|
? timestampToLocalDatetime(
|
||||||
|
(rollingRangeForDisplay?.startDate ??
|
||||||
|
draftFilters.startDate) as number,
|
||||||
|
)
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const timestamp = localDatetimeToTimestamp(e.target.value);
|
const timestamp = localDatetimeToTimestamp(e.target.value);
|
||||||
setTempFilters({
|
setDraftTimeMode("fixed");
|
||||||
...tempFilters,
|
setDraftFilters({
|
||||||
|
...draftFilters,
|
||||||
startDate: timestamp,
|
startDate: timestamp,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -191,14 +277,18 @@ export function RequestLogTable() {
|
|||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
className="h-8 w-[200px] bg-background"
|
className="h-8 w-[200px] bg-background"
|
||||||
value={
|
value={
|
||||||
tempFilters.endDate
|
(rollingRangeForDisplay?.endDate ?? draftFilters.endDate)
|
||||||
? timestampToLocalDatetime(tempFilters.endDate)
|
? timestampToLocalDatetime(
|
||||||
|
(rollingRangeForDisplay?.endDate ??
|
||||||
|
draftFilters.endDate) as number,
|
||||||
|
)
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const timestamp = localDatetimeToTimestamp(e.target.value);
|
const timestamp = localDatetimeToTimestamp(e.target.value);
|
||||||
setTempFilters({
|
setDraftTimeMode("fixed");
|
||||||
...tempFilters,
|
setDraftFilters({
|
||||||
|
...draftFilters,
|
||||||
endDate: timestamp,
|
endDate: timestamp,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -234,6 +324,10 @@ export function RequestLogTable() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{validationError && (
|
||||||
|
<div className="text-sm text-red-600">{validationError}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -293,9 +387,7 @@ export function RequestLogTable() {
|
|||||||
logs.map((log) => (
|
logs.map((log) => (
|
||||||
<TableRow key={log.requestId}>
|
<TableRow key={log.requestId}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{new Date(log.createdAt * 1000).toLocaleString(
|
{new Date(log.createdAt * 1000).toLocaleString(locale)}
|
||||||
dateLocale,
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{log.providerName || t("usage.unknownProvider")}
|
{log.providerName || t("usage.unknownProvider")}
|
||||||
@@ -321,19 +413,19 @@ export function RequestLogTable() {
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{log.inputTokens.toLocaleString()}
|
{fmtInt(log.inputTokens, locale)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{log.outputTokens.toLocaleString()}
|
{fmtInt(log.outputTokens, locale)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{log.cacheReadTokens.toLocaleString()}
|
{fmtInt(log.cacheReadTokens, locale)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{log.cacheCreationTokens.toLocaleString()}
|
{fmtInt(log.cacheCreationTokens, locale)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right font-mono text-xs">
|
<TableCell className="text-right font-mono text-xs">
|
||||||
{parseFloat(log.costMultiplier) !== 1 ? (
|
{(parseFiniteNumber(log.costMultiplier) ?? 1) !== 1 ? (
|
||||||
<span className="text-orange-600">
|
<span className="text-orange-600">
|
||||||
×{log.costMultiplier}
|
×{log.costMultiplier}
|
||||||
</span>
|
</span>
|
||||||
@@ -342,24 +434,30 @@ export function RequestLogTable() {
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
${parseFloat(log.totalCostUsd).toFixed(6)}
|
{fmtUsd(log.totalCostUsd, 6)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center gap-1">
|
||||||
{(() => {
|
{(() => {
|
||||||
const durationSec =
|
const durationMs =
|
||||||
(log.durationMs ?? log.latencyMs) / 1000;
|
typeof log.durationMs === "number"
|
||||||
const durationColor =
|
? log.durationMs
|
||||||
durationSec <= 5
|
: log.latencyMs;
|
||||||
|
const durationSec = durationMs / 1000;
|
||||||
|
const durationColor = Number.isFinite(durationSec)
|
||||||
|
? durationSec <= 5
|
||||||
? "bg-green-100 text-green-800"
|
? "bg-green-100 text-green-800"
|
||||||
: durationSec <= 120
|
: durationSec <= 120
|
||||||
? "bg-orange-100 text-orange-800"
|
? "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 (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center justify-center rounded-full px-2 py-0.5 text-xs ${durationColor}`}
|
className={`inline-flex items-center justify-center rounded-full px-2 py-0.5 text-xs ${durationColor}`}
|
||||||
>
|
>
|
||||||
{Math.round(durationSec)}s
|
{Number.isFinite(durationSec)
|
||||||
|
? `${Math.round(durationSec)}s`
|
||||||
|
: "--"}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -367,17 +465,20 @@ export function RequestLogTable() {
|
|||||||
log.firstTokenMs != null &&
|
log.firstTokenMs != null &&
|
||||||
(() => {
|
(() => {
|
||||||
const firstSec = log.firstTokenMs / 1000;
|
const firstSec = log.firstTokenMs / 1000;
|
||||||
const firstColor =
|
const firstColor = Number.isFinite(firstSec)
|
||||||
firstSec <= 5
|
? firstSec <= 5
|
||||||
? "bg-green-100 text-green-800"
|
? "bg-green-100 text-green-800"
|
||||||
: firstSec <= 120
|
: firstSec <= 120
|
||||||
? "bg-orange-100 text-orange-800"
|
? "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 (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center justify-center rounded-full px-2 py-0.5 text-xs ${firstColor}`}
|
className={`inline-flex items-center justify-center rounded-full px-2 py-0.5 text-xs ${firstColor}`}
|
||||||
>
|
>
|
||||||
{firstSec.toFixed(1)}s
|
{Number.isFinite(firstSec)
|
||||||
|
? `${firstSec.toFixed(1)}s`
|
||||||
|
: "--"}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
@@ -8,11 +8,28 @@ import { ProviderStatsTable } from "./ProviderStatsTable";
|
|||||||
import { ModelStatsTable } from "./ModelStatsTable";
|
import { ModelStatsTable } from "./ModelStatsTable";
|
||||||
import type { TimeRange } from "@/types/usage";
|
import type { TimeRange } from "@/types/usage";
|
||||||
import { motion } from "framer-motion";
|
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() {
|
export function UsageDashboard() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const [timeRange, setTimeRange] = useState<TimeRange>("1d");
|
const [timeRange, setTimeRange] = useState<TimeRange>("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;
|
const days = timeRange === "1d" ? 1 : timeRange === "7d" ? 7 : 30;
|
||||||
|
|
||||||
@@ -34,32 +51,45 @@ export function UsageDashboard() {
|
|||||||
onValueChange={(v) => setTimeRange(v as TimeRange)}
|
onValueChange={(v) => setTimeRange(v as TimeRange)}
|
||||||
className="w-full sm:w-auto"
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
<TabsList className="flex w-full sm:w-auto bg-card/60 border border-border/50 backdrop-blur-sm shadow-sm h-10 p-1">
|
<div className="flex w-full sm:w-auto items-center gap-1">
|
||||||
<TabsTrigger
|
<Button
|
||||||
value="1d"
|
type="button"
|
||||||
className="flex-1 sm:flex-none sm:px-6 data-[state=active]:bg-primary/10 data-[state=active]:text-primary hover:text-primary transition-colors"
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-10 px-2 text-xs text-muted-foreground"
|
||||||
|
title={t("common.refresh", "刷新")}
|
||||||
|
onClick={changeRefreshInterval}
|
||||||
>
|
>
|
||||||
{t("usage.today")}
|
<RefreshCw className="mr-1 h-3.5 w-3.5" />
|
||||||
</TabsTrigger>
|
{refreshIntervalMs > 0 ? `${refreshIntervalMs / 1000}s` : "--"}
|
||||||
<TabsTrigger
|
</Button>
|
||||||
value="7d"
|
<TabsList className="flex w-full sm:w-auto bg-card/60 border border-border/50 backdrop-blur-sm shadow-sm h-10 p-1">
|
||||||
className="flex-1 sm:flex-none sm:px-6 data-[state=active]:bg-primary/10 data-[state=active]:text-primary hover:text-primary transition-colors"
|
<TabsTrigger
|
||||||
>
|
value="1d"
|
||||||
{t("usage.last7days")}
|
className="flex-1 sm:flex-none sm:px-6 data-[state=active]:bg-primary/10 data-[state=active]:text-primary hover:text-primary transition-colors"
|
||||||
</TabsTrigger>
|
>
|
||||||
<TabsTrigger
|
{t("usage.today")}
|
||||||
value="30d"
|
</TabsTrigger>
|
||||||
className="flex-1 sm:flex-none sm:px-6 data-[state=active]:bg-primary/10 data-[state=active]:text-primary hover:text-primary transition-colors"
|
<TabsTrigger
|
||||||
>
|
value="7d"
|
||||||
{t("usage.last30days")}
|
className="flex-1 sm:flex-none sm:px-6 data-[state=active]:bg-primary/10 data-[state=active]:text-primary hover:text-primary transition-colors"
|
||||||
</TabsTrigger>
|
>
|
||||||
</TabsList>
|
{t("usage.last7days")}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="30d"
|
||||||
|
className="flex-1 sm:flex-none sm:px-6 data-[state=active]:bg-primary/10 data-[state=active]:text-primary hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
{t("usage.last30days")}
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UsageSummaryCards days={days} />
|
<UsageSummaryCards days={days} refreshIntervalMs={refreshIntervalMs} />
|
||||||
|
|
||||||
<UsageTrendChart days={days} />
|
<UsageTrendChart days={days} refreshIntervalMs={refreshIntervalMs} />
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Tabs defaultValue="logs" className="w-full">
|
<Tabs defaultValue="logs" className="w-full">
|
||||||
@@ -86,15 +116,15 @@ export function UsageDashboard() {
|
|||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
>
|
>
|
||||||
<TabsContent value="logs" className="mt-0">
|
<TabsContent value="logs" className="mt-0">
|
||||||
<RequestLogTable />
|
<RequestLogTable refreshIntervalMs={refreshIntervalMs} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="providers" className="mt-0">
|
<TabsContent value="providers" className="mt-0">
|
||||||
<ProviderStatsTable />
|
<ProviderStatsTable refreshIntervalMs={refreshIntervalMs} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="models" className="mt-0">
|
<TabsContent value="models" className="mt-0">
|
||||||
<ModelStatsTable />
|
<ModelStatsTable refreshIntervalMs={refreshIntervalMs} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -4,19 +4,26 @@ import { Card, CardContent } from "@/components/ui/card";
|
|||||||
import { useUsageSummary } from "@/lib/query/usage";
|
import { useUsageSummary } from "@/lib/query/usage";
|
||||||
import { Activity, DollarSign, Layers, Database, Loader2 } from "lucide-react";
|
import { Activity, DollarSign, Layers, Database, Loader2 } from "lucide-react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import { fmtUsd, parseFiniteNumber } from "./format";
|
||||||
|
|
||||||
interface UsageSummaryCardsProps {
|
interface UsageSummaryCardsProps {
|
||||||
days: number;
|
days: number;
|
||||||
|
refreshIntervalMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UsageSummaryCards({ days }: UsageSummaryCardsProps) {
|
export function UsageSummaryCards({
|
||||||
|
days,
|
||||||
|
refreshIntervalMs,
|
||||||
|
}: UsageSummaryCardsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data: summary, isLoading } = useUsageSummary(days);
|
const { data: summary, isLoading } = useUsageSummary(days, {
|
||||||
|
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
||||||
|
});
|
||||||
|
|
||||||
const stats = useMemo(() => {
|
const stats = useMemo(() => {
|
||||||
const totalRequests = summary?.totalRequests ?? 0;
|
const totalRequests = summary?.totalRequests ?? 0;
|
||||||
const totalCost = parseFloat(summary?.totalCost || "0");
|
const totalCost = parseFiniteNumber(summary?.totalCost);
|
||||||
|
|
||||||
const inputTokens = summary?.totalInputTokens ?? 0;
|
const inputTokens = summary?.totalInputTokens ?? 0;
|
||||||
const outputTokens = summary?.totalOutputTokens ?? 0;
|
const outputTokens = summary?.totalOutputTokens ?? 0;
|
||||||
@@ -37,7 +44,7 @@ export function UsageSummaryCards({ days }: UsageSummaryCardsProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("usage.totalCost"),
|
title: t("usage.totalCost"),
|
||||||
value: `$${totalCost.toFixed(4)}`,
|
value: totalCost == null ? "--" : fmtUsd(totalCost, 4),
|
||||||
icon: DollarSign,
|
icon: DollarSign,
|
||||||
color: "text-green-500",
|
color: "text-green-500",
|
||||||
bg: "bg-green-500/10",
|
bg: "bg-green-500/10",
|
||||||
|
|||||||
@@ -11,14 +11,26 @@ import {
|
|||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { useUsageTrends } from "@/lib/query/usage";
|
import { useUsageTrends } from "@/lib/query/usage";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
import {
|
||||||
|
fmtInt,
|
||||||
|
fmtUsd,
|
||||||
|
getLocaleFromLanguage,
|
||||||
|
parseFiniteNumber,
|
||||||
|
} from "./format";
|
||||||
|
|
||||||
interface UsageTrendChartProps {
|
interface UsageTrendChartProps {
|
||||||
days: number;
|
days: number;
|
||||||
|
refreshIntervalMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UsageTrendChart({ days }: UsageTrendChartProps) {
|
export function UsageTrendChart({
|
||||||
|
days,
|
||||||
|
refreshIntervalMs,
|
||||||
|
}: UsageTrendChartProps) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const { data: trends, isLoading } = useUsageTrends(days);
|
const { data: trends, isLoading } = useUsageTrends(days, {
|
||||||
|
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
|
||||||
|
});
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -29,15 +41,12 @@ export function UsageTrendChart({ days }: UsageTrendChartProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isToday = days === 1;
|
const isToday = days === 1;
|
||||||
const dateLocale =
|
const language = i18n.resolvedLanguage || i18n.language || "en";
|
||||||
i18n.language === "zh"
|
const dateLocale = getLocaleFromLanguage(language);
|
||||||
? "zh-CN"
|
|
||||||
: i18n.language === "ja"
|
|
||||||
? "ja-JP"
|
|
||||||
: "en-US";
|
|
||||||
const chartData =
|
const chartData =
|
||||||
trends?.map((stat) => {
|
trends?.map((stat) => {
|
||||||
const pointDate = new Date(stat.date);
|
const pointDate = new Date(stat.date);
|
||||||
|
const cost = parseFiniteNumber(stat.totalCost);
|
||||||
return {
|
return {
|
||||||
rawDate: stat.date,
|
rawDate: stat.date,
|
||||||
label: isToday
|
label: isToday
|
||||||
@@ -56,7 +65,7 @@ export function UsageTrendChart({ days }: UsageTrendChartProps) {
|
|||||||
outputTokens: stat.totalOutputTokens,
|
outputTokens: stat.totalOutputTokens,
|
||||||
cacheCreationTokens: stat.totalCacheCreationTokens,
|
cacheCreationTokens: stat.totalCacheCreationTokens,
|
||||||
cacheReadTokens: stat.totalCacheReadTokens,
|
cacheReadTokens: stat.totalCacheReadTokens,
|
||||||
cost: parseFloat(stat.totalCost),
|
cost: cost ?? null,
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
@@ -79,9 +88,9 @@ export function UsageTrendChart({ days }: UsageTrendChartProps) {
|
|||||||
/>
|
/>
|
||||||
<span className="font-medium">{entry.name}:</span>
|
<span className="font-medium">{entry.name}:</span>
|
||||||
<span>
|
<span>
|
||||||
{entry.name.includes(t("usage.cost", "成本"))
|
{entry.dataKey === "cost"
|
||||||
? `$${typeof entry.value === "number" ? entry.value.toFixed(6) : entry.value}`
|
? fmtUsd(entry.value, 6)
|
||||||
: entry.value.toLocaleString()}
|
: fmtInt(entry.value, dateLocale)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
39
src/components/usage/format.ts
Normal file
39
src/components/usage/format.ts
Normal file
@@ -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";
|
||||||
|
}
|
||||||
@@ -2,6 +2,35 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { usageApi } from "@/lib/api/usage";
|
import { usageApi } from "@/lib/api/usage";
|
||||||
import type { LogFilters } from "@/types/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
|
// Query keys
|
||||||
export const usageKeys = {
|
export const usageKeys = {
|
||||||
all: ["usage"] as const,
|
all: ["usage"] as const,
|
||||||
@@ -9,8 +38,21 @@ export const usageKeys = {
|
|||||||
trends: (days: number) => [...usageKeys.all, "trends", days] as const,
|
trends: (days: number) => [...usageKeys.all, "trends", days] as const,
|
||||||
providerStats: () => [...usageKeys.all, "provider-stats"] as const,
|
providerStats: () => [...usageKeys.all, "provider-stats"] as const,
|
||||||
modelStats: () => [...usageKeys.all, "model-stats"] as const,
|
modelStats: () => [...usageKeys.all, "model-stats"] as const,
|
||||||
logs: (filters: LogFilters, page: number, pageSize: number) =>
|
logs: (key: RequestLogsKey, page: number, pageSize: number) =>
|
||||||
[...usageKeys.all, "logs", filters, page, pageSize] as const,
|
[
|
||||||
|
...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) =>
|
detail: (requestId: string) =>
|
||||||
[...usageKeys.all, "detail", requestId] as const,
|
[...usageKeys.all, "detail", requestId] as const,
|
||||||
pricing: () => [...usageKeys.all, "pricing"] as const,
|
pricing: () => [...usageKeys.all, "pricing"] as const,
|
||||||
@@ -25,58 +67,85 @@ const getWindow = (days: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
export function useUsageSummary(days: number) {
|
export function useUsageSummary(days: number, options?: UsageQueryOptions) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: usageKeys.summary(days),
|
queryKey: usageKeys.summary(days),
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
const { startDate, endDate } = getWindow(days);
|
const { startDate, endDate } = getWindow(days);
|
||||||
return usageApi.getUsageSummary(startDate, endDate);
|
return usageApi.getUsageSummary(startDate, endDate);
|
||||||
},
|
},
|
||||||
refetchInterval: 30000, // 每30秒自动刷新
|
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新
|
||||||
refetchIntervalInBackground: false, // 后台不刷新
|
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false, // 后台不刷新
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUsageTrends(days: number) {
|
export function useUsageTrends(days: number, options?: UsageQueryOptions) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: usageKeys.trends(days),
|
queryKey: usageKeys.trends(days),
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
const { startDate, endDate } = getWindow(days);
|
const { startDate, endDate } = getWindow(days);
|
||||||
return usageApi.getUsageTrends(startDate, endDate);
|
return usageApi.getUsageTrends(startDate, endDate);
|
||||||
},
|
},
|
||||||
refetchInterval: 30000, // 每30秒自动刷新
|
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新
|
||||||
refetchIntervalInBackground: false,
|
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useProviderStats() {
|
export function useProviderStats(options?: UsageQueryOptions) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: usageKeys.providerStats(),
|
queryKey: usageKeys.providerStats(),
|
||||||
queryFn: usageApi.getProviderStats,
|
queryFn: usageApi.getProviderStats,
|
||||||
refetchInterval: 30000, // 每30秒自动刷新
|
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新
|
||||||
refetchIntervalInBackground: false,
|
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useModelStats() {
|
export function useModelStats(options?: UsageQueryOptions) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: usageKeys.modelStats(),
|
queryKey: usageKeys.modelStats(),
|
||||||
queryFn: usageApi.getModelStats,
|
queryFn: usageApi.getModelStats,
|
||||||
refetchInterval: 30000, // 每30秒自动刷新
|
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新
|
||||||
refetchIntervalInBackground: false,
|
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRequestLogs(
|
const getRollingRange = (windowSeconds: number) => {
|
||||||
filters: LogFilters,
|
const endDate = Math.floor(Date.now() / 1000);
|
||||||
page: number = 0,
|
const startDate = endDate - windowSeconds;
|
||||||
pageSize: number = 20,
|
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({
|
return useQuery({
|
||||||
queryKey: usageKeys.logs(filters, page, pageSize),
|
queryKey: usageKeys.logs(key, page, pageSize),
|
||||||
queryFn: () => usageApi.getRequestLogs(filters, page, pageSize),
|
queryFn: () => {
|
||||||
refetchInterval: 30000, // 每30秒自动刷新
|
const effectiveFilters =
|
||||||
refetchIntervalInBackground: false,
|
timeMode === "rolling"
|
||||||
|
? { ...filters, ...getRollingRange(rollingWindowSeconds) }
|
||||||
|
: filters;
|
||||||
|
return usageApi.getRequestLogs(effectiveFilters, page, pageSize);
|
||||||
|
},
|
||||||
|
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新
|
||||||
|
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user