You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

如何让NextUI虚拟化ListBox自动滚动至最后一个ListItem并解决输入时滚顶问题

如何让NextUI虚拟化ListBox自动滚动至最后一个ListItem并解决输入时滚顶问题

嘿,看你折腾这个NextUI聊天列表滚动的问题挺闹心的,我结合你的情况梳理下可行的解决方案,还有你遇到的输入滚顶问题的根源:

一、先搞懂为什么你之前的scrollIntoView不管用

你开启了ListBox的isVirtualized属性,这意味着列表只会渲染当前可视区域内的项,当聊天记录较多时,最后一条消息的ListItem根本没被挂载到DOM里,你给它加的messagesEndRef自然拿不到有效元素,不管设置block: 'end'还是start,都没法触发滚动。

二、解决方案分两种场景

场景1:放弃虚拟化列表(适合多数聊天场景,也是你最后采用的方案)

如果你的聊天记录不是那种几十万条的量级,直接用CSS原生滚动更省心,步骤如下:

  1. 去掉ListBox的isVirtualized属性和virtualization配置
  2. ListboxWrapper加上overflow-y: auto和固定高度(比如你之前设置的600px),让容器自己处理滚动
  3. 用容器的scrollTop属性直接滚动到底部,比scrollIntoView更可靠:
const scrollToBottom = () => {
  if (containerRef.current) {
    // 直接把滚动条拉到容器的最大高度
    containerRef.current.scrollTop = containerRef.current.scrollHeight;
  }
};
  1. 每次聊天记录更新后调用这个方法,也可以用useEffect监听chats变化自动触发:
useEffect(() => {
  scrollToBottom();
}, [chats]);

场景2:继续使用虚拟化列表(适合超大量聊天记录)

如果必须用虚拟化,得用NextUI ListBox提供的原生滚动方法,而不是原生DOM的scrollIntoView

  1. 给ListBox加一个ref:
const listboxRef = useRef(null);
  1. scrollToBottom里调用ListBox的scrollToIndex方法,直接滚动到最后一项的索引:
const scrollToBottom = () => {
  if (listboxRef.current && chats.length > 0) {
    listboxRef.current.scrollToIndex(chats.length - 1);
  }
};
  1. 把ref绑定到ListBox组件上:
<Listbox
  ref={listboxRef}
  isVirtualized
  aria-label="Dynamic Actions"
  items={chats}
  selectionMode="none"
  virtualization={{
    maxListboxHeight: 600,
    itemHeight: 40,
  }}
>
  {/* 其他内容 */}
</Listbox>

三、解决输入时自动滚顶的问题

你说输入文字时列表自动滚到顶部,大概率是这两个原因:

  1. 不必要的组件重渲染:你维护了selectedKeys状态,但设置了selectionMode="none",这个状态完全没必要保留,去掉selectedKeysonSelectionChange配置,避免ListBox因为状态更新重新渲染并重置滚动位置
  2. 虚拟化列表的渲染逻辑:虚拟化列表在重渲染时会默认把滚动位置重置到顶部,用上面场景2的scrollToIndex方法可以强制滚动到底部,或者改用场景1的原生滚动方案,滚动位置会被浏览器自动保留

调整后的完整示例(原生滚动方案)

import { useState, useRef, useEffect } from 'react';
import { Listbox, ListboxItem, Input } from '@nextui-org/react';

const ListboxWrapper = ({children}) => (
    <div className="grid row-span-6 w-full h-[600px] border-small px-1 py-2 rounded-small border-default-200 dark:border-default-100 overflow-y-auto">
        {children}
    </div>
);

export default function ChatFeed() {
  const [selectedServer, setSelectedServer] = useState(null);
  const [chats, setChats] = useState([]);
  const [textValue, setTextValue] = useState("");
  const containerRef = useRef(null);

  const scrollToBottom = () => {
    if (containerRef.current) {
      containerRef.current.scrollTop = containerRef.current.scrollHeight;
    }
  };

  // 聊天记录更新后自动滚动到底部
  useEffect(() => {
    scrollToBottom();
  }, [chats]);

  const onKeyPressHandler = e => {
    if (e.key === "Enter" && selectedServer) {
      // 你的API请求逻辑
      const message = new ApiCall();
      const chatRequest = new ChatRequest();
      chatRequest.setChattext(textValue);
      chatRequest.setServerid(selectedServer.serverID);
      message.setChatrequest(chatRequest);
      const apiCall = message.serializeBinary();

      const requestOptions = {
        method: 'POST',
        headers: {'Content-Type': 'application/x-protobuf'},
        body: apiCall
      };

      fetch('http://localhost:8080/api/chats', requestOptions)
        .then(response => {
            if (response.ok) {
                getChats();
            }
            setTextValue("");
        })
    }
  };

  const onChangeHandler = e => {
    setTextValue(e.target.value);
  };

  const getChats = () => {
    fetch(`http://localhost:8080/api/chats?serverID=${selectedServer.serverID}`)
      .then(res => res.json())
      .then(data => {
          setChats(data);
      })
  };

  return (
    <>
      <h1>{selectedServer ? selectedServer.serverAlias : "Chat Title"}</h1>
      <ListboxWrapper ref={containerRef}>
          <Listbox
              aria-label="Chat Messages"
              items={chats}
              selectionMode="none"
          >
              {(item) => (
                  <ListboxItem key={item.id}>
                      {item.text}
                  </ListboxItem>
              )}
          </Listbox>
      </ListboxWrapper>

      <Input
          label="Send Message"
          type={selectedServer ? "text" : "hidden"}
          radius="full"
          onKeyUp={onKeyPressHandler}
          value={textValue}
          onChange={onChangeHandler}
      />
    </>
  );
}

备注:内容来源于stack exchange,提问作者Kris Rice

火山引擎 最新活动