313 lines
9.2 KiB
JavaScript
313 lines
9.2 KiB
JavaScript
|
|
/*
|
|||
|
|
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';
|
|||
|
|
|
|||
|
|
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 [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 [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);
|
|||
|
|
const startTime = Date.now();
|
|||
|
|
try {
|
|||
|
|
let url = '';
|
|||
|
|
const { start_timestamp, end_timestamp, username } = inputs;
|
|||
|
|
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}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
data.sort((a, b) => a.created_at - b.created_at);
|
|||
|
|
return data;
|
|||
|
|
} else {
|
|||
|
|
showError(message);
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
const elapsed = Date.now() - startTime;
|
|||
|
|
const remainingTime = Math.max(0, 500 - elapsed);
|
|||
|
|
setTimeout(() => {
|
|||
|
|
setLoading(false);
|
|||
|
|
}, remainingTime);
|
|||
|
|
}
|
|||
|
|
}, [inputs, dataExportDefaultTime, isAdminUser, now]);
|
|||
|
|
|
|||
|
|
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();
|
|||
|
|
return data;
|
|||
|
|
}, [loadQuotaData, loadUptimeData]);
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
greetingVisible,
|
|||
|
|
searchModalVisible,
|
|||
|
|
|
|||
|
|
// 输入状态
|
|||
|
|
inputs,
|
|||
|
|
dataExportDefaultTime,
|
|||
|
|
|
|||
|
|
// 数据状态
|
|||
|
|
quotaData,
|
|||
|
|
consumeQuota,
|
|||
|
|
setConsumeQuota,
|
|||
|
|
consumeTokens,
|
|||
|
|
setConsumeTokens,
|
|||
|
|
times,
|
|||
|
|
setTimes,
|
|||
|
|
pieData,
|
|||
|
|
setPieData,
|
|||
|
|
lineData,
|
|||
|
|
setLineData,
|
|||
|
|
modelColors,
|
|||
|
|
setModelColors,
|
|||
|
|
|
|||
|
|
// 图表状态
|
|||
|
|
activeChartTab,
|
|||
|
|
setActiveChartTab,
|
|||
|
|
|
|||
|
|
// 趋势数据
|
|||
|
|
trendData,
|
|||
|
|
setTrendData,
|
|||
|
|
|
|||
|
|
// Uptime 数据
|
|||
|
|
uptimeData,
|
|||
|
|
uptimeLoading,
|
|||
|
|
activeUptimeTab,
|
|||
|
|
setActiveUptimeTab,
|
|||
|
|
|
|||
|
|
// 计算值
|
|||
|
|
timeOptions,
|
|||
|
|
performanceMetrics,
|
|||
|
|
getGreeting,
|
|||
|
|
isAdminUser,
|
|||
|
|
hasApiInfoPanel,
|
|||
|
|
hasInfoPanels,
|
|||
|
|
apiInfoEnabled,
|
|||
|
|
announcementsEnabled,
|
|||
|
|
faqEnabled,
|
|||
|
|
uptimeEnabled,
|
|||
|
|
|
|||
|
|
// 函数
|
|||
|
|
handleInputChange,
|
|||
|
|
showSearchModal,
|
|||
|
|
handleCloseModal,
|
|||
|
|
loadQuotaData,
|
|||
|
|
loadUptimeData,
|
|||
|
|
getUserData,
|
|||
|
|
refresh,
|
|||
|
|
handleSearchConfirm,
|
|||
|
|
|
|||
|
|
// 导航和翻译
|
|||
|
|
navigate,
|
|||
|
|
t,
|
|||
|
|
isMobile
|
|||
|
|
};
|
|||
|
|
};
|