feat(usage): enhance usage stats backend and query hooks

This commit is contained in:
YoVinchen
2026-04-11 15:36:06 +08:00
parent df01755328
commit 7402f1ca05
5 changed files with 309 additions and 95 deletions
+12 -4
View File
@@ -63,12 +63,20 @@ export const usageApi = {
return invoke("get_usage_trends", { startDate, endDate, appType });
},
getProviderStats: async (appType?: string): Promise<ProviderStats[]> => {
return invoke("get_provider_stats", { appType });
getProviderStats: async (
startDate?: number,
endDate?: number,
appType?: string,
): Promise<ProviderStats[]> => {
return invoke("get_provider_stats", { startDate, endDate, appType });
},
getModelStats: async (appType?: string): Promise<ModelStats[]> => {
return invoke("get_model_stats", { appType });
getModelStats: async (
startDate?: number,
endDate?: number,
appType?: string,
): Promise<ModelStats[]> => {
return invoke("get_model_stats", { startDate, endDate, appType });
},
getRequestLogs: async (
+112 -55
View File
@@ -1,6 +1,7 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { usageApi } from "@/lib/api/usage";
import type { LogFilters } from "@/types/usage";
import { resolveUsageRange } from "@/lib/usageRange";
import type { LogFilters, UsageRangeSelection } from "@/types/usage";
const DEFAULT_REFETCH_INTERVAL_MS = 30000;
@@ -9,51 +10,94 @@ type UsageQueryOptions = {
refetchIntervalInBackground?: boolean;
};
type RequestLogsTimeMode = "rolling" | "fixed";
type RequestLogsQueryArgs = {
filters: LogFilters;
timeMode: RequestLogsTimeMode;
range: UsageRangeSelection;
page?: number;
pageSize?: number;
rollingWindowSeconds?: number;
options?: UsageQueryOptions;
};
type RequestLogsKey = {
timeMode: RequestLogsTimeMode;
rollingWindowSeconds?: number;
preset: UsageRangeSelection["preset"];
customStartDate?: number;
customEndDate?: number;
appType?: string;
providerName?: string;
model?: string;
statusCode?: number;
startDate?: number;
endDate?: number;
};
// Query keys
export const usageKeys = {
all: ["usage"] as const,
summary: (days: number, appType?: string) =>
[...usageKeys.all, "summary", days, appType ?? "all"] as const,
trends: (days: number, appType?: string) =>
[...usageKeys.all, "trends", days, appType ?? "all"] as const,
providerStats: (appType?: string) =>
[...usageKeys.all, "provider-stats", appType ?? "all"] as const,
modelStats: (appType?: string) =>
[...usageKeys.all, "model-stats", appType ?? "all"] as const,
summary: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
appType?: string,
) =>
[
...usageKeys.all,
"summary",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
appType ?? "all",
] as const,
trends: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
appType?: string,
) =>
[
...usageKeys.all,
"trends",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
appType ?? "all",
] as const,
providerStats: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
appType?: string,
) =>
[
...usageKeys.all,
"provider-stats",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
appType ?? "all",
] as const,
modelStats: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
appType?: string,
) =>
[
...usageKeys.all,
"model-stats",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
appType ?? "all",
] as const,
logs: (key: RequestLogsKey, page: number, pageSize: number) =>
[
...usageKeys.all,
"logs",
key.timeMode,
key.rollingWindowSeconds ?? 0,
key.preset,
key.customStartDate ?? 0,
key.customEndDate ?? 0,
key.appType ?? "",
key.providerName ?? "",
key.model ?? "",
key.statusCode ?? -1,
key.startDate ?? 0,
key.endDate ?? 0,
page,
pageSize,
] as const,
@@ -64,23 +108,22 @@ export const usageKeys = {
[...usageKeys.all, "limits", providerId, appType] as const,
};
const getWindow = (days: number) => {
const endDate = Math.floor(Date.now() / 1000);
const startDate = endDate - days * 24 * 60 * 60;
return { startDate, endDate };
};
// Hooks
export function useUsageSummary(
days: number,
range: UsageRangeSelection,
appType?: string,
options?: UsageQueryOptions,
) {
const effectiveAppType = appType === "all" ? undefined : appType;
return useQuery({
queryKey: usageKeys.summary(days, appType),
queryKey: usageKeys.summary(
range.preset,
range.customStartDate,
range.customEndDate,
appType,
),
queryFn: () => {
const { startDate, endDate } = getWindow(days);
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getUsageSummary(startDate, endDate, effectiveAppType);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
@@ -89,15 +132,20 @@ export function useUsageSummary(
}
export function useUsageTrends(
days: number,
range: UsageRangeSelection,
appType?: string,
options?: UsageQueryOptions,
) {
const effectiveAppType = appType === "all" ? undefined : appType;
return useQuery({
queryKey: usageKeys.trends(days, appType),
queryKey: usageKeys.trends(
range.preset,
range.customStartDate,
range.customEndDate,
appType,
),
queryFn: () => {
const { startDate, endDate } = getWindow(days);
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getUsageTrends(startDate, endDate, effectiveAppType);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
@@ -106,61 +154,70 @@ export function useUsageTrends(
}
export function useProviderStats(
range: UsageRangeSelection,
appType?: string,
options?: UsageQueryOptions,
) {
const effectiveAppType = appType === "all" ? undefined : appType;
return useQuery({
queryKey: usageKeys.providerStats(appType),
queryFn: () => usageApi.getProviderStats(effectiveAppType),
queryKey: usageKeys.providerStats(
range.preset,
range.customStartDate,
range.customEndDate,
appType,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getProviderStats(startDate, endDate, effectiveAppType);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
export function useModelStats(appType?: string, options?: UsageQueryOptions) {
export function useModelStats(
range: UsageRangeSelection,
appType?: string,
options?: UsageQueryOptions,
) {
const effectiveAppType = appType === "all" ? undefined : appType;
return useQuery({
queryKey: usageKeys.modelStats(appType),
queryFn: () => usageApi.getModelStats(effectiveAppType),
queryKey: usageKeys.modelStats(
range.preset,
range.customStartDate,
range.customEndDate,
appType,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getModelStats(startDate, endDate, effectiveAppType);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
const getRollingRange = (windowSeconds: number) => {
const endDate = Math.floor(Date.now() / 1000);
const startDate = endDate - windowSeconds;
return { startDate, endDate };
};
export function useRequestLogs({
filters,
timeMode,
range,
page = 0,
pageSize = 20,
rollingWindowSeconds = 24 * 60 * 60,
options,
}: RequestLogsQueryArgs) {
const key: RequestLogsKey = {
timeMode,
rollingWindowSeconds:
timeMode === "rolling" ? rollingWindowSeconds : undefined,
preset: range.preset,
customStartDate: range.customStartDate,
customEndDate: range.customEndDate,
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(key, page, pageSize),
queryFn: () => {
const effectiveFilters =
timeMode === "rolling"
? { ...filters, ...getRollingRange(rollingWindowSeconds) }
: filters;
const effectiveFilters = { ...filters, ...resolveUsageRange(range) };
return usageApi.getRequestLogs(effectiveFilters, page, pageSize);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新
+8 -2
View File
@@ -121,12 +121,18 @@ export interface ProviderLimitStatus {
monthlyExceeded: boolean;
}
export type TimeRange = "1d" | "7d" | "30d";
export type UsageRangePreset = "today" | "1d" | "7d" | "14d" | "30d" | "custom";
export interface UsageRangeSelection {
preset: UsageRangePreset;
customStartDate?: number;
customEndDate?: number;
}
export type AppTypeFilter = "all" | "claude" | "codex" | "gemini";
export interface StatsFilters {
timeRange: TimeRange;
timeRange: UsageRangePreset;
providerId?: string;
appType?: string;
}