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/components/picker/conversation-picker-panel.vue | 370 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 370 insertions(+), 0 deletions(-)
diff --git a/src/views/im/home/components/picker/conversation-picker-panel.vue b/src/views/im/home/components/picker/conversation-picker-panel.vue
new file mode 100644
index 0000000..b4c944c
--- /dev/null
+++ b/src/views/im/home/components/picker/conversation-picker-panel.vue
@@ -0,0 +1,370 @@
+<script lang="ts" setup>
+import type { Conversation } from '../../types'
+
+import { computed, ref } from 'vue'
+
+import { IconifyIcon as Icon } from '#/packages/icons/src'
+
+import { Input, message } from 'ant-design-vue'
+
+import { ImConversationType } from '../../../utils/constants'
+import { filterConversationsByKeyword, getConversationKey } from '../../../utils/conversation'
+import { GroupAvatar } from '../group'
+import { UserAvatar } from '../user'
+
+defineOptions({ name: 'ImConversationPickerPanel' })
+
+const props = withDefaults(
+ defineProps<{
+ /** 鍏ㄩ噺浼氳瘽鍒楄〃 */
+ conversations: Conversation[]
+ /** 闅愯棌 key锛氫粠鍊欓�� / 宸查�� / 鏈�杩戣浆鍙戦噷閮藉墧闄わ紙涓嶈兘杞彂鍥炶嚜宸便�佹帹鑽愬悕鐗囪嚜韬殑浼氳瘽绛夛級 */
+ hideKeys?: string[]
+ /** 宸查�夋暟涓婇檺锛涗笉浼犳垨 <=0 鏃朵笉闄� */
+ maxSize?: number
+ /** 鏈�杩戣浆鍙戜細璇� key 鍒楄〃锛涘睍绀哄湪宸︽爮椤堕儴妯悜澶村儚鍖� */
+ recentForwardConversationKeys?: string[]
+ /** 宸查�変細璇� key锛坴-model锛夛紱key 鐢� getConversationKey 鐢熸垚 */
+ selectedKeys: string[]
+ /** 鏄惁灞曠ず銆屽垱寤鸿亰澶┿�嶅叆鍙� */
+ showCreateChat?: boolean
+ }>(),
+ {
+ recentForwardConversationKeys: () => [],
+ hideKeys: () => [],
+ maxSize: 0,
+ showCreateChat: false
+ }
+)
+
+const emit = defineEmits<{
+ createChat: []
+ /** 鐢ㄦ埛鍦ㄣ�屾渶杩戣浆鍙戙�嶆杩涘叆绉婚櫎妯″紡鍚庣偣 脳锛涗笟鍔″3鏀跺埌鍚庤皟 conversationStore.removeRecentForwardConversationKey 钀界洏 */
+ removeRecent: [key: string]
+ 'update:selectedKeys': [value: string[]]
+}>()
+
+const keyword = ref('')
+const recentRemoveMode = ref(false) // 銆屾渶杩戣浆鍙戙�嶆鏄惁澶勪簬绉婚櫎妯″紡锛歵rue 鏃跺ご鍍忓彸涓婅鍙� 脳 涓嶅啀鍒囧嬀閫�
+
+/** 鍏ㄩ噺浼氳瘽鐨� key鈫扖onversation 鏄犲皠锛屽凡閫� / 鏈�杩戣浆鍙戝弽鏌ュ叡鐢紝閬垮厤姣忔 O(N) 鎵� */
+const byKey = computed(() => {
+ const map = new Map<string, Conversation>()
+ for (const conversation of props.conversations) {
+ map.set(getConversationKey(conversation), conversation)
+ }
+ return map
+})
+
+/** 闅愯棌闆嗗悎锛氭瘡娆¤繃婊ゅ鐢� */
+const hideSet = computed(() => new Set(props.hideKeys))
+
+/** 宸查�夐泦鍚堬細鍦嗗舰鎸囩ず鍣� isSelected 璧� set 蹇煡 */
+const selectedSet = computed(() => new Set(props.selectedKeys))
+
+/** 鍊欓�変細璇濓細鍓旈櫎 hideKeys */
+const candidateConversations = computed(() =>
+ props.conversations.filter((c) => !hideSet.value.has(getConversationKey(c)))
+)
+
+/** 宸︽爮灞曠ず鍒楄〃锛氬湪鍊欓�夊熀纭�涓婃寜 keyword 杩囨护 */
+const shownConversations = computed(() =>
+ filterConversationsByKeyword(candidateConversations.value, keyword.value)
+)
+
+/** 鏈�杩戣浆鍙戠殑浼氳瘽瀵硅薄鍒楄〃锛氫粠 recentForwardConversationKeys 鍙嶆煡锛涘墧闄� hide / 涓嶅瓨鍦ㄧ殑 key */
+const recentForwardConversations = computed(() =>
+ props.recentForwardConversationKeys
+ .map((key) => byKey.value.get(key))
+ .filter((c): c is Conversation => c != null && !hideSet.value.has(getConversationKey(c)))
+)
+
+/** 鏄惁灞曠ず銆屾渶杩戣浆鍙戙�嶆锛歬eyword 涓虹┖ + 鏈夋暟鎹椂鎵嶅睍绀猴紝鎼滅储鏃惰浣� */
+const showRecentSection = computed(
+ () => !keyword.value.trim() && recentForwardConversations.value.length > 0
+)
+
+/** 宸查�変細璇濆垪琛細鎸� selectedKeys 鏁扮粍椤哄簭锛堝嵆鐐瑰嚮椤哄簭锛夊弽鏌ワ紱杩囨护 hideSet 閬垮厤鐖剁粍浠跺姩鎬侀殣钘忕殑浼氳瘽浠嶅湪鍙充晶娓叉煋 / 鎻愪氦 */
+const selectedConversations = computed(() =>
+ props.selectedKeys
+ .map((key) => byKey.value.get(key))
+ .filter(
+ (conversation): conversation is Conversation =>
+ conversation != null && !hideSet.value.has(getConversationKey(conversation))
+ )
+)
+
+/** 鍙虫爮鏍囬鏂囨锛氬崟閫夈�屽彂閫佺粰銆嶃�佸閫夈�屽垎鍒彂閫佺粰銆� */
+const sendTitle = computed(() => (props.selectedKeys.length > 1 ? '鍒嗗埆鍙戦�佺粰' : '鍙戦�佺粰'))
+
+/** 鏄惁宸查�変腑锛氬乏鏍忓渾褰㈡寚绀哄櫒 / 鏈�杩戣浆鍙戝ご鍍忚鏍囧叡鐢� */
+function isSelected(conversation: Conversation): boolean {
+ return selectedSet.value.has(getConversationKey(conversation))
+}
+
+/** 銆屾渶杩戣浆鍙戙�嶅ご鍍忕偣鍑伙細绉婚櫎妯″紡涓嬩笉鍒囧嬀閫夛紙绉婚櫎鐢� 脳 瑙掓爣澶勭悊锛� */
+function handleRecentTileClick(conversation: Conversation) {
+ if (recentRemoveMode.value) {
+ return
+ }
+ handleToggle(conversation)
+}
+
+/** 鍒囨崲閫変腑鎬侊細宸︽爮 row / 鏈�杩戣浆鍙戝ご鍍� / 鍙虫爮 脳 绉婚櫎閮借蛋杩欓噷 */
+function handleToggle(conversation: Conversation) {
+ const key = getConversationKey(conversation)
+ const next = [...props.selectedKeys]
+ const index = next.indexOf(key)
+ if (index === -1) {
+ // 鐖剁粍浠舵爣璁伴殣钘忕殑浼氳瘽鍗充究鏈夎矾寰勮Е杈句篃涓嶅簲鍏ラ��
+ if (hideSet.value.has(key)) {
+ return
+ }
+ if (props.maxSize > 0 && next.length >= props.maxSize) {
+ message.error(`鏈�澶氶�夋嫨 ${props.maxSize} 涓細璇漙)
+ return
+ }
+ next.push(key)
+ } else {
+ next.splice(index, 1)
+ }
+ emit('update:selectedKeys', next)
+}
+</script>
+
+<template>
+ <!--
+ 浼氳瘽閫夋嫨闈㈡澘锛氱敤浜庢帹鑽愬悕鐗� / 杞彂娑堟伅绛�"閫夊凡鏈変細璇�"鍦烘櫙
+ - 宸︼細鎼滅储 + 鏈�杩戣浆鍙戞í鍚戝ご鍍� + 鍒涘缓鑱婂ぉ鍏ュ彛 + 鏈�杩戣亰澶╁垪琛紙鍦嗗舰鍕鹃�夛級
+ - 鍙筹細宸查�夋暟鏍囬 + 宸查�変細璇濆垪琛紙鎸夌偣鍑婚『搴忥級+ footer slot
+ - Panel 涓嶅甫 el-dialog 澹筹紱dialog 鐢变笟鍔″3鎸佹湁
+ - footer slot 娓叉煋鍦ㄥ彸鏍忓凡閫夊垪琛ㄤ笅鏂癸紝涓氬姟澹虫斁棰勮鍗� / 鐣欒█ / 鎻愪氦鎸夐挳
+ -->
+ <div class="flex h-full">
+ <!-- 宸︽爮 -->
+ <div
+ class="flex flex-col w-[280px] border-r border-r-solid border-[var(--ant-color-border-secondary)] bg-[var(--ant-color-fill-secondary)]"
+ >
+ <!-- 鎼滅储妗� -->
+ <div class="flex-shrink-0 px-3 py-2">
+ <Input v-model:value="keyword" placeholder="鎼滅储" allow-clear>
+ <template #prefix>
+ <Icon icon="ant-design:search-outlined" />
+ </template>
+ </Input>
+ </div>
+
+ <div class="flex-1">
+ <!-- 鏈�杩戣浆鍙戞í鍚戝ご鍍忓尯锛歬eyword 涓虹┖ + 鏈夋渶杩戣浆鍙戞暟鎹椂灞曠ず -->
+ <template v-if="showRecentSection">
+ <div class="flex justify-between items-center pl-3 pr-2 pb-1.5">
+ <span class="text-13px text-[var(--ant-color-text-secondary)]">鏈�杩戣浆鍙�</span>
+ <span
+ class="px-1 cursor-pointer text-13px text-[var(--ant-color-primary)] hover:opacity-80"
+ @click="recentRemoveMode = !recentRemoveMode"
+ >
+ {{ recentRemoveMode ? '瀹屾垚' : '绉婚櫎' }}
+ </span>
+ </div>
+ <div
+ class="flex gap-2 pl-3 pr-2 pt-1 pb-2 overflow-x-auto im-conversation-picker__recent"
+ >
+ <div
+ v-for="conversation in recentForwardConversations"
+ :key="getConversationKey(conversation)"
+ class="flex flex-col flex-shrink-0 gap-1 items-center"
+ :class="{ 'cursor-pointer': !recentRemoveMode }"
+ @click="handleRecentTileClick(conversation)"
+ >
+ <div class="relative">
+ <GroupAvatar
+ v-if="conversation.type === ImConversationType.GROUP"
+ :group-id="conversation.targetId"
+ :url="conversation.avatar"
+ :name="conversation.name"
+ :size="36"
+ />
+ <UserAvatar
+ v-else
+ :url="conversation.avatar"
+ :name="conversation.name"
+ :size="36"
+ :clickable="false"
+ />
+ <!-- 绉婚櫎妯″紡锛氬彸涓婅 脳 鍦嗚鏍囷紝鐐瑰嚮鎶婅繖鏉� key 浠� recentForwardConversationKeys 鍒犳帀 -->
+ <span
+ v-if="recentRemoveMode"
+ class="flex absolute -top-1 -right-1 justify-center items-center w-4 h-4 rounded-full cursor-pointer bg-[var(--ant-color-fill-dark)] text-[var(--ant-color-text)]"
+ @click.stop="emit('removeRecent', getConversationKey(conversation))"
+ >
+ <Icon icon="ant-design:close-outlined" :size="10" />
+ </span>
+ <!-- 闈炵Щ闄ゆā寮忥細鍙充笂瑙掑渾褰㈠嬀閫夋寚绀哄櫒锛涙湭閫夌伆绌哄績鍦堛�侀�変腑缁垮簳鐧藉鍕� -->
+ <span
+ v-else
+ class="flex absolute -top-1 -right-1 justify-center items-center w-4 h-4 rounded-full transition-colors"
+ :class="
+ isSelected(conversation)
+ ? 'bg-[#07c160] border border-solid border-[#07c160]'
+ : 'border border-solid border-[var(--ant-color-border)] bg-[var(--ant-color-bg-container)]'
+ "
+ >
+ <Icon
+ v-if="isSelected(conversation)"
+ icon="ant-design:check-outlined"
+ :size="10"
+ color="#fff"
+ />
+ </span>
+ </div>
+ <span
+ class="overflow-hidden max-w-[48px] text-12px truncate text-[var(--ant-color-text)]"
+ >
+ {{ conversation.name }}
+ </span>
+ </div>
+ </div>
+ </template>
+
+ <!-- 鍒涘缓鑱婂ぉ鍏ュ彛锛歬eyword 涓虹┖ + showCreateChat=true 鏃跺睍绀� -->
+ <div
+ v-if="showCreateChat && !keyword.trim()"
+ class="flex gap-2.5 items-center px-3 py-1.5 cursor-pointer hover:bg-[var(--ant-color-fill)]"
+ @click="emit('createChat')"
+ >
+ <span
+ class="flex flex-shrink-0 justify-center items-center w-8 h-8 rounded-full bg-[var(--ant-color-fill)] text-[var(--ant-color-text-secondary)]"
+ >
+ <Icon icon="ant-design:plus-outlined" :size="16" />
+ </span>
+ <span class="text-sm text-[var(--ant-color-text)]">鍒涘缓鑱婂ぉ</span>
+ </div>
+
+ <!-- 鏈�杩戣亰澶╁垎缁勬爣棰� -->
+ <div class="px-3 pb-1.5 text-13px text-[var(--ant-color-text-secondary)]">鏈�杩戣亰澶�</div>
+
+ <!-- 浼氳瘽鍒楄〃 -->
+ <div
+ v-for="conversation in shownConversations"
+ :key="getConversationKey(conversation)"
+ class="flex gap-2.5 items-center px-3 py-1.5 cursor-pointer hover:bg-[var(--ant-color-fill)]"
+ @click="handleToggle(conversation)"
+ >
+ <!-- 鍦嗗舰鍕鹃�夋寚绀哄櫒锛氭湭閫夌伆鑹茬┖蹇冨渾锛岄�変腑瀹炲績寰俊缁� + 鐧藉鍕� -->
+ <span
+ class="flex flex-shrink-0 justify-center items-center w-5 h-5 rounded-full transition-colors"
+ :class="
+ isSelected(conversation)
+ ? 'bg-[#07c160] border border-solid border-[#07c160]'
+ : 'border border-solid border-[var(--ant-color-border)] bg-[var(--ant-color-bg-container)]'
+ "
+ >
+ <Icon
+ v-if="isSelected(conversation)"
+ icon="ant-design:check-outlined"
+ :size="12"
+ color="#fff"
+ />
+ </span>
+ <GroupAvatar
+ v-if="conversation.type === ImConversationType.GROUP"
+ :group-id="conversation.targetId"
+ :url="conversation.avatar"
+ :name="conversation.name"
+ :size="32"
+ />
+ <UserAvatar
+ v-else
+ :url="conversation.avatar"
+ :name="conversation.name"
+ :size="32"
+ :clickable="false"
+ />
+ <span
+ class="flex-1 min-w-0 overflow-hidden text-sm truncate text-[var(--ant-color-text)]"
+ >
+ {{ conversation.name }}
+ </span>
+ </div>
+
+ <!-- 绌烘�� -->
+ <div
+ v-if="shownConversations.length === 0"
+ class="py-10 text-13px text-center text-[var(--ant-color-text-disabled)]"
+ >
+ {{ keyword ? '娌℃湁婊¤冻鏉′欢鐨勪細璇�' : '鏆傛棤浼氳瘽' }}
+ </div>
+ </div>
+ </div>
+
+ <!-- 鍙虫爮 -->
+ <div class="flex flex-col flex-1 min-w-0">
+ <!-- 鏍囬锛氶�� 0/1銆屽彂閫佺粰銆嶃�佸涓�屽垎鍒彂閫佺粰銆嶏紙涓庡井淇℃枃妗堜竴鑷达級 -->
+ <div
+ class="flex-shrink-0 px-4 py-3 border-b border-b-solid text-13px text-[var(--ant-color-text-secondary)] border-[var(--ant-color-border-secondary)]"
+ >
+ {{ sendTitle }}
+ </div>
+
+ <!-- 宸查�夐瑙堬細鎸� selectedKeys 鏁扮粍椤哄簭锛堢偣鍑婚『搴忥級灞曠ず -->
+ <div class="flex-1">
+ <div
+ v-for="conversation in selectedConversations"
+ :key="getConversationKey(conversation)"
+ class="flex gap-2.5 items-center px-4 py-2"
+ >
+ <GroupAvatar
+ v-if="conversation.type === ImConversationType.GROUP"
+ :group-id="conversation.targetId"
+ :url="conversation.avatar"
+ :name="conversation.name"
+ :size="32"
+ />
+ <UserAvatar
+ v-else
+ :url="conversation.avatar"
+ :name="conversation.name"
+ :size="32"
+ :clickable="false"
+ />
+ <span
+ class="flex-1 min-w-0 overflow-hidden text-sm truncate text-[var(--ant-color-text)]"
+ >
+ {{ conversation.name }}
+ </span>
+ <Icon
+ icon="ant-design:close-outlined"
+ :size="14"
+ class="flex-shrink-0 cursor-pointer transition-colors text-[var(--ant-color-text-placeholder)] hover:text-[var(--ant-color-error)]"
+ @click="handleToggle(conversation)"
+ />
+ </div>
+ <div
+ v-if="selectedConversations.length === 0"
+ class="py-10 text-13px text-center text-[var(--ant-color-text-disabled)]"
+ >
+ 浠庡乏渚ч�夋嫨濂藉弸鎴栫兢鑱�
+ </div>
+ </div>
+
+ <!-- 涓氬姟澹冲棰勮鍗� / 鐣欒█ / 鎻愪氦鎸夐挳鐨勪綅缃� -->
+ <div
+ v-if="$slots.footer"
+ class="flex-shrink-0 border-t border-t-solid border-[var(--ant-color-border-secondary)]"
+ >
+ <slot name="footer"></slot>
+ </div>
+ </div>
+ </div>
+</template>
+
+<style scoped>
+/* 妯悜婊氬姩鏉″仛绐勪竴鐐归伩鍏嶅崰瑙嗚锛涜蛋 ::-webkit-scrollbar 娴忚鍣ㄤ吉鍏冪礌 */
+.im-conversation-picker__recent::-webkit-scrollbar {
+ height: 4px;
+}
+.im-conversation-picker__recent::-webkit-scrollbar-thumb {
+ background-color: var(--ant-color-border);
+ border-radius: 2px;
+}
+</style>
--
Gitblit v1.9.3