mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 19:30:51 +08:00
213 lines
5.7 KiB
TypeScript
213 lines
5.7 KiB
TypeScript
import { useState, useMemo } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
Chart as ChartJS,
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
Filler
|
|
} from 'chart.js';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
|
import { useHeaderRefresh } from '@/hooks/useHeaderRefresh';
|
|
import { useThemeStore } from '@/stores';
|
|
import {
|
|
StatCards,
|
|
UsageChart,
|
|
ChartLineSelector,
|
|
ApiDetailsCard,
|
|
ModelStatsCard,
|
|
PriceSettingsCard,
|
|
useUsageData,
|
|
useSparklines,
|
|
useChartData
|
|
} from '@/components/usage';
|
|
import { getModelNamesFromUsage, getApiStats, getModelStats } from '@/utils/usage';
|
|
import styles from './UsagePage.module.scss';
|
|
|
|
// Register Chart.js components
|
|
ChartJS.register(
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
Filler
|
|
);
|
|
|
|
export function UsagePage() {
|
|
const { t } = useTranslation();
|
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
|
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
|
|
const isDark = resolvedTheme === 'dark';
|
|
|
|
// Data hook
|
|
const {
|
|
usage,
|
|
loading,
|
|
error,
|
|
modelPrices,
|
|
setModelPrices,
|
|
loadUsage,
|
|
handleExport,
|
|
handleImport,
|
|
handleImportChange,
|
|
importInputRef,
|
|
exporting,
|
|
importing
|
|
} = useUsageData();
|
|
|
|
useHeaderRefresh(loadUsage);
|
|
|
|
// Chart lines state
|
|
const [chartLines, setChartLines] = useState<string[]>(['all']);
|
|
const MAX_CHART_LINES = 9;
|
|
|
|
// Sparklines hook
|
|
const {
|
|
requestsSparkline,
|
|
tokensSparkline,
|
|
rpmSparkline,
|
|
tpmSparkline,
|
|
costSparkline
|
|
} = useSparklines({ usage, loading });
|
|
|
|
// Chart data hook
|
|
const {
|
|
requestsPeriod,
|
|
setRequestsPeriod,
|
|
tokensPeriod,
|
|
setTokensPeriod,
|
|
requestsChartData,
|
|
tokensChartData,
|
|
requestsChartOptions,
|
|
tokensChartOptions
|
|
} = useChartData({ usage, chartLines, isDark, isMobile });
|
|
|
|
// Derived data
|
|
const modelNames = useMemo(() => getModelNamesFromUsage(usage), [usage]);
|
|
const apiStats = useMemo(() => getApiStats(usage, modelPrices), [usage, modelPrices]);
|
|
const modelStats = useMemo(() => getModelStats(usage, modelPrices), [usage, modelPrices]);
|
|
const hasPrices = Object.keys(modelPrices).length > 0;
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
{loading && !usage && (
|
|
<div className={styles.loadingOverlay} aria-busy="true">
|
|
<div className={styles.loadingOverlayContent}>
|
|
<LoadingSpinner size={28} className={styles.loadingOverlaySpinner} />
|
|
<span className={styles.loadingOverlayText}>{t('common.loading')}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className={styles.header}>
|
|
<h1 className={styles.pageTitle}>{t('usage_stats.title')}</h1>
|
|
<div className={styles.headerActions}>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
onClick={handleExport}
|
|
loading={exporting}
|
|
disabled={loading || importing}
|
|
>
|
|
{t('usage_stats.export')}
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
onClick={handleImport}
|
|
loading={importing}
|
|
disabled={loading || exporting}
|
|
>
|
|
{t('usage_stats.import')}
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
onClick={loadUsage}
|
|
disabled={loading || exporting || importing}
|
|
>
|
|
{loading ? t('common.loading') : t('usage_stats.refresh')}
|
|
</Button>
|
|
<input
|
|
ref={importInputRef}
|
|
type="file"
|
|
accept=".json,application/json"
|
|
style={{ display: 'none' }}
|
|
onChange={handleImportChange}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{error && <div className={styles.errorBox}>{error}</div>}
|
|
|
|
{/* Stats Overview Cards */}
|
|
<StatCards
|
|
usage={usage}
|
|
loading={loading}
|
|
modelPrices={modelPrices}
|
|
sparklines={{
|
|
requests: requestsSparkline,
|
|
tokens: tokensSparkline,
|
|
rpm: rpmSparkline,
|
|
tpm: tpmSparkline,
|
|
cost: costSparkline
|
|
}}
|
|
/>
|
|
|
|
{/* Chart Line Selection */}
|
|
<ChartLineSelector
|
|
chartLines={chartLines}
|
|
modelNames={modelNames}
|
|
maxLines={MAX_CHART_LINES}
|
|
onChange={setChartLines}
|
|
/>
|
|
|
|
{/* Charts Grid */}
|
|
<div className={styles.chartsGrid}>
|
|
<UsageChart
|
|
title={t('usage_stats.requests_trend')}
|
|
period={requestsPeriod}
|
|
onPeriodChange={setRequestsPeriod}
|
|
chartData={requestsChartData}
|
|
chartOptions={requestsChartOptions}
|
|
loading={loading}
|
|
isMobile={isMobile}
|
|
emptyText={t('usage_stats.no_data')}
|
|
/>
|
|
<UsageChart
|
|
title={t('usage_stats.tokens_trend')}
|
|
period={tokensPeriod}
|
|
onPeriodChange={setTokensPeriod}
|
|
chartData={tokensChartData}
|
|
chartOptions={tokensChartOptions}
|
|
loading={loading}
|
|
isMobile={isMobile}
|
|
emptyText={t('usage_stats.no_data')}
|
|
/>
|
|
</div>
|
|
|
|
{/* Details Grid */}
|
|
<div className={styles.detailsGrid}>
|
|
<ApiDetailsCard apiStats={apiStats} loading={loading} hasPrices={hasPrices} />
|
|
<ModelStatsCard modelStats={modelStats} loading={loading} hasPrices={hasPrices} />
|
|
</div>
|
|
|
|
{/* Price Settings */}
|
|
<PriceSettingsCard
|
|
modelNames={modelNames}
|
|
modelPrices={modelPrices}
|
|
onPricesChange={setModelPrices}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|