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