From 787ccc59ba89bacc075562a161ecf02bc76ebadc Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期五, 24 七月 2026 17:15:53 +0800
Subject: [PATCH] 银川 1.采购申请添加入库状态查询条件

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