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/friend-picker-panel.vue |  245 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 245 insertions(+), 0 deletions(-)

diff --git a/src/views/im/home/components/picker/friend-picker-panel.vue b/src/views/im/home/components/picker/friend-picker-panel.vue
new file mode 100644
index 0000000..2773d08
--- /dev/null
+++ b/src/views/im/home/components/picker/friend-picker-panel.vue
@@ -0,0 +1,245 @@
+<script lang="ts" setup>
+import type { FriendLite } from '../../types'
+
+import { computed, ref } from 'vue'
+
+import { IconifyIcon as Icon } from '#/packages/icons/src'
+
+import { Input, message } from 'ant-design-vue'
+
+import { PagedScroller } from '..'
+import { useFriendBuckets } from '../../composables/useFriendBuckets'
+import { useSelectedItems } from '../../composables/useSelectedItems'
+import { UserAvatar } from '../user'
+
+defineOptions({ name: 'ImFriendPickerPanel' })
+
+const props = withDefaults(
+  defineProps<{
+    /** 绂佺敤 id锛氬垪琛ㄩ噷灞曠ず缃伆銆佷笉鍙嬀閫夈�佷笉璁″叆宸查�夋暟锛堝吀鍨嬶細閭�璇峰叆缇ゆ椂宸插湪缇ゆ垚鍛橈級 */
+    disabledIds?: number[]
+    /** 鍏ㄩ噺濂藉弸鍒楄〃 */
+    friends: FriendLite[]
+    /** 闅愯棌 id锛氫笉灞曠ず锛坔ide > locked > disabled锛� */
+    hideIds?: number[]
+    /** 閿佸畾 id锛氶粯璁ゅ嬀閫夈�佷笉鍙彇娑堛�佽鍏ュ凡閫夋暟锛堝吀鍨嬶細绉佽亰渚� +寤虹兢閿佸畾瀵规柟锛� */
+    lockedIds?: number[]
+    /** 宸查�夋暟涓婇檺锛涗笉浼犳垨 <=0 鏃朵笉闄� */
+    maxSize?: number
+    /** 宸查�夊ソ鍙� id锛坴-model锛夛紱鎸夋暟缁勯『搴忓嵆涓虹偣鍑婚『搴� */
+    selectedIds: number[]
+  }>(),
+  {
+    lockedIds: () => [],
+    disabledIds: () => [],
+    hideIds: () => [],
+    maxSize: 0
+  }
+)
+
+const emit = defineEmits<{
+  'update:selectedIds': [value: number[]]
+}>()
+
+const keyword = ref('')
+
+/** id 鈫� friend 鏄犲皠锛屽凡閫夊弽鏌� / 涓夋�佸垽瀹氬叡鐢紝閬垮厤姣忔 O(N) 鎵� */
+const byId = computed(() => {
+  const map = new Map<number, FriendLite>()
+  for (const friend of props.friends) {
+    map.set(friend.id, friend)
+  }
+  return map
+})
+
+/** 涓夋�� id 闆嗗悎锛氭瘡娆¤繃婊ゅ鐢� */
+const hideSet = computed(() => new Set(props.hideIds))
+const lockedSet = computed(() => new Set(props.lockedIds))
+const disabledSet = computed(() => new Set(props.disabledIds))
+const selectedSet = computed(() => new Set(props.selectedIds))
+
+/** 鍊欓�夊ソ鍙嬶細鍓旈櫎 hideIds锛坔ide 浼樺厛绾ф渶楂橈級 */
+const candidates = computed(() =>
+  props.friends.filter((friend) => !hideSet.value.has(friend.id))
+)
+
+/** 濮旀墭 useFriendBuckets锛氭悳绱㈣鍒欏鐢紝宸︿晶鍒楄〃鎸夋粴鍔ㄥ垎椤垫覆鏌� */
+const { filtered } = useFriendBuckets(candidates, keyword)
+
+/** 宸查�夋暟 + 宸查�夊ソ鍙嬪垪琛細涓夋�佷紭鍏堢骇 + 椤哄簭鎷兼帴鐢� useSelectedItems 缁熶竴鎵挎媴 */
+const { selectedCount, selectedItems: selectedFriends } = useSelectedItems<FriendLite>(
+  () => props.selectedIds,
+  () => props.lockedIds,
+  () => props.disabledIds,
+  () => props.hideIds,
+  byId
+)
+
+/** 鏄惁琚攣瀹� */
+function isLocked(friend: FriendLite): boolean {
+  return lockedSet.value.has(friend.id)
+}
+
+/** 鏄惁琚鐢細locked / hide 宸茶鍓嶇疆杩囨护锛屽墿涓嬬殑鎵嶇畻 disabled */
+function isDisabled(friend: FriendLite): boolean {
+  return !lockedSet.value.has(friend.id) && disabledSet.value.has(friend.id)
+}
+
+/** 鏄惁閫変腑锛歭ocked 瑙嗕负姘歌繙閫変腑 */
+function isSelected(friend: FriendLite): boolean {
+  return selectedSet.value.has(friend.id)
+}
+
+/** 鍦嗗舰鍕鹃�夋寚绀哄櫒鐨� class锛氶�変腑 / 閿佸畾璧扮豢搴曪紝绂佺敤鐏板簳锛屾湭閫夌┖蹇冨渾 */
+function getCheckClass(friend: FriendLite): string {
+  if (isLocked(friend) || isSelected(friend)) {
+    return 'bg-[#07c160] border border-solid border-[#07c160]'
+  }
+  if (isDisabled(friend)) {
+    return 'bg-[var(--ant-color-fill)] border border-solid border-[var(--ant-color-border)]'
+  }
+  return 'border border-solid border-[var(--ant-color-border)] bg-[var(--ant-color-bg-container)]'
+}
+
+/** 鍒囨崲閫変腑鎬侊細locked / disabled 涓嶅搷搴旓紱鍙虫爮 脳 绉婚櫎 / 琛� click 閮借蛋杩欓噷 */
+function handleToggle(friend: FriendLite) {
+  if (isLocked(friend) || isDisabled(friend)) {
+    return
+  }
+  const next = [...props.selectedIds]
+  const index = next.indexOf(friend.id)
+  if (index === -1) {
+    if (props.maxSize > 0 && selectedCount.value >= props.maxSize) {
+      message.error(`鏈�澶氶�夋嫨 ${props.maxSize} 浣嶅ソ鍙媊)
+      return
+    }
+    next.push(friend.id)
+  } else {
+    next.splice(index, 1)
+  }
+  emit('update:selectedIds', next)
+}
+</script>
+
+<template>
+  <!--
+    濂藉弸閫夋嫨闈㈡澘锛氱敤浜庛�屾柊寤虹兢鑱� / 閭�璇峰ソ鍙� / 鎺ㄨ崘鏃跺垱寤鸿亰澶┿�嶇瓑濂藉弸閫夋嫨鍦烘櫙
+    - 宸︼細鎼滅储 + 濂藉弸鍒楄〃锛堝渾褰㈠嬀閫夛級
+    - 鍙筹細宸查�夋暟鏍囬 + 宸查�夊ソ鍙嬪垪琛紙鎸夌偣鍑婚『搴忥級
+    - Panel 涓嶅甫 el-dialog 澹筹紱dialog 鐢变笟鍔″3鎸佹湁
+    - 涓夋�佽涔夛細hide > locked > disabled锛堣瑙� contract锛�
+  -->
+  <div class="flex h-full">
+    <!-- 宸︽爮 -->
+    <div
+      class="flex flex-col flex-1 min-w-0 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 min-h-0">
+        <PagedScroller v-if="filtered.length > 0" :items="filtered" :page-size="30" item-key="id">
+          <template #default="{ item }">
+            <div
+              :key="(item as FriendLite).id"
+              class="flex gap-2.5 items-center px-3 py-2 cursor-pointer hover:bg-[var(--ant-color-fill)]"
+              :class="{
+                'opacity-60 cursor-not-allowed hover:bg-transparent': isDisabled(item as FriendLite)
+              }"
+              @click="handleToggle(item as FriendLite)"
+            >
+              <!-- 鍦嗗舰鍕鹃�夋寚绀哄櫒锛氭湭閫夌伆鑹茬┖蹇冨渾锛岄�変腑瀹炲績寰俊缁� + 鐧藉鍕撅紱閿佸畾 / 绂佺敤璧扮伆搴� -->
+              <span
+                class="flex flex-shrink-0 justify-center items-center w-5 h-5 rounded-full transition-colors"
+                :class="getCheckClass(item as FriendLite)"
+              >
+                <Icon
+                  v-if="isSelected(item as FriendLite) || isLocked(item as FriendLite)"
+                  icon="ant-design:check-outlined"
+                  :size="12"
+                  color="#fff"
+                />
+              </span>
+              <UserAvatar
+                :id="(item as FriendLite).id"
+                :url="(item as FriendLite).avatar"
+                :name="(item as FriendLite).nickname"
+                :size="36"
+                :clickable="false"
+              />
+              <!-- 琛屽唴鍚嶅瓧锛氬娉ㄤ紭鍏堬紝鍒楄〃閲屼笉閲嶅灞曠ず鏄电О -->
+              <span
+                class="flex-1 min-w-0 overflow-hidden text-sm truncate text-[var(--ant-color-text)]"
+              >
+                {{ (item as FriendLite).displayName || (item as FriendLite).nickname }}
+              </span>
+            </div>
+          </template>
+        </PagedScroller>
+        <div v-else 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">
+      <!-- 鏍囬锛氬凡閫夋暟锛涢珮搴﹀榻愬乏渚� input default锛�32px锛夛紝淇濊瘉涓や晶绗竴椤硅捣鐐瑰悓姘村钩 -->
+      <div
+        class="flex-shrink-0 h-12 px-4 leading-[3rem] border-b border-b-solid text-13px text-[var(--ant-color-text-secondary)] border-[var(--ant-color-border-secondary)]"
+      >
+        宸查�夋嫨 {{ selectedCount }} 涓ソ鍙�
+      </div>
+
+      <!-- 宸查�夐瑙堬細鎸� selectedIds + lockedIds 鏁扮粍椤哄簭锛堢偣鍑婚『搴忥級灞曠ず -->
+      <div class="flex-1">
+        <div
+          v-for="friend in selectedFriends"
+          :key="friend.id"
+          class="flex gap-2.5 items-center px-4 py-2"
+        >
+          <UserAvatar
+            :id="friend.id"
+            :url="friend.avatar"
+            :name="friend.nickname"
+            :size="36"
+            :clickable="false"
+          />
+          <span
+            class="flex-1 min-w-0 overflow-hidden text-sm truncate text-[var(--ant-color-text)]"
+          >
+            {{ friend.displayName || friend.nickname }}
+          </span>
+          <!-- 閿佸畾椤逛笉娓叉煋 脳锛岄伩鍏嶈浠ヤ负鍙Щ闄� -->
+          <Icon
+            v-if="!isLocked(friend)"
+            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(friend)"
+          />
+        </div>
+        <div
+          v-if="selectedFriends.length === 0"
+          class="py-10 text-13px text-center text-[var(--ant-color-text-disabled)]"
+        >
+          璇蜂粠宸︿晶閫夋嫨濂藉弸
+        </div>
+      </div>
+
+      <!-- 涓氬姟澹冲棰濆鍐呭鐨勪綅缃紱FriendPickerPanel 涓绘祦鍦烘櫙涓嶉渶瑕� footer -->
+      <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>

--
Gitblit v1.9.3