mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-05 00:03:42 +08:00
feat: basic overview is done
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-toastify": "^9.0.8",
|
"react-toastify": "^9.0.8",
|
||||||
"react-turnstile": "^1.0.5",
|
"react-turnstile": "^1.0.5",
|
||||||
|
"recharts": "^2.15.1",
|
||||||
"semantic-ui-css": "^2.5.0",
|
"semantic-ui-css": "^2.5.0",
|
||||||
"semantic-ui-react": "^2.1.3"
|
"semantic-ui-react": "^2.1.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import TopUp from './pages/TopUp';
|
|||||||
import Log from './pages/Log';
|
import Log from './pages/Log';
|
||||||
import Chat from './pages/Chat';
|
import Chat from './pages/Chat';
|
||||||
import LarkOAuth from './components/LarkOAuth';
|
import LarkOAuth from './components/LarkOAuth';
|
||||||
|
import Dashboard from './pages/Dashboard';
|
||||||
|
|
||||||
const Home = lazy(() => import('./pages/Home'));
|
const Home = lazy(() => import('./pages/Home'));
|
||||||
const About = lazy(() => import('./pages/About'));
|
const About = lazy(() => import('./pages/About'));
|
||||||
@@ -292,9 +293,15 @@ function App() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path='*' element={
|
<Route
|
||||||
<NotFound />
|
path='/dashboard'
|
||||||
} />
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<Dashboard />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path='*' element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,22 @@ import React, { useContext, useState } from 'react';
|
|||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
|
|
||||||
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
|
import {
|
||||||
import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers';
|
Button,
|
||||||
|
Container,
|
||||||
|
Dropdown,
|
||||||
|
Icon,
|
||||||
|
Menu,
|
||||||
|
Segment,
|
||||||
|
} from 'semantic-ui-react';
|
||||||
|
import {
|
||||||
|
API,
|
||||||
|
getLogo,
|
||||||
|
getSystemName,
|
||||||
|
isAdmin,
|
||||||
|
isMobile,
|
||||||
|
showSuccess,
|
||||||
|
} from '../helpers';
|
||||||
import '../index.css';
|
import '../index.css';
|
||||||
|
|
||||||
// Header Buttons
|
// Header Buttons
|
||||||
@@ -11,58 +25,63 @@ let headerButtons = [
|
|||||||
{
|
{
|
||||||
name: '首页',
|
name: '首页',
|
||||||
to: '/',
|
to: '/',
|
||||||
icon: 'home'
|
icon: 'home',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '渠道',
|
name: '渠道',
|
||||||
to: '/channel',
|
to: '/channel',
|
||||||
icon: 'sitemap',
|
icon: 'sitemap',
|
||||||
admin: true
|
admin: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '令牌',
|
name: '令牌',
|
||||||
to: '/token',
|
to: '/token',
|
||||||
icon: 'key'
|
icon: 'key',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '兑换',
|
name: '兑换',
|
||||||
to: '/redemption',
|
to: '/redemption',
|
||||||
icon: 'dollar sign',
|
icon: 'dollar sign',
|
||||||
admin: true
|
admin: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '充值',
|
name: '充值',
|
||||||
to: '/topup',
|
to: '/topup',
|
||||||
icon: 'cart'
|
icon: 'cart',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '用户',
|
name: '用户',
|
||||||
to: '/user',
|
to: '/user',
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
admin: true
|
admin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '总览',
|
||||||
|
to: '/dashboard',
|
||||||
|
icon: 'chart bar',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '日志',
|
name: '日志',
|
||||||
to: '/log',
|
to: '/log',
|
||||||
icon: 'book'
|
icon: 'book',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '设置',
|
name: '设置',
|
||||||
to: '/setting',
|
to: '/setting',
|
||||||
icon: 'setting'
|
icon: 'setting',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '关于',
|
name: '关于',
|
||||||
to: '/about',
|
to: '/about',
|
||||||
icon: 'info circle'
|
icon: 'info circle',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (localStorage.getItem('chat_link')) {
|
if (localStorage.getItem('chat_link')) {
|
||||||
headerButtons.splice(1, 0, {
|
headerButtons.splice(1, 0, {
|
||||||
name: '聊天',
|
name: '聊天',
|
||||||
to: '/chat',
|
to: '/chat',
|
||||||
icon: 'comments'
|
icon: 'comments',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,18 +142,14 @@ const Header = () => {
|
|||||||
borderBottom: 'none',
|
borderBottom: 'none',
|
||||||
marginBottom: '0',
|
marginBottom: '0',
|
||||||
borderTop: 'none',
|
borderTop: 'none',
|
||||||
height: '51px'
|
height: '51px',
|
||||||
}
|
}
|
||||||
: { borderTop: 'none', height: '52px' }
|
: { borderTop: 'none', height: '52px' }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Container>
|
<Container>
|
||||||
<Menu.Item as={Link} to='/'>
|
<Menu.Item as={Link} to='/'>
|
||||||
<img
|
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
||||||
src={logo}
|
|
||||||
alt='logo'
|
|
||||||
style={{ marginRight: '0.75em' }}
|
|
||||||
/>
|
|
||||||
<div style={{ fontSize: '20px' }}>
|
<div style={{ fontSize: '20px' }}>
|
||||||
<b>{systemName}</b>
|
<b>{systemName}</b>
|
||||||
</div>
|
</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;
|
||||||
Reference in New Issue
Block a user