| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- /*
- 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 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) => (
- <div className='flex items-center gap-2'>
- <Icon size={16} />
- {text}
- </div>
- );
- export const createFormField = (Component, props, FORM_FIELD_PROPS) => (
- <Component {...FORM_FIELD_PROPS} {...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 (
- <div className='flex justify-center items-center py-4'>
- <Empty
- image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
- darkModeImage={
- <IllustrationConstructionDark style={ILLUSTRATION_SIZE} />
- }
- title={t('暂无监控数据')}
- />
- </div>
- );
- }
- const grouped = {};
- monitors.forEach((m) => {
- const g = m.group || '';
- if (!grouped[g]) grouped[g] = [];
- grouped[g].push(m);
- });
- const renderItem = (monitor, idx) => (
- <div key={idx} className='p-2 hover:bg-white rounded-lg transition-colors'>
- <div className='flex items-center justify-between mb-1'>
- <div className='flex items-center gap-2'>
- <div
- className='w-2 h-2 rounded-full flex-shrink-0'
- style={{ backgroundColor: getUptimeStatusColor(monitor.status) }}
- />
- <span className='text-sm font-medium text-gray-900'>
- {monitor.name}
- </span>
- </div>
- <span className='text-xs text-gray-500'>
- {((monitor.uptime || 0) * 100).toFixed(2)}%
- </span>
- </div>
- <div className='flex items-center gap-2'>
- <span className='text-xs text-gray-500'>
- {getUptimeStatusText(monitor.status)}
- </span>
- <div className='flex-1'>
- <Progress
- percent={(monitor.uptime || 0) * 100}
- showInfo={false}
- aria-label={`${monitor.name} uptime`}
- stroke={getUptimeStatusColor(monitor.status)}
- />
- </div>
- </div>
- </div>
- );
- return Object.entries(grouped).map(([gname, list]) => (
- <div key={gname || 'default'} className='mb-2'>
- {gname && (
- <>
- <div className='text-md font-semibold text-gray-500 px-2 py-1'>
- {gname}
- </div>
- <Divider />
- </>
- )}
- {list.map(renderItem)}
- </div>
- ));
- };
- // ========== 数据处理函数 ==========
- 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;
- };
|