mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-12 19:33:41 +08:00
feat: basic overview is done
This commit is contained in:
63
web/default/src/pages/Dashboard/Dashboard.css
Normal file
63
web/default/src/pages/Dashboard/Dashboard.css
Normal file
@@ -0,0 +1,63 @@
|
||||
.dashboard-container {
|
||||
padding: 20px;
|
||||
background-color: #f7f9fc;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #2185d0 0%, #1678c2 100%) !important;
|
||||
color: white !important;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
|
||||
transition: transform 0.2s ease !important;
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.stat-card .statistic {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.charts-grid {
|
||||
margin-top: 1rem !important;
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.charts-grid .column {
|
||||
padding: 0.5rem !important;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
margin: 0 !important;
|
||||
height: 100%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ui.card > .content > .header {
|
||||
color: #1a1a1a;
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* 优化图表响应式布局 */
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.charts-grid .column {
|
||||
padding: 0.25rem !important;
|
||||
}
|
||||
}
|
||||
295
web/default/src/pages/Dashboard/index.js
Normal file
295
web/default/src/pages/Dashboard/index.js
Normal file
@@ -0,0 +1,295 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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';
|
||||
|
||||
const Dashboard = () => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const calculateSummary = (dashboardData) => {
|
||||
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) => {
|
||||
const colors = [
|
||||
'#1f77b4',
|
||||
'#ff7f0e',
|
||||
'#2ca02c',
|
||||
'#d62728',
|
||||
'#9467bd',
|
||||
'#8c564b',
|
||||
'#e377c2',
|
||||
'#7f7f7f',
|
||||
'#bcbd22',
|
||||
'#17becf',
|
||||
];
|
||||
return colors[index % colors.length];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='dashboard-container'>
|
||||
<Grid columns={3} stackable>
|
||||
<Grid.Column>
|
||||
<Card fluid className='stat-card'>
|
||||
<Card.Content>
|
||||
<Statistic>
|
||||
<Statistic.Value>{summaryData.todayRequests}</Statistic.Value>
|
||||
<Statistic.Label>今日请求量</Statistic.Label>
|
||||
</Statistic>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</Grid.Column>
|
||||
<Grid.Column>
|
||||
<Card fluid className='stat-card'>
|
||||
<Card.Content>
|
||||
<Statistic>
|
||||
<Statistic.Value>
|
||||
${summaryData.todayQuota.toFixed(3)}
|
||||
</Statistic.Value>
|
||||
<Statistic.Label>今日消费</Statistic.Label>
|
||||
</Statistic>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</Grid.Column>
|
||||
<Grid.Column>
|
||||
<Card fluid className='stat-card'>
|
||||
<Card.Content>
|
||||
<Statistic>
|
||||
<Statistic.Value>{summaryData.todayTokens}</Statistic.Value>
|
||||
<Statistic.Label>今日 token</Statistic.Label>
|
||||
</Statistic>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
|
||||
{/* 三个并排的折线图 */}
|
||||
<Grid columns={3} stackable className='charts-grid'>
|
||||
<Grid.Column>
|
||||
<Card fluid className='chart-card'>
|
||||
<Card.Content>
|
||||
<Card.Header>今日请求量</Card.Header>
|
||||
<div className='chart-container'>
|
||||
<ResponsiveContainer width='100%' height={120}>
|
||||
<LineChart data={timeSeriesData}>
|
||||
<CartesianGrid strokeDasharray='3 3' />
|
||||
<XAxis dataKey='date' />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='requests'
|
||||
stroke='#2185d0'
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</Grid.Column>
|
||||
|
||||
<Grid.Column>
|
||||
<Card fluid className='chart-card'>
|
||||
<Card.Content>
|
||||
<Card.Header>今日消费</Card.Header>
|
||||
<div className='chart-container'>
|
||||
<ResponsiveContainer width='100%' height={120}>
|
||||
<LineChart data={timeSeriesData}>
|
||||
<CartesianGrid strokeDasharray='3 3' />
|
||||
<XAxis dataKey='date' />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='quota'
|
||||
stroke='#21ba45'
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</Grid.Column>
|
||||
|
||||
<Grid.Column>
|
||||
<Card fluid className='chart-card'>
|
||||
<Card.Content>
|
||||
<Card.Header>今日 token</Card.Header>
|
||||
<div className='chart-container'>
|
||||
<ResponsiveContainer width='100%' height={120}>
|
||||
<LineChart data={timeSeriesData}>
|
||||
<CartesianGrid strokeDasharray='3 3' />
|
||||
<XAxis dataKey='date' />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='tokens'
|
||||
stroke='#f2711c'
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
|
||||
{/* 模型使用统计 */}
|
||||
<Card fluid className='chart-card'>
|
||||
<Card.Content>
|
||||
<Card.Header>统计</Card.Header>
|
||||
<div className='chart-container'>
|
||||
<ResponsiveContainer width='100%' height={300}>
|
||||
<BarChart data={modelData}>
|
||||
<CartesianGrid strokeDasharray='3 3' />
|
||||
<XAxis dataKey='date' />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
{models.map((model, index) => (
|
||||
<Bar
|
||||
key={model}
|
||||
dataKey={model}
|
||||
stackId='a'
|
||||
fill={getRandomColor(index)}
|
||||
name={model}
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
Reference in New Issue
Block a user