从零开始用react + tailwindcss + express + mongodb实现一个聊天程序(十三) 优化聊天页面

1、 ChatBox组件

目前我们收到新的消息 不会滚动到视野当中

修改chatbox组件

// 最后一条消息的ref

const lastMessageRef = useRef(null)

// 滚动到最新消息

useEffect(()=>{

if(lastMessageRef.current && messages) {

lastMessageRef.current.scrollIntoView({behavior: 'smooth'})

}

})

javascript 复制代码
import {useEffect,useRef} from "react"
import { useChatStore } from "../store/useChatStore"
import { useAuthStore } from "../store/useAuthStore"
import {formatMessageTime} from "@/lib/util"
import ChatHeader from "./ChatHeader"
import MessageInput from "./MessageInput"

const ChatBox = () => {
  const {messages, getMessages, isMessagesLoading, selectedUser, subscribeToMessages, unsubscribeFromMessages} = useChatStore()
  const {authUser} = useAuthStore()
  // 最后一条消息的ref
  const lastMessageRef = useRef(null)
  useEffect(()=>{
    getMessages(selectedUser._id)

    // 开始订阅消息
    subscribeToMessages()

    return () => unsubscribeFromMessages()
  },[selectedUser._id, getMessages,subscribeToMessages, unsubscribeFromMessages])

  // 滚动到最新消息
  useEffect(()=>{
    if(lastMessageRef.current && messages) {
      lastMessageRef.current.scrollIntoView({behavior: 'smooth'})
    }
  })
  if(isMessagesLoading) return <div>Loading...</div>
  return (
    <div className="flex-1 flex flex-col overflow-auto">
      {/* 聊天框头部 */}
      <ChatHeader/>
      
      {/* 聊天消息 */}
      <div className="flex-1 overflow-auto p-4 space-y-4">
          {messages.map((message)=> (
             <div
              key={message._id}
              // 消息的发送者id和当前用户id一致,则显示在右侧,否则显示在左侧
              className={`chat ${message.senderId===authUser._id ? 'chat-end' : 'chat-start'}`}
              ref={lastMessageRef}
             >
              <div className="chat-image avatar">
                 <div className="size-10 rounded-full border">
                    <img
                      src={message.senderId === authUser._id ? authUser.profilePic || 'http://via.placeholder.com/150' : selectedUser.profilePic}
                      alt=""
                    />
                 </div>
              </div>

              <div className="chat-header mb-1">
                 <time className="text-xs opacity-50 ml-1">{formatMessageTime(message.createdAt)}</time>
              </div>
              {/* flex-col 图片和文字上下排列 */}
              <div className="chat-bubble flex flex-col"> 
                 {message.image && (
                   <img 
                    src={message.image}
                    alt=""
                    className="sm:max-w-[200px] rounded-md mb-2"
                   />
                 )}
                 {message.text && <p>{message.text}</p>}
              </div>
             </div>
          ))}

      </div>

      {/* 消息输入 */}
      <MessageInput/>
    </div>
  )
}

export default ChatBox

2、Sidebar组件

展示在线人员

javascript 复制代码
import { useEffect,useState} from "react"
import { useChatStore } from "../store/useChatStore"
import { useAuthStore } from "../store/useAuthStore"
import {User} from "lucide-react"

const Sidebar = () => {
    const {getUsers,users,selectedUser, setSelectedUser,isUsersLoading} = useChatStore()
    const {onlineUsers} = useAuthStore()
    const [showOnlyOnlineUsers, setShowOnlyOnlineUsers] = useState(false)
    // const onlineUsers = [];

    // 过滤在线用户
    const filterOnlineUsers = showOnlyOnlineUsers ? users.filter((user) => onlineUsers.includes(user._id)) :users
    useEffect(() => {
        getUsers()
    },[getUsers])

    if(isUsersLoading) return <div>Loading...</div>
  return (
    <aside className="h-full w-20 lg:w-72 border-r border-base-300 flex flex-col transition-all duration-200">
       <div className="border-b border-base-300 w-full p-5">
            <div className="flex items-center gap-2">
                <User  className="size-6" />
                <span className="font-medium hidden lg:block">联系人</span>
            </div>
            {/* 在线人员过滤 */}

            <div className="mt-3 hidden lg:flex items-center gap-2">
                <label className="cursor-pointer flex items-center gap-2">
                    <input 
                        type="checkbox"
                        checked={showOnlyOnlineUsers}
                        onChange={(e) => setShowOnlyOnlineUsers(e.target.checked)}
                        className="checkbox checkbox-sm"
                    />
                    <span className="text-sm">只显示在线</span>
                </label>
                <span className="text-xs text-zinc-500">({onlineUsers.length -1}在线)</span>
            </div>
       </div>

       <div className="overflow-y-auto w-full py-3">

         {filterOnlineUsers.map((user) =>(
            <button
                key={user._id}
                onClick={() => setSelectedUser(user)}
                className={`w-full p-3 flex items-center gap-3
                    hover:bg-base-300 transition-colors
                    ${selectedUser?._id===user._id}?"bg-base-300 ring-l ring-base-300":""
                `}
            >
                <div className="relative mx-auto lg:mx-0">
                    <img 
                        src={user.profilePic || "https://picsum.photos/200" }
                        alt={user.userName}
                        className="size-12 object-cover rounded-full"
                    
                    />
                    {onlineUsers.includes(user._id) &&(
                        <span className="absolute bottom-0 right-0 bg-green-500 size-4 rounded-full ring-2 ring-zinc-900">

                        </span>
                    )}
                </div>

                {/* 用户信息 只在大屏显示 */}
                <div className="hidden lg:block text-left min-w-0">
                    <div className="font-medium truncate">{user.userName}</div>
                    <div className="text-sm text-zinc-400">
                        {onlineUsers.includes(user._id) ? "在线" : "离线"}
                    </div>
                </div>
            </button>
         ))}

         {filterOnlineUsers.length === 0 && (
            <div className="text-center text-zinc-500 py-4">无在线用户</div>
         )}
       </div>
    </aside>
  )
}

export default Sidebar

3、ChatHeader组件

javascript 复制代码
import { useChatStore } from "../store/useChatStore";
import { useAuthStore } from "../store/useAuthStore";
import { X } from "lucide-react";

const ChatHeader = () => {
    const {selectedUser,setSelectedUser} = useChatStore();
    const {onlineUsers} = useAuthStore()
  return (
    <div className="p-2.5 border-b border-base-300">
       <div className="flex items-center justify-between">
          <div className="flex items-center">
            {/* 头像 */}
            <div className="avatar">
                <div className="size-10 rounded-full relative">
                    <img src={selectedUser.profilePic || "https://picsum.photos/200"} alt={selectedUser.userName} />
                </div>
            </div>

            {/* 用户信息 */}
            <div>
                <h3 className="font-medium">{selectedUser.userName}</h3>
                <p className="text-sm text-base-content/70">
                    {onlineUsers.includes(selectedUser._id) ? "在线" : "离线"}
                </p>
            </div>
          </div>

          {/* 关闭按钮 */}
          <button onClick={()=>setSelectedUser(null)}>
            <X/>
          </button>
       </div>
    </div>
  )
}

export default ChatHeader

这下页面体验就更好了!

相关推荐
一只一只妖2 小时前
突发奇想,还未实践,在Vben5的Antd模式下,将表单从「JS 配置化」改写成「模板可视化」形式(豆包版)
前端·javascript·vue.js
悟能不能悟5 小时前
js闭包问题
开发语言·前端·javascript
秋秋_瑶瑶5 小时前
vue-amap组件呈现的效果图如何截图
前端·javascript·vue-amap
LFly_ice6 小时前
学习React-9-useSyncExternalStore
javascript·学习·react.js
gnip6 小时前
js上下文
前端·javascript
中草药z6 小时前
【Stream API】高效简化集合处理
java·前端·javascript·stream·parallelstream·并行流
不知名raver(学python版)7 小时前
npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR!
前端·npm·node.js
醉方休7 小时前
React中使用DDD(领域驱动设计)
前端·react.js·前端框架
excel7 小时前
📖 小说网站的预导航实战:link 预加载 + fetch + 前进后退全支持
前端