dashboard.js 9.1 KB

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