/* 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 . For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; import { Progress, Divider, Empty } from '@douyinfe/semi-ui'; import { IllustrationConstruction, IllustrationConstructionDark, } from '@douyinfe/semi-illustrations'; import { timestamp2string, timestamp2string1, copy, showSuccess, } from './utils'; import { STORAGE_KEYS, DEFAULT_TIME_INTERVALS, DEFAULTS, ILLUSTRATION_SIZE, } from '../constants/dashboard.constants'; // ========== 时间相关工具函数 ========== export const getDefaultTime = () => { return localStorage.getItem(STORAGE_KEYS.DATA_EXPORT_DEFAULT_TIME) || 'hour'; }; export const getTimeInterval = (timeType, isSeconds = false) => { const intervals = DEFAULT_TIME_INTERVALS[timeType] || DEFAULT_TIME_INTERVALS.hour; return isSeconds ? intervals.seconds : intervals.minutes; }; export const getInitialTimestamp = () => { const defaultTime = getDefaultTime(); const now = new Date().getTime() / 1000; switch (defaultTime) { case 'hour': return timestamp2string(now - 86400); case 'week': return timestamp2string(now - 86400 * 30); default: return timestamp2string(now - 86400 * 7); } }; // ========== 数据处理工具函数 ========== export const updateMapValue = (map, key, value) => { if (!map.has(key)) { map.set(key, 0); } map.set(key, map.get(key) + value); }; export const initializeMaps = (key, ...maps) => { maps.forEach((map) => { if (!map.has(key)) { map.set(key, 0); } }); }; // ========== 图表相关工具函数 ========== export const updateChartSpec = ( setterFunc, newData, subtitle, newColors, dataId, ) => { setterFunc((prev) => ({ ...prev, data: [{ id: dataId, values: newData }], title: { ...prev.title, subtext: subtitle, }, color: { specified: newColors, }, })); }; export const getTrendSpec = (data, color) => ({ type: 'line', data: [{ id: 'trend', values: data.map((val, idx) => ({ x: idx, y: val })) }], xField: 'x', yField: 'y', height: 40, width: 100, axes: [ { orient: 'bottom', visible: false, }, { orient: 'left', visible: false, }, ], padding: 0, autoFit: false, legends: { visible: false }, tooltip: { visible: false }, crosshair: { visible: false }, line: { style: { stroke: color, lineWidth: 2, }, }, point: { visible: false, }, background: { fill: 'transparent', }, }); // ========== UI 工具函数 ========== export const createSectionTitle = (Icon, text) => (
{text}
); export const createFormField = (Component, props, FORM_FIELD_PROPS) => ( ); // ========== 操作处理函数 ========== export const handleCopyUrl = async (url, t) => { if (await copy(url)) { showSuccess(t('复制成功')); } }; export const handleSpeedTest = (apiUrl) => { const encodedUrl = encodeURIComponent(apiUrl); const speedTestUrl = `https://www.tcptest.cn/http/${encodedUrl}`; window.open(speedTestUrl, '_blank', 'noopener,noreferrer'); }; // ========== 状态映射函数 ========== export const getUptimeStatusColor = (status, uptimeStatusMap) => uptimeStatusMap[status]?.color || '#8b9aa7'; export const getUptimeStatusText = (status, uptimeStatusMap, t) => uptimeStatusMap[status]?.text || t('未知'); // ========== 监控列表渲染函数 ========== export const renderMonitorList = ( monitors, getUptimeStatusColor, getUptimeStatusText, t, ) => { if (!monitors || monitors.length === 0) { return (
} darkModeImage={ } title={t('暂无监控数据')} />
); } const grouped = {}; monitors.forEach((m) => { const g = m.group || ''; if (!grouped[g]) grouped[g] = []; grouped[g].push(m); }); const renderItem = (monitor, idx) => (
{monitor.name}
{((monitor.uptime || 0) * 100).toFixed(2)}%
{getUptimeStatusText(monitor.status)}
); return Object.entries(grouped).map(([gname, list]) => (
{gname && ( <>
{gname}
)} {list.map(renderItem)}
)); }; // ========== 数据处理函数 ========== export const processRawData = ( data, dataExportDefaultTime, initializeMaps, updateMapValue, ) => { const result = { totalQuota: 0, totalTimes: 0, totalTokens: 0, uniqueModels: new Set(), timePoints: [], timeQuotaMap: new Map(), timeTokensMap: new Map(), timeCountMap: new Map(), }; data.forEach((item) => { result.uniqueModels.add(item.model_name); result.totalTokens += item.token_used; result.totalQuota += item.quota; result.totalTimes += item.count; const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime); if (!result.timePoints.includes(timeKey)) { result.timePoints.push(timeKey); } initializeMaps( timeKey, result.timeQuotaMap, result.timeTokensMap, result.timeCountMap, ); updateMapValue(result.timeQuotaMap, timeKey, item.quota); updateMapValue(result.timeTokensMap, timeKey, item.token_used); updateMapValue(result.timeCountMap, timeKey, item.count); }); result.timePoints.sort(); return result; }; export const calculateTrendData = ( timePoints, timeQuotaMap, timeTokensMap, timeCountMap, dataExportDefaultTime, ) => { const quotaTrend = timePoints.map((time) => timeQuotaMap.get(time) || 0); const tokensTrend = timePoints.map((time) => timeTokensMap.get(time) || 0); const countTrend = timePoints.map((time) => timeCountMap.get(time) || 0); const rpmTrend = []; const tpmTrend = []; if (timePoints.length >= 2) { const interval = getTimeInterval(dataExportDefaultTime); for (let i = 0; i < timePoints.length; i++) { rpmTrend.push(timeCountMap.get(timePoints[i]) / interval); tpmTrend.push(timeTokensMap.get(timePoints[i]) / interval); } } return { balance: [], usedQuota: [], requestCount: [], times: countTrend, consumeQuota: quotaTrend, tokens: tokensTrend, rpm: rpmTrend, tpm: tpmTrend, }; }; export const aggregateDataByTimeAndModel = (data, dataExportDefaultTime) => { const aggregatedData = new Map(); data.forEach((item) => { const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime); const modelKey = item.model_name; const key = `${timeKey}-${modelKey}`; if (!aggregatedData.has(key)) { aggregatedData.set(key, { time: timeKey, model: modelKey, quota: 0, count: 0, }); } const existing = aggregatedData.get(key); existing.quota += item.quota; existing.count += item.count; }); return aggregatedData; }; export const generateChartTimePoints = ( aggregatedData, data, dataExportDefaultTime, ) => { let chartTimePoints = Array.from( new Set([...aggregatedData.values()].map((d) => d.time)), ); if (chartTimePoints.length < DEFAULTS.MAX_TREND_POINTS) { const lastTime = Math.max(...data.map((item) => item.created_at)); const interval = getTimeInterval(dataExportDefaultTime, true); chartTimePoints = Array.from( { length: DEFAULTS.MAX_TREND_POINTS }, (_, i) => timestamp2string1(lastTime - (6 - i) * interval, dataExportDefaultTime), ); } return chartTimePoints; };