refactor(usage): modularize UsagePage into separate section components

This commit is contained in:
Supra4E8C
2026-01-02 16:19:04 +08:00
parent 58154063ed
commit 47f0042bf0
14 changed files with 1316 additions and 937 deletions

View File

@@ -0,0 +1,153 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useNotificationStore } from '@/stores';
import { usageApi } from '@/services/api/usage';
import { loadModelPrices, saveModelPrices, type ModelPrice } from '@/utils/usage';
export interface UsagePayload {
total_requests?: number;
success_count?: number;
failure_count?: number;
total_tokens?: number;
apis?: Record<string, unknown>;
[key: string]: unknown;
}
export interface UseUsageDataReturn {
usage: UsagePayload | null;
loading: boolean;
error: string;
modelPrices: Record<string, ModelPrice>;
setModelPrices: (prices: Record<string, ModelPrice>) => void;
loadUsage: () => Promise<void>;
handleExport: () => Promise<void>;
handleImport: () => void;
handleImportChange: (event: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
importInputRef: React.RefObject<HTMLInputElement | null>;
exporting: boolean;
importing: boolean;
}
export function useUsageData(): UseUsageDataReturn {
const { t } = useTranslation();
const { showNotification } = useNotificationStore();
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 importInputRef = useRef<HTMLInputElement | null>(null);
const loadUsage = useCallback(async () => {
setLoading(true);
setError('');
try {
const data = await usageApi.getUsage();
const payload = data?.usage ?? data;
setUsage(payload);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : t('usage_stats.loading_error');
setError(message);
} finally {
setLoading(false);
}
}, [t]);
useEffect(() => {
loadUsage();
setModelPrices(loadModelPrices());
}, [loadUsage]);
const handleExport = async () => {
setExporting(true);
try {
const data = await usageApi.exportUsage();
const exportedAt =
typeof data?.exported_at === 'string' ? new Date(data.exported_at) : new Date();
const safeTimestamp = Number.isNaN(exportedAt.getTime())
? new Date().toISOString()
: exportedAt.toISOString();
const filename = `usage-export-${safeTimestamp.replace(/[:.]/g, '-')}.json`;
const blob = new Blob([JSON.stringify(data ?? {}, null, 2)], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
window.URL.revokeObjectURL(url);
showNotification(t('usage_stats.export_success'), 'success');
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '';
showNotification(
`${t('notification.download_failed')}${message ? `: ${message}` : ''}`,
'error'
);
} finally {
setExporting(false);
}
};
const handleImport = () => {
importInputRef.current?.click();
};
const handleImportChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
event.target.value = '';
if (!file) return;
setImporting(true);
try {
const text = await file.text();
let payload: unknown;
try {
payload = JSON.parse(text);
} catch {
showNotification(t('usage_stats.import_invalid'), 'error');
return;
}
const result = await usageApi.importUsage(payload);
showNotification(
t('usage_stats.import_success', {
added: result?.added ?? 0,
skipped: result?.skipped ?? 0,
total: result?.total_requests ?? 0,
failed: result?.failed_requests ?? 0
}),
'success'
);
await loadUsage();
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '';
showNotification(
`${t('notification.upload_failed')}${message ? `: ${message}` : ''}`,
'error'
);
} finally {
setImporting(false);
}
};
const handleSetModelPrices = useCallback((prices: Record<string, ModelPrice>) => {
setModelPrices(prices);
saveModelPrices(prices);
}, []);
return {
usage,
loading,
error,
modelPrices,
setModelPrices: handleSetModelPrices,
loadUsage,
handleExport,
handleImport,
handleImportChange,
importInputRef,
exporting,
importing
};
}