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/input/face-picker.vue |  353 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 353 insertions(+), 0 deletions(-)

diff --git a/src/views/im/home/pages/conversation/components/input/face-picker.vue b/src/views/im/home/pages/conversation/components/input/face-picker.vue
new file mode 100644
index 0000000..b06de9e
--- /dev/null
+++ b/src/views/im/home/pages/conversation/components/input/face-picker.vue
@@ -0,0 +1,353 @@
+<script lang="ts" setup>
+import type { ImFacePackApi } from '#/api/im/face/pack'
+import type { ImFaceUserItemApi } from '#/api/im/face/userItem'
+
+import { computed, onUnmounted, ref, useTemplateRef, watch } from 'vue'
+
+import { IconifyIcon as Icon } from '#/packages/icons/src'
+
+import { message, Tooltip } from 'ant-design-vue'
+
+import { uploadFile } from '#/api/infra/file'
+import { useFaceStore } from '#/views/im/home/store/faceStore'
+import { IM_EMOJI_LIST } from '#/views/im/utils/emoji'
+import { probeImageSize } from '#/views/im/utils/image'
+
+defineOptions({ name: 'ImFacePicker' })
+
+const props = withDefaults(
+  defineProps<{
+    /** full锛歟moji + 涓汉琛ㄦ儏 + 绯荤粺鍖咃紙鑱婂ぉ涓昏緭鍏ョ敤锛夛紱emoji锛氫粎 emoji锛堢暀瑷� / 璇勮鍦烘櫙锛� */
+    mode?: FacePickerMode
+    visible: boolean
+  }>(),
+  { mode: 'full' }
+)
+
+const emit = defineEmits<{
+  /** 閫変腑 Unicode emoji锛堝 馃榾锛夛紝璋冪敤鏂瑰簲鎻掑叆鍒拌緭鍏ユ璧� TEXT 閫氶亾 */
+  selectEmoji: [emoji: string]
+  /** 閫変腑琛ㄦ儏璐村浘锛岃皟鐢ㄦ柟搴旇蛋 FACE 娑堟伅鍙戦�� */
+  selectFace: [face: { height: number; name?: string; url: string; width: number; }]
+  'update:visible': [value: boolean]
+}>()
+
+/** 闈㈡澘妯″紡 */
+type FacePickerMode = 'emoji' | 'full'
+
+const rootRef = useTemplateRef<HTMLDivElement>('rootRef')
+const uploadInputRef = useTemplateRef<HTMLInputElement>('uploadInputRef')
+
+const faceStore = useFaceStore()
+
+// tab 鏍囪瘑甯搁噺锛沺ack:N 绫荤敤 packTabKey() 鎷煎嚭锛岄伩鍏嶆暎钀藉瓧绗︿覆瀛楅潰閲�
+const FACE_TAB = {
+  EMOJI: 'emoji',
+  MINE: 'mine'
+} as const
+const packTabKey = (packId: number) => `pack:${packId}`
+
+const activeTab = ref<string>(FACE_TAB.EMOJI) // 褰撳墠婵�娲荤殑 tab
+
+/** 鏄惁瀹屾暣妯″紡锛堝惈涓汉 / 绯荤粺鍖� tab锛� */
+const isFullMode = computed(() => props.mode === 'full')
+
+const uploading = ref(false) // 涓婁紶涓爣璁帮紝閬垮厤杩炵画鐐瑰嚮瑙﹀彂骞跺彂涓婁紶
+
+/** 閫� emoji 瀛楃锛氭彃鍒拌緭鍏ユ锛涢�夊畬涓嶅叧闈㈡澘锛屾柟渚跨敤鎴疯繛鍙戝涓� */
+function handleSelectEmoji(emoji: string) {
+  emit('selectEmoji', emoji)
+}
+
+/** 閫変釜浜鸿〃鎯咃細鐩存帴鍙戯紱鐐瑰畬鍏抽潰鏉匡紝瀵归綈寰俊 */
+function handleSelectFaceUserItem(item: ImFaceUserItemApi.FaceUserItem) {
+  emit('selectFace', { url: item.url, width: item.width, height: item.height, name: item.name })
+  emit('update:visible', false)
+}
+
+/** 閫夌郴缁熻〃鎯呭寘鍐呰〃鎯咃細鐩存帴鍙戯紱鐐瑰畬鍏抽潰鏉� */
+function handleSelectPackItem(item: ImFacePackApi.FacePackUserItem) {
+  emit('selectFace', { url: item.url, width: item.width, height: item.height, name: item.name })
+  emit('update:visible', false)
+}
+
+/** 闀挎寜 / 鍙抽敭鍒犻櫎涓汉琛ㄦ儏 */
+async function handleDeleteUserItem(item: ImFaceUserItemApi.FaceUserItem) {
+  if (!confirm('纭鍒犻櫎璇ヨ〃鎯咃紵')) {
+    return
+  }
+  await faceStore.removeFaceUserItem(item.id)
+}
+
+/** 鐐� + 瑙﹀彂鏂囦欢閫夋嫨 */
+function onUploadClick() {
+  uploadInputRef.value?.click()
+}
+
+/** 鏂囦欢閫夊畬鍗充笂浼狅紝鎴愬姛鍚庡啓鍏� faceStore 涓汉琛ㄦ儏鍒楄〃 */
+async function onUploadPicked(e: Event) {
+  const input = e.target as HTMLInputElement
+  const file = input.files?.[0]
+  input.value = ''
+  if (!file) {
+    return
+  }
+  uploading.value = true
+  let size: { height: number; width: number; }
+  try {
+    size = await probeImageSize(file)
+  } catch (error) {
+    console.warn('[IM] 瑙f瀽涓汉琛ㄦ儏澶辫触', error)
+    message.error('鍥剧墖瑙f瀽澶辫触')
+    uploading.value = false
+    return
+  }
+
+  try {
+    const url = await uploadFile({ file })
+    if (!url) {
+      message.error('涓婁紶澶辫触')
+      return
+    }
+    const payload = { url, width: size.width, height: size.height }
+    await faceStore.addFaceUserItem(payload)
+  } finally {
+    uploading.value = false
+  }
+}
+
+/** 闈㈡澘灞曞紑鏃舵媺鏁版嵁 + 鎸傚叏灞� click锛沵ode='emoji' 鏃朵笉鎷夌郴缁熷寘 / 涓汉琛ㄦ儏 */
+watch(
+  () => props.visible,
+  (visible) => {
+    if (visible) {
+      document.addEventListener('click', handleDocumentClick)
+      if (isFullMode.value) {
+        // 绯荤粺鍖呴�氬父宸茶 home onMounted 棰勬媺杩囷紱ensureXxx 鍐呴儴 promise 缂撳瓨閬垮厤閲嶅璇锋眰
+        void faceStore.ensureFacePackList()
+        void faceStore.ensureFaceUserItemList()
+      }
+    } else {
+      document.removeEventListener('click', handleDocumentClick)
+    }
+  },
+  { immediate: true }
+)
+
+/** 鐐瑰嚮闈㈡澘澶栭儴鍏抽棴 */
+function handleDocumentClick(e: MouseEvent) {
+  if (!props.visible || !rootRef.value) {
+    return
+  }
+  if (!rootRef.value.contains(e.target as Node)) {
+    emit('update:visible', false)
+  }
+}
+
+onUnmounted(() => {
+  document.removeEventListener('click', handleDocumentClick)
+})
+</script>
+
+<template>
+  <!--
+    琛ㄦ儏闈㈡澘锛堝 tab锛夛細emoji / 涓汉琛ㄦ儏 / N 涓郴缁熻〃鎯呭寘
+    - 瀵归綈寰俊 PC锛氬簳閮� tab 鏍忓垏鎹㈤潰鏉垮唴瀹癸紱emoji 淇濇寔 Unicode锛堜粛鐢� TEXT 閫氶亾鍙戦�侊級
+    - 涓汉琛ㄦ儏 / 绯荤粺琛ㄦ儏璧� FACE 鍐呭绫诲瀷锛岄�氳繃 selectFace 浜嬩欢鐢辫皟鐢ㄦ柟璧� sendRaw 鍙戦��
+    - mode='emoji' 鏃跺彧鏄剧ず emoji tab + 闅愯棌搴曢儴 tab 鏍忥紝缁欑暀瑷� / 璇勮杩欑被鍙彂鏂囨湰鐨勫満鏅敤
+    - 瀹氫綅鐢辫皟鐢ㄦ柟鍐冲畾锛堥�氬父娴湪琛ㄦ儏鎸夐挳涓婃柟锛�
+  -->
+  <div
+    v-if="visible"
+    ref="rootRef"
+    class="im-popover-arrow absolute z-100 flex flex-col w-[380px] rounded-md bg-[var(--ant-color-bg-container)] shadow-[0_4px_16px_rgba(0,0,0,0.12)]"
+    @click.stop
+  >
+    <!-- 涓诲唴瀹瑰尯锛氶珮搴﹀浐瀹� + 鍚� tab 鐢� v-show 鍒囨崲锛岄伩鍏嶆瘡娆″垏 tab 閲嶅缓 scrollbar 閫犳垚婊氬姩浣嶇疆涓㈠け -->
+    <div class="relative h-[300px] overflow-hidden">
+      <!-- emoji 缃戞牸 -->
+      <div v-show="activeTab === FACE_TAB.EMOJI" style="height: 300px; overflow-y: auto">
+        <div class="grid grid-cols-10 gap-0.5 p-2">
+          <button
+            v-for="emoji in IM_EMOJI_LIST"
+            :key="emoji"
+            class="p-1 text-xl leading-none bg-transparent border-none rounded cursor-pointer transition-colors hover:bg-[var(--ant-color-fill)]"
+            type="button"
+            @click="handleSelectEmoji(emoji)"
+          >
+            {{ emoji }}
+          </button>
+        </div>
+      </div>
+
+      <!-- 涓汉琛ㄦ儏锛�5 鍒楁柟鏍硷紝鏃犲悕瀛楁爣绛撅紱鏈熬銆�+銆嶄笂浼� -->
+      <div v-if="isFullMode" v-show="activeTab === FACE_TAB.MINE" style="height: 300px; overflow-y: auto">
+        <div class="grid grid-cols-5 gap-2 p-3">
+          <!-- 涓婁紶鍏ュ彛鍥哄畾鏀剧涓�鏍硷紱dashed border 涓庤〃鎯呮牸瀛愬尯鍒嗚瑙夎涔夛紝瀵归綈 el-upload 瑙傛劅 -->
+          <button
+            class="aspect-square flex items-center justify-center rounded-md cursor-pointer transition-colors border border-dashed border-[var(--ant-color-border)] bg-transparent text-[var(--ant-color-text-placeholder)] hover:border-[var(--ant-color-primary)] hover:text-[var(--ant-color-primary)] disabled:cursor-not-allowed disabled:text-[var(--ant-color-text-disabled)]"
+            type="button"
+            :disabled="uploading"
+            :title="uploading ? '涓婁紶涓��' : '涓婁紶鍥剧墖鍒颁釜浜鸿〃鎯�'"
+            @click="onUploadClick"
+          >
+            <Icon
+              :icon="uploading ? 'eos-icons:bubble-loading' : 'ant-design:plus-outlined'"
+              :size="22"
+            />
+          </button>
+          <div
+            v-for="item in faceStore.faceUserItems"
+            :key="item.id"
+            class="im-face-grid-cell group relative aspect-square flex items-center justify-center rounded-md bg-[var(--ant-color-fill-tertiary)] cursor-pointer transition-colors hover:bg-[var(--ant-color-fill)]"
+            :title="item.name || '鐐瑰嚮鍙戦�� / 鍙抽敭鍒犻櫎'"
+            @click="handleSelectFaceUserItem(item)"
+            @contextmenu.prevent="handleDeleteUserItem(item)"
+          >
+            <img
+              :src="item.url"
+              :alt="item.name || '琛ㄦ儏'"
+              class="max-w-full max-h-full object-contain"
+              draggable="false"
+            />
+          </div>
+          <div
+            v-if="faceStore.faceUserItems.length === 0"
+            class="col-span-5 flex items-center justify-center py-12 text-sm text-[var(--ant-color-text-placeholder)]"
+          >
+            杩樻病鏈変釜浜鸿〃鎯咃紝鐐� + 涓婁紶 / 鍦ㄨ亰澶╅噷闀挎寜娑堟伅娣诲姞
+          </div>
+        </div>
+      </div>
+
+      <!-- 绯荤粺琛ㄦ儏鍖咃細5 鍒楁柟鏍� + 涓嬫柟琛ㄦ儏鍚嶏紙鏃� name 鏃朵笉鏄剧ず鏍囩锛屼繚鎸侀珮搴︿竴鑷达級 -->
+      <template v-if="isFullMode">
+        <div
+          v-for="pack in faceStore.facePacks"
+          v-show="activeTab === packTabKey(pack.id)"
+          :key="pack.id"
+          style="height: 300px; overflow-y: auto"
+        >
+          <div class="grid grid-cols-5 gap-2 p-3">
+            <div
+              v-for="item in pack.items"
+              :key="item.id"
+              class="im-face-grid-cell flex flex-col items-center gap-1 cursor-pointer rounded-md p-1 transition-colors hover:bg-[var(--ant-color-fill)]"
+              :title="item.name || ''"
+              @click="handleSelectPackItem(item)"
+            >
+              <div
+                class="aspect-square w-full flex items-center justify-center bg-[var(--ant-color-fill-tertiary)] rounded"
+              >
+                <img
+                  :src="item.url"
+                  :alt="item.name || '琛ㄦ儏'"
+                  class="max-w-full max-h-full object-contain"
+                  draggable="false"
+                />
+              </div>
+              <div
+                class="h-4 text-xs text-[var(--ant-color-text-secondary)] truncate w-full text-center leading-4"
+              >
+                {{ item.name || '' }}
+              </div>
+            </div>
+            <div
+              v-if="pack.items.length === 0"
+              class="col-span-5 flex items-center justify-center py-12 text-sm text-[var(--ant-color-text-placeholder)]"
+            >
+              璇ヨ〃鎯呭寘鏆傛棤琛ㄦ儏
+            </div>
+          </div>
+        </div>
+      </template>
+    </div>
+
+    <!-- 搴曢儴 tab 鏍忥細[ emoji / 涓汉 / 绯荤粺鍖� 1..N ]锛沵ode='emoji' 鏃堕殣钘� -->
+    <div
+      v-if="isFullMode"
+      class="flex flex-shrink-0 items-center gap-1 px-2 py-1.5 border-t border-t-solid border-[var(--ant-color-border-secondary)]"
+    >
+      <Tooltip title="Emoji 琛ㄦ儏" placement="top" :mouse-enter-delay="300">
+        <button
+          class="inline-flex items-center justify-center w-[30px] h-[30px] border-none rounded bg-transparent cursor-pointer transition-colors hover:bg-[var(--ant-color-fill)]"
+          :class="
+            activeTab === FACE_TAB.EMOJI
+              ? 'bg-[var(--ant-color-fill)] text-[var(--ant-color-primary)]'
+              : 'text-[var(--ant-color-text)]'
+          "
+          type="button"
+          @click="activeTab = FACE_TAB.EMOJI"
+        >
+          <Icon icon="ant-design:smile-outlined" :size="18" />
+        </button>
+      </Tooltip>
+      <Tooltip title="涓汉琛ㄦ儏" placement="top" :mouse-enter-delay="300">
+        <button
+          class="inline-flex items-center justify-center w-[30px] h-[30px] border-none rounded bg-transparent cursor-pointer transition-colors hover:bg-[var(--ant-color-fill)]"
+          :class="
+            activeTab === FACE_TAB.MINE
+              ? 'bg-[var(--ant-color-fill)] text-[var(--ant-color-primary)]'
+              : 'text-[var(--ant-color-text)]'
+          "
+          type="button"
+          @click="activeTab = FACE_TAB.MINE"
+        >
+          <Icon icon="ant-design:heart-outlined" :size="18" />
+        </button>
+      </Tooltip>
+      <Tooltip
+        v-for="pack in faceStore.facePacks"
+        :key="pack.id"
+        :title="pack.name"
+        placement="top"
+        :mouse-enter-delay="300"
+      >
+        <button
+          class="inline-flex items-center justify-center w-[30px] h-[30px] border-none rounded bg-transparent cursor-pointer transition-colors hover:bg-[var(--ant-color-fill)]"
+          :class="
+            activeTab === packTabKey(pack.id)
+              ? 'bg-[var(--ant-color-fill)] text-[var(--ant-color-primary)]'
+              : 'text-[var(--ant-color-text)]'
+          "
+          type="button"
+          @click="activeTab = packTabKey(pack.id)"
+        >
+          <img
+            v-if="pack.icon"
+            :src="pack.icon"
+            :alt="pack.name"
+            class="w-[18px] h-[18px] object-contain"
+            draggable="false"
+          />
+          <Icon v-else icon="ant-design:appstore-outlined" :size="18" />
+        </button>
+      </Tooltip>
+    </div>
+
+    <!-- 闅愯棌鐨勬枃浠堕�夋嫨鍣紙鐐� + 鏃惰Е鍙戯級 -->
+    <input
+      v-if="isFullMode"
+      ref="uploadInputRef"
+      type="file"
+      accept="image/*"
+      hidden
+      @change="onUploadPicked"
+    />
+  </div>
+</template>
+
+<style scoped>
+/* 搴曢儴灏忎笁瑙掞細鎸囧悜瑙﹀彂鍥炬爣锛屼豢寰俊 PC 姘旀场鎸囬拡锛沴eft 鍋忕Щ瀵瑰簲琛ㄦ儏鎸夐挳锛堝伐鍏锋爮 1st icon锛� */
+.im-popover-arrow::after {
+  content: '';
+  position: absolute;
+  top: calc(100% - 1px);
+  left: 10px;
+  border-style: solid;
+  border-width: 6px 6px 0 6px;
+  border-color: var(--ant-color-bg-container) transparent transparent transparent;
+  filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.08));
+}
+
+</style>

--
Gitblit v1.9.3