1
0
mirror of https://github.com/songquanpeng/one-api.git synced 2025-02-06 17:11:32 +00:00

feat: basic overview is done

This commit is contained in:
JustSong 2025-01-31 22:18:02 +08:00
parent b4e69df802
commit dc8c3bc69e
5 changed files with 412 additions and 31 deletions

View File

@ -13,6 +13,7 @@
"react-scripts": "5.0.1",
"react-toastify": "^9.0.8",
"react-turnstile": "^1.0.5",
"recharts": "^2.15.1",
"semantic-ui-css": "^2.5.0",
"semantic-ui-react": "^2.1.3"
},

View File

@ -25,6 +25,7 @@ import TopUp from './pages/TopUp';
import Log from './pages/Log';
import Chat from './pages/Chat';
import LarkOAuth from './components/LarkOAuth';
import Dashboard from './pages/Dashboard';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
@ -261,11 +262,11 @@ function App() {
<Route
path='/topup'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<TopUp />
</Suspense>
</PrivateRoute>
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<TopUp />
</Suspense>
</PrivateRoute>
}
/>
<Route
@ -292,9 +293,15 @@ function App() {
</Suspense>
}
/>
<Route path='*' element={
<NotFound />
} />
<Route
path='/dashboard'
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route path='*' element={<NotFound />} />
</Routes>
);
}

View File

@ -2,8 +2,22 @@ import React, { useContext, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User';
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers';
import {
Button,
Container,
Dropdown,
Icon,
Menu,
Segment,
} from 'semantic-ui-react';
import {
API,
getLogo,
getSystemName,
isAdmin,
isMobile,
showSuccess,
} from '../helpers';
import '../index.css';
// Header Buttons
@ -11,58 +25,63 @@ let headerButtons = [
{
name: '首页',
to: '/',
icon: 'home'
icon: 'home',
},
{
name: '渠道',
to: '/channel',
icon: 'sitemap',
admin: true
admin: true,
},
{
name: '令牌',
to: '/token',
icon: 'key'
icon: 'key',
},
{
name: '兑换',
to: '/redemption',
icon: 'dollar sign',
admin: true
admin: true,
},
{
name: '充值',
to: '/topup',
icon: 'cart'
icon: 'cart',
},
{
name: '用户',
to: '/user',
icon: 'user',
admin: true
admin: true,
},
{
name: '总览',
to: '/dashboard',
icon: 'chart bar',
},
{
name: '日志',
to: '/log',
icon: 'book'
icon: 'book',
},
{
name: '设置',
to: '/setting',
icon: 'setting'
icon: 'setting',
},
{
name: '关于',
to: '/about',
icon: 'info circle'
}
icon: 'info circle',
},
];
if (localStorage.getItem('chat_link')) {
headerButtons.splice(1, 0, {
name: '聊天',
to: '/chat',
icon: 'comments'
icon: 'comments',
});
}
@ -120,21 +139,17 @@ const Header = () => {
style={
showSidebar
? {
borderBottom: 'none',
marginBottom: '0',
borderTop: 'none',
height: '51px'
}
borderBottom: 'none',
marginBottom: '0',
borderTop: 'none',
height: '51px',
}
: { borderTop: 'none', height: '52px' }
}
>
<Container>
<Menu.Item as={Link} to='/'>
<img
src={logo}
alt='logo'
style={{ marginRight: '0.75em' }}
/>
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
<div style={{ fontSize: '20px' }}>
<b>{systemName}</b>
</div>

View 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;
}
}

View 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;