✨ feat(im): 增加群消息的置顶
parent
ffb69063b9
commit
01e0e8e37b
|
|
@ -1,4 +1,5 @@
|
||||||
import request from '@/config/axios'
|
import request from '@/config/axios'
|
||||||
|
import type { ImGroupMessageRespVO } from '@/api/im/message/group'
|
||||||
|
|
||||||
// 群 Response VO
|
// 群 Response VO
|
||||||
export interface ImGroupRespVO {
|
export interface ImGroupRespVO {
|
||||||
|
|
@ -12,6 +13,14 @@ export interface ImGroupRespVO {
|
||||||
status: number // 群状态(0=正常,1=已解散)
|
status: number // 群状态(0=正常,1=已解散)
|
||||||
dissolvedTime?: string // 解散时间
|
dissolvedTime?: string // 解散时间
|
||||||
createTime?: string // 创建时间
|
createTime?: string // 创建时间
|
||||||
|
// TODO @AI:不太对,返回的就是 ImGroupMessageRespVO 数组
|
||||||
|
pinnedMessages?: ImGroupMessageRespVO[] // 群置顶消息列表(后端关联回填,仅当登录用户是群成员时非空)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 群消息置顶 / 取消置顶 Request VO
|
||||||
|
export interface ImGroupMessagePinReqVO {
|
||||||
|
groupId: number // 群编号
|
||||||
|
messageId: number // 消息编号
|
||||||
}
|
}
|
||||||
|
|
||||||
// 群创建 Request VO
|
// 群创建 Request VO
|
||||||
|
|
@ -79,3 +88,13 @@ export const removeGroupAdmin = (data: ImGroupAdminReqVO) => {
|
||||||
export const transferGroupOwner = (data: ImGroupTransferOwnerReqVO) => {
|
export const transferGroupOwner = (data: ImGroupTransferOwnerReqVO) => {
|
||||||
return request.put<boolean>({ url: '/im/group/transfer-owner', data })
|
return request.put<boolean>({ url: '/im/group/transfer-owner', data })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 置顶群消息(仅群主 / 管理员可调)
|
||||||
|
export const pinGroupMessage = (data: ImGroupMessagePinReqVO) => {
|
||||||
|
return request.put<boolean>({ url: '/im/group/pin-message', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消置顶群消息(仅群主 / 管理员可调)
|
||||||
|
export const unpinGroupMessage = (data: ImGroupMessagePinReqVO) => {
|
||||||
|
return request.put<boolean>({ url: '/im/group/unpin-message', data })
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,9 @@
|
||||||
|
|
||||||
<!-- ==================== 查找聊天内容 ==================== -->
|
<!-- ==================== 查找聊天内容 ==================== -->
|
||||||
<!-- 点击 → 父组件打开 MessageHistory 弹窗 -->
|
<!-- 点击 → 父组件打开 MessageHistory 弹窗 -->
|
||||||
|
|
||||||
|
<div class="im-conversation-group-side__spacer"></div>
|
||||||
|
|
||||||
<div class="im-conversation-group-side__section">
|
<div class="im-conversation-group-side__section">
|
||||||
<div
|
<div
|
||||||
class="im-conversation-group-side__row im-conversation-group-side__row--clickable"
|
class="im-conversation-group-side__row im-conversation-group-side__row--clickable"
|
||||||
|
|
@ -356,15 +359,16 @@ import { useMessage } from '@/hooks/web/useMessage'
|
||||||
|
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
import { CommonStatusEnum } from '@/utils/constants'
|
import { CommonStatusEnum } from '@/utils/constants'
|
||||||
import { updateGroup, addGroupAdmin, removeGroupAdmin, transferGroupOwner } from '@/api/im/group'
|
import {
|
||||||
|
updateGroup,
|
||||||
|
addGroupAdmin,
|
||||||
|
removeGroupAdmin,
|
||||||
|
transferGroupOwner
|
||||||
|
} from '@/api/im/group'
|
||||||
import { quitGroup, removeGroupMember, updateGroupMember } from '@/api/im/group/member'
|
import { quitGroup, removeGroupMember, updateGroupMember } from '@/api/im/group/member'
|
||||||
import { useConversationStore } from '../../../../store/conversationStore'
|
import { useConversationStore } from '../../../../store/conversationStore'
|
||||||
import { useGroupStore } from '../../../../store/groupStore'
|
import { useGroupStore } from '../../../../store/groupStore'
|
||||||
import {
|
import { GROUP_ADMIN_MAX_COUNT, ImConversationType, ImGroupMemberRole } from '@/views/im/utils/constants'
|
||||||
GROUP_ADMIN_MAX_COUNT,
|
|
||||||
ImConversationType,
|
|
||||||
ImGroupMemberRole
|
|
||||||
} from '@/views/im/utils/constants'
|
|
||||||
import GroupMemberGrid from '../../../../components/group/GroupMemberGrid.vue'
|
import GroupMemberGrid from '../../../../components/group/GroupMemberGrid.vue'
|
||||||
import GroupMemberAddDialog from '../../../../components/group/GroupMemberAddDialog.vue'
|
import GroupMemberAddDialog from '../../../../components/group/GroupMemberAddDialog.vue'
|
||||||
import GroupMemberSelector, {
|
import GroupMemberSelector, {
|
||||||
|
|
@ -462,6 +466,7 @@ const isOwnerOrAdmin = computed(
|
||||||
() => myRole.value === ImGroupMemberRole.OWNER || myRole.value === ImGroupMemberRole.ADMIN
|
() => myRole.value === ImGroupMemberRole.OWNER || myRole.value === ImGroupMemberRole.ADMIN
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
// 排除已退群成员 + 关键字过滤;按角色排序:群主→管理员→普通成员(同角色按 userId 稳定)
|
// 排除已退群成员 + 关键字过滤;按角色排序:群主→管理员→普通成员(同角色按 userId 稳定)
|
||||||
const visibleMembers = computed(() => {
|
const visibleMembers = computed(() => {
|
||||||
return props.members
|
return props.members
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
<template>
|
||||||
|
<!-- 群聊置顶消息:仅群聊 + 有置顶时显示,悬挂在群聊头部下方左上角;不占整行(对齐微信 PC) -->
|
||||||
|
<div v-if="pinnedMessages.length > 0" class="im-conversation-group-pinned">
|
||||||
|
<!-- 顶部胶囊:单条点击跳转;多条折叠点击展开;多条展开点击折叠 -->
|
||||||
|
<div
|
||||||
|
class="im-conversation-group-pinned__row im-conversation-group-pinned__row--clickable"
|
||||||
|
@click="handleTopClick"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:pushpin-outlined" :size="14" class="im-conversation-group-pinned__icon" />
|
||||||
|
<span class="im-conversation-group-pinned__sender">{{ getSenderName(latest) }}:</span>
|
||||||
|
<span class="im-conversation-group-pinned__text">{{ getPreview(latest) }}</span>
|
||||||
|
<!-- 单条:移除按钮;多条折叠:共 N 条;多条展开:收起箭头 -->
|
||||||
|
<span
|
||||||
|
v-if="pinnedMessages.length === 1 && canManage"
|
||||||
|
v-loading="removingId === latest.id"
|
||||||
|
class="im-conversation-group-pinned__remove"
|
||||||
|
@click.stop="handleRemove(latest)"
|
||||||
|
>
|
||||||
|
移除
|
||||||
|
</span>
|
||||||
|
<template v-else-if="pinnedMessages.length > 1">
|
||||||
|
<span class="im-conversation-group-pinned__count">共 {{ pinnedMessages.length }} 条</span>
|
||||||
|
<Icon
|
||||||
|
:icon="expanded ? 'ant-design:up-outlined' : 'ant-design:down-outlined'"
|
||||||
|
:size="11"
|
||||||
|
class="im-conversation-group-pinned__chevron"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 多条展开:浅色面板包裹完整列表,每条独立胶囊;点击跳转到对应消息位置 -->
|
||||||
|
<div v-if="pinnedMessages.length > 1 && expanded" class="im-conversation-group-pinned__list">
|
||||||
|
<div
|
||||||
|
v-for="msg in pinnedMessages"
|
||||||
|
:key="msg.id"
|
||||||
|
class="im-conversation-group-pinned__row im-conversation-group-pinned__row--list im-conversation-group-pinned__row--clickable"
|
||||||
|
@click="handleLocate(msg)"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:pushpin-outlined" :size="14" class="im-conversation-group-pinned__icon" />
|
||||||
|
<span class="im-conversation-group-pinned__sender">{{ getSenderName(msg) }}:</span>
|
||||||
|
<span class="im-conversation-group-pinned__text">{{ getPreview(msg) }}</span>
|
||||||
|
<span
|
||||||
|
v-if="canManage"
|
||||||
|
v-loading="removingId === msg.id"
|
||||||
|
class="im-conversation-group-pinned__remove"
|
||||||
|
@click.stop="handleRemove(msg)"
|
||||||
|
>
|
||||||
|
移除
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import Icon from '@/components/Icon/src/Icon.vue'
|
||||||
|
import { useMessage } from '@/hooks/web/useMessage'
|
||||||
|
import { ImConversationType, ImGroupMemberRole } from '@/views/im/utils/constants'
|
||||||
|
import { unpinGroupMessage as apiUnpinGroupMessage } from '@/api/im/group'
|
||||||
|
import { getSenderDisplayName } from '@/views/im/utils/user'
|
||||||
|
import { resolveConversationLastContent } from '@/views/im/utils/conversation'
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import { useGroupStore } from '../../../../store/groupStore'
|
||||||
|
import type { Message } from '../../../../types'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImConversationGroupPinned' })
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 当前群编号(自行从 groupStore 拿完整 Group,跟随响应式) */
|
||||||
|
groupId: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
/** 点击置顶消息 → 父级 MessagePanel 滚动定位到原消息位置 */
|
||||||
|
locate: [messageId: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const groupStore = useGroupStore()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
/** 当前群(含 pinnedMessages) */
|
||||||
|
const group = computed(() => groupStore.getGroup(props.groupId))
|
||||||
|
|
||||||
|
const expanded = ref(false)
|
||||||
|
const removingId = ref<number | null>(null)
|
||||||
|
|
||||||
|
/** 当前群置顶消息列表(直接走 group.value,跟随响应式) */
|
||||||
|
const pinnedMessages = computed<Message[]>(() => group.value?.pinnedMessages ?? [])
|
||||||
|
|
||||||
|
/** 顶部胶囊展示的最新一条(即列表最后一条,pin 顺序追加) */
|
||||||
|
const latest = computed(() => pinnedMessages.value[pinnedMessages.value.length - 1])
|
||||||
|
|
||||||
|
/** 当前用户是否群主 / 管理员(决定是否显示「移除」入口) */
|
||||||
|
const canManage = computed(() => {
|
||||||
|
const myId = Number(userStore.getUser?.id) || 0
|
||||||
|
const role = group.value?.members?.find((m) => m.userId === myId)?.role
|
||||||
|
return role === ImGroupMemberRole.OWNER || role === ImGroupMemberRole.ADMIN
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 顶部胶囊点击:单条直接跳转原消息位置;多条切换展开 / 折叠 */
|
||||||
|
function handleTopClick() {
|
||||||
|
if (pinnedMessages.value.length === 1) {
|
||||||
|
handleLocate(latest.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expanded.value = !expanded.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点击置顶消息行 → 触发跳转 + 收起弹出层 */
|
||||||
|
function handleLocate(msg: Message) {
|
||||||
|
emit('locate', msg.id)
|
||||||
|
expanded.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 置顶消息发送人显示名 */
|
||||||
|
function getSenderName(msg: Message): string {
|
||||||
|
return group.value ? getSenderDisplayName(msg.senderId, ImConversationType.GROUP, group.value.id) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 置顶消息预览文本:复用会话最后一条摘要逻辑([图片] / [文件] / 文本等) */
|
||||||
|
function getPreview(msg: Message): string {
|
||||||
|
return group.value ? resolveConversationLastContent(msg, ImConversationType.GROUP, group.value.id) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移除置顶:调后端 API,loading 期间禁止重复点;后端广播 GROUP_MESSAGE_UNPIN 由 dispatcher 自动同步本地 */
|
||||||
|
async function handleRemove(msg: Message) {
|
||||||
|
if (!group.value || removingId.value !== null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
removingId.value = msg.id
|
||||||
|
try {
|
||||||
|
await apiUnpinGroupMessage({ groupId: group.value.id, messageId: msg.id })
|
||||||
|
message.success('已取消置顶')
|
||||||
|
} finally {
|
||||||
|
removingId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 容器:左对齐悬浮在 header 下方;不占整行;relative 让展开列表绝对定位贴顶部胶囊下方 */
|
||||||
|
.im-conversation-group-pinned {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 6px 16px 8px;
|
||||||
|
background-color: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表面板:绝对定位悬浮在顶部胶囊正下方;白色背景 + 强阴影跟 header 浅灰对比,呈现卡片层级感
|
||||||
|
margin-top: -1px 让弹出层往上盖住 header bottom 那 1px 分隔线,避免视觉上有横向空隙 */
|
||||||
|
.im-conversation-group-pinned__list {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
margin-top: -1px;
|
||||||
|
left: 6px;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
width: 380px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
/* 弹出层箭头:朝上的三角,颜色跟弹出层 background 一致(白色),跟 header 浅灰强对比 */
|
||||||
|
.im-conversation-group-pinned__list::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
left: 184px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 8px solid transparent;
|
||||||
|
border-right: 8px solid transparent;
|
||||||
|
border-bottom: 8px solid var(--el-bg-color);
|
||||||
|
filter: drop-shadow(0 -2px 1px rgba(0, 0, 0, 0.04));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 胶囊基础样式:顶部固定 width;弹出层里的胶囊撑满外框宽度 */
|
||||||
|
.im-conversation-group-pinned__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: 360px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
.im-conversation-group-pinned__row--clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.im-conversation-group-pinned__row--clickable:hover {
|
||||||
|
background-color: var(--el-fill-color-lighter);
|
||||||
|
}
|
||||||
|
/* 列表里的胶囊:撑满弹出层宽度;浅灰背景跟弹出层白色区分,hover 反相白色 */
|
||||||
|
.im-conversation-group-pinned__row--list {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--el-fill-color-light);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.im-conversation-group-pinned__row--list:hover {
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.im-conversation-group-pinned__icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--el-color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.im-conversation-group-pinned__sender {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
.im-conversation-group-pinned__text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.im-conversation-group-pinned__count {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.im-conversation-group-pinned__chevron {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.im-conversation-group-pinned__remove {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.im-conversation-group-pinned__remove:hover {
|
||||||
|
color: var(--el-color-primary-light-3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -228,8 +228,11 @@ import {
|
||||||
ImMessageStatus,
|
ImMessageStatus,
|
||||||
ImGroupReceiptStatus,
|
ImGroupReceiptStatus,
|
||||||
ImConversationType,
|
ImConversationType,
|
||||||
isGroupNotification
|
ImGroupMemberRole,
|
||||||
} from '../../../../../utils/constants'
|
isGroupNotification,
|
||||||
|
isNormalMessage
|
||||||
|
} from '@/views/im/utils/constants'
|
||||||
|
import { pinGroupMessage as apiPinGroupMessage } from '@/api/im/group'
|
||||||
import {
|
import {
|
||||||
buildQuoteFromMessage,
|
buildQuoteFromMessage,
|
||||||
getQuoteFromMessage,
|
getQuoteFromMessage,
|
||||||
|
|
@ -241,7 +244,7 @@ import {
|
||||||
type FileMessage,
|
type FileMessage,
|
||||||
type AudioMessage,
|
type AudioMessage,
|
||||||
type VideoMessage
|
type VideoMessage
|
||||||
} from '../../../../../utils/message'
|
} from '@/views/im/utils/message'
|
||||||
import { buildRecallTip } from '../../../../../utils/conversation'
|
import { buildRecallTip } from '../../../../../utils/conversation'
|
||||||
import { formatSeconds } from '@/utils/formatTime'
|
import { formatSeconds } from '@/utils/formatTime'
|
||||||
import { formatFileSize } from '@/utils/file'
|
import { formatFileSize } from '@/utils/file'
|
||||||
|
|
@ -255,7 +258,7 @@ import {
|
||||||
getSenderDisplayName,
|
getSenderDisplayName,
|
||||||
getSenderRealNickname,
|
getSenderRealNickname,
|
||||||
resolveGroupNotificationText
|
resolveGroupNotificationText
|
||||||
} from '../../../../../utils/user'
|
} from '@/views/im/utils/user'
|
||||||
import { useImUiStore } from '../../../../store/uiStore'
|
import { useImUiStore } from '../../../../store/uiStore'
|
||||||
import { useMessageSender } from '../../../../composables/useMessageSender'
|
import { useMessageSender } from '../../../../composables/useMessageSender'
|
||||||
import type { Message } from '../../../../types'
|
import type { Message } from '../../../../types'
|
||||||
|
|
@ -264,6 +267,8 @@ import ReplyPreview from './ReplyPreview.vue'
|
||||||
import UserAvatar from '../../../../components/user/UserAvatar.vue'
|
import UserAvatar from '../../../../components/user/UserAvatar.vue'
|
||||||
import type { GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
import type { GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
||||||
|
|
||||||
|
// TODO @AI:参考 /Users/yunai/Java/yudao-all-in-im/yudao-ui-admin-vue3/src/views/im/home/pages/conversation/components/conversation/ConversationGroupSide.vue 做下分块?
|
||||||
|
|
||||||
defineOptions({ name: 'ImMessageItem' })
|
defineOptions({ name: 'ImMessageItem' })
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -283,7 +288,7 @@ const draftStore = useDraftStore()
|
||||||
const uiStore = useImUiStore()
|
const uiStore = useImUiStore()
|
||||||
const { recall, sendRaw } = useMessageSender()
|
const { recall, sendRaw } = useMessageSender()
|
||||||
// 仅用 confirm,避免 message 跟 props.message 同名冲突(vue/no-dupe-keys)
|
// 仅用 confirm,避免 message 跟 props.message 同名冲突(vue/no-dupe-keys)
|
||||||
const { confirm: confirmDialog } = useMessage()
|
const { confirm: confirmDialog, success: successMessage } = useMessage()
|
||||||
|
|
||||||
/** 是否已撤回:pull / WS 两路都会调 recallMessage 把原消息更新为 type=RECALL,渲染只需识别 type */
|
/** 是否已撤回:pull / WS 两路都会调 recallMessage 把原消息更新为 type=RECALL,渲染只需识别 type */
|
||||||
const isRecall = computed(() => props.message.type === ImMessageType.RECALL)
|
const isRecall = computed(() => props.message.type === ImMessageType.RECALL)
|
||||||
|
|
@ -516,6 +521,7 @@ const isAtMe = computed(() => {
|
||||||
/** 右键菜单 key 常量;push 端和分发端从同一处取,typo 编译期就能抓 */
|
/** 右键菜单 key 常量;push 端和分发端从同一处取,typo 编译期就能抓 */
|
||||||
const MENU_KEYS = {
|
const MENU_KEYS = {
|
||||||
REPLY: 'REPLY',
|
REPLY: 'REPLY',
|
||||||
|
PIN: 'PIN',
|
||||||
RECALL: 'RECALL',
|
RECALL: 'RECALL',
|
||||||
DELETE: 'DELETE'
|
DELETE: 'DELETE'
|
||||||
} as const
|
} as const
|
||||||
|
|
@ -552,6 +558,15 @@ async function handleContextMenu(e: MouseEvent) {
|
||||||
icon: 'bxs:quote-alt-left'
|
icon: 'bxs:quote-alt-left'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 「置顶」:仅群聊 + 普通消息 + 已落库 + 未撤回 + 群主或管理员;已置顶不再展示,由置顶面板的「移除」入口承接
|
||||||
|
// 不本地预判上限,让后端校验,超限时通过 error toast 反馈
|
||||||
|
if (canPin.value) {
|
||||||
|
items.push({
|
||||||
|
key: MENU_KEYS.PIN,
|
||||||
|
name: '置顶',
|
||||||
|
icon: 'ant-design:pushpin-outlined'
|
||||||
|
})
|
||||||
|
}
|
||||||
// 「撤回 / 删除」二选一:
|
// 「撤回 / 删除」二选一:
|
||||||
// - 自己发送 + 已落库(id≠0)+ 未撤回 + 在撤回窗口内 → 撤回(推服务器把消息态置 RECALL)
|
// - 自己发送 + 已落库(id≠0)+ 未撤回 + 在撤回窗口内 → 撤回(推服务器把消息态置 RECALL)
|
||||||
// - 其它(对方消息 / 已撤回 / 超出撤回窗口)→ 删除(仅本地清,不动后端)
|
// - 其它(对方消息 / 已撤回 / 超出撤回窗口)→ 删除(仅本地清,不动后端)
|
||||||
|
|
@ -583,6 +598,8 @@ async function handleContextMenu(e: MouseEvent) {
|
||||||
uiStore.openContextMenu({ x: e.clientX, y: e.clientY }, items, async (item) => {
|
uiStore.openContextMenu({ x: e.clientX, y: e.clientY }, items, async (item) => {
|
||||||
if (item.key === MENU_KEYS.REPLY) {
|
if (item.key === MENU_KEYS.REPLY) {
|
||||||
handleReply()
|
handleReply()
|
||||||
|
} else if (item.key === MENU_KEYS.PIN) {
|
||||||
|
await handlePin()
|
||||||
} else if (item.key === MENU_KEYS.RECALL) {
|
} else if (item.key === MENU_KEYS.RECALL) {
|
||||||
await handleRecall()
|
await handleRecall()
|
||||||
} else if (item.key === MENU_KEYS.DELETE) {
|
} else if (item.key === MENU_KEYS.DELETE) {
|
||||||
|
|
@ -591,6 +608,45 @@ async function handleContextMenu(e: MouseEvent) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 当前激活会话对应的群(私聊场景为 undefined) */
|
||||||
|
const currentGroup = computed(() => {
|
||||||
|
const conversation = conversationStore.activeConversation
|
||||||
|
if (!conversation || conversation.type !== ImConversationType.GROUP) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return groupStore.getGroup(conversation.targetId)
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 当前用户在该群里的角色;私聊或非群成员 → undefined */
|
||||||
|
const myGroupRole = computed(() => {
|
||||||
|
const myId = Number(userStore.getUser?.id) || 0
|
||||||
|
return currentGroup.value?.members?.find((m) => m.userId === myId)?.role
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 是否允许置顶(已置顶消息不再展示菜单项,由置顶面板的「移除」入口承接):群聊 + 普通消息 + 已落库 + 未撤回 + 群主或管理员 + 未置顶 */
|
||||||
|
const canPin = computed(
|
||||||
|
() =>
|
||||||
|
!!currentGroup.value &&
|
||||||
|
isNormalMessage(props.message.type) &&
|
||||||
|
!!props.message.id &&
|
||||||
|
!isRecall.value &&
|
||||||
|
(myGroupRole.value === ImGroupMemberRole.OWNER || myGroupRole.value === ImGroupMemberRole.ADMIN) &&
|
||||||
|
!currentGroup.value.pinnedMessages?.some((m) => m.id === props.message.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 置顶消息:二次确认 → 调后端 pin-message;后端广播 GROUP_MESSAGE_PIN,本端 dispatcher 拉最新 pinnedMessages */
|
||||||
|
async function handlePin() {
|
||||||
|
const group = currentGroup.value
|
||||||
|
if (!group) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await confirmDialog('将在当前群成员的聊天中置顶', '置顶消息', { confirmButtonText: '置顶' })
|
||||||
|
await apiPinGroupMessage({ groupId: group.id, messageId: props.message.id })
|
||||||
|
successMessage('已置顶')
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
/** 进入引用模式:把当前消息构造成 QuoteMessage 写入 draftStore,MessageInput 顶部引用条响应式出现 */
|
/** 进入引用模式:把当前消息构造成 QuoteMessage 写入 draftStore,MessageInput 顶部引用条响应式出现 */
|
||||||
function handleReply() {
|
function handleReply() {
|
||||||
const conversation = conversationStore.activeConversation
|
const conversation = conversationStore.activeConversation
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,68 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-1 flex-col min-w-0 bg-[var(--el-fill-color-light)]">
|
<div class="flex flex-1 flex-col min-w-0 bg-[var(--el-fill-color-light)]">
|
||||||
<template v-if="conversationStore.activeConversation">
|
<template v-if="conversationStore.activeConversation">
|
||||||
<!-- 顶部:会话名(群聊带人数)+ 右侧功能图标;border 走 scoped CSS(项目 UnoCSS 不带 border-style preflight) -->
|
<!-- 顶部 header:第一行群名 + 右侧图标,第二行嵌入置顶气泡(仅群聊 + 有置顶);border 走 scoped CSS -->
|
||||||
<div
|
<div class="message-panel__header flex flex-col bg-[var(--el-fill-color-light)]">
|
||||||
class="message-panel__header flex items-center justify-between h-14 px-5 bg-[var(--el-fill-color-light)]"
|
<div class="flex items-center justify-between h-14 px-5">
|
||||||
>
|
<span class="flex flex-col min-w-0">
|
||||||
<span class="flex flex-col min-w-0">
|
<span class="flex items-baseline gap-1.5 min-w-0">
|
||||||
<span class="flex items-baseline gap-1.5 min-w-0">
|
<span
|
||||||
<span
|
class="overflow-hidden text-base font-medium truncate text-[var(--el-text-color-primary)]"
|
||||||
class="overflow-hidden text-base font-medium truncate text-[var(--el-text-color-primary)]"
|
>
|
||||||
>
|
{{ conversationStore.activeConversation?.name || '' }}
|
||||||
{{ conversationStore.activeConversation?.name || '' }}
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="isGroup && headerMemberCount > 0"
|
||||||
|
class="flex-shrink-0 text-sm text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
|
({{ headerMemberCount }})
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
<!-- 副标题:备注 ≠ 群名时展示原群名,提示用户当前看到的主名是自己设的备注 -->
|
||||||
<span
|
<span
|
||||||
v-if="isGroup && headerMemberCount > 0"
|
v-if="headerSubtitle"
|
||||||
class="flex-shrink-0 text-sm text-[var(--el-text-color-secondary)]"
|
class="overflow-hidden text-xs truncate text-[var(--el-text-color-secondary)]"
|
||||||
>
|
>
|
||||||
({{ headerMemberCount }})
|
{{ headerSubtitle }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<!-- 副标题:备注 ≠ 群名时展示原群名,提示用户当前看到的主名是自己设的备注 -->
|
<div class="flex gap-3 items-center">
|
||||||
<span
|
<!-- 聊天历史 -->
|
||||||
v-if="headerSubtitle"
|
<el-tooltip content="聊天历史" placement="bottom">
|
||||||
class="overflow-hidden text-xs truncate text-[var(--el-text-color-secondary)]"
|
<Icon
|
||||||
>
|
icon="ep:chat-dot-round"
|
||||||
{{ headerSubtitle }}
|
:size="20"
|
||||||
</span>
|
class="message-panel__header-icon cursor-pointer"
|
||||||
</span>
|
@click="historyVisible = true"
|
||||||
<div class="flex gap-3 items-center">
|
/>
|
||||||
<!-- 聊天历史 -->
|
</el-tooltip>
|
||||||
<el-tooltip content="聊天历史" placement="bottom">
|
<!-- 通话入口:暂未开放,先放占位图标对齐微信 PC -->
|
||||||
<Icon
|
<el-tooltip content="通话" placement="bottom">
|
||||||
icon="ep:chat-dot-round"
|
<Icon
|
||||||
:size="20"
|
icon="ant-design:phone-outlined"
|
||||||
class="message-panel__header-icon cursor-pointer"
|
:size="20"
|
||||||
@click="historyVisible = true"
|
class="message-panel__header-icon cursor-pointer"
|
||||||
/>
|
@click="handleCall"
|
||||||
</el-tooltip>
|
/>
|
||||||
<!-- 通话入口:暂未开放,先放占位图标对齐微信 PC -->
|
</el-tooltip>
|
||||||
<el-tooltip content="通话" placement="bottom">
|
<!-- 信息抽屉入口 -->
|
||||||
<Icon
|
<el-tooltip :content="isGroup ? '群聊信息' : '聊天信息'" placement="bottom">
|
||||||
icon="ant-design:phone-outlined"
|
<Icon
|
||||||
:size="20"
|
icon="ant-design:ellipsis-outlined"
|
||||||
class="message-panel__header-icon cursor-pointer"
|
:size="20"
|
||||||
@click="handleCall"
|
class="message-panel__header-icon cursor-pointer"
|
||||||
/>
|
@click="toggleSide"
|
||||||
</el-tooltip>
|
/>
|
||||||
<!-- 信息抽屉入口 -->
|
</el-tooltip>
|
||||||
<el-tooltip :content="isGroup ? '群聊信息' : '聊天信息'" placement="bottom">
|
</div>
|
||||||
<Icon
|
|
||||||
icon="ant-design:ellipsis-outlined"
|
|
||||||
:size="20"
|
|
||||||
class="message-panel__header-icon cursor-pointer"
|
|
||||||
@click="toggleSide"
|
|
||||||
/>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 群置顶消息:第二行嵌入 header;仅群聊 + 有置顶时显示 -->
|
||||||
|
<ConversationGroupPinned
|
||||||
|
v-if="isGroup && conversationStore.activeConversation"
|
||||||
|
:group-id="conversationStore.activeConversation.targetId"
|
||||||
|
@locate="handleLocate"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 中间:消息列表 -->
|
<!-- 中间:消息列表 -->
|
||||||
|
|
@ -147,6 +154,7 @@ import MessageItem from './MessageItem.vue'
|
||||||
import MessageInput from '../input/MessageInput.vue'
|
import MessageInput from '../input/MessageInput.vue'
|
||||||
import MessageHistory from './MessageHistory.vue'
|
import MessageHistory from './MessageHistory.vue'
|
||||||
import ConversationGroupSide from '../conversation/ConversationGroupSide.vue'
|
import ConversationGroupSide from '../conversation/ConversationGroupSide.vue'
|
||||||
|
import ConversationGroupPinned from './ConversationGroupPinned.vue'
|
||||||
import ConversationPrivateSide from '../conversation/ConversationPrivateSide.vue'
|
import ConversationPrivateSide from '../conversation/ConversationPrivateSide.vue'
|
||||||
import type { FriendLite, GroupLite } from '../../../../types'
|
import type { FriendLite, GroupLite } from '../../../../types'
|
||||||
import type { GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
import type { GroupMemberLite } from '../../../../components/group/GroupMember.vue'
|
||||||
|
|
@ -478,6 +486,7 @@ watch(
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 顶部分隔线:UnoCSS 不带 border-style preflight,class 写法只设色 / 宽不出线,走 scoped 显式 shorthand */
|
/* 顶部分隔线:UnoCSS 不带 border-style preflight,class 写法只设色 / 宽不出线,走 scoped 显式 shorthand */
|
||||||
.message-panel__header {
|
.message-panel__header {
|
||||||
|
flex-shrink: 0;
|
||||||
border-bottom: 1px solid var(--el-border-color-light);
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import {
|
||||||
StorageKeys
|
StorageKeys
|
||||||
} from '../../utils/storage'
|
} from '../../utils/storage'
|
||||||
import { getGroupDisplayName, type GroupNotificationPayload } from '../../utils/user'
|
import { getGroupDisplayName, type GroupNotificationPayload } from '../../utils/user'
|
||||||
import type { Group, GroupMember } from '../types'
|
import type { Group, GroupMember, Message } from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* fetchGroupMembers 并发去重表:同 groupId 同时进的请求共用一个 Promise
|
* fetchGroupMembers 并发去重表:同 groupId 同时进的请求共用一个 Promise
|
||||||
|
|
@ -543,6 +543,12 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
case ImMessageType.GROUP_OWNER_TRANSFER:
|
case ImMessageType.GROUP_OWNER_TRANSFER:
|
||||||
this.applyGroupOwnerTransferNotification(groupId, payload)
|
this.applyGroupOwnerTransferNotification(groupId, payload)
|
||||||
break
|
break
|
||||||
|
case ImMessageType.GROUP_MESSAGE_PIN:
|
||||||
|
this.applyGroupMessagePinNotification(groupId, payload)
|
||||||
|
break
|
||||||
|
case ImMessageType.GROUP_MESSAGE_UNPIN:
|
||||||
|
this.applyGroupMessageUnpinNotification(groupId, payload)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -629,6 +635,58 @@ export const useGroupStore = defineStore('imGroupStore', {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 群消息置顶:从 payload 直接拿完整消息对象 push 到 pinnedMessages */
|
||||||
|
applyGroupMessagePinNotification(groupId: number, payload: GroupNotificationPayload) {
|
||||||
|
if (!payload.message) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const group = this.getGroup(groupId)
|
||||||
|
if (!group) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO @AI:可以直接使用 payload,简化这样设置。。。
|
||||||
|
const message: Message = {
|
||||||
|
id: payload.message.id,
|
||||||
|
clientMessageId: payload.message.clientMessageId || '',
|
||||||
|
type: payload.message.type,
|
||||||
|
content: payload.message.content,
|
||||||
|
status: payload.message.status,
|
||||||
|
sendTime: new Date(payload.message.sendTime).getTime(),
|
||||||
|
senderId: payload.message.senderId,
|
||||||
|
targetId: payload.message.groupId,
|
||||||
|
selfSend: payload.message.senderId === getCurrentUserId(),
|
||||||
|
atUserIds: payload.message.atUserIds || [],
|
||||||
|
receiverUserIds: payload.message.receiverUserIds || [],
|
||||||
|
receiptStatus: payload.message.receiptStatus,
|
||||||
|
readCount: payload.message.readCount
|
||||||
|
}
|
||||||
|
// 幂等:已存在同 messageId 不重复 push
|
||||||
|
const existing = group.pinnedMessages || []
|
||||||
|
if (existing.some((m) => m.id === message.id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
group.pinnedMessages = [...existing, message]
|
||||||
|
this.saveGroups()
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 群消息取消置顶:按 messageId 从本地置顶列表中移除 */
|
||||||
|
applyGroupMessageUnpinNotification(groupId: number, payload: GroupNotificationPayload) {
|
||||||
|
if (!payload.messageId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const group = this.getGroup(groupId)
|
||||||
|
if (!group?.pinnedMessages?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO @AI:不要用 next 这样的单词,大家不好理解。可以用 newXXXX 这样。其它地方也看看。
|
||||||
|
const next = group.pinnedMessages.filter((m) => m.id !== payload.messageId)
|
||||||
|
if (next.length === group.pinnedMessages.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
group.pinnedMessages = next
|
||||||
|
this.saveGroups()
|
||||||
|
},
|
||||||
|
|
||||||
/** 切账号时仅清 in-memory,IDB 按 userId 分桶天然隔离,回切秒开 */
|
/** 切账号时仅清 in-memory,IDB 按 userId 分桶天然隔离,回切秒开 */
|
||||||
clear() {
|
clear() {
|
||||||
this.groups = []
|
this.groups = []
|
||||||
|
|
@ -646,7 +704,28 @@ function convertGroup(vo: ImGroupRespVO): Group {
|
||||||
name: vo.name,
|
name: vo.name,
|
||||||
avatar: vo.avatar,
|
avatar: vo.avatar,
|
||||||
notice: vo.notice,
|
notice: vo.notice,
|
||||||
ownerUserId: vo.ownerUserId
|
ownerUserId: vo.ownerUserId,
|
||||||
|
pinnedMessages: vo.pinnedMessages?.map(convertGroupMessageVO)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 后端 ImGroupMessageRespVO -> 前端 Message:补 targetId / selfSend / sendTime 等派生字段 */
|
||||||
|
function convertGroupMessageVO(message: NonNullable<ImGroupRespVO['pinnedMessages']>[number]): Message {
|
||||||
|
const currentUserId = getCurrentUserId()
|
||||||
|
return {
|
||||||
|
id: message.id,
|
||||||
|
clientMessageId: message.clientMessageId || '',
|
||||||
|
type: message.type,
|
||||||
|
content: message.content,
|
||||||
|
status: message.status,
|
||||||
|
sendTime: new Date(message.sendTime).getTime(),
|
||||||
|
senderId: message.senderId,
|
||||||
|
targetId: message.groupId,
|
||||||
|
selfSend: !!currentUserId && message.senderId === currentUserId,
|
||||||
|
atUserIds: message.atUserIds || [],
|
||||||
|
receiverUserIds: message.receiverUserIds || [],
|
||||||
|
receiptStatus: message.receiptStatus,
|
||||||
|
readCount: message.readCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ export interface Group {
|
||||||
avatar?: string // 群头像
|
avatar?: string // 群头像
|
||||||
notice?: string // 群公告
|
notice?: string // 群公告
|
||||||
ownerUserId?: number // 群主用户编号
|
ownerUserId?: number // 群主用户编号
|
||||||
|
pinnedMessages?: Message[] // 群置顶消息列表
|
||||||
|
|
||||||
// ========== 前端扩展字段(user-per-group 维度) ==========
|
// ========== 前端扩展字段(user-per-group 维度) ==========
|
||||||
muted?: boolean // 是否免打扰。从当前用户的 GroupMember 回填
|
muted?: boolean // 是否免打扰。从当前用户的 GroupMember 回填
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,18 @@ export const ImMessageType = {
|
||||||
GROUP_ADMIN_REMOVE: 1518, // 撤销管理员
|
GROUP_ADMIN_REMOVE: 1518, // 撤销管理员
|
||||||
GROUP_NOTICE_UPDATE: 1519, // 群公告变更
|
GROUP_NOTICE_UPDATE: 1519, // 群公告变更
|
||||||
GROUP_NAME_UPDATE: 1520, // 群名变更
|
GROUP_NAME_UPDATE: 1520, // 群名变更
|
||||||
GROUP_MEMBER_SETTING_UPDATE: 1530 // 群成员个人设置变更:muted / groupRemark 个人多端同步
|
GROUP_MEMBER_SETTING_UPDATE: 1530, // 群成员个人设置变更:muted / groupRemark 个人多端同步
|
||||||
|
GROUP_MESSAGE_PIN: 1531, // 群消息置顶
|
||||||
|
GROUP_MESSAGE_UNPIN: 1532 // 群消息取消置顶
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/** 判断是否「群广播事件」:[GROUP_CREATE, GROUP_MEMBER_SETTING_UPDATE) 段位都算,GROUP_MEMBER_SETTING_UPDATE 是个人信号不算 */
|
/** 判断是否「群广播事件」:[GROUP_CREATE, GROUP_MESSAGE_UNPIN] 段位都算,仅 GROUP_MEMBER_SETTING_UPDATE 是个人信号排除 */
|
||||||
export function isGroupNotification(type: number): boolean {
|
export function isGroupNotification(type: number): boolean {
|
||||||
return type >= ImMessageType.GROUP_CREATE && type < ImMessageType.GROUP_MEMBER_SETTING_UPDATE
|
return (
|
||||||
|
type >= ImMessageType.GROUP_CREATE
|
||||||
|
&& type <= ImMessageType.GROUP_MESSAGE_UNPIN
|
||||||
|
&& type !== ImMessageType.GROUP_MEMBER_SETTING_UPDATE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** IM 普通消息类型集合(聊天气泡中显示,并作为会话最后一条摘要) */
|
/** IM 普通消息类型集合(聊天气泡中显示,并作为会话最后一条摘要) */
|
||||||
|
|
@ -98,6 +104,9 @@ export const ImGroupMemberRole = {
|
||||||
/** 群管理员人数上限(对齐后端 GROUP_ADMIN_MAX_COUNT) */
|
/** 群管理员人数上限(对齐后端 GROUP_ADMIN_MAX_COUNT) */
|
||||||
export const GROUP_ADMIN_MAX_COUNT = 3
|
export const GROUP_ADMIN_MAX_COUNT = 3
|
||||||
|
|
||||||
|
/** 群置顶消息条数上限(对齐后端 GROUP_PIN_MAX_COUNT) */
|
||||||
|
export const GROUP_PIN_MAX_COUNT = 5
|
||||||
|
|
||||||
/** 每次拉取私聊消息的最大条数(后端上限 1000,前端取保守值 100) */
|
/** 每次拉取私聊消息的最大条数(后端上限 1000,前端取保守值 100) */
|
||||||
export const PRIVATE_MESSAGE_PULL_SIZE = 100
|
export const PRIVATE_MESSAGE_PULL_SIZE = 100
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,22 @@ export type GroupNotificationPayload = {
|
||||||
oldAvatar?: string
|
oldAvatar?: string
|
||||||
newAvatar?: string
|
newAvatar?: string
|
||||||
displayUserName?: string
|
displayUserName?: string
|
||||||
|
messageId?: number
|
||||||
|
/** PIN 事件携带的完整被置顶消息对象(前端直接 push 进 group.pinnedMessages,避免回查群详情) */
|
||||||
|
message?: {
|
||||||
|
id: number
|
||||||
|
clientMessageId?: string
|
||||||
|
senderId: number
|
||||||
|
groupId: number
|
||||||
|
type: number
|
||||||
|
content: string
|
||||||
|
status: number
|
||||||
|
sendTime: string
|
||||||
|
atUserIds?: number[]
|
||||||
|
receiverUserIds?: number[]
|
||||||
|
receiptStatus?: number
|
||||||
|
readCount?: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveGroupNotificationText(
|
export function resolveGroupNotificationText(
|
||||||
|
|
@ -216,6 +232,10 @@ export function resolveGroupNotificationText(
|
||||||
return `${operatorName} 撤销了 ${memberNames} 的管理员身份`
|
return `${operatorName} 撤销了 ${memberNames} 的管理员身份`
|
||||||
case ImMessageType.GROUP_OWNER_TRANSFER:
|
case ImMessageType.GROUP_OWNER_TRANSFER:
|
||||||
return `${operatorName} 已将群主转让给 ${newOwnerName}`
|
return `${operatorName} 已将群主转让给 ${newOwnerName}`
|
||||||
|
case ImMessageType.GROUP_MESSAGE_PIN:
|
||||||
|
return `${operatorName} 置顶了一条消息`
|
||||||
|
case ImMessageType.GROUP_MESSAGE_UNPIN:
|
||||||
|
return `${operatorName} 取消了一条置顶消息`
|
||||||
default:
|
default:
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue