@@ -85,7 +85,7 @@ func SaveQuotaDataCache() {
|
|||||||
//quotaDataDB.Count += quotaData.Count
|
//quotaDataDB.Count += quotaData.Count
|
||||||
//quotaDataDB.Quota += quotaData.Quota
|
//quotaDataDB.Quota += quotaData.Quota
|
||||||
//DB.Table("quota_data").Save(quotaDataDB)
|
//DB.Table("quota_data").Save(quotaDataDB)
|
||||||
increaseQuotaData(quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.Count, quotaData.Quota, quotaData.CreatedAt)
|
increaseQuotaData(quotaData.UserID, quotaData.Username, quotaData.ModelName, quotaData.Count, quotaData.Quota, quotaData.CreatedAt, quotaData.TokenUsed)
|
||||||
} else {
|
} else {
|
||||||
DB.Table("quota_data").Create(quotaData)
|
DB.Table("quota_data").Create(quotaData)
|
||||||
}
|
}
|
||||||
@@ -94,11 +94,12 @@ func SaveQuotaDataCache() {
|
|||||||
common.SysLog(fmt.Sprintf("保存数据看板数据成功,共保存%d条数据", size))
|
common.SysLog(fmt.Sprintf("保存数据看板数据成功,共保存%d条数据", size))
|
||||||
}
|
}
|
||||||
|
|
||||||
func increaseQuotaData(userId int, username string, modelName string, count int, quota int, createdAt int64) {
|
func increaseQuotaData(userId int, username string, modelName string, count int, quota int, createdAt int64, tokenUsed int) {
|
||||||
err := DB.Table("quota_data").Where("user_id = ? and username = ? and model_name = ? and created_at = ?",
|
err := DB.Table("quota_data").Where("user_id = ? and username = ? and model_name = ? and created_at = ?",
|
||||||
userId, username, modelName, createdAt).Updates(map[string]interface{}{
|
userId, username, modelName, createdAt).Updates(map[string]interface{}{
|
||||||
"count": gorm.Expr("count + ?", count),
|
"count": gorm.Expr("count + ?", count),
|
||||||
"quota": gorm.Expr("quota + ?", quota),
|
"quota": gorm.Expr("quota + ?", quota),
|
||||||
|
"token_used": gorm.Expr("token_used + ?", tokenUsed),
|
||||||
}).Error
|
}).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysLog(fmt.Sprintf("increaseQuotaData error: %s", err))
|
common.SysLog(fmt.Sprintf("increaseQuotaData error: %s", err))
|
||||||
@@ -127,6 +128,6 @@ func GetAllQuotaDates(startTime int64, endTime int64, username string) (quotaDat
|
|||||||
// 从quota_data表中查询数据
|
// 从quota_data表中查询数据
|
||||||
// only select model_name, sum(count) as count, sum(quota) as quota, model_name, created_at from quota_data group by model_name, created_at;
|
// only select model_name, sum(count) as count, sum(quota) as quota, model_name, created_at from quota_data group by model_name, created_at;
|
||||||
//err = DB.Table("quota_data").Where("created_at >= ? and created_at <= ?", startTime, endTime).Find("aDatas).Error
|
//err = DB.Table("quota_data").Where("created_at >= ? and created_at <= ?", startTime, endTime).Find("aDatas).Error
|
||||||
err = DB.Table("quota_data").Select("model_name, sum(count) as count, sum(quota) as quota, created_at").Where("created_at >= ? and created_at <= ?", startTime, endTime).Group("model_name, created_at").Find("aDatas).Error
|
err = DB.Table("quota_data").Select("model_name, sum(count) as count, sum(quota) as quota, sum(token_used) as token_used, created_at").Where("created_at >= ? and created_at <= ?", startTime, endTime).Group("model_name, created_at").Find("aDatas).Error
|
||||||
return quotaDatas, err
|
return quotaDatas, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,12 +73,6 @@ const SiderBar = () => {
|
|||||||
to: '/playground',
|
to: '/playground',
|
||||||
icon: <IconCommentStroked />,
|
icon: <IconCommentStroked />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: '模型价格',
|
|
||||||
itemKey: 'pricing',
|
|
||||||
to: '/pricing',
|
|
||||||
icon: <IconPriceTag />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: '渠道',
|
text: '渠道',
|
||||||
itemKey: 'channel',
|
itemKey: 'channel',
|
||||||
@@ -102,6 +96,16 @@ const SiderBar = () => {
|
|||||||
to: '/token',
|
to: '/token',
|
||||||
icon: <IconKey />,
|
icon: <IconKey />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: '数据看板',
|
||||||
|
itemKey: 'detail',
|
||||||
|
to: '/detail',
|
||||||
|
icon: <IconCalendarClock />,
|
||||||
|
className:
|
||||||
|
localStorage.getItem('enable_data_export') === 'true'
|
||||||
|
? 'semi-navigation-item-normal'
|
||||||
|
: 'tableHiddle',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: '兑换码',
|
text: '兑换码',
|
||||||
itemKey: 'redemption',
|
itemKey: 'redemption',
|
||||||
@@ -128,16 +132,6 @@ const SiderBar = () => {
|
|||||||
to: '/log',
|
to: '/log',
|
||||||
icon: <IconHistogram />,
|
icon: <IconHistogram />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: '数据看板',
|
|
||||||
itemKey: 'detail',
|
|
||||||
to: '/detail',
|
|
||||||
icon: <IconCalendarClock />,
|
|
||||||
className:
|
|
||||||
localStorage.getItem('enable_data_export') === 'true'
|
|
||||||
? 'semi-navigation-item-normal'
|
|
||||||
: 'tableHiddle',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: '绘图',
|
text: '绘图',
|
||||||
itemKey: 'midjourney',
|
itemKey: 'midjourney',
|
||||||
|
|||||||
@@ -268,6 +268,44 @@ const colors = [
|
|||||||
'yellow',
|
'yellow',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 基础10色色板 (N ≤ 10)
|
||||||
|
const baseColors = [
|
||||||
|
'#1664FF', // 主色
|
||||||
|
'#1AC6FF',
|
||||||
|
'#FF8A00',
|
||||||
|
'#3CC780',
|
||||||
|
'#7442D4',
|
||||||
|
'#FFC400',
|
||||||
|
'#304D77',
|
||||||
|
'#B48DEB',
|
||||||
|
'#009488',
|
||||||
|
'#FF7DDA'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 扩展20色色板 (10 < N ≤ 20)
|
||||||
|
const extendedColors = [
|
||||||
|
'#1664FF',
|
||||||
|
'#B2CFFF',
|
||||||
|
'#1AC6FF',
|
||||||
|
'#94EFFF',
|
||||||
|
'#FF8A00',
|
||||||
|
'#FFCE7A',
|
||||||
|
'#3CC780',
|
||||||
|
'#B9EDCD',
|
||||||
|
'#7442D4',
|
||||||
|
'#DDC5FA',
|
||||||
|
'#FFC400',
|
||||||
|
'#FAE878',
|
||||||
|
'#304D77',
|
||||||
|
'#8B959E',
|
||||||
|
'#B48DEB',
|
||||||
|
'#EFE3FF',
|
||||||
|
'#009488',
|
||||||
|
'#59BAA8',
|
||||||
|
'#FF7DDA',
|
||||||
|
'#FFCFEE'
|
||||||
|
];
|
||||||
|
|
||||||
export const modelColorMap = {
|
export const modelColorMap = {
|
||||||
'dall-e': 'rgb(147,112,219)', // 深紫色
|
'dall-e': 'rgb(147,112,219)', // 深紫色
|
||||||
// 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
|
// 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
|
||||||
@@ -312,14 +350,33 @@ export const modelColorMap = {
|
|||||||
'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
|
'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function modelToColor(modelName) {
|
||||||
|
// 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
|
||||||
|
if (modelColorMap[modelName]) {
|
||||||
|
return modelColorMap[modelName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 生成一个稳定的数字作为索引
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < modelName.length; i++) {
|
||||||
|
hash = ((hash << 5) - hash) + modelName.charCodeAt(i);
|
||||||
|
hash = hash & hash; // Convert to 32-bit integer
|
||||||
|
}
|
||||||
|
hash = Math.abs(hash);
|
||||||
|
|
||||||
|
// 3. 根据模型名称长度选择不同的色板
|
||||||
|
const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
|
||||||
|
|
||||||
|
// 4. 使用hash值选择颜色
|
||||||
|
const index = hash % colorPalette.length;
|
||||||
|
return colorPalette[index];
|
||||||
|
}
|
||||||
|
|
||||||
export function stringToColor(str) {
|
export function stringToColor(str) {
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
// 对字符串中的每个字符进行操作
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
// 将字符的ASCII值加到sum中
|
|
||||||
sum += str.charCodeAt(i);
|
sum += str.charCodeAt(i);
|
||||||
}
|
}
|
||||||
// 使用模运算得到个位数
|
|
||||||
let i = sum % colors.length;
|
let i = sum % colors.length;
|
||||||
return colors[i];
|
return colors[i];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,21 @@ body {
|
|||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.semi-descriptions-double-small .semi-descriptions-item {
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-desc-card {
|
||||||
|
/*min-width: 320px;*/
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 767px) {
|
@media only screen and (max-width: 767px) {
|
||||||
|
#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
#root > section > header > section > div > div > div > div.semi-navigation-footer > div:nth-child(1) > a > li {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
.semi-table-tbody,
|
.semi-table-tbody,
|
||||||
.semi-table-row,
|
.semi-table-row,
|
||||||
.semi-table-row-cell {
|
.semi-table-row-cell {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||||
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
|
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/vchart';
|
import { VChart } from "@visactor/react-vchart";
|
||||||
import {
|
import {
|
||||||
API,
|
API,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
@@ -17,11 +17,16 @@ import {
|
|||||||
renderQuota,
|
renderQuota,
|
||||||
renderQuotaNumberWithDigit,
|
renderQuotaNumberWithDigit,
|
||||||
stringToColor,
|
stringToColor,
|
||||||
|
modelToColor,
|
||||||
} from '../../helpers/render';
|
} from '../../helpers/render';
|
||||||
|
import { UserContext } from '../../context/User/index.js';
|
||||||
|
import { StyleContext } from '../../context/Style/index.js';
|
||||||
|
|
||||||
const Detail = (props) => {
|
const Detail = (props) => {
|
||||||
const formRef = useRef();
|
const formRef = useRef();
|
||||||
let now = new Date();
|
let now = new Date();
|
||||||
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
|
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
username: '',
|
username: '',
|
||||||
token_name: '',
|
token_name: '',
|
||||||
@@ -40,32 +45,76 @@ const Detail = (props) => {
|
|||||||
inputs;
|
inputs;
|
||||||
const isAdminUser = isAdmin();
|
const isAdminUser = isAdmin();
|
||||||
const initialized = useRef(false);
|
const initialized = useRef(false);
|
||||||
const [modelDataChart, setModelDataChart] = useState(null);
|
|
||||||
const [modelDataPieChart, setModelDataPieChart] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [quotaData, setQuotaData] = useState([]);
|
const [quotaData, setQuotaData] = useState([]);
|
||||||
const [consumeQuota, setConsumeQuota] = useState(0);
|
const [consumeQuota, setConsumeQuota] = useState(0);
|
||||||
|
const [consumeTokens, setConsumeTokens] = useState(0);
|
||||||
const [times, setTimes] = useState(0);
|
const [times, setTimes] = useState(0);
|
||||||
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
|
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
|
||||||
localStorage.getItem('data_export_default_time') || 'hour',
|
localStorage.getItem('data_export_default_time') || 'hour',
|
||||||
);
|
);
|
||||||
|
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
|
||||||
const handleInputChange = (value, name) => {
|
const [lineData, setLineData] = useState([]);
|
||||||
if (name === 'data_export_default_time') {
|
const [spec_pie, setSpecPie] = useState({
|
||||||
setDataExportDefaultTime(value);
|
type: 'pie',
|
||||||
return;
|
data: [{
|
||||||
}
|
id: 'id0',
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
values: pieData
|
||||||
};
|
}],
|
||||||
|
outerRadius: 0.8,
|
||||||
const spec_line = {
|
innerRadius: 0.5,
|
||||||
type: 'bar',
|
padAngle: 0.6,
|
||||||
data: [
|
valueField: 'value',
|
||||||
|
categoryField: 'type',
|
||||||
|
pie: {
|
||||||
|
style: {
|
||||||
|
cornerRadius: 10,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
hover: {
|
||||||
|
outerRadius: 0.85,
|
||||||
|
stroke: '#000',
|
||||||
|
lineWidth: 1,
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
outerRadius: 0.85,
|
||||||
|
stroke: '#000',
|
||||||
|
lineWidth: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
visible: true,
|
||||||
|
text: '模型调用次数占比',
|
||||||
|
subtext: `总计:${renderNumber(times)}`,
|
||||||
|
},
|
||||||
|
legends: {
|
||||||
|
visible: true,
|
||||||
|
orient: 'left',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mark: {
|
||||||
|
content: [
|
||||||
{
|
{
|
||||||
id: 'barData',
|
key: (datum) => datum['type'],
|
||||||
values: [],
|
value: (datum) => renderNumber(datum['value']),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
specified: modelColorMap,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [spec_line, setSpecLine] = useState({
|
||||||
|
type: 'bar',
|
||||||
|
data: [{
|
||||||
|
id: 'barData',
|
||||||
|
values: lineData
|
||||||
|
}],
|
||||||
xField: 'Time',
|
xField: 'Time',
|
||||||
yField: 'Usage',
|
yField: 'Usage',
|
||||||
seriesField: 'Model',
|
seriesField: 'Model',
|
||||||
@@ -77,7 +126,7 @@ const Detail = (props) => {
|
|||||||
title: {
|
title: {
|
||||||
visible: true,
|
visible: true,
|
||||||
text: '模型消耗分布',
|
text: '模型消耗分布',
|
||||||
subtext: '0',
|
subtext: `总计:${renderQuota(consumeQuota, 2)}`,
|
||||||
},
|
},
|
||||||
bar: {
|
bar: {
|
||||||
// The state style of bar
|
// The state style of bar
|
||||||
@@ -129,67 +178,22 @@ const Detail = (props) => {
|
|||||||
color: {
|
color: {
|
||||||
specified: modelColorMap,
|
specified: modelColorMap,
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加一个新的状态来存储模型-颜色映射
|
||||||
|
const [modelColors, setModelColors] = useState({});
|
||||||
|
|
||||||
|
const handleInputChange = (value, name) => {
|
||||||
|
if (name === 'data_export_default_time') {
|
||||||
|
setDataExportDefaultTime(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const spec_pie = {
|
const loadQuotaData = async () => {
|
||||||
type: 'pie',
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
id: 'id0',
|
|
||||||
values: [{ type: 'null', value: '0' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
outerRadius: 0.8,
|
|
||||||
innerRadius: 0.5,
|
|
||||||
padAngle: 0.6,
|
|
||||||
valueField: 'value',
|
|
||||||
categoryField: 'type',
|
|
||||||
pie: {
|
|
||||||
style: {
|
|
||||||
cornerRadius: 10,
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
hover: {
|
|
||||||
outerRadius: 0.85,
|
|
||||||
stroke: '#000',
|
|
||||||
lineWidth: 1,
|
|
||||||
},
|
|
||||||
selected: {
|
|
||||||
outerRadius: 0.85,
|
|
||||||
stroke: '#000',
|
|
||||||
lineWidth: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
visible: true,
|
|
||||||
text: '模型调用次数占比',
|
|
||||||
},
|
|
||||||
legends: {
|
|
||||||
visible: true,
|
|
||||||
orient: 'left',
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
visible: true,
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
mark: {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
key: (datum) => datum['type'],
|
|
||||||
value: (datum) => renderNumber(datum['value']),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
specified: modelColorMap,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadQuotaData = async (lineChart, pieChart) => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
try {
|
||||||
let url = '';
|
let url = '';
|
||||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||||
@@ -223,102 +227,148 @@ const Detail = (props) => {
|
|||||||
item['created_at'] =
|
item['created_at'] =
|
||||||
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
|
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
|
||||||
});
|
});
|
||||||
updateChart(lineChart, pieChart, data);
|
updateChartData(data);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
await loadQuotaData(modelDataChart, modelDataPieChart);
|
await loadQuotaData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const initChart = async () => {
|
const initChart = async () => {
|
||||||
let lineChart = modelDataChart;
|
await loadQuotaData();
|
||||||
if (!modelDataChart) {
|
|
||||||
lineChart = new VChart(spec_line, { dom: 'model_data' });
|
|
||||||
setModelDataChart(lineChart);
|
|
||||||
lineChart.renderAsync();
|
|
||||||
}
|
|
||||||
let pieChart = modelDataPieChart;
|
|
||||||
if (!modelDataPieChart) {
|
|
||||||
pieChart = new VChart(spec_pie, { dom: 'model_pie' });
|
|
||||||
setModelDataPieChart(pieChart);
|
|
||||||
pieChart.renderAsync();
|
|
||||||
}
|
|
||||||
console.log('init vchart');
|
|
||||||
await loadQuotaData(lineChart, pieChart);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateChart = (lineChart, pieChart, data) => {
|
const updateChartData = (data) => {
|
||||||
if (isAdminUser) {
|
let newPieData = [];
|
||||||
// 将所有用户合并
|
let newLineData = [];
|
||||||
}
|
let totalQuota = 0;
|
||||||
let pieData = [];
|
let totalTimes = 0;
|
||||||
let lineData = [];
|
let uniqueModels = new Set();
|
||||||
let consumeQuota = 0;
|
let totalTokens = 0;
|
||||||
let times = 0;
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
// 收集所有唯一的模型名称和时间点
|
||||||
const item = data[i];
|
let uniqueTimes = new Set();
|
||||||
consumeQuota += item.quota;
|
data.forEach(item => {
|
||||||
times += item.count;
|
uniqueModels.add(item.model_name);
|
||||||
// 合并model_name
|
uniqueTimes.add(timestamp2string1(item.created_at, dataExportDefaultTime));
|
||||||
let pieItem = pieData.find((it) => it.type === item.model_name);
|
totalTokens += item.token_used;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理颜色映射
|
||||||
|
const newModelColors = {};
|
||||||
|
Array.from(uniqueModels).forEach((modelName) => {
|
||||||
|
newModelColors[modelName] = modelColorMap[modelName] ||
|
||||||
|
modelColors[modelName] ||
|
||||||
|
modelToColor(modelName);
|
||||||
|
});
|
||||||
|
setModelColors(newModelColors);
|
||||||
|
|
||||||
|
// 处理饼图数据
|
||||||
|
for (let item of data) {
|
||||||
|
totalQuota += item.quota;
|
||||||
|
totalTimes += item.count;
|
||||||
|
|
||||||
|
let pieItem = newPieData.find((it) => it.type === item.model_name);
|
||||||
if (pieItem) {
|
if (pieItem) {
|
||||||
pieItem.value += item.count;
|
pieItem.value += item.count;
|
||||||
} else {
|
} else {
|
||||||
pieData.push({
|
newPieData.push({
|
||||||
type: item.model_name,
|
type: item.model_name,
|
||||||
value: item.count,
|
value: item.count,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
|
}
|
||||||
// 转换日期格式
|
|
||||||
let createTime = timestamp2string1(
|
// 处理柱状图数据
|
||||||
item.created_at,
|
let timePoints = Array.from(uniqueTimes);
|
||||||
dataExportDefaultTime,
|
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
|
||||||
);
|
);
|
||||||
let lineItem = lineData.find(
|
|
||||||
(it) => it.Time === createTime && it.Model === item.model_name,
|
newLineData.push({
|
||||||
);
|
Time: time,
|
||||||
if (lineItem) {
|
Model: model,
|
||||||
lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
|
Usage: existingData ? parseFloat(getQuotaWithUnit(existingData.quota)) : 0
|
||||||
} else {
|
|
||||||
lineData.push({
|
|
||||||
Time: createTime,
|
|
||||||
Model: item.model_name,
|
|
||||||
Usage: parseFloat(getQuotaWithUnit(item.quota)),
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
newPieData.sort((a, b) => b.value - a.value);
|
||||||
|
newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
|
||||||
|
|
||||||
|
// 更新图表配置和数据
|
||||||
|
setSpecPie(prev => ({
|
||||||
|
...prev,
|
||||||
|
data: [{ id: 'id0', values: newPieData }],
|
||||||
|
title: {
|
||||||
|
...prev.title,
|
||||||
|
subtext: `总计:${renderNumber(totalTimes)}`
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
specified: newModelColors
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
setSpecLine(prev => ({
|
||||||
|
...prev,
|
||||||
|
data: [{ id: 'barData', values: newLineData }],
|
||||||
|
title: {
|
||||||
|
...prev.title,
|
||||||
|
subtext: `总计:${renderQuota(totalQuota, 2)}`
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
specified: newModelColors
|
||||||
}
|
}
|
||||||
setConsumeQuota(consumeQuota);
|
}));
|
||||||
setTimes(times);
|
|
||||||
|
|
||||||
// sort by count
|
setPieData(newPieData);
|
||||||
pieData.sort((a, b) => b.value - a.value);
|
setLineData(newLineData);
|
||||||
spec_pie.title.subtext = `总计:${renderNumber(times)}`;
|
setConsumeQuota(totalQuota);
|
||||||
spec_pie.data[0].values = pieData;
|
setTimes(totalTimes);
|
||||||
|
setConsumeTokens(totalTokens);
|
||||||
|
};
|
||||||
|
|
||||||
spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`;
|
const getUserData = async () => {
|
||||||
spec_line.data[0].values = lineData;
|
let res = await API.get(`/api/user/self`);
|
||||||
pieChart.updateSpec(spec_pie);
|
const {success, message, data} = res.data;
|
||||||
lineChart.updateSpec(spec_line);
|
if (success) {
|
||||||
|
userDispatch({type: 'login', payload: data});
|
||||||
// pieChart.updateData('id0', pieData);
|
} else {
|
||||||
// lineChart.updateData('barData', lineData);
|
showError(message);
|
||||||
pieChart.reLayout();
|
}
|
||||||
lineChart.reLayout();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
|
getUserData()
|
||||||
// if (dataExportDefaultTime === 'day') {
|
|
||||||
// // 设置开始时间为7天前
|
|
||||||
// let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
|
|
||||||
// inputs.start_timestamp = st;
|
|
||||||
// formRef.current.formApi.setValue('start_timestamp', st);
|
|
||||||
// }
|
|
||||||
if (!initialized.current) {
|
if (!initialized.current) {
|
||||||
initVChartSemiTheme({
|
initVChartSemiTheme({
|
||||||
isWatchingThemeSwitch: true,
|
isWatchingThemeSwitch: true,
|
||||||
@@ -389,7 +439,6 @@ const Detail = (props) => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Form.Section>
|
|
||||||
<Button
|
<Button
|
||||||
label='查询'
|
label='查询'
|
||||||
type='primary'
|
type='primary'
|
||||||
@@ -397,25 +446,90 @@ const Detail = (props) => {
|
|||||||
className='btn-margin-right'
|
className='btn-margin-right'
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
style={{ marginTop: 24 }}
|
||||||
>
|
>
|
||||||
查询
|
查询
|
||||||
</Button>
|
</Button>
|
||||||
|
<Form.Section>
|
||||||
</Form.Section>
|
</Form.Section>
|
||||||
</>
|
</>
|
||||||
</Form>
|
</Form>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
|
<Row gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }} style={{marginTop: 20}} type="flex" justify="space-between">
|
||||||
|
<Col span={styleState.isMobile?24:8}>
|
||||||
|
<Card className='panel-desc-card'>
|
||||||
|
<Descriptions row size="small">
|
||||||
|
<Descriptions.Item itemKey='当前余额'>
|
||||||
|
{renderQuota(userState?.user?.quota)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item itemKey='历史消耗'>
|
||||||
|
{renderQuota(userState?.user?.used_quota)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item itemKey='请求次数'>
|
||||||
|
{userState.user?.request_count}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={styleState.isMobile?24:8}>
|
||||||
|
<Card>
|
||||||
|
<Descriptions row size="small">
|
||||||
|
<Descriptions.Item itemKey='统计额度'>
|
||||||
|
{renderQuota(consumeQuota)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item itemKey='统计Tokens'>
|
||||||
|
{consumeTokens}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item itemKey='统计次数'>
|
||||||
|
{times}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={styleState.isMobile ? 24 : 8}>
|
||||||
|
<Card>
|
||||||
|
<Descriptions row size='small'>
|
||||||
|
<Descriptions.Item itemKey='平均RPM'>
|
||||||
|
{renderNumber(
|
||||||
|
times /
|
||||||
|
((Date.parse(end_timestamp) -
|
||||||
|
Date.parse(start_timestamp)) /
|
||||||
|
60000),
|
||||||
|
).toFixed(3)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item itemKey='平均TPM'>
|
||||||
|
{renderNumber(
|
||||||
|
consumeTokens /
|
||||||
|
((Date.parse(end_timestamp) -
|
||||||
|
Date.parse(start_timestamp)) /
|
||||||
|
60000),
|
||||||
|
).toFixed(3)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Card style={{marginTop: 20}}>
|
||||||
|
<Tabs type="line" defaultActiveKey="1">
|
||||||
|
<Tabs.TabPane tab="消耗分布" itemKey="1">
|
||||||
<div style={{ height: 500 }}>
|
<div style={{ height: 500 }}>
|
||||||
<div
|
<VChart
|
||||||
id='model_pie'
|
spec={spec_line}
|
||||||
style={{ width: '100%', minWidth: 100 }}
|
option={{ mode: "desktop-browser" }}
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="调用次数分布" itemKey="2">
|
||||||
<div style={{ height: 500 }}>
|
<div style={{ height: 500 }}>
|
||||||
<div
|
<VChart
|
||||||
id='model_data'
|
spec={spec_pie}
|
||||||
style={{ width: '100%', minWidth: 100 }}
|
option={{ mode: "desktop-browser" }}
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
</Spin>
|
</Spin>
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
Reference in New Issue
Block a user