You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

聊天应用开发:如何获取视口中最后一个可见消息项的索引?

问题:获取聊天视口中最后一个可见消息的索引

我正在开发一款聊天应用,目前要实现滚动时根据视口中可见消息的日期,在顶部显示粘性日期的功能,但不知道如何获取视口中最后一个可见项的索引。

现有代码

const MessageComponent = () => {
  const { id } = useParams();
  const {
    data: messageData,
    isSuccess,
    isError,
    isLoading,
    isFetching,
    hasMoreData,
    firstData,
    isUninitialized,
  } = useGetData(id);

  const { loadMore } = useGetScrollData(messageData, id);

  const user = User();

  const chatId =
    messageData[0]?.type == "date"
      ? messageData[1]?.chat?._id
      : messageData[0]?.chat?._id;
  const islastItemChatId: boolean = messageData.length > 0 && chatId != id;
  const scrollRef = useScrollRef(islastItemChatId, id);

  const scrollFunc = (e: any) => {
    // let m = e.target.scrollHeight + e.target.scrollTop;
    // let i = e.target.scrollHeight - m;
    // console.log({ i, e });
    if (!scrollRef.current) return;
    const containerMiddle =
      scrollRef.current.scrollTop +
      scrollRef.current.getBoundingClientRect().height / 2;
    const infiniteScrollItems = scrollRef.current.children[0].children;
    console.log({ containerMiddle, infiniteScrollItems, e: e.target });
  };

  return (
    <>
      {messageData.length > 0 && (
        <div
          id="scrollableDiv"
          style={{
            height: "80%",
            overflow: "auto",
            display: "flex",
            flexDirection: "column-reverse",
            padding: "10px 0px",
          }}
          ref={scrollRef}
        >
          <InfiniteScroll
            dataLength={messageData.length}
            hasMore={hasMoreData}
            onScroll={scrollFunc}
            loader={
              <div className="loading-container">
                <div className="lds-ring">
                  <div></div>
                </div>
              </div>
            }
            endMessage={
              <div className="message-container date">
                <div className={`text-container sender large-margin`}>
                  <span>You have seen all the messages</span>
                </div>
              </div>
            }
            style={{ display: "flex", flexDirection: "column-reverse" }}
            next={loadMore}
            inverse={true}
            scrollableTarget="scrollableDiv"
          >
            {messageData.map((item, index: number) => {
              const isUserChat = item?.sender?._id === user._id;
              const className =
                item?.type == "date"
                  ? "date"
                  : isUserChat
                  ? "user-message"
                  : "sender-message";

              const prevItem: IMessageData | null =
                index < messageData?.length ? messageData[index - 1] : null;

              const nextItem: IMessageData | null =
                index < messageData?.length ? messageData[index + 1] : null;

              return (
                <Message
                  key={item._id}
                  item={item}
                  prevItem={prevItem}
                  className={className}
                  isUserChat={isUserChat}
                  index={index}
                  nextItem={nextItem}
                />
              );
            })}
          </InfiniteScroll>
        </div>
      )}
    </>
  );
};

解决方案

方法1:利用getBoundingClientRect在滚动事件中计算

直接修改scrollFunc,遍历所有消息元素,判断元素是否在容器视口中,记录最后一个可见的元素索引:

const scrollFunc = (e: any) => {
  if (!scrollRef.current) return;
  
  const container = scrollRef.current;
  const containerRect = container.getBoundingClientRect();
  const messageElements = Array.from(container.children[0].children);
  let lastVisibleIndex = -1;

  // 遍历所有消息元素,判断可见性
  messageElements.forEach((el, index) => {
    const elRect = el.getBoundingClientRect();
    // 元素与容器视口有交集即判定为可见
    const isVisible = elRect.top <= containerRect.bottom && elRect.bottom >= containerRect.top;
    
    if (isVisible) {
      lastVisibleIndex = index;
    }
  });

  // lastVisibleIndex对应messageData中的索引,可直接获取对应的消息项
  if (lastVisibleIndex !== -1) {
    const lastVisibleItem = messageData[lastVisibleIndex];
    console.log("最后一个可见元素:", lastVisibleItem);
    // 这里可以根据lastVisibleItem的日期更新顶部粘性日期
  }
};

方法2:使用IntersectionObserver(性能更优)

滚动事件触发频繁,getBoundingClientRect会导致频繁重排,推荐用IntersectionObserver监听元素可见性:

  1. 给每个Message组件添加ref:
// 初始化ref数组存储每个Message的DOM节点
const messageRefs = useRef<(HTMLElement | null)[]>([]);

// 在map渲染时绑定ref
return (
  <Message
    key={item._id}
    ref={(el) => messageRefs.current[index] = el}
    item={item}
    prevItem={prevItem}
    className={className}
    isUserChat={isUserChat}
    index={index}
    nextItem={nextItem}
  />
);
  1. 在useEffect中初始化观察者,监听元素可见性:
useEffect(() => {
  if (!scrollRef.current) return;

  // 存储所有可见的元素索引
  const visibleIndices = new Set<number>();

  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach(entry => {
        const elementIndex = messageRefs.current.indexOf(entry.target as HTMLElement);
        if (elementIndex === -1) return;

        if (entry.isIntersecting) {
          visibleIndices.add(elementIndex);
        } else {
          visibleIndices.delete(elementIndex);
        }

        // 获取最大的索引(即视口中最顶部的可见元素)
        const lastVisibleIndex = visibleIndices.size > 0 ? Math.max(...visibleIndices) : -1;
        if (lastVisibleIndex !== -1) {
          const lastVisibleItem = messageData[lastVisibleIndex];
          console.log("最后一个可见元素:", lastVisibleItem);
          // 更新顶部粘性日期逻辑
        }
      });
    },
    {
      root: scrollRef.current, // 监听的根容器
      rootMargin: "0px",
      threshold: 0.1 // 元素有10%可见即判定为可见
    }
  );

  // 遍历所有消息元素添加监听
  messageRefs.current.forEach(ref => {
    if (ref) observer.observe(ref);
  });

  // 组件卸载时取消监听
  return () => {
    messageRefs.current.forEach(ref => {
      if (ref) observer.unobserve(ref);
    });
  };
}, [messageData]);

注意事项

由于容器使用了flex-direction: column-reverse,渲染的元素顺序与messageData数组顺序一致,但页面显示时是从下到上排列的,所以lastVisibleIndex对应的是messageData数组中的索引,直接取对应项即可拿到视口中最顶部的可见消息。

内容的提问来源于stack exchange,提问作者Ayush Kumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 11:57:18