✨ feat(im): 修 L-13/L-16:PagedScroller 加 itemKey 防索引乱位、私聊 Message.targetId 改对端 userId、抽 getPrivateMessagePeerId 收敛 4 处 peer 计算
parent
1015423431
commit
7a236b4378
|
|
@ -5,7 +5,12 @@
|
|||
- 通过 slot 暴露每一项,让调用方自己决定渲染
|
||||
-->
|
||||
<el-scrollbar ref="scrollbarRef" class="w-full h-full">
|
||||
<slot v-for="(item, idx) in displayItems" :item="item" :index="idx" :key="idx"></slot>
|
||||
<slot
|
||||
v-for="(item, idx) in displayItems"
|
||||
:item="item"
|
||||
:index="idx"
|
||||
:key="resolveItemKey(item, idx)"
|
||||
></slot>
|
||||
<div
|
||||
v-if="showFooter"
|
||||
class="py-3 text-xs text-center text-[var(--el-text-color-secondary)]"
|
||||
|
|
@ -26,6 +31,7 @@ const props = withDefaults(
|
|||
items: T[] // 全量数据
|
||||
pageSize?: number // 每页渲染条数
|
||||
threshold?: number // 距底多少 px 触发下一页
|
||||
itemKey?: string // 业务 id 字段名(如 'userId' / 'id');不传 / 字段值非 string|number 时回退 idx
|
||||
}>(),
|
||||
{
|
||||
pageSize: 30,
|
||||
|
|
@ -33,6 +39,15 @@ const props = withDefaults(
|
|||
}
|
||||
)
|
||||
|
||||
/** 解析每条 item 的 :key:caller 传 itemKey 则按字段取,无效 / 缺失回退索引,避免传错字段时全表 undefined key */
|
||||
function resolveItemKey(item: T, idx: number): string | number {
|
||||
if (!props.itemKey || item == null || typeof item !== 'object') {
|
||||
return idx
|
||||
}
|
||||
const value = (item as Record<string, unknown>)[props.itemKey]
|
||||
return typeof value === 'string' || typeof value === 'number' ? value : idx
|
||||
}
|
||||
|
||||
const scrollbarRef = useTemplateRef<InstanceType<typeof ElScrollbar>>('scrollbarRef')
|
||||
const page = ref(1)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
</div>
|
||||
|
||||
<div class="flex-1 min-h-0">
|
||||
<PagedScroller :items="shownMembers" :page-size="30">
|
||||
<PagedScroller :items="shownMembers" :page-size="30" item-key="userId">
|
||||
<template #default="{ item }">
|
||||
<GroupMemberItem
|
||||
:member="(item as GroupMemberLite)"
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
} from '../../utils/config'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { buildChannelConversationStub } from '../../utils/channel'
|
||||
import { getPrivateMessagePeerId } from '../../utils/message'
|
||||
import type { Message } from '../types'
|
||||
|
||||
/**
|
||||
|
|
@ -52,11 +53,11 @@ export const useMessagePuller = () => {
|
|||
const groupStore = useGroupStore()
|
||||
const currentUserId = Number(userStore.getUser?.id) || 0
|
||||
|
||||
/** 私聊会话归属:自己发的算"发给 receiverId 的会话",否则算"发送方的会话" */
|
||||
/** 私聊会话归属:自己发的算"发给 receiverId 的会话",否则算"发送方的会话";curry currentUserId 进闭包减少 3 处调用方的样板 */
|
||||
const getPrivatePeerId = (message: ImPrivateMessageRespVO) =>
|
||||
message.senderId === currentUserId ? message.receiverId : message.senderId
|
||||
getPrivateMessagePeerId(message, currentUserId)
|
||||
|
||||
/** 服务端私聊消息 -> 本地 Message */
|
||||
/** 服务端私聊消息 -> 本地 Message:targetId 是会话主键(对端 userId) */
|
||||
const convertPrivateMessage = (message: ImPrivateMessageRespVO): Message => {
|
||||
return {
|
||||
id: message.id,
|
||||
|
|
@ -66,7 +67,7 @@ export const useMessagePuller = () => {
|
|||
status: message.status,
|
||||
sendTime: new Date(message.sendTime).getTime(),
|
||||
senderId: message.senderId,
|
||||
targetId: message.receiverId,
|
||||
targetId: getPrivatePeerId(message),
|
||||
selfSend: message.senderId === currentUserId
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
<el-tabs v-model="activeTab" stretch>
|
||||
<el-tab-pane :label="`已读(${readMembers.length})`" name="read">
|
||||
<PagedScroller :items="readMembers" :page-size="20" class="h-75">
|
||||
<PagedScroller :items="readMembers" :page-size="20" item-key="userId" class="h-75">
|
||||
<template #default="{ item }">
|
||||
<GroupMember :member="item as GroupMemberLite" :height="40" :clickable="false" />
|
||||
</template>
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="`未读(${unreadMembers.length})`" name="unread">
|
||||
<PagedScroller :items="unreadMembers" :page-size="20" class="h-75">
|
||||
<PagedScroller :items="unreadMembers" :page-size="20" item-key="userId" class="h-75">
|
||||
<template #default="{ item }">
|
||||
<GroupMember :member="item as GroupMemberLite" :height="40" :clickable="false" />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ import {
|
|||
isGroupRequestNotification,
|
||||
isNormalMessage
|
||||
} from '../../utils/constants'
|
||||
import { playAudioTip, resolveCallEndReasonText } from '../../utils/message'
|
||||
import {
|
||||
getPrivateMessagePeerId,
|
||||
playAudioTip,
|
||||
resolveCallEndReasonText
|
||||
} from '../../utils/message'
|
||||
import { MESSAGE_PRIVATE_READ_ENABLED, MESSAGE_GROUP_READ_ENABLED } from '../../utils/config'
|
||||
import { useConversationStore } from './conversationStore'
|
||||
import { useFriendStore, type FriendNotificationPayload } from './friendStore'
|
||||
|
|
@ -54,7 +58,7 @@ const isFriendDeleteWithClear = (frame: ImPrivateMessageDTO): boolean => {
|
|||
}
|
||||
|
||||
/**
|
||||
* WebSocket 私聊 DTO -> 前端 Message
|
||||
* WebSocket 私聊 DTO -> 前端 Message;targetId 是会话主键(对端 userId)
|
||||
* 不写发送人名字段:渲染层走 utils/user 实时算(备注 / 群昵称变更后历史消息自动刷新)
|
||||
*/
|
||||
const convertPrivateMessage = (
|
||||
|
|
@ -68,7 +72,7 @@ const convertPrivateMessage = (
|
|||
status: websocketMessage.status,
|
||||
sendTime: new Date(websocketMessage.sendTime).getTime(),
|
||||
senderId: websocketMessage.senderId,
|
||||
targetId: websocketMessage.receiverId,
|
||||
targetId: getPrivateMessagePeerId(websocketMessage, currentUserId),
|
||||
selfSend: websocketMessage.senderId === currentUserId
|
||||
})
|
||||
|
||||
|
|
@ -442,7 +446,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
const friendStore = useFriendStore()
|
||||
const currentUserId = Number(userStore.getUser?.id) || 0
|
||||
const selfSend = websocketMessage.senderId === currentUserId
|
||||
const peerId = selfSend ? websocketMessage.receiverId : websocketMessage.senderId
|
||||
const peerId = getPrivateMessagePeerId(websocketMessage, currentUserId)
|
||||
// 未知对端(陌生人加好友前先收到消息等场景):异步补拉一次,下次再渲染就有 name/avatar
|
||||
const friend = friendStore.getFriend(peerId)
|
||||
if (!friend) {
|
||||
|
|
@ -640,7 +644,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
computeFriendPeerId(frame: ImPrivateMessageDTO): number {
|
||||
const userStore = useUserStore()
|
||||
const currentUserId = Number(userStore.getUser?.id) || 0
|
||||
return frame.senderId === currentUserId ? frame.receiverId : frame.senderId
|
||||
return getPrivateMessagePeerId(frame, currentUserId)
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export interface Message {
|
|||
// ========== 前端扩展字段 ==========
|
||||
// 发送人显示名一律渲染时实时算:utils/user.getSenderDisplayName / getSenderRealNickname
|
||||
// 不在 Message 上存任何名字快照,避免备注 / 群昵称变更后历史消息显示陈旧
|
||||
targetId: number // 会话目标编号(私聊=receiverId / 群聊=groupId),与 Conversation.targetId 一致
|
||||
targetId: number // 会话目标编号(私聊=对端 userId / 群聊=groupId),与 Conversation.targetId 一致
|
||||
selfSend: boolean // 是否自己发送(前端按 senderId 计算)
|
||||
uploadProgress?: number // 媒体消息上传进度(0-100);status=SENDING 期间持续更新;ack 后置 undefined
|
||||
// 媒体消息内存中保留的原始 File;下划线前缀表示不进 JSON / 不持久化(IDB 恢复后必为 undefined)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,21 @@ export const generateClientMessageId = (): string => {
|
|||
return generateUUID()
|
||||
}
|
||||
|
||||
// ==================== 私聊对端 userId ====================
|
||||
|
||||
/**
|
||||
* 私聊消息 / DTO 的对端 userId:自己发的对端是 receiver,别人发的对端是 sender
|
||||
*
|
||||
* 收口 4 处旧 inline(websocketStore.convertPrivateMessage / handlePrivateMessage / computeFriendPeerId
|
||||
* 和 useMessagePuller.getPrivatePeerId),结构类型只要 senderId / receiverId 两个字段,REST 与 WS DTO 都满足
|
||||
*/
|
||||
export function getPrivateMessagePeerId(
|
||||
message: { senderId: number; receiverId: number },
|
||||
currentUserId: number
|
||||
): number {
|
||||
return message.senderId === currentUserId ? message.receiverId : message.senderId
|
||||
}
|
||||
|
||||
// ==================== 文本片段(tip 文案 + TEXT 气泡共用) ====================
|
||||
// 既用于灰条 tip("XX 邀请 YY 加入群聊"),也用于 TEXT 气泡正文(@xxx 高亮 + URL 自动识别)。
|
||||
// mention 段携带 userId 用于挂点击弹 UserInfoCard;link 段携带 href 用于 <a> 跳转;
|
||||
|
|
|
|||
Loading…
Reference in New Issue