Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 

390 wiersze
9.8 KiB

  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 React from 'react';
  16. import { Progress, Divider, Empty } from '@douyinfe/semi-ui';
  17. import {
  18. IllustrationConstruction,
  19. IllustrationConstructionDark,
  20. } from '@douyinfe/semi-illustrations';
  21. import {
  22. timestamp2string,
  23. timestamp2string1,
  24. isDataCrossYear,
  25. copy,
  26. showSuccess,
  27. } from './utils';
  28. import {
  29. STORAGE_KEYS,
  30. DEFAULT_TIME_INTERVALS,
  31. DEFAULTS,
  32. ILLUSTRATION_SIZE,
  33. } from '../constants/dashboard.constants';
  34. // ========== 时间相关工具函数 ==========
  35. export const getDefaultTime = () => {
  36. return localStorage.getItem(STORAGE_KEYS.DATA_EXPORT_DEFAULT_TIME) || 'hour';
  37. };
  38. export const getTimeInterval = (timeType, isSeconds = false) => {
  39. const intervals =
  40. DEFAULT_TIME_INTERVALS[timeType] || DEFAULT_TIME_INTERVALS.hour;
  41. return isSeconds ? intervals.seconds : intervals.minutes;
  42. };
  43. export const getInitialTimestamp = () => {
  44. const defaultTime = getDefaultTime();
  45. const now = new Date().getTime() / 1000;
  46. switch (defaultTime) {
  47. case 'hour':
  48. return timestamp2string(now - 86400);
  49. case 'week':
  50. return timestamp2string(now - 86400 * 30);
  51. default:
  52. return timestamp2string(now - 86400 * 7);
  53. }
  54. };
  55. // ========== 数据处理工具函数 ==========
  56. export const updateMapValue = (map, key, value) => {
  57. if (!map.has(key)) {
  58. map.set(key, 0);
  59. }
  60. map.set(key, map.get(key) + value);
  61. };
  62. export const initializeMaps = (key, ...maps) => {
  63. maps.forEach((map) => {
  64. if (!map.has(key)) {
  65. map.set(key, 0);
  66. }
  67. });
  68. };
  69. // ========== 图表相关工具函数 ==========
  70. export const updateChartSpec = (
  71. setterFunc,
  72. newData,
  73. subtitle,
  74. newColors,
  75. dataId,
  76. ) => {
  77. setterFunc((prev) => ({
  78. ...prev,
  79. data: [{ id: dataId, values: newData }],
  80. title: {
  81. ...prev.title,
  82. subtext: subtitle,
  83. },
  84. color: {
  85. specified: newColors,
  86. },
  87. }));
  88. };
  89. export const getTrendSpec = (data, color) => ({
  90. type: 'line',
  91. data: [{ id: 'trend', values: data.map((val, idx) => ({ x: idx, y: val })) }],
  92. xField: 'x',
  93. yField: 'y',
  94. height: 40,
  95. width: 100,
  96. axes: [
  97. {
  98. orient: 'bottom',
  99. visible: false,
  100. },
  101. {
  102. orient: 'left',
  103. visible: false,
  104. },
  105. ],
  106. padding: 0,
  107. autoFit: false,
  108. legends: { visible: false },
  109. tooltip: { visible: false },
  110. crosshair: { visible: false },
  111. line: {
  112. style: {
  113. stroke: color,
  114. lineWidth: 2,
  115. },
  116. },
  117. point: {
  118. visible: false,
  119. },
  120. background: {
  121. fill: 'transparent',
  122. },
  123. });
  124. // ========== UI 工具函数 ==========
  125. export const createSectionTitle = (Icon, text) => (
  126. <div className='flex items-center gap-2'>
  127. <Icon size={16} />
  128. {text}
  129. </div>
  130. );
  131. export const createFormField = (Component, props, FORM_FIELD_PROPS) => (
  132. <Component {...FORM_FIELD_PROPS} {...props} />
  133. );
  134. // ========== 操作处理函数 ==========
  135. export const handleCopyUrl = async (url, t) => {
  136. if (await copy(url)) {
  137. showSuccess(t('复制成功'));
  138. }
  139. };
  140. export const handleSpeedTest = (apiUrl) => {
  141. const encodedUrl = encodeURIComponent(apiUrl);
  142. const speedTestUrl = `https://www.tcptest.cn/http/${encodedUrl}`;
  143. window.open(speedTestUrl, '_blank', 'noopener,noreferrer');
  144. };
  145. // ========== 状态映射函数 ==========
  146. export const getUptimeStatusColor = (status, uptimeStatusMap) =>
  147. uptimeStatusMap[status]?.color || '#8b9aa7';
  148. export const getUptimeStatusText = (status, uptimeStatusMap, t) =>
  149. uptimeStatusMap[status]?.text || t('未知');
  150. // ========== 监控列表渲染函数 ==========
  151. export const renderMonitorList = (
  152. monitors,
  153. getUptimeStatusColor,
  154. getUptimeStatusText,
  155. t,
  156. ) => {
  157. if (!monitors || monitors.length === 0) {
  158. return (
  159. <div className='flex justify-center items-center py-4'>
  160. <Empty
  161. image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
  162. darkModeImage={
  163. <IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
  164. }
  165. title={t('暂无监控数据')}
  166. />
  167. </div>
  168. );
  169. }
  170. const grouped = {};
  171. monitors.forEach((m) => {
  172. const g = m.group || '';
  173. if (!grouped[g]) grouped[g] = [];
  174. grouped[g].push(m);
  175. });
  176. const renderItem = (monitor, idx) => (
  177. <div key={idx} className='p-2 hover:bg-white rounded-lg transition-colors'>
  178. <div className='flex items-center justify-between mb-1'>
  179. <div className='flex items-center gap-2'>
  180. <div
  181. className='w-2 h-2 rounded-full flex-shrink-0'
  182. style={{ backgroundColor: getUptimeStatusColor(monitor.status) }}
  183. />
  184. <span className='text-sm font-medium text-gray-900'>
  185. {monitor.name}
  186. </span>
  187. </div>
  188. <span className='text-xs text-gray-500'>
  189. {((monitor.uptime || 0) * 100).toFixed(2)}%
  190. </span>
  191. </div>
  192. <div className='flex items-center gap-2'>
  193. <span className='text-xs text-gray-500'>
  194. {getUptimeStatusText(monitor.status)}
  195. </span>
  196. <div className='flex-1'>
  197. <Progress
  198. percent={(monitor.uptime || 0) * 100}
  199. showInfo={false}
  200. aria-label={`${monitor.name} uptime`}
  201. stroke={getUptimeStatusColor(monitor.status)}
  202. />
  203. </div>
  204. </div>
  205. </div>
  206. );
  207. return Object.entries(grouped).map(([gname, list]) => (
  208. <div key={gname || 'default'} className='mb-2'>
  209. {gname && (
  210. <>
  211. <div className='text-md font-semibold text-gray-500 px-2 py-1'>
  212. {gname}
  213. </div>
  214. <Divider />
  215. </>
  216. )}
  217. {list.map(renderItem)}
  218. </div>
  219. ));
  220. };
  221. // ========== 数据处理函数 ==========
  222. export const processRawData = (
  223. data,
  224. dataExportDefaultTime,
  225. initializeMaps,
  226. updateMapValue,
  227. ) => {
  228. const result = {
  229. totalQuota: 0,
  230. totalTimes: 0,
  231. totalTokens: 0,
  232. uniqueModels: new Set(),
  233. timePoints: [],
  234. timeQuotaMap: new Map(),
  235. timeTokensMap: new Map(),
  236. timeCountMap: new Map(),
  237. };
  238. // 检查数据是否跨年
  239. const showYear = isDataCrossYear(data.map((item) => item.created_at));
  240. data.forEach((item) => {
  241. result.uniqueModels.add(item.model_name);
  242. result.totalTokens += item.token_used;
  243. result.totalQuota += item.quota;
  244. result.totalTimes += item.count;
  245. const timeKey = timestamp2string1(
  246. item.created_at,
  247. dataExportDefaultTime,
  248. showYear,
  249. );
  250. if (!result.timePoints.includes(timeKey)) {
  251. result.timePoints.push(timeKey);
  252. }
  253. initializeMaps(
  254. timeKey,
  255. result.timeQuotaMap,
  256. result.timeTokensMap,
  257. result.timeCountMap,
  258. );
  259. updateMapValue(result.timeQuotaMap, timeKey, item.quota);
  260. updateMapValue(result.timeTokensMap, timeKey, item.token_used);
  261. updateMapValue(result.timeCountMap, timeKey, item.count);
  262. });
  263. result.timePoints.sort();
  264. return result;
  265. };
  266. export const calculateTrendData = (
  267. timePoints,
  268. timeQuotaMap,
  269. timeTokensMap,
  270. timeCountMap,
  271. dataExportDefaultTime,
  272. ) => {
  273. const quotaTrend = timePoints.map((time) => timeQuotaMap.get(time) || 0);
  274. const tokensTrend = timePoints.map((time) => timeTokensMap.get(time) || 0);
  275. const countTrend = timePoints.map((time) => timeCountMap.get(time) || 0);
  276. const rpmTrend = [];
  277. const tpmTrend = [];
  278. if (timePoints.length >= 2) {
  279. const interval = getTimeInterval(dataExportDefaultTime);
  280. for (let i = 0; i < timePoints.length; i++) {
  281. rpmTrend.push(timeCountMap.get(timePoints[i]) / interval);
  282. tpmTrend.push(timeTokensMap.get(timePoints[i]) / interval);
  283. }
  284. }
  285. return {
  286. balance: [],
  287. usedQuota: [],
  288. requestCount: [],
  289. times: countTrend,
  290. consumeQuota: quotaTrend,
  291. tokens: tokensTrend,
  292. rpm: rpmTrend,
  293. tpm: tpmTrend,
  294. };
  295. };
  296. export const aggregateDataByTimeAndModel = (data, dataExportDefaultTime) => {
  297. const aggregatedData = new Map();
  298. // 检查数据是否跨年
  299. const showYear = isDataCrossYear(data.map((item) => item.created_at));
  300. data.forEach((item) => {
  301. const timeKey = timestamp2string1(
  302. item.created_at,
  303. dataExportDefaultTime,
  304. showYear,
  305. );
  306. const modelKey = item.model_name;
  307. const key = `${timeKey}-${modelKey}`;
  308. if (!aggregatedData.has(key)) {
  309. aggregatedData.set(key, {
  310. time: timeKey,
  311. model: modelKey,
  312. quota: 0,
  313. count: 0,
  314. });
  315. }
  316. const existing = aggregatedData.get(key);
  317. existing.quota += item.quota;
  318. existing.count += item.count;
  319. });
  320. return aggregatedData;
  321. };
  322. export const generateChartTimePoints = (
  323. aggregatedData,
  324. data,
  325. dataExportDefaultTime,
  326. ) => {
  327. let chartTimePoints = Array.from(
  328. new Set([...aggregatedData.values()].map((d) => d.time)),
  329. );
  330. if (chartTimePoints.length < DEFAULTS.MAX_TREND_POINTS) {
  331. const lastTime = Math.max(...data.map((item) => item.created_at));
  332. const interval = getTimeInterval(dataExportDefaultTime, true);
  333. // 生成时间点数组,用于检查是否跨年
  334. const generatedTimestamps = Array.from(
  335. { length: DEFAULTS.MAX_TREND_POINTS },
  336. (_, i) => lastTime - (6 - i) * interval,
  337. );
  338. const showYear = isDataCrossYear(generatedTimestamps);
  339. chartTimePoints = generatedTimestamps.map((ts) =>
  340. timestamp2string1(ts, dataExportDefaultTime, showYear),
  341. );
  342. }
  343. return chartTimePoints;
  344. };