import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react'; import { initVChartSemiTheme } from '@visactor/vchart-semi-theme'; import { useNavigate } from 'react-router-dom'; import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle } from 'lucide-react'; import { Card, Form, Spin, IconButton, Modal, Avatar, Tabs, TabPane, Empty, Tag, Timeline, Collapse, Progress, Divider } from '@douyinfe/semi-ui'; import { IconRefresh, IconSearch, IconMoneyExchangeStroked, IconHistogram, IconRotate, IconCoinMoneyStroked, IconTextStroked, IconPulse, IconStopwatchStroked, IconTypograph, IconPieChart2Stroked, IconPlus, IconMinus } from '@douyinfe/semi-icons'; import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations'; import { VChart } from '@visactor/react-vchart'; import { API, isAdmin, isMobile, showError, timestamp2string, timestamp2string1, getQuotaWithUnit, modelColorMap, renderNumber, renderQuota, modelToColor, copy, showSuccess, getRelativeTime } from '../../helpers'; import { UserContext } from '../../context/User/index.js'; import { StatusContext } from '../../context/Status/index.js'; import { useTranslation } from 'react-i18next'; const Detail = (props) => { // ========== Hooks - Context ========== const [userState, userDispatch] = useContext(UserContext); const [statusState, statusDispatch] = useContext(StatusContext); // ========== Hooks - Navigation & Translation ========== const { t } = useTranslation(); const navigate = useNavigate(); // ========== Hooks - Refs ========== const formRef = useRef(); const initialized = useRef(false); const apiScrollRef = useRef(null); // ========== Constants & Shared Configurations ========== const CHART_CONFIG = { mode: 'desktop-browser' }; const CARD_PROPS = { shadows: 'always', bordered: false, headerLine: true }; const FORM_FIELD_PROPS = { className: "w-full mb-2 !rounded-lg", size: 'large' }; const ICON_BUTTON_CLASS = "text-white hover:bg-opacity-80 !rounded-full"; const FLEX_CENTER_GAP2 = "flex items-center gap-2"; const ILLUSTRATION_SIZE = { width: 96, height: 96 }; // ========== Constants ========== let now = new Date(); const isAdminUser = isAdmin(); // ========== Panel enable flags ========== const apiInfoEnabled = statusState?.status?.api_info_enabled ?? true; const announcementsEnabled = statusState?.status?.announcements_enabled ?? true; const faqEnabled = statusState?.status?.faq_enabled ?? true; const uptimeEnabled = statusState?.status?.uptime_kuma_enabled ?? true; const hasApiInfoPanel = apiInfoEnabled; const hasInfoPanels = announcementsEnabled || faqEnabled || uptimeEnabled; // ========== Helper Functions ========== const getDefaultTime = useCallback(() => { return localStorage.getItem('data_export_default_time') || 'hour'; }, []); const getTimeInterval = useCallback((timeType, isSeconds = false) => { const intervals = { hour: isSeconds ? 3600 : 60, day: isSeconds ? 86400 : 1440, week: isSeconds ? 604800 : 10080 }; return intervals[timeType] || intervals.hour; }, []); const getInitialTimestamp = useCallback(() => { 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); } }, [getDefaultTime]); const updateMapValue = useCallback((map, key, value) => { if (!map.has(key)) { map.set(key, 0); } map.set(key, map.get(key) + value); }, []); const initializeMaps = useCallback((key, ...maps) => { maps.forEach(map => { if (!map.has(key)) { map.set(key, 0); } }); }, []); const updateChartSpec = useCallback((setterFunc, newData, subtitle, newColors, dataId) => { setterFunc(prev => ({ ...prev, data: [{ id: dataId, values: newData }], title: { ...prev.title, subtext: subtitle, }, color: { specified: newColors, }, })); }, []); const createSectionTitle = useCallback((Icon, text) => (
{text}
), []); const createFormField = useCallback((Component, props) => ( ), []); // ========== Time Options ========== const timeOptions = useMemo(() => [ { label: t('小时'), value: 'hour' }, { label: t('天'), value: 'day' }, { label: t('周'), value: 'week' }, ], [t]); // ========== Hooks - State ========== const [inputs, setInputs] = useState({ username: '', token_name: '', model_name: '', start_timestamp: getInitialTimestamp(), end_timestamp: timestamp2string(now.getTime() / 1000 + 3600), channel: '', data_export_default_time: '', }); const [dataExportDefaultTime, setDataExportDefaultTime] = useState(getDefaultTime()); const [loading, setLoading] = useState(false); const [quotaData, setQuotaData] = useState([]); const [consumeQuota, setConsumeQuota] = useState(0); const [consumeTokens, setConsumeTokens] = useState(0); const [times, setTimes] = useState(0); const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]); const [lineData, setLineData] = useState([]); const [modelColors, setModelColors] = useState({}); const [activeChartTab, setActiveChartTab] = useState('1'); const [showApiScrollHint, setShowApiScrollHint] = useState(false); const [searchModalVisible, setSearchModalVisible] = useState(false); const [trendData, setTrendData] = useState({ balance: [], usedQuota: [], requestCount: [], times: [], consumeQuota: [], tokens: [], rpm: [], tpm: [] }); // ========== Additional Refs for new cards ========== const announcementScrollRef = useRef(null); const faqScrollRef = useRef(null); const uptimeScrollRef = useRef(null); const uptimeTabScrollRefs = useRef({}); // ========== Additional State for scroll hints ========== const [showAnnouncementScrollHint, setShowAnnouncementScrollHint] = useState(false); const [showFaqScrollHint, setShowFaqScrollHint] = useState(false); const [showUptimeScrollHint, setShowUptimeScrollHint] = useState(false); // ========== Uptime data ========== const [uptimeData, setUptimeData] = useState([]); const [uptimeLoading, setUptimeLoading] = useState(false); const [activeUptimeTab, setActiveUptimeTab] = useState(''); // ========== Props Destructuring ========== const { username, model_name, start_timestamp, end_timestamp, channel } = inputs; // ========== Chart Specs State ========== const [spec_pie, setSpecPie] = useState({ type: 'pie', data: [ { id: 'id0', values: pieData, }, ], outerRadius: 0.8, innerRadius: 0.5, padAngle: 0.6, valueField: 'value', categoryField: 'type', pie: { style: { cornerRadius: 10, }, state: { hover: { outerRadius: 0.85, stroke: '#000', lineWidth: 1, }, selected: { outerRadius: 0.85, stroke: '#000', lineWidth: 1, }, }, }, title: { visible: true, text: t('模型调用次数占比'), subtext: `${t('总计')}:${renderNumber(times)}`, }, legends: { visible: true, orient: 'left', }, label: { visible: true, }, tooltip: { mark: { content: [ { key: (datum) => datum['type'], value: (datum) => renderNumber(datum['value']), }, ], }, }, color: { specified: modelColorMap, }, }); const [spec_line, setSpecLine] = useState({ type: 'bar', data: [ { id: 'barData', values: lineData, }, ], xField: 'Time', yField: 'Usage', seriesField: 'Model', stack: true, legends: { visible: true, selectMode: 'single', }, title: { visible: true, text: t('模型消耗分布'), subtext: `${t('总计')}:${renderQuota(consumeQuota, 2)}`, }, bar: { state: { hover: { stroke: '#000', lineWidth: 1, }, }, }, tooltip: { mark: { content: [ { key: (datum) => datum['Model'], value: (datum) => renderQuota(datum['rawQuota'] || 0, 4), }, ], }, dimension: { content: [ { key: (datum) => datum['Model'], value: (datum) => datum['rawQuota'] || 0, }, ], updateContent: (array) => { array.sort((a, b) => b.value - a.value); let sum = 0; for (let i = 0; i < array.length; i++) { if (array[i].key == '其他') { continue; } let value = parseFloat(array[i].value); if (isNaN(value)) { value = 0; } if (array[i].datum && array[i].datum.TimeSum) { sum = array[i].datum.TimeSum; } array[i].value = renderQuota(value, 4); } array.unshift({ key: t('总计'), value: renderQuota(sum, 4), }); return array; }, }, }, color: { specified: modelColorMap, }, }); // ========== Hooks - Memoized Values ========== const performanceMetrics = useMemo(() => { const timeDiff = (Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000; const avgRPM = (times / timeDiff).toFixed(3); const avgTPM = isNaN(consumeTokens / timeDiff) ? '0' : (consumeTokens / timeDiff).toFixed(3); return { avgRPM, avgTPM, timeDiff }; }, [times, consumeTokens, end_timestamp, start_timestamp]); const getGreeting = useMemo(() => { const hours = new Date().getHours(); let greeting = ''; if (hours >= 5 && hours < 12) { greeting = t('早上好'); } else if (hours >= 12 && hours < 14) { greeting = t('中午好'); } else if (hours >= 14 && hours < 18) { greeting = t('下午好'); } else { greeting = t('晚上好'); } const username = userState?.user?.username || ''; return `👋${greeting},${username}`; }, [t, userState?.user?.username]); // ========== Hooks - Callbacks ========== const getTrendSpec = useCallback((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' } }), []); const groupedStatsData = useMemo(() => [ { title: createSectionTitle(Wallet, t('账户数据')), color: 'bg-blue-50', items: [ { title: t('当前余额'), value: renderQuota(userState?.user?.quota), icon: , avatarColor: 'blue', onClick: () => navigate('/console/topup'), trendData: [], trendColor: '#3b82f6' }, { title: t('历史消耗'), value: renderQuota(userState?.user?.used_quota), icon: , avatarColor: 'purple', trendData: [], trendColor: '#8b5cf6' } ] }, { title: createSectionTitle(Activity, t('使用统计')), color: 'bg-green-50', items: [ { title: t('请求次数'), value: userState.user?.request_count, icon: , avatarColor: 'green', trendData: [], trendColor: '#10b981' }, { title: t('统计次数'), value: times, icon: , avatarColor: 'cyan', trendData: trendData.times, trendColor: '#06b6d4' } ] }, { title: createSectionTitle(Zap, t('资源消耗')), color: 'bg-yellow-50', items: [ { title: t('统计额度'), value: renderQuota(consumeQuota), icon: , avatarColor: 'yellow', trendData: trendData.consumeQuota, trendColor: '#f59e0b' }, { title: t('统计Tokens'), value: isNaN(consumeTokens) ? 0 : consumeTokens, icon: , avatarColor: 'pink', trendData: trendData.tokens, trendColor: '#ec4899' } ] }, { title: createSectionTitle(Gauge, t('性能指标')), color: 'bg-indigo-50', items: [ { title: t('平均RPM'), value: performanceMetrics.avgRPM, icon: , avatarColor: 'indigo', trendData: trendData.rpm, trendColor: '#6366f1' }, { title: t('平均TPM'), value: performanceMetrics.avgTPM, icon: , avatarColor: 'orange', trendData: trendData.tpm, trendColor: '#f97316' } ] } ], [ createSectionTitle, t, userState?.user?.quota, userState?.user?.used_quota, userState?.user?.request_count, times, consumeQuota, consumeTokens, trendData, performanceMetrics, navigate ]); const handleCopyUrl = useCallback(async (url) => { if (await copy(url)) { showSuccess(t('复制成功')); } }, [t]); const handleSpeedTest = useCallback((apiUrl) => { const encodedUrl = encodeURIComponent(apiUrl); const speedTestUrl = `https://www.tcptest.cn/http/${encodedUrl}`; window.open(speedTestUrl, '_blank'); }, []); const handleInputChange = useCallback((value, name) => { if (name === 'data_export_default_time') { setDataExportDefaultTime(value); return; } setInputs((inputs) => ({ ...inputs, [name]: value })); }, []); const loadQuotaData = useCallback(async () => { setLoading(true); try { let url = ''; let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localEndTimestamp = Date.parse(end_timestamp) / 1000; if (isAdminUser) { url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`; } else { url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`; } const res = await API.get(url); const { success, message, data } = res.data; if (success) { setQuotaData(data); if (data.length === 0) { data.push({ count: 0, model_name: '无数据', quota: 0, created_at: now.getTime() / 1000, }); } data.sort((a, b) => a.created_at - b.created_at); updateChartData(data); } else { showError(message); } } finally { setLoading(false); } }, [start_timestamp, end_timestamp, username, dataExportDefaultTime, isAdminUser]); const loadUptimeData = useCallback(async () => { setUptimeLoading(true); try { const res = await API.get('/api/uptime/status'); const { success, message, data } = res.data; if (success) { setUptimeData(data || []); if (data && data.length > 0 && !activeUptimeTab) { setActiveUptimeTab(data[0].categoryName); } } else { showError(message); } } catch (err) { console.error(err); } finally { setUptimeLoading(false); } }, [activeUptimeTab]); const refresh = useCallback(async () => { await Promise.all([loadQuotaData(), loadUptimeData()]); }, [loadQuotaData, loadUptimeData]); const handleSearchConfirm = useCallback(() => { refresh(); setSearchModalVisible(false); }, [refresh]); const initChart = useCallback(async () => { await loadQuotaData(); await loadUptimeData(); }, [loadQuotaData, loadUptimeData]); const showSearchModal = useCallback(() => { setSearchModalVisible(true); }, []); const handleCloseModal = useCallback(() => { setSearchModalVisible(false); }, []); // ========== Regular Functions ========== const checkApiScrollable = () => { if (apiScrollRef.current) { const element = apiScrollRef.current; const isScrollable = element.scrollHeight > element.clientHeight; const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - 5; setShowApiScrollHint(isScrollable && !isAtBottom); } }; const handleApiScroll = () => { checkApiScrollable(); }; const checkCardScrollable = (ref, setHintFunction) => { if (ref.current) { const element = ref.current; const isScrollable = element.scrollHeight > element.clientHeight; const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - 5; setHintFunction(isScrollable && !isAtBottom); } }; const handleCardScroll = (ref, setHintFunction) => { checkCardScrollable(ref, setHintFunction); }; // ========== Effects for scroll detection ========== useEffect(() => { const timer = setTimeout(() => { checkApiScrollable(); checkCardScrollable(announcementScrollRef, setShowAnnouncementScrollHint); checkCardScrollable(faqScrollRef, setShowFaqScrollHint); if (uptimeData.length === 1) { checkCardScrollable(uptimeScrollRef, setShowUptimeScrollHint); } else if (uptimeData.length > 1 && activeUptimeTab) { const activeTabRef = uptimeTabScrollRefs.current[activeUptimeTab]; if (activeTabRef) { checkCardScrollable(activeTabRef, setShowUptimeScrollHint); } } }, 100); return () => clearTimeout(timer); }, [uptimeData, activeUptimeTab]); const getUserData = async () => { let res = await API.get(`/api/user/self`); const { success, message, data } = res.data; if (success) { userDispatch({ type: 'login', payload: data }); } else { showError(message); } }; // ========== Data Processing Functions ========== const processRawData = useCallback((data) => { 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; }, [dataExportDefaultTime, initializeMaps, updateMapValue]); const calculateTrendData = useCallback((timePoints, timeQuotaMap, timeTokensMap, timeCountMap) => { 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 }; }, [dataExportDefaultTime, getTimeInterval]); const generateModelColors = useCallback((uniqueModels) => { const newModelColors = {}; Array.from(uniqueModels).forEach((modelName) => { newModelColors[modelName] = modelColorMap[modelName] || modelColors[modelName] || modelToColor(modelName); }); return newModelColors; }, [modelColors]); const aggregateDataByTimeAndModel = useCallback((data) => { 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; }, [dataExportDefaultTime]); const generateChartTimePoints = useCallback((aggregatedData, data) => { let chartTimePoints = Array.from( new Set([...aggregatedData.values()].map((d) => d.time)), ); if (chartTimePoints.length < 7) { const lastTime = Math.max(...data.map((item) => item.created_at)); const interval = getTimeInterval(dataExportDefaultTime, true); chartTimePoints = Array.from({ length: 7 }, (_, i) => timestamp2string1(lastTime - (6 - i) * interval, dataExportDefaultTime), ); } return chartTimePoints; }, [dataExportDefaultTime, getTimeInterval]); const updateChartData = useCallback((data) => { const processedData = processRawData(data); const { totalQuota, totalTimes, totalTokens, uniqueModels, timePoints, timeQuotaMap, timeTokensMap, timeCountMap } = processedData; const trendDataResult = calculateTrendData(timePoints, timeQuotaMap, timeTokensMap, timeCountMap); setTrendData(trendDataResult); const newModelColors = generateModelColors(uniqueModels); setModelColors(newModelColors); const aggregatedData = aggregateDataByTimeAndModel(data); const modelTotals = new Map(); for (let [_, value] of aggregatedData) { updateMapValue(modelTotals, value.model, value.count); } const newPieData = Array.from(modelTotals).map(([model, count]) => ({ type: model, value: count, })).sort((a, b) => b.value - a.value); const chartTimePoints = generateChartTimePoints(aggregatedData, data); let newLineData = []; chartTimePoints.forEach((time) => { let timeData = Array.from(uniqueModels).map((model) => { const key = `${time}-${model}`; const aggregated = aggregatedData.get(key); return { Time: time, Model: model, rawQuota: aggregated?.quota || 0, Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0, }; }); const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0); timeData.sort((a, b) => b.rawQuota - a.rawQuota); timeData = timeData.map((item) => ({ ...item, TimeSum: timeSum })); newLineData.push(...timeData); }); newLineData.sort((a, b) => a.Time.localeCompare(b.Time)); updateChartSpec( setSpecPie, newPieData, `${t('总计')}:${renderNumber(totalTimes)}`, newModelColors, 'id0' ); updateChartSpec( setSpecLine, newLineData, `${t('总计')}:${renderQuota(totalQuota, 2)}`, newModelColors, 'barData' ); setPieData(newPieData); setLineData(newLineData); setConsumeQuota(totalQuota); setTimes(totalTimes); setConsumeTokens(totalTokens); }, [ processRawData, calculateTrendData, generateModelColors, aggregateDataByTimeAndModel, generateChartTimePoints, updateChartSpec, updateMapValue, t ]); // ========== Status Data Management ========== const announcementLegendData = useMemo(() => [ { color: 'grey', label: t('默认'), type: 'default' }, { color: 'blue', label: t('进行中'), type: 'ongoing' }, { color: 'green', label: t('成功'), type: 'success' }, { color: 'orange', label: t('警告'), type: 'warning' }, { color: 'red', label: t('异常'), type: 'error' } ], [t]); const uptimeStatusMap = useMemo(() => ({ 1: { color: '#10b981', label: t('正常'), text: t('可用率') }, // UP 0: { color: '#ef4444', label: t('异常'), text: t('有异常') }, // DOWN 2: { color: '#f59e0b', label: t('高延迟'), text: t('高延迟') }, // PENDING 3: { color: '#3b82f6', label: t('维护中'), text: t('维护中') } // MAINTENANCE }), [t]); const uptimeLegendData = useMemo(() => Object.entries(uptimeStatusMap).map(([status, info]) => ({ status: Number(status), color: info.color, label: info.label })), [uptimeStatusMap]); const getUptimeStatusColor = useCallback((status) => uptimeStatusMap[status]?.color || '#8b9aa7', [uptimeStatusMap]); const getUptimeStatusText = useCallback((status) => uptimeStatusMap[status]?.text || t('未知'), [uptimeStatusMap, t]); const apiInfoData = useMemo(() => { return statusState?.status?.api_info || []; }, [statusState?.status?.api_info]); const announcementData = useMemo(() => { const announcements = statusState?.status?.announcements || []; return announcements.map(item => ({ ...item, time: getRelativeTime(item.publishDate) })); }, [statusState?.status?.announcements]); const faqData = useMemo(() => { return statusState?.status?.faq || []; }, [statusState?.status?.faq]); const renderMonitorList = useCallback((monitors) => { 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)}
)); }, [t, getUptimeStatusColor, getUptimeStatusText]); // ========== Hooks - Effects ========== useEffect(() => { getUserData(); if (!initialized.current) { initVChartSemiTheme({ isWatchingThemeSwitch: true, }); initialized.current = true; initChart(); } }, []); return (

{getGreeting}

} onClick={showSearchModal} className={`bg-green-500 hover:bg-green-600 ${ICON_BUTTON_CLASS}`} /> } onClick={refresh} loading={loading} className={`bg-blue-500 hover:bg-blue-600 ${ICON_BUTTON_CLASS}`} />
{/* 搜索条件Modal */}
{createFormField(Form.DatePicker, { field: 'start_timestamp', label: t('起始时间'), initValue: start_timestamp, value: start_timestamp, type: 'dateTime', name: 'start_timestamp', onChange: (value) => handleInputChange(value, 'start_timestamp') })} {createFormField(Form.DatePicker, { field: 'end_timestamp', label: t('结束时间'), initValue: end_timestamp, value: end_timestamp, type: 'dateTime', name: 'end_timestamp', onChange: (value) => handleInputChange(value, 'end_timestamp') })} {createFormField(Form.Select, { field: 'data_export_default_time', label: t('时间粒度'), initValue: dataExportDefaultTime, placeholder: t('时间粒度'), name: 'data_export_default_time', optionList: timeOptions, onChange: (value) => handleInputChange(value, 'data_export_default_time') })} {isAdminUser && createFormField(Form.Input, { field: 'username', label: t('用户名称'), value: username, placeholder: t('可选值'), name: 'username', onChange: (value) => handleInputChange(value, 'username') })}
{groupedStatsData.map((group, idx) => (
{group.items.map((item, itemIdx) => (
{item.icon}
{item.title}
{item.value}
{item.trendData && item.trendData.length > 0 && (
)}
))}
))}
{t('模型数据分析')}
{t('消耗分布')} } itemKey="1" /> {t('调用次数分布')} } itemKey="2" />
} >
{activeChartTab === '1' ? ( ) : ( )}
{hasApiInfoPanel && ( {t('API信息')}
} >
{apiInfoData.length > 0 ? ( apiInfoData.map((api) => (
{api.route.substring(0, 2)}
} size="small" color="white" shape='circle' onClick={() => handleSpeedTest(api.url)} className="cursor-pointer hover:opacity-80 text-xs" > {t('测速')} {api.route}
handleCopyUrl(api.url)} > {api.url}
{api.description}
)) ) : (
} darkModeImage={} title={t('暂无API信息')} description={t('请联系管理员在系统设置中配置API信息')} />
)}
)}
{/* 系统公告和常见问答卡片 */} {hasInfoPanels && (
{/* 公告卡片 */} {announcementsEnabled && (
{t('系统公告')} {t('显示最新20条')}
{/* 图例 */}
{announcementLegendData.map((legend, index) => (
{legend.label}
))}
} >
handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)} > {announcementData.length > 0 ? ( ) : (
} darkModeImage={} title={t('暂无系统公告')} description={t('请联系管理员在系统设置中配置公告信息')} />
)}
)} {/* 常见问答卡片 */} {faqEnabled && ( {t('常见问答')}
} bodyStyle={{ padding: 0 }} >
handleCardScroll(faqScrollRef, setShowFaqScrollHint)} > {faqData.length > 0 ? ( } collapseIcon={} > {faqData.map((item, index) => (

{item.answer}

))}
) : (
} darkModeImage={} title={t('暂无常见问答')} description={t('请联系管理员在系统设置中配置常见问答')} />
)}
)} {/* 服务可用性卡片 */} {uptimeEnabled && (
{t('服务可用性')}
} onClick={loadUptimeData} loading={uptimeLoading} size="small" theme="borderless" className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full" />
} bodyStyle={{ padding: 0 }} > {/* 内容区域 */}
{uptimeData.length > 0 ? ( uptimeData.length === 1 ? (
handleCardScroll(uptimeScrollRef, setShowUptimeScrollHint)} > {renderMonitorList(uptimeData[0].monitors)}
) : ( {uptimeData.map((group, groupIdx) => { if (!uptimeTabScrollRefs.current[group.categoryName]) { uptimeTabScrollRefs.current[group.categoryName] = React.createRef(); } const tabScrollRef = uptimeTabScrollRefs.current[group.categoryName]; return ( {group.categoryName} {group.monitors ? group.monitors.length : 0} } itemKey={group.categoryName} key={groupIdx} >
handleCardScroll(tabScrollRef, setShowUptimeScrollHint)} > {renderMonitorList(group.monitors)}
); })} ) ) : (
} darkModeImage={} title={t('暂无监控数据')} description={t('请联系管理员在系统设置中配置Uptime')} />
)}
{/* 固定在底部的图例 */} {uptimeData.length > 0 && (
{uptimeLegendData.map((legend, index) => (
{legend.label}
))}
)} )}
)}
); }; export default Detail;