从零开始用react + tailwindcss + express + mongodb实现一个聊天程序(十) 收发消息

1.聊天框

首先我们完善前端的消息输入框

components下面新建MessageInput组件

javascript 复制代码
import { useState,useRef } from "react"
import {X,Image,Send} from "lucide-react"

import { useChatStore } from "../store/useChatStore"
import toast from "react-hot-toast"
const MessageInput = () => {
    const [text, setText] = useState("")
    const [imagePreview, setImagePreview] = useState(null) // 预览的图片
    const fileInputRef = useRef(null) // 文件输入框
    const {sendMessages} = useChatStore(); // 发送消息

    const handleImageChange =(e) => {
        const file = e.target.files[0]
        if(!file.type.startsWith("image/")) {
            toast.error("请选择图片文件")
            return
        }
        const reader = new FileReader()
        reader.onload = () => {
            setImagePreview(reader.result)
        }
        reader.readAsDataURL(file)
    }
    const removeImage = () => {
        setImagePreview(null)
        if(fileInputRef.current)  fileInputRef.current.value = ""
    }
    const handleSendMessage = async(e) => {
        e.preventDefault()
        if(!text.trim() && !imagePreview) return
        try {
            await sendMessages({text:text.trim(),image:imagePreview})
            toast.success("发送消息成功")
            setText("")
            setImagePreview(null)
            if(fileInputRef.current)  fileInputRef.current.value = ""
        } catch (error) {
            console.log(error)
            toast.error("发送消息失败:",error.message)
        }
    }
  return (
    <div className="p-4 w-full">
       {imagePreview && (
         <div className="mb-3 flex items-center gap-2">

            <div className="relative">
                <img 
                    src={imagePreview} 
                    alt="Preview"
                    className="size-20 object-cover rounded-lg border border-zinc-700"
                />
                <button
                    onClick={removeImage}
                    className="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-base-300
                    flex items-center justify-center"
                    type="button"
                >
                    <X className="size-3"/>
                </button>
            </div>
         </div>
       )}

       {/* 消息输入 */}
       <form onSubmit={handleSendMessage} className="flex items-center gap-2">
          <div className="flex-1 flex gap-2">
            <input
                type="text"
                className="w-full input input-bordered rounded-lg input-sm sm:input-md"
                placeholder="输入消息"
                value={text}
                onChange={(e) => setText(e.target.value)}
            />
            <input 
                type="file"
                accept="image/*"
                className="hidden"
                ref={fileInputRef}
                onChange={handleImageChange}
            />
            <button
                type="button"
                className={`hidden sm:flex btn btn-circle
                ${imagePreview?"text-emerald-500":"text-zinc-400"}`}
                onClick={() => fileInputRef.current.click()}
            >
                <Image size={20} />
            </button>
          </div>

           <button
            type="submit"
            className="btn btn-sm btn-circle"
            disabled={!imagePreview && !text.trim()}
           >
                <Send size={22}/>
           </button>
       </form>
    </div>
  )
}

export default MessageInput

聊天的气泡我们参考daisyUi的 chat-start chat-end 效果如下

ChatBox.jsx页面代码

javascript 复制代码
import {useEffect} 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} = useChatStore()
  const {authUser} = useAuthStore()
  useEffect(()=>{
    getMessages(selectedUser._id)

  },[selectedUser._id, getMessages])

  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'}`}
             >
              <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

这是我们在左侧发送消息 右侧用户就能收到消息了 并且自己发送的消息在右侧 收到的消息在左侧 正是用了 chat-start chat-end 这2个class

我们的判断逻辑是

// 消息的发送者id和当前用户id一致,则显示在右侧,否则显示在左侧

className={`chat ${message.senderId===authUser._id ? 'chat-end' : 'chat-start'}`}

思考一个问题 目前我们发送消息给对方 只有刷新页面才能收到新的消息 这是不符合要求的 所以我们引入socket.io 实现实时 的 收发消息 功能。

下篇继续。。。

相关推荐
Points7 分钟前
开源分享:一个轻量级管理iOS数据自动升级的管理类-SpeSqliteManager4IOS
前端
Mintopia11 分钟前
3D Quickhull 算法:用可见性与冲突图搭建空间凸壳
前端·javascript·计算机图形学
Mintopia12 分钟前
Three.js 三维数据交互与高并发优化:从点云到地图的底层修炼
前端·javascript·three.js
陌小路17 分钟前
5天 Vibe Coding 出一个在线音乐分享空间应用是什么体验
前端·aigc·vibecoding
成长ing1213825 分钟前
cocos creator 3.x shader 流光
前端·cocos creator
Alo36533 分钟前
antd 组件部分API使用方法
前端
BillKu36 分钟前
Vue3数组去重方法总结
前端·javascript·vue.js
GDAL39 分钟前
Object.freeze() 深度解析:不可变性的实现与实战指南
javascript·freeze
都给我1 小时前
服务器中涉及节流(Throttle)的硬件组件及其应用注意事项
服务器·网络·express
江城开朗的豌豆1 小时前
Vue+JSX真香现场:告别模板语法,解锁新姿势!
前端·javascript·vue.js