2024-12-12 16:11:17 +08:00
|
|
|
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
2024-03-23 20:22:00 +08:00
|
|
|
|
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
|
2025-05-26 23:30:26 +08:00
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
2024-03-23 20:22:00 +08:00
|
|
|
|
|
2025-04-04 12:00:38 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Form,
|
|
|
|
|
|
Spin,
|
2025-05-20 18:01:38 +08:00
|
|
|
|
Typography,
|
|
|
|
|
|
IconButton,
|
|
|
|
|
|
Modal,
|
2025-05-25 13:30:47 +08:00
|
|
|
|
Avatar,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
} from '@douyinfe/semi-ui';
|
2025-05-25 13:30:47 +08:00
|
|
|
|
import {
|
|
|
|
|
|
IconRefresh,
|
|
|
|
|
|
IconSearch,
|
|
|
|
|
|
IconMoneyExchangeStroked,
|
|
|
|
|
|
IconHistogram,
|
|
|
|
|
|
IconRotate,
|
|
|
|
|
|
IconCoinMoneyStroked,
|
|
|
|
|
|
IconTextStroked,
|
|
|
|
|
|
IconPulse,
|
|
|
|
|
|
IconStopwatchStroked,
|
|
|
|
|
|
IconTypograph,
|
|
|
|
|
|
} from '@douyinfe/semi-icons';
|
2025-04-04 12:00:38 +08:00
|
|
|
|
import { VChart } from '@visactor/react-vchart';
|
2024-01-10 17:49:55 +08:00
|
|
|
|
import {
|
2024-03-23 21:24:39 +08:00
|
|
|
|
API,
|
|
|
|
|
|
isAdmin,
|
2025-05-26 15:13:55 +08:00
|
|
|
|
isMobile,
|
2024-03-23 21:24:39 +08:00
|
|
|
|
showError,
|
|
|
|
|
|
timestamp2string,
|
|
|
|
|
|
timestamp2string1,
|
|
|
|
|
|
} from '../../helpers';
|
|
|
|
|
|
import {
|
|
|
|
|
|
getQuotaWithUnit,
|
|
|
|
|
|
modelColorMap,
|
|
|
|
|
|
renderNumber,
|
|
|
|
|
|
renderQuota,
|
2024-12-12 14:56:16 +08:00
|
|
|
|
modelToColor,
|
2024-03-23 21:24:39 +08:00
|
|
|
|
} from '../../helpers/render';
|
2024-12-12 16:11:17 +08:00
|
|
|
|
import { UserContext } from '../../context/User/index.js';
|
|
|
|
|
|
import { StyleContext } from '../../context/Style/index.js';
|
2024-12-13 19:03:14 +08:00
|
|
|
|
import { useTranslation } from 'react-i18next';
|
2024-01-07 18:31:14 +08:00
|
|
|
|
|
|
|
|
|
|
const Detail = (props) => {
|
2024-12-13 19:03:14 +08:00
|
|
|
|
const { t } = useTranslation();
|
2025-05-26 23:30:26 +08:00
|
|
|
|
const navigate = useNavigate();
|
2025-05-20 18:01:38 +08:00
|
|
|
|
const { Text } = Typography;
|
2024-03-23 21:24:39 +08:00
|
|
|
|
const formRef = useRef();
|
|
|
|
|
|
let now = new Date();
|
2024-12-12 16:11:17 +08:00
|
|
|
|
const [userState, userDispatch] = useContext(UserContext);
|
|
|
|
|
|
const [styleState, styleDispatch] = useContext(StyleContext);
|
2024-03-23 21:24:39 +08:00
|
|
|
|
const [inputs, setInputs] = useState({
|
|
|
|
|
|
username: '',
|
|
|
|
|
|
token_name: '',
|
|
|
|
|
|
model_name: '',
|
|
|
|
|
|
start_timestamp:
|
|
|
|
|
|
localStorage.getItem('data_export_default_time') === 'hour'
|
|
|
|
|
|
? timestamp2string(now.getTime() / 1000 - 86400)
|
|
|
|
|
|
: localStorage.getItem('data_export_default_time') === 'week'
|
|
|
|
|
|
? timestamp2string(now.getTime() / 1000 - 86400 * 30)
|
|
|
|
|
|
: timestamp2string(now.getTime() / 1000 - 86400 * 7),
|
|
|
|
|
|
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
|
|
|
|
|
|
channel: '',
|
|
|
|
|
|
data_export_default_time: '',
|
|
|
|
|
|
});
|
|
|
|
|
|
const { username, model_name, start_timestamp, end_timestamp, channel } =
|
|
|
|
|
|
inputs;
|
|
|
|
|
|
const isAdminUser = isAdmin();
|
|
|
|
|
|
const initialized = useRef(false);
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [quotaData, setQuotaData] = useState([]);
|
|
|
|
|
|
const [consumeQuota, setConsumeQuota] = useState(0);
|
2024-12-12 16:11:17 +08:00
|
|
|
|
const [consumeTokens, setConsumeTokens] = useState(0);
|
2024-03-23 21:24:39 +08:00
|
|
|
|
const [times, setTimes] = useState(0);
|
|
|
|
|
|
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
|
|
|
|
|
|
localStorage.getItem('data_export_default_time') || 'hour',
|
|
|
|
|
|
);
|
2024-12-12 14:56:16 +08:00
|
|
|
|
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
|
|
|
|
|
|
const [lineData, setLineData] = useState([]);
|
2025-05-20 18:01:38 +08:00
|
|
|
|
const [searchModalVisible, setSearchModalVisible] = useState(false);
|
|
|
|
|
|
|
2024-12-12 14:56:16 +08:00
|
|
|
|
const [spec_pie, setSpecPie] = useState({
|
|
|
|
|
|
type: 'pie',
|
2025-04-04 12:00:38 +08:00
|
|
|
|
data: [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'id0',
|
|
|
|
|
|
values: pieData,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2024-12-12 14:56:16 +08:00
|
|
|
|
outerRadius: 0.8,
|
|
|
|
|
|
innerRadius: 0.5,
|
|
|
|
|
|
padAngle: 0.6,
|
|
|
|
|
|
valueField: 'value',
|
|
|
|
|
|
categoryField: 'type',
|
|
|
|
|
|
pie: {
|
|
|
|
|
|
style: {
|
|
|
|
|
|
cornerRadius: 10,
|
2024-03-23 21:24:39 +08:00
|
|
|
|
},
|
2024-12-12 14:56:16 +08:00
|
|
|
|
state: {
|
|
|
|
|
|
hover: {
|
|
|
|
|
|
outerRadius: 0.85,
|
|
|
|
|
|
stroke: '#000',
|
|
|
|
|
|
lineWidth: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
selected: {
|
|
|
|
|
|
outerRadius: 0.85,
|
|
|
|
|
|
stroke: '#000',
|
|
|
|
|
|
lineWidth: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
title: {
|
|
|
|
|
|
visible: true,
|
2024-12-13 19:03:14 +08:00
|
|
|
|
text: t('模型调用次数占比'),
|
|
|
|
|
|
subtext: `${t('总计')}:${renderNumber(times)}`,
|
2024-12-12 14:56:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
legends: {
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
orient: 'left',
|
|
|
|
|
|
},
|
|
|
|
|
|
label: {
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
mark: {
|
|
|
|
|
|
content: [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: (datum) => datum['type'],
|
|
|
|
|
|
value: (datum) => renderNumber(datum['value']),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
color: {
|
|
|
|
|
|
specified: modelColorMap,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
const [spec_line, setSpecLine] = useState({
|
|
|
|
|
|
type: 'bar',
|
2025-04-04 12:00:38 +08:00
|
|
|
|
data: [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'barData',
|
|
|
|
|
|
values: lineData,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
2024-03-23 21:24:39 +08:00
|
|
|
|
xField: 'Time',
|
|
|
|
|
|
yField: 'Usage',
|
|
|
|
|
|
seriesField: 'Model',
|
|
|
|
|
|
stack: true,
|
|
|
|
|
|
legends: {
|
|
|
|
|
|
visible: true,
|
2024-04-18 19:37:52 +08:00
|
|
|
|
selectMode: 'single',
|
2024-03-23 21:24:39 +08:00
|
|
|
|
},
|
|
|
|
|
|
title: {
|
|
|
|
|
|
visible: true,
|
2024-12-13 19:03:14 +08:00
|
|
|
|
text: t('模型消耗分布'),
|
|
|
|
|
|
subtext: `${t('总计')}:${renderQuota(consumeQuota, 2)}`,
|
2024-03-23 21:24:39 +08:00
|
|
|
|
},
|
|
|
|
|
|
bar: {
|
|
|
|
|
|
state: {
|
|
|
|
|
|
hover: {
|
|
|
|
|
|
stroke: '#000',
|
|
|
|
|
|
lineWidth: 1,
|
2024-01-07 18:31:14 +08:00
|
|
|
|
},
|
2024-03-23 21:24:39 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
mark: {
|
|
|
|
|
|
content: [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: (datum) => datum['Model'],
|
2024-12-26 14:25:44 +08:00
|
|
|
|
value: (datum) => renderQuota(datum['rawQuota'] || 0, 4),
|
2024-03-23 21:24:39 +08:00
|
|
|
|
},
|
2024-01-07 18:31:14 +08:00
|
|
|
|
],
|
2024-03-23 21:24:39 +08:00
|
|
|
|
},
|
|
|
|
|
|
dimension: {
|
|
|
|
|
|
content: [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: (datum) => datum['Model'],
|
2024-12-25 23:16:35 +08:00
|
|
|
|
value: (datum) => datum['rawQuota'] || 0,
|
2024-03-23 21:24:39 +08:00
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
updateContent: (array) => {
|
|
|
|
|
|
array.sort((a, b) => b.value - a.value);
|
|
|
|
|
|
let sum = 0;
|
|
|
|
|
|
for (let i = 0; i < array.length; i++) {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
if (array[i].key == '其他') {
|
2024-12-25 23:16:35 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
let value = parseFloat(array[i].value);
|
|
|
|
|
|
if (isNaN(value)) {
|
|
|
|
|
|
value = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (array[i].datum && array[i].datum.TimeSum) {
|
|
|
|
|
|
sum = array[i].datum.TimeSum;
|
|
|
|
|
|
}
|
|
|
|
|
|
array[i].value = renderQuota(value, 4);
|
2024-03-23 21:24:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
array.unshift({
|
2024-12-13 19:03:14 +08:00
|
|
|
|
key: t('总计'),
|
2024-12-25 23:16:35 +08:00
|
|
|
|
value: renderQuota(sum, 4),
|
2024-03-23 21:24:39 +08:00
|
|
|
|
});
|
|
|
|
|
|
return array;
|
2024-01-07 18:31:14 +08:00
|
|
|
|
},
|
2024-03-23 21:24:39 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
color: {
|
|
|
|
|
|
specified: modelColorMap,
|
|
|
|
|
|
},
|
2024-12-12 14:56:16 +08:00
|
|
|
|
});
|
2024-03-23 21:24:39 +08:00
|
|
|
|
|
2024-12-12 14:56:16 +08:00
|
|
|
|
// 添加一个新的状态来存储模型-颜色映射
|
|
|
|
|
|
const [modelColors, setModelColors] = useState({});
|
|
|
|
|
|
|
2025-05-20 18:01:38 +08:00
|
|
|
|
// 显示搜索Modal
|
|
|
|
|
|
const showSearchModal = () => {
|
|
|
|
|
|
setSearchModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭搜索Modal
|
|
|
|
|
|
const handleCloseModal = () => {
|
|
|
|
|
|
setSearchModalVisible(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 搜索Modal确认按钮
|
|
|
|
|
|
const handleSearchConfirm = () => {
|
|
|
|
|
|
refresh();
|
|
|
|
|
|
setSearchModalVisible(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-12-12 14:56:16 +08:00
|
|
|
|
const handleInputChange = (value, name) => {
|
|
|
|
|
|
if (name === 'data_export_default_time') {
|
|
|
|
|
|
setDataExportDefaultTime(value);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
2024-03-23 21:24:39 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2024-12-12 14:56:16 +08:00
|
|
|
|
const loadQuotaData = async () => {
|
2024-03-23 21:24:39 +08:00
|
|
|
|
setLoading(true);
|
2024-12-12 14:56:16 +08:00
|
|
|
|
try {
|
|
|
|
|
|
let url = '';
|
|
|
|
|
|
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
|
|
|
|
|
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
|
|
|
|
|
if (isAdminUser) {
|
|
|
|
|
|
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
|
2024-03-23 21:24:39 +08:00
|
|
|
|
}
|
2024-12-12 14:56:16 +08:00
|
|
|
|
const res = await API.get(url);
|
|
|
|
|
|
const { success, message, data } = res.data;
|
|
|
|
|
|
if (success) {
|
|
|
|
|
|
setQuotaData(data);
|
|
|
|
|
|
if (data.length === 0) {
|
|
|
|
|
|
data.push({
|
|
|
|
|
|
count: 0,
|
|
|
|
|
|
model_name: '无数据',
|
|
|
|
|
|
quota: 0,
|
|
|
|
|
|
created_at: now.getTime() / 1000,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
// sort created_at
|
|
|
|
|
|
data.sort((a, b) => a.created_at - b.created_at);
|
|
|
|
|
|
updateChartData(data);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showError(message);
|
2024-03-23 21:24:39 +08:00
|
|
|
|
}
|
2024-12-12 14:56:16 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
2024-03-23 21:24:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
2024-01-07 18:31:14 +08:00
|
|
|
|
|
2024-03-23 21:24:39 +08:00
|
|
|
|
const refresh = async () => {
|
2024-12-12 14:56:16 +08:00
|
|
|
|
await loadQuotaData();
|
2024-03-23 21:24:39 +08:00
|
|
|
|
};
|
2024-01-07 18:31:14 +08:00
|
|
|
|
|
2024-03-23 21:24:39 +08:00
|
|
|
|
const initChart = async () => {
|
2024-12-12 14:56:16 +08:00
|
|
|
|
await loadQuotaData();
|
2024-03-23 21:24:39 +08:00
|
|
|
|
};
|
2024-01-07 18:31:14 +08:00
|
|
|
|
|
2024-12-12 14:56:16 +08:00
|
|
|
|
const updateChartData = (data) => {
|
|
|
|
|
|
let newPieData = [];
|
|
|
|
|
|
let newLineData = [];
|
|
|
|
|
|
let totalQuota = 0;
|
|
|
|
|
|
let totalTimes = 0;
|
|
|
|
|
|
let uniqueModels = new Set();
|
2024-12-12 16:11:17 +08:00
|
|
|
|
let totalTokens = 0;
|
2024-12-12 14:56:16 +08:00
|
|
|
|
|
2024-12-25 23:16:35 +08:00
|
|
|
|
// 收集所有唯一的模型名称
|
2025-04-04 12:00:38 +08:00
|
|
|
|
data.forEach((item) => {
|
2024-12-12 16:11:17 +08:00
|
|
|
|
uniqueModels.add(item.model_name);
|
|
|
|
|
|
totalTokens += item.token_used;
|
2024-12-25 23:16:35 +08:00
|
|
|
|
totalQuota += item.quota;
|
|
|
|
|
|
totalTimes += item.count;
|
2024-12-12 16:11:17 +08:00
|
|
|
|
});
|
2024-12-25 23:16:35 +08:00
|
|
|
|
|
2024-12-12 16:11:17 +08:00
|
|
|
|
// 处理颜色映射
|
2024-12-12 14:56:16 +08:00
|
|
|
|
const newModelColors = {};
|
|
|
|
|
|
Array.from(uniqueModels).forEach((modelName) => {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
newModelColors[modelName] =
|
|
|
|
|
|
modelColorMap[modelName] ||
|
|
|
|
|
|
modelColors[modelName] ||
|
2024-12-12 16:11:17 +08:00
|
|
|
|
modelToColor(modelName);
|
2024-12-12 14:56:16 +08:00
|
|
|
|
});
|
|
|
|
|
|
setModelColors(newModelColors);
|
|
|
|
|
|
|
2024-12-25 23:16:35 +08:00
|
|
|
|
// 按时间和模型聚合数据
|
|
|
|
|
|
let aggregatedData = new Map();
|
2025-04-04 12:00:38 +08:00
|
|
|
|
data.forEach((item) => {
|
2024-12-25 23:16:35 +08:00
|
|
|
|
const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime);
|
|
|
|
|
|
const modelKey = item.model_name;
|
|
|
|
|
|
const key = `${timeKey}-${modelKey}`;
|
|
|
|
|
|
|
|
|
|
|
|
if (!aggregatedData.has(key)) {
|
|
|
|
|
|
aggregatedData.set(key, {
|
|
|
|
|
|
time: timeKey,
|
|
|
|
|
|
model: modelKey,
|
|
|
|
|
|
quota: 0,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
count: 0,
|
2024-03-23 21:24:39 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2024-12-25 23:16:35 +08:00
|
|
|
|
const existing = aggregatedData.get(key);
|
|
|
|
|
|
existing.quota += item.quota;
|
|
|
|
|
|
existing.count += item.count;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 处理饼图数据
|
|
|
|
|
|
let modelTotals = new Map();
|
|
|
|
|
|
for (let [_, value] of aggregatedData) {
|
|
|
|
|
|
if (!modelTotals.has(value.model)) {
|
|
|
|
|
|
modelTotals.set(value.model, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
modelTotals.set(value.model, modelTotals.get(value.model) + value.count);
|
2024-12-12 16:11:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-25 23:16:35 +08:00
|
|
|
|
newPieData = Array.from(modelTotals).map(([model, count]) => ({
|
|
|
|
|
|
type: model,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
value: count,
|
2024-12-25 23:16:35 +08:00
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 生成时间点序列
|
2025-04-04 12:00:38 +08:00
|
|
|
|
let timePoints = Array.from(
|
|
|
|
|
|
new Set([...aggregatedData.values()].map((d) => d.time)),
|
|
|
|
|
|
);
|
2024-12-12 16:11:17 +08:00
|
|
|
|
if (timePoints.length < 7) {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
const lastTime = Math.max(...data.map((item) => item.created_at));
|
|
|
|
|
|
const interval =
|
|
|
|
|
|
dataExportDefaultTime === 'hour'
|
|
|
|
|
|
? 3600
|
|
|
|
|
|
: dataExportDefaultTime === 'day'
|
|
|
|
|
|
? 86400
|
|
|
|
|
|
: 604800;
|
|
|
|
|
|
|
|
|
|
|
|
timePoints = Array.from({ length: 7 }, (_, i) =>
|
|
|
|
|
|
timestamp2string1(lastTime - (6 - i) * interval, dataExportDefaultTime),
|
2024-12-25 23:16:35 +08:00
|
|
|
|
);
|
2024-12-12 16:11:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-25 23:16:35 +08:00
|
|
|
|
// 生成柱状图数据
|
2025-04-04 12:00:38 +08:00
|
|
|
|
timePoints.forEach((time) => {
|
2024-12-25 23:16:35 +08:00
|
|
|
|
// 为每个时间点收集所有模型的数据
|
2025-04-04 12:00:38 +08:00
|
|
|
|
let timeData = Array.from(uniqueModels).map((model) => {
|
2024-12-25 23:16:35 +08:00
|
|
|
|
const key = `${time}-${model}`;
|
|
|
|
|
|
const aggregated = aggregatedData.get(key);
|
|
|
|
|
|
return {
|
2024-12-12 16:11:17 +08:00
|
|
|
|
Time: time,
|
|
|
|
|
|
Model: model,
|
2024-12-25 23:16:35 +08:00
|
|
|
|
rawQuota: aggregated?.quota || 0,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0,
|
2024-12-25 23:16:35 +08:00
|
|
|
|
};
|
2024-12-12 16:11:17 +08:00
|
|
|
|
});
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2024-12-25 23:16:35 +08:00
|
|
|
|
// 计算该时间点的总计
|
|
|
|
|
|
const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0);
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2024-12-25 23:16:35 +08:00
|
|
|
|
// 按照 rawQuota 从大到小排序
|
|
|
|
|
|
timeData.sort((a, b) => b.rawQuota - a.rawQuota);
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2024-12-25 23:16:35 +08:00
|
|
|
|
// 为每个数据点添加该时间的总计
|
2025-04-04 12:00:38 +08:00
|
|
|
|
timeData = timeData.map((item) => ({
|
2024-12-25 23:16:35 +08:00
|
|
|
|
...item,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
TimeSum: timeSum,
|
2024-12-25 23:16:35 +08:00
|
|
|
|
}));
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2024-12-25 23:16:35 +08:00
|
|
|
|
// 将排序后的数据添加到 newLineData
|
|
|
|
|
|
newLineData.push(...timeData);
|
2024-12-12 16:11:17 +08:00
|
|
|
|
});
|
2024-01-07 18:31:14 +08:00
|
|
|
|
|
2024-12-12 16:11:17 +08:00
|
|
|
|
// 排序
|
2024-12-12 14:56:16 +08:00
|
|
|
|
newPieData.sort((a, b) => b.value - a.value);
|
2024-12-12 16:11:17 +08:00
|
|
|
|
newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
|
2024-01-10 17:49:55 +08:00
|
|
|
|
|
2024-12-12 14:56:16 +08:00
|
|
|
|
// 更新图表配置和数据
|
2025-04-04 12:00:38 +08:00
|
|
|
|
setSpecPie((prev) => ({
|
2024-12-12 14:56:16 +08:00
|
|
|
|
...prev,
|
|
|
|
|
|
data: [{ id: 'id0', values: newPieData }],
|
|
|
|
|
|
title: {
|
|
|
|
|
|
...prev.title,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
subtext: `${t('总计')}:${renderNumber(totalTimes)}`,
|
2024-12-12 14:56:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
color: {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
specified: newModelColors,
|
|
|
|
|
|
},
|
2024-12-12 14:56:16 +08:00
|
|
|
|
}));
|
2024-01-10 17:49:55 +08:00
|
|
|
|
|
2025-04-04 12:00:38 +08:00
|
|
|
|
setSpecLine((prev) => ({
|
2024-12-12 14:56:16 +08:00
|
|
|
|
...prev,
|
|
|
|
|
|
data: [{ id: 'barData', values: newLineData }],
|
|
|
|
|
|
title: {
|
|
|
|
|
|
...prev.title,
|
2025-04-04 12:00:38 +08:00
|
|
|
|
subtext: `${t('总计')}:${renderQuota(totalQuota, 2)}`,
|
2024-12-12 14:56:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
color: {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
specified: newModelColors,
|
|
|
|
|
|
},
|
2024-12-12 14:56:16 +08:00
|
|
|
|
}));
|
2025-04-04 12:00:38 +08:00
|
|
|
|
|
2024-12-12 14:56:16 +08:00
|
|
|
|
setPieData(newPieData);
|
|
|
|
|
|
setLineData(newLineData);
|
|
|
|
|
|
setConsumeQuota(totalQuota);
|
|
|
|
|
|
setTimes(totalTimes);
|
2024-12-12 16:11:17 +08:00
|
|
|
|
setConsumeTokens(totalTokens);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getUserData = async () => {
|
|
|
|
|
|
let res = await API.get(`/api/user/self`);
|
2025-04-04 12:00:38 +08:00
|
|
|
|
const { success, message, data } = res.data;
|
2024-12-12 16:11:17 +08:00
|
|
|
|
if (success) {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
userDispatch({ type: 'login', payload: data });
|
2024-12-12 16:11:17 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
showError(message);
|
|
|
|
|
|
}
|
2024-03-23 21:24:39 +08:00
|
|
|
|
};
|
2024-01-10 17:49:55 +08:00
|
|
|
|
|
2024-03-23 21:24:39 +08:00
|
|
|
|
useEffect(() => {
|
2025-04-04 12:00:38 +08:00
|
|
|
|
getUserData();
|
2024-03-23 21:24:39 +08:00
|
|
|
|
if (!initialized.current) {
|
|
|
|
|
|
initVChartSemiTheme({
|
|
|
|
|
|
isWatchingThemeSwitch: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
initialized.current = true;
|
|
|
|
|
|
initChart();
|
2024-01-07 19:47:35 +08:00
|
|
|
|
}
|
2024-03-23 21:24:39 +08:00
|
|
|
|
}, []);
|
2024-01-07 19:47:35 +08:00
|
|
|
|
|
2025-05-20 18:01:38 +08:00
|
|
|
|
// 数据卡片信息
|
|
|
|
|
|
const statsData = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('当前余额'),
|
|
|
|
|
|
value: renderQuota(userState?.user?.quota),
|
2025-05-26 22:30:04 +08:00
|
|
|
|
icon: <IconMoneyExchangeStroked size="large" />,
|
2025-05-20 18:01:38 +08:00
|
|
|
|
color: 'bg-blue-50',
|
2025-05-25 13:30:47 +08:00
|
|
|
|
avatarColor: 'blue',
|
2025-05-26 23:30:26 +08:00
|
|
|
|
onClick: () => navigate('/console/topup'),
|
2025-05-20 18:01:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('历史消耗'),
|
|
|
|
|
|
value: renderQuota(userState?.user?.used_quota),
|
2025-05-26 22:30:04 +08:00
|
|
|
|
icon: <IconHistogram size="large" />,
|
2025-05-20 18:01:38 +08:00
|
|
|
|
color: 'bg-purple-50',
|
2025-05-25 13:30:47 +08:00
|
|
|
|
avatarColor: 'purple',
|
2025-05-20 18:01:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('请求次数'),
|
|
|
|
|
|
value: userState.user?.request_count,
|
2025-05-26 22:30:04 +08:00
|
|
|
|
icon: <IconRotate size="large" />,
|
2025-05-20 18:01:38 +08:00
|
|
|
|
color: 'bg-green-50',
|
2025-05-25 13:30:47 +08:00
|
|
|
|
avatarColor: 'green',
|
2025-05-20 18:01:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('统计额度'),
|
|
|
|
|
|
value: renderQuota(consumeQuota),
|
2025-05-26 22:30:04 +08:00
|
|
|
|
icon: <IconCoinMoneyStroked size="large" />,
|
2025-05-20 18:01:38 +08:00
|
|
|
|
color: 'bg-yellow-50',
|
2025-05-25 13:30:47 +08:00
|
|
|
|
avatarColor: 'yellow',
|
2025-05-20 18:01:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('统计Tokens'),
|
|
|
|
|
|
value: isNaN(consumeTokens) ? 0 : consumeTokens,
|
2025-05-26 22:30:04 +08:00
|
|
|
|
icon: <IconTextStroked size="large" />,
|
2025-05-20 18:01:38 +08:00
|
|
|
|
color: 'bg-pink-50',
|
2025-05-25 13:30:47 +08:00
|
|
|
|
avatarColor: 'pink',
|
2025-05-20 18:01:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('统计次数'),
|
|
|
|
|
|
value: times,
|
2025-05-26 22:30:04 +08:00
|
|
|
|
icon: <IconPulse size="large" />,
|
2025-05-20 18:01:38 +08:00
|
|
|
|
color: 'bg-teal-50',
|
2025-05-25 13:30:47 +08:00
|
|
|
|
avatarColor: 'cyan',
|
2025-05-20 18:01:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('平均RPM'),
|
|
|
|
|
|
value: (
|
|
|
|
|
|
times /
|
|
|
|
|
|
((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000)
|
|
|
|
|
|
).toFixed(3),
|
2025-05-26 22:30:04 +08:00
|
|
|
|
icon: <IconStopwatchStroked size="large" />,
|
2025-05-20 18:01:38 +08:00
|
|
|
|
color: 'bg-indigo-50',
|
2025-05-25 13:30:47 +08:00
|
|
|
|
avatarColor: 'indigo',
|
2025-05-20 18:01:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: t('平均TPM'),
|
|
|
|
|
|
value: (() => {
|
|
|
|
|
|
const tpm = consumeTokens /
|
|
|
|
|
|
((Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000);
|
|
|
|
|
|
return isNaN(tpm) ? '0' : tpm.toFixed(3);
|
|
|
|
|
|
})(),
|
2025-05-26 22:30:04 +08:00
|
|
|
|
icon: <IconTypograph size="large" />,
|
2025-05-20 18:01:38 +08:00
|
|
|
|
color: 'bg-orange-50',
|
2025-05-25 13:30:47 +08:00
|
|
|
|
avatarColor: 'orange',
|
2025-05-20 18:01:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 获取问候语
|
|
|
|
|
|
const getGreeting = () => {
|
|
|
|
|
|
const hours = new Date().getHours();
|
|
|
|
|
|
let greeting = '';
|
2025-05-25 13:30:47 +08:00
|
|
|
|
|
2025-05-20 18:01:38 +08:00
|
|
|
|
if (hours >= 5 && hours < 12) {
|
|
|
|
|
|
greeting = t('早上好');
|
|
|
|
|
|
} else if (hours >= 12 && hours < 14) {
|
|
|
|
|
|
greeting = t('中午好');
|
|
|
|
|
|
} else if (hours >= 14 && hours < 18) {
|
|
|
|
|
|
greeting = t('下午好');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
greeting = t('晚上好');
|
|
|
|
|
|
}
|
2025-05-25 13:30:47 +08:00
|
|
|
|
|
2025-05-20 18:01:38 +08:00
|
|
|
|
const username = userState?.user?.username || '';
|
|
|
|
|
|
return `👋${greeting},${username}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-03-23 21:24:39 +08:00
|
|
|
|
return (
|
2025-05-20 18:01:38 +08:00
|
|
|
|
<div className="bg-gray-50 min-h-screen">
|
2025-05-25 13:30:47 +08:00
|
|
|
|
<div className="flex items-center justify-between mb-4">
|
2025-05-20 18:01:38 +08:00
|
|
|
|
<h2 className="text-2xl font-semibold text-gray-800">{getGreeting()}</h2>
|
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
|
<IconButton
|
|
|
|
|
|
icon={<IconSearch />}
|
|
|
|
|
|
onClick={showSearchModal}
|
|
|
|
|
|
className="bg-green-500 text-white hover:bg-green-600 !rounded-full"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<IconButton
|
|
|
|
|
|
icon={<IconRefresh />}
|
|
|
|
|
|
onClick={refresh}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
className="bg-blue-500 text-white hover:bg-blue-600 !rounded-full"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 搜索条件Modal */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title={t('搜索条件')}
|
|
|
|
|
|
visible={searchModalVisible}
|
|
|
|
|
|
onOk={handleSearchConfirm}
|
|
|
|
|
|
onCancel={handleCloseModal}
|
|
|
|
|
|
closeOnEsc={true}
|
2025-05-26 15:13:55 +08:00
|
|
|
|
size={isMobile() ? 'full-width' : 'small'}
|
2025-05-20 18:01:38 +08:00
|
|
|
|
centered
|
|
|
|
|
|
>
|
|
|
|
|
|
<Form ref={formRef} layout='vertical' className="w-full">
|
|
|
|
|
|
<Form.DatePicker
|
|
|
|
|
|
field='start_timestamp'
|
|
|
|
|
|
label={t('起始时间')}
|
2025-05-26 15:13:55 +08:00
|
|
|
|
className="w-full mb-2 !rounded-lg"
|
2025-05-20 18:01:38 +08:00
|
|
|
|
initValue={start_timestamp}
|
|
|
|
|
|
value={start_timestamp}
|
|
|
|
|
|
type='dateTime'
|
|
|
|
|
|
name='start_timestamp'
|
2025-05-26 15:13:55 +08:00
|
|
|
|
size='large'
|
2025-05-20 18:01:38 +08:00
|
|
|
|
onChange={(value) => handleInputChange(value, 'start_timestamp')}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Form.DatePicker
|
|
|
|
|
|
field='end_timestamp'
|
|
|
|
|
|
label={t('结束时间')}
|
2025-05-26 15:13:55 +08:00
|
|
|
|
className="w-full mb-2 !rounded-lg"
|
2025-05-20 18:01:38 +08:00
|
|
|
|
initValue={end_timestamp}
|
|
|
|
|
|
value={end_timestamp}
|
|
|
|
|
|
type='dateTime'
|
|
|
|
|
|
name='end_timestamp'
|
2025-05-26 15:13:55 +08:00
|
|
|
|
size='large'
|
2025-05-20 18:01:38 +08:00
|
|
|
|
onChange={(value) => handleInputChange(value, 'end_timestamp')}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Form.Select
|
|
|
|
|
|
field='data_export_default_time'
|
|
|
|
|
|
label={t('时间粒度')}
|
2025-05-26 15:13:55 +08:00
|
|
|
|
className="w-full mb-2 !rounded-lg"
|
2025-05-20 18:01:38 +08:00
|
|
|
|
initValue={dataExportDefaultTime}
|
|
|
|
|
|
placeholder={t('时间粒度')}
|
|
|
|
|
|
name='data_export_default_time'
|
2025-05-26 15:13:55 +08:00
|
|
|
|
size='large'
|
2025-05-20 18:01:38 +08:00
|
|
|
|
optionList={[
|
|
|
|
|
|
{ label: t('小时'), value: 'hour' },
|
|
|
|
|
|
{ label: t('天'), value: 'day' },
|
|
|
|
|
|
{ label: t('周'), value: 'week' },
|
|
|
|
|
|
]}
|
|
|
|
|
|
onChange={(value) => handleInputChange(value, 'data_export_default_time')}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{isAdminUser && (
|
|
|
|
|
|
<Form.Input
|
|
|
|
|
|
field='username'
|
|
|
|
|
|
label={t('用户名称')}
|
2025-05-26 15:13:55 +08:00
|
|
|
|
className="w-full mb-2 !rounded-lg"
|
2025-05-20 18:01:38 +08:00
|
|
|
|
value={username}
|
|
|
|
|
|
placeholder={t('可选值')}
|
|
|
|
|
|
name='username'
|
2025-05-26 15:13:55 +08:00
|
|
|
|
size='large'
|
2025-05-20 18:01:38 +08:00
|
|
|
|
onChange={(value) => handleInputChange(value, 'username')}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
<Spin spinning={loading}>
|
2025-05-26 22:30:04 +08:00
|
|
|
|
<div className="mb-4">
|
2025-05-20 18:01:38 +08:00
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
|
|
|
|
{statsData.map((stat, idx) => (
|
|
|
|
|
|
<Card
|
|
|
|
|
|
key={idx}
|
|
|
|
|
|
shadows='hover'
|
|
|
|
|
|
className={`${stat.color} border-0 !rounded-2xl w-full`}
|
|
|
|
|
|
headerLine={false}
|
2025-05-26 23:30:26 +08:00
|
|
|
|
onClick={stat.onClick}
|
2024-12-12 16:11:17 +08:00
|
|
|
|
>
|
2025-05-20 18:01:38 +08:00
|
|
|
|
<div className="flex items-center">
|
2025-05-25 13:30:47 +08:00
|
|
|
|
<Avatar
|
|
|
|
|
|
className="mr-3"
|
|
|
|
|
|
size="medium"
|
|
|
|
|
|
color={stat.avatarColor}
|
|
|
|
|
|
>
|
|
|
|
|
|
{stat.icon}
|
|
|
|
|
|
</Avatar>
|
2025-05-20 18:01:38 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<div className="text-sm text-gray-500">{stat.title}</div>
|
|
|
|
|
|
<div className="text-xl font-semibold">{stat.value}</div>
|
2024-12-12 16:11:17 +08:00
|
|
|
|
</div>
|
2025-05-20 18:01:38 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
|
|
|
|
<Card shadows='hover' className="shadow-sm !rounded-2xl" headerLine={true} title={t('模型消耗分布')}>
|
|
|
|
|
|
<div style={{ height: 400 }}>
|
|
|
|
|
|
<VChart
|
|
|
|
|
|
spec={spec_line}
|
|
|
|
|
|
option={{ mode: 'desktop-browser' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Card shadows='hover' className="shadow-sm !rounded-2xl" headerLine={true} title={t('模型调用次数占比')}>
|
|
|
|
|
|
<div style={{ height: 400 }}>
|
|
|
|
|
|
<VChart
|
|
|
|
|
|
spec={spec_pie}
|
|
|
|
|
|
option={{ mode: 'desktop-browser' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Spin>
|
|
|
|
|
|
</div>
|
2024-03-23 21:24:39 +08:00
|
|
|
|
);
|
2024-01-07 18:31:14 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default Detail;
|