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/message/reply-preview.vue |  234 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 234 insertions(+), 0 deletions(-)

diff --git a/src/views/im/home/pages/conversation/components/message/reply-preview.vue b/src/views/im/home/pages/conversation/components/message/reply-preview.vue
new file mode 100644
index 0000000..6ada968
--- /dev/null
+++ b/src/views/im/home/pages/conversation/components/message/reply-preview.vue
@@ -0,0 +1,234 @@
+<script lang="ts" setup>
+import { computed } from 'vue'
+
+import { IconifyIcon as Icon } from '#/packages/icons/src'
+import { formatFileSize } from '#/packages/utils/src'
+
+import { CardLineLabel } from '#/views/im/home/components/card'
+import { ImContentType } from '#/views/im/utils/constants'
+import { getClientConversationId } from '#/views/im/utils/db'
+import {
+  type AudioMessage,
+  type CardMessage,
+  type FaceMessage,
+  type FileMessage,
+  getFileIconInfo,
+  type ImageMessage,
+  type MaterialMessage,
+  parseMessage,
+  type QuoteMessage,
+  type TextMessage,
+  type VideoMessage
+} from '#/views/im/utils/message'
+import { formatSeconds } from '#/views/im/utils/time'
+import { getSenderDisplayName } from '#/views/im/utils/user'
+
+import { useConversationStore } from '../../../../store/conversationStore'
+import { useMessageStore } from '../../../../store/messageStore'
+
+defineOptions({ name: 'ImReplyPreview' })
+
+const props = withDefaults(
+  defineProps<{
+    /** 姘旀场鍐呬负 true 鏀寔鐐瑰嚮璺宠浆,杈撳叆鏉′负 false */
+    clickable?: boolean
+    /** 杈撳叆鏉′负 true 鏄剧ず 脳 鍏抽棴鎸夐挳 */
+    closable?: boolean
+    /** 鑷繁鍙戦�佺殑姘旀场涓� true锛屾妸绔栫嚎闀滃儚鍒板彸渚э紝涓庢皵娉″悓渚� */
+    mirrored?: boolean
+    quote: QuoteMessage
+  }>(),
+  {
+    clickable: false,
+    closable: false,
+    mirrored: false
+  }
+)
+
+const emit = defineEmits<{
+  close: []
+  locate: [messageId: number]
+}>()
+
+const MAX_TEXT_PREVIEW_LEN = 60 // 鏂囨湰鎽樿鍦ㄥ紩鐢ㄥ潡閲屽睍绀虹殑鏈�澶у瓧绗︽暟
+
+const conversationStore = useConversationStore()
+const messageStore = useMessageStore()
+
+/** 鍦ㄥ綋鍓嶄細璇濇秷鎭垪琛ㄩ噷鏌ユ壘鍘熸秷鎭�,浠呯敤浜庡疄鏃跺垽鏂槸鍚﹀凡鎾ゅ洖;鎽樿 / 缂╃暐鍥鹃兘浠� quote.content 鐩存帴娲剧敓 */
+const liveMessage = computed(() => {
+  const conversation = conversationStore.activeConversation
+  if (!conversation || !props.quote.messageId) {
+    return undefined
+  }
+  return messageStore
+    .getMessages(getClientConversationId(conversation.type, conversation.targetId))
+    .find((message) => message.id === props.quote.messageId)
+})
+
+/** 鍛戒腑鏈湴缂撳瓨涓� type === RECALL 鎵嶅垽瀹氫负宸叉挙鍥�;涓嶅湪缂撳瓨鐨勫綋蹇収浠嶆湁鏁� */
+const isRecalled = computed(() => liveMessage.value?.type === ImContentType.RECALL)
+
+/** 娓叉煋鏃跺疄鏃剁畻,涓庢皵娉′笂鏂规樉绀哄悕璧板悓涓�濂楄鍒�,閬垮厤澶囨敞鍙樻洿鍚庡紩鐢ㄥ潡闄堟棫 */
+const senderName = computed(() => {
+  const conversation = conversationStore.activeConversation
+  if (!conversation) {
+    return ''
+  }
+  return getSenderDisplayName(props.quote.senderId, conversation.type, conversation.targetId)
+})
+
+/** quote.content 瑙f瀽涓�娆$紦瀛橈紝璁╁涓� computed 澶嶇敤锛岄暱浼氳瘽姣忔潯寮曠敤姘旀场灏戜竴娆� JSON.parse */
+type AnyQuotePayload = Partial<
+  AudioMessage &
+    CardMessage &
+    FaceMessage &
+    FileMessage &
+    ImageMessage &
+    MaterialMessage &
+    TextMessage &
+    VideoMessage
+>
+const parsedPayload = computed(() => parseMessage<AnyQuotePayload>(props.quote.content))
+
+const isText = computed(() => props.quote.type === ImContentType.TEXT)
+const isFile = computed(() => props.quote.type === ImContentType.FILE)
+const isVoice = computed(() => props.quote.type === ImContentType.VOICE)
+const isCard = computed(() => props.quote.type === ImContentType.CARD)
+const isFace = computed(() => props.quote.type === ImContentType.FACE)
+const isMaterial = computed(() => props.quote.type === ImContentType.MATERIAL)
+
+/** 鏂囨湰瓒呰繃 MAX_TEXT_PREVIEW_LEN 鎴柇锛岄暱鍐呭涓嶆拺鐖嗗紩鐢ㄥ潡 */
+const textPreview = computed(() => {
+  const text = parsedPayload.value?.content ?? ''
+  return text.length <= MAX_TEXT_PREVIEW_LEN ? text : `${text.slice(0, Math.max(0, MAX_TEXT_PREVIEW_LEN))}鈥
+})
+
+/** 鏂囦欢 icon锛氭寜鎵╁睍鍚嶆寫鑹诧紝璺熶富姘旀场娓叉煋鍚屾簮 */
+const fileIcon = computed(() => getFileIconInfo(parsedPayload.value?.name))
+
+/** 缂╃暐鍥� URL锛氬浘鐗� / 瑙嗛 / 琛ㄦ儏璐村浘 / 棰戦亾绱犳潗灏侀潰浠� quote.content 鐩存帴鍙栵紝涓嶄緷璧栨湰鍦扮紦瀛� */
+const thumbnailUrl = computed<string | undefined>(() => {
+  if (isRecalled.value) {
+    return undefined
+  }
+  const { type } = props.quote
+  if (type === ImContentType.IMAGE) {
+    return parsedPayload.value?.thumbnailUrl || parsedPayload.value?.url
+  }
+  if (type === ImContentType.VIDEO || type === ImContentType.MATERIAL) {
+    return parsedPayload.value?.coverUrl
+  }
+  if (type === ImContentType.FACE) {
+    return parsedPayload.value?.url
+  }
+  return undefined
+})
+
+/** 浠� clickable 涓旀湭鎾ゅ洖鏃惰Е鍙戣烦杞� */
+function onClick() {
+  if (!props.clickable || isRecalled.value) {
+    return
+  }
+  emit('locate', props.quote.messageId)
+}
+</script>
+
+<template>
+  <!--
+    寮曠敤棰勮鍧楋紙瀵归綈寰俊 PC锛夛細2px 绔栫嚎浣滀负銆屽紩鐢ㄣ�嶈瑙夋爣璇� + 绱у噾鍐呭棰勮
+    - clickable=true锛堟皵娉″唴锛夛細鐐瑰嚮瑙﹀彂 locate emit锛涙挙鍥炴�佺鐢ㄨ烦杞�
+    - closable=true锛堣緭鍏ユ潯锛夛細灏鹃儴 脳 鍏抽棴鎸夐挳
+    - mirrored=true锛堣嚜宸卞彂閫佺殑姘旀场锛夛細绔栫嚎闀滃儚鍒板彸渚э紝涓庢皵娉″悓渚�
+    - 鍐呭棰勮锛堜笌涓绘皵娉″悓婧愶紝涓嶇缉鍐欐垚銆孾鏂囦欢]/[璇煶]銆嶏級锛�
+      - 鏂囨湰锛氭埅鏂悗绾枃瀛�
+      - 鍥剧墖 / 瑙嗛锛氱缉鐣ュ浘锛圛MAGE 鐢� thumbnailUrl/url锛孷IDEO 鐢� coverUrl锛�
+      - 鏂囦欢锛歠ile icon + 鏂囦欢鍚� + 澶у皬
+      - 璇煶锛歛udio icon + 鏃堕暱
+      - 鎾ゅ洖锛堝懡涓湰鍦扮紦瀛樹笖 type === RECALL锛夛細銆屽師娑堟伅宸叉挙鍥炪�嶆枩浣撶伆瀛�
+  -->
+  <div
+    class="flex w-fit gap-1.5 items-center min-w-0 py-0.5 text-12px text-[var(--ant-color-text-secondary)] rounded transition-colors"
+    :class="[
+      mirrored
+        ? 'pl-1 pr-2 border-r-2 border-r-solid border-r-[var(--ant-color-border)]'
+        : 'pl-2 pr-1 border-l-2 border-l-solid border-l-[var(--ant-color-border)]',
+      {
+        'cursor-pointer hover:text-[var(--ant-color-text)]': clickable && !isRecalled,
+        'hover:bg-[var(--ant-color-fill-secondary)]': (clickable && !isRecalled) || closable
+      }
+    ]"
+    @click="onClick"
+  >
+    <span class="flex-shrink-0">{{ senderName }}:</span>
+
+    <!-- 鎾ゅ洖闄嶇骇 -->
+    <span v-if="isRecalled" class="italic">鍘熸秷鎭凡鎾ゅ洖</span>
+
+    <!-- 鏂囨湰 -->
+    <span v-else-if="isText" class="min-w-0 line-clamp-2 break-words">{{ textPreview }}</span>
+
+    <!-- 鏂囦欢锛歩con + 鏂囦欢鍚� + 澶у皬 -->
+    <template v-else-if="isFile">
+      <Icon :icon="fileIcon.icon" :color="fileIcon.color" :size="14" class="flex-shrink-0" />
+      <span v-if="parsedPayload?.name" class="min-w-0 line-clamp-2 break-words">
+        {{ parsedPayload.name }}
+      </span>
+      <span
+        v-if="parsedPayload?.size"
+        class="flex-shrink-0 text-[var(--ant-color-text-placeholder)]"
+      >
+        {{ formatFileSize(parsedPayload.size) }}
+      </span>
+    </template>
+
+    <!-- 璇煶锛歛udio icon + 鏃堕暱 -->
+    <template v-else-if="isVoice">
+      <Icon icon="ant-design:audio-outlined" :size="14" class="flex-shrink-0" />
+      <span v-if="parsedPayload?.duration" class="flex-shrink-0">
+        {{ formatSeconds(parsedPayload.duration) }}
+      </span>
+    </template>
+
+    <!-- 鍚嶇墖 -->
+    <CardLineLabel
+      v-else-if="isCard"
+      :card="parsedPayload"
+      class="min-w-0 line-clamp-2 break-words"
+    />
+
+    <!-- 琛ㄦ儏璐村浘锛氱缉鐣ュ浘 + name锛堟棤 name 浠呮樉绀� [琛ㄦ儏]锛� -->
+    <template v-else-if="isFace">
+      <span class="flex-shrink-0">[琛ㄦ儏]</span>
+      <span v-if="parsedPayload?.name" class="min-w-0 line-clamp-2 break-words">
+        {{ parsedPayload.name }}
+      </span>
+    </template>
+
+    <!-- 棰戦亾绱犳潗锛歔棰戦亾] + 鏍囬 + 灏侀潰缂╃暐鍥� -->
+    <template v-else-if="isMaterial">
+      <span class="flex-shrink-0">[棰戦亾]</span>
+      <span v-if="parsedPayload?.title" class="min-w-0 line-clamp-2 break-words">
+        {{ parsedPayload.title }}
+      </span>
+    </template>
+
+    <!-- 鍥剧墖 / 瑙嗛 / 琛ㄦ儏璐村浘 / 棰戦亾绱犳潗缂╃暐鍥� -->
+    <img
+      v-if="thumbnailUrl"
+      :src="thumbnailUrl"
+      class="flex-shrink-0 object-cover w-6 h-6 rounded"
+      alt=""
+    />
+
+    <!-- 鍏抽棴鎸夐挳 -->
+    <button
+      v-if="closable"
+      type="button"
+      class="flex-shrink-0 inline-flex items-center justify-center w-4 h-4 cursor-pointer rounded-full bg-transparent border-none text-[var(--ant-color-text-secondary)] hover:bg-[var(--ant-color-fill)] hover:text-[var(--ant-color-text)]"
+      @click.stop="emit('close')"
+    >
+      <Icon icon="ant-design:close-outlined" :size="10" />
+    </button>
+  </div>
+</template>

--
Gitblit v1.9.3