右键复制撤回功能
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) {
|
export function goEasyRemoveConversation(conversation) {
|
||||||
if (!isGoEasyEnabled()) {
|
if (!isGoEasyEnabled()) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 消息页面 -->
|
<!-- 消息页面 -->
|
||||||
<div class="message-page">
|
<div class="message-page" ref="messagePageRef">
|
||||||
<div class="message-layout">
|
<div class="message-layout">
|
||||||
<!-- 会话列表 -->
|
<!-- 会话列表 -->
|
||||||
<div class="conversation-panel">
|
<div class="conversation-panel">
|
||||||
@@ -45,7 +45,12 @@
|
|||||||
class="message-item"
|
class="message-item"
|
||||||
:class="{ mine: msg.senderId == currentUser.id }"
|
:class="{ mine: msg.senderId == currentUser.id }"
|
||||||
>
|
>
|
||||||
<div class="message-bubble">
|
<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>
|
<div v-if="msg.type === 'text'" class="text-message">{{ msg.payload.text }}</div>
|
||||||
<PictureMessage v-else-if="msg.type === 'image'" :item="msg" />
|
<PictureMessage v-else-if="msg.type === 'image'" :item="msg" />
|
||||||
<MiniPKMessage v-else-if="msg.type === 'pk'" :item="msg" />
|
<MiniPKMessage v-else-if="msg.type === 'pk'" :item="msg" />
|
||||||
@@ -53,6 +58,7 @@
|
|||||||
<div v-if="msg.senderId == currentUser.id" class="read-status">
|
<div v-if="msg.senderId == currentUser.id" class="read-status">
|
||||||
{{ msg.read ? '已读' : '未读' }}
|
{{ msg.read ? '已读' : '未读' }}
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,6 +95,16 @@
|
|||||||
style="display: none"
|
style="display: none"
|
||||||
@change="handleFileSelect"
|
@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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -104,6 +120,7 @@ import {
|
|||||||
goEasySendImageMessage,
|
goEasySendImageMessage,
|
||||||
goEasyMessageRead,
|
goEasyMessageRead,
|
||||||
goEasyRemoveConversation,
|
goEasyRemoveConversation,
|
||||||
|
goEasyRecallMessage,
|
||||||
getPkGoEasy,
|
getPkGoEasy,
|
||||||
GoEasy
|
GoEasy
|
||||||
} from '@/utils/pk-mini/goeasy'
|
} from '@/utils/pk-mini/goeasy'
|
||||||
@@ -123,6 +140,53 @@ const isScrollReady = ref(false)
|
|||||||
const chatMessagesRef = ref(null)
|
const chatMessagesRef = ref(null)
|
||||||
const fileInputRef = ref(null)
|
const fileInputRef = ref(null)
|
||||||
const unreadStore = pkUnreadStore()
|
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
|
const formatTime = TimestamptolocalTime
|
||||||
|
|
||||||
@@ -273,6 +337,7 @@ async function removeConversation(item, index) {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
currentUser.value = getMainUserData() || {}
|
currentUser.value = getMainUserData() || {}
|
||||||
|
document.addEventListener('click', hideContextMenu)
|
||||||
if (isGoEasyEnabled()) {
|
if (isGoEasyEnabled()) {
|
||||||
loadConversations()
|
loadConversations()
|
||||||
const goeasy = getPkGoEasy()
|
const goeasy = getPkGoEasy()
|
||||||
@@ -280,6 +345,7 @@ onMounted(() => {
|
|||||||
goeasy.im.on(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, onMessageReceived)
|
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.CONVERSATIONS_UPDATED, onConversationsUpdated)
|
||||||
goeasy.im.on(GoEasy.IM_EVENT.MESSAGE_READ, onMessageRead)
|
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) {
|
function onMessageRead(messages) {
|
||||||
// 收到对方已读回执,更新本地消息的 read 状态
|
|
||||||
messages.forEach(readMsg => {
|
messages.forEach(readMsg => {
|
||||||
const target = messagesList.value.find(m => m.messageId === readMsg.messageId)
|
const index = messagesList.value.findIndex(m => m.messageId === readMsg.messageId)
|
||||||
if (target) {
|
if (index !== -1) {
|
||||||
target.read = true
|
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(() => {
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', hideContextMenu)
|
||||||
if (isGoEasyEnabled()) {
|
if (isGoEasyEnabled()) {
|
||||||
const goeasy = getPkGoEasy()
|
const goeasy = getPkGoEasy()
|
||||||
if (goeasy) {
|
if (goeasy) {
|
||||||
@@ -334,6 +409,7 @@ onUnmounted(() => {
|
|||||||
goeasy.im.off(GoEasy.IM_EVENT.PRIVATE_MESSAGE_RECEIVED, onMessageReceived)
|
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.CONVERSATIONS_UPDATED, onConversationsUpdated)
|
||||||
goeasy.im.off(GoEasy.IM_EVENT.MESSAGE_READ, onMessageRead)
|
goeasy.im.off(GoEasy.IM_EVENT.MESSAGE_READ, onMessageRead)
|
||||||
|
goeasy.im.off(GoEasy.IM_EVENT.MESSAGE_RECALLED, onMessageRecalled)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('清理 GoEasy 监听器失败', e)
|
console.warn('清理 GoEasy 监听器失败', e)
|
||||||
}
|
}
|
||||||
@@ -344,6 +420,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.message-page {
|
.message-page {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
@@ -591,6 +668,40 @@ onUnmounted(() => {
|
|||||||
color: #60a5fa;
|
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 {
|
.empty-tip {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
|
|||||||
Reference in New Issue
Block a user