✨ feat(im): 增加群聊消息的管理
parent
dfbae06afa
commit
d64a695673
|
|
@ -0,0 +1,160 @@
|
|||
<template>
|
||||
<!-- 文本 / 系统提示文本:直接显示纯文本 -->
|
||||
<span v-if="isText" class="whitespace-pre-wrap break-all">{{ textContent }}</span>
|
||||
|
||||
<!-- 图片:缩略图 + 点击放大 -->
|
||||
<el-image
|
||||
v-else-if="isImage && imagePayload"
|
||||
class="w-60px h-60px rounded align-middle"
|
||||
:src="imagePayload.thumbnailUrl || imagePayload.url"
|
||||
:preview-src-list="[imagePayload.url]"
|
||||
:preview-teleported="true"
|
||||
fit="cover"
|
||||
/>
|
||||
|
||||
<!-- 文件:图标 + 名称 + 大小,单行内显示 -->
|
||||
<span v-else-if="isFile && filePayload" class="inline-flex gap-1.5 items-center">
|
||||
<Icon :icon="fileIconInfo.icon" :color="fileIconInfo.color" :size="18" />
|
||||
<span class="max-w-200px truncate">{{ filePayload.name }}</span>
|
||||
<span v-if="filePayload.size" class="text-12px text-[var(--el-text-color-secondary)]">
|
||||
{{ formatFileSize(filePayload.size) }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- 语音:图标 + 时长 -->
|
||||
<span v-else-if="isVoice && voicePayload" class="inline-flex gap-1.5 items-center">
|
||||
<Icon icon="ant-design:audio-outlined" :size="16" color="#606266" />
|
||||
<span>{{ formatSeconds(voicePayload.duration ?? 0) }}</span>
|
||||
</span>
|
||||
|
||||
<!-- 视频:图标 + 占位文案 + 大小 -->
|
||||
<span v-else-if="isVideo" class="inline-flex gap-1.5 items-center">
|
||||
<Icon icon="ant-design:video-camera-filled" :size="16" color="#9c27b0" />
|
||||
<span>[视频]</span>
|
||||
<span v-if="videoPayload?.size" class="text-12px text-[var(--el-text-color-secondary)]">
|
||||
{{ formatFileSize(videoPayload.size) }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- 控制类消息:撤回 / 已读 / 回执 -->
|
||||
<span
|
||||
v-else-if="props.type === ImMessageType.RECALL"
|
||||
class="text-12px text-[var(--el-text-color-secondary)]"
|
||||
>
|
||||
[消息已撤回]
|
||||
</span>
|
||||
<span
|
||||
v-else-if="props.type === ImMessageType.READ"
|
||||
class="text-12px text-[var(--el-text-color-secondary)]"
|
||||
>
|
||||
[已读回执]
|
||||
</span>
|
||||
<span
|
||||
v-else-if="props.type === ImMessageType.RECEIPT"
|
||||
class="text-12px text-[var(--el-text-color-secondary)]"
|
||||
>
|
||||
[回执]
|
||||
</span>
|
||||
|
||||
<!-- 系统事件类(FRIEND_* / GROUP_*):content 通常是结构化 JSON,回退原始预览 -->
|
||||
<span v-else class="whitespace-pre-wrap break-all">{{ fallbackText }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import Icon from '@/components/Icon/src/Icon.vue'
|
||||
import { formatFileSize } from '@/utils/file'
|
||||
import { formatSeconds } from '@/utils/formatTime'
|
||||
import { ImMessageType } from '@/views/im/utils/constants'
|
||||
import {
|
||||
parseMessage,
|
||||
resolveTipText,
|
||||
type ImageMessage,
|
||||
type FileMessage,
|
||||
type AudioMessage,
|
||||
type VideoMessage
|
||||
} from '@/views/im/utils/message'
|
||||
|
||||
defineOptions({ name: 'ImMessageContentPreview' })
|
||||
|
||||
const props = defineProps<{
|
||||
/** 消息类型,对应 ImMessageType */
|
||||
type?: number
|
||||
/** 消息 content(JSON 字符串或裸文本) */
|
||||
content?: string
|
||||
}>()
|
||||
|
||||
/** 各类型判定 */
|
||||
const isText = computed(
|
||||
() => props.type === ImMessageType.TEXT || props.type === ImMessageType.TIP_TEXT
|
||||
)
|
||||
const isImage = computed(() => props.type === ImMessageType.IMAGE)
|
||||
const isFile = computed(() => props.type === ImMessageType.FILE)
|
||||
const isVoice = computed(() => props.type === ImMessageType.VOICE)
|
||||
const isVideo = computed(() => props.type === ImMessageType.VIDEO)
|
||||
|
||||
/** 文本内容:兼容 JSON 包裹和裸字符串两种形态 */
|
||||
const textContent = computed(() => resolveTipText(props.content || ''))
|
||||
|
||||
const imagePayload = computed(() =>
|
||||
isImage.value ? parseMessage<ImageMessage>(props.content || '') : null
|
||||
)
|
||||
const filePayload = computed(() =>
|
||||
isFile.value ? parseMessage<FileMessage>(props.content || '') : null
|
||||
)
|
||||
const voicePayload = computed(() =>
|
||||
isVoice.value ? parseMessage<AudioMessage>(props.content || '') : null
|
||||
)
|
||||
const videoPayload = computed(() =>
|
||||
isVideo.value ? parseMessage<VideoMessage>(props.content || '') : null
|
||||
)
|
||||
|
||||
/** 文件图标:按扩展名分配 icon + 颜色,对齐 home 端 MessageItem 的观感 */
|
||||
const fileIconInfo = computed<{ icon: string; color: string }>(() => {
|
||||
const name = filePayload.value?.name || ''
|
||||
const ext = name.split('.').pop()?.toLowerCase() || ''
|
||||
if (ext === 'pdf') {
|
||||
return { icon: 'ant-design:file-pdf-filled', color: '#ed5757' }
|
||||
}
|
||||
if (['doc', 'docx'].includes(ext)) {
|
||||
return { icon: 'ant-design:file-word-filled', color: '#2b7cd3' }
|
||||
}
|
||||
if (['xls', 'xlsx'].includes(ext)) {
|
||||
return { icon: 'ant-design:file-excel-filled', color: '#1f7244' }
|
||||
}
|
||||
if (['ppt', 'pptx'].includes(ext)) {
|
||||
return { icon: 'ant-design:file-ppt-filled', color: '#d24726' }
|
||||
}
|
||||
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {
|
||||
return { icon: 'ant-design:file-zip-filled', color: '#f0ad4e' }
|
||||
}
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) {
|
||||
return { icon: 'ant-design:file-image-filled', color: '#9c27b0' }
|
||||
}
|
||||
if (['mp4', 'mov', 'avi', 'mkv', 'wmv', 'flv'].includes(ext)) {
|
||||
return { icon: 'ant-design:video-camera-filled', color: '#9c27b0' }
|
||||
}
|
||||
if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(ext)) {
|
||||
return { icon: 'ant-design:audio-filled', color: '#9c27b0' }
|
||||
}
|
||||
if (['txt', 'md', 'log', 'json', 'xml'].includes(ext)) {
|
||||
return { icon: 'ant-design:file-text-filled', color: '#909399' }
|
||||
}
|
||||
return { icon: 'ant-design:file-filled', color: '#909399' }
|
||||
})
|
||||
|
||||
/** 系统事件 / 未知类型 fallback:取 JSON 首层 content,否则原文 */
|
||||
const fallbackText = computed(() => {
|
||||
const raw = props.content || ''
|
||||
if (!raw) {
|
||||
return ''
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (parsed && typeof parsed === 'object' && parsed.content) {
|
||||
return String(parsed.content)
|
||||
}
|
||||
} catch {}
|
||||
return raw
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<el-dialog v-model="dialogVisible" title="群聊消息详情" width="700">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="编号">{{ detail.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户端编号">{{ detail.clientMessageId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="群">
|
||||
{{ detail.groupName }} ({{ detail.groupId }})
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发送人">
|
||||
{{ detail.senderNickname }} ({{ detail.senderId }})
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">
|
||||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_TYPE" :value="detail.type" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<dict-tag :type="DICT_TYPE.IM_GROUP_MESSAGE_STATUS" :value="detail.status" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="@用户" :span="2">
|
||||
<template v-if="detail.atUserIds?.length">
|
||||
<span v-for="(userId, idx) in detail.atUserIds" :key="userId">
|
||||
<span v-if="idx > 0">、</span>
|
||||
<template v-if="userId === IM_AT_ALL_USER_ID">@{{ IM_AT_ALL_NICKNAME }}</template>
|
||||
<template v-else>
|
||||
@{{ detail.atUserNicknames?.[idx] || userId }}
|
||||
<span class="text-gray-400">({{ userId }})</span>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发送时间" :span="2">
|
||||
{{ formatDate(detail.sendTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="消息内容" :span="2">
|
||||
<MessageContentPreview :type="detail.type" :content="detail.content" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="原始 JSON" :span="2">
|
||||
<pre class="content-pre">{{ formatJson(detail.content) }}</pre>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { IM_AT_ALL_NICKNAME, IM_AT_ALL_USER_ID } from '@/views/im/utils/constants'
|
||||
import { formatJson } from '@/views/im/utils/message'
|
||||
import * as ManagerGroupMessageApi from '@/api/im/manager/message/group'
|
||||
import MessageContentPreview from '../MessageContentPreview.vue'
|
||||
|
||||
defineOptions({ name: 'ImGroupMessageDetailDialog' })
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的显示
|
||||
const detail = ref<ManagerGroupMessageApi.ImManagerGroupMessageVO>(
|
||||
{} as ManagerGroupMessageApi.ImManagerGroupMessageVO
|
||||
) // 当前详情数据
|
||||
|
||||
/** 打开详情弹窗 */
|
||||
const open = (row: ManagerGroupMessageApi.ImManagerGroupMessageVO) => {
|
||||
detail.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: 'Menlo', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
background: #f5f5f5;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="80px"
|
||||
label-width="88px"
|
||||
>
|
||||
<el-form-item label="群编号" prop="groupId">
|
||||
<el-input
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
placeholder="请输入群编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-200px"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="发送人编号" prop="senderId">
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
placeholder="请输入发送人用户编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-200px"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="消息类型" prop="type">
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
v-model="queryParams.type"
|
||||
placeholder="请选择消息类型"
|
||||
clearable
|
||||
class="!w-160px"
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.IM_MESSAGE_TYPE)"
|
||||
|
|
@ -46,10 +46,10 @@
|
|||
v-model="queryParams.status"
|
||||
placeholder="请选择消息状态"
|
||||
clearable
|
||||
class="!w-160px"
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.IM_MESSAGE_STATUS)"
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.IM_GROUP_MESSAGE_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
|
|
@ -95,24 +95,9 @@
|
|||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_TYPE" :value="row.type" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="内容预览" align="left" min-width="240" show-overflow-tooltip>
|
||||
<el-table-column label="内容预览" align="left" min-width="240">
|
||||
<template #default="{ row }">
|
||||
{{ getContentPreview(row.content) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="@" align="center" width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.atUserIds?.length ? row.atUserIds.length : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回执" align="center" prop="receiptStatus" width="100">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.IM_GROUP_MESSAGE_RECEIPT_STATUS" :value="row.receiptStatus" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_STATUS" :value="row.status" />
|
||||
<MessageContentPreview :type="row.type" :content="row.content" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
|
|
@ -122,6 +107,31 @@
|
|||
width="180"
|
||||
:formatter="dateFormatter"
|
||||
/>
|
||||
<el-table-column label="@用户" align="left" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.atUserIds?.length">
|
||||
<span v-for="(userId, idx) in row.atUserIds" :key="userId">
|
||||
<span v-if="idx > 0">、</span>
|
||||
<template v-if="userId === IM_AT_ALL_USER_ID">@{{ IM_AT_ALL_NICKNAME }}</template>
|
||||
<template v-else>
|
||||
@{{ row.atUserNicknames?.[idx] || userId }}
|
||||
<span class="text-gray-400">({{ userId }})</span>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回执" align="center" prop="receiptStatus" width="110">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.IM_GROUP_MESSAGE_RECEIPT_STATUS" :value="row.receiptStatus" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :type="DICT_TYPE.IM_GROUP_MESSAGE_STATUS" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
|
|
@ -135,6 +145,7 @@
|
|||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
|
|
@ -143,36 +154,17 @@
|
|||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog v-model="detailVisible" title="群聊消息详情" width="700">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="编号">{{ detail.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户端编号">{{ detail.clientMessageId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="群">{{ detail.groupName }} ({{ detail.groupId }})</el-descriptions-item>
|
||||
<el-descriptions-item label="发送人">{{ detail.senderNickname }} ({{ detail.senderId }})</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">
|
||||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_TYPE" :value="detail.type" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<dict-tag :type="DICT_TYPE.IM_MESSAGE_STATUS" :value="detail.status" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="@ 用户" :span="2">
|
||||
{{ detail.atUserIds?.length ? detail.atUserIds.join(', ') : '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发送时间">{{ formatDate(detail.sendTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(detail.createTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="消息内容(原始 JSON)" :span="2">
|
||||
<pre class="content-pre">{{ formatJson(detail.content) }}</pre>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
<!-- 详情 -->
|
||||
<GroupMessageDetail ref="detailRef" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { dateFormatter, formatDate } from '@/utils/formatTime'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { IM_AT_ALL_NICKNAME, IM_AT_ALL_USER_ID } from '@/views/im/utils/constants'
|
||||
import * as ManagerGroupMessageApi from '@/api/im/manager/message/group'
|
||||
import { getContentPreview, formatJson } from '@/views/im/utils/message'
|
||||
import MessageContentPreview from '../MessageContentPreview.vue'
|
||||
import GroupMessageDetail from './GroupMessageDetail.vue'
|
||||
|
||||
defineOptions({ name: 'ImGroupMessage' })
|
||||
|
||||
|
|
@ -214,16 +206,10 @@ const resetQuery = () => {
|
|||
handleQuery()
|
||||
}
|
||||
|
||||
/** 详情弹窗 */
|
||||
const detailVisible = ref(false) // 详情弹窗的显示
|
||||
const detail = ref<ManagerGroupMessageApi.ImManagerGroupMessageVO>(
|
||||
{} as ManagerGroupMessageApi.ImManagerGroupMessageVO
|
||||
) // 当前详情数据
|
||||
|
||||
/** 打开详情弹窗 */
|
||||
const detailRef = ref<InstanceType<typeof GroupMessageDetail>>() // 详情弹窗 Ref
|
||||
const openDetail = (row: ManagerGroupMessageApi.ImManagerGroupMessageVO) => {
|
||||
detail.value = row
|
||||
detailVisible.value = true
|
||||
detailRef.value?.open(row)
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
|
|
@ -231,16 +217,3 @@ onMounted(() => {
|
|||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
font-family: 'Menlo', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
background: #f5f5f5;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue