合并转发

This commit is contained in:
bob
2025-05-12 21:18:21 +08:00
parent 7618b68713
commit fd5a645bba
14 changed files with 824 additions and 33 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -114,7 +114,7 @@ watch(searchTab, () => {
v-model="isShowSearchDialog"
:show-close="false"
:modal="false"
:z-index="1"
:z-index="1000"
@open="onOpen"
>
<template #header>

View File

@@ -20,7 +20,8 @@ export const msgContentType = {
AUDIO: 4, // 音频文件
EMOJI: 5, // 表情
VIDEO: 6, // 视频
DOCUMENT: 7 // 文档
DOCUMENT: 7, // 文档
FORWARD_TOGETHER: 8 // 合并转发消息
}
// 消息发送状态

View File

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

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

View File

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

View File

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

View File

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