右键复制撤回功能
This commit is contained in:
@@ -263,6 +263,28 @@ export function goEasyMessageRead(data) {
|
||||
})
|
||||
}
|
||||
|
||||
// 撤回消息
|
||||
export function goEasyRecallMessage(message) {
|
||||
if (!isGoEasyEnabled()) {
|
||||
return Promise.reject(new Error('GoEasy 未启用'))
|
||||
}
|
||||
|
||||
const goeasy = getPkGoEasy()
|
||||
const im = goeasy.im
|
||||
return new Promise((resolve, reject) => {
|
||||
im.recallMessage({
|
||||
messages: [message],
|
||||
onSuccess: function () {
|
||||
resolve(true)
|
||||
},
|
||||
onFailed: function (error) {
|
||||
console.log('撤回失败, code:' + error.code + ' content:' + error.content)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 删除会话
|
||||
export function goEasyRemoveConversation(conversation) {
|
||||
if (!isGoEasyEnabled()) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- 消息页面 -->
|
||||
<div class="message-page">
|
||||
<div class="message-page" ref="messagePageRef">
|
||||
<div class="message-layout">
|
||||
<!-- 会话列表 -->
|
||||
<div class="conversation-panel">
|
||||
@@ -45,14 +45,20 @@
|
||||
class="message-item"
|
||||
:class="{ mine: msg.senderId == currentUser.id }"
|
||||
>
|
||||
<div class="message-bubble">
|
||||
<div v-if="msg.type === 'text'" class="text-message">{{ msg.payload.text }}</div>
|
||||
<PictureMessage v-else-if="msg.type === 'image'" :item="msg" />
|
||||
<MiniPKMessage v-else-if="msg.type === 'pk'" :item="msg" />
|
||||
<VoiceMessage v-else-if="msg.type === 'audio'" :item="msg.payload.url" :size="msg.payload.duration" />
|
||||
<div v-if="msg.senderId == currentUser.id" class="read-status">
|
||||
{{ msg.read ? '已读' : '未读' }}
|
||||
</div>
|
||||
<div
|
||||
class="message-bubble"
|
||||
@contextmenu.prevent="msg.type !== 'pk' ? showContextMenu($event, msg, index) : null"
|
||||
>
|
||||
<div v-if="msg.recalled" class="recalled-tip">消息已撤回</div>
|
||||
<template v-else>
|
||||
<div v-if="msg.type === 'text'" class="text-message">{{ msg.payload.text }}</div>
|
||||
<PictureMessage v-else-if="msg.type === 'image'" :item="msg" />
|
||||
<MiniPKMessage v-else-if="msg.type === 'pk'" :item="msg" />
|
||||
<VoiceMessage v-else-if="msg.type === 'audio'" :item="msg.payload.url" :size="msg.payload.duration" />
|
||||
<div v-if="msg.senderId == currentUser.id" class="read-status">
|
||||
{{ msg.read ? '已读' : '未读' }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,6 +95,16 @@
|
||||
style="display: none"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
<!-- 右键菜单 -->
|
||||
<div
|
||||
v-if="contextMenu.visible"
|
||||
class="context-menu"
|
||||
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
|
||||
@mouseleave="hideContextMenu"
|
||||
>
|
||||
<div v-if="contextMenu.msg?.type === 'text'" class="context-menu-item" @click="copyMessage">复制</div>
|
||||
<div v-if="contextMenu.msg?.senderId == currentUser.id" class="context-menu-item danger" @click="recallMessage">撤回</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -104,6 +120,7 @@ import {
|
||||
goEasySendImageMessage,
|
||||
goEasyMessageRead,
|
||||
goEasyRemoveConversation,
|
||||
goEasyRecallMessage,
|
||||
getPkGoEasy,
|
||||
GoEasy
|
||||
} from '@/utils/pk-mini/goeasy'
|
||||
@@ -123,6 +140,53 @@ const isScrollReady = ref(false)
|
||||
const chatMessagesRef = ref(null)
|
||||
const fileInputRef = ref(null)
|
||||
const unreadStore = pkUnreadStore()
|
||||
const contextMenu = ref({ visible: false, x: 0, y: 0, msg: null, index: -1 })
|
||||
const messagePageRef = ref(null)
|
||||
|
||||
function showContextMenu(event, msg, index) {
|
||||
const menuWidth = 110
|
||||
const menuHeight = msg.senderId == currentUser.value.id ? 80 : 44
|
||||
const rect = messagePageRef.value?.getBoundingClientRect() || { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight }
|
||||
let x = event.clientX - rect.left
|
||||
let y = event.clientY - rect.top
|
||||
if (x + menuWidth > rect.width) x -= menuWidth
|
||||
if (y + menuHeight > rect.height) y -= menuHeight
|
||||
contextMenu.value = { visible: true, x, y, msg, index }
|
||||
}
|
||||
|
||||
function hideContextMenu() {
|
||||
contextMenu.value.visible = false
|
||||
}
|
||||
|
||||
function copyMessage() {
|
||||
const text = contextMenu.value.msg?.payload?.text || ''
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).catch(() => {})
|
||||
} else {
|
||||
const el = document.createElement('textarea')
|
||||
el.value = text
|
||||
document.body.appendChild(el)
|
||||
el.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
}
|
||||
hideContextMenu()
|
||||
}
|
||||
|
||||
async function recallMessage() {
|
||||
const msg = contextMenu.value.msg
|
||||
const index = contextMenu.value.index
|
||||
hideContextMenu()
|
||||
if (!msg) return
|
||||
try {
|
||||
await goEasyRecallMessage(msg)
|
||||
// 用对象替换触发 Vue 响应式更新
|
||||
messagesList.value.splice(index, 1, { ...msg, recalled: true })
|
||||
} catch (e) {
|
||||
console.error('撤回失败', e)
|
||||
ElMessage.error('撤回失败,消息超过4小时或发送中')
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = TimestamptolocalTime
|
||||
|
||||
@@ -273,6 +337,7 @@ async function removeConversation(item, index) {
|
||||
|
||||
onMounted(() => {
|
||||
currentUser.value = getMainUserData() || {}
|
||||
document.addEventListener('click', hideContextMenu)
|
||||
if (isGoEasyEnabled()) {
|
||||
loadConversations()
|
||||
const goeasy = getPkGoEasy()
|
||||
@@ -280,6 +345,7 @@ onMounted(() => {
|
||||
goeasy.im.on(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, onMessageReceived)
|
||||
goeasy.im.on(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, onConversationsUpdated)
|
||||
goeasy.im.on(GoEasy.IM_EVENT.MESSAGE_READ, onMessageRead)
|
||||
goeasy.im.on(GoEasy.IM_EVENT.MESSAGE_RECALLED, onMessageRecalled)
|
||||
}
|
||||
}
|
||||
// 切换回消息页面时,滚到聊天记录最底部
|
||||
@@ -302,11 +368,19 @@ function onConversationsUpdated(conversations) {
|
||||
}
|
||||
|
||||
function onMessageRead(messages) {
|
||||
// 收到对方已读回执,更新本地消息的 read 状态
|
||||
messages.forEach(readMsg => {
|
||||
const target = messagesList.value.find(m => m.messageId === readMsg.messageId)
|
||||
if (target) {
|
||||
target.read = true
|
||||
const index = messagesList.value.findIndex(m => m.messageId === readMsg.messageId)
|
||||
if (index !== -1) {
|
||||
messagesList.value.splice(index, 1, { ...messagesList.value[index], read: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onMessageRecalled(messages) {
|
||||
messages.forEach(recalled => {
|
||||
const index = messagesList.value.findIndex(m => m.messageId === recalled.messageId)
|
||||
if (index !== -1) {
|
||||
messagesList.value.splice(index, 1, { ...messagesList.value[index], recalled: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -327,6 +401,7 @@ function onMessageReceived(message) {
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', hideContextMenu)
|
||||
if (isGoEasyEnabled()) {
|
||||
const goeasy = getPkGoEasy()
|
||||
if (goeasy) {
|
||||
@@ -334,6 +409,7 @@ onUnmounted(() => {
|
||||
goeasy.im.off(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, onMessageReceived)
|
||||
goeasy.im.off(GoEasy.IM_EVENT.CONVERSATIONS_UPDATED, onConversationsUpdated)
|
||||
goeasy.im.off(GoEasy.IM_EVENT.MESSAGE_READ, onMessageRead)
|
||||
goeasy.im.off(GoEasy.IM_EVENT.MESSAGE_RECALLED, onMessageRecalled)
|
||||
} catch (e) {
|
||||
console.warn('清理 GoEasy 监听器失败', e)
|
||||
}
|
||||
@@ -344,6 +420,7 @@ onUnmounted(() => {
|
||||
|
||||
<style scoped lang="less">
|
||||
.message-page {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
@@ -591,6 +668,40 @@ onUnmounted(() => {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.recalled-tip {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
font-style: italic;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
|
||||
min-width: 100px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: #374151;
|
||||
&:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
&.danger {
|
||||
color: #ef4444;
|
||||
&:hover {
|
||||
background-color: #fee2e2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
|
||||
Reference in New Issue
Block a user