初始化导入 new-api 源码
This commit is contained in:
447
web/src/hooks/dashboard/useDashboardCharts.jsx
vendored
Normal file
447
web/src/hooks/dashboard/useDashboardCharts.jsx
vendored
Normal file
@@ -0,0 +1,447 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
|
||||
import {
|
||||
modelColorMap,
|
||||
renderNumber,
|
||||
renderQuota,
|
||||
modelToColor,
|
||||
getQuotaWithUnit,
|
||||
} from '../../helpers';
|
||||
import {
|
||||
processRawData,
|
||||
calculateTrendData,
|
||||
aggregateDataByTimeAndModel,
|
||||
generateChartTimePoints,
|
||||
updateChartSpec,
|
||||
updateMapValue,
|
||||
initializeMaps,
|
||||
} from '../../helpers/dashboard';
|
||||
|
||||
export const useDashboardCharts = (
|
||||
dataExportDefaultTime,
|
||||
setTrendData,
|
||||
setConsumeQuota,
|
||||
setTimes,
|
||||
setConsumeTokens,
|
||||
setPieData,
|
||||
setLineData,
|
||||
setModelColors,
|
||||
t,
|
||||
) => {
|
||||
// ========== 图表规格状态 ==========
|
||||
const [spec_pie, setSpecPie] = useState({
|
||||
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: t('模型调用次数占比'),
|
||||
subtext: `${t('总计')}:${renderNumber(0)}`,
|
||||
},
|
||||
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',
|
||||
data: [
|
||||
{
|
||||
id: 'barData',
|
||||
values: [],
|
||||
},
|
||||
],
|
||||
xField: 'Time',
|
||||
yField: 'Usage',
|
||||
seriesField: 'Model',
|
||||
stack: true,
|
||||
legends: {
|
||||
visible: true,
|
||||
selectMode: 'single',
|
||||
},
|
||||
title: {
|
||||
visible: true,
|
||||
text: t('模型消耗分布'),
|
||||
subtext: `${t('总计')}:${renderQuota(0, 2)}`,
|
||||
},
|
||||
bar: {
|
||||
state: {
|
||||
hover: {
|
||||
stroke: '#000',
|
||||
lineWidth: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
mark: {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['Model'],
|
||||
value: (datum) => renderQuota(datum['rawQuota'] || 0, 4),
|
||||
},
|
||||
],
|
||||
},
|
||||
dimension: {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['Model'],
|
||||
value: (datum) => datum['rawQuota'] || 0,
|
||||
},
|
||||
],
|
||||
updateContent: (array) => {
|
||||
array.sort((a, b) => b.value - a.value);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i].key == '其他') {
|
||||
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);
|
||||
}
|
||||
array.unshift({
|
||||
key: t('总计'),
|
||||
value: renderQuota(sum, 4),
|
||||
});
|
||||
return array;
|
||||
},
|
||||
},
|
||||
},
|
||||
color: {
|
||||
specified: modelColorMap,
|
||||
},
|
||||
});
|
||||
|
||||
// 模型消耗趋势折线图
|
||||
const [spec_model_line, setSpecModelLine] = useState({
|
||||
type: 'line',
|
||||
data: [
|
||||
{
|
||||
id: 'lineData',
|
||||
values: [],
|
||||
},
|
||||
],
|
||||
xField: 'Time',
|
||||
yField: 'Count',
|
||||
seriesField: 'Model',
|
||||
legends: {
|
||||
visible: true,
|
||||
selectMode: 'single',
|
||||
},
|
||||
title: {
|
||||
visible: true,
|
||||
text: t('模型消耗趋势'),
|
||||
subtext: '',
|
||||
},
|
||||
tooltip: {
|
||||
mark: {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['Model'],
|
||||
value: (datum) => renderNumber(datum['Count']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
color: {
|
||||
specified: modelColorMap,
|
||||
},
|
||||
});
|
||||
|
||||
// 模型调用次数排行柱状图
|
||||
const [spec_rank_bar, setSpecRankBar] = useState({
|
||||
type: 'bar',
|
||||
data: [
|
||||
{
|
||||
id: 'rankData',
|
||||
values: [],
|
||||
},
|
||||
],
|
||||
xField: 'Model',
|
||||
yField: 'Count',
|
||||
seriesField: 'Model',
|
||||
legends: {
|
||||
visible: true,
|
||||
selectMode: 'single',
|
||||
},
|
||||
title: {
|
||||
visible: true,
|
||||
text: t('模型调用次数排行'),
|
||||
subtext: '',
|
||||
},
|
||||
bar: {
|
||||
state: {
|
||||
hover: {
|
||||
stroke: '#000',
|
||||
lineWidth: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
mark: {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['Model'],
|
||||
value: (datum) => renderNumber(datum['Count']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
color: {
|
||||
specified: modelColorMap,
|
||||
},
|
||||
});
|
||||
|
||||
// ========== 数据处理函数 ==========
|
||||
const generateModelColors = useCallback((uniqueModels, modelColors) => {
|
||||
const newModelColors = {};
|
||||
Array.from(uniqueModels).forEach((modelName) => {
|
||||
newModelColors[modelName] =
|
||||
modelColorMap[modelName] ||
|
||||
modelColors[modelName] ||
|
||||
modelToColor(modelName);
|
||||
});
|
||||
return newModelColors;
|
||||
}, []);
|
||||
|
||||
const updateChartData = useCallback(
|
||||
(data) => {
|
||||
const processedData = processRawData(
|
||||
data,
|
||||
dataExportDefaultTime,
|
||||
initializeMaps,
|
||||
updateMapValue,
|
||||
);
|
||||
|
||||
const {
|
||||
totalQuota,
|
||||
totalTimes,
|
||||
totalTokens,
|
||||
uniqueModels,
|
||||
timePoints,
|
||||
timeQuotaMap,
|
||||
timeTokensMap,
|
||||
timeCountMap,
|
||||
} = processedData;
|
||||
|
||||
const trendDataResult = calculateTrendData(
|
||||
timePoints,
|
||||
timeQuotaMap,
|
||||
timeTokensMap,
|
||||
timeCountMap,
|
||||
dataExportDefaultTime,
|
||||
);
|
||||
setTrendData(trendDataResult);
|
||||
|
||||
const newModelColors = generateModelColors(uniqueModels, {});
|
||||
setModelColors(newModelColors);
|
||||
|
||||
const aggregatedData = aggregateDataByTimeAndModel(
|
||||
data,
|
||||
dataExportDefaultTime,
|
||||
);
|
||||
|
||||
const modelTotals = new Map();
|
||||
for (let [_, value] of aggregatedData) {
|
||||
updateMapValue(modelTotals, value.model, value.count);
|
||||
}
|
||||
|
||||
const newPieData = Array.from(modelTotals)
|
||||
.map(([model, count]) => ({
|
||||
type: model,
|
||||
value: count,
|
||||
}))
|
||||
.sort((a, b) => b.value - a.value);
|
||||
|
||||
const chartTimePoints = generateChartTimePoints(
|
||||
aggregatedData,
|
||||
data,
|
||||
dataExportDefaultTime,
|
||||
);
|
||||
|
||||
let newLineData = [];
|
||||
|
||||
chartTimePoints.forEach((time) => {
|
||||
let timeData = Array.from(uniqueModels).map((model) => {
|
||||
const key = `${time}-${model}`;
|
||||
const aggregated = aggregatedData.get(key);
|
||||
return {
|
||||
Time: time,
|
||||
Model: model,
|
||||
rawQuota: aggregated?.quota || 0,
|
||||
Usage: aggregated?.quota
|
||||
? getQuotaWithUnit(aggregated.quota, 4)
|
||||
: 0,
|
||||
};
|
||||
});
|
||||
|
||||
const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0);
|
||||
timeData.sort((a, b) => b.rawQuota - a.rawQuota);
|
||||
timeData = timeData.map((item) => ({ ...item, TimeSum: timeSum }));
|
||||
newLineData.push(...timeData);
|
||||
});
|
||||
|
||||
newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
|
||||
|
||||
updateChartSpec(
|
||||
setSpecPie,
|
||||
newPieData,
|
||||
`${t('总计')}:${renderNumber(totalTimes)}`,
|
||||
newModelColors,
|
||||
'id0',
|
||||
);
|
||||
|
||||
updateChartSpec(
|
||||
setSpecLine,
|
||||
newLineData,
|
||||
`${t('总计')}:${renderQuota(totalQuota, 2)}`,
|
||||
newModelColors,
|
||||
'barData',
|
||||
);
|
||||
|
||||
// ===== 模型调用次数折线图 =====
|
||||
let modelLineData = [];
|
||||
chartTimePoints.forEach((time) => {
|
||||
const timeData = Array.from(uniqueModels).map((model) => {
|
||||
const key = `${time}-${model}`;
|
||||
const aggregated = aggregatedData.get(key);
|
||||
return {
|
||||
Time: time,
|
||||
Model: model,
|
||||
Count: aggregated?.count || 0,
|
||||
};
|
||||
});
|
||||
modelLineData.push(...timeData);
|
||||
});
|
||||
modelLineData.sort((a, b) => a.Time.localeCompare(b.Time));
|
||||
|
||||
// ===== 模型调用次数排行柱状图 =====
|
||||
const rankData = Array.from(modelTotals)
|
||||
.map(([model, count]) => ({
|
||||
Model: model,
|
||||
Count: count,
|
||||
}))
|
||||
.sort((a, b) => b.Count - a.Count);
|
||||
|
||||
updateChartSpec(
|
||||
setSpecModelLine,
|
||||
modelLineData,
|
||||
`${t('总计')}:${renderNumber(totalTimes)}`,
|
||||
newModelColors,
|
||||
'lineData',
|
||||
);
|
||||
|
||||
updateChartSpec(
|
||||
setSpecRankBar,
|
||||
rankData,
|
||||
`${t('总计')}:${renderNumber(totalTimes)}`,
|
||||
newModelColors,
|
||||
'rankData',
|
||||
);
|
||||
|
||||
setPieData(newPieData);
|
||||
setLineData(newLineData);
|
||||
setConsumeQuota(totalQuota);
|
||||
setTimes(totalTimes);
|
||||
setConsumeTokens(totalTokens);
|
||||
},
|
||||
[
|
||||
dataExportDefaultTime,
|
||||
setTrendData,
|
||||
generateModelColors,
|
||||
setModelColors,
|
||||
setPieData,
|
||||
setLineData,
|
||||
setConsumeQuota,
|
||||
setTimes,
|
||||
setConsumeTokens,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
// ========== 初始化图表主题 ==========
|
||||
useEffect(() => {
|
||||
initVChartSemiTheme({
|
||||
isWatchingThemeSwitch: true,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// 图表规格
|
||||
spec_pie,
|
||||
spec_line,
|
||||
spec_model_line,
|
||||
spec_rank_bar,
|
||||
|
||||
// 函数
|
||||
updateChartData,
|
||||
generateModelColors,
|
||||
};
|
||||
};
|
||||
346
web/src/hooks/dashboard/useDashboardData.js
vendored
Normal file
346
web/src/hooks/dashboard/useDashboardData.js
vendored
Normal file
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, isAdmin, showError, timestamp2string } from '../../helpers';
|
||||
import { getDefaultTime, getInitialTimestamp } from '../../helpers/dashboard';
|
||||
import { TIME_OPTIONS } from '../../constants/dashboard.constants';
|
||||
import { useIsMobile } from '../common/useIsMobile';
|
||||
import { useMinimumLoadingTime } from '../common/useMinimumLoadingTime';
|
||||
|
||||
export const useDashboardData = (userState, userDispatch, statusState) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const isMobile = useIsMobile();
|
||||
const initialized = useRef(false);
|
||||
|
||||
// ========== 基础状态 ==========
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [greetingVisible, setGreetingVisible] = useState(false);
|
||||
const [searchModalVisible, setSearchModalVisible] = useState(false);
|
||||
const showLoading = useMinimumLoadingTime(loading);
|
||||
|
||||
// ========== 输入状态 ==========
|
||||
const [inputs, setInputs] = useState({
|
||||
username: '',
|
||||
token_name: '',
|
||||
model_name: '',
|
||||
start_timestamp: getInitialTimestamp(),
|
||||
end_timestamp: timestamp2string(new Date().getTime() / 1000 + 3600),
|
||||
channel: '',
|
||||
data_export_default_time: '',
|
||||
});
|
||||
|
||||
const [dataExportDefaultTime, setDataExportDefaultTime] =
|
||||
useState(getDefaultTime());
|
||||
|
||||
// ========== 数据状态 ==========
|
||||
const [quotaData, setQuotaData] = useState([]);
|
||||
const [consumeQuota, setConsumeQuota] = useState(0);
|
||||
const [consumeTokens, setConsumeTokens] = useState(0);
|
||||
const [times, setTimes] = useState(0);
|
||||
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
|
||||
const [lineData, setLineData] = useState([]);
|
||||
const [modelColors, setModelColors] = useState({});
|
||||
|
||||
// ========== 图表状态 ==========
|
||||
const [activeChartTab, setActiveChartTab] = useState('1');
|
||||
|
||||
// ========== 趋势数据 ==========
|
||||
const [cacheMetrics, setCacheMetrics] = useState({ overall: {}, today: {} });
|
||||
|
||||
const [trendData, setTrendData] = useState({
|
||||
balance: [],
|
||||
usedQuota: [],
|
||||
requestCount: [],
|
||||
times: [],
|
||||
consumeQuota: [],
|
||||
tokens: [],
|
||||
rpm: [],
|
||||
tpm: [],
|
||||
});
|
||||
|
||||
// ========== Uptime 数据 ==========
|
||||
const [uptimeData, setUptimeData] = useState([]);
|
||||
const [uptimeLoading, setUptimeLoading] = useState(false);
|
||||
const [activeUptimeTab, setActiveUptimeTab] = useState('');
|
||||
|
||||
// ========== 常量 ==========
|
||||
const now = new Date();
|
||||
const isAdminUser = isAdmin();
|
||||
|
||||
// ========== Panel enable flags ==========
|
||||
const apiInfoEnabled = statusState?.status?.api_info_enabled ?? true;
|
||||
const announcementsEnabled =
|
||||
statusState?.status?.announcements_enabled ?? true;
|
||||
const faqEnabled = statusState?.status?.faq_enabled ?? true;
|
||||
const uptimeEnabled = statusState?.status?.uptime_kuma_enabled ?? true;
|
||||
|
||||
const hasApiInfoPanel = apiInfoEnabled;
|
||||
const hasInfoPanels = announcementsEnabled || faqEnabled || uptimeEnabled;
|
||||
|
||||
// ========== Memoized Values ==========
|
||||
const timeOptions = useMemo(
|
||||
() =>
|
||||
TIME_OPTIONS.map((option) => ({
|
||||
...option,
|
||||
label: t(option.label),
|
||||
})),
|
||||
[t],
|
||||
);
|
||||
|
||||
const performanceMetrics = useMemo(() => {
|
||||
const { start_timestamp, end_timestamp } = inputs;
|
||||
const timeDiff =
|
||||
(Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000;
|
||||
const avgRPM = isNaN(times / timeDiff)
|
||||
? '0'
|
||||
: (times / timeDiff).toFixed(3);
|
||||
const avgTPM = isNaN(consumeTokens / timeDiff)
|
||||
? '0'
|
||||
: (consumeTokens / timeDiff).toFixed(3);
|
||||
|
||||
return { avgRPM, avgTPM, timeDiff };
|
||||
}, [times, consumeTokens, inputs.start_timestamp, inputs.end_timestamp]);
|
||||
|
||||
const getGreeting = useMemo(() => {
|
||||
const hours = new Date().getHours();
|
||||
let greeting = '';
|
||||
|
||||
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('晚上好');
|
||||
}
|
||||
|
||||
const username = userState?.user?.username || '';
|
||||
return `👋${greeting},${username}`;
|
||||
}, [t, userState?.user?.username]);
|
||||
|
||||
// ========== 回调函数 ==========
|
||||
const handleInputChange = useCallback((value, name) => {
|
||||
if (name === 'data_export_default_time') {
|
||||
setDataExportDefaultTime(value);
|
||||
localStorage.setItem('data_export_default_time', value);
|
||||
return;
|
||||
}
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
}, []);
|
||||
|
||||
const showSearchModal = useCallback(() => {
|
||||
setSearchModalVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
setSearchModalVisible(false);
|
||||
}, []);
|
||||
|
||||
// ========== API 调用函数 ==========
|
||||
const loadQuotaData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let url = '';
|
||||
const { start_timestamp, end_timestamp, username } = inputs;
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
const cacheBust = `_=${Date.now()}`;
|
||||
|
||||
if (isAdminUser) {
|
||||
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}&${cacheBust}`;
|
||||
} else {
|
||||
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}&${cacheBust}`;
|
||||
}
|
||||
|
||||
const res = await API.get(url, { disableDuplicate: true });
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
const normalizedData = Array.isArray(data) ? [...data] : [];
|
||||
setQuotaData(normalizedData);
|
||||
if (normalizedData.length === 0) {
|
||||
normalizedData.push({
|
||||
count: 0,
|
||||
model_name: '无数据',
|
||||
quota: 0,
|
||||
token_used: 0,
|
||||
created_at: now.getTime() / 1000,
|
||||
});
|
||||
}
|
||||
normalizedData.sort((a, b) => a.created_at - b.created_at);
|
||||
return normalizedData;
|
||||
} else {
|
||||
showError(message);
|
||||
return [];
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [inputs, dataExportDefaultTime, isAdminUser, now]);
|
||||
|
||||
const loadCacheStats = useCallback(async () => {
|
||||
try {
|
||||
const res = await API.get(`/api/log/self/stat?type=2&_=${Date.now()}`, {
|
||||
disableDuplicate: true,
|
||||
});
|
||||
if (res.data?.success) {
|
||||
setCacheMetrics({
|
||||
overall: res.data?.data?.cache_overall || {},
|
||||
today: res.data?.data?.cache_today || {},
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}, []);
|
||||
|
||||
const loadUptimeData = useCallback(async () => {
|
||||
setUptimeLoading(true);
|
||||
try {
|
||||
const res = await API.get('/api/uptime/status');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setUptimeData(data || []);
|
||||
if (data && data.length > 0 && !activeUptimeTab) {
|
||||
setActiveUptimeTab(data[0].categoryName);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setUptimeLoading(false);
|
||||
}
|
||||
}, [activeUptimeTab]);
|
||||
|
||||
const getUserData = useCallback(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);
|
||||
}
|
||||
}, [userDispatch]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const data = await loadQuotaData();
|
||||
await loadUptimeData();
|
||||
await loadCacheStats();
|
||||
return data;
|
||||
}, [loadQuotaData, loadUptimeData, loadCacheStats]);
|
||||
|
||||
const handleSearchConfirm = useCallback(
|
||||
async (updateChartDataCallback) => {
|
||||
const data = await refresh();
|
||||
if (data && data.length > 0 && updateChartDataCallback) {
|
||||
updateChartDataCallback(data);
|
||||
}
|
||||
setSearchModalVisible(false);
|
||||
},
|
||||
[refresh],
|
||||
);
|
||||
|
||||
// ========== Effects ==========
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setGreetingVisible(true);
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized.current) {
|
||||
getUserData();
|
||||
initialized.current = true;
|
||||
}
|
||||
}, [getUserData]);
|
||||
|
||||
return {
|
||||
// 基础状态
|
||||
loading: showLoading,
|
||||
greetingVisible,
|
||||
searchModalVisible,
|
||||
|
||||
// 输入状态
|
||||
inputs,
|
||||
dataExportDefaultTime,
|
||||
|
||||
// 数据状态
|
||||
quotaData,
|
||||
consumeQuota,
|
||||
setConsumeQuota,
|
||||
consumeTokens,
|
||||
setConsumeTokens,
|
||||
times,
|
||||
setTimes,
|
||||
pieData,
|
||||
setPieData,
|
||||
lineData,
|
||||
setLineData,
|
||||
modelColors,
|
||||
setModelColors,
|
||||
|
||||
// 图表状态
|
||||
activeChartTab,
|
||||
setActiveChartTab,
|
||||
|
||||
// 趋势数据
|
||||
trendData,
|
||||
setTrendData,
|
||||
cacheMetrics,
|
||||
|
||||
// Uptime 数据
|
||||
uptimeData,
|
||||
uptimeLoading,
|
||||
activeUptimeTab,
|
||||
setActiveUptimeTab,
|
||||
|
||||
// 计算值
|
||||
timeOptions,
|
||||
performanceMetrics,
|
||||
getGreeting,
|
||||
isAdminUser,
|
||||
hasApiInfoPanel,
|
||||
hasInfoPanels,
|
||||
apiInfoEnabled,
|
||||
announcementsEnabled,
|
||||
faqEnabled,
|
||||
uptimeEnabled,
|
||||
|
||||
// 函数
|
||||
handleInputChange,
|
||||
showSearchModal,
|
||||
handleCloseModal,
|
||||
loadQuotaData,
|
||||
loadUptimeData,
|
||||
loadCacheStats,
|
||||
getUserData,
|
||||
refresh,
|
||||
handleSearchConfirm,
|
||||
|
||||
// 导航和翻译
|
||||
navigate,
|
||||
t,
|
||||
isMobile,
|
||||
};
|
||||
};
|
||||
171
web/src/hooks/dashboard/useDashboardStats.jsx
vendored
Normal file
171
web/src/hooks/dashboard/useDashboardStats.jsx
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { Wallet, Activity, Zap, Gauge } from 'lucide-react';
|
||||
import {
|
||||
IconMoneyExchangeStroked,
|
||||
IconHistogram,
|
||||
IconCoinMoneyStroked,
|
||||
IconTextStroked,
|
||||
IconPulse,
|
||||
IconStopwatchStroked,
|
||||
IconTypograph,
|
||||
IconSend,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { renderQuota } from '../../helpers';
|
||||
import { createSectionTitle } from '../../helpers/dashboard';
|
||||
|
||||
export const useDashboardStats = (
|
||||
userState,
|
||||
consumeQuota,
|
||||
consumeTokens,
|
||||
times,
|
||||
trendData,
|
||||
performanceMetrics,
|
||||
cacheMetrics,
|
||||
navigate,
|
||||
t,
|
||||
) => {
|
||||
const groupedStatsData = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: createSectionTitle(Wallet, t('账户数据')),
|
||||
color: 'bg-blue-50',
|
||||
items: [
|
||||
{
|
||||
title: t('当前余额'),
|
||||
value: renderQuota(userState?.user?.quota),
|
||||
icon: <IconMoneyExchangeStroked />,
|
||||
avatarColor: 'blue',
|
||||
trendData: [],
|
||||
trendColor: '#3b82f6',
|
||||
},
|
||||
{
|
||||
title: t('历史消耗'),
|
||||
value: renderQuota(userState?.user?.used_quota),
|
||||
icon: <IconHistogram />,
|
||||
avatarColor: 'purple',
|
||||
trendData: [],
|
||||
trendColor: '#8b5cf6',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: createSectionTitle(Activity, t('使用统计')),
|
||||
color: 'bg-green-50',
|
||||
items: [
|
||||
{
|
||||
title: t('请求次数'),
|
||||
value: userState.user?.request_count,
|
||||
icon: <IconSend />,
|
||||
avatarColor: 'green',
|
||||
trendData: [],
|
||||
trendColor: '#10b981',
|
||||
},
|
||||
{
|
||||
title: t('统计次数'),
|
||||
value: times,
|
||||
icon: <IconPulse />,
|
||||
avatarColor: 'cyan',
|
||||
trendData: trendData.times,
|
||||
trendColor: '#06b6d4',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: createSectionTitle(Zap, t('资源消耗')),
|
||||
color: 'bg-yellow-50',
|
||||
items: [
|
||||
{
|
||||
title: t('统计额度'),
|
||||
value: renderQuota(consumeQuota),
|
||||
icon: <IconCoinMoneyStroked />,
|
||||
avatarColor: 'yellow',
|
||||
trendData: trendData.consumeQuota,
|
||||
trendColor: '#f59e0b',
|
||||
},
|
||||
{
|
||||
title: t('统计Tokens'),
|
||||
value: isNaN(consumeTokens) ? 0 : consumeTokens.toLocaleString(),
|
||||
icon: <IconTextStroked />,
|
||||
avatarColor: 'pink',
|
||||
trendData: trendData.tokens,
|
||||
trendColor: '#ec4899',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: createSectionTitle(Gauge, t('缓存指标')),
|
||||
color: 'bg-indigo-50',
|
||||
items: [
|
||||
{
|
||||
title: t('缓存命中率(总体)'),
|
||||
value: `${(((cacheMetrics?.overall?.cache_hit_rate || 0) * 100) || 0).toFixed(2)}%`,
|
||||
icon: <IconStopwatchStroked />,
|
||||
avatarColor: 'indigo',
|
||||
trendData: trendData.rpm,
|
||||
trendColor: '#6366f1',
|
||||
},
|
||||
{
|
||||
title: t('缓存命中率(今日)'),
|
||||
value: `${(((cacheMetrics?.today?.cache_hit_rate || 0) * 100) || 0).toFixed(2)}%`,
|
||||
icon: <IconStopwatchStroked />,
|
||||
avatarColor: 'violet',
|
||||
trendData: trendData.rpm,
|
||||
trendColor: '#8b5cf6',
|
||||
},
|
||||
{
|
||||
title: t('缓存Token(总体)'),
|
||||
value: ((cacheMetrics?.overall?.cache_tokens || 0)).toLocaleString(),
|
||||
icon: <IconTypograph />,
|
||||
avatarColor: 'blue',
|
||||
trendData: trendData.tpm,
|
||||
trendColor: '#3b82f6',
|
||||
},
|
||||
{
|
||||
title: t('缓存Token(今日)'),
|
||||
value: ((cacheMetrics?.today?.cache_tokens || 0)).toLocaleString(),
|
||||
icon: <IconTypograph />,
|
||||
avatarColor: 'orange',
|
||||
trendData: trendData.tpm,
|
||||
trendColor: '#f97316',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
userState?.user?.quota,
|
||||
userState?.user?.used_quota,
|
||||
userState?.user?.request_count,
|
||||
times,
|
||||
consumeQuota,
|
||||
consumeTokens,
|
||||
trendData,
|
||||
performanceMetrics,
|
||||
cacheMetrics,
|
||||
navigate,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
groupedStatsData,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user