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.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 */}
{groupedStatsData.map((group, idx) => (
{group.items.map((item, itemIdx) => (
{item.icon}
{item.title}
{item.value}
{item.trendData && item.trendData.length > 0 && (
)}
))}
))}
{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) => (
))}
}
>
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) => (
))}
)}
)}
)}
);
};
export default Detail;