/* 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 . 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 (
{children}
); }; 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;