diff --git a/src/api/message.js b/src/api/message.js index 0f088ee..d657dab 100644 --- a/src/api/message.js +++ b/src/api/message.js @@ -12,6 +12,10 @@ export const msgChatPullMsgService = (obj) => { return request.get('/chat/pullMsg', { params: obj }) } +export const msgChatRevokeMsgService = (obj) => { + return request.post('/chat/revokeMsg', obj) +} + export const msgAtService = () => { return request.get('/chat/queryAt') } diff --git a/src/const/msgConst.js b/src/const/msgConst.js index a6d7909..046890a 100644 --- a/src/const/msgConst.js +++ b/src/const/msgConst.js @@ -41,3 +41,13 @@ export const msgFileUploadStatus = { UPLOAD_SUCCESS: 2, // 上传成功 UPLOAD_FAILED: 3 // 上传失败 } + +/** + * 消息撤回时间限制 10分钟 + */ +export const MSG_REVOKE_TIME_LIMIT = 10 * 60 * 1000 + +/** + * 消息撤回后能重新编辑的时间限制 2分钟 + */ +export const MSG_REEDIT_TIME_LIMIT = 2 * 60 * 1000 diff --git a/src/js/event/index.js b/src/js/event/index.js index bff6a7c..c762468 100644 --- a/src/js/event/index.js +++ b/src/js/event/index.js @@ -5,3 +5,4 @@ export * from './receiveGroupChatMsg' export * from './receiveGroupChatReadMsg' export * from './receiveGroupSystemMsg' export * from './receiveAtMsg' +export * from './receiveRevokeMsg' diff --git a/src/js/event/receiveRevokeMsg.js b/src/js/event/receiveRevokeMsg.js new file mode 100644 index 0000000..022de5e --- /dev/null +++ b/src/js/event/receiveRevokeMsg.js @@ -0,0 +1,10 @@ +import { useMessageStore } from '@/stores' + +export const onReceiveRevokeMsg = () => { + return (msg) => { + const messageData = useMessageStore() + const sessionId = msg.body.sessionId + const revokeMsgId = msg.body.content + messageData.revokeMsgRcord(sessionId, revokeMsgId) + } +} diff --git a/src/js/websocket/wsConnect.js b/src/js/websocket/wsConnect.js index cae761f..6bff0f8 100644 --- a/src/js/websocket/wsConnect.js +++ b/src/js/websocket/wsConnect.js @@ -20,7 +20,8 @@ import { onReceiveGroupChatMsg, onReceiveGroupChatReadMsg, onReceiveGroupSystemMsg, - onReceiveAtMsg + onReceiveAtMsg, + onReceiveRevokeMsg } from '@/js/event' class WsConnect { @@ -121,6 +122,7 @@ class WsConnect { [MsgType.GROUP_CHAT]: onReceiveGroupChatMsg(), [MsgType.GROUP_CHAT_READ]: onReceiveGroupChatReadMsg(), [MsgType.AT]: onReceiveAtMsg(), + [MsgType.REVOKE]: onReceiveRevokeMsg(), [MsgType.SYS_GROUP_CREATE]: onReceiveGroupSystemMsg(), [MsgType.SYS_GROUP_ADD_MEMBER]: onReceiveGroupSystemMsg(), [MsgType.SYS_GROUP_DEL_MEMBER]: onReceiveGroupSystemMsg(), diff --git a/src/proto/msg.js b/src/proto/msg.js index 979db57..e7391d9 100644 --- a/src/proto/msg.js +++ b/src/proto/msg.js @@ -294,6 +294,7 @@ export const Msg = ($root.Msg = (() => { * @property {number} STATUS_RES=9 STATUS_RES value * @property {number} STATUS_SYNC=10 STATUS_SYNC value * @property {number} AT=11 AT value + * @property {number} REVOKE=12 REVOKE value * @property {number} SYS_GROUP_CREATE=21 SYS_GROUP_CREATE value * @property {number} SYS_GROUP_ADD_MEMBER=22 SYS_GROUP_ADD_MEMBER value * @property {number} SYS_GROUP_DEL_MEMBER=23 SYS_GROUP_DEL_MEMBER value @@ -331,6 +332,7 @@ export const MsgType = ($root.MsgType = (() => { values[(valuesById[9] = 'STATUS_RES')] = 9 values[(valuesById[10] = 'STATUS_SYNC')] = 10 values[(valuesById[11] = 'AT')] = 11 + values[(valuesById[12] = 'REVOKE')] = 12 values[(valuesById[21] = 'SYS_GROUP_CREATE')] = 21 values[(valuesById[22] = 'SYS_GROUP_ADD_MEMBER')] = 22 values[(valuesById[23] = 'SYS_GROUP_DEL_MEMBER')] = 23 @@ -546,6 +548,7 @@ export const Header = ($root.Header = (() => { case 9: case 10: case 11: + case 12: case 21: case 22: case 23: @@ -642,6 +645,10 @@ export const Header = ($root.Header = (() => { case 11: message.msgType = 11 break + case 'REVOKE': + case 12: + message.msgType = 12 + break case 'SYS_GROUP_CREATE': case 21: message.msgType = 21 @@ -825,18 +832,18 @@ export const Body = ($root.Body = (() => { * | 8 | seq | - | - | M | M | M | M | O | O | M | todo | todo | * | 9 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo | * +---+--------------+------+-----------+---------|-----------+---------------+-----------------+----------+----------------+----------+-------------------+---------------------+ - * NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) - * +---+--------------+------------+------------+-------------+------------+---------+---------+ - * | 1 | fromId | M | M | M | - | M | M | - * | 2 | fromClient | M | M | M | - | M | M | - * | 3 | toId | - | - | - | - | - | M | - * | 4 | toClient | - | - | - | - | - | M | - * | 5 | groupId | - | - | - | M | M | M | - * | 6 | msgId | - | - | - | M | - | M | - * | 7 | content | M | M | M | M | M | M | - * | 8 | seq | - | - | - | - | M | M | - * | 9 | sessionId | - | - | - | M | M | M | - * +---+--------------+------------+------------+-------------+------------+---------+---------+ + * NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) REVOKE + * +---+--------------+------------+------------+-------------+------------+---------+---------+-----------+ + * | 1 | fromId | M | M | M | - | M | M | M | + * | 2 | fromClient | M | M | M | - | M | M | - | + * | 3 | toId | - | - | - | - | - | M | o | + * | 4 | toClient | - | - | - | - | - | M | - | + * | 5 | groupId | - | - | - | M | M | M | o | + * | 6 | msgId | - | - | - | M | - | M | M | + * | 7 | content | M | M | M | M | M | M | M | + * | 8 | seq | - | - | - | - | M | M | - | + * | 9 | sessionId | - | - | - | M | M | M | M | + * +---+--------------+------------+------------+-------------+------------+---------+---------+-----------+ * @implements IBody * @constructor * @param {IBody=} [properties] Properties to set diff --git a/src/proto/msg.proto b/src/proto/msg.proto index 4d7bd0e..6ad3d1a 100644 --- a/src/proto/msg.proto +++ b/src/proto/msg.proto @@ -19,6 +19,7 @@ enum MsgType { STATUS_RES = 9; //连接状态响应 STATUS_SYNC = 10; //端侧的连接状态同步给云端(比如在线,离开) AT = 11; //@消息 + REVOKE = 12; //撤回消息 SYS_GROUP_CREATE = 21; //系统消息之创建群组 SYS_GROUP_ADD_MEMBER = 22; //系统消息之添加群组成员 @@ -66,18 +67,18 @@ message Header { | 8 | seq | - | - | M | M | M | M | O | O | M | todo | todo | | 9 | sessionId | - | - | M | M | M | M | M | M | M | todo | todo | +---+--------------+------+-----------+---------|-----------+---------------+-----------------+----------+----------------+----------+-------------------+---------------------+ - NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) -+---+--------------+------------+------------+-------------+------------+---------+---------+ -| 1 | fromId | M | M | M | - | M | M | -| 2 | fromClient | M | M | M | - | M | M | -| 3 | toId | - | - | - | - | - | M | -| 4 | toClient | - | - | - | - | - | M | -| 5 | groupId | - | - | - | M | M | M | -| 6 | msgId | - | - | - | M | - | M | -| 7 | content | M | M | M | M | M | M | -| 8 | seq | - | - | - | - | M | M | -| 9 | sessionId | - | - | - | M | M | M | -+---+--------------+------------+------------+-------------+------------+---------+---------+ + NO filed STATUS_REQ STATUS_RES STATUS_SYNC SYS_GROUP_XXX AT(up) AT(down) REVOKE ++---+--------------+------------+------------+-------------+------------+---------+---------+-----------+ +| 1 | fromId | M | M | M | - | M | M | M | +| 2 | fromClient | M | M | M | - | M | M | - | +| 3 | toId | - | - | - | - | - | M | o | +| 4 | toClient | - | - | - | - | - | M | - | +| 5 | groupId | - | - | - | M | M | M | o | +| 6 | msgId | - | - | - | M | - | M | M | +| 7 | content | M | M | M | M | M | M | M | +| 8 | seq | - | - | - | - | M | M | - | +| 9 | sessionId | - | - | - | M | M | M | M | ++---+--------------+------------+------------+-------------+------------+---------+---------+-----------+ */ message Body { optional string fromId = 1; diff --git a/src/stores/message.js b/src/stores/message.js index c63e304..21b8593 100644 --- a/src/stores/message.js +++ b/src/stores/message.js @@ -181,6 +181,12 @@ export const useMessageStore = defineStore('anylink-message', () => { } } + const revokeMsgRcord = (sessionId, msgKey) => { + if (msgRecordsList.value[sessionId] && msgKey in msgRecordsList.value[sessionId]) { + msgRecordsList.value[sessionId][msgKey].revoke = true + } + } + const getMsg = (sessionId, msgKey) => { if (!msgRecordsList.value[sessionId] || !msgRecordsList.value[sessionId][msgKey]) { return ref({}) @@ -333,6 +339,7 @@ export const useMessageStore = defineStore('anylink-message', () => { updateMsgKeySort, addMsgRecords, removeMsgRecord, + revokeMsgRcord, getMsg, updateMsg, diff --git a/src/views/message/MessageLayout.vue b/src/views/message/MessageLayout.vue index a8c0c6a..2a71c3f 100644 --- a/src/views/message/MessageLayout.vue +++ b/src/views/message/MessageLayout.vue @@ -1296,6 +1296,7 @@ const onShowRecorder = () => { :lastMsgId="lastMsgId" :hasNoMoreMsg="hasNoMoreMsg" :isLoadMoreLoading="selectedSessionCache[selectedSessionId]?.isLoadMoreLoading" + :inputEditorRef="inputEditorRef" @loadMore="onLoadMore" @showUserCard="onShowUserCard" @showGroupCard="onShowGroupCard" diff --git a/src/views/message/components/InputEditor.vue b/src/views/message/components/InputEditor.vue index cf54098..f38c5a9 100644 --- a/src/views/message/components/InputEditor.vue +++ b/src/views/message/components/InputEditor.vue @@ -27,7 +27,7 @@ import AgreeBeforeSend from '@/views/message/components/AgreeBeforeSend.vue' const Clipboard = Quill.import('modules/clipboard') class PlainClipboard extends Clipboard { onPaste(range, { text }) { - renderContent(text) + handlePaste(range, text) } } Quill.register( @@ -110,7 +110,7 @@ onMounted(async () => { // 给组件增加滚动条样式 document.querySelector('.ql-editor').classList.add('my-scrollbar') await imageData.loadImageInfoFromContent(props.draft) - renderContent(props.draft) + renderContent(props.draft) // 渲染草稿 quill.value.on('composition-start', () => { // 当用户使用拼音输入法开始输入汉字时,这个事件就会被触发 quill.value.root.dataset.placeholder = '' @@ -122,7 +122,7 @@ onMounted(async () => { // 监听文本变化检测@符号 quill.value.on('text-change', (delta, oldDelta, source) => { - if (session.value.sessionType === MsgType.GROUP_CHAT && source === 'user') { + if (session.value.sessionType === MsgType.GROUP_CHAT && source === Quill.sources.USER) { const insertOps = delta.ops.filter((op) => op.insert && typeof op.insert === 'string') const insertContent = insertOps.map((item) => item.insert).join('') if (insertContent.length > 0) { @@ -393,16 +393,76 @@ watch( fn(contentObj.contentFromLocal.join('').trim()) - renderContent(messageData.sessionList[newSessionId].draft || '') + renderContent(messageData.sessionList[newSessionId].draft || '') // 切换session时渲染新session的草稿 }, { deep: true } ) -let pasteContent -let pasteContentType -let pasteFileName -let pasteFileSize -let pasteUrl +// 实现消息复制的效果,步骤如下 +// 1. 拷贝原消息中的content内容 +// 2. 粘贴时自动调用renderContent渲染内容 +// 3. 渲染时保存复制内容 +// 4. 发送时使用保存的复制内容 +const pasteObj = { + content: null, + contentType: null, + fileName: null, + fileSize: null, + url: null +} + +const clearPasteObj = () => { + pasteObj.content = null + pasteObj.contentType = null + pasteObj.fileName = null + pasteObj.fileSize = null + pasteObj.url = null +} + +const handlePaste = (range, content) => { + if (!content) { + return + } + + const jsonContent = jsonParseSafe(content) + if (jsonContent && jsonContent['type'] && jsonContent['value']) { + clearPasteObj() + pasteObj.content = content + pasteObj.contentType = jsonContent['type'] + const fileId = jsonContent['value'] + switch (pasteObj.contentType) { + case msgContentType.IMAGE: + pasteObj.fileName = imageData.image[fileId]?.fileName + pasteObj.fileSize = imageData.image[fileId]?.size + pasteObj.url = imageData.image[fileId]?.thumbUrl + break + case msgContentType.AUDIO: + pasteObj.fileName = audioData.audio[fileId]?.fileName + pasteObj.fileSize = audioData.audio[fileId]?.size + break + case msgContentType.VIDEO: + pasteObj.fileName = videoData.video[fileId]?.fileName + pasteObj.fileSize = videoData.video[fileId]?.size + break + case msgContentType.DOCUMENT: + pasteObj.fileName = documentData.document[fileId]?.fileName + pasteObj.fileSize = documentData.document[fileId]?.size + break + default: + break + } + + // 文件确实存在才发送 + if (pasteObj.fileName) { + showAgreeDialog.value = true + return + } + } + + const delta = new Delta().retain(range.index).delete(range.length).insert(content) + quill.value.updateContents(delta, Quill.sources.USER) + quill.value.setSelection(delta.length() - range.length, Quill.sources.USER) +} /** * 把输入框的字符串内容渲染成富媒体内容 @@ -413,82 +473,62 @@ const renderContent = (content) => { quill.value.setText('') return } - pasteContent = content - const jsonContent = jsonParseSafe(content) - if (jsonContent && jsonContent['type'] && jsonContent['value']) { - pasteContentType = jsonContent['type'] - const fileId = jsonContent['value'] - switch (pasteContentType) { - case msgContentType.IMAGE: - pasteFileName = imageData.image[fileId]?.fileName - pasteFileSize = imageData.image[fileId]?.size - pasteUrl = imageData.image[fileId]?.thumbUrl - break - case msgContentType.AUDIO: - pasteFileName = audioData.audio[fileId]?.fileName - pasteFileSize = audioData.audio[fileId]?.size - break - case msgContentType.VIDEO: - pasteFileName = videoData.video[fileId]?.fileName - pasteFileSize = videoData.video[fileId]?.size - break - case msgContentType.DOCUMENT: - pasteFileName = documentData.document[fileId]?.fileName - pasteFileSize = documentData.document[fileId]?.size - break - default: - break - } - showAgreeDialog.value = true - } else { - let contentArray = [] - //匹配内容中的图片 - content.split(/(\{.*?\})/).forEach((item) => { - //匹配内容中的表情 - item.split(/(\[.*?\])/).forEach((item) => { - //匹配内容中的@ - item.split(/(<.*?>)/).forEach((item) => { - if (item) { - contentArray.push(item) - } - }) + + let contentArray = [] + //匹配内容中的图片 + content.split(/(\{.*?\})/).forEach((item) => { + //匹配内容中的表情 + item.split(/(\[.*?\])/).forEach((item) => { + //匹配内容中的@ + item.split(/(<.*?>)/).forEach((item) => { + if (item) { + contentArray.push(item) + } }) }) + }) - // 创建一个新的 Delta 对象 - const delta = new Delta() - contentArray.map((item) => { - if (item.startsWith('{') && item.endsWith('}')) { - const imageId = item.slice(1, -1) - const imageUrl = imageData.image[imageId].originUrl + // 创建一个新的 Delta 对象 + const delta = new Delta() + contentArray.map((item) => { + if (item.startsWith('{') && item.endsWith('}')) { + const imageId = item.slice(1, -1) + const imageUrl = imageData.image[imageId]?.originUrl + if (imageUrl) { delta.insert({ image: imageUrl }, { alt: item }) - } else if (item.startsWith('[') && item.endsWith(']')) { - const emojiUrl = emojis[item] + } else { + delta.insert(item) + } + } else if (item.startsWith('[') && item.endsWith(']')) { + const emojiUrl = emojis[item] + if (emojiUrl) { delta.insert({ image: emojiUrl }, { alt: item }) - } else if (item.startsWith('<') && item.endsWith('>')) { - const content = item.slice(1, -1) - const index = content.indexOf('-') - if (index !== -1) { - const account = content.slice(0, index) - const nickName = content.slice(index + 1) - if (nickName) { - toSendAtList.value.push(account) - delta.insert({ atMention: { account, nickName } }) - } else { - delta.insert(item) - } + } else { + delta.insert(item) + } + } else if (item.startsWith('<') && item.endsWith('>')) { + const content = item.slice(1, -1) + const index = content.indexOf('-') + if (index !== -1) { + const account = content.slice(0, index) + const nickName = content.slice(index + 1) + if (nickName) { + toSendAtList.value.push(account) + delta.insert({ atMention: { account, nickName } }) } else { delta.insert(item) } } else { delta.insert(item) } - }) + } else { + delta.insert(item) + } + }) - quill.value.setText('') // 清空编辑器内容 - quill.value.updateContents(delta) // 使用 Delta 对象更新编辑器内容 - quill.value.setSelection(quill.value.getLength(), 0, 'user') // 设置光标位置 - } + quill.value.setText('') // 清空编辑器内容 + quill.value.updateContents(delta) // 使用 Delta 对象更新编辑器内容 + quill.value.setSelection(quill.value.getLength(), 0, Quill.sources.USER) // 设置光标位置 } const handleEnter = async () => { @@ -502,8 +542,8 @@ const handleEnter = async () => { allUploadedSuccessFn: () => {} } - const contentObj = pasteContent - ? { contentFromLocal: [pasteContent], contentFromServer: [pasteContent] } + const contentObj = pasteObj.content + ? { contentFromLocal: [pasteObj.content], contentFromServer: [pasteObj.content] } : await parseContent(callbacks) const content = contentObj.contentFromLocal.join('').trim() @@ -561,9 +601,10 @@ const handleEnter = async () => { emit('sendMessage', { msg, atTargets }) } - pasteContent = '' - quill.value.setText('') // 编辑窗口置空 + clearPasteObj() toSendAtList.value = [] + quill.value.setText('') // 编辑窗口置空 + quill.value.setSelection(0, 0, Quill.sources.USER) // 设置光标位置 } const options = { @@ -600,7 +641,7 @@ const addEmoji = (key) => { delta.retain(index) delta.insert({ image: emojis[key] }, { alt: key }) quill.value.updateContents(delta) - quill.value.setSelection(index + 1, 0, 'user') + quill.value.setSelection(index + 1, 0, Quill.sources.USER) } const onSelectedAtTarget = ({ account, nickName }) => { @@ -613,18 +654,30 @@ const onSelectedAtTarget = ({ account, nickName }) => { if (range.index >= atIndex.value) { const delLen = range.index - atIndex.value + 1 // 删除用户输入的@符号及搜索关键字 quill.value.deleteText(atIndex.value - 1, delLen) - quill.value.insertEmbed(atIndex.value - 1, 'atMention', { account, nickName }, 'user') // 插入Blot(占据1个位置) - quill.value.insertText(atIndex.value, ' ', 'user') // 插入空格 - quill.value.setSelection(atIndex.value + 1, 0, 'user') // 定位光标 + quill.value.insertEmbed( + atIndex.value - 1, + 'atMention', + { account, nickName }, + Quill.sources.USER + ) // 插入Blot(占据1个位置) + quill.value.insertText(atIndex.value, ' ', Quill.sources.USER) // 插入空格 + quill.value.setSelection(atIndex.value + 1, 0, Quill.sources.USER) // 定位光标 } else { - quill.value.insertEmbed(range.index, 'atMention', { account, nickName }, 'user') // 插入Blot(占据1个位置) - quill.value.insertText(range.index + 1, ' ', 'user') // 插入空格 - quill.value.setSelection(range.index + 1 + 1, 0, 'user') // 定位光标 + quill.value.insertEmbed(range.index, 'atMention', { account, nickName }, Quill.sources.USER) // 插入Blot(占据1个位置) + quill.value.insertText(range.index + 1, ' ', Quill.sources.USER) // 插入空格 + quill.value.setSelection(range.index + 1 + 1, 0, Quill.sources.USER) // 定位光标 } } +const reeditFromRevoke = (content) => { + quill.value.setText('') // 清空编辑器内容 + quill.value.setSelection(0, 0, Quill.sources.SILENT) // 设置光标位置 + renderContent(content) +} + defineExpose({ - addEmoji + addEmoji, + reeditFromRevoke }) @@ -647,10 +700,10 @@ defineExpose({ diff --git a/src/views/message/components/MenuMsgItem.vue b/src/views/message/components/MenuMsgItem.vue index 1efeccd..accb19a 100644 --- a/src/views/message/components/MenuMsgItem.vue +++ b/src/views/message/components/MenuMsgItem.vue @@ -6,18 +6,25 @@ import DeletemsgIcon from '@/assets/svg/deletemsg.svg' import CopyIcon from '@/assets/svg/copy.svg' import MultiselectIcon from '@/assets/svg/multiselect.svg' import RevokeIcon from '@/assets/svg/revoke.svg' -import { useMenuStore } from '@/stores' +import { useUserStore, useMenuStore } from '@/stores' import { jsonParseSafe } from '@/js/utils/common' -import { msgContentType } from '@/const/msgConst' +import { MSG_REVOKE_TIME_LIMIT, msgContentType } from '@/const/msgConst' const props = defineProps(['msg']) const emit = defineEmits(['selectMenu']) +const userData = useUserStore() const menuData = useMenuStore() +const openMenuTime = ref(null) + const menuName = computed(() => { return 'MenuMsgItem-' + props.msg.msgId }) +const myAccount = computed(() => { + return userData.user.account +}) + const contentType = computed(() => { const contentJson = jsonParseSafe(props.msg.content) if (!contentJson) { @@ -37,38 +44,51 @@ const menu = computed(() => { { label: 'forward', desc: '转发', - icon: markRaw(ForwardIcon) + icon: markRaw(ForwardIcon), + index: 1 }, { label: 'multiSelect', desc: '多选', - icon: markRaw(MultiselectIcon) + icon: markRaw(MultiselectIcon), + index: 2 }, { label: 'quote', desc: '引用', - icon: markRaw(QuoteIcon) - }, - { - label: 'revoke', - desc: '撤回', - icon: markRaw(RevokeIcon) + icon: markRaw(QuoteIcon), + index: 3 }, { label: 'delete', desc: '删除', - icon: markRaw(DeletemsgIcon) + icon: markRaw(DeletemsgIcon), + index: 5 } ] if (contentType.value !== msgContentType.RECORDING) { - o.unshift({ + o.push({ label: 'copy', desc: '复制', - icon: markRaw(CopyIcon) + icon: markRaw(CopyIcon), + index: 0 }) } - return o + + if ( + myAccount.value === props.msg.fromId && + openMenuTime.value - new Date(props.msg.msgTime) < MSG_REVOKE_TIME_LIMIT + ) { + o.push({ + label: 'revoke', + desc: '撤回', + icon: markRaw(RevokeIcon), + index: 4 + }) + } + + return o.sort((a, b) => a.index - b.index) }) const containerRef = ref() @@ -106,6 +126,7 @@ const handleShowMenu = (e) => { e.stopPropagation() // 阻止冒泡 isShowMenu.value = true menuData.setActiveMenu(menuName.value) + openMenuTime.value = new Date() nextTick(() => { //如果发现菜单超出window.innerWidth屏幕宽度,x要修正一下,往左边弹出菜单 if (e.clientX + menuRef.value.clientWidth > window.innerWidth) { @@ -129,6 +150,7 @@ const handleEscEvent = (event) => { const closeMenu = () => { isShowMenu.value = false + openMenuTime.value = null } const handleClick = (item) => { diff --git a/src/views/message/components/MessageItem.vue b/src/views/message/components/MessageItem.vue index ab00f50..7bfcc60 100644 --- a/src/views/message/components/MessageItem.vue +++ b/src/views/message/components/MessageItem.vue @@ -1,5 +1,5 @@