408 lines
13 KiB
JavaScript
408 lines
13 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Card, Grid, Statistic } from 'semantic-ui-react';
|
|
import {
|
|
LineChart,
|
|
Line,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
BarChart,
|
|
Bar,
|
|
Legend,
|
|
} from 'recharts';
|
|
import axios from 'axios';
|
|
import './Dashboard.css';
|
|
|
|
// 在 Dashboard 组件内添加自定义配置
|
|
const chartConfig = {
|
|
lineChart: {
|
|
style: {
|
|
background: '#fff',
|
|
borderRadius: '8px',
|
|
},
|
|
line: {
|
|
strokeWidth: 2,
|
|
dot: false,
|
|
activeDot: { r: 4 },
|
|
},
|
|
grid: {
|
|
vertical: false,
|
|
horizontal: true,
|
|
opacity: 0.1,
|
|
},
|
|
},
|
|
colors: {
|
|
requests: '#4318FF',
|
|
quota: '#00B5D8',
|
|
tokens: '#6C63FF',
|
|
},
|
|
barColors: [
|
|
'#4318FF', // 深紫色
|
|
'#00B5D8', // 青色
|
|
'#6C63FF', // 紫色
|
|
'#05CD99', // 绿色
|
|
'#FFB547', // 橙色
|
|
'#FF5E7D', // 粉色
|
|
'#41B883', // 翠绿
|
|
'#7983FF', // 淡紫
|
|
'#FF8F6B', // 珊瑚色
|
|
'#49BEFF', // 天蓝
|
|
],
|
|
};
|
|
|
|
const Dashboard = () => {
|
|
const { t } = useTranslation();
|
|
const [data, setData] = useState([]);
|
|
const [summaryData, setSummaryData] = useState({
|
|
todayRequests: 0,
|
|
todayQuota: 0,
|
|
todayTokens: 0,
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchDashboardData();
|
|
}, []);
|
|
|
|
const fetchDashboardData = async () => {
|
|
try {
|
|
const response = await axios.get('/api/user/dashboard');
|
|
if (response.data.success) {
|
|
const dashboardData = response.data.data || [];
|
|
setData(dashboardData);
|
|
calculateSummary(dashboardData);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch dashboard data:', error);
|
|
setData([]);
|
|
calculateSummary([]);
|
|
}
|
|
};
|
|
|
|
const calculateSummary = (dashboardData) => {
|
|
if (!Array.isArray(dashboardData) || dashboardData.length === 0) {
|
|
setSummaryData({
|
|
todayRequests: 0,
|
|
todayQuota: 0,
|
|
todayTokens: 0
|
|
});
|
|
return;
|
|
}
|
|
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const todayData = dashboardData.filter((item) => item.Day === today);
|
|
|
|
const summary = {
|
|
todayRequests: todayData.reduce(
|
|
(sum, item) => sum + item.RequestCount,
|
|
0
|
|
),
|
|
todayQuota:
|
|
todayData.reduce((sum, item) => sum + item.Quota, 0) / 1000000,
|
|
todayTokens: todayData.reduce(
|
|
(sum, item) => sum + item.PromptTokens + item.CompletionTokens,
|
|
0
|
|
),
|
|
};
|
|
|
|
setSummaryData(summary);
|
|
};
|
|
|
|
// 处理数据以供折线图使用,补充缺失的日期
|
|
const processTimeSeriesData = () => {
|
|
const dailyData = {};
|
|
|
|
// 获取日期范围
|
|
const dates = data.map((item) => item.Day);
|
|
const minDate = new Date(Math.min(...dates.map((d) => new Date(d))));
|
|
const maxDate = new Date(Math.max(...dates.map((d) => new Date(d))));
|
|
|
|
// 生成所有日期
|
|
for (let d = new Date(minDate); d <= maxDate; d.setDate(d.getDate() + 1)) {
|
|
const dateStr = d.toISOString().split('T')[0];
|
|
dailyData[dateStr] = {
|
|
date: dateStr,
|
|
requests: 0,
|
|
quota: 0,
|
|
tokens: 0,
|
|
};
|
|
}
|
|
|
|
// 填充实际数据
|
|
data.forEach((item) => {
|
|
dailyData[item.Day].requests += item.RequestCount;
|
|
dailyData[item.Day].quota += item.Quota / 1000000;
|
|
dailyData[item.Day].tokens += item.PromptTokens + item.CompletionTokens;
|
|
});
|
|
|
|
return Object.values(dailyData).sort((a, b) =>
|
|
a.date.localeCompare(b.date)
|
|
);
|
|
};
|
|
|
|
// 处理数据以供堆叠柱状图使用
|
|
const processModelData = () => {
|
|
const timeData = {};
|
|
|
|
// 获取日期范围
|
|
const dates = data.map((item) => item.Day);
|
|
const minDate = new Date(Math.min(...dates.map((d) => new Date(d))));
|
|
const maxDate = new Date(Math.max(...dates.map((d) => new Date(d))));
|
|
|
|
// 生成所有日期
|
|
for (let d = new Date(minDate); d <= maxDate; d.setDate(d.getDate() + 1)) {
|
|
const dateStr = d.toISOString().split('T')[0];
|
|
timeData[dateStr] = {
|
|
date: dateStr,
|
|
};
|
|
|
|
// 初始化所有模型的数据为0
|
|
const models = [...new Set(data.map((item) => item.ModelName))];
|
|
models.forEach((model) => {
|
|
timeData[dateStr][model] = 0;
|
|
});
|
|
}
|
|
|
|
// 填充实际数据
|
|
data.forEach((item) => {
|
|
timeData[item.Day][item.ModelName] =
|
|
item.PromptTokens + item.CompletionTokens;
|
|
});
|
|
|
|
return Object.values(timeData).sort((a, b) => a.date.localeCompare(b.date));
|
|
};
|
|
|
|
// 获取所有唯一的模型名称
|
|
const getUniqueModels = () => {
|
|
return [...new Set(data.map((item) => item.ModelName))];
|
|
};
|
|
|
|
const timeSeriesData = processTimeSeriesData();
|
|
const modelData = processModelData();
|
|
const models = getUniqueModels();
|
|
|
|
// 生成随机颜色
|
|
const getRandomColor = (index) => {
|
|
return chartConfig.barColors[index % chartConfig.barColors.length];
|
|
};
|
|
|
|
return (
|
|
<div className='dashboard-container'>
|
|
{/* 三个并排的折线图 */}
|
|
<Grid columns={3} stackable className='charts-grid'>
|
|
<Grid.Column>
|
|
<Card fluid className='chart-card'>
|
|
<Card.Content>
|
|
<Card.Header>
|
|
{t('dashboard.charts.requests.title')}
|
|
<span className='stat-value'>{summaryData.todayRequests}</span>
|
|
</Card.Header>
|
|
<div className='chart-container'>
|
|
<ResponsiveContainer width='100%' height={120}>
|
|
<LineChart data={timeSeriesData}>
|
|
<CartesianGrid
|
|
strokeDasharray='3 3'
|
|
vertical={chartConfig.lineChart.grid.vertical}
|
|
horizontal={chartConfig.lineChart.grid.horizontal}
|
|
opacity={chartConfig.lineChart.grid.opacity}
|
|
/>
|
|
<XAxis
|
|
dataKey='date'
|
|
axisLine={false}
|
|
tickLine={false}
|
|
tick={{ fontSize: 12, fill: '#A3AED0' }}
|
|
/>
|
|
<YAxis hide={true} />
|
|
<Tooltip
|
|
contentStyle={{
|
|
background: '#fff',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
|
}}
|
|
formatter={(value) => [
|
|
value,
|
|
t('dashboard.charts.requests.tooltip')
|
|
]}
|
|
labelFormatter={(label) => `${t('dashboard.tooltip.date')}: ${label}`}
|
|
/>
|
|
<Line
|
|
type='monotone'
|
|
dataKey='requests'
|
|
stroke={chartConfig.colors.requests}
|
|
strokeWidth={chartConfig.lineChart.line.strokeWidth}
|
|
dot={chartConfig.lineChart.line.dot}
|
|
activeDot={chartConfig.lineChart.line.activeDot}
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</Card.Content>
|
|
</Card>
|
|
</Grid.Column>
|
|
|
|
<Grid.Column>
|
|
<Card fluid className='chart-card'>
|
|
<Card.Content>
|
|
<Card.Header>
|
|
{t('dashboard.charts.quota.title')}
|
|
<span className='stat-value'>
|
|
${summaryData.todayQuota.toFixed(3)}
|
|
</span>
|
|
</Card.Header>
|
|
<div className='chart-container'>
|
|
<ResponsiveContainer width='100%' height={120}>
|
|
<LineChart data={timeSeriesData}>
|
|
<CartesianGrid
|
|
strokeDasharray='3 3'
|
|
vertical={chartConfig.lineChart.grid.vertical}
|
|
horizontal={chartConfig.lineChart.grid.horizontal}
|
|
opacity={chartConfig.lineChart.grid.opacity}
|
|
/>
|
|
<XAxis
|
|
dataKey='date'
|
|
axisLine={false}
|
|
tickLine={false}
|
|
tick={{ fontSize: 12, fill: '#A3AED0' }}
|
|
/>
|
|
<YAxis hide={true} />
|
|
<Tooltip
|
|
contentStyle={{
|
|
background: '#fff',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
|
}}
|
|
formatter={(value) => [
|
|
value,
|
|
t('dashboard.charts.quota.tooltip')
|
|
]}
|
|
labelFormatter={(label) => `${t('dashboard.tooltip.date')}: ${label}`}
|
|
/>
|
|
<Line
|
|
type='monotone'
|
|
dataKey='quota'
|
|
stroke={chartConfig.colors.quota}
|
|
strokeWidth={chartConfig.lineChart.line.strokeWidth}
|
|
dot={chartConfig.lineChart.line.dot}
|
|
activeDot={chartConfig.lineChart.line.activeDot}
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</Card.Content>
|
|
</Card>
|
|
</Grid.Column>
|
|
|
|
<Grid.Column>
|
|
<Card fluid className='chart-card'>
|
|
<Card.Content>
|
|
<Card.Header>
|
|
{t('dashboard.charts.tokens.title')}
|
|
<span className='stat-value'>{summaryData.todayTokens}</span>
|
|
</Card.Header>
|
|
<div className='chart-container'>
|
|
<ResponsiveContainer width='100%' height={120}>
|
|
<LineChart data={timeSeriesData}>
|
|
<CartesianGrid
|
|
strokeDasharray='3 3'
|
|
vertical={chartConfig.lineChart.grid.vertical}
|
|
horizontal={chartConfig.lineChart.grid.horizontal}
|
|
opacity={chartConfig.lineChart.grid.opacity}
|
|
/>
|
|
<XAxis
|
|
dataKey='date'
|
|
axisLine={false}
|
|
tickLine={false}
|
|
tick={{ fontSize: 12, fill: '#A3AED0' }}
|
|
/>
|
|
<YAxis hide={true} />
|
|
<Tooltip
|
|
contentStyle={{
|
|
background: '#fff',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
|
}}
|
|
formatter={(value) => [
|
|
value,
|
|
t('dashboard.charts.tokens.tooltip')
|
|
]}
|
|
labelFormatter={(label) => `${t('dashboard.tooltip.date')}: ${label}`}
|
|
/>
|
|
<Line
|
|
type='monotone'
|
|
dataKey='tokens'
|
|
stroke={chartConfig.colors.tokens}
|
|
strokeWidth={chartConfig.lineChart.line.strokeWidth}
|
|
dot={chartConfig.lineChart.line.dot}
|
|
activeDot={chartConfig.lineChart.line.activeDot}
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</Card.Content>
|
|
</Card>
|
|
</Grid.Column>
|
|
</Grid>
|
|
|
|
{/* 模型使用统计 */}
|
|
<Card fluid className='chart-card'>
|
|
<Card.Content>
|
|
<Card.Header>{t('dashboard.statistics.title')}</Card.Header>
|
|
<div className='chart-container'>
|
|
<ResponsiveContainer width='100%' height={300}>
|
|
<BarChart data={modelData}>
|
|
<CartesianGrid
|
|
strokeDasharray='3 3'
|
|
vertical={false}
|
|
opacity={0.1}
|
|
/>
|
|
<XAxis
|
|
dataKey='date'
|
|
axisLine={false}
|
|
tickLine={false}
|
|
tick={{ fontSize: 12, fill: '#A3AED0' }}
|
|
/>
|
|
<YAxis
|
|
axisLine={false}
|
|
tickLine={false}
|
|
tick={{ fontSize: 12, fill: '#A3AED0' }}
|
|
/>
|
|
<Tooltip
|
|
contentStyle={{
|
|
background: '#fff',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
|
}}
|
|
labelFormatter={(label) => `${t('dashboard.tooltip.date')}: ${label}`}
|
|
/>
|
|
<Legend
|
|
wrapperStyle={{
|
|
paddingTop: '20px',
|
|
}}
|
|
/>
|
|
{models.map((model, index) => (
|
|
<Bar
|
|
key={model}
|
|
dataKey={model}
|
|
stackId='a'
|
|
fill={getRandomColor(index)}
|
|
name={model}
|
|
radius={[4, 4, 0, 0]}
|
|
/>
|
|
))}
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</Card.Content>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Dashboard;
|