vue 使用mqtt模仿微信聊天,可多人聊天,历史记录查询,聊天独立界面
vue
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
项目地址:https://gitcode.com/gh_mirrors/vu/vue
免费下载资源
·
<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>
GitHub 加速计划 / vu / vue
80
16
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:4 个月前 )
73486cb5
* chore: fix link broken
Signed-off-by: snoppy <michaleli@foxmail.com>
* Update packages/template-compiler/README.md [skip ci]
---------
Signed-off-by: snoppy <michaleli@foxmail.com>
Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 6 个月前
e428d891
Updated Browser Compatibility reference. The previous currently returns HTTP 404. 6 个月前
更多推荐
已为社区贡献2条内容
所有评论(0)