从零开始用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

这下页面体验就更好了!

相关推荐
NiceCloud喜云5 小时前
Opus 4.8 的 Effort Control 怎么选:Low 到 Max 五档策略
android·java·大数据·前端·c++·python·spring
ccddsdsdfsdf6 小时前
DBeaver怎么链接mongoDB
数据库·mongodb
wordbaby6 小时前
React Native + RNOH:跨页面数据回传的最佳实践与避坑指南
前端·react native
GISer_Jing6 小时前
Three.js着色器编译机制深度解析
javascript·webgl·着色器
丷丩6 小时前
MapLibre GL JS第22课:查看本地GeoJSON
前端·javascript·map·mapbox·maplibre gl js
油炸自行车6 小时前
Claude Code 错误:API Error: 400 Failed to deserialize the JSON body into the
开发语言·javascript·json·trae·claude code·api error 400
星夜夏空997 小时前
FreeRTOS学习(4)——内存映射
数据库·学习·mongodb
Front思7 小时前
AI前端工程师需要具备能力+
前端·人工智能·ai
ZC跨境爬虫9 小时前
跟着 MDN 学CSS day_29:(掌握文本与字体样式的核心艺术)
前端·css·ui·html·tensorflow
李子琪。10 小时前
网络空间安全深度实战:CSRF 漏洞原理剖析与基于 Token 的纵深防御体系构建(全栈实验报告)
前端·安全·csrf