diff --git a/web/default/package.json b/web/default/package.json index ba45011f..d03288ed 100644 --- a/web/default/package.json +++ b/web/default/package.json @@ -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" }, diff --git a/web/default/src/App.js b/web/default/src/App.js index 4ece4eeb..3597f71b 100644 --- a/web/default/src/App.js +++ b/web/default/src/App.js @@ -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() { - }> - - - + + }> + + + } /> } /> - - } /> + + + + } + /> + } /> ); } diff --git a/web/default/src/components/Header.js b/web/default/src/components/Header.js index 21ebcab6..6f81972e 100644 --- a/web/default/src/components/Header.js +++ b/web/default/src/components/Header.js @@ -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' } } > - logo + logo
{systemName}
diff --git a/web/default/src/pages/Dashboard/Dashboard.css b/web/default/src/pages/Dashboard/Dashboard.css new file mode 100644 index 00000000..d0a3d52c --- /dev/null +++ b/web/default/src/pages/Dashboard/Dashboard.css @@ -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; + } +} \ No newline at end of file diff --git a/web/default/src/pages/Dashboard/index.js b/web/default/src/pages/Dashboard/index.js new file mode 100644 index 00000000..dd5f9996 --- /dev/null +++ b/web/default/src/pages/Dashboard/index.js @@ -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 ( +
+ + + + + + {summaryData.todayRequests} + 今日请求量 + + + + + + + + + + ${summaryData.todayQuota.toFixed(3)} + + 今日消费 + + + + + + + + + {summaryData.todayTokens} + 今日 token + + + + + + + {/* 三个并排的折线图 */} + + + + + 今日请求量 +
+ + + + + + + + + +
+
+
+
+ + + + + 今日消费 +
+ + + + + + + + + +
+
+
+
+ + + + + 今日 token +
+ + + + + + + + + +
+
+
+
+
+ + {/* 模型使用统计 */} + + + 统计 +
+ + + + + + + + {models.map((model, index) => ( + + ))} + + +
+
+
+
+ ); +}; + +export default Dashboard;