useDashboardData.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
  16. import { useNavigate } from 'react-router-dom';
  17. import { useTranslation } from 'react-i18next';
  18. import { API, isAdmin, showError, timestamp2string } from '../../helpers';
  19. import { getDefaultTime, getInitialTimestamp } from '../../helpers/dashboard';
  20. import { TIME_OPTIONS } from '../../constants/dashboard.constants';
  21. import { useIsMobile } from '../common/useIsMobile';
  22. import { useMinimumLoadingTime } from '../common/useMinimumLoadingTime';
  23. export const useDashboardData = (userState, userDispatch, statusState) => {
  24. const { t } = useTranslation();
  25. const navigate = useNavigate();
  26. const isMobile = useIsMobile();
  27. const initialized = useRef(false);
  28. // ========== 基础状态 ==========
  29. const [loading, setLoading] = useState(false);
  30. const [greetingVisible, setGreetingVisible] = useState(false);
  31. const [searchModalVisible, setSearchModalVisible] = useState(false);
  32. const showLoading = useMinimumLoadingTime(loading);
  33. // ========== 输入状态 ==========
  34. const [inputs, setInputs] = useState({
  35. username: '',
  36. token_name: '',
  37. model_name: '',
  38. start_timestamp: getInitialTimestamp(),
  39. end_timestamp: timestamp2string(new Date().getTime() / 1000 + 3600),
  40. channel: '',
  41. data_export_default_time: '',
  42. });
  43. const [dataExportDefaultTime, setDataExportDefaultTime] =
  44. useState(getDefaultTime());
  45. // ========== 数据状态 ==========
  46. const [quotaData, setQuotaData] = useState([]);
  47. const [consumeQuota, setConsumeQuota] = useState(0);
  48. const [consumeTokens, setConsumeTokens] = useState(0);
  49. const [times, setTimes] = useState(0);
  50. const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
  51. const [lineData, setLineData] = useState([]);
  52. const [modelColors, setModelColors] = useState({});
  53. // ========== 图表状态 ==========
  54. const [activeChartTab, setActiveChartTab] = useState('1');
  55. // ========== 趋势数据 ==========
  56. const [trendData, setTrendData] = useState({
  57. balance: [],
  58. usedQuota: [],
  59. requestCount: [],
  60. times: [],
  61. consumeQuota: [],
  62. tokens: [],
  63. rpm: [],
  64. tpm: [],
  65. });
  66. // ========== Uptime 数据 ==========
  67. const [uptimeData, setUptimeData] = useState([]);
  68. const [uptimeLoading, setUptimeLoading] = useState(false);
  69. const [activeUptimeTab, setActiveUptimeTab] = useState('');
  70. // ========== 常量 ==========
  71. const now = new Date();
  72. const isAdminUser = isAdmin();
  73. // ========== Panel enable flags ==========
  74. const apiInfoEnabled = statusState?.status?.api_info_enabled ?? true;
  75. const announcementsEnabled =
  76. statusState?.status?.announcements_enabled ?? true;
  77. const faqEnabled = statusState?.status?.faq_enabled ?? true;
  78. const uptimeEnabled = statusState?.status?.uptime_kuma_enabled ?? true;
  79. const hasApiInfoPanel = apiInfoEnabled;
  80. const hasInfoPanels = announcementsEnabled || faqEnabled || uptimeEnabled;
  81. // ========== Memoized Values ==========
  82. const timeOptions = useMemo(
  83. () =>
  84. TIME_OPTIONS.map((option) => ({
  85. ...option,
  86. label: t(option.label),
  87. })),
  88. [t],
  89. );
  90. const performanceMetrics = useMemo(() => {
  91. const { start_timestamp, end_timestamp } = inputs;
  92. const timeDiff =
  93. (Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000;
  94. const avgRPM = isNaN(times / timeDiff)
  95. ? '0'
  96. : (times / timeDiff).toFixed(3);
  97. const avgTPM = isNaN(consumeTokens / timeDiff)
  98. ? '0'
  99. : (consumeTokens / timeDiff).toFixed(3);
  100. return { avgRPM, avgTPM, timeDiff };
  101. }, [times, consumeTokens, inputs.start_timestamp, inputs.end_timestamp]);
  102. const getGreeting = useMemo(() => {
  103. const hours = new Date().getHours();
  104. let greeting = '';
  105. if (hours >= 5 && hours < 12) {
  106. greeting = t('早上好');
  107. } else if (hours >= 12 && hours < 14) {
  108. greeting = t('中午好');
  109. } else if (hours >= 14 && hours < 18) {
  110. greeting = t('下午好');
  111. } else {
  112. greeting = t('晚上好');
  113. }
  114. const username = userState?.user?.username || '';
  115. return `👋${greeting},${username}`;
  116. }, [t, userState?.user?.username]);
  117. // ========== 回调函数 ==========
  118. const handleInputChange = useCallback((value, name) => {
  119. if (name === 'data_export_default_time') {
  120. setDataExportDefaultTime(value);
  121. localStorage.setItem('data_export_default_time', value);
  122. return;
  123. }
  124. setInputs((inputs) => ({ ...inputs, [name]: value }));
  125. }, []);
  126. const showSearchModal = useCallback(() => {
  127. setSearchModalVisible(true);
  128. }, []);
  129. const handleCloseModal = useCallback(() => {
  130. setSearchModalVisible(false);
  131. }, []);
  132. // ========== API 调用函数 ==========
  133. const loadQuotaData = useCallback(async () => {
  134. setLoading(true);
  135. try {
  136. let url = '';
  137. const { start_timestamp, end_timestamp, username } = inputs;
  138. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  139. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  140. if (isAdminUser) {
  141. url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
  142. } else {
  143. url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
  144. }
  145. const res = await API.get(url);
  146. const { success, message, data } = res.data;
  147. if (success) {
  148. setQuotaData(data);
  149. if (data.length === 0) {
  150. data.push({
  151. count: 0,
  152. model_name: '无数据',
  153. quota: 0,
  154. created_at: now.getTime() / 1000,
  155. });
  156. }
  157. data.sort((a, b) => a.created_at - b.created_at);
  158. return data;
  159. } else {
  160. showError(message);
  161. return [];
  162. }
  163. } finally {
  164. setLoading(false);
  165. }
  166. }, [inputs, dataExportDefaultTime, isAdminUser, now]);
  167. const loadUptimeData = useCallback(async () => {
  168. setUptimeLoading(true);
  169. try {
  170. const res = await API.get('/api/uptime/status');
  171. const { success, message, data } = res.data;
  172. if (success) {
  173. setUptimeData(data || []);
  174. if (data && data.length > 0 && !activeUptimeTab) {
  175. setActiveUptimeTab(data[0].categoryName);
  176. }
  177. } else {
  178. showError(message);
  179. }
  180. } catch (err) {
  181. console.error(err);
  182. } finally {
  183. setUptimeLoading(false);
  184. }
  185. }, [activeUptimeTab]);
  186. const getUserData = useCallback(async () => {
  187. let res = await API.get(`/api/user/self`);
  188. const { success, message, data } = res.data;
  189. if (success) {
  190. userDispatch({ type: 'login', payload: data });
  191. } else {
  192. showError(message);
  193. }
  194. }, [userDispatch]);
  195. const refresh = useCallback(async () => {
  196. const data = await loadQuotaData();
  197. await loadUptimeData();
  198. return data;
  199. }, [loadQuotaData, loadUptimeData]);
  200. const handleSearchConfirm = useCallback(
  201. async (updateChartDataCallback) => {
  202. const data = await refresh();
  203. if (data && data.length > 0 && updateChartDataCallback) {
  204. updateChartDataCallback(data);
  205. }
  206. setSearchModalVisible(false);
  207. },
  208. [refresh],
  209. );
  210. // ========== Effects ==========
  211. useEffect(() => {
  212. const timer = setTimeout(() => {
  213. setGreetingVisible(true);
  214. }, 100);
  215. return () => clearTimeout(timer);
  216. }, []);
  217. useEffect(() => {
  218. if (!initialized.current) {
  219. getUserData();
  220. initialized.current = true;
  221. }
  222. }, [getUserData]);
  223. return {
  224. // 基础状态
  225. loading: showLoading,
  226. greetingVisible,
  227. searchModalVisible,
  228. // 输入状态
  229. inputs,
  230. dataExportDefaultTime,
  231. // 数据状态
  232. quotaData,
  233. consumeQuota,
  234. setConsumeQuota,
  235. consumeTokens,
  236. setConsumeTokens,
  237. times,
  238. setTimes,
  239. pieData,
  240. setPieData,
  241. lineData,
  242. setLineData,
  243. modelColors,
  244. setModelColors,
  245. // 图表状态
  246. activeChartTab,
  247. setActiveChartTab,
  248. // 趋势数据
  249. trendData,
  250. setTrendData,
  251. // Uptime 数据
  252. uptimeData,
  253. uptimeLoading,
  254. activeUptimeTab,
  255. setActiveUptimeTab,
  256. // 计算值
  257. timeOptions,
  258. performanceMetrics,
  259. getGreeting,
  260. isAdminUser,
  261. hasApiInfoPanel,
  262. hasInfoPanels,
  263. apiInfoEnabled,
  264. announcementsEnabled,
  265. faqEnabled,
  266. uptimeEnabled,
  267. // 函数
  268. handleInputChange,
  269. showSearchModal,
  270. handleCloseModal,
  271. loadQuotaData,
  272. loadUptimeData,
  273. getUserData,
  274. refresh,
  275. handleSearchConfirm,
  276. // 导航和翻译
  277. navigate,
  278. t,
  279. isMobile,
  280. };
  281. };