useBottom + useScrollupListener,流式输出场景的"回到底部"按钮
最近在写大模型流式返回的时候,默认是拼接文字直接滚动到底部的。对于初始化的时候历史内容过多,则需要展示"滚动到底部"的按钮。此 hook 就是监听设置的元素是否在可视区域,不在的展示滚动到底部按钮。
import { useRef, useState, useEffect } from 'react';
export function useBottom(chatBottomRef: any) {
const [showGoBottom, setShowGoBottom] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
// 当参考元素完全不在视口中时显示按钮
entries.forEach((entry) => {
setShowGoBottom(!entry.isIntersecting);
});
},
{
root: null, // 使用浏览器视口作为根元素
threshold: 0.1, // 当元素至少有10%可见时认为可见
rootMargin: '0px'
}
);
// 开始观察参考元素
if (chatBottomRef.current) {
observer.observe(chatBottomRef.current);
}
// 组件卸载时停止观察
return () => {
if (chatBottomRef.current) {
observer.unobserve(chatBottomRef.current);
}
};
}, [chatBottomRef]);
return {
showGoBottom
};
}
const { showGoBottom } = useBottom(chatBottomRef);
<div
className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-5"
ref={containterRef}
>
{/* 底部滚动 */}
<div ref={chatBottomRef} className="mb-40 h-[1px]"></div>
</div>
{/* 滚动动画 */}
{showGoBottom && (
<GoBottom streaming={streaming} go={resetBottom}></GoBottom>
)}
import { useRef, useState, useEffect } from 'react';
export function useScrollupListener(elementRef) {
const isScrollingUp = useRef(false);
const lastScrollTop = useRef(0);
useEffect(() => {
const target = elementRef?.current || window;
const handleScroll = () => {
const scrollTop = elementRef
? target.scrollTop
: window.scrollY || document.documentElement.scrollTop;
// 判断是否向上滚动
if (scrollTop < lastScrollTop.current) {
isScrollingUp.current = true;
}
lastScrollTop.current = scrollTop;
};
target.addEventListener('scroll', handleScroll);
return () => {
target.removeEventListener('scroll', handleScroll);
};
}, [elementRef]);
return { isScrollingUp };
}
| hook | 核心机制 | 典型用途 |
|---|---|---|
| useBottom | IntersectionObserver 监听底部占位元素,不可见时置 showGoBottom | 流式输出时按需展示"回到底部"按钮 |
| useScrollupListener | scroll 事件对比 lastScrollTop,向上滚时置 isScrollingUp | 判断用户是否在回看历史、暂停自动滚动 |