♻️ refactor: Extract scroll effect logic into reusable ScrollableContainer component
- Create new ScrollableContainer component in @/components/common/ui
- Provides automatic scroll detection and fade indicator
- Supports customizable height, styling, and event callbacks
- Includes comprehensive PropTypes for type safety
- Optimized with useCallback for better performance
- Refactor Detail page to use ScrollableContainer
- Remove manual scroll detection functions (checkApiScrollable, checkCardScrollable)
- Remove scroll event handlers (handleApiScroll, handleCardScroll)
- Remove scroll-related refs and state variables
- Replace all card scroll containers with ScrollableContainer component
* API info card
* System announcements card
* FAQ card
* Uptime monitoring card (both single and multi-tab scenarios)
- Benefits:
- Improved code reusability and maintainability
- Reduced code duplication across components
- Consistent scroll behavior throughout the application
- Easier to maintain and extend scroll functionality
Breaking changes: None
Migration: Existing scroll behavior is preserved with no user-facing changes
This commit is contained in:
131
web/src/components/common/ui/ScrollableContainer.js
Normal file
131
web/src/components/common/ui/ScrollableContainer.js
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
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, { useRef, useState, useEffect, useCallback } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScrollableContainer 可滚动容器组件
|
||||||
|
*
|
||||||
|
* 提供自动检测滚动状态和显示渐变指示器的功能
|
||||||
|
* 当内容超出容器高度且未滚动到底部时,会显示底部渐变指示器
|
||||||
|
*/
|
||||||
|
const ScrollableContainer = ({
|
||||||
|
children,
|
||||||
|
maxHeight = '24rem',
|
||||||
|
className = '',
|
||||||
|
contentClassName = 'p-2',
|
||||||
|
fadeIndicatorClassName = '',
|
||||||
|
checkInterval = 100,
|
||||||
|
scrollThreshold = 5,
|
||||||
|
onScroll,
|
||||||
|
onScrollStateChange,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const scrollRef = useRef(null);
|
||||||
|
const [showScrollHint, setShowScrollHint] = useState(false);
|
||||||
|
|
||||||
|
// 检查是否可滚动且未滚动到底部
|
||||||
|
const checkScrollable = useCallback(() => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
const element = scrollRef.current;
|
||||||
|
const isScrollable = element.scrollHeight > element.clientHeight;
|
||||||
|
const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - scrollThreshold;
|
||||||
|
const shouldShowHint = isScrollable && !isAtBottom;
|
||||||
|
|
||||||
|
setShowScrollHint(shouldShowHint);
|
||||||
|
|
||||||
|
// 通知父组件滚动状态变化
|
||||||
|
if (onScrollStateChange) {
|
||||||
|
onScrollStateChange({
|
||||||
|
isScrollable,
|
||||||
|
isAtBottom,
|
||||||
|
showScrollHint: shouldShowHint,
|
||||||
|
scrollTop: element.scrollTop,
|
||||||
|
scrollHeight: element.scrollHeight,
|
||||||
|
clientHeight: element.clientHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [scrollThreshold, onScrollStateChange]);
|
||||||
|
|
||||||
|
// 处理滚动事件
|
||||||
|
const handleScroll = useCallback((e) => {
|
||||||
|
checkScrollable();
|
||||||
|
if (onScroll) {
|
||||||
|
onScroll(e);
|
||||||
|
}
|
||||||
|
}, [checkScrollable, onScroll]);
|
||||||
|
|
||||||
|
// 初始检查和内容变化时检查
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
checkScrollable();
|
||||||
|
}, checkInterval);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [children, checkScrollable, checkInterval]);
|
||||||
|
|
||||||
|
// 暴露检查方法给父组件
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.checkScrollable = checkScrollable;
|
||||||
|
}
|
||||||
|
}, [checkScrollable]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`card-content-container ${className}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className={`overflow-y-auto card-content-scroll ${contentClassName}`}
|
||||||
|
style={{ maxHeight }}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`card-content-fade-indicator ${fadeIndicatorClassName}`}
|
||||||
|
style={{ opacity: showScrollHint ? 1 : 0 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ScrollableContainer.propTypes = {
|
||||||
|
// 子组件内容
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
|
||||||
|
// 样式相关
|
||||||
|
maxHeight: PropTypes.string,
|
||||||
|
className: PropTypes.string,
|
||||||
|
contentClassName: PropTypes.string,
|
||||||
|
fadeIndicatorClassName: PropTypes.string,
|
||||||
|
|
||||||
|
// 行为配置
|
||||||
|
checkInterval: PropTypes.number,
|
||||||
|
scrollThreshold: PropTypes.number,
|
||||||
|
|
||||||
|
// 事件回调
|
||||||
|
onScroll: PropTypes.func,
|
||||||
|
onScrollStateChange: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScrollableContainer;
|
||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Skeleton
|
Skeleton
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
|
import ScrollableContainer from '../../components/common/ui/ScrollableContainer';
|
||||||
import {
|
import {
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
@@ -91,7 +92,6 @@ const Detail = (props) => {
|
|||||||
// ========== Hooks - Refs ==========
|
// ========== Hooks - Refs ==========
|
||||||
const formRef = useRef();
|
const formRef = useRef();
|
||||||
const initialized = useRef(false);
|
const initialized = useRef(false);
|
||||||
const apiScrollRef = useRef(null);
|
|
||||||
|
|
||||||
// ========== Constants & Shared Configurations ==========
|
// ========== Constants & Shared Configurations ==========
|
||||||
const CHART_CONFIG = { mode: 'desktop-browser' };
|
const CHART_CONFIG = { mode: 'desktop-browser' };
|
||||||
@@ -224,7 +224,6 @@ const Detail = (props) => {
|
|||||||
|
|
||||||
const [modelColors, setModelColors] = useState({});
|
const [modelColors, setModelColors] = useState({});
|
||||||
const [activeChartTab, setActiveChartTab] = useState('1');
|
const [activeChartTab, setActiveChartTab] = useState('1');
|
||||||
const [showApiScrollHint, setShowApiScrollHint] = useState(false);
|
|
||||||
const [searchModalVisible, setSearchModalVisible] = useState(false);
|
const [searchModalVisible, setSearchModalVisible] = useState(false);
|
||||||
|
|
||||||
const [trendData, setTrendData] = useState({
|
const [trendData, setTrendData] = useState({
|
||||||
@@ -238,16 +237,7 @@ const Detail = (props) => {
|
|||||||
tpm: []
|
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 ==========
|
// ========== Uptime data ==========
|
||||||
const [uptimeData, setUptimeData] = useState([]);
|
const [uptimeData, setUptimeData] = useState([]);
|
||||||
@@ -728,51 +718,9 @@ const Detail = (props) => {
|
|||||||
setSearchModalVisible(false);
|
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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -1360,82 +1308,72 @@ const Detail = (props) => {
|
|||||||
}
|
}
|
||||||
bodyStyle={{ padding: 0 }}
|
bodyStyle={{ padding: 0 }}
|
||||||
>
|
>
|
||||||
<div className="card-content-container">
|
<ScrollableContainer maxHeight="24rem">
|
||||||
<div
|
{apiInfoData.length > 0 ? (
|
||||||
ref={apiScrollRef}
|
apiInfoData.map((api) => (
|
||||||
className="p-2 max-h-96 overflow-y-auto card-content-scroll"
|
<>
|
||||||
onScroll={handleApiScroll}
|
<div key={api.id} className="flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer">
|
||||||
>
|
<div className="flex-shrink-0 mr-3">
|
||||||
{apiInfoData.length > 0 ? (
|
<Avatar
|
||||||
apiInfoData.map((api) => (
|
size="extra-small"
|
||||||
<>
|
color={api.color}
|
||||||
<div key={api.id} className="flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer">
|
>
|
||||||
<div className="flex-shrink-0 mr-3">
|
{api.route.substring(0, 2)}
|
||||||
<Avatar
|
</Avatar>
|
||||||
size="extra-small"
|
</div>
|
||||||
color={api.color}
|
<div className="flex-1">
|
||||||
>
|
<div className="flex flex-wrap items-center justify-between mb-1 w-full gap-2">
|
||||||
{api.route.substring(0, 2)}
|
<span className="text-sm font-medium text-gray-900 !font-bold break-all">
|
||||||
</Avatar>
|
{api.route}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1 mt-1 lg:mt-0">
|
||||||
|
<Tag
|
||||||
|
prefixIcon={<Gauge size={12} />}
|
||||||
|
size="small"
|
||||||
|
color="white"
|
||||||
|
shape='circle'
|
||||||
|
onClick={() => handleSpeedTest(api.url)}
|
||||||
|
className="cursor-pointer hover:opacity-80 text-xs"
|
||||||
|
>
|
||||||
|
{t('测速')}
|
||||||
|
</Tag>
|
||||||
|
<Tag
|
||||||
|
prefixIcon={<ExternalLink size={12} />}
|
||||||
|
size="small"
|
||||||
|
color="white"
|
||||||
|
shape='circle'
|
||||||
|
onClick={() => window.open(api.url, '_blank', 'noopener,noreferrer')}
|
||||||
|
className="cursor-pointer hover:opacity-80 text-xs"
|
||||||
|
>
|
||||||
|
{t('跳转')}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div
|
||||||
<div className="flex flex-wrap items-center justify-between mb-1 w-full gap-2">
|
className="!text-semi-color-primary break-all cursor-pointer hover:underline mb-1"
|
||||||
<span className="text-sm font-medium text-gray-900 !font-bold break-all">
|
onClick={() => handleCopyUrl(api.url)}
|
||||||
{api.route}
|
>
|
||||||
</span>
|
{api.url}
|
||||||
<div className="flex items-center gap-1 mt-1 lg:mt-0">
|
</div>
|
||||||
<Tag
|
<div className="text-gray-500">
|
||||||
prefixIcon={<Gauge size={12} />}
|
{api.description}
|
||||||
size="small"
|
|
||||||
color="white"
|
|
||||||
shape='circle'
|
|
||||||
onClick={() => handleSpeedTest(api.url)}
|
|
||||||
className="cursor-pointer hover:opacity-80 text-xs"
|
|
||||||
>
|
|
||||||
{t('测速')}
|
|
||||||
</Tag>
|
|
||||||
<Tag
|
|
||||||
prefixIcon={<ExternalLink size={12} />}
|
|
||||||
size="small"
|
|
||||||
color="white"
|
|
||||||
shape='circle'
|
|
||||||
onClick={() => window.open(api.url, '_blank', 'noopener,noreferrer')}
|
|
||||||
className="cursor-pointer hover:opacity-80 text-xs"
|
|
||||||
>
|
|
||||||
{t('跳转')}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="!text-semi-color-primary break-all cursor-pointer hover:underline mb-1"
|
|
||||||
onClick={() => handleCopyUrl(api.url)}
|
|
||||||
>
|
|
||||||
{api.url}
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-500">
|
|
||||||
{api.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
</div>
|
||||||
</>
|
<Divider />
|
||||||
))
|
</>
|
||||||
) : (
|
))
|
||||||
<div className="flex justify-center items-center py-8">
|
) : (
|
||||||
<Empty
|
<div className="flex justify-center items-center py-8">
|
||||||
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
<Empty
|
||||||
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||||
title={t('暂无API信息')}
|
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
||||||
description={t('请联系管理员在系统设置中配置API信息')}
|
title={t('暂无API信息')}
|
||||||
/>
|
description={t('请联系管理员在系统设置中配置API信息')}
|
||||||
</div>
|
/>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div
|
</ScrollableContainer>
|
||||||
className="card-content-fade-indicator"
|
|
||||||
style={{ opacity: showApiScrollHint ? 1 : 0 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1482,50 +1420,40 @@ const Detail = (props) => {
|
|||||||
}
|
}
|
||||||
bodyStyle={{ padding: 0 }}
|
bodyStyle={{ padding: 0 }}
|
||||||
>
|
>
|
||||||
<div className="card-content-container">
|
<ScrollableContainer maxHeight="24rem">
|
||||||
<div
|
{announcementData.length > 0 ? (
|
||||||
ref={announcementScrollRef}
|
<Timeline mode="alternate">
|
||||||
className="p-2 max-h-96 overflow-y-auto card-content-scroll"
|
{announcementData.map((item, idx) => (
|
||||||
onScroll={() => handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)}
|
<Timeline.Item
|
||||||
>
|
key={idx}
|
||||||
{announcementData.length > 0 ? (
|
type={item.type || 'default'}
|
||||||
<Timeline mode="alternate">
|
time={item.time}
|
||||||
{announcementData.map((item, idx) => (
|
>
|
||||||
<Timeline.Item
|
<div>
|
||||||
key={idx}
|
<div
|
||||||
type={item.type || 'default'}
|
dangerouslySetInnerHTML={{ __html: marked.parse(item.content || '') }}
|
||||||
time={item.time}
|
/>
|
||||||
>
|
{item.extra && (
|
||||||
<div>
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{ __html: marked.parse(item.content || '') }}
|
className="text-xs text-gray-500"
|
||||||
|
dangerouslySetInnerHTML={{ __html: marked.parse(item.extra) }}
|
||||||
/>
|
/>
|
||||||
{item.extra && (
|
)}
|
||||||
<div
|
</div>
|
||||||
className="text-xs text-gray-500"
|
</Timeline.Item>
|
||||||
dangerouslySetInnerHTML={{ __html: marked.parse(item.extra) }}
|
))}
|
||||||
/>
|
</Timeline>
|
||||||
)}
|
) : (
|
||||||
</div>
|
<div className="flex justify-center items-center py-8">
|
||||||
</Timeline.Item>
|
<Empty
|
||||||
))}
|
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||||
</Timeline>
|
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
||||||
) : (
|
title={t('暂无系统公告')}
|
||||||
<div className="flex justify-center items-center py-8">
|
description={t('请联系管理员在系统设置中配置公告信息')}
|
||||||
<Empty
|
/>
|
||||||
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
</div>
|
||||||
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
)}
|
||||||
title={t('暂无系统公告')}
|
</ScrollableContainer>
|
||||||
description={t('请联系管理员在系统设置中配置公告信息')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="card-content-fade-indicator"
|
|
||||||
style={{ opacity: showAnnouncementScrollHint ? 1 : 0 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1542,46 +1470,36 @@ const Detail = (props) => {
|
|||||||
}
|
}
|
||||||
bodyStyle={{ padding: 0 }}
|
bodyStyle={{ padding: 0 }}
|
||||||
>
|
>
|
||||||
<div className="card-content-container">
|
<ScrollableContainer maxHeight="24rem">
|
||||||
<div
|
{faqData.length > 0 ? (
|
||||||
ref={faqScrollRef}
|
<Collapse
|
||||||
className="p-2 max-h-96 overflow-y-auto card-content-scroll"
|
accordion
|
||||||
onScroll={() => handleCardScroll(faqScrollRef, setShowFaqScrollHint)}
|
expandIcon={<IconPlus />}
|
||||||
>
|
collapseIcon={<IconMinus />}
|
||||||
{faqData.length > 0 ? (
|
>
|
||||||
<Collapse
|
{faqData.map((item, index) => (
|
||||||
accordion
|
<Collapse.Panel
|
||||||
expandIcon={<IconPlus />}
|
key={index}
|
||||||
collapseIcon={<IconMinus />}
|
header={item.question}
|
||||||
>
|
itemKey={index.toString()}
|
||||||
{faqData.map((item, index) => (
|
>
|
||||||
<Collapse.Panel
|
<div
|
||||||
key={index}
|
dangerouslySetInnerHTML={{ __html: marked.parse(item.answer || '') }}
|
||||||
header={item.question}
|
/>
|
||||||
itemKey={index.toString()}
|
</Collapse.Panel>
|
||||||
>
|
))}
|
||||||
<div
|
</Collapse>
|
||||||
dangerouslySetInnerHTML={{ __html: marked.parse(item.answer || '') }}
|
) : (
|
||||||
/>
|
<div className="flex justify-center items-center py-8">
|
||||||
</Collapse.Panel>
|
<Empty
|
||||||
))}
|
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
||||||
</Collapse>
|
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
||||||
) : (
|
title={t('暂无常见问答')}
|
||||||
<div className="flex justify-center items-center py-8">
|
description={t('请联系管理员在系统设置中配置常见问答')}
|
||||||
<Empty
|
/>
|
||||||
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
|
</div>
|
||||||
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
|
)}
|
||||||
title={t('暂无常见问答')}
|
</ScrollableContainer>
|
||||||
description={t('请联系管理员在系统设置中配置常见问答')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="card-content-fade-indicator"
|
|
||||||
style={{ opacity: showFaqScrollHint ? 1 : 0 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1614,19 +1532,9 @@ const Detail = (props) => {
|
|||||||
<Spin spinning={uptimeLoading}>
|
<Spin spinning={uptimeLoading}>
|
||||||
{uptimeData.length > 0 ? (
|
{uptimeData.length > 0 ? (
|
||||||
uptimeData.length === 1 ? (
|
uptimeData.length === 1 ? (
|
||||||
<div className="card-content-container">
|
<ScrollableContainer maxHeight="24rem">
|
||||||
<div
|
{renderMonitorList(uptimeData[0].monitors)}
|
||||||
ref={uptimeScrollRef}
|
</ScrollableContainer>
|
||||||
className="p-2 max-h-[24rem] overflow-y-auto card-content-scroll"
|
|
||||||
onScroll={() => handleCardScroll(uptimeScrollRef, setShowUptimeScrollHint)}
|
|
||||||
>
|
|
||||||
{renderMonitorList(uptimeData[0].monitors)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="card-content-fade-indicator"
|
|
||||||
style={{ opacity: showUptimeScrollHint ? 1 : 0 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<Tabs
|
<Tabs
|
||||||
type="card"
|
type="card"
|
||||||
@@ -1635,46 +1543,29 @@ const Detail = (props) => {
|
|||||||
onChange={setActiveUptimeTab}
|
onChange={setActiveUptimeTab}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
{uptimeData.map((group, groupIdx) => {
|
{uptimeData.map((group, groupIdx) => (
|
||||||
if (!uptimeTabScrollRefs.current[group.categoryName]) {
|
<TabPane
|
||||||
uptimeTabScrollRefs.current[group.categoryName] = React.createRef();
|
tab={
|
||||||
}
|
<span className="flex items-center gap-2">
|
||||||
const tabScrollRef = uptimeTabScrollRefs.current[group.categoryName];
|
<Gauge size={14} />
|
||||||
|
{group.categoryName}
|
||||||
return (
|
<Tag
|
||||||
<TabPane
|
color={activeUptimeTab === group.categoryName ? 'red' : 'grey'}
|
||||||
tab={
|
size='small'
|
||||||
<span className="flex items-center gap-2">
|
shape='circle'
|
||||||
<Gauge size={14} />
|
|
||||||
{group.categoryName}
|
|
||||||
<Tag
|
|
||||||
color={activeUptimeTab === group.categoryName ? 'red' : 'grey'}
|
|
||||||
size='small'
|
|
||||||
shape='circle'
|
|
||||||
>
|
|
||||||
{group.monitors ? group.monitors.length : 0}
|
|
||||||
</Tag>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
itemKey={group.categoryName}
|
|
||||||
key={groupIdx}
|
|
||||||
>
|
|
||||||
<div className="card-content-container">
|
|
||||||
<div
|
|
||||||
ref={tabScrollRef}
|
|
||||||
className="p-2 max-h-[21.5rem] overflow-y-auto card-content-scroll"
|
|
||||||
onScroll={() => handleCardScroll(tabScrollRef, setShowUptimeScrollHint)}
|
|
||||||
>
|
>
|
||||||
{renderMonitorList(group.monitors)}
|
{group.monitors ? group.monitors.length : 0}
|
||||||
</div>
|
</Tag>
|
||||||
<div
|
</span>
|
||||||
className="card-content-fade-indicator"
|
}
|
||||||
style={{ opacity: activeUptimeTab === group.categoryName ? showUptimeScrollHint ? 1 : 0 : 0 }}
|
itemKey={group.categoryName}
|
||||||
/>
|
key={groupIdx}
|
||||||
</div>
|
>
|
||||||
</TabPane>
|
<ScrollableContainer maxHeight="21.5rem">
|
||||||
);
|
{renderMonitorList(group.monitors)}
|
||||||
})}
|
</ScrollableContainer>
|
||||||
|
</TabPane>
|
||||||
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user