From b2d36b946d03f68abf7311bfaff44b46bb1ad565 Mon Sep 17 00:00:00 2001
From: CalciumIon <1808837298@qq.com>
Date: Thu, 12 Dec 2024 16:11:17 +0800
Subject: [PATCH] feat: Update SiderBar and Detail components for improved
navigation and data visualization
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Removed the '模型价格' (Pricing) link from the SiderBar for a cleaner interface.
- Added a new '数据看板' (Data Dashboard) link to the SiderBar, enhancing navigation options.
- Refactored the Detail component to include user context and style context for better state management.
- Introduced new state variables to track token consumption and updated data handling for charts.
- Enhanced the layout with additional cards and tabs for displaying user quota and usage statistics.
- Improved data processing logic for pie and line charts, ensuring accurate representation of user data.
---
web/src/components/SiderBar.js | 26 ++---
web/src/pages/Detail/index.js | 206 +++++++++++++++++++++++++--------
2 files changed, 166 insertions(+), 66 deletions(-)
diff --git a/web/src/components/SiderBar.js b/web/src/components/SiderBar.js
index d282e40e..0bad3142 100644
--- a/web/src/components/SiderBar.js
+++ b/web/src/components/SiderBar.js
@@ -73,12 +73,6 @@ const SiderBar = () => {
to: '/playground',
icon: ,
},
- {
- text: '模型价格',
- itemKey: 'pricing',
- to: '/pricing',
- icon: ,
- },
{
text: '渠道',
itemKey: 'channel',
@@ -102,6 +96,16 @@ const SiderBar = () => {
to: '/token',
icon: ,
},
+ {
+ text: '数据看板',
+ itemKey: 'detail',
+ to: '/detail',
+ icon: ,
+ className:
+ localStorage.getItem('enable_data_export') === 'true'
+ ? 'semi-navigation-item-normal'
+ : 'tableHiddle',
+ },
{
text: '兑换码',
itemKey: 'redemption',
@@ -128,16 +132,6 @@ const SiderBar = () => {
to: '/log',
icon: ,
},
- {
- text: '数据看板',
- itemKey: 'detail',
- to: '/detail',
- icon: ,
- className:
- localStorage.getItem('enable_data_export') === 'true'
- ? 'semi-navigation-item-normal'
- : 'tableHiddle',
- },
{
text: '绘图',
itemKey: 'midjourney',
diff --git a/web/src/pages/Detail/index.js b/web/src/pages/Detail/index.js
index a00b3453..e202f086 100644
--- a/web/src/pages/Detail/index.js
+++ b/web/src/pages/Detail/index.js
@@ -1,7 +1,7 @@
-import React, { useEffect, useRef, useState } from 'react';
+import React, { useContext, useEffect, useRef, useState } from 'react';
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
-import { Button, Col, Form, Layout, Row, Spin } from '@douyinfe/semi-ui';
+import { Button, Card, Col, Descriptions, Form, Layout, Row, Spin, Tabs } from '@douyinfe/semi-ui';
import { VChart } from "@visactor/react-vchart";
import {
API,
@@ -19,10 +19,14 @@ import {
stringToColor,
modelToColor,
} from '../../helpers/render';
+import { UserContext } from '../../context/User/index.js';
+import { StyleContext } from '../../context/Style/index.js';
const Detail = (props) => {
const formRef = useRef();
let now = new Date();
+ const [userState, userDispatch] = useContext(UserContext);
+ const [styleState, styleDispatch] = useContext(StyleContext);
const [inputs, setInputs] = useState({
username: '',
token_name: '',
@@ -44,6 +48,7 @@ const Detail = (props) => {
const [loading, setLoading] = useState(false);
const [quotaData, setQuotaData] = useState([]);
const [consumeQuota, setConsumeQuota] = useState(0);
+ const [consumeTokens, setConsumeTokens] = useState(0);
const [times, setTimes] = useState(0);
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
localStorage.getItem('data_export_default_time') || 'hour',
@@ -245,25 +250,30 @@ const Detail = (props) => {
let totalQuota = 0;
let totalTimes = 0;
let uniqueModels = new Set();
+ let totalTokens = 0;
- // 首先收集所有唯一的模型名称
- data.forEach(item => uniqueModels.add(item.model_name));
+ // 收集所有唯一的模型名称和时间点
+ let uniqueTimes = new Set();
+ data.forEach(item => {
+ uniqueModels.add(item.model_name);
+ uniqueTimes.add(timestamp2string1(item.created_at, dataExportDefaultTime));
+ totalTokens += item.token_used;
+ });
- // 为每个唯一的模型生成或获取颜色
+ // 处理颜色映射
const newModelColors = {};
Array.from(uniqueModels).forEach((modelName) => {
- // 优先使用 modelColorMap 中的颜色,然后是已存在的颜色,最后使用新的颜色生成函数
newModelColors[modelName] = modelColorMap[modelName] ||
modelColors[modelName] ||
- modelToColor(modelName); // 使用新的颜色生成函数替代 stringToColor
+ modelToColor(modelName);
});
setModelColors(newModelColors);
- for (let i = 0; i < data.length; i++) {
- const item = data[i];
+ // 处理饼图数据
+ for (let item of data) {
totalQuota += item.quota;
totalTimes += item.count;
- // 合并model_name
+
let pieItem = newPieData.find((it) => it.type === item.model_name);
if (pieItem) {
pieItem.value += item.count;
@@ -273,27 +283,47 @@ const Detail = (props) => {
value: item.count,
});
}
- // 合并created_at和model_name 为 lineData
- let createTime = timestamp2string1(
- item.created_at,
- dataExportDefaultTime,
- );
- let lineItem = newLineData.find(
- (it) => it.Time === createTime && it.Model === item.model_name,
- );
- if (lineItem) {
- lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
- } else {
- newLineData.push({
- Time: createTime,
- Model: item.model_name,
- Usage: parseFloat(getQuotaWithUnit(item.quota)),
- });
- }
}
- // sort by count
+ // 处理柱状图数据
+ let timePoints = Array.from(uniqueTimes);
+ if (timePoints.length < 7) {
+ // 根据时间粒度生成合适的时间点
+ const generateTimePoints = () => {
+ let lastTime = Math.max(...data.map(item => item.created_at));
+ let points = [];
+ let interval = dataExportDefaultTime === 'hour' ? 3600
+ : dataExportDefaultTime === 'day' ? 86400
+ : 604800;
+
+ for (let i = 0; i < 7; i++) {
+ points.push(timestamp2string1(lastTime - (i * interval), dataExportDefaultTime));
+ }
+ return points.reverse();
+ };
+
+ timePoints = generateTimePoints();
+ }
+
+ // 为每个时间点和模型生成数据
+ timePoints.forEach(time => {
+ Array.from(uniqueModels).forEach(model => {
+ let existingData = data.find(item =>
+ timestamp2string1(item.created_at, dataExportDefaultTime) === time &&
+ item.model_name === model
+ );
+
+ newLineData.push({
+ Time: time,
+ Model: model,
+ Usage: existingData ? parseFloat(getQuotaWithUnit(existingData.quota)) : 0
+ });
+ });
+ });
+
+ // 排序
newPieData.sort((a, b) => b.value - a.value);
+ newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
// 更新图表配置和数据
setSpecPie(prev => ({
@@ -324,9 +354,21 @@ const Detail = (props) => {
setLineData(newLineData);
setConsumeQuota(totalQuota);
setTimes(totalTimes);
+ setConsumeTokens(totalTokens);
+ };
+
+ const getUserData = async () => {
+ let res = await API.get(`/api/user/self`);
+ const {success, message, data} = res.data;
+ if (success) {
+ userDispatch({type: 'login', payload: data});
+ } else {
+ showError(message);
+ }
};
useEffect(() => {
+ getUserData()
if (!initialized.current) {
initVChartSemiTheme({
isWatchingThemeSwitch: true,
@@ -397,33 +439,97 @@ const Detail = (props) => {
/>
>
)}
+
-
>
-
-
-
-
-
-
+
+
+
+
+
+ {renderQuota(userState?.user?.quota)}
+
+
+ {renderQuota(userState?.user?.used_quota)}
+
+
+ {userState.user?.request_count}
+
+
+
+
+
+
+
+
+ {renderQuota(consumeQuota)}
+
+
+ {consumeTokens}
+
+
+ {times}
+
+
+
+
+
+
+
+
+ {renderNumber(
+ times /
+ ((Date.parse(end_timestamp) -
+ Date.parse(start_timestamp)) /
+ 60000),
+ ).toFixed(3)}
+
+
+ {renderNumber(
+ consumeTokens /
+ ((Date.parse(end_timestamp) -
+ Date.parse(start_timestamp)) /
+ 60000),
+ ).toFixed(3)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+