first-update

This commit is contained in:
2026-03-17 14:36:31 +08:00
parent 72f08aee7c
commit 4eddf05e79
516 changed files with 115270 additions and 1 deletions

View File

@@ -0,0 +1,188 @@
import React from 'react';
import { Card, CardContent, Typography, Box, useTheme } from '@mui/material';
import { useTranslation } from 'react-i18next';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell,
Legend
} from 'recharts';
export default function Charts({ trendData, modelDistribution }) {
const theme = useTheme();
const { t } = useTranslation();
const COLORS = [
theme.palette.primary.main,
theme.palette.secondary.main,
theme.palette.success.main,
theme.palette.warning.main,
theme.palette.error.main,
theme.palette.info.main
];
return (
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
{/* 趋势图 */}
<Box sx={{ flex: '1 1 calc(66.67% - 16px)', minWidth: 500 }}>
<Card
elevation={0}
sx={{
height: 400,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2
}}
>
<CardContent sx={{ height: '100%' }}>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Typography variant="h6" fontWeight="bold">
{t('monitoring.charts.tokenTrend')}
</Typography>
<Box display="flex" gap={2}>
<Box display="flex" alignItems="center" gap={1}>
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: theme.palette.primary.main }} />
<Typography variant="caption" color="text.secondary">
{t('monitoring.charts.inputLegend')}
</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: theme.palette.success.main }} />
<Typography variant="caption" color="text.secondary">
{t('monitoring.charts.outputLegend')}
</Typography>
</Box>
</Box>
</Box>
<ResponsiveContainer width="100%" height="85%">
<AreaChart data={trendData} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="colorInput" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={theme.palette.primary.main} stopOpacity={0.1} />
<stop offset="95%" stopColor={theme.palette.primary.main} stopOpacity={0} />
</linearGradient>
<linearGradient id="colorOutput" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={theme.palette.success.main} stopOpacity={0.1} />
<stop offset="95%" stopColor={theme.palette.success.main} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={theme.palette.divider} />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: theme.palette.text.secondary, fontSize: 12 }}
dy={10}
/>
<YAxis axisLine={false} tickLine={false} tick={{ fill: theme.palette.text.secondary, fontSize: 12 }} />
<Tooltip
contentStyle={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8
}}
/>
<Area
type="monotone"
dataKey="input"
stroke={theme.palette.primary.main}
fillOpacity={1}
fill="url(#colorInput)"
strokeWidth={2}
/>
<Area
type="monotone"
dataKey="output"
stroke={theme.palette.success.main}
fillOpacity={1}
fill="url(#colorOutput)"
strokeWidth={2}
/>
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
</Box>
{/* 模型分布图 */}
<Box sx={{ flex: '1 1 calc(33.33% - 16px)', minWidth: 350 }}>
<Card
elevation={0}
sx={{
height: 400,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2
}}
>
<CardContent sx={{ height: '100%' }}>
<Typography variant="h6" fontWeight="bold" mb={2}>
{t('monitoring.charts.distributionTitle')}
</Typography>
<Typography variant="body2" color="text.secondary" mb={4}>
{t('monitoring.charts.distributionSubtitle')}
</Typography>
<Box sx={{ height: '70%', position: 'relative' }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={modelDistribution}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={5}
dataKey="value"
>
{modelDistribution.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip
formatter={value => t('monitoring.charts.tokensTooltip', { value: (value / 1000).toFixed(1) })}
contentStyle={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8
}}
/>
<Legend
verticalAlign="bottom"
height={36}
formatter={(value, entry, index) => (
<span style={{ color: theme.palette.text.primary, fontSize: 12, marginLeft: 5 }}>{value}</span>
)}
/>
</PieChart>
</ResponsiveContainer>
{/* 中间文字 */}
{/* <Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center'
}}
>
<Typography variant="h5" fontWeight="bold">
{modelDistribution.length}
</Typography>
<Typography variant="caption" color="text.secondary">
Models
</Typography>
</Box> */}
</Box>
</CardContent>
</Card>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,145 @@
import React from 'react';
import { Box, Card, CardContent, Grid, Typography, Stack, useTheme, alpha } from '@mui/material';
import {
Storage as StorageIcon,
Balance as BalanceIcon,
Bolt as BoltIcon,
AccessTime as AccessTimeIcon
} from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
function StatCard({ title, value, subValue, icon: Icon, color }) {
const theme = useTheme();
return (
<Card
elevation={0}
sx={{
height: '100%',
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2,
transition: 'all 0.3s ease',
'&:hover': {
boxShadow: theme.shadows[4],
transform: 'translateY(-2px)'
}
}}
>
<CardContent sx={{ p: 3 }}>
<Stack direction="row" alignItems="center" spacing={2} mb={2}>
<Box
sx={{
p: 1.5,
borderRadius: 2,
bgcolor: alpha(color, 0.1),
color: color,
display: 'flex'
}}
>
<Icon fontSize="medium" />
</Box>
<Typography variant="body2" color="text.secondary" fontWeight={500}>
{title}
</Typography>
</Stack>
<Typography variant="h3" fontWeight="bold" sx={{ mb: 1.5, color: 'text.primary' }}>
{value}
</Typography>
{subValue && (
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.6 }}>
{subValue}
</Typography>
)}
</CardContent>
</Card>
);
}
export default function StatsCards({ data }) {
const theme = useTheme();
const { t } = useTranslation();
// 格式化数字
const formatNumber = num => {
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
return num;
};
return (
<Box sx={{ display: 'flex', gap: 3, flexWrap: 'wrap' }}>
{/* 总 Token 消耗 */}
<Box sx={{ flex: '1 1 calc(25% - 18px)', minWidth: 250 }}>
<StatCard
title={t('monitoring.stats.totalTokens')}
value={formatNumber(data.totalTokens)}
subValue={t('monitoring.stats.inputOutput', {
input: formatNumber(data.inputTokens),
output: formatNumber(data.outputTokens)
})}
icon={StorageIcon}
color={theme.palette.primary.main}
/>
</Box>
{/* 平均 Token 消耗/次 */}
<Box sx={{ flex: '1 1 calc(25% - 18px)', minWidth: 250 }}>
<StatCard
title={t('monitoring.stats.avgTokensPerCall')}
value={formatNumber(data.avgTokensPerCall)}
subValue={t('monitoring.stats.inputOutput', {
input: formatNumber(Math.round(data.inputTokens / (data.totalCalls || 1))),
output: formatNumber(Math.round(data.outputTokens / (data.totalCalls || 1)))
})}
icon={BalanceIcon}
color={theme.palette.info.main}
/>
</Box>
{/* 总调用次数 */}
<Box sx={{ flex: '1 1 calc(25% - 18px)', minWidth: 250 }}>
<StatCard
title={t('monitoring.stats.totalCalls')}
value={formatNumber(data.totalCalls)}
subValue={
<Box component="span">
<Box component="span" sx={{ color: theme.palette.success.main, fontWeight: 600 }}>
{t('monitoring.stats.successCalls', { count: formatNumber(data.successCalls) })}
</Box>
<Box component="span" sx={{ mx: 1 }}>
·
</Box>
<Box component="span" sx={{ color: theme.palette.error.main, fontWeight: 600 }}>
{t('monitoring.stats.failedCalls', { count: formatNumber(data.failedCalls) })}
</Box>
{data.totalCalls > 0 && (
<Box component="span" sx={{ ml: 1, color: 'text.disabled' }}>
({t('monitoring.stats.failureRate', { rate: ((data.failureRate || 0) * 100).toFixed(1) })})
</Box>
)}
</Box>
}
icon={BoltIcon}
color={theme.palette.success.main}
/>
</Box>
{/* 平均响应耗时 */}
<Box sx={{ flex: '1 1 calc(25% - 18px)', minWidth: 250 }}>
<StatCard
title={t('monitoring.stats.avgLatency')}
value={`${(data.avgLatency / 1000).toFixed(2)}s`}
subValue={
data.successCalls > 0
? t('monitoring.stats.basedOnSuccessCalls', { count: formatNumber(data.successCalls) })
: t('monitoring.stats.noSuccessCalls')
}
icon={AccessTimeIcon}
color={theme.palette.warning.main}
/>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,215 @@
import React, { useState } from 'react';
import {
Box,
Card,
CardContent,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Chip,
TextField,
InputAdornment,
TablePagination,
useTheme,
alpha,
Tooltip
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import { useTranslation } from 'react-i18next';
const statusColors = {
SUCCESS: 'success',
FAILED: 'error'
};
export default function UsageTable({
data,
total,
page,
pageSize,
onPageChange,
onPageSizeChange,
searchTerm,
onSearchChange
}) {
const theme = useTheme();
const { t } = useTranslation();
const handleChangePage = (event, newPage) => {
onPageChange(newPage + 1); // MUI uses 0-indexed, our API uses 1-indexed
};
const handleChangeRowsPerPage = event => {
onPageSizeChange(parseInt(event.target.value, 10));
};
const handleSearchChange = event => {
onSearchChange(event.target.value);
};
// 直接使用传入的数据,分页和搜索已在后端完成
return (
<Card
elevation={0}
sx={{
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2,
mt: 3
}}
>
<CardContent>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
<Typography variant="h6" fontWeight="bold">
{t('monitoring.table.title')}
</Typography>
<TextField
size="small"
placeholder={t('monitoring.table.searchPlaceholder')}
value={searchTerm}
onChange={handleSearchChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" color="action" />
</InputAdornment>
)
}}
sx={{ width: 300 }}
/>
</Box>
<TableContainer>
<Table sx={{ minWidth: 650 }} aria-label="usage table">
<TableHead>
<TableRow>
<TableCell sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.projectName')}
</TableCell>
<TableCell sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.provider')}
</TableCell>
<TableCell sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.model')}
</TableCell>
<TableCell sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.status')}
</TableCell>
<TableCell sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.failureReason')}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.inputTokens')}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.outputTokens')}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.totalTokens')}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.calls')}
</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
{t('monitoring.table.columns.avgLatency')}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.map((row, index) => (
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component="th" scope="row">
<Typography variant="body2" fontWeight="500">
{row.projectName}
</Typography>
</TableCell>
<TableCell>
<Chip
label={row.provider}
size="small"
sx={{
borderRadius: 1,
bgcolor: alpha(theme.palette.primary.main, 0.1),
color: theme.palette.primary.main,
fontWeight: 500
}}
/>
</TableCell>
<TableCell>
<Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
{row.model}
</Typography>
</TableCell>
<TableCell>
<Chip
label={row.status === 'SUCCESS' ? t('monitoring.status.success') : t('monitoring.status.failed')}
color={statusColors[row.status] || 'default'}
size="small"
variant="outlined"
sx={{
borderRadius: 1,
borderColor: theme.palette[statusColors[row.status]]?.main,
color: theme.palette[statusColors[row.status]]?.main,
bgcolor: alpha(theme.palette[statusColors[row.status]]?.main || '#000', 0.05)
}}
/>
</TableCell>
<TableCell>
{row.failureReason ? (
<Tooltip title={row.failureReason} arrow placement="top">
<Chip
label={
row.failureReason.length > 20 ? row.failureReason.slice(0, 20) + '...' : row.failureReason
}
size="small"
color="error"
variant="soft"
sx={{
maxWidth: 200,
bgcolor: alpha(theme.palette.error.main, 0.1),
color: theme.palette.error.dark,
cursor: 'pointer'
}}
/>
</Tooltip>
) : (
'-'
)}
</TableCell>
<TableCell align="right">{row.inputTokens.toLocaleString()}</TableCell>
<TableCell align="right">{row.outputTokens.toLocaleString()}</TableCell>
<TableCell align="right" sx={{ fontWeight: 'bold' }}>
{row.totalTokens.toLocaleString()}
</TableCell>
<TableCell align="right">{row.calls}</TableCell>
<TableCell align="right">{row.avgLatency}</TableCell>
</TableRow>
))}
{data.length === 0 && (
<TableRow>
<TableCell colSpan={10} align="center" sx={{ py: 8 }}>
<Typography color="text.secondary">{t('monitoring.table.empty')}</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 50]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
labelRowsPerPage={t('monitoring.table.rowsPerPage')}
/>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,128 @@
import { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
export function useMonitoringData() {
const { t } = useTranslation();
const [loading, setLoading] = useState(true);
const [summaryData, setSummaryData] = useState({
summary: {
totalTokens: 0,
inputTokens: 0,
outputTokens: 0,
totalCalls: 0,
successCalls: 0,
failedCalls: 0,
totalLatency: 0,
avgLatency: 0,
avgTokensPerCall: 0,
failureRate: 0
},
trend: [],
modelDistribution: [],
projects: [],
providers: []
});
const [logsData, setLogsData] = useState({
details: [],
total: 0,
page: 1,
pageSize: 10,
totalPages: 0
});
const [filters, setFilters] = useState({
timeRange: '7d',
projectId: 'all',
provider: 'all',
status: 'all'
});
const [pagination, setPagination] = useState({
page: 1,
pageSize: 10
});
const [searchTerm, setSearchTerm] = useState('');
// 获取汇总数据
const fetchSummary = useCallback(async () => {
try {
const response = await axios.get('/api/monitoring/summary', {
params: filters
});
setSummaryData(response.data);
} catch (error) {
console.error('Failed to fetch monitoring summary:', error);
toast.error(t('monitoring.errors.fetchSummaryFailed'));
}
}, [filters, t]);
// 获取日志列表
const fetchLogs = useCallback(async () => {
try {
const response = await axios.get('/api/monitoring/logs', {
params: {
...filters,
page: pagination.page,
pageSize: pagination.pageSize,
search: searchTerm
}
});
setLogsData(response.data);
} catch (error) {
console.error('Failed to fetch monitoring logs:', error);
toast.error(t('monitoring.errors.fetchLogsFailed'));
}
}, [filters, pagination, searchTerm, t]);
// 初始加载
useEffect(() => {
const fetchData = async () => {
setLoading(true);
await Promise.all([fetchSummary(), fetchLogs()]);
setLoading(false);
};
fetchData();
}, [fetchSummary, fetchLogs]);
const handleFilterChange = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
setPagination(prev => ({ ...prev, page: 1 })); // 重置到第一页
};
const handlePageChange = newPage => {
setPagination(prev => ({ ...prev, page: newPage }));
};
const handlePageSizeChange = newPageSize => {
setPagination({ page: 1, pageSize: newPageSize });
};
const handleSearchChange = term => {
setSearchTerm(term);
setPagination(prev => ({ ...prev, page: 1 })); // 重置到第一页
};
const refresh = useCallback(async () => {
setLoading(true);
await Promise.all([fetchSummary(), fetchLogs()]);
setLoading(false);
}, [fetchSummary, fetchLogs]);
return {
loading,
summaryData,
logsData,
filters,
pagination,
searchTerm,
handleFilterChange,
handlePageChange,
handlePageSizeChange,
handleSearchChange,
refresh
};
}

View File

@@ -0,0 +1,244 @@
'use client';
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Stack,
Button,
FormControl,
Select,
MenuItem,
ToggleButton,
ToggleButtonGroup,
CircularProgress,
useTheme
} from '@mui/material';
import {
Download as DownloadIcon,
FilterList as FilterListIcon,
CloudQueue as CloudQueueIcon,
CheckCircle as CheckCircleIcon
} from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import Navbar from '@/components/Navbar/index';
import StatsCards from './components/StatsCards';
import Charts from './components/Charts';
import UsageTable from './components/UsageTable';
import { useMonitoringData } from './hooks/useMonitoringData';
export default function MonitoringPage() {
const theme = useTheme();
const { t } = useTranslation();
const [projects, setProjects] = useState([]);
const {
loading,
summaryData,
logsData,
filters,
pagination,
searchTerm,
handleFilterChange,
handlePageChange,
handlePageSizeChange,
handleSearchChange
} = useMonitoringData();
// 获取项目列表用于 Navbar
useEffect(() => {
async function fetchProjects() {
try {
const response = await fetch('/api/projects');
if (response.ok) {
const data = await response.json();
setProjects(data);
}
} catch (error) {
console.error('Failed to fetch projects:', error);
}
}
fetchProjects();
}, []);
const handleTimeRangeChange = (event, newRange) => {
if (newRange !== null) {
handleFilterChange('timeRange', newRange);
}
};
const handleExport = () => {
// 简单的导出功能实现,将当前 logsData.details 导出为 CSV
if (!logsData.details || logsData.details.length === 0) return;
const headers = [
t('monitoring.table.columns.projectName'),
t('monitoring.table.columns.provider'),
t('monitoring.table.columns.model'),
t('monitoring.table.columns.status'),
t('monitoring.table.columns.failureReason'),
t('monitoring.table.columns.inputTokens'),
t('monitoring.table.columns.outputTokens'),
t('monitoring.table.columns.totalTokens'),
t('monitoring.table.columns.calls'),
t('monitoring.table.columns.avgLatency')
];
const csvContent = [
headers.join(','),
...logsData.details.map(row =>
[
row.projectName,
row.provider,
row.model,
row.status,
(row.failureReason || '').replace(/,/g, ' '),
row.inputTokens,
row.outputTokens,
row.totalTokens,
row.calls,
row.avgLatency
].join(',')
)
].join('\n');
const blob = new Blob([`\uFEFF${csvContent}`], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `llm-monitoring-export-${new Date().toISOString().slice(0, 10)}.csv`;
link.click();
};
return (
<>
<Navbar projects={projects} currentProject={null} />
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 8 }}>
<Container maxWidth="xl" sx={{ pt: 4 }}>
{/* Header Area */}
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'flex-start', md: 'center' }}
spacing={2}
mb={4}
>
{/* Time Range Selector */}
<ToggleButtonGroup
value={filters.timeRange}
exclusive
onChange={handleTimeRangeChange}
aria-label="time range"
size="small"
sx={{
bgcolor: 'background.paper',
'& .MuiToggleButton-root': {
px: 3,
py: 1,
border: `1px solid ${theme.palette.divider}`,
textTransform: 'none',
fontWeight: 500,
'&.Mui-selected': {
bgcolor: 'primary.main',
color: 'primary.contrastText',
'&:hover': {
bgcolor: 'primary.dark'
}
}
}
}}
>
<ToggleButton value="24h">{t('monitoring.timeRange.24h')}</ToggleButton>
<ToggleButton value="7d">{t('monitoring.timeRange.7d')}</ToggleButton>
<ToggleButton value="30d">{t('monitoring.timeRange.30d')}</ToggleButton>
</ToggleButtonGroup>
{/* Filters & Actions */}
<Stack direction="row" spacing={2} alignItems="center">
<FormControl size="small" sx={{ minWidth: 120 }}>
<Select
value={filters.projectId}
onChange={e => handleFilterChange('projectId', e.target.value)}
displayEmpty
startAdornment={<FilterListIcon fontSize="small" sx={{ mr: 1, color: 'text.secondary' }} />}
sx={{ bgcolor: 'background.paper' }}
>
<MenuItem value="all">{t('monitoring.filters.allProjects')}</MenuItem>
{summaryData.projects.map(p => (
<MenuItem key={p.id} value={p.id}>
{p.name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 120 }}>
<Select
value={filters.provider}
onChange={e => handleFilterChange('provider', e.target.value)}
displayEmpty
startAdornment={<CloudQueueIcon fontSize="small" sx={{ mr: 1, color: 'text.secondary' }} />}
sx={{ bgcolor: 'background.paper' }}
>
<MenuItem value="all">{t('monitoring.filters.allProviders')}</MenuItem>
{summaryData.providers.map(provider => (
<MenuItem key={provider} value={provider}>
{provider}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl size="small" sx={{ minWidth: 120 }}>
<Select
value={filters.status}
onChange={e => handleFilterChange('status', e.target.value)}
displayEmpty
startAdornment={<CheckCircleIcon fontSize="small" sx={{ mr: 1, color: 'text.secondary' }} />}
sx={{ bgcolor: 'background.paper' }}
>
<MenuItem value="all">{t('monitoring.filters.allStatus')}</MenuItem>
<MenuItem value="SUCCESS">{t('monitoring.status.success')}</MenuItem>
<MenuItem value="FAILED">{t('monitoring.status.failed')}</MenuItem>
</Select>
</FormControl>
<Button
variant="contained"
startIcon={<DownloadIcon />}
onClick={handleExport}
sx={{ textTransform: 'none', px: 3 }}
>
{t('monitoring.actions.export')}
</Button>
</Stack>
</Stack>
{loading ? (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="60vh">
<CircularProgress />
</Box>
) : (
<Stack spacing={3}>
{/* 统计卡片 */}
<StatsCards data={summaryData.summary} />
{/* 图表区域 */}
<Charts trendData={summaryData.trend} modelDistribution={summaryData.modelDistribution} />
{/* 详细表格 */}
<UsageTable
data={logsData.details}
total={logsData.total}
page={pagination.page}
pageSize={pagination.pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
searchTerm={searchTerm}
onSearchChange={handleSearchChange}
/>
</Stack>
)}
</Container>
</Box>
</>
);
}