feat(usage): add shared usage stats store

This commit is contained in:
Supra4E8C
2026-02-22 03:35:35 +08:00
parent 028568c3ae
commit b15c70fd6b
7 changed files with 204 additions and 81 deletions
@@ -1,37 +1,26 @@
import { useCallback, useRef, useState } from 'react';
import { useCallback } from 'react';
import { useInterval } from '@/hooks/useInterval';
import { usageApi } from '@/services/api';
import { collectUsageDetails, type KeyStats, type UsageDetail } from '@/utils/usage';
const EMPTY_STATS: KeyStats = { bySource: {}, byAuthIndex: {} };
import { USAGE_STATS_STALE_TIME_MS, useUsageStatsStore } from '@/stores';
export const useProviderStats = () => {
const [keyStats, setKeyStats] = useState<KeyStats>(EMPTY_STATS);
const [usageDetails, setUsageDetails] = useState<UsageDetail[]>([]);
const [isLoading, setIsLoading] = useState(false);
const loadingRef = useRef(false);
const keyStats = useUsageStatsStore((state) => state.keyStats);
const usageDetails = useUsageStatsStore((state) => state.usageDetails);
const isLoading = useUsageStatsStore((state) => state.loading);
const loadUsageStats = useUsageStatsStore((state) => state.loadUsageStats);
// 加载 key 统计和 usage 明细(API 层已有60秒超时)
// 首次进入页面优先复用缓存,避免跨页面重复拉取 /usage。
const loadKeyStats = useCallback(async () => {
if (loadingRef.current) return;
loadingRef.current = true;
setIsLoading(true);
try {
const usageResponse = await usageApi.getUsage();
const usageData = usageResponse?.usage ?? usageResponse;
const stats = await usageApi.getKeyStats(usageData);
setKeyStats(stats);
setUsageDetails(collectUsageDetails(usageData));
} catch {
// 静默失败
} finally {
loadingRef.current = false;
setIsLoading(false);
}
}, []);
await loadUsageStats({ staleTimeMs: USAGE_STATS_STALE_TIME_MS });
}, [loadUsageStats]);
// 定时刷新状态数据(每240秒)
useInterval(loadKeyStats, 240_000);
// 定时器触发时强制刷新共享 usage。
const refreshKeyStats = useCallback(async () => {
await loadUsageStats({ force: true, staleTimeMs: USAGE_STATS_STALE_TIME_MS });
}, [loadUsageStats]);
return { keyStats, usageDetails, loadKeyStats, isLoading };
useInterval(() => {
void refreshKeyStats();
}, 240_000);
return { keyStats, usageDetails, loadKeyStats, refreshKeyStats, isLoading };
};
+15 -22
View File
@@ -1,6 +1,6 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useNotificationStore } from '@/stores';
import { USAGE_STATS_STALE_TIME_MS, useNotificationStore, useUsageStatsStore } from '@/stores';
import { usageApi } from '@/services/api/usage';
import { loadModelPrices, saveModelPrices, type ModelPrice } from '@/utils/usage';
@@ -32,36 +32,25 @@ export interface UseUsageDataReturn {
export function useUsageData(): UseUsageDataReturn {
const { t } = useTranslation();
const { showNotification } = useNotificationStore();
const usageSnapshot = useUsageStatsStore((state) => state.usage);
const loading = useUsageStatsStore((state) => state.loading);
const storeError = useUsageStatsStore((state) => state.error);
const lastRefreshedAtTs = useUsageStatsStore((state) => state.lastRefreshedAt);
const loadUsageStats = useUsageStatsStore((state) => state.loadUsageStats);
const [usage, setUsage] = useState<UsagePayload | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [modelPrices, setModelPrices] = useState<Record<string, ModelPrice>>({});
const [exporting, setExporting] = useState(false);
const [importing, setImporting] = useState(false);
const [lastRefreshedAt, setLastRefreshedAt] = useState<Date | null>(null);
const importInputRef = useRef<HTMLInputElement | null>(null);
const loadUsage = useCallback(async () => {
setLoading(true);
setError('');
try {
const data = await usageApi.getUsage();
const payload = (data?.usage ?? data) as unknown;
setUsage(payload && typeof payload === 'object' ? (payload as UsagePayload) : null);
setLastRefreshedAt(new Date());
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('usage_stats.loading_error');
setError(message);
} finally {
setLoading(false);
}
}, [t]);
await loadUsageStats({ force: true, staleTimeMs: USAGE_STATS_STALE_TIME_MS });
}, [loadUsageStats]);
useEffect(() => {
loadUsage();
void loadUsageStats({ staleTimeMs: USAGE_STATS_STALE_TIME_MS });
setModelPrices(loadModelPrices());
}, [loadUsage]);
}, [loadUsageStats]);
const handleExport = async () => {
setExporting(true);
@@ -122,7 +111,7 @@ export function useUsageData(): UseUsageDataReturn {
}),
'success'
);
await loadUsage();
await loadUsageStats({ force: true, staleTimeMs: USAGE_STATS_STALE_TIME_MS });
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '';
showNotification(
@@ -139,6 +128,10 @@ export function useUsageData(): UseUsageDataReturn {
saveModelPrices(prices);
}, []);
const usage = usageSnapshot as UsagePayload | null;
const error = storeError || '';
const lastRefreshedAt = lastRefreshedAtTs ? new Date(lastRefreshedAtTs) : null;
return {
usage,
loading,