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