mirror of
https://gitee.com/lijingbo-2021/open-anylink-web.git
synced 2025-12-30 02:52:26 +00:00
合并转发
This commit is contained in:
@@ -689,7 +689,7 @@ const onChangePartition = () => {
|
||||
:modelValue="groupCardData.isShow"
|
||||
:direction="'rtl'"
|
||||
:size="385"
|
||||
:z-index="1"
|
||||
:z-index="1000"
|
||||
modal-class="group-card-modal"
|
||||
:show-close="false"
|
||||
@close="groupCardData.setClosed()"
|
||||
|
||||
@@ -28,7 +28,7 @@ watch([() => props.isShow, () => props.defaultInput], ([newIsShow, newDefaultInp
|
||||
:modal="false"
|
||||
:top="'40vh'"
|
||||
:width="'360px'"
|
||||
:z-index="1"
|
||||
:z-index="1000"
|
||||
style="border-radius: 10px"
|
||||
@close="onClose"
|
||||
>
|
||||
|
||||
@@ -118,7 +118,7 @@ const onRemoveSelectedItem = (index) => {
|
||||
:modal="false"
|
||||
:top="'30vh'"
|
||||
:width="'610px'"
|
||||
:z-index="1"
|
||||
:z-index="1000"
|
||||
style="border-radius: 10px"
|
||||
@open="onOpen"
|
||||
@close="onClose"
|
||||
|
||||
@@ -147,7 +147,7 @@ const onRemoveSelectedItem = (index) => {
|
||||
:modal="false"
|
||||
:top="'30vh'"
|
||||
:width="'610px'"
|
||||
:z-index="1"
|
||||
:z-index="1000"
|
||||
style="border-radius: 10px"
|
||||
@open="onOpen"
|
||||
@close="onClose"
|
||||
|
||||
@@ -121,7 +121,7 @@ const onRemoveSelectedItem = (index) => {
|
||||
:modal="false"
|
||||
:top="'30vh'"
|
||||
:width="'610px'"
|
||||
:z-index="1"
|
||||
:z-index="1000"
|
||||
style="border-radius: 10px"
|
||||
@open="onOpen"
|
||||
@close="onClose"
|
||||
|
||||
@@ -69,7 +69,7 @@ const onConfirm = () => {
|
||||
:modal="false"
|
||||
:top="'30vh'"
|
||||
:width="'300px'"
|
||||
:z-index="1"
|
||||
:z-index="1000"
|
||||
style="height: 460px; border-radius: 10px"
|
||||
@open="onOpen"
|
||||
@close="onClose"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, computed, watch } from 'vue'
|
||||
import { getAvatarColor, getFontColor } from '@/js/utils/common'
|
||||
import { STATUS } from '@/const/userConst'
|
||||
import default_avatar from '@/assets/image/default_avatar.png'
|
||||
import { ElAvatar } from 'element-plus'
|
||||
|
||||
const props = defineProps(['showName', 'showId', 'showAvatarThumb', 'userStatus', 'size'])
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ watch(searchTab, () => {
|
||||
v-model="isShowSearchDialog"
|
||||
:show-close="false"
|
||||
:modal="false"
|
||||
:z-index="1"
|
||||
:z-index="1000"
|
||||
@open="onOpen"
|
||||
>
|
||||
<template #header>
|
||||
|
||||
@@ -20,7 +20,8 @@ export const msgContentType = {
|
||||
AUDIO: 4, // 音频文件
|
||||
EMOJI: 5, // 表情
|
||||
VIDEO: 6, // 视频
|
||||
DOCUMENT: 7 // 文档
|
||||
DOCUMENT: 7, // 文档
|
||||
FORWARD_TOGETHER: 8 // 合并转发消息
|
||||
}
|
||||
|
||||
// 消息发送状态
|
||||
|
||||
@@ -1176,6 +1176,11 @@ const handleCancleMultiSelect = () => {
|
||||
multiSelectedMsgIds.value.clear()
|
||||
}
|
||||
|
||||
const handleForwardTogether = () => {
|
||||
isShowForwardMsgDialog.value = true
|
||||
showForwardMsgDialogTitle.value = '合并转发'
|
||||
}
|
||||
|
||||
const handleForwardOneByOne = () => {
|
||||
isShowForwardMsgDialog.value = true
|
||||
showForwardMsgDialogTitle.value = '逐条转发'
|
||||
@@ -1312,7 +1317,11 @@ const handleGlobalMouseUp = (e) => {
|
||||
itemLeft > selectionRect.right
|
||||
)
|
||||
|
||||
if (isIntersect) {
|
||||
if (
|
||||
isIntersect &&
|
||||
(selectionRect.right - selectionRect.left > 50 || // 移动超过50 + 50才算有效
|
||||
selectionRect.bottom - selectionRect.top > 50)
|
||||
) {
|
||||
if (!isMultiSelect.value) {
|
||||
isMultiSelect.value = true
|
||||
}
|
||||
@@ -1341,15 +1350,37 @@ const showForwardMsgDialogTitle = ref('')
|
||||
const forwardMsgs = computed(() => {
|
||||
let msgs = []
|
||||
multiSelectedMsgIds.value.forEach((item) => {
|
||||
msgs.push(messageData.getMsg(selectedSessionId.value, item))
|
||||
const msg = messageData.getMsg(selectedSessionId.value, item)
|
||||
let nickName = ''
|
||||
if (msg.msgType === MsgType.CHAT) {
|
||||
if (myAccount.value === msg.fromId) {
|
||||
nickName = userData.user.nickName
|
||||
} else {
|
||||
nickName = messageData.sessionList[msg.sessionId].objectInfo.nickName
|
||||
}
|
||||
} else if (msg.msgType === MsgType.GROUP_CHAT) {
|
||||
const groupId = messageData.sessionList[msg.sessionId].remoteId
|
||||
const members = groupData.groupMembersList[groupId]
|
||||
nickName = members[msg.fromId].nickName
|
||||
}
|
||||
msgs.push({
|
||||
...msg,
|
||||
nickName
|
||||
})
|
||||
})
|
||||
|
||||
return msgs.sort((a, b) => {
|
||||
const timeA = new Date(a.sendTime || a.msgTime).getTime()
|
||||
const timeB = new Date(b.sendTime || b.msgTime).getTime()
|
||||
return timeA - timeB
|
||||
})
|
||||
if (showForwardMsgDialogTitle.value === '合并转发') {
|
||||
return [
|
||||
{
|
||||
type: msgContentType.FORWARD_TOGETHER,
|
||||
value: msgs
|
||||
}
|
||||
]
|
||||
} else {
|
||||
return msgs
|
||||
}
|
||||
})
|
||||
|
||||
const sessionListSortedKey = computed(() => {
|
||||
return sessionListSorted.value
|
||||
.filter((item) => {
|
||||
@@ -1382,24 +1413,13 @@ const handleConfirmForwardMsg = async (sessions) => {
|
||||
}
|
||||
|
||||
for (const forwardMsg of forwardMsgs.value) {
|
||||
const content =
|
||||
showForwardMsgDialogTitle.value !== '合并转发'
|
||||
? forwardMsg.content
|
||||
: JSON.stringify(forwardMsg)
|
||||
await handleSendForwardMsg({
|
||||
session: item,
|
||||
content: forwardMsg.content
|
||||
.split(/(<.*?>)/)
|
||||
.map((item) => {
|
||||
const sliceStr = item.slice(1, -1)
|
||||
const index = sliceStr.indexOf('-')
|
||||
if (index !== -1) {
|
||||
const nickName = sliceStr.slice(index + 1)
|
||||
if (nickName) {
|
||||
return `@${nickName}`
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
.join('')
|
||||
content: content
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1710,6 +1730,7 @@ const onShowRecorder = () => {
|
||||
ref="inputMultiSelectRef"
|
||||
:selectedCount="multiSelectedMsgIds.size"
|
||||
@exit="handleCancleMultiSelect"
|
||||
@forwardTogether="handleForwardTogether"
|
||||
@forwardOneByOne="handleForwardOneByOne"
|
||||
@batchDelete="handleBatchDeleteMsg"
|
||||
></InputMultiSelect>
|
||||
|
||||
602
src/views/message/components/DialogForMsgList.vue
Normal file
602
src/views/message/components/DialogForMsgList.vue
Normal file
@@ -0,0 +1,602 @@
|
||||
<script setup lang="jsx">
|
||||
import { onMounted, computed, watch, createApp, h } from 'vue'
|
||||
import { ElDialog, ElLoading } from 'element-plus'
|
||||
import {
|
||||
useUserStore,
|
||||
useUserCardStore,
|
||||
useMessageStore,
|
||||
useImageStore,
|
||||
useAudioStore,
|
||||
useVideoStore,
|
||||
useDocumentStore
|
||||
} from '@/stores'
|
||||
import { showTimeFormat, jsonParseSafe } from '@/js/utils/common'
|
||||
import UserAvatarIcon from '@/components/common/UserAvatarIcon.vue'
|
||||
import { el_loading_options } from '@/const/commonConst'
|
||||
import { userQueryService } from '@/api/user'
|
||||
import router from '@/router'
|
||||
import { msgContentType } 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'
|
||||
import MsgBoxVideo from '@/views/message/components/MsgBoxVideo.vue'
|
||||
import MsgBoxDocument from '@/views/message/components/MsgBoxDocument.vue'
|
||||
import DialogForMsgList from '@/views/message/components/DialogForMsgList.vue'
|
||||
import { emojis } from '@/js/utils/emojis'
|
||||
import { MsgType } from '@/proto/msg'
|
||||
|
||||
const props = defineProps(['isShow', 'title', 'sessionId', 'msgs', 'tier'])
|
||||
const emit = defineEmits(['update:isShow', 'showUserCard', 'close'])
|
||||
|
||||
const userData = useUserStore()
|
||||
const userCardData = useUserCardStore()
|
||||
const messageData = useMessageStore()
|
||||
const imageData = useImageStore()
|
||||
const audioData = useAudioStore()
|
||||
const videoData = useVideoStore()
|
||||
const documentData = useDocumentStore()
|
||||
|
||||
onMounted(async () => {
|
||||
await messageData.preloadResource(props.msgs)
|
||||
})
|
||||
|
||||
/**
|
||||
* 切换session时要强制关闭:比如点击列表中头像 => 弹出的UserCard => 点击发送消息按钮
|
||||
*/
|
||||
watch(
|
||||
() => router.currentRoute.value.query.sessionId,
|
||||
() => {
|
||||
onClose()
|
||||
}
|
||||
)
|
||||
|
||||
const myAccount = computed(() => {
|
||||
return userData.user.account
|
||||
})
|
||||
|
||||
const isMyAccount = (account) => {
|
||||
return myAccount.value === account
|
||||
}
|
||||
|
||||
const renderContent = ({ msg }) => {
|
||||
const content = msg.content
|
||||
const msgId = msg.msgId
|
||||
const msgType = msg.msgType
|
||||
const contentJson = jsonParseSafe(content)
|
||||
if (!contentJson) {
|
||||
return renderMix(content, msgType)
|
||||
}
|
||||
|
||||
const type = contentJson['type']
|
||||
const value = contentJson['value']
|
||||
if (!type || !value) {
|
||||
return renderMix(content, msgType)
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case msgContentType.MIX:
|
||||
return renderMix(value, msgType)
|
||||
case msgContentType.TEXT:
|
||||
return renderText(value)
|
||||
case msgContentType.RECORDING:
|
||||
return renderRecording(value)
|
||||
case msgContentType.AUDIO:
|
||||
return renderAudio(value)
|
||||
case msgContentType.IMAGE:
|
||||
return renderImage(value)
|
||||
case msgContentType.EMOJI:
|
||||
return renderEmoji(value)
|
||||
case msgContentType.VIDEO:
|
||||
return renderVideo(value, msgId)
|
||||
case msgContentType.DOCUMENT:
|
||||
return renderDocument(value)
|
||||
case msgContentType.FORWARD_TOGETHER:
|
||||
return renderForwardTogether(value)
|
||||
default:
|
||||
return <span>{content}</span>
|
||||
}
|
||||
}
|
||||
|
||||
const renderMix = (content, msgType) => {
|
||||
if (!content) return h('div', [])
|
||||
let contentArray = []
|
||||
|
||||
// 1. 先匹配quote引用内容
|
||||
content.split(/(「\{.*?\}」)/).forEach((item) => {
|
||||
if (item.startsWith('「{') && item.endsWith('}」')) {
|
||||
// quote引用内容直接添加如数组
|
||||
contentArray.push(item)
|
||||
} else {
|
||||
//2. 匹配内容中的图片
|
||||
item.split(/(\{\d+\})/).forEach((item) => {
|
||||
//3. 匹配内容中的表情
|
||||
item.split(/(\[.*?\])/).forEach((item) => {
|
||||
item.split(/(<.*?>)/).forEach((item) => {
|
||||
if (item) {
|
||||
contentArray.push(item)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return contentArray.map((item) => {
|
||||
if (item.startsWith('{') && item.endsWith('}')) {
|
||||
return renderImage(item.slice(1, -1), true)
|
||||
} else if (item.startsWith('[') && item.endsWith(']')) {
|
||||
return renderEmoji(item.slice(1, -1))
|
||||
} else if (item.startsWith('<') && item.endsWith('>')) {
|
||||
return renderAt(item.slice(1, -1), msgType)
|
||||
} else if (item.startsWith('「{') && item.endsWith('}」')) {
|
||||
return renderQuote(item.slice(1, -1))
|
||||
} else {
|
||||
return <span>{item.trim()}</span>
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const renderText = (content) => {
|
||||
return <span>{content}</span>
|
||||
}
|
||||
|
||||
const renderRecording = (content) => {
|
||||
const audioId = content
|
||||
const url = audioData.audio[audioId]?.downloadUrl
|
||||
const duration = audioData.audio[audioId]?.duration
|
||||
if (url) {
|
||||
return <MsgBoxRecording audioUrl={url} duration={duration}></MsgBoxRecording>
|
||||
} else {
|
||||
return <span>{content}</span>
|
||||
}
|
||||
}
|
||||
|
||||
const renderAudio = (content) => {
|
||||
const audioId = content
|
||||
const url = audioData.audio[audioId]?.downloadUrl
|
||||
if (url) {
|
||||
return (
|
||||
<MsgBoxAudio
|
||||
url={url}
|
||||
fileName={audioData.audio[audioId].fileName}
|
||||
size={audioData.audio[audioId].size}
|
||||
></MsgBoxAudio>
|
||||
)
|
||||
} else {
|
||||
return <span>{content}</span>
|
||||
}
|
||||
}
|
||||
|
||||
const renderEmoji = (content) => {
|
||||
const emojiId = `[${content}]`
|
||||
const url = emojis[emojiId]
|
||||
if (url) {
|
||||
return <img class={'emoji'} src={url} alt={emojiId} title={content}></img>
|
||||
} else {
|
||||
return <span>{content}</span>
|
||||
}
|
||||
}
|
||||
|
||||
const renderImage = (content, isForMix = false) => {
|
||||
const imgId = content
|
||||
imageData.setImageInSession(props.sessionId, imageData.image[imgId])
|
||||
if (imageData.image[imgId]) {
|
||||
return (
|
||||
<MsgBoxImage
|
||||
sessionId={props.sessionId}
|
||||
imgId={imgId}
|
||||
isForMix={isForMix}
|
||||
thumbWidth={imageData.image[imgId].thumbWidth}
|
||||
thumbHeight={imageData.image[imgId].thumbHeight}
|
||||
></MsgBoxImage>
|
||||
)
|
||||
} else {
|
||||
return <span>{isForMix ? content : `[${content}]`}</span>
|
||||
}
|
||||
}
|
||||
|
||||
const renderVideo = (content, msgId) => {
|
||||
const videoId = content
|
||||
const url = videoData.video[videoId]?.downloadUrl
|
||||
if (url) {
|
||||
return (
|
||||
<MsgBoxVideo
|
||||
msgId={msgId + '-' + new Date().getTime().toString()} // 加个时间戳,避免视频播放组件的id冲突
|
||||
videoId={videoId}
|
||||
url={url}
|
||||
fileName={videoData.video[videoId].fileName}
|
||||
size={videoData.video[videoId].size}
|
||||
width={videoData.video[videoId].width}
|
||||
height={videoData.video[videoId].height}
|
||||
></MsgBoxVideo>
|
||||
)
|
||||
} else {
|
||||
return <span>{content}</span>
|
||||
}
|
||||
}
|
||||
|
||||
const renderDocument = (content) => {
|
||||
const documentId = content
|
||||
const url = documentData.document[documentId]?.downloadUrl
|
||||
if (url) {
|
||||
return (
|
||||
<MsgBoxDocument
|
||||
url={url}
|
||||
fileName={documentData.document[documentId].fileName}
|
||||
fileSize={documentData.document[documentId].size}
|
||||
contentType={documentData.document[documentId].documentType}
|
||||
></MsgBoxDocument>
|
||||
)
|
||||
} else {
|
||||
return <span>{content}</span>
|
||||
}
|
||||
}
|
||||
|
||||
const renderForwardTogether = (msgs) => {
|
||||
const title = '聊天记录'
|
||||
const msgsSorted = msgs.sort((a, b) => {
|
||||
const timeA = new Date(a.sendTime || a.msgTime).getTime()
|
||||
const timeB = new Date(b.sendTime || b.msgTime).getTime()
|
||||
return timeA - timeB
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
class={'forward-together'}
|
||||
onClick={() => {
|
||||
// 创建挂载容器
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
const app = createApp({
|
||||
render: () => {
|
||||
return h(DialogForMsgList, {
|
||||
isShow: true,
|
||||
title,
|
||||
sessionId: msgsSorted[0].sessionId,
|
||||
msgs: msgsSorted,
|
||||
tier: (props.tier || 0) + 1,
|
||||
onClose: () => {
|
||||
app.unmount()
|
||||
document.body.removeChild(container)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// 挂载到新创建的容器
|
||||
app.mount(container)
|
||||
}}
|
||||
>
|
||||
<div class={'main'}>
|
||||
<span class={'title'}>{title}</span>
|
||||
<div class={'msg-list'}>
|
||||
{msgsSorted.map((msg, index) => {
|
||||
return (
|
||||
<div key={index} class={'msg-item'}>
|
||||
<span class={'msg-item-nickname'}>{msg.nickName || msg.fromId}</span>
|
||||
<span>{':'}</span>
|
||||
<span class={'msg-item-content'}>
|
||||
{showMsgContentInForwardTogether(msg.content)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<span class={'footer bdr-t'}>{`查看${msgsSorted.length}条转发消息`}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderAt = (content, msgType) => {
|
||||
const index = content.indexOf('-')
|
||||
if (index !== -1) {
|
||||
const nickName = content.slice(index + 1)
|
||||
if (msgType === MsgType.GROUP_CHAT && nickName) {
|
||||
return <span>{`@${nickName} `}</span>
|
||||
} else {
|
||||
return <span>{`<${content}>`}</span>
|
||||
}
|
||||
} else {
|
||||
return <span>{`<${content}>`}</span>
|
||||
}
|
||||
}
|
||||
|
||||
const renderQuote = (quoteContent) => {
|
||||
const { nickName, content, msgTime } = jsonParseSafe(quoteContent)
|
||||
let showContent = content || ''
|
||||
if (content) {
|
||||
const defaultContent = content.replace(/<(?:.*?)-(.*?)>/g, '@$1').replace(/\{\d+\}/g, '[图片]')
|
||||
showContent = defaultContent
|
||||
const contentJson = jsonParseSafe(defaultContent)
|
||||
if (contentJson) {
|
||||
const type = contentJson['type']
|
||||
const objectId = contentJson['value']
|
||||
switch (type) {
|
||||
case msgContentType.RECORDING:
|
||||
showContent = '[语音]'
|
||||
break
|
||||
case msgContentType.AUDIO:
|
||||
showContent = `[音频] ${audioData.audio[objectId].fileName}`
|
||||
break
|
||||
case msgContentType.IMAGE:
|
||||
showContent = `[图片] ${imageData.image[objectId].fileName}`
|
||||
break
|
||||
case msgContentType.VIDEO:
|
||||
showContent = `[视频] ${videoData.video[objectId].fileName}`
|
||||
break
|
||||
case msgContentType.DOCUMENT:
|
||||
showContent = `[文档] ${documentData.document[objectId].fileName}`
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 和InputEditor.vue中的结构保持一致,使用相同class可以复用样式
|
||||
return (
|
||||
<div class={'quote-block'}>
|
||||
<div class={'quote-wrapper'}>
|
||||
<div class={'quote-sender'}>
|
||||
<span class="quote-nickName">{nickName + ' '}</span>
|
||||
<span class={'quote-msgTime'}>{msgTime + ':'}</span>
|
||||
</div>
|
||||
<span class={'quote-content'}>{showContent}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const showMsgContentInForwardTogether = (content) => {
|
||||
const jsonContent = jsonParseSafe(content)
|
||||
let template
|
||||
if (jsonContent && jsonContent['type'] && jsonContent['value']) {
|
||||
if (jsonContent['type'] == msgContentType.IMAGE) {
|
||||
template = '[图片]'
|
||||
} else if (jsonContent['type'] == msgContentType.AUDIO) {
|
||||
template = '[音频]'
|
||||
} else if (jsonContent['type'] == msgContentType.RECORDING) {
|
||||
template = '[语音]'
|
||||
} else if (jsonContent['type'] == msgContentType.VIDEO) {
|
||||
template = '[视频]'
|
||||
} else if (jsonContent['type'] == msgContentType.DOCUMENT) {
|
||||
template = '[文件]'
|
||||
} else if (jsonContent['type'] == msgContentType.FORWARD_TOGETHER) {
|
||||
template = '[聊天记录]'
|
||||
} else {
|
||||
template = jsonContent['value']
|
||||
}
|
||||
return template
|
||||
} else {
|
||||
return content
|
||||
.replace(/\{\d+\}/g, '[图片]')
|
||||
.replace(/(「\{.*?\}」)/, '[引用]')
|
||||
.split(/(<.*?>)/)
|
||||
.map((item) => {
|
||||
const sliceStr = item.slice(1, -1)
|
||||
const index = sliceStr.indexOf('-')
|
||||
if (index !== -1) {
|
||||
const nickName = sliceStr.slice(index + 1)
|
||||
if (nickName) {
|
||||
return `@${nickName}`
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
.join('')
|
||||
}
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
emit('update:isShow', false)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const onShowUserCard = (account) => {
|
||||
const loadingInstance = ElLoading.service(el_loading_options)
|
||||
if (myAccount.value === account) {
|
||||
userData
|
||||
.updateUser()
|
||||
.then(() => {
|
||||
userCardData.setUserInfo(userData.user)
|
||||
userCardData.setIsShow(true)
|
||||
})
|
||||
.finally(() => {
|
||||
loadingInstance.close()
|
||||
})
|
||||
} else {
|
||||
userQueryService({ account: account })
|
||||
.then((res) => {
|
||||
userCardData.setUserInfo(res.data.data)
|
||||
userCardData.setIsShow(true)
|
||||
})
|
||||
.finally(() => {
|
||||
loadingInstance.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dialog-msg-list-wrapper">
|
||||
<el-dialog
|
||||
class="dialog-msg-list"
|
||||
:model-value="props.isShow"
|
||||
:modal="false"
|
||||
draggable
|
||||
:width="'600px'"
|
||||
:top="`${30 + (props.tier || 0)}vh`"
|
||||
:z-index="1000"
|
||||
:style="{
|
||||
minHeight: '360px',
|
||||
marginLeft: `calc(50% - 300px + ${props.tier || 0} * 1vw)`
|
||||
}"
|
||||
@closed="onClose"
|
||||
>
|
||||
<template #header>
|
||||
<span class="title bdr-b">{{ props.title }}</span>
|
||||
</template>
|
||||
<div class="dialog-msg-item-container my-scrollbar">
|
||||
<div
|
||||
v-for="item in props.msgs"
|
||||
:key="item.msgId"
|
||||
class="dialog-msg-item"
|
||||
:style="{
|
||||
flexDirection: isMyAccount(item.fromId) ? 'row-reverse' : 'row',
|
||||
justifyContent: isMyAccount(item.fromId) ? 'end' : 'start'
|
||||
}"
|
||||
>
|
||||
<div class="dialog-msg-item-avatar">
|
||||
<UserAvatarIcon
|
||||
class="avatar-message-item"
|
||||
:size="'small'"
|
||||
:showId="item.fromId"
|
||||
:showName="item.nickName"
|
||||
@click="onShowUserCard(item.fromId)"
|
||||
></UserAvatarIcon>
|
||||
</div>
|
||||
<div class="dialog-msg-item-main">
|
||||
<div
|
||||
class="dialog-msg-item-header"
|
||||
:style="{
|
||||
justifyContent: isMyAccount(item.fromId) ? 'end' : 'start'
|
||||
}"
|
||||
>
|
||||
<div class="dialog-msg-item-nickname">{{ item.nickName }}</div>
|
||||
<div class="dialog-msg-item-time">{{ showTimeFormat(item.msgTime) }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="dialog-msg-item-body"
|
||||
:style="{
|
||||
justifyContent: isMyAccount(item.fromId) ? 'end' : 'start'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="dialog-msg-item-content"
|
||||
:style="{
|
||||
borderTopLeftRadius: isMyAccount(item.fromId) ? '10px' : '0',
|
||||
borderTopRightRadius: isMyAccount(item.fromId) ? '0' : '10px',
|
||||
backgroundColor: isMyAccount(item.fromId) ? '#c6e2ff' : '#dedfe0'
|
||||
}"
|
||||
>
|
||||
<renderContent :msg="item" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dialog-msg-list-wrapper {
|
||||
:deep(.el-dialog) {
|
||||
.el-dialog__header {
|
||||
.title {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-bottom: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-msg-list {
|
||||
.dialog-msg-item-container {
|
||||
max-height: 480px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 0 5px;
|
||||
overflow-y: scroll;
|
||||
|
||||
.dialog-msg-item {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
|
||||
.dialog-msg-item-main {
|
||||
max-width: 480px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.dialog-msg-item-header {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.dialog-msg-item-body {
|
||||
display: flex;
|
||||
|
||||
.dialog-msg-item-content {
|
||||
padding: 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// h函数中动态生成的组件,这里的样式需要用deep穿透
|
||||
:deep(.forward-together) {
|
||||
width: 240px;
|
||||
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
.msg-list {
|
||||
max-height: 72px;
|
||||
color: gray;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.msg-item {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
|
||||
.msg-item-nickname {
|
||||
max-width: 80px;
|
||||
white-space: nowrap; //防止文本自动换行,确保在一行内显示,这样当文本超出宽度时才会触发省略号
|
||||
overflow: hidden; //当文本超出元素范围时,隐藏超出的部分。
|
||||
text-overflow: ellipsis; //在文本溢出并且overflow属性设置为hidden时,显示省略号。
|
||||
}
|
||||
|
||||
.msg-item-content {
|
||||
flex: 1;
|
||||
white-space: nowrap; //防止文本自动换行,确保在一行内显示,这样当文本超出宽度时才会触发省略号
|
||||
overflow: hidden; //当文本超出元素范围时,隐藏超出的部分。
|
||||
text-overflow: ellipsis; //在文本溢出并且overflow属性设置为hidden时,显示省略号。
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -29,6 +29,12 @@ onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
const handleForwardTogether = () => {
|
||||
if (props.selectedCount > 0) {
|
||||
emit('forwardTogether')
|
||||
}
|
||||
}
|
||||
|
||||
const handleForwardOneByOne = () => {
|
||||
if (props.selectedCount > 0) {
|
||||
emit('forwardOneByOne')
|
||||
@@ -53,7 +59,7 @@ const handleBatchDelete = () => {
|
||||
<span class="selected-count">已选中:{{ props.selectedCount || 0 }}条消息</span>
|
||||
<div class="multi-select-funtions">
|
||||
<div class="function-item">
|
||||
<div class="fun-icon">
|
||||
<div class="fun-icon" @click="handleForwardTogether">
|
||||
<ForwardIcon></ForwardIcon>
|
||||
</div>
|
||||
<span>合并转发</span>
|
||||
|
||||
@@ -24,6 +24,7 @@ import MsgBoxDocument from '@/views/message/components/MsgBoxDocument.vue'
|
||||
import MenuMsgItem from '@/views/message/components/MenuMsgItem.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { msgChatDeleteMsgService, msgChatRevokeMsgService } from '@/api/message'
|
||||
import DialogForMsgList from '@/views/message/components/DialogForMsgList.vue'
|
||||
|
||||
const props = defineProps([
|
||||
'sessionId',
|
||||
@@ -112,6 +113,8 @@ const renderComponent = (content) => {
|
||||
return renderVideo(value)
|
||||
case msgContentType.DOCUMENT:
|
||||
return renderDocument(value)
|
||||
case msgContentType.FORWARD_TOGETHER:
|
||||
return renderForwardTogether(value)
|
||||
default:
|
||||
return h('span', content)
|
||||
}
|
||||
@@ -218,6 +221,104 @@ const renderQuote = (quoteContent) => {
|
||||
)
|
||||
}
|
||||
|
||||
const showMsgContentInForwardTogether = (content) => {
|
||||
const jsonContent = jsonParseSafe(content)
|
||||
let template
|
||||
if (jsonContent && jsonContent['type'] && jsonContent['value']) {
|
||||
if (jsonContent['type'] == msgContentType.IMAGE) {
|
||||
template = '[图片]'
|
||||
} else if (jsonContent['type'] == msgContentType.AUDIO) {
|
||||
template = '[音频]'
|
||||
} else if (jsonContent['type'] == msgContentType.RECORDING) {
|
||||
template = '[语音]'
|
||||
} else if (jsonContent['type'] == msgContentType.VIDEO) {
|
||||
template = '[视频]'
|
||||
} else if (jsonContent['type'] == msgContentType.DOCUMENT) {
|
||||
template = '[文件]'
|
||||
} else if (jsonContent['type'] == msgContentType.FORWARD_TOGETHER) {
|
||||
template = '[聊天记录]'
|
||||
} else {
|
||||
template = jsonContent['value']
|
||||
}
|
||||
return template
|
||||
} else {
|
||||
return content
|
||||
.replace(/\{\d+\}/g, '[图片]')
|
||||
.replace(/(「\{.*?\}」)/, '[引用]')
|
||||
.split(/(<.*?>)/)
|
||||
.map((item) => {
|
||||
const sliceStr = item.slice(1, -1)
|
||||
const index = sliceStr.indexOf('-')
|
||||
if (index !== -1) {
|
||||
const nickName = sliceStr.slice(index + 1)
|
||||
if (nickName) {
|
||||
return `@${nickName}`
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
.join('')
|
||||
}
|
||||
}
|
||||
|
||||
const renderForwardTogether = (msgs) => {
|
||||
const title =
|
||||
(msgs[0].msgType === MsgType.GROUP_CHAT ? '群聊' : nickNameFromMsg.value) + '的聊天记录'
|
||||
|
||||
const msgsSorted = msgs.sort((a, b) => {
|
||||
const timeA = new Date(a.sendTime || a.msgTime).getTime()
|
||||
const timeB = new Date(b.sendTime || b.msgTime).getTime()
|
||||
return timeA - timeB
|
||||
})
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: 'forward-together',
|
||||
onClick: () => {
|
||||
// 创建挂载容器
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
const app = createApp({
|
||||
render: () => {
|
||||
return h(DialogForMsgList, {
|
||||
isShow: true,
|
||||
title,
|
||||
sessionId: msgsSorted[0].sessionId,
|
||||
msgs: msgsSorted,
|
||||
onClose: () => {
|
||||
app.unmount()
|
||||
document.body.removeChild(container)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// 挂载到新创建的容器
|
||||
app.mount(container)
|
||||
}
|
||||
},
|
||||
[
|
||||
h('div', { class: 'main' }, [
|
||||
h('span', { class: 'title' }, title),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'msg-list' },
|
||||
msgsSorted.map((msg, index) => {
|
||||
return h('div', { class: 'msg-item', key: index }, [
|
||||
h('span', { class: 'msg-item-nickname' }, msg.nickName || msg.fromId),
|
||||
h('span', ':'),
|
||||
h('span', { class: 'msg-item-content' }, showMsgContentInForwardTogether(msg.content))
|
||||
])
|
||||
})
|
||||
)
|
||||
]),
|
||||
h('span', { class: 'footer bdr-t' }, `查看${msgsSorted.length}条转发消息`)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
const renderAt = (content) => {
|
||||
const index = content.indexOf('-')
|
||||
if (index !== -1) {
|
||||
@@ -712,7 +813,7 @@ const isSelf = computed(() => {
|
||||
})
|
||||
|
||||
const objectInfoFromMsg = computed(() => {
|
||||
if (msg.value.msgType === MsgType.GROUP_CHAT) {
|
||||
if (isGroupChatMsgType.value) {
|
||||
const groupId = messageData.sessionList[props.sessionId]?.remoteId
|
||||
const members = groupData.groupMembersList[groupId]
|
||||
return members ? members[msg.value.fromId] : { account: msg.value.fromId }
|
||||
@@ -1330,4 +1431,61 @@ const handleItemClick = () => {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
// h函数中动态生成的组件,这里的样式需要用deep穿透
|
||||
:deep(.forward-together) {
|
||||
width: 240px;
|
||||
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
.msg-list {
|
||||
max-height: 72px;
|
||||
color: gray;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.msg-item {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
|
||||
.msg-item-nickname {
|
||||
max-width: 80px;
|
||||
white-space: nowrap; //防止文本自动换行,确保在一行内显示,这样当文本超出宽度时才会触发省略号
|
||||
overflow: hidden; //当文本超出元素范围时,隐藏超出的部分。
|
||||
text-overflow: ellipsis; //在文本溢出并且overflow属性设置为hidden时,显示省略号。
|
||||
}
|
||||
|
||||
.msg-item-content {
|
||||
flex: 1;
|
||||
white-space: nowrap; //防止文本自动换行,确保在一行内显示,这样当文本超出宽度时才会触发省略号
|
||||
overflow: hidden; //当文本超出元素范围时,隐藏超出的部分。
|
||||
text-overflow: ellipsis; //在文本溢出并且overflow属性设置为hidden时,显示省略号。
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -257,6 +257,8 @@ const showDetailContent = computed(() => {
|
||||
template = '[视频]'
|
||||
} else if (jsonContent['type'] == msgContentType.DOCUMENT) {
|
||||
template = '[文件]'
|
||||
} else if (jsonContent['type'] == msgContentType.FORWARD_TOGETHER) {
|
||||
template = '[聊天记录]'
|
||||
} else {
|
||||
template = jsonContent['value']
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user