From 27cd042df9aca0383a49f3514bc21958dd890912 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 29 六月 2026 15:42:23 +0800
Subject: [PATCH] 银川 1.联调产品维护页面 2.添加IM即时通讯模块
---
src/views/im/home/pages/conversation/components/message/message-history.vue | 807 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 807 insertions(+), 0 deletions(-)
diff --git a/src/views/im/home/pages/conversation/components/message/message-history.vue b/src/views/im/home/pages/conversation/components/message/message-history.vue
new file mode 100644
index 0000000..a2c1312
--- /dev/null
+++ b/src/views/im/home/pages/conversation/components/message/message-history.vue
@@ -0,0 +1,807 @@
+<script lang="ts" setup>
+import type { Message } from '#/views/im/home/types'
+
+import { computed, inject, ref, watch } from 'vue'
+
+import { IconifyIcon as Icon } from '#/packages/icons/src'
+import { useUserStore } from '#/packages/stores/src'
+
+import { Button, Calendar, Input, Modal, Popover, Tag } from 'ant-design-vue'
+import dayjs from 'dayjs'
+
+import { getGroupMessageList as apiGetGroupMessageList } from '#/api/im/message/group'
+import { getPrivateMessageList as apiGetPrivateMessageList } from '#/api/im/message/private'
+import { useMessagePuller } from '#/views/im/home/composables/useMessagePuller'
+import { useVoicePlayer } from '#/views/im/home/composables/useVoicePlayer'
+import {
+ ImContentType,
+ ImConversationType,
+ isFriendChatTip,
+ isGroupNotification
+} from '#/views/im/utils/constants'
+import {
+ buildFacePreviewText,
+ buildRecallTip,
+ buildRecallTipSegments,
+ getConversationKey
+} from '#/views/im/utils/conversation'
+import { getClientConversationId } from '#/views/im/utils/db'
+import {
+ resolveFriendNotificationSegments,
+ resolveFriendNotificationText,
+ resolveGroupNotificationSegments
+} from '#/views/im/utils/message'
+import {
+ type CardMessage,
+ type FaceMessage,
+ type FileMessage,
+ getCardLabelInfo,
+ type MergeMessage,
+ parseMessage,
+ type TextMessage
+} from '#/views/im/utils/message'
+import { formatHistoryTime } from '#/views/im/utils/time'
+import {
+ getMemberDisplayName,
+ getSenderDisplayName,
+ getSenderRealNickname
+} from '#/views/im/utils/user'
+
+import { GroupMember, type GroupMemberLite } from '../../../../components/group'
+import { UserAvatar } from '../../../../components/user'
+import { useConversationStore } from '../../../../store/conversationStore'
+import { useFriendStore } from '../../../../store/friendStore'
+import { useGroupStore } from '../../../../store/groupStore'
+import { useMessageStore } from '../../../../store/messageStore'
+import { IM_MERGE_DETAIL_DIALOG_KEY } from './forward/keys'
+import MessageBubble from './message-bubble.vue'
+import TipSegments from './tip-segments.vue'
+
+defineOptions({ name: 'ImMessageHistory' })
+
+const emit = defineEmits<{
+ // 鍘嗗彶娑堟伅琛屼笂鐨�"瀹氫綅"鎸夐挳锛氶�氱煡鐖剁粍浠� MessagePanel 婊氬埌瀵瑰簲娑堟伅浣嶇疆 + 鍏虫帀鑷繁
+ locate: [messageId: number]
+}>()
+
+const userStore = useUserStore()
+const conversationStore = useConversationStore()
+const messageStore = useMessageStore()
+const groupStore = useGroupStore()
+const friendStore = useFriendStore()
+const openMergeDetail = inject(IM_MERGE_DETAIL_DIALOG_KEY)
+const voicePlayer = useVoicePlayer()
+const { convertPrivateMessage, convertGroupMessage } = useMessagePuller()
+
+const visible = ref(false)
+
+defineExpose({
+ /** 鎵撳紑鍘嗗彶娑堟伅鎶藉眽 */
+ open() {
+ visible.value = true
+ }
+})
+
+const conversation = computed(() => conversationStore.activeConversation)
+const isGroup = computed(() => conversation.value?.type === ImConversationType.GROUP)
+const allMessages = computed<Message[]>(() =>
+ conversation.value
+ ? messageStore.getMessages(
+ getClientConversationId(conversation.value.type, conversation.value.targetId)
+ )
+ : []
+)
+
+/** 鍗曟潯娑堟伅鐨勫彂閫佷汉鏄剧ず鍚嶏細娓叉煋鏃舵寜 conversation 涓婁笅鏂囪蛋 WeChat 浼樺厛绾у疄鏃剁畻 */
+function senderDisplayNameOf(message: Message): string {
+ return getSenderDisplayName(
+ message.senderId,
+ conversation.value?.type ?? 0,
+ conversation.value?.targetId ?? 0
+ )
+}
+
+/** 缇ゅ箍鎾簨浠� segments 鐨勬垚鍛樺悕瑙f瀽鍣紱鎸夊綋鍓嶄細璇� targetId 璧� getSenderDisplayName */
+function resolveGroupMemberName(message: Message): (userId: number) => string {
+ return (id: number) => getSenderDisplayName(id, ImConversationType.GROUP, message.targetId ?? 0)
+}
+
+/** 鍗曟潯娑堟伅鐨勫彂閫佷汉鐪熷疄鏄电О锛氱粰 UserAvatar 鑹插崱 / alt 鐢紝姘歌繙鏄� nickname 涓嶆幒澶囨敞 */
+function senderRealNicknameOf(message: Message): string {
+ return getSenderRealNickname(
+ message.senderId,
+ conversation.value?.type ?? 0,
+ conversation.value?.targetId ?? 0
+ )
+}
+
+/** 鍗曟潯鎾ゅ洖娑堟伅鐨� tip 鏂囨锛歜uildRecallTip 鍐呴儴鎸� conversation 涓婁笅鏂囧疄鏃剁畻 sender 鍚� */
+function recallTipOf(message: Message): string {
+ return buildRecallTip(
+ message.senderId,
+ message.selfSend,
+ conversation.value?.type ?? 0,
+ conversation.value?.targetId ?? 0
+ )
+}
+
+/** 鍗曟潯鎾ゅ洖娑堟伅鐨� tip segments锛歴ender 鍚嶆鎸傚彲鐐瑰嚮 mention */
+function recallTipSegmentsOf(message: Message) {
+ return buildRecallTipSegments(
+ message.senderId,
+ message.selfSend,
+ conversation.value?.type ?? 0,
+ conversation.value?.targetId ?? 0
+ )
+}
+
+// ==================== 鏍囬 ====================
+
+/**
+ * 寮圭獥鏍囬锛氬弬鑰冨井淇� PC
+ * - 绉佽亰锛氫笌"X"鐨勮亰澶╄褰�
+ * - 缇よ亰锛�"X 缇�"鐨勮亰澶╄褰�(N)
+ */
+const title = computed(() => {
+ if (!conversation.value) {
+ return '鑱婂ぉ璁板綍'
+ }
+ const name = conversation.value.name
+ if (isGroup.value) {
+ return `"${name}"鐨勮亰澶╄褰�(${allMessages.value.length})`
+ }
+ return `涓�"${name}"鐨勮亰澶╄褰昤
+})
+
+// ==================== 鎼滅储 + 绛涢�� chip ====================
+
+/** 褰撳墠婵�娲荤殑绛涢�夋潯浠� 鈥斺�� 鍗� chip 妯″紡锛堝悓鏃跺彧 1 涓級锛岀偣鍑� tab 钀芥柊鍊硷紝脳 娓呯┖ */
+type ActiveFilter =
+ | { day: string; kind: 'date'; } // YYYY-MM-DD
+ | { kind: 'file' }
+ | { kind: 'image' }
+ | { kind: 'member'; nickname: string; userId: number; }
+ | { kind: 'voice' }
+
+const keyword = ref('')
+const activeFilter = ref<ActiveFilter | null>(null)
+
+/** chip 鏂囨锛氭棩鏈� / 缇ゆ垚鍛� 澶氬甫鍊硷紝鍏朵粬鐩存帴 tab 鍚� */
+const filterChipLabel = computed(() => {
+ if (!activeFilter.value) {
+ return ''
+ }
+ switch (activeFilter.value.kind) {
+ case 'date': {
+ return `鏃ユ湡锛�${activeFilter.value.day}`
+ }
+ case 'file': {
+ return '鏂囦欢'
+ }
+ case 'image': {
+ return '鍥剧墖'
+ }
+ case 'member': {
+ return `@${activeFilter.value.nickname}`
+ }
+ case 'voice': {
+ return '璇煶'
+ }
+ default: {
+ return ''
+ }
+ }
+})
+
+/** 鐐� tab 钀界瓫閫夛紱鍚� kind 閲嶅鐐瑰嚮 鈫� 褰� toggle 鍏虫帀锛堥伩鍏嶈糠鎯戯級 */
+function setFilter(filter: ActiveFilter) {
+ if (
+ activeFilter.value?.kind === filter.kind &&
+ filter.kind !== 'date' &&
+ filter.kind !== 'member'
+ ) {
+ activeFilter.value = null
+ return
+ }
+ activeFilter.value = filter
+}
+
+/** chip 脳 鍏抽棴 / 閲嶇疆 */
+function clearFilter() {
+ activeFilter.value = null
+}
+
+// ==================== 鏃ユ湡 popover ====================
+
+const datePopoverVisible = ref(false)
+const datePickerValue = ref(dayjs())
+
+/** 鏃ユ湡 popover 纭畾锛氭妸 Date 鈫� YYYY-MM-DD 钀藉埌 activeFilter锛屽叧 popover */
+function onDateConfirm() {
+ if (!datePickerValue.value) {
+ datePopoverVisible.value = false
+ return
+ }
+ const day = datePickerValue.value.format('YYYY-MM-DD')
+ activeFilter.value = { kind: 'date', day }
+ datePopoverVisible.value = false
+}
+
+// ==================== 缇ゆ垚鍛� popover ====================
+
+const memberPopoverVisible = ref(false)
+const memberSearchKeyword = ref('')
+
+/** 缇ゆ垚鍛� picker 鍒楄〃锛氫粠 groupStore 鎷� + 閫傞厤 GroupMemberLite + 鍏抽敭瀛楄繃婊� */
+const filteredMembersForPicker = computed<GroupMemberLite[]>(() => {
+ if (!isGroup.value || !conversation.value) {
+ return []
+ }
+ const group = groupStore.getGroup(conversation.value.targetId)
+ const all = (group?.members || []).map((member) => {
+ const friend = friendStore.getFriend(member.userId)
+ return {
+ userId: member.userId,
+ showName: getMemberDisplayName(member, friend),
+ nickname: member.nickname,
+ avatar: member.avatar,
+ status: member.status
+ }
+ })
+ const trimmedKeyword = memberSearchKeyword.value.trim()
+ if (!trimmedKeyword) {
+ return all
+ }
+ return all.filter((member) => member.showName.includes(trimmedKeyword))
+})
+
+/** 缇ゆ垚鍛� picker 閫夋嫨锛氳惤 activeFilter + 鍏� popover + 娓呮悳绱㈣瘝 */
+function onMemberSelect(member: GroupMemberLite) {
+ activeFilter.value = {
+ kind: 'member',
+ userId: member.userId,
+ nickname: member.showName
+ }
+ memberPopoverVisible.value = false
+ memberSearchKeyword.value = ''
+}
+
+// ==================== 鍒楄〃杩囨护 ====================
+
+/** activeFilter 鍛戒腑锛氶粯璁ゆ棤绛涢�夋椂鍏ㄩ儴鍛戒腑 */
+function matchesActiveFilter(message: Message): boolean {
+ if (!activeFilter.value) {
+ return true
+ }
+ switch (activeFilter.value.kind) {
+ case 'date': {
+ return dayjs(message.sendTime).format('YYYY-MM-DD') === activeFilter.value.day
+ }
+ case 'file': {
+ return message.type === ImContentType.FILE
+ }
+ case 'image': {
+ return message.type === ImContentType.IMAGE
+ }
+ case 'member': {
+ return message.senderId === activeFilter.value.userId
+ }
+ case 'voice': {
+ return message.type === ImContentType.VOICE
+ }
+ default: {
+ return true
+ }
+ }
+}
+
+/**
+ * 褰撳墠鍒楄〃锛氬厛 activeFilter 杩囨护銆佸啀 keyword 妯$硦鍛戒腑銆佹渶鍚� reverse锛堟渶鏂板湪鍓嶏級
+ *
+ * 鍏抽敭瀛楀懡涓蛋 textSnippetOf 鈥斺�� 鏂囨湰鎷垮師鏂囥�佸獟浣撴嬁"[鍥剧墖]"绛夊崰浣嶈瘝銆佹枃浠舵嬁鏂囦欢鍚�
+ */
+const currentList = computed<Message[]>(() => {
+ const trimmedKeyword = keyword.value.trim()
+ let list = allMessages.value.filter((message) => matchesActiveFilter(message))
+ if (trimmedKeyword) {
+ list = list.filter((message) => textSnippetOf(message).includes(trimmedKeyword))
+ }
+ return list.toReversed()
+})
+
+// ==================== 鍔犺浇鏇存棭娑堟伅 ====================
+
+const HISTORY_PAGE_SIZE = 50
+const loadingMore = ref(false)
+const hasMore = ref(true)
+
+/**
+ * 鍔犺浇鏇存棭娑堟伅锛氭嬁褰撳墠鏈�鏃╀竴鏉� id 浣� maxId锛堜笉鍚級锛岃皟 list 鎺ュ彛鎷変竴椤� + convert + prepend
+ *
+ * - 鏈鎺� list 鎺ュ彛鐨� type / keyword / sender 杩囨护鍙傛暟锛氬悗绔彧鏀寔 maxId + limit 娓告爣鍒嗛〉锛�
+ * tab 绛涢�夊湪鍓嶇鍋氾紙鏁版嵁鏉ュ洖鍒版湰鍦板悗杩囨护锛�
+ * - 鏈湴鍗犱綅璺宠繃锛氬悗绔病娉曟寜 messageId 鏌�
+ * - 杩斿洖鏁伴噺 < limit 瑙嗕负鍒伴《
+ */
+async function loadEarlier() {
+ // 閲嶅叆 / 鍒伴《 / 鏃犱細璇� 鏃╅��锛氶伩鍏嶉噸澶嶈姹傛垨鍦� conversation 鍒囨崲闂撮殭瑙﹀彂
+ if (loadingMore.value || !hasMore.value || !conversation.value) {
+ return
+ }
+ // 浠� PRIVATE / GROUP 璧板垎椤垫帴鍙o紱CHANNEL 鍗曞悜骞挎挱銆佹病鏈� list 鎺ュ彛锛岃惤鍒� else 浼氳璋冪鑱婃帴鍙o紙receiverId 浼� channelId锛�
+ const requestedType = conversation.value.type
+ if (requestedType !== ImConversationType.PRIVATE && requestedType !== ImConversationType.GROUP) {
+ return
+ }
+ // 蹇収褰撳墠浼氳瘽涓婚敭锛歛wait 鏈熼棿鐢ㄦ埛鍒囪蛋 / 鍏抽棴闈㈡澘鏃朵涪寮冨搷搴旓紝閬垮厤鏃т細璇濆巻鍙茶 prepend 鍒版柊浼氳瘽閫犳垚涓插彿
+ const requestedKey = getConversationKey(conversation.value)
+ const requestedTargetId = conversation.value.targetId
+ const requestedIsGroup = requestedType === ImConversationType.GROUP
+
+ loadingMore.value = true
+ try {
+ // 绠� maxId锛堜笉鍚紝浣滀负鍚庣娓告爣锛夛細鍙栧綋鍓嶄細璇濇湰鍦扮紦瀛橀噷鏈�鏃╀竴鏉℃湇鍔$ id锛�
+ // 鏈湴涔愯鍗犱綅娑堟伅娌℃湁鏈嶅姟绔� id锛岃鍓旈櫎
+ // 鍏ㄦ槸鍗犱綅 / 鍒楄〃涓虹┖鏃� reduce 涓嶆洿鏂板垵鍊硷紙POSITIVE_INFINITY锛夛紝杞垚 undefined 鈫� 鍚庣浠庢渶鏂版媺
+ let earliestId = Number.POSITIVE_INFINITY
+ for (const message of allMessages.value) {
+ if (message.id && message.id > 0) {
+ earliestId = Math.min(earliestId, message.id)
+ }
+ }
+ const maxId = Number.isFinite(earliestId) ? earliestId : undefined
+
+ // 璋冨悗绔� list 鎺ュ彛锛氱鑱� / 缇よ亰鎺ュ彛绛惧悕涓嶅悓锛屽垎鏀皟搴︼紱杩斿洖缁撴灉鐢� useMessagePuller
+ // 鏆撮湶鐨� convert 鍑芥暟杞垚鏈湴 Message锛堜笌 puller 鍚屼竴浠藉瓧娈垫槧灏勶紝閬垮厤鍒嗘锛�
+ let earlier: Message[] = []
+ let pageLength = 0
+ if (requestedIsGroup) {
+ const list = await apiGetGroupMessageList({
+ groupId: requestedTargetId,
+ maxId,
+ limit: HISTORY_PAGE_SIZE
+ })
+ earlier = (list || []).map((message) => convertGroupMessage(message))
+ pageLength = list?.length ?? 0
+ } else {
+ const list = await apiGetPrivateMessageList({
+ receiverId: requestedTargetId,
+ maxId,
+ limit: HISTORY_PAGE_SIZE
+ })
+ earlier = (list || []).map((message) => convertPrivateMessage(message))
+ pageLength = list?.length ?? 0
+ }
+
+ // await 鏈熼棿 active 鍙兘琚閮ㄧ疆 null / 鎹富閿細鐩存帴涓㈠純鍝嶅簲锛涗笉鏇存柊 hasMore锛堟棫浼氳瘽鍒伴《涓嶄唬琛ㄦ柊浼氳瘽鍒伴《锛変篃涓� prepend
+ if (!conversation.value || getConversationKey(conversation.value) !== requestedKey) {
+ return
+ }
+
+ // 杩斿洖鏁伴噺 < limit 瑙嗕负鍒伴《 鈥斺�� 鍏抽棴"鍔犺浇鏇存棭"鎸夐挳锛岄伩鍏嶅悗缁偣鍑荤┖璺戞帴鍙�
+ if (pageLength < HISTORY_PAGE_SIZE) {
+ hasMore.value = false
+ }
+ // 鍚堝苟鍒� messageStore锛歱rependMessageList 鍐呴儴鍘婚噸 + 鍗囧簭鍚堝苟 + 钀� IndexedDB锛�
+ // 涓昏亰澶╅潰鏉跨殑 messages 鏄悓涓�浠藉紩鐢紝鑰佹秷鎭篃浼氫竴璧峰嚭鐜板湪涓婚潰鏉块噷锛堢鍚堥鏈燂級
+ messageStore.prependMessageList(requestedType, requestedTargetId, earlier)
+ } finally {
+ loadingMore.value = false
+ }
+}
+
+// ==================== 寮圭獥鎵撳紑 reset ====================
+
+/** 寮圭獥鎵撳紑鏃舵妸涓婃鐨� chip / 鎼滅储 / 鍔犺浇鐘舵�侀兘娓呭共鍑�锛岄伩鍏嶄笂娆$殑鐘舵�佹畫鐣欒糠鎯� */
+function onDialogOpen() {
+ activeFilter.value = null
+ keyword.value = ''
+ hasMore.value = true
+ datePopoverVisible.value = false
+ memberPopoverVisible.value = false
+ memberSearchKeyword.value = ''
+ datePickerValue.value = dayjs()
+ // 鏈湴鏃犳秷鎭椂绔嬪嵆鎷変竴娆★紙maxId=undefined 浠庢渶鏂板紑濮嬶級锛岄伩鍏嶆柊璁惧 / 缂撳瓨娓呭悗寮圭獥鍙樉绀�"鏆傛棤娑堟伅"
+ if (allMessages.value.length === 0 && hasMore.value && conversation.value) {
+ void loadEarlier()
+ }
+}
+
+/** 鎶藉眽鍏抽棴鏃跺浣� + 鍋滆闊� */
+watch(visible, (value) => {
+ if (!value) {
+ activeFilter.value = null
+ keyword.value = ''
+ voicePlayer.stop()
+ }
+})
+
+/**
+ * 鎶藉眽寮�鐫�鏃跺閮ㄥ垏浜� active conversation锛歞ialog 鐨� title / 鍒楄〃 / isGroup 鍏ㄩ儴璺熺潃鏂� conversation 璧帮紝
+ * 杩欓噷鎶婂垎椤垫�佷竴骞堕噸缃紱鍚﹀垯鏃т細璇濇畫鐣欑殑 loadingMore=true / hasMore=false 浼氳鏂颁細璇�"鍔犺浇鏇存棭"鎸夐挳澶辨晥
+ */
+watch(conversation, () => {
+ loadingMore.value = false
+ hasMore.value = true
+})
+
+// ==================== helper ====================
+
+/** 鍙栧ご鍍� url锛氳嚜宸辩敤 userStore锛岀兢閲屾煡 groupStore 鎴愬憳锛岀鑱婄敤 conversation.avatar */
+function getAvatar(message: Message): string {
+ if (message.selfSend) {
+ return userStore.userInfo?.avatar || ''
+ }
+ if (!conversation.value) {
+ return ''
+ }
+ if (isGroup.value) {
+ const group = groupStore.getGroup(conversation.value.targetId)
+ return group?.members?.find((member) => member.userId === message.senderId)?.avatar || ''
+ }
+ return conversation.value.avatar || ''
+}
+
+/** 鍏抽敭瀛楀懡涓枃鏈細鏂囨湰绫昏繑鍥炲師鏂囥�佹枃浠惰繑鍥炴枃浠跺悕锛堝埄浜庢寜鏂囦欢鍚嶆悳锛夈�佸叾浠栬繑鍥炲崰浣嶈瘝 */
+function textSnippetOf(message: Message): string {
+ if (isFriendChatTip(message.type)) {
+ return resolveFriendNotificationText(message)
+ }
+ switch (message.type) {
+ case ImContentType.CARD: {
+ const card = parseMessage<CardMessage>(message.content)
+ return `[${getCardLabelInfo(card).label}] ${card?.name ?? ''}`
+ }
+ case ImContentType.FACE: {
+ return buildFacePreviewText(parseMessage<FaceMessage>(message.content))
+ }
+ case ImContentType.FILE: {
+ return parseMessage<FileMessage>(message.content)?.name ?? '[鏂囦欢]'
+ }
+ case ImContentType.IMAGE: {
+ return '[鍥剧墖]'
+ }
+ case ImContentType.MERGE: {
+ const merge = parseMessage<MergeMessage>(message.content)
+ return merge?.title ?? '[鑱婂ぉ璁板綍]'
+ }
+ case ImContentType.RECALL: {
+ return recallTipOf(message)
+ }
+ case ImContentType.TEXT: {
+ return parseMessage<TextMessage>(message.content)?.content ?? ''
+ }
+ case ImContentType.VIDEO: {
+ return '[瑙嗛]'
+ }
+ case ImContentType.VOICE: {
+ return '[璇煶]'
+ }
+ default: {
+ return ''
+ }
+ }
+}
+
+/**
+ * 瀹氫綅鍒拌亰澶╀綅缃細emit 缁� MessagePanel 璧� scrollIntoView + 鐭殏楂樹寒锛屽啀鍏虫帀鑷繁
+ * messageId === 0锛堟湰鍦板崰浣嶆秷鎭級璺宠繃鈥斺�旇繕娌℃嬁鍒扮湡瀹� id锛孌OM 涓婃病娉� querySelector
+ */
+function locateMessage(messageId: number) {
+ if (!messageId) {
+ return
+ }
+ emit('locate', messageId)
+ visible.value = false
+}
+</script>
+
+<template>
+ <!--
+ 鍘嗗彶娑堟伅寮圭獥锛堝榻愬井淇� PC锛�
+ - 灞呬腑 dialog锛宼itle "涓庛�孹銆嶈亰澶╄褰�" / "銆孹 缇ゃ�嶈亰澶╄褰�(N)"
+ - 榛樿鏃犵瓫閫夛紝鍏ㄩ噺鍊掑簭灞曠ず锛涚偣鍑� tab 钀界瓫閫� 鈫� 鎼滅储妗嗗乏渚у嚭 chip 脳 鍙竻
+ - 鏂囦欢 / 鍥剧墖 / 閾炬帴锛氱洿鎺ヨ惤绛涢�夛紱鏃ユ湡 / 缇ゆ垚鍛橈細寮� popover 浜屾閫夋嫨鍐嶈惤
+ - "鍔犺浇鏇存棭娑堟伅"锛氬垪琛ㄥ簳閮ㄦ寜閽紝鐐瑰嚮璋� /im/message/{private,group}/list 鎷夸竴椤� + prepend
+ -->
+ <Modal
+ v-model:open="visible"
+ :title="title"
+ width="640px"
+ :footer="null"
+ class="im-message-history__dialog"
+ @after-open-change="(open: boolean) => open && onDialogOpen()"
+ >
+ <div class="flex flex-col gap-3 h-[520px]">
+ <!-- 鎼滅储鍖猴細activeFilter 瀛樺湪鏃跺乏渚у嚭 chip 脳 鍙竻锛屽惁鍒欑函鎼滅储妗� -->
+ <div
+ class="im-message-history__searchbar flex items-center gap-2 px-3 py-1.5 rounded-md bg-[var(--ant-color-fill-secondary)]"
+ >
+ <Tag
+ v-if="activeFilter"
+ closable
+ size="small"
+ class="im-message-history__chip flex-shrink-0"
+ @close="clearFilter"
+ >
+ {{ filterChipLabel }}
+ </Tag>
+ <Icon
+ v-else
+ icon="ant-design:search-outlined"
+ class="text-[var(--ant-color-text-secondary)]"
+ />
+ <input
+ v-model="keyword"
+ type="text"
+ placeholder="鎼滅储"
+ class="im-message-history__search-input flex-1 min-w-0 bg-transparent border-none outline-none text-sm text-[var(--ant-color-text)]"
+ />
+ </div>
+
+ <!-- Tab 琛岋細鏂囦欢 / 鍥剧墖 / 璇煶 / 鏃ユ湡(popover) / 缇ゆ垚鍛�(popover, 浠呯兢鑱�)锛涘簳閮ㄤ竴鏉″垎鍓茬嚎鎶� tab 鍖鸿窡娑堟伅鍒楄〃鍒嗗紑锛屽榻愬井淇¤鎰� -->
+ <div
+ class="flex gap-5 px-2 pb-2 text-sm flex-shrink-0 text-[#1989fa] border-b border-b-solid border-[var(--ant-color-border-secondary)]"
+ >
+ <span
+ class="im-message-history__tab cursor-pointer"
+ :class="{ 'im-message-history__tab--active': activeFilter?.kind === 'file' }"
+ @click="setFilter({ kind: 'file' })"
+ >
+ 鏂囦欢
+ </span>
+ <span
+ class="im-message-history__tab cursor-pointer"
+ :class="{ 'im-message-history__tab--active': activeFilter?.kind === 'image' }"
+ @click="setFilter({ kind: 'image' })"
+ >
+ 鍥剧墖
+ </span>
+ <span
+ class="im-message-history__tab cursor-pointer"
+ :class="{ 'im-message-history__tab--active': activeFilter?.kind === 'voice' }"
+ @click="setFilter({ kind: 'voice' })"
+ >
+ 璇煶
+ </span>
+ <!-- 鏃ユ湡锛歟l-popover 鍖� el-calendar锛岀‘璁ゅ悗鎵嶈惤绛涢�� -->
+ <Popover
+ v-model:open="datePopoverVisible"
+ trigger="click"
+ placement="bottom"
+ :overlay-style="{ width: '320px' }"
+ >
+ <span
+ class="im-message-history__tab cursor-pointer"
+ :class="{ 'im-message-history__tab--active': activeFilter?.kind === 'date' }"
+ >
+ 鏃ユ湡
+ </span>
+ <template #content>
+ <div class="im-message-history__date-panel">
+ <div class="px-2 pt-1 pb-2 text-13px font-medium text-[var(--ant-color-text)]">
+ 閫夋嫨鍙戦�佹棩鏈�
+ </div>
+ <Calendar
+ v-model:value="datePickerValue"
+ :fullscreen="false"
+ class="im-message-history__calendar"
+ />
+ <div class="flex gap-2 justify-end px-2 pt-2">
+ <Button size="small" @click="datePopoverVisible = false">鍙栨秷</Button>
+ <Button size="small" type="primary" @click="onDateConfirm">纭畾</Button>
+ </div>
+ </div>
+ </template>
+ </Popover>
+ <!-- 缇ゆ垚鍛橈細浠呯兢鑱婏紱popover 鍐呰嚜甯︽悳绱� + GroupMember 鍒楄〃 -->
+ <Popover
+ v-if="isGroup"
+ v-model:open="memberPopoverVisible"
+ trigger="click"
+ placement="bottom"
+ :overlay-style="{ width: '320px' }"
+ >
+ <span
+ class="im-message-history__tab cursor-pointer"
+ :class="{ 'im-message-history__tab--active': activeFilter?.kind === 'member' }"
+ >
+ 缇ゆ垚鍛�
+ </span>
+ <template #content>
+ <div>
+ <Input v-model:value="memberSearchKeyword" placeholder="鎼滅储缇ゆ垚鍛�" size="small">
+ <template #prefix>
+ <Icon icon="ant-design:search-outlined" />
+ </template>
+ </Input>
+ <div class="max-h-[360px] overflow-y-auto mt-2">
+ <GroupMember
+ v-for="member in filteredMembersForPicker"
+ :key="member.userId"
+ :member="member"
+ :height="44"
+ :clickable="false"
+ class="cursor-pointer hover:bg-[var(--ant-color-fill)]"
+ @click="onMemberSelect(member)"
+ />
+ <div
+ v-if="filteredMembersForPicker.length === 0"
+ class="py-6 text-12px text-center text-[var(--ant-color-text-disabled)]"
+ >
+ 娌℃湁鍖归厤鐨勬垚鍛�
+ </div>
+ </div>
+ </div>
+ </template>
+ </Popover>
+ </div>
+
+ <!-- 娑堟伅鍒楄〃 -->
+ <div class="flex-1 overflow-y-auto">
+ <template v-for="message in currentList" :key="message.id || message.clientMessageId">
+ <!-- 濂藉弸浼氳瘽浜嬩欢锛團RIEND_ADD / FRIEND_DELETE锛夛細灞呬腑鐏拌壊锛屼笉鎸傚ご鍍� / sender锛�
+ 璺熶富鑱婂ぉ闈㈡澘閲� MessageItem 鐨勬覆鏌撹涔夊榻� -->
+ <div
+ v-if="isFriendChatTip(message.type)"
+ class="px-4 py-3 text-12px text-center italic text-[var(--ant-color-text-secondary)] border-b border-b-solid border-[var(--ant-color-border-secondary)]"
+ >
+ <TipSegments :segments="resolveFriendNotificationSegments(message)" />
+ </div>
+
+ <!-- 缇ゅ箍鎾簨浠讹細璺熷ソ鍙嬩簨浠跺悓鐏拌壊鏍峰紡锛宮ention 娈垫寕鐐瑰嚮寮� UserInfoCard -->
+ <div
+ v-else-if="isGroupNotification(message.type)"
+ class="px-4 py-3 text-12px text-center italic text-[var(--ant-color-text-secondary)] border-b border-b-solid border-[var(--ant-color-border-secondary)]"
+ >
+ <TipSegments
+ :segments="resolveGroupNotificationSegments(message, resolveGroupMemberName(message))"
+ />
+ </div>
+
+ <!-- 鏅�氭秷鎭 -->
+ <div
+ v-else
+ class="im-message-history__row flex gap-3 items-start px-1 py-3 border-b border-b-solid border-[var(--ant-color-border-secondary)]"
+ >
+ <UserAvatar
+ :url="getAvatar(message)"
+ :name="senderRealNicknameOf(message)"
+ :size="36"
+ :clickable="false"
+ />
+ <div class="flex-1 min-w-0">
+ <div
+ class="flex justify-between items-start text-12px text-[var(--ant-color-text-secondary)]"
+ >
+ <span class="font-medium text-[var(--ant-color-text)]">
+ {{ senderDisplayNameOf(message) }}
+ </span>
+ <span class="im-message-history__meta relative flex-shrink-0">
+ <span class="block text-right">{{ formatHistoryTime(message.sendTime) }}</span>
+ <!-- 瀹氫綅鍒拌亰澶╀綅缃細absolute 娴湪鏃堕棿涓嬫柟锛岃 hover 鎵嶆樉绀猴紝
+ 涓嶅弬涓庡彸渚ф爮 flex 鎺掔増锛堥伩鍏嶉殣钘忔椂鍗犱綅璁�"鎴�"鍜屽唴瀹逛箣闂寸暀绌猴級锛�
+ 浠呮湁鐪熷疄 id 鐨勬秷鎭墠鏀寔锛堟湰鍦板崰浣嶆秷鎭笉琛岋級 -->
+ <span
+ v-if="message.id && message.id > 0"
+ class="im-message-history__locate"
+ @click="locateMessage(message.id!)"
+ >
+ 瀹氫綅鍒拌亰澶╀綅缃�
+ </span>
+ </span>
+ </div>
+
+ <div class="mt-1.5">
+ <!-- 鎾ゅ洖锛氬崟鐙蛋鐏拌壊 tip锛宻ender 鍚嶆鍙偣鍑� -->
+ <div
+ v-if="message.type === ImContentType.RECALL"
+ class="text-sm italic text-[var(--ant-color-text-secondary)]"
+ >
+ <TipSegments :segments="recallTipSegmentsOf(message)" />
+ </div>
+ <!-- 鍏跺畠绫诲瀷璧� MessageBubble 澶嶇敤涓昏亰澶╂皵娉� -->
+ <MessageBubble
+ v-else
+ :type="message.type"
+ :content="message.content"
+ @open-merge="openMergeDetail?.($event)"
+ />
+ </div>
+ </div>
+ </div>
+ </template>
+
+ <div
+ v-if="currentList.length === 0"
+ class="py-10 text-center text-13px text-[var(--ant-color-text-disabled)]"
+ >
+ {{ keyword || activeFilter ? '娌℃湁鍖归厤鐨勬秷鎭�' : '鏆傛棤娑堟伅' }}
+ </div>
+
+ <!-- 鍔犺浇鏇存棭娑堟伅锛氬垪琛ㄥ簳閮� trigger锛坮everse 鍚庢渶鏃╃殑鍦ㄦ渶涓嬶紝鎸夐挳鏀惧簳閮ㄦ洿鑷劧锛夛紱
+ filter 鍛戒腑 0 鏉℃椂浠嶄繚鐣� 鈥斺�� 鍔犺浇鏇存棭鍙兘甯﹀洖鍖归厤鍐呭 -->
+ <div
+ v-if="hasMore && allMessages.length > 0"
+ class="py-3 text-center border-t border-t-solid border-[var(--ant-color-border-secondary)]"
+ >
+ <Button :loading="loadingMore" type="link" @click="loadEarlier">
+ 鍔犺浇鏇存棭娑堟伅
+ </Button>
+ </div>
+ <!-- "娌℃湁鏇存棭" 鍙湪宸叉湁鍖归厤椤规椂闇插嚭锛岄伩鍏嶅拰涓婇潰"娌℃湁鍖归厤鐨勬秷鎭�"绌烘�佹枃妗堥噸鍙� -->
+ <div
+ v-else-if="!hasMore && currentList.length > 0"
+ class="py-3 text-12px text-center text-[var(--ant-color-text-disabled)]"
+ >
+ 娌℃湁鏇存棭鐨勬秷鎭簡
+ </div>
+ </div>
+ </div>
+ </Modal>
+</template>
+
+<style scoped>
+/* :deep 绌块�� el-tag 瀛愮粍浠� DOM锛涙悳绱㈠尯 chip 绂佹帀 hover 棰滆壊杩囨浮 / 脳 鍥炬爣鍔ㄦ晥锛岄伩鍏嶅湪鎼滅储鍖洪噷鏈夋姈鍔ㄦ劅 */
+.im-message-history__chip,
+.im-message-history__chip :deep(.el-tag__close) {
+ transition: none !important;
+ animation: none !important;
+}
+
+/* 銆屽畾浣嶅埌鑱婂ぉ浣嶇疆銆嶆寜閽細鐖惰 hover 鎵嶆樉绀猴紝璧� .parent:hover .child 璺ㄥ厓绱犵姸鎬佽仈鍔� */
+.im-message-history__locate {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ margin-top: 4px;
+ display: none;
+ white-space: nowrap;
+ color: #1989fa;
+ cursor: pointer;
+ transition: color 0.15s;
+}
+.im-message-history__row:hover .im-message-history__locate {
+ display: block;
+}
+.im-message-history__locate:hover {
+ color: #146fc7;
+}
+
+/* ::after 浼厓绱犵敾婵�娲讳笅鍒掔嚎锛泃ab 榛樿钃濊壊琛屽唴鏂囧瓧锛屾縺娲绘�佸姞涓嬪垝绾� */
+.im-message-history__tab {
+ position: relative;
+ padding: 4px 2px;
+ color: #1989fa;
+ transition: color 0.15s;
+}
+.im-message-history__tab:hover {
+ color: #2f81d4;
+}
+.im-message-history__tab--active::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ height: 2px;
+ background: #1989fa;
+ border-radius: 1px;
+}
+
+/* :deep 绌块�� antd Calendar锛坒ullscreen=false 鍗$墖妯″紡锛夊瓙 DOM锛屽帇涓�鍘嬪杩� 320 popover */
+.im-message-history__calendar :deep(.ant-picker-calendar-header) {
+ padding: 4px 8px;
+}
+.im-message-history__calendar :deep(.ant-picker-content) {
+ font-size: 12px;
+}
+.im-message-history__calendar :deep(.ant-picker-cell) {
+ padding: 1px 0;
+}
+.im-message-history__calendar :deep(.ant-picker-cell .ant-picker-calendar-date) {
+ height: 28px;
+ margin: 0 2px;
+ padding: 2px 4px 0;
+}
+</style>
--
Gitblit v1.9.3