Bootstrap

vue3 webSocket

前端

效果图片

1.webSocket.js 

import store from "@/store"
var url="http://localhost:3000".replace("https://","wss://").replace("http://","ws://")+"/websocket/"
import axios from "axios"
const webSocket={
    //链接
    getUser(userId){
        var ws= new WebSocket(url+userId)
        return new Promise((resolve,reject)=>{
            //链接成功
            ws.onopen=function () {
                console.log("链接成功!");
                resolve(200)
            }
            //链接失败
            ws.error=function () {
                console.log("连接错误")
                reject(404)
            }
        })
    },
    // //发送消息
    // onSend(username,SendText){
    //     var ws= new WebSocket(url+username)
    //      //链接成功
    //      ws.onopen=function () {
    //         console.log("链接成功!");
    //     }
    //     //链接失败
    //     ws.error=function () {
    //         console.log("连接错误")
    //     }
    //     setTimeout(()=>{
    //         ws.send(JSON.stringify(SendText))
    //     },200)
    // },
    //收到消息
    onMessage(username){
        var ws= new WebSocket(url+username)
         //链接成功
        //  console.log(store);
         ws.onopen=function () {
            console.log("链接成功!");
            //离线消息
            axios.get("http://localhost:3000/Chat/sendOfflineMessage/"+localStorage.getItem("username"))
        }
        //链接失败
        ws.error=function () {
            console.log("连接错误")
        }
        ws.onmessage=function(msg){
            store.commit("OnMsg",JSON.parse(msg.data))
            console.log(JSON.parse(msg.data));
        }
    },
    //关闭
    onClose(username){
        var ws= new WebSocket(url+username)
         //链接成功
         ws.onopen=function () {
            console.log("链接成功!");
        }
        //链接失败
        ws.error=function () {
            console.log("连接错误")
        }
        ws.onclose=function () {
            console.log("关闭链接...");
            return 403
        }
    }
}
export default webSocket

2.websocket.vue主件

<template>
    <div style="width: 1200px">
      <div v-if="true">
        <div>
          <input type="text" v-model="userId" placeholder="请输入发信人">
          <button  @click="login">发信人</button>
          <input type="text" v-model="sendId" placeholder="请输入收信人">
          <button @click="sender">收信人</button>
        </div>
        <Send v-if="isFlag"></Send>
      </div>
     
    </div>
  </template>
  
  <script setup>
  import Send from "./Sender/index.vue"
  import Table from "@/Table/index.vue"
  const userId=ref("")
  const sendId=ref("")
  const isFlag=ref(false)
  
  //收信人
  const login=()=>{
    // webSocket.getUser(user.value)
    localStorage.setItem('userId',userId.value)
    isFlag.value=true
    // ElMessage.error(`${userId.value}登录成功`);
    // msg()
  }
  //发信人
  const sender=()=>{
    localStorage.setItem("sendId",sendId.value)
  }
  // webSocket.onClose(localStorage.getItem("userId"))
  //表格
  
  //样式
  // const headerStyle=ref({
  //     'background-color': '#F9FAFB',
  //     'font-weight':'bold',
  //     'color':'#333',
  //     "padding-left":"50px",
  //     "padding-right":"50px"
  // })
  // const CellStyle=ref({
  //   'padding-left':'50px',
  //   'padding-right':'50px'
  // })
  // const tableData=istableData(1)
  </script>
  <style scoped>
    .bg{
      width: 786px;
      height: 720px;
      background:#fff;
    }
  </style>

2.1index.vue 

<template>
    <div>
      <div class="bg">
        <div class="pt20">
          <Content class="mr50 ml50"></Content>
          <Send></Send>
        </div>
      </div>
      <!-- <Emoji></Emoji> -->
    </div>
  </template>
  
  <script setup>
    import Content from "./Content.vue"
    import Send from "./Send.vue"
    import {ref} from "vue"
    import emoji from "@/assets/emoji.json"
    console.log(emoji);
    let emojis= "😃".codePointAt(0).toString(10)
    console.log(emojis);
    const m=ref("&#128515;")
  </script>
  <style scoped>
    .bg{
      width: 786px;
      height: 720px;
      background:#fff;
    }
  </style>

3.Send,vue发送文字&发送信息

<template>
    <div class="">
        <hr>
        <div>
            <div class="justify-space-between emojibag">
                <div class="emoji" @click="BtnEmoji">&#128515;</div>
                <div class="emoji" @click="BtnCommonly">🐷</div>
                <div class="emoji">
                   <UploadImg></UploadImg>
                </div>|
                <div class="tag">发简历</div>
                <div class="tag">换微信</div>
                <div class="tag">换电话</div>
                <!-- 表情 -->
                <div class="isemoji" v-if="isEmoji" @mouseleave="BtnIsemoji" @mouseenter="isEmoji=true">
                    <Emoji></Emoji>
                </div>
                <!-- 常用语 -->
                <div class="isCommonly" v-if="isCommonly" @mouseenter="isCommonly=true" @mouseleave="BtnIsCommonly">
                    <Commonly></Commonly>    
                </div>
            </div>
        </div>
        <div class="ml50 mr50" style="position: relative" @keyup.enter.native="BtnSend()">
            <div 
                ref="textarea" 
                class="SendText" 
                v-html="Text" 
                contenteditable="true" 
                @click="changeData($event)" 
                @keyup="changeData($event)">
            </div>
        </div>
        <div class="justify-end mr50 pointer" @click="BtnSend()">
            <el-tag>发送</el-tag>
        </div>
    </div>
</template>
<script setup>
    import Emoji from "./emoji.vue"
    import Busevent from "@/assets/Busevent.js";
    import emoji from "@/assets/emoji.json"
    import Commonly from "./Commonly.vue";
    import {beforeupload} from "./Upfile.js"
    import UploadImg from "./UploadImg.vue"
    import webSocket from "@/webSocket.js"
    import {useStore} from "vuex"
    const store=new useStore()
    let message={
     "createTime":new Date().toLocaleString('zh', { hour12: false }).replaceAll('/', '-'),
     "headImg": "https://file-1313175594.cos.ap-guangzhou.myqcloud.com/images/d1f9c7e77ec641bbb92ac41dd22b1c1e.jpg",
     "msg": "",
     "sendId":localStorage.getItem("userId"), //发送者
     "type": "",
     "userId":localStorage.getItem("sendId"), //发给
     "userIds": []
    }
    // beforeupload()
    //textarea显示消息
    const SendText=ref("")
   
    // 移入显示表情
    const isEmoji=ref(false)
    const BtnEmoji=()=>{
        isEmoji.value=true
    }
    
    //移出表情隐藏
    const BtnIsemoji=()=>{
        setTimeout(()=>{
            isEmoji.value=false
        },1000)
    }
   
    //选择表情
    const Text=ref("") //v-html
    const startOffset=ref() //光标位置
    Busevent.on("emoji",e=>{
        let start=SendText.value.slice(0,startOffset.value)
        console.log(start);
        let end=SendText.value.slice(startOffset.value)
        console.log(end);
        SendText.value=start+e.emoji+end
        // SendText.value=e.emoji
        Text.value=SendText.value
        isEmoji.value=false
    })
    
    //移入显示常用语
    const isCommonly=ref(false)
    const BtnCommonly=()=>{
        isCommonly.value=true
    }
     //移出常用语隐藏
     const BtnIsCommonly=()=>{
        setTimeout(()=>{
            isCommonly.value=false
        },1000)
    }
    //选择常用语
    Busevent.on("Commonly",e=>{
        // console.log(e.msg);
        SendText.value=""
        Text.value=""
        //清空内容
        let hArr = document.querySelector('.SendText');
        while (hArr.hasChildNodes()) {
            hArr.removeChild(hArr.firstChild);
        }
        SendText.value=e.msg
        Text.value=SendText.value
        isCommonly.value=false
        BtnSend()
    })
    
    //选择图片
    Busevent.on("uploadImg",e=>{
        // let img=e
        message.msg=e
        //判断是否服务器上传图片 img图片 msg消息
        message.type="img"
        store.dispatch("SendMsg",message).then(res=>{
            if (res.data.code==200) {
                store.commit("SendMsg",message)
            }
        })
    })

    //发送消息
    const msgSend=ref("")
    const BtnSend=()=>{
        // textarea.value.blur()
        //处理表情包
        SendText.value=SendText.value.replace("<div><br></div>","")
        let a=[...SendText.value]
        // console.log(a);
        const emojiRegex = /[\u{1F600}-\u{1F64F}]/gu;
        // const emojiRegex = /[\uD83C|\uD83D|\uD83E][\uDC00-\uDFFF][\u200D|\uFE0F]|[\uD83C|\uD83D|\uD83E][\uDC00-\uDFFF]|[0-9|*|#]\uFE0F\u20E3|[0-9|#]\u20E3|[\u203C-\u3299]\uFE0F\u200D|[\u203C-\u3299]\uFE0F|[\u2122-\u2B55]|\u303D|[\A9|\AE]\u3030|\uA9|\uAE|\u3030/gi;
        let s=""
        let m
        for (const i in a) {
            // console.log(a[i]);
            if(emojiRegex.test(a[i])) {
                    m="&#"+ a[i].codePointAt(0).toString(10)+";"
                    s+=m
                }else{
                s+=a[i]
            }
        }
        console.log(s);
        msgSend.value=s
        // console.log(msgSend.value);
        message.type="msg"
        message.msg=msgSend.value
        store.dispatch("SendMsg",message).then(res=>{
            if (res.data.code==200) {
                store.commit("SendMsg",message)
            }
        })
        console.log("wwwwwwww:",message);
        //清空内容
        let hArr = document.querySelector('.SendText');
        while (hArr.hasChildNodes()) {
            hArr.removeChild(hArr.firstChild);
        }
        SendText.value=""
        Text.value=""
        // console.log(Text.value);
        textarea.value.focus()
    }
    
    //获取焦点
    const textarea=ref(null)
    nextTick(()=>{
        textarea.value.focus()
    })
   
    //获取v-html内容
    const changeData= (event) =>{
       SendText.value=event.srcElement.innerHTML
       // 获取选定对象
       var selection = getSelection();
       // 设置最后光标对象
       var lastEditRange = selection.getRangeAt(0)
        // console.log(lastEditRange);
        //获取光标index
        startOffset.value=lastEditRange.startOffset
    }

</script>
<style scoped>
    .emojibag{
        width: 300px;
        margin-top: 5px;
        margin-left: 50px;
        position: relative;
    }
    .tag{
        font-size: 12px;
        border: 1px solid rgb(208, 212, 204);
        border-radius: 5px;
        width: 50px;
        height: 25px;
        text-align: center;
        line-height: 25px;
        cursor: pointer;
    }
    .emoji{
        font-size: 18px;
        cursor: pointer;
    }
    /* 表情框 */
    .isemoji{
        position:absolute;
        top:-230px;
        left: 0px;
    }
    /* 常用语框 */
    .isCommonly{
        position: absolute;;
        top: -158px;
        left: 40px;
    }
    .BtnSend{
        position: absolute;
        top:10;
        font-weight: 400;
        font-family:'Microsoft YaHei'
    }
    .SendText{
        outline: none;
        overflow-y: scroll;
        height: 60px;
        width: 300px;
    }
     /*滚动条整体样式*/
     .SendText::-webkit-scrollbar {
        width: 4px; /*高宽分别对应横竖滚动条的尺寸*/
        height: 4px;
    }
    /*滚动条里面小方块*/
    .SendText::-webkit-scrollbar-thumb {
        border-radius: 5px;
        -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
        background: rgba(0,0,0,0.2);
        display: none;
    }
  
</style>

4.发送表情

<template>
    <br>
    <div class="bg">
       <div class="emoji">
            <div v-for="(item,index) in emoji">
                <!-- <el-tooltip
                    class="box-item"
                    effect="light"
                    placement="top"
                >
                <template #content>
                    &nbsp;{{item.cn}}
                    <br />
                    <div style="font-size:20px">{{item.emoji}}</div>
                </template>
                    <div class="boxemoji" @click="BtnEmoji">{{item.emoji}}</div>
                </el-tooltip> -->
                <div class="boxemoji" @click="BtnEmoji(item)">{{item.emoji}}</div>
            </div>

       </div>
    </div>
</template>
<script setup>
  import emoji from "@/assets/emoji.json"
  import {ref,onMounted } from "vue"
  import Busevent from "@/assets/Busevent.js";
  //点击按钮
  const BtnEmoji=(e)=>{
    //   console.log(e);
    Busevent.emit("emoji",e)
  }

</script>
<style scoped>
    .bg{
        background-color:#E0EAFD;
        height: 200px;
        width: 300px;
        overflow: hidden;
        overflow-y: scroll;
        border-radius: 5px;
        box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
    }
    .bg::before{
        content: '';
        position: absolute;
        width: 0;
        height: 0;
        bottom: -20px;
        left: 1px;
        border: 10px solid;
        border-color: transparent #E0EAFD transparent transparent ;
        box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
        transform: rotate(-90deg);
    }
     /*滚动条整体样式*/
     .bg::-webkit-scrollbar {
        width: 4px; /*高宽分别对应横竖滚动条的尺寸*/
        height: 4px;
    }
    /*滚动条里面小方块*/
    .bg::-webkit-scrollbar-thumb {
        border-radius: 5px;
        -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
        background: rgba(0,0,0,0.2);
        display: none;
    }
    /*滚动条里面轨道*/
    .bg::-webkit-scrollbar-track {
        -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
        border-radius: 0;
        background: rgba(0,0,0,0.1);
        display: none;
     }
    .emoji{
        display: flex;
        flex-wrap: wrap;
    }
    .boxemoji{
        margin-left: 2px;
        padding: 2px;
        cursor: pointer;
        font-size: 24px;
        width: 35px;
        /* border: 1px solid red; */
    }
</style>

4.1emoji.json文件

{
	"微笑": {
		"cn": "微笑",
		"zb": "Smile",
		"emoji": "🙂"
	},
	"撇嘴": {
		"cn": "撇嘴",
		"zb": "Grimace",
		"emoji": "😦"
	},
	"色": {
		"cn": "色",
		"zb": "Drool",
		"emoji": "😍"
	},
	"发呆": {
		"cn": "发呆",
		"zb": "Scowl",
		"emoji": "😍"
	},
	"得意": {
		"cn": "得意",
		"zb": "CoolGuy",
		"emoji": "😎"
	},
	"流泪": {
		"cn": "流泪",
		"zb": "Sob",
		"emoji": "😭"
	},
	"害羞": {
		"cn": "害羞",
		"zb": "Shy",
		"emoji": "😊"
	},
	"闭嘴": {
		"cn": "闭嘴",
		"zb": "Silent",
		"emoji": "😷"
	},
	"睡": {
		"cn": "睡",
		"zb": "Sleep",
		"emoji": "😴"
	},
	"大哭] ": {
		"cn": "大哭] ",
		"zb": "Cry] ",
		"emoji": "😡"
	},
	"尴尬": {
		"cn": "尴尬",
		"zb": "Awkward",
		"emoji": "😡"
	},
	"发怒": {
		"cn": "发怒",
		"zb": "Angry",
		"emoji": "😛"
	},
	"调皮": {
		"cn": "调皮",
		"zb": "Tongue",
		"emoji": "😀"
	},
	"呲牙": {
		"cn": "呲牙",
		"zb": "Grin",
		"emoji": "😯"
	},
	"惊讶": {
		"cn": "惊讶",
		"zb": "Surprise",
		"emoji": "🙁"
	},
	"难过": {
		"cn": "难过",
		"zb": "Frown",
		"emoji": "😎"
	},
	"酷": {
		"cn": "酷",
		"zb": "Ruthless",
		"emoji": "😨"
	},
	"冷汗": {
		"cn": "冷汗",
		"zb": "Blush",
		"emoji": "😱"
	},
	"抓狂": {
		"cn": "抓狂",
		"zb": "Scream",
		"emoji": "😵"
	},
	"吐] ": {
		"cn": "吐",
		"zb": "Puke",
		"emoji": "😋"
	},
	"愉快": {
		"cn": "愉快",
		"zb": "Joyful",
		"emoji": "🙄"
	},
	"白眼": {
		"cn": "白眼",
		"zb": "Slight",
		"emoji": "🙄"
	},
	"傲慢": {
		"cn": "傲慢",
		"zb": "Smug",
		"emoji": "😋"
	},
	"饥饿": {
		"cn": "饥饿",
		"zb": "Hungry",
		"emoji": "😪"
	},
	"困": {
		"cn": "困",
		"zb": "Drowsy",
		"emoji": "😫"
	},
	"惊恐": {
		"cn": "惊恐",
		"zb": "Panic",
		"emoji": "😓"
	},
	"流汗": {
		"cn": "流汗",
		"zb": "Sweat",
		"emoji": "😃"
	},
	"憨笑": {
		"cn": "憨笑",
		"zb": "Laugh",
		"emoji": "😃"
	},
	"悠闲] ": {
		"cn": "悠闲",
		"zb": "Commando",
		"emoji": "😆"
	},
	"奋斗": {
		"cn": "奋斗",
		"zb": "Determined",
		"emoji": "😆"
	},
	"咒骂": {
		"cn": "咒骂",
		"zb": "Scold",
		"emoji": "😆"
	},
	"疑问": {
		"cn": "疑问",
		"zb": "Shocked",
		"emoji": "😆"
	},
	"嘘": {
		"cn": "嘘",
		"zb": "Shhh",
		"emoji": "😵"
	},
	"晕": {
		"cn": "晕",
		"zb": "Dizzy",
		"emoji": "😆"
	},
	"疯了": {
		"cn": "疯了",
		"zb": "Tormented",
		"emoji": "😆"
	},
	"衰": {
		"cn": "衰",
		"zb": "Toasted",
		"emoji": "😆"
	},
	"骷髅": {
		"cn": "骷髅",
		"zb": "Skull",
		"emoji": "💀"
	},
	"敲打": {
		"cn": "敲打",
		"zb": "Hammer",
		"emoji": "😬"
	},
	"再见] ": {
		"cn": "再见] ",
		"zb": "Wave] ",
		"emoji": "😘"
	},
	"擦汗": {
		"cn": "擦汗",
		"zb": "Speechless",
		"emoji": "😆"
	},
	"抠鼻": {
		"cn": "抠鼻",
		"zb": "NosePick",
		"emoji": "😆"
	},
	"鼓掌": {
		"cn": "鼓掌",
		"zb": "Clap",
		"emoji": "👏"
	},
	"糗大了": {
		"cn": "糗大了",
		"zb": "Shame",
		"emoji": "😆"
	},
	"坏笑": {
		"cn": "坏笑",
		"zb": "Trick",
		"emoji": "😆"
	},
	"左哼哼": {
		"cn": "左哼哼",
		"zb": "Bah!L",
		"emoji": "😆"
	},
	"右哼哼": {
		"cn": "右哼哼",
		"zb": "Bah!R",
		"emoji": "😆"
	},
	"哈欠": {
		"cn": "哈欠",
		"zb": "Yawn",
		"emoji": "😆"
	},
	"鄙视": {
		"cn": "鄙视",
		"zb": "Pooh-pooh",
		"emoji": "😆"
	},
	"委屈] ": {
		"cn": "委屈",
		"zb": "Shrunken",
		"emoji": "😆"
	},
	"快哭了": {
		"cn": "快哭了",
		"zb": "TearingUp",
		"emoji": "😆"
	},
	"阴险": {
		"cn": "阴险",
		"zb": "Sly",
		"emoji": "😆"
	},
	"亲亲": {
		"cn": "亲亲",
		"zb": "Kiss",
		"emoji": "😘"
	},
	"吓": {
		"cn": "吓",
		"zb": "Wrath",
		"emoji": "😓"
	},
	"可怜": {
		"cn": "可怜",
		"zb": "Whimper",
		"emoji": "😆"
	},
	"菜刀": {
		"cn": "菜刀",
		"zb": "Cleaver",
		"emoji": "🔪"
	},
	"西瓜": {
		"cn": "西瓜",
		"zb": "Watermelon",
		"emoji": "🍉"
	},
	"啤酒": {
		"cn": "啤酒",
		"zb": "Beer",
		"emoji": "🍺"
	},
	"篮球": {
		"cn": "篮球",
		"zb": "Basketball",
		"emoji": "🏀"
	},
	"乒乓] ": {
		"cn": "乒乓",
		"zb": "PingPong",
		"emoji": "⚪"
	},
	"咖啡": {
		"cn": "咖啡",
		"zb": "Coffee",
		"emoji": "☕"
	},
	"饭": {
		"cn": "饭",
		"zb": "Rice",
		"emoji": "🍚"
	},
	"猪头": {
		"cn": "猪头",
		"zb": "Pig",
		"emoji": "🐷"
	},
	"玫瑰": {
		"cn": "玫瑰",
		"zb": "Rose",
		"emoji": "🌹"
	},
	"凋谢": {
		"cn": "凋谢",
		"zb": "Wilt",
		"emoji": "🌹"
	},
	"嘴唇": {
		"cn": "嘴唇",
		"zb": "Lips",
		"emoji": "👄"
	},
	"爱心": {
		"cn": "爱心",
		"zb": "Heart",
		"emoji": "💗"
	},
	"心碎": {
		"cn": "心碎",
		"zb": "BrokenHeart",
		"emoji": "💔"
	},
	"蛋糕": {
		"cn": "蛋糕",
		"zb": "Cake",
		"emoji": "🎂"
	},
	"闪电] ": {
		"cn": "闪电] ",
		"zb": "Lightning",
		"emoji": "⚡"
	},
	"炸弹": {
		"cn": "炸弹",
		"zb": "Bomb",
		"emoji": "💣"
	},
	"刀": {
		"cn": "刀",
		"zb": "Dagger",
		"emoji": "🗡"
	},
	"足球": {
		"cn": "足球",
		"zb": "Soccer",
		"emoji": "⚽"
	},
	"瓢虫": {
		"cn": "瓢虫",
		"zb": "Ladybug",
		"emoji": "🐞"
	},
	"便便": {
		"cn": "便便",
		"zb": "Poop",
		"emoji": "💩"
	},
	"月亮": {
		"cn": "月亮",
		"zb": "Moon",
		"emoji": "🌙"
	},
	"太阳": {
		"cn": "太阳",
		"zb": "Sun",
		"emoji": "☀"
	},
	"礼物": {
		"cn": "礼物",
		"zb": "Gift",
		"emoji": "🎁"
	},
	"拥抱": {
		"cn": "拥抱",
		"zb": "Hug",
		"emoji": "🤗"
	},
	"强] ": {
		"cn": "强",
		"zb": "ThumbsUp",
		"emoji": "👍"
	},
	"弱": {
		"cn": "弱",
		"zb": "ThumbsDown",
		"emoji": "👎"
	},
	"握手": {
		"cn": "握手",
		"zb": "Shake",
		"emoji": "👍"
	},
	"胜利": {
		"cn": "胜利",
		"zb": "Peace",
		"emoji": "✌"
	},
	"抱拳": {
		"cn": "抱拳",
		"zb": "Fight",
		"emoji": "✊"
	},
	"勾引": {
		"cn": "勾引",
		"zb": "Beckon",
		"emoji": "✌"
	},
	"拳头": {
		"cn": "拳头",
		"zb": "Fist",
		"emoji": "✊"
	},
	"差劲": {
		"cn": "差劲",
		"zb": "Pinky",
		"emoji": "✌"
	},
	"爱你": {
		"cn": "爱你",
		"zb": "RockOn",
		"emoji": "✌"
	},
	"NO": {
		"cn": "NO",
		"zb": "Nuh-uh",
		"emoji": "✌"
	},
	"OK": {
		"cn": "OK",
		"zb": "OK",
		"emoji": "🙂"
	},
	"嘿哈": {
		"cn": "嘿哈",
		"zb": "Hey",
		"emoji": "🙂"
	},
	"捂脸": {
		"cn": "捂脸",
		"zb": "Facepalm",
		"emoji": "🙂"
	},
	"奸笑": {
		"cn": "奸笑",
		"zb": "Smirk",
		"emoji": "🙂"
	},
	"机智": {
		"cn": "机智",
		"zb": "Smart",
		"emoji": "🙂"
	},
	"皱眉": {
		"cn": "皱眉",
		"zb": "Concerned",
		"emoji": "🙂"
	},
	"耶": {
		"cn": "耶",
		"zb": "Yeah!",
		"emoji": "🙂"
	},
	"吃瓜": {
		"cn": "吃瓜",
		"zb": "Onlooker",
		"emoji": "🙂"
	},
	"加油": {
		"cn": "加油",
		"zb": "GoForIt",
		"emoji": "🙂"
	},
	"汗": {
		"cn": "汗",
		"zb": "Sweats",
		"emoji": "🙂"
	},
	"天啊": {
		"cn": "天啊",
		"zb": "OMG",
		"emoji": "👌"
	},
	"社会社会": {
		"cn": "社会社会",
		"zb": "Respect",
		"emoji": "🙂"
	},
	"旺柴": {
		"cn": "旺柴",
		"zb": "Doge",
		"emoji": "🙂"
	},
	"好的": {
		"cn": "好的",
		"zb": "NoProb",
		"emoji": "🙂"
	},
	"哇": {
		"cn": "哇",
		"zb": "Wow",
		"emoji": "🙂"
	}
}

5.发送快捷信息Commonly.vue

<template>
    <div class="bg" @mouseleave="isCommonly=0">
        <div v-for="item in CommonlyArr">
            <div :class="{Commonly:true,isCommonly:item.index==isCommonly}" @click="BtnSubmit(item)" @mouseenter="BtnCurrr(item)">
                {{item.index}}.{{item.msg}}
            </div>
        </div>
    </div>
</template>
<script setup>
    import Busevent from "@/assets/Busevent.js";
    const CommonlyArr=ref([
        {index:1,msg:"嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡嗡"},
        {index:2,msg:"qqqqqqqqqqq"},
        {index:3,msg:"rrrrrrrrrrrrrr"},
        {index:4,msg:"eeeeeeeeeeeeeeee"},
        {index:5,msg:"eeeeeeeeeeeeeeee"},
        {index:6,msg:"eeeeeeeeeeeeeeee"},
        {index:7,msg:"eeeeeeeeeeeeeeee"}
    ])
    // 选中样式
    const isCommonly=ref(null)
    const BtnCurrr=(e)=>{
        isCommonly.value=e.index
    }
    //确认
    const BtnSubmit=(e)=>{
        Busevent.emit("Commonly",e)
    }
</script>
<style scoped>
    .bg{
        background-color:#E0EAFD;
        height: 150px;
        width: 240px;
        overflow: hidden;
        overflow-y: scroll;
        border-radius: 5px;
        box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
    }
    .bg::before{
        content: '';
        position: absolute;
        width: 0;
        height: 0;
        bottom: -20px;
        left: 1px;
        border: 10px solid;
        border-color: transparent #E0EAFD transparent transparent ;
        box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
        transform: rotate(-90deg);
    }
    /*滚动条整体样式*/
     .bg::-webkit-scrollbar {
        /*高宽分别对应横竖滚动条的尺寸*/
        width: 0px; 
        height: 0px;
    }
    /*滚动条里面小方块*/
    .bg::-webkit-scrollbar-thumb {
        border-radius: 5px;
        -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
        background: rgba(0,0,0,0.2);
        display: none;
    }
    /*滚动条里面轨道*/
    .bg::-webkit-scrollbar-track {
        -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
        border-radius: 0;
        background: rgba(0,0,0,0.1);
        display: none;
     }
     .Commonly{
        padding:5px 20px;
        cursor: pointer;
     }
     .isCommonly{
        background: gainsboro;
        color: #fff;
     }
</style>

6.发送图片

<template>
    <el-upload
      v-model:file-list="fileList"
      class="upload-demo"
      action=""
      :http-request="uploadImg"
      accept=".jpg,.jpeg,.png,.gif"
      :show-file-list="false"
      :before-upload="onChange"
    >
      <img src="./logo.png" alt="" srcset="" class="w30">
    </el-upload>
  </template>
  <script lang="ts" setup>
  import { ref } from 'vue'
  import {uploadFile} from "./Upfile.js"
  import Busevent from "@/assets/Busevent.js";
  const uploadImg=(val)=>{
    uploadFile(val).then(res=>{
        console.log(res);
        Busevent.emit("uploadImg",res)
    }).catch(err=>{
      Busevent.emit("uploadImg",err)
    })
  }
  </script>
  

6.1上传图片

import { ElMessage } from 'element-plus'
import axios from "axios"
/**
 * before-upload上传文件之前的钩子
 * @param {file} file 
 * @param {类型} type 
 * @param {大小} size
 */
 export const beforeupload=(file,type,size)=>{
    const isType=file.type===type 
    const issize=file.size/1024/1024<size
    if (!isType) {
        ElMessage.error("上传头像图片只能是"+type+"格式!")
        return false
    }
    if (!issize) {
        ElMessage.error("上传头像图片大小不能超过"+type+"MB!")
        return false
    }
    return true
}
/**
 * 上传成功
 * @param {file} file 
 */
export const handleAvatarSuccess=(file)=>{
    console.log(file);
    // return file.data
}
/**
 * 
 * @param {file} files 
 * @param {路径} url 
 * @param {token} token 
 * @returns 
 */
export const uploadFile=(files,url,token)=>{
    // let imgurl=url+''
    //base64
    let reader = new FileReader();
    reader.readAsDataURL(files.file)
    let base64
    reader.onload=e=>{
        base64=e.target.result
        // console.log(base64);
    }
    //正常上传
    let imgurl="http://xxxx"
//请求头
    http.defaults.headers["Authorization"] ='Bearer'+ token;
    const formData=new FormData()
    formData.append("file",files.file)
    return new Promise((resolve, reject) => {
        http({
            url:imgurl,
            data:formData,
            method:'post',
        }).then(res=>{
            resolve(res.data.data)
        }).catch(err=>{
            reject(base64)
        })
    })
}

7.Content.vue内容展示

<template>
   <div class="scrollBox" >
    <div style="text-align:center" class="mt10 mb10">
        <!-- {{store.state.OnMsg}} -->
    {{DateTime}}
    </div>
    <div v-for="item in Message" >
        <div v-if="item.userId!=''">
            <div class="row mb20"  v-if="item.sendId!=userId">
                <div class="headimg">
                   <img :src="item.headImg" alt=""  class="headimg">
               </div>
                <div class="boxMsg">
                   <div v-if="item.type=='img'" class="p">
                       <el-image 
                       class='el-image' 
                       style='width: 100px' 
                       :src='item.msg' 
                       lazy :zoom-rate='1.2' 
                       :preview-src-list='srcList' 
                       @click="BtnImg(item.msg)"
                       fit='contain'/>
                   </div>
                   <p v-html="item.msg" v-else></p>
                </div>
              </div>
              <!-- 我send -->
              <div class="myMessage mb20" v-else>
               <div class="myheadimg">
                   <img :src="item.headImg" alt="" class="myheadimg">
               </div>
               <div class="boxMsg">
                   <div v-if="item.type=='img'" class="p">
                       <el-image 
                       class='el-image' 
                       style='width: 100px; ' 
                       :src='item.msg' 
                       lazy :zoom-rate='1.2' 
                       :preview-src-list='srcList' 
                       @click="BtnImg(item.msg)"
                       fit='contain'/>
                    </div>
                   <p v-html="item.msg" v-else></p>
               </div>
              </div>
        </div>
       <!-- {{msg}} -->
    </div>
    <!-- 默认置底 -->
   </div>
   
</template>
<script setup>
    import {ref,onMounted,watch} from "vue"
    import {useStore} from "vuex"
    const store=new useStore()
    
    // const msg=ref(webSocket.onMessage(localStorage.getItem('userId')))
    //预览图片
    const srcList = ref([])
    const BtnImg=(e)=>{
        srcList.value=[]
        srcList.value.push(e)
    }
    //type:img:图片,msg:消息,pdf:简历,phone:手机号码,wx:微信
    const Message=ref(store.state.OnMsg)
    //时间
    const DateTime=ref(new Date().toLocaleString('zh', { hour12: false }).replaceAll('/', '-'))
</script>
<style scoped>


    .scrollBox{
        max-height: 480px;
        min-height: 480px;
        overflow: hidden;
        overflow-y: scroll;
        /* -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2); */
        
    }
    /*滚动条整体样式*/
    .scrollBox::-webkit-scrollbar {
        width: 4px; /*高宽分别对应横竖滚动条的尺寸*/
        height: 4px;
    }
    /*滚动条里面小方块*/
    .scrollBox::-webkit-scrollbar-thumb {
        border-radius: 5px;
        -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
        background: rgba(0,0,0,0.2);
        display: none;
    }
    /*滚动条里面轨道*/
    .innerbox::-webkit-scrollbar-track {
        -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
        border-radius: 0;
        background: rgba(0,0,0,0.1);
     }
    .boxMsg{
        max-width: 350px;
        background-color:#E0EAFD;
        border-radius: 2px;
        padding: 6px;
        margin: 0px;
        box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
    }
    .myMessage{
      display: flex;
      flex-direction:row-reverse;
    }
    .headimg{
        width: 35px;
        height: 35px;
        border-radius: 50%;
        margin-right: 10px;
    }
    .myheadimg{
        width: 35px;
        height: 35px;
        border-radius: 50%;
        margin-left: 5px;
        margin-right: 10px;
    }
    p,.p{
      padding-top:6px;
      padding-left: 3px;
      padding-right: 3px;
      
    }
</style>

后端

<!--WebSocket-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
            <scope>true</scope>
        </dependency>
        <!--Hutool工具-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.9</version>
        </dependency>

1.WebSocketServer 

package com.appapi.websocket;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.appapi.comm.R;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import io.netty.util.internal.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.socket.WebSocketSession;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Resource;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @Author: 文君
 * @Date: 2023-08-30-8:14
 * @Description:
 */
@ServerEndpoint(value = "/websocket/{userId}")
@Component
@Slf4j
public class WebSocketServer {
    //redis
    @Autowired
    private RedisTemplate redisTemplate;

    private static Logger L=  LoggerFactory.getLogger(WebSocketServer.class);
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    //用户ID
    private String userId;
    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    //虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
    //  注:底下WebSocket是当前类名
    private static CopyOnWriteArraySet<WebSocketServer> webSockets =new CopyOnWriteArraySet<>();
    // 用来存在线连接用户信息
    private static ConcurrentHashMap<String,Session> sessionPool = new ConcurrentHashMap<String,Session>();
    /**
     * @Description 链接建立
     * @Param session
     * @Param userId
     * @Return
     * @Author wenjun
     * @Date 2023/8/30 8:20
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId){
        try {
            this.session = session;
            this.userId = userId;
            webSockets.add(this);
            sessionPool.put(userId, session);
            log.info("【websocket消息】有新的连接,总数为:"+webSockets.size());
        } catch (Exception e) {
            log.info(userId+"上线的时候通知所有人发生了错误");
        }
    }

    /**
     * @Description 链接关闭
     * @Param
     * @Return
     * @Author wenjun
     * @Date 2023/8/30 8:29
     */
    @OnClose
    public void onClose(){
        try {
            webSockets.remove(this);
            sessionPool.remove(this.userId);
            log.info("【websocket消息】连接断开,总数为:"+webSockets.size());
            System.out.println("链接关闭");
        } catch (Exception e) {

        }
    }

    /**
     * @Description 接收信息
     * @Param message
     * @Param session
     * @Return
     * @Author wenjun
     * @Date 2023/8/30 8:32
     */
    @OnMessage
    public void onMessage(String message,Session session){
        log.info("【websocket消息】收到客户端消息:"+message);
//       try{

//       }catch (Exception e){
//           e.printStackTrace();
//       }
    }

    /**
     * @Description 离线消息
     * @Param userId
     * @Param message
     * @Return
     * @Author wenjun
     * @Date 2023/8/31 11:29
     */
    public void sendOfflineMessage(String userId){
        try {
            //发送离线消息
            Object[] range = redisTemplate.opsForList().range(userId, 0, -1).toArray();
            Session session = sessionPool.get(userId);
            int RangIndex=range.length-1;
            if (session != null&&session.isOpen()) {
                for (int i = 0; i <range.length; i++) {
                    session.getAsyncRemote().sendText(range[i].toString());
                    System.out.println(range[i]);
                    System.out.println(i<range.length);
                    System.out.println(RangIndex+"RangIndex");
                    System.out.println(i+"i");
                    if (i==RangIndex){
                        //删除离线记录
                        redisTemplate.delete(userId);
                        return;
                    }
                }
            }else {
                log.info("用户:{}不在线",userId);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @Description 此为广播消息(全体发送)
     * @Param message
     * @Return
     * @Author wenjun
     * @Date 2023/8/30 9:17
     */
    public void sendAllMessage(String message) {
        log.info("【websocket消息】广播消息:"+message);
        for(WebSocketServer webSocket : webSockets) {
            try {
                if(webSocket.session.isOpen()) {
                    webSocket.session.getAsyncRemote().sendText(message);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * @Description 此为单点消息
     * @Param userId
     * @Param message
     * @Return
     * @Author wenjun
     * @Date 2023/8/30 9:17
     */
    public void sendOneMessage(String userId, String message) {
       try {
           Session session = sessionPool.get(userId);
           if (session != null&&session.isOpen()) {
               try {
                   log.info("【websocket消息】 单点消息:"+message);
                   session.getAsyncRemote().sendText(message);
               } catch (Exception e) {
                   e.printStackTrace();
               }
           }else {
               log.info("用户:{}不在线",userId);
               JSONObject obj = JSONObject.parseObject(message);
               redisTemplate.opsForList().leftPush(userId,obj);
           }
       }catch (Exception e) {
           e.printStackTrace();
       }


    }

    /**
     * @Description 此为单点消息(多人)
     * @Param userIds
     * @Param message
     * @Return
     * @Author wenjun
     * @Date 2023/8/30 9:17
     */
    public void sendMoreMessage(String[] userIds, String message) {
        for(String userId:userIds) {
            Session session = sessionPool.get(userId);
            if (session != null&&session.isOpen()) {
                try {
                    log.info("【websocket消息】 单点消息:"+message);
                    session.getAsyncRemote().sendText(message);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }else {
                log.info("用户:{}不在线",userId);
            }
        }
    }
    /**
     * @Description 发送错误时的处理
     * @Param session
     * @Param error
     * @Return
     * @Author wenjun
     * @Date 2023/8/30 8:58
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("用户错误,原因:"+error.getMessage());
        error.printStackTrace();
    }

}

2.ChatMessage

package com.appapi.websocket;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import lombok.Data;

import java.time.LocalDateTime;
import java.util.List;

/**
 * @Author: 文君
 * @Date: 2023-08-30-11:47
 * @Description:
 */
@Data
@ApiModel(value = "ChatMessage对象", description = "消息")
public class ChatMessage {

    @ApiModelProperty("收信人")
    private String userId;

    @ApiModelProperty("多个收信人")
    private String[] userIds;

    @ApiModelProperty("消息")
    private String msg;

    @ApiModelProperty("消息类型")
    private String type;

    @ApiModelProperty("用户头像")
    private String headImg;

    @ApiModelProperty("发信人")
    private String sendId;

    @ApiModelProperty("发送时间")
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @TableField(value = "create_time",fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
}

3.ChatController

package com.appapi.websocket;

import com.alibaba.fastjson2.JSONObject;
import com.appapi.comm.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.websocket.server.PathParam;
import java.time.LocalDateTime;

/**
 * @Author: 文君
 * @Date: 2023-08-30-11:37
 * @Description:
 */
@Slf4j
@RestController
@RequestMapping("/Chat")
@Api(tags = "WebSocket")
public class ChatController {
    @Resource
    private WebSocketServer webSocketServer;
    /**
     * @Description 单发信息通过id
     * @Param chatMessage
     * @Return {@link R< ChatMessage>}
     * @Author wenjun
     * @Date 2023/8/30 14:19
     */
    @PostMapping("/sendOneMessage")
    @ApiOperation("单发信息通过id")
    public R<ChatMessage> OneMessage(@RequestBody ChatMessage chatMessage){
        chatMessage.setCreateTime(LocalDateTime.now());
        JSONObject obj=JSONObject.from(chatMessage);
        webSocketServer.sendOneMessage(chatMessage.getUserId(),obj.toJSONString());
        log.info(chatMessage.toString());
        return R.success();
    }
    /**
     * @Description 多发信息通过id
     * @Param chatMessage
     * @Return {@link R< ChatMessage>}
     * @Author wenjun
     * @Date 2023/8/30 14:21
     */
    @PostMapping("/sendMoreMessage")
    @ApiOperation("多发信息通过id")
    public R<ChatMessage> MoreMessage(@RequestBody ChatMessage chatMessage){
        chatMessage.setCreateTime(LocalDateTime.now());
        JSONObject obj=JSONObject.from(chatMessage);
        webSocketServer.sendMoreMessage(chatMessage.getUserIds(),obj.toJSONString());
        log.info(chatMessage.toString());
        return R.success();
    }
    /**
     * @Description 全部发送
     * @Param chatMessage
     * @Return {@link R< ChatMessage>}
     * @Author wenjun
     * @Date 2023/8/31 11:35
     */
    @PostMapping("/sendAllMessage")
    @ApiOperation("全部发送")
    public R<ChatMessage> AllMessage(@RequestBody ChatMessage chatMessage){
        chatMessage.setCreateTime(LocalDateTime.now());
        //实体转json
        JSONObject obj=JSONObject.from(chatMessage);
        webSocketServer.sendAllMessage(obj.toJSONString());
        log.info(chatMessage.toString());
        return R.success();
    }
    /**
     * @Description 离线消息
     * @Param userId
     * @Return {@link R< ChatMessage>}
     * @Author wenjun
     * @Date 2023/8/31 11:35
     */
    @GetMapping("/sendOfflineMessage/{userId}")
    @ApiOperation("离线消息")
    public R<ChatMessage> OfflineMessage(@PathVariable String userId){
        webSocketServer.sendOfflineMessage(userId);
        return R.success();
    }

}

;