vue 使用mqtt模仿微信聊天,可多人聊天,历史记录查询,聊天独立界面

<template>
  <div class="msg-all">
    <div class="message-box" ref="msgBox">
      <h4 style="height:20px">{{ name }}</h4>
      <div class="message-list" ref="messageContainer" @click="handleClickInside" id="topDiv"
        :style="{ height: topDivHeight }">
        <div v-for="message in currentMessages" :key="message.id"  :class="{ 'message': true, 'sent': message.sent }">
          <div class="message-info">{{ message.name }}</div>
          <div class="message-bubble" @contextmenu.prevent="$event.preventDefault(); showContextMenu($event, message)">
            {{message.content }}
          </div>
        </div>
        <div v-if="contextMenu.visible" class="context-menu"
          :style="{ top: contextMenu.top + 'px', left: contextMenu.left + 'px' }">
          <div class="context-menu-item" @click="deleteMessage">删除</div>
          <div class="context-menu-item" @click="deleteMessage" v-if="withinTwoMinutes()">撤回</div>
        </div>
      </div>
      <div class="resize" id="midResizeDiv" @mousedown.stop="startResize"></div>
      <div class="input-area" id="bottomDiv" :style="{ height: bottomDivHeight }">
        <div>
          <el-popover placement="top" :width="401" trigger="click" v-model:visible="popoverVisible">
            <template #reference>
              <el-icon style="font-size:20px;margin:5px;" title="表情">
                <PictureRounded />
              </el-icon>
            </template>
            <div>
              <div v-for="(item, index) in emojis" :key="index" @click="onEmojiSelect(item)" class="emoji">
                {{ item }}
              </div>
            </div>
          </el-popover>
          <el-icon style="font-size:20px;margin:5px;" @click="isHistory = !isHistory" title="历史记录">
            <Clock />
          </el-icon>
          <el-icon style="font-size:20px;margin:5px;" @click="handlerecovery" title="恢复">
            <Refresh />
          </el-icon>
        </div>
        <el-input v-model="newMessage" type="textarea" @keyup.enter.prevent="sendMessage" autofocus
          ref="messageInput" />
        <button @click="sendMessage">发送</button>
      </div>
    </div>
    <div class="msg-History" v-if="isHistory">
      <div class="datepicker">
        <el-date-picker v-model="selectedDate" type="date" :editable="false" :clearable="false"
          :disabledDate="disabledDate" @change="scrollToMessage"/>     
      </div>
      <div style="height: calc(100vh - 96px);overflow: auto;" ref="messageHistory" @scroll="handleScroll">
        <div v-for="message in currenthistoryMessages" :key="message.id" :id="'message'+'-'+ message.id" :class="{ 'message': true, }" style="text-align:left">
          <div class="message-info">
            <span>{{ message.name }}</span>
            <span style="margin-left: 8px;">{{timestampToTime(message.id)}}</span>
          </div>
          <div class="message-bubble">
            {{message.content }}
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import notify from "devextreme/ui/notify";
import Bus from '@/utils/bus.ts';
import mqtt from 'mqtt';
import { mqttMessageCache, queryMessagebyScore, deleteMessage,recoverMessage } from "@/api/message.js"
import { emojis } from "./expression.js"
var client
const options = {
  port: 8000,
  connectTimeout: 4000,
  clientId: "mqtt_" + Math.random().toString(16).substr(2, 8),
  username: "admin-user",
  password: "admin-password",
}
client = mqtt.connect('ws://192.168.11.136/mqtt', options)
export default {
  components: {
    // Vue3EmojiPicker
  },
  data() {
    return {
      selectedDate: new Date(),
      topDivHeight: '77%', // 顶部盒子的初始高度
      bottomDivHeight: '20%', // 底部盒子的初始高度
      minPercentageHeight: 30, // 最小高度百分比
      isResizing: false, // 是否正在拖拽中
      popoverVisible: false,
      emojis: emojis,
      messagesMap: {},
      newMessage: '',
      messageForm: {},
      name: "",
      topicValue: "",
      subscribed: false,
      topic: "",
      uniqueIds: [],//关联的所有会话对象id
      accep_id: "",
      FocuUserInfo: {},
      msgObj: {},
      contextMenu: { // 消息操作弹窗状态
        visible: false,
        top: 0,
        left: 0,
      },
      itemObj: {},
      isHistory: false,
      timestamps: [],
      messagesClone: {}
    };
  },
  watch: {
    isHistory(val) {
      if (val == true) {
        this.$nextTick(() => {
            const messageContainer = this.$refs.messageHistory;
            if (messageContainer) {
              messageContainer.scrollTop = messageContainer.scrollHeight;
            }
          });
      }
    }
  },
  created() {
    this.initMqtt()
    this.topic = sessionStorage.getItem('topic') || ''; // 初始化 topic
    this.FocuUserInfo = JSON.parse(sessionStorage.getItem('FocuUserInfo')) || {};//获取当前登录人的id
    Bus.on("msgList", e => {
      const arr = [];
      if (e.groupSession) {
        arr.push(...e.groupSession);
      }
      if (e.personSession) {
        arr.push(...e.personSession);
      }
      if (e.discussion_session) {
        arr.push(...e.discussion_session);
      }

      this.uniqueIds = Array.from(new Set(arr.map(item => item._id)));
    })

    Bus.on("itemData", e => {
      this.initMqtt()
      this.itemObj = e
      this.topic = sessionStorage.getItem('topic') || ''; // 初始化 topic
      this.FocuUserInfo = JSON.parse(sessionStorage.getItem('FocuUserInfo')) || {};//获取当前登录人的id
      //选择不同的对话对象
      this.name = e._name
      this.topicValue = e.node_id_session
      if (!this.messagesMap.hasOwnProperty(e._id)) {
        this.messagesMap[e._id] = [];
      }
      this.accep_id = e._shortcut ? e._shortcut : e._id
      let params = {}
      if (e.isGroup) {
        //群组
        params = {
          "sendKey": this.accep_id, //发送者id_接收者id
          "receiverKey": this.accep_id //接收着_发送者
        }
      } else {
        const id =this.FocuUserInfo._shortcut?this.FocuUserInfo._shortcut:this.FocuUserInfo._id
        params = {
          "sendKey": id + "_" + this.accep_id, //发送者id_接收者id
          "receiverKey": this.accep_id + "_" + id //接收着_发送者
        }
      }

      this.queryMessagebyScore(params)
    })
    
  },
  mounted() {
    document.addEventListener('click', this.handleClickOutside);
  },
  computed: {
    currentMessages() {
      return this.messagesMap[this.accep_id] || [];
    },
    currenthistoryMessages() {
      return this.messagesClone[this.accep_id] || [];
    }
  },
  methods: {
    handlerecovery() {
      const id =this.FocuUserInfo._shortcut?this.FocuUserInfo._shortcut:this.FocuUserInfo._id
      let params = {
        key:id + "_" + this.accep_id
      }
      recoverMessage(params).then(res => {
        if (this.itemObj.isGroup) {
        //群组
        params = {
          "sendKey": this.accep_id, //发送者id_接收者id
          "receiverKey": this.accep_id //接收着_发送者
        }
      } else {
        const id =this.FocuUserInfo._shortcut?this.FocuUserInfo._shortcut:this.FocuUserInfo._id
        params = {
          "sendKey": id + "_" + this.accep_id, //发送者id_接收者id
          "receiverKey": this.accep_id + "_" + id //接收着_发送者
        }
      }

      this.queryMessagebyScore(params)
      })
    },
    handleScroll() {
      const currenthistoryMessages = this.$refs.currenthistoryMessages;
      const todayDate = new Date().toDateString();
      let currentDate = todayDate;
      let visibleMessageDate = null;

      // 遍历消息列表中的每条消息
      for (let i = 0; i < this.currenthistoryMessages.length; i++) {
        const messageElement =document.getElementById(`message-${this.currenthistoryMessages[i].id}`)
        // 如果消息在可见区域内,则更新可见消息的日期并跳出循环
        if (this.isElementInViewport(messageElement)) {
          this.selectedDate=Number(this.currenthistoryMessages[i].id);
          break;
        }
      }
    },
    isElementInViewport(element) {
      if (!element) return false;
      const rect = element.getBoundingClientRect();
      return (
        rect.top >= 0 &&
        rect.left >= 0 &&
        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
        rect.right <= (window.innerWidth || document.documentElement.clientWidth)
      );
    },
    scrollToMessage() {
      const selectedDate = this.getNewTime(this.selectedDate);
      const messages = this.currenthistoryMessages;
      for (let i = 0; i < messages.length; i++) {
        const messageDate = this.getNewTime(messages[i].id); // 假设消息的日期存储在message.date中
        if (selectedDate == messageDate) {
          const messageElement = document.getElementById(`message-${messages[i].id}`); // 假设每个消息有一个唯一的id
          if (messageElement) {
            messageElement.scrollIntoView({ behavior: "auto", block: "nearest" });
            // window.scrollTo(0, window.scrollY);
            break;
          }
        }
      }
    },
    disabledDate(date) {
      // 将日期转换为时间戳,并检查是否在时间戳数组中
      const timestamp = date.getTime();
      return !this.timestamps.includes(timestamp);
    },
    startResize(event) {
      this.isResizing = true;
      const startY = event.clientY;
      const startTopHeight = parseFloat(this.topDivHeight);
      const startBottomHeight = parseFloat(this.bottomDivHeight);
      const containerHeight = this.$refs.msgBox.clientHeight; // 获取容器高度
      console.log(containerHeight)
      // 在移动和松开鼠标时进行相应的操作
      const handleMouseMove = (moveEvent) => {
        if (!this.isResizing) return;
        const deltaY = (moveEvent.clientY - startY) / (containerHeight) * 100;
        // 计算新的高度
        // let Gheight = 24 /containerHeight *100
        let newTopHeight = startTopHeight + deltaY;
        let newBottomHeight = startBottomHeight - deltaY;
        console.log(newTopHeight)
        // 应用最小高度限制
        if (newTopHeight < this.minPercentageHeight) {
          newTopHeight = this.minPercentageHeight;
          newBottomHeight = 100 - this.minPercentageHeight;
        } else if (newBottomHeight < this.minPercentageHeight) {
          newTopHeight = 100 - this.minPercentageHeight;
          newBottomHeight = this.minPercentageHeight;
        }

        // 更新高度
        this.topDivHeight = newTopHeight + '%';
        this.bottomDivHeight = newBottomHeight + '%';
      };

      const handleMouseUp = () => {
        this.isResizing = false;
        // 移除事件监听器
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      };

      // 添加事件监听器
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    },
    withinTwoMinutes() {
      // 当前时间戳
      const currentTimestamp = new Date().getTime();
      // 两分钟的毫秒数
      const twoMinutesInMilliseconds = 2 * 60 * 1000;
      // 计算时间差
      const timeDifference = Math.abs(currentTimestamp - this.msgObj.id);
      return timeDifference <= twoMinutesInMilliseconds;
    },
    deleteMessage() {
      const id =this.FocuUserInfo._shortcut?this.FocuUserInfo._shortcut:this.FocuUserInfo._id
      let params = {
        time: this.msgObj.id,
        key: this.msgObj.sent == false ? this.accep_id + "_" + id : id + "_" + this.accep_id
      }
      deleteMessage(params).then(res => {
        if (res.data.code == 200) {
          const index = this.messagesMap[this.accep_id].findIndex(item => item.id === this.msgObj.id);
          if (index !== -1) {
            this.messagesMap[this.accep_id].splice(index, 1);
          }
        }
      })
    },
    showContextMenu(event, message) {
      // 阻止默认的右键菜单事件
      event.preventDefault();
      // 将当前消息的 showContextMenu 属性设为 true,显示删除按钮
      // message.showContextMenu = true;
      this.contextMenu.visible = true;
      this.contextMenu.top = event.clientY -
       20;
      this.contextMenu.left = event.layerX;
      this.msgObj = message
    },
    onEmojiSelect(emoji) {
      this.newMessage += emoji
      this.popoverVisible = false
    },
    initMqtt() {
      client.on('connect', (e) => {
        if (e.returnCode == 0) {
          setTimeout(() => {
            if (!this.subscribed) {
              const ids = this.FocuUserInfo?.discussion_session?.map(session => session.id) || [];
              ids.push(this.topic)
              client.subscribe(ids, { qos: 0 }, (error) => {
                if (error) {
                  console.error('订阅失败:', error);
                } else {
                  console.log('订阅成功');
                  this.subscribed = true;
                }
              })
            }
          }, 1000)
        } else {
          this.reconnect()
        }
      })
      client.on('message', (topic, message) => {
        console.log('收到来自' + topic, '的消息', message.toString())
        try {
          const msgObj = JSON.parse(message.toString())
          console.log(msgObj)
          if (!this.messagesMap.hasOwnProperty(msgObj._id)) {
            this.messagesMap[msgObj._id] = [];
          }
          if (this.messageForm.id != msgObj.id && this.messageForm.name != msgObj.name) {
            this.messagesMap[msgObj._id].push(msgObj);
          }
          this.$nextTick(() => {
            const messageContainer = this.$refs.messageContainer;
            if (messageContainer) {
              messageContainer.scrollTop = messageContainer.scrollHeight;
            }
          });
        } catch (error) {
          // console.error('解析消息失败:', error);
        }
      })
    },
    reconnect() {
      client.end(true, () => {
        client.reconnect();
      });
    },
    sendMessage() {
      if (this.topicValue === '') {
        notify("请选择发送对象", "warning", 2000, {
          direction: "up-push",
        });
        return
      } else if (!this.newMessage.trim()) {
        notify("请输入发送内容", "warning", 2000, {
          direction: "up-push",
        });
        return
      }
      const id =this.FocuUserInfo._shortcut?this.FocuUserInfo._shortcut:this.FocuUserInfo._id
      this.messageForm = {
        id: Date.now(),
        content: this.newMessage,
        name: this.FocuUserInfo._name,
        _id: this.itemObj.isGroup ? this.itemObj._id : id,
      };
      //this.topicValue 发送对象的主题
      client.publish(this.topicValue, JSON.stringify(this.messageForm), { qos: 0 }, (err) => {
        if (err) {
          console.error('Error publishing message:', err);
        } else {
          if (!this.messagesMap.hasOwnProperty(this.accep_id)) {
            this.messagesMap[this.accep_id] = [];
          }
          this.messagesMap[this.accep_id].push({
            id: Date.now(),
            content: this.newMessage,
            name: this.messageForm.name,
            sent: true,
            showContextMenu: false
          });
          if (!this.messagesClone.hasOwnProperty(this.accep_id)) {
            this.messagesClone[this.accep_id] = []
          }
          this.messagesClone[this.accep_id].push({
            id: Date.now(),
            content: this.newMessage,
            name: this.messageForm.name,
            sent: true,
            showContextMenu: false
            });
          let params = {
            "sender": id, //发送者id
            "receiver": this.accep_id,//接收者id
            "content": this.newMessage, //消息内容
            "time": Date.now(), //发送消息时间
            "type": "0", //0文本 1 表情包
            "senderName": this.FocuUserInfo._name //发送人的名称
          }
          this.mqttMessageCache(params)
          this.newMessage = '';
          this.$refs.messageInput.focus();
          this.$nextTick(() => {
            const messageContainer = this.$refs.messageContainer;
            messageContainer.scrollTop = messageContainer.scrollHeight;
          });
        }
      });
    },
    mqttMessageCache(params) {
      mqttMessageCache(params).then(res => {
        console.log(res)
      })
    },
    //查询记录
    queryMessagebyScore(params) {
      queryMessagebyScore(params).then(res => {
        if (res.data.code == 200) {
          this.messagesMap[this.accep_id] = [] //这是为了给每个对话对像设置单独的聊天界面
          this.messagesClone[this.accep_id] = [] // 历史记录
          let msgList = res.data.data.slice().sort((a, b) => a.date - b.date);
          msgList.forEach(element => {
            const newTime = this.getNewTime(element.date)
            this.timestamps.push(newTime)
            this.messagesMap[this.accep_id].push({
              id: element.date,
              content: element.content,
              name: element.label,
              sent: element.label == this.FocuUserInfo._name,
              showContextMenu: false
            });
            this.messagesClone[this.accep_id].push({
              id: element.date,
              content: element.content,
              name: element.label,
              sent: element.label == this.FocuUserInfo._name,
              showContextMenu: false
            });
          });
          this.selectedDate = this.timestamps[this.timestamps.length -1]
          //	让滚动条默认滚动到底部
          this.$nextTick(() => {
            const messageContainer = this.$refs.messageContainer;
            if (messageContainer) {
              messageContainer.scrollTop = messageContainer.scrollHeight;
            }
          });
        }
      })
    },
    getNewTime(time) {
      const date = new Date(Number(time));
      const year = date.getFullYear();
      const month = date.getMonth() + 1;
      const day = date.getDate();
      const newTimestamp = new Date(year, month - 1, day).getTime();
      return newTimestamp
    },
    handleClickOutside(event) {
      // 点击消息列表之外的空白区域时隐藏消息操作弹窗
      if (!this.$refs.messageContainer?.contains(event.target)) {
        this.contextMenu.visible = false;
      }
    },
    handleClickInside(event) {
      // 点击消息列表内部的空白区域时隐藏消息操作弹窗
      if (!event.target.closest('.message-bubble')) {
        this.contextMenu.visible = false;
      }
    },
    timestampToTime(timestamp) {
      if(!timestamp) return
      const date = new Date(Number(timestamp));
      const year = date.getFullYear();
      const month = date.getMonth() + 1;
      const day = date.getDate();
      const hours = date.getHours().toString().padStart(2, '0');
      const minutes = date.getMinutes().toString().padStart(2, '0');
      const seconds =date.getSeconds().toString().padStart(2, '0');
      return `${hours}:${minutes}:${seconds}`;
    }
  },
  beforeDestroy() {
    document.removeEventListener('click', this.handleClickOutside);
     if (client != null) {
       client.end(true, null, () => {
         client = null;
      });
    }
  },
};
</script>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值