|
@@ -1,7 +1,7 @@
|
|
|
import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
|
import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
|
|
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
|
|
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
-import { Wallet, Activity, Zap, Gauge, PieChart, Server } from 'lucide-react';
|
|
|
|
|
|
|
+import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle } from 'lucide-react';
|
|
|
|
|
|
|
|
import {
|
|
import {
|
|
|
Card,
|
|
Card,
|
|
@@ -13,7 +13,9 @@ import {
|
|
|
Tabs,
|
|
Tabs,
|
|
|
TabPane,
|
|
TabPane,
|
|
|
Empty,
|
|
Empty,
|
|
|
- Tag
|
|
|
|
|
|
|
+ Tag,
|
|
|
|
|
+ Timeline,
|
|
|
|
|
+ Collapse
|
|
|
} from '@douyinfe/semi-ui';
|
|
} from '@douyinfe/semi-ui';
|
|
|
import {
|
|
import {
|
|
|
IconRefresh,
|
|
IconRefresh,
|
|
@@ -26,7 +28,9 @@ import {
|
|
|
IconPulse,
|
|
IconPulse,
|
|
|
IconStopwatchStroked,
|
|
IconStopwatchStroked,
|
|
|
IconTypograph,
|
|
IconTypograph,
|
|
|
- IconPieChart2Stroked
|
|
|
|
|
|
|
+ IconPieChart2Stroked,
|
|
|
|
|
+ IconPlus,
|
|
|
|
|
+ IconMinus
|
|
|
} from '@douyinfe/semi-icons';
|
|
} from '@douyinfe/semi-icons';
|
|
|
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
|
|
import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations';
|
|
|
import { VChart } from '@visactor/react-vchart';
|
|
import { VChart } from '@visactor/react-vchart';
|
|
@@ -43,7 +47,8 @@ import {
|
|
|
renderQuota,
|
|
renderQuota,
|
|
|
modelToColor,
|
|
modelToColor,
|
|
|
copy,
|
|
copy,
|
|
|
- showSuccess
|
|
|
|
|
|
|
+ showSuccess,
|
|
|
|
|
+ getRelativeTime
|
|
|
} from '../../helpers';
|
|
} from '../../helpers';
|
|
|
import { UserContext } from '../../context/User/index.js';
|
|
import { UserContext } from '../../context/User/index.js';
|
|
|
import { StatusContext } from '../../context/Status/index.js';
|
|
import { StatusContext } from '../../context/Status/index.js';
|
|
@@ -179,7 +184,7 @@ const Detail = (props) => {
|
|
|
const [times, setTimes] = useState(0);
|
|
const [times, setTimes] = useState(0);
|
|
|
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
|
|
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
|
|
|
const [lineData, setLineData] = useState([]);
|
|
const [lineData, setLineData] = useState([]);
|
|
|
- const [apiInfoData, setApiInfoData] = useState([]);
|
|
|
|
|
|
|
+
|
|
|
const [modelColors, setModelColors] = useState({});
|
|
const [modelColors, setModelColors] = useState({});
|
|
|
const [activeChartTab, setActiveChartTab] = useState('1');
|
|
const [activeChartTab, setActiveChartTab] = useState('1');
|
|
|
const [showApiScrollHint, setShowApiScrollHint] = useState(false);
|
|
const [showApiScrollHint, setShowApiScrollHint] = useState(false);
|
|
@@ -578,6 +583,37 @@ const Detail = (props) => {
|
|
|
checkApiScrollable();
|
|
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);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // ========== Additional Refs for new cards ==========
|
|
|
|
|
+ const announcementScrollRef = useRef(null);
|
|
|
|
|
+ const faqScrollRef = useRef(null);
|
|
|
|
|
+
|
|
|
|
|
+ // ========== Additional State for scroll hints ==========
|
|
|
|
|
+ const [showAnnouncementScrollHint, setShowAnnouncementScrollHint] = useState(false);
|
|
|
|
|
+ const [showFaqScrollHint, setShowFaqScrollHint] = useState(false);
|
|
|
|
|
+
|
|
|
|
|
+ // ========== Effects for scroll detection ==========
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const timer = setTimeout(() => {
|
|
|
|
|
+ checkApiScrollable();
|
|
|
|
|
+ checkCardScrollable(announcementScrollRef, setShowAnnouncementScrollHint);
|
|
|
|
|
+ checkCardScrollable(faqScrollRef, setShowFaqScrollHint);
|
|
|
|
|
+ }, 100);
|
|
|
|
|
+ return () => clearTimeout(timer);
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
const getUserData = async () => {
|
|
const getUserData = async () => {
|
|
|
let res = await API.get(`/api/user/self`);
|
|
let res = await API.get(`/api/user/self`);
|
|
|
const { success, message, data } = res.data;
|
|
const { success, message, data } = res.data;
|
|
@@ -775,6 +811,32 @@ const Detail = (props) => {
|
|
|
generateChartTimePoints, updateChartSpec, updateMapValue, t
|
|
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 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]);
|
|
|
|
|
+
|
|
|
// ========== Hooks - Effects ==========
|
|
// ========== Hooks - Effects ==========
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
getUserData();
|
|
getUserData();
|
|
@@ -787,19 +849,6 @@ const Detail = (props) => {
|
|
|
}
|
|
}
|
|
|
}, []);
|
|
}, []);
|
|
|
|
|
|
|
|
- useEffect(() => {
|
|
|
|
|
- if (statusState?.status?.api_info) {
|
|
|
|
|
- setApiInfoData(statusState.status.api_info);
|
|
|
|
|
- }
|
|
|
|
|
- }, [statusState?.status?.api_info]);
|
|
|
|
|
-
|
|
|
|
|
- useEffect(() => {
|
|
|
|
|
- const timer = setTimeout(() => {
|
|
|
|
|
- checkApiScrollable();
|
|
|
|
|
- }, 100);
|
|
|
|
|
- return () => clearTimeout(timer);
|
|
|
|
|
- }, []);
|
|
|
|
|
-
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className="bg-gray-50 h-full">
|
|
<div className="bg-gray-50 h-full">
|
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
@@ -975,10 +1024,10 @@ const Detail = (props) => {
|
|
|
</div>
|
|
</div>
|
|
|
}
|
|
}
|
|
|
>
|
|
>
|
|
|
- <div className="api-info-container">
|
|
|
|
|
|
|
+ <div className="card-content-container">
|
|
|
<div
|
|
<div
|
|
|
ref={apiScrollRef}
|
|
ref={apiScrollRef}
|
|
|
- className="space-y-3 max-h-96 overflow-y-auto api-info-scroll"
|
|
|
|
|
|
|
+ className="space-y-3 max-h-96 overflow-y-auto card-content-scroll"
|
|
|
onScroll={handleApiScroll}
|
|
onScroll={handleApiScroll}
|
|
|
>
|
|
>
|
|
|
{apiInfoData.length > 0 ? (
|
|
{apiInfoData.length > 0 ? (
|
|
@@ -1023,7 +1072,7 @@ const Detail = (props) => {
|
|
|
<Empty
|
|
<Empty
|
|
|
image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
|
|
image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
|
|
|
darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
|
|
darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
|
|
|
- title={t('暂无API信息配置')}
|
|
|
|
|
|
|
+ title={t('暂无API信息')}
|
|
|
description={t('请联系管理员在系统设置中配置API信息')}
|
|
description={t('请联系管理员在系统设置中配置API信息')}
|
|
|
style={{ padding: '12px' }}
|
|
style={{ padding: '12px' }}
|
|
|
/>
|
|
/>
|
|
@@ -1031,7 +1080,7 @@ const Detail = (props) => {
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
<div
|
|
<div
|
|
|
- className="api-info-fade-indicator"
|
|
|
|
|
|
|
+ className="card-content-fade-indicator"
|
|
|
style={{ opacity: showApiScrollHint ? 1 : 0 }}
|
|
style={{ opacity: showApiScrollHint ? 1 : 0 }}
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
@@ -1039,6 +1088,129 @@ const Detail = (props) => {
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 系统公告和常见问答卡片 */}
|
|
|
|
|
+ {!statusState?.status?.self_use_mode_enabled && (
|
|
|
|
|
+ <div className="mb-4">
|
|
|
|
|
+ <div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
|
|
|
|
+ {/* 公告卡片 */}
|
|
|
|
|
+ <Card
|
|
|
|
|
+ {...CARD_PROPS}
|
|
|
|
|
+ className="shadow-sm !rounded-2xl lg:col-span-2"
|
|
|
|
|
+ title={
|
|
|
|
|
+ <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Bell size={16} />
|
|
|
|
|
+ {t('系统公告')}
|
|
|
|
|
+ <Tag size="small" color="grey" shape="circle">
|
|
|
|
|
+ {t('显示最新20条')}
|
|
|
|
|
+ </Tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/* 图例 */}
|
|
|
|
|
+ <div className="flex flex-wrap gap-3 text-xs">
|
|
|
|
|
+ {announcementLegendData.map((legend, index) => (
|
|
|
|
|
+ <div key={index} className="flex items-center gap-1">
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="w-2 h-2 rounded-full"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ backgroundColor: legend.color === 'grey' ? '#8b9aa7' :
|
|
|
|
|
+ legend.color === 'blue' ? '#3b82f6' :
|
|
|
|
|
+ legend.color === 'green' ? '#10b981' :
|
|
|
|
|
+ legend.color === 'orange' ? '#f59e0b' :
|
|
|
|
|
+ legend.color === 'red' ? '#ef4444' : '#8b9aa7'
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ <span className="text-gray-600">{legend.label}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className="card-content-container">
|
|
|
|
|
+ <div
|
|
|
|
|
+ ref={announcementScrollRef}
|
|
|
|
|
+ className="p-2 max-h-96 overflow-y-auto card-content-scroll"
|
|
|
|
|
+ onScroll={() => handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)}
|
|
|
|
|
+ >
|
|
|
|
|
+ {announcementData.length > 0 ? (
|
|
|
|
|
+ <Timeline
|
|
|
|
|
+ mode="alternate"
|
|
|
|
|
+ dataSource={announcementData}
|
|
|
|
|
+ />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="flex justify-center items-center py-8">
|
|
|
|
|
+ <Empty
|
|
|
|
|
+ image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
|
|
|
|
|
+ darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
|
|
|
|
|
+ title={t('暂无系统公告')}
|
|
|
|
|
+ description={t('请联系管理员在系统设置中配置公告信息')}
|
|
|
|
|
+ style={{ padding: '12px' }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="card-content-fade-indicator"
|
|
|
|
|
+ style={{ opacity: showAnnouncementScrollHint ? 1 : 0 }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 常见问答卡片 */}
|
|
|
|
|
+ <Card
|
|
|
|
|
+ {...CARD_PROPS}
|
|
|
|
|
+ className="shadow-sm !rounded-2xl lg:col-span-2"
|
|
|
|
|
+ title={
|
|
|
|
|
+ <div className={FLEX_CENTER_GAP2}>
|
|
|
|
|
+ <HelpCircle size={16} />
|
|
|
|
|
+ {t('常见问答')}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className="card-content-container">
|
|
|
|
|
+ <div
|
|
|
|
|
+ ref={faqScrollRef}
|
|
|
|
|
+ className="p-2 max-h-96 overflow-y-auto card-content-scroll"
|
|
|
|
|
+ onScroll={() => handleCardScroll(faqScrollRef, setShowFaqScrollHint)}
|
|
|
|
|
+ >
|
|
|
|
|
+ {faqData.length > 0 ? (
|
|
|
|
|
+ <Collapse
|
|
|
|
|
+ accordion
|
|
|
|
|
+ expandIcon={<IconPlus />}
|
|
|
|
|
+ collapseIcon={<IconMinus />}
|
|
|
|
|
+ >
|
|
|
|
|
+ {faqData.map((item, index) => (
|
|
|
|
|
+ <Collapse.Panel
|
|
|
|
|
+ key={index}
|
|
|
|
|
+ header={item.title}
|
|
|
|
|
+ itemKey={index.toString()}
|
|
|
|
|
+ >
|
|
|
|
|
+ <p>{item.content}</p>
|
|
|
|
|
+ </Collapse.Panel>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Collapse>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="flex justify-center items-center py-8">
|
|
|
|
|
+ <Empty
|
|
|
|
|
+ image={<IllustrationConstruction style={{ width: 80, height: 80 }} />}
|
|
|
|
|
+ darkModeImage={<IllustrationConstructionDark style={{ width: 80, height: 80 }} />}
|
|
|
|
|
+ title={t('暂无常见问答')}
|
|
|
|
|
+ description={t('请联系管理员在系统设置中配置常见问答')}
|
|
|
|
|
+ style={{ padding: '12px' }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="card-content-fade-indicator"
|
|
|
|
|
+ style={{ opacity: showFaqScrollHint ? 1 : 0 }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</Spin>
|
|
</Spin>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|