From b64a0deae5b5d33f9e20671a68936b27f0b9b00b Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 21 七月 2026 18:03:03 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_pro2.0' into dev_pro2.0

---
 src/views/im/manager/message/content-preview.vue |  254 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 254 insertions(+), 0 deletions(-)

diff --git a/src/views/im/manager/message/content-preview.vue b/src/views/im/manager/message/content-preview.vue
new file mode 100644
index 0000000..c9b3fdb
--- /dev/null
+++ b/src/views/im/manager/message/content-preview.vue
@@ -0,0 +1,254 @@
+<script lang="ts" setup>
+import { computed } from 'vue';
+
+import { getDictObj } from '#/packages/effects/hooks/src';
+import { IconifyIcon } from '#/packages/icons/src';
+import { formatFileSize, openSafeUrl } from '#/packages/utils/src';
+
+import { Image } from 'ant-design-vue';
+
+import { CardLineLabel } from '#/views/im/home/components/card';
+import { MESSAGE_MERGE_PREVIEW_LINES } from '#/views/im/utils/config';
+import {
+  ImContentType,
+  ImRtcCallEndReason,
+  ImRtcCallMediaType,
+  isFriendChatTip,
+  isGroupNotification,
+  isRtcCallTip,
+} from '#/views/im/utils/constants';
+import {
+  buildFacePreviewText,
+  summarizeMessageContent,
+} from '#/views/im/utils/conversation';
+import {
+  getFileIconInfo,
+  parseMessage,
+  parseRtcCallPayload,
+  resolveFriendNotificationText,
+  resolveGroupNotificationText,
+} from '#/views/im/utils/message';
+import { formatSeconds } from '#/views/im/utils/time';
+
+defineOptions({ name: 'ImManagerMessageContentPreview' });
+
+const props = defineProps<{
+  content?: string;
+  senderNickname?: string;
+  type?: number;
+}>();
+
+const payload = computed<null | Record<string, any>>(() =>
+  parseMessage<Record<string, any>>(props.content ?? ''),
+);
+
+const textContent = computed(() => payload.value?.content || '');
+
+const fileIconInfo = computed(() => getFileIconInfo(payload.value?.name));
+
+const cardPayload = computed(() => {
+  if (props.type !== ImContentType.CARD || !payload.value) {
+    return undefined;
+  }
+  return {
+    ...payload.value,
+    name: payload.value.name || payload.value.nickname || payload.value.userId || '',
+  };
+});
+
+const mergePreviewLines = computed(() => {
+  const messages = payload.value?.messages;
+  if (!Array.isArray(messages)) {
+    return [];
+  }
+  return messages
+    .slice(0, MESSAGE_MERGE_PREVIEW_LINES)
+    .map((item) => `${item.senderNickname}锛�${summarizeMessageContent(item)}`);
+});
+
+const fallbackText = computed(() => {
+  const raw = props.content || '';
+  if (!raw) {
+    return '';
+  }
+  try {
+    const parsed = JSON.parse(raw);
+    if (parsed && typeof parsed === 'object' && parsed.content) {
+      return String(parsed.content);
+    }
+  } catch {}
+  return raw;
+});
+
+const friendTipText = computed(() =>
+  resolveFriendNotificationText({ type: props.type }),
+);
+
+const groupTipText = computed(() =>
+  resolveGroupNotificationText(
+    { type: props.type, content: props.content },
+    (id) => `鐢ㄦ埛(${id})`,
+    props.senderNickname,
+  ),
+);
+
+const rtcCallTipText = computed(() => {
+  const rtcPayload = parseRtcCallPayload(props.content);
+  if (!rtcPayload) {
+    return '';
+  }
+  const mediaLabel =
+    rtcPayload.mediaType === ImRtcCallMediaType.VIDEO ? '瑙嗛' : '璇煶';
+  if (props.type === ImContentType.RTC_CALL_START) {
+    const inviter =
+      rtcPayload.inviterNickname?.trim() ||
+      `鐢ㄦ埛(${rtcPayload.inviterUserId ?? ''})`;
+    return `${inviter} 鍙戣捣浜�${mediaLabel}閫氳瘽`;
+  }
+  const segments = [`${mediaLabel}閫氳瘽宸茬粨鏉焋];
+  if (
+    rtcPayload.endReason &&
+    rtcPayload.endReason !== ImRtcCallEndReason.HANGUP
+  ) {
+    const reason = getDictObj(
+      'im_rtc_call_end_reason',
+      String(rtcPayload.endReason),
+    )?.label;
+    if (reason) {
+      segments.push(reason);
+    }
+  }
+  const duration = rtcPayload.durationSeconds ?? 0;
+  if (duration > 0) {
+    segments.push(`鏃堕暱 ${formatSeconds(duration)}`);
+  }
+  return segments.join('锛�');
+});
+
+/** 鎵撳紑瑙嗛 */
+function openVideo() {
+  openSafeUrl(payload.value?.url);
+}
+</script>
+
+<template>
+  <span v-if="type === ImContentType.TEXT" class="whitespace-pre-wrap break-all">
+    {{ textContent }}
+  </span>
+
+  <Image
+    v-else-if="type === ImContentType.IMAGE && payload"
+    :height="60"
+    :src="payload.thumbnailUrl || payload.url"
+    :width="60"
+  />
+
+  <span
+    v-else-if="type === ImContentType.FILE && payload"
+    class="inline-flex items-center gap-1.5"
+  >
+    <IconifyIcon
+      :color="fileIconInfo.color"
+      :icon="fileIconInfo.icon"
+      class="size-4"
+    />
+    <span class="max-w-52 truncate">{{ payload.name }}</span>
+    <span v-if="payload.size > 0" class="text-xs text-gray-400">
+      {{ formatFileSize(payload.size) }}
+    </span>
+  </span>
+
+  <span
+    v-else-if="type === ImContentType.VOICE && payload"
+    class="inline-flex items-center gap-1.5"
+  >
+    <IconifyIcon class="size-4" icon="ant-design:audio-outlined" />
+    <span>{{ formatSeconds(payload.duration || 0) }}</span>
+  </span>
+
+  <span
+    v-else-if="type === ImContentType.VIDEO && payload"
+    class="inline-flex items-center gap-1.5"
+  >
+    <button
+      v-if="payload.coverUrl"
+      class="relative size-[60px] overflow-hidden rounded border-0 p-0"
+      type="button"
+      @click="openVideo"
+    >
+      <img :src="payload.coverUrl" class="size-full object-cover" />
+      <IconifyIcon
+        class="absolute inset-0 m-auto size-6 text-white drop-shadow"
+        icon="ant-design:play-circle-filled"
+      />
+    </button>
+    <span v-else class="inline-flex items-center gap-1.5">
+      <IconifyIcon class="size-4" icon="ant-design:video-camera-filled" />
+      <span>[瑙嗛]</span>
+    </span>
+    <span v-if="payload.duration" class="text-xs text-gray-400">
+      {{ formatSeconds(payload.duration) }}
+    </span>
+    <span v-if="payload.size > 0" class="text-xs text-gray-400">
+      {{ formatFileSize(payload.size) }}
+    </span>
+  </span>
+
+  <CardLineLabel
+    v-else-if="type === ImContentType.CARD && cardPayload"
+    :card="cardPayload"
+    :icon-size="16"
+  />
+
+  <span
+    v-else-if="type === ImContentType.FACE && payload"
+    class="inline-flex items-center gap-1.5"
+  >
+    <img
+      v-if="payload.url"
+      :alt="payload.name || '琛ㄦ儏'"
+      :src="payload.url"
+      class="size-[30px] rounded object-contain"
+      draggable="false"
+    />
+    <span>{{ buildFacePreviewText(payload) }}</span>
+  </span>
+
+  <span
+    v-else-if="type === ImContentType.MERGE && payload"
+    class="inline-flex flex-col gap-0.5 align-middle"
+  >
+    <span>[鑱婂ぉ璁板綍] {{ payload.title }}</span>
+    <span
+      v-for="(line, index) in mergePreviewLines"
+      :key="index"
+      class="truncate text-xs text-gray-400"
+    >
+      {{ line }}
+    </span>
+  </span>
+
+  <span v-else-if="type === ImContentType.RECALL" class="text-xs text-gray-400">
+    [娑堟伅宸叉挙鍥瀅
+  </span>
+  <span v-else-if="type === ImContentType.READ" class="text-xs text-gray-400">
+    [宸茶鍥炴墽]
+  </span>
+  <span v-else-if="type === ImContentType.RECEIPT" class="text-xs text-gray-400">
+    [鍥炴墽]
+  </span>
+  <span v-else-if="isGroupNotification(type ?? -1)" class="text-xs text-gray-400">
+    {{ groupTipText }}
+  </span>
+  <span v-else-if="isFriendChatTip(type ?? -1)" class="text-xs text-gray-400">
+    {{ friendTipText }}
+  </span>
+  <span
+    v-else-if="isRtcCallTip(type ?? -1)"
+    class="inline-flex items-center gap-1.5 text-xs text-gray-400"
+  >
+    <IconifyIcon class="size-4 rotate-[135deg]" icon="ant-design:phone-outlined" />
+    <span>{{ rtcCallTipText }}</span>
+  </span>
+  <span v-else class="whitespace-pre-wrap break-all">{{ fallbackText }}</span>
+</template>

--
Gitblit v1.9.3