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:
parent
b4e69df802
commit
dc8c3bc69e
@ -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"
|
||||
},
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
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;
|
Loading…
x
Reference in New Issue
Block a user