实现消息撤回功能

This commit is contained in:
bob
2025-04-29 16:44:00 +08:00
parent ea1e74fd08
commit f88b6a0388
12 changed files with 330 additions and 133 deletions

View File

@@ -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')
}

View File

@@ -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

View File

@@ -5,3 +5,4 @@ export * from './receiveGroupChatMsg'
export * from './receiveGroupChatReadMsg'
export * from './receiveGroupSystemMsg'
export * from './receiveAtMsg'
export * from './receiveRevokeMsg'

View File

@@ -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)
}
}

View File

@@ -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(),

View File

@@ -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

View File

@@ -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;

View File

@@ -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,

View File

@@ -1296,6 +1296,7 @@ const onShowRecorder = () => {
:lastMsgId="lastMsgId"
:hasNoMoreMsg="hasNoMoreMsg"
:isLoadMoreLoading="selectedSessionCache[selectedSessionId]?.isLoadMoreLoading"
:inputEditorRef="inputEditorRef"
@loadMore="onLoadMore"
@showUserCard="onShowUserCard"
@showGroupCard="onShowGroupCard"

View File

@@ -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,34 +473,7 @@ 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) => {
@@ -460,11 +493,19 @@ const renderContent = (content) => {
contentArray.map((item) => {
if (item.startsWith('{') && item.endsWith('}')) {
const imageId = item.slice(1, -1)
const imageUrl = imageData.image[imageId].originUrl
const imageUrl = imageData.image[imageId]?.originUrl
if (imageUrl) {
delta.insert({ image: imageUrl }, { alt: item })
} else {
delta.insert(item)
}
} else if (item.startsWith('[') && item.endsWith(']')) {
const emojiUrl = emojis[item]
if (emojiUrl) {
delta.insert({ image: emojiUrl }, { alt: item })
} else {
delta.insert(item)
}
} else if (item.startsWith('<') && item.endsWith('>')) {
const content = item.slice(1, -1)
const index = content.indexOf('-')
@@ -487,8 +528,7 @@ const renderContent = (content) => {
quill.value.setText('') // 清空编辑器内容
quill.value.updateContents(delta) // 使用 Delta 对象更新编辑器内容
quill.value.setSelection(quill.value.getLength(), 0, 'user') // 设置光标位置
}
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
})
</script>
@@ -647,10 +700,10 @@ defineExpose({
<AgreeBeforeSend
v-model:isShow="showAgreeDialog"
:target="remoteName"
:contentType="pasteContentType"
:fileName="pasteFileName"
:fileSize="pasteFileSize"
:src="pasteUrl"
:contentType="pasteObj.contentType"
:fileName="pasteObj.fileName"
:fileSize="pasteObj.fileSize"
:src="pasteObj.url"
@confirm="handleEnter"
></AgreeBeforeSend>
</div>

View File

@@ -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) => {

View File

@@ -1,5 +1,5 @@
<script setup>
import { computed, onMounted, h, createApp, watch, nextTick } from 'vue'
import { ref, computed, onMounted, h, createApp, watch, nextTick } from 'vue'
import { WarningFilled } from '@element-plus/icons-vue'
import { MsgType } from '@/proto/msg'
import {
@@ -15,7 +15,7 @@ import {
import { messageSysShowTime, showTimeFormat, jsonParseSafe } from '@/js/utils/common'
import UserAvatarIcon from '@/components/common/UserAvatarIcon.vue'
import { emojis } from '@/js/utils/emojis'
import { msgContentType, msgSendStatus } from '@/const/msgConst'
import { MSG_REEDIT_TIME_LIMIT, msgContentType, msgSendStatus } from '@/const/msgConst'
import MsgBoxRecording from '@/views/message/components/MsgBoxRecording.vue'
import MsgBoxImage from '@/views/message/components/MsgBoxImage.vue'
import MsgBoxAudio from '@/views/message/components/MsgBoxAudio.vue'
@@ -23,6 +23,7 @@ import MsgBoxVideo from '@/views/message/components/MsgBoxVideo.vue'
import MsgBoxDocument from '@/views/message/components/MsgBoxDocument.vue'
import MenuMsgItem from '@/views/message/components/MenuMsgItem.vue'
import { ElMessage } from 'element-plus'
import { msgChatRevokeMsgService } from '@/api/message'
const props = defineProps([
'sessionId',
@@ -34,7 +35,8 @@ const props = defineProps([
'firstMsgId',
'lastMsgId',
'hasNoMoreMsg',
'isLoadMoreLoading'
'isLoadMoreLoading',
'inputEditorRef'
])
const emit = defineEmits(['loadMore', 'showUserCard', 'showGroupCard', 'resendMsg', 'loadFinished'])
@@ -100,7 +102,7 @@ const renderComponent = (content) => {
case msgContentType.DOCUMENT:
return renderDocument(value)
default:
return h('div', [])
return h('span', content)
}
}
@@ -625,6 +627,32 @@ const sysShowTime = computed(() => {
return messageSysShowTime(new Date(msg.value.msgTime))
})
const isRevoke = computed(() => {
return msg.value.revoke
})
const isReedit = computed(() => {
const contentJson = jsonParseSafe(msg.value.content)
if (!contentJson) {
return true
}
const type = contentJson['type']
if (
type === msgContentType.IMAGE ||
type === msgContentType.RECORDING ||
type === msgContentType.AUDIO ||
type === msgContentType.VIDEO ||
type === msgContentType.DOCUMENT
) {
return false
} else {
return true
}
})
const isReeditTimeOut = ref(true)
// 判断是否是连续的会话与上个会话时间差小于5分钟
const isContinuousSession = computed(() => {
if (!props.extend.preMsgTime) {
@@ -672,7 +700,6 @@ const onResendMsg = () => {
}
const onSelectMenuMsgItem = async (label) => {
console.log(label)
switch (label) {
case 'copy':
try {
@@ -682,11 +709,37 @@ const onSelectMenuMsgItem = async (label) => {
ElMessage.error('复制出错')
}
break
case 'revoke':
msgChatRevokeMsgService({
sessionId: props.sessionId,
revokeMsgId: msg.value.msgId, // 服务器上删除用msg.value.msgId
isGroupChat: isGroupChatMsgType.value,
remoteId: messageData.sessionList[props.sessionId].remoteId
})
.then((res) => {
if (res.data.code === 0) {
// 本地删除用props.msgKey因为key有可能是发送消息时产生的本地UUID
messageData.revokeMsgRcord(props.sessionId, props.msgKey)
isReeditTimeOut.value = false
setTimeout(() => {
isReeditTimeOut.value = true
}, MSG_REEDIT_TIME_LIMIT)
ElMessage.success('消息已撤回')
}
})
.catch((error) => {
console.error(error)
})
break
default:
break
}
}
const handleReedit = () => {
props.inputEditorRef.reeditFromRevoke(msg.value.content)
}
/**
* 只要内容发生变化,重新渲染
*/
@@ -725,6 +778,22 @@ watch(
v-html="systemMsgContent"
@click="onClickSystemMsg"
></div>
<div v-else-if="!isSystemMsg && isRevoke" class="revoke-wrapper">
<div v-if="isSelf">
<span>你撤回了一条消息</span>
<span
v-if="isReedit && !isReeditTimeOut"
style="margin-left: 2px; color: #409eff; cursor: pointer"
@click="handleReedit"
>
重新编辑
</span>
</div>
<div v-else>
<div v-if="isChatMsgType">对方撤回了一条消息</div>
<div v-else>{{ `${objectInfo.nickName}撤回了一条消息` }}</div>
</div>
</div>
<div v-else class="message-container-wrapper">
<el-container class="el-container-right" v-if="isSelf">
<el-main class="el-main-right">
@@ -880,6 +949,16 @@ watch(
user-select: text;
}
.revoke-wrapper {
padding: 2px 4px 2px 4px;
margin-top: 10px;
margin-bottom: 10px;
border-radius: 4px;
font-size: 14px;
color: gray;
background-color: #fff;
}
.message-container-wrapper {
width: 100%;
padding: 8px;