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/material-bubble.vue | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 170 insertions(+), 0 deletions(-)
diff --git a/src/views/im/home/pages/conversation/components/message/material-bubble.vue b/src/views/im/home/pages/conversation/components/message/material-bubble.vue
new file mode 100644
index 0000000..9f75df4
--- /dev/null
+++ b/src/views/im/home/pages/conversation/components/message/material-bubble.vue
@@ -0,0 +1,170 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue'
+
+import { IconifyIcon as Icon } from '#/packages/icons/src'
+import { openSafeUrl } from '#/packages/utils/src'
+
+import { Modal, Spin } from 'ant-design-vue'
+
+import { getChannelMaterial } from '#/api/im/channel/material'
+import { useChannelStore } from '#/views/im/home/store/channelStore'
+import { useConversationStore } from '#/views/im/home/store/conversationStore'
+import { ImConversationType } from '#/views/im/utils/constants'
+import { type MaterialMessage, parseMessage } from '#/views/im/utils/message'
+
+const props = defineProps<{
+ content: string
+}>()
+
+const conversationStore = useConversationStore()
+const channelStore = useChannelStore()
+
+/** 褰撳墠鏄惁鍦ㄥ叕浼楀彿 / 棰戦亾浼氳瘽閲岋細鍐冲畾璧板ぇ鍗$墖杩樻槸绱у噾杞彂鍗$墖 */
+const isChannelView = computed(
+ () => conversationStore.activeConversation?.type === ImConversationType.CHANNEL
+)
+
+/** 鍙嶅簭鍒楀寲 content JSON 涓� payload 瀵硅薄 */
+const payload = computed<MaterialMessage>(
+ () => parseMessage<MaterialMessage>(props.content) ?? {}
+)
+
+/** 鏉ユ簮棰戦亾锛涚揣鍑戝崱搴曢儴娓叉煋澶村儚 + 鍚嶇О */
+const sourceChannel = computed(() =>
+ payload.value.channelId ? channelStore.getChannel(payload.value.channelId) : undefined
+)
+
+const detailVisible = ref(false)
+const detailLoading = ref(false)
+const detailHtml = ref('')
+
+// 鐐瑰嚮琛屼负锛歶rl 闈炵┖璺冲閾撅紱涓虹┖鍒欐寜 payload.materialId 鎷夊瘜鏂囨湰姝f枃锛屽叏灞� dialog 娓叉煋
+const onClick = async () => {
+ if (payload.value.url) {
+ openSafeUrl(payload.value.url)
+ return
+ }
+ if (!payload.value.materialId) {
+ return
+ }
+ detailVisible.value = true
+ detailLoading.value = true
+ detailHtml.value = ''
+ try {
+ const material = await getChannelMaterial(payload.value.materialId)
+ detailHtml.value = material?.content ?? ''
+ } catch (error) {
+ console.error('[Material] 鎷夊彇姝f枃澶辫触', error)
+ } finally {
+ detailLoading.value = false
+ }
+}
+</script>
+
+<template>
+ <!-- 鍏紬鍙蜂細璇濆唴澶у崱鐗囷細瀵归綈寰俊鍏紬鍙峰崟鍥炬枃鍗★紙灏侀潰 9:5 + 涓嬫柟鐧藉簳鍔犵矖鏍囬鏉★級 -->
+ <div
+ v-if="isChannelView"
+ class="material-card cursor-pointer w-full overflow-hidden rounded-lg bg-[var(--ant-color-bg-container)] border border-solid border-[var(--im-border-color-lighter)]"
+ @click="onClick"
+ >
+ <img v-if="payload.coverUrl" class="block w-full h-[200px] object-cover" :src="payload.coverUrl" />
+ <div class="px-3.5 py-3 text-15px font-600 leading-[1.4] text-[var(--ant-color-text)] line-clamp-2">
+ {{ payload.title || '(鏃犳爣棰�)' }}
+ </div>
+ </div>
+
+ <!-- 绉佽亰 / 缇よ亰閲岃杞彂鐨勭礌鏉愮揣鍑戝崱鐗囷細鏍囬 + 鎽樿鍦ㄥ乏銆佸皬灏侀潰鍦ㄥ彸銆佸簳閮ㄩ閬撳ご鍍� + 鍚嶇О锛堝榻愬井淇″叕浼楀彿杞彂鍗★級 -->
+ <div
+ v-else
+ class="material-card cursor-pointer flex flex-col w-[260px] px-3.5 pt-3 pb-2.5 rounded-lg bg-[var(--ant-color-bg-container)] border border-solid border-[var(--im-border-color-lighter)]"
+ @click="onClick"
+ >
+ <div class="flex gap-2.5 items-start">
+ <div class="flex flex-1 flex-col gap-1.5 min-w-0">
+ <div class="text-15px font-600 leading-[1.4] text-[var(--ant-color-text)] line-clamp-2 break-all">
+ {{ payload.title || '(鏃犳爣棰�)' }}
+ </div>
+ <div
+ v-if="payload.summary"
+ class="text-12px leading-[1.5] text-[var(--ant-color-text-secondary)] line-clamp-2 break-all"
+ >
+ {{ payload.summary }}
+ </div>
+ </div>
+ <img
+ v-if="payload.coverUrl"
+ class="flex-shrink-0 w-[60px] h-[60px] object-cover rounded bg-[var(--ant-color-fill-secondary)]"
+ :src="payload.coverUrl"
+ />
+ </div>
+ <div class="flex items-center gap-1.5 mt-2.5 pt-2 border-t border-t-solid border-[var(--im-border-color-lighter)] text-12px text-[var(--ant-color-text-secondary)]">
+ <img
+ v-if="sourceChannel?.avatar"
+ class="w-4 h-4 rounded-full object-cover flex-shrink-0"
+ :src="sourceChannel.avatar"
+ />
+ <Icon v-else icon="ep:promotion" :size="14" />
+ <span class="truncate">{{ sourceChannel?.name || '棰戦亾娑堟伅' }}</span>
+ </div>
+ </div>
+
+ <!-- 瀵屾枃鏈鎯咃細鍏ㄥ睆寮圭獥鎸夐渶鎸傝浇锛屽叧闂悗閲婃斁瀵屾枃鏈� DOM -->
+ <Modal
+ v-model:open="detailVisible"
+ :title="payload.title || '璇︽儏'"
+ :footer="null"
+ width="100vw"
+ wrap-class-name="im-material-detail-modal"
+ destroy-on-close
+ >
+ <Spin :spinning="detailLoading" wrapper-class-name="w-full">
+ <div class="material-detail-body max-w-[720px] mx-auto px-5 pt-6 pb-20 min-h-[60vh]">
+ <div class="text-[22px] font-600 leading-[1.4] text-[var(--ant-color-text)] mb-5">
+ {{ payload.title || '' }}
+ </div>
+ <div
+ v-if="detailHtml"
+ class="article-content text-15px leading-[1.75] text-[var(--ant-color-text)]"
+ v-dompurify-html="detailHtml"
+ ></div>
+ <div
+ v-else-if="!detailLoading"
+ class="text-center text-[var(--ant-color-text-secondary)] mt-20"
+ >
+ 鏆傛棤姝f枃
+ </div>
+ </div>
+ </Spin>
+ </Modal>
+</template>
+
+<style scoped lang="scss">
+/* hover 闃村奖 + transition锛氬悎鍐欎竴澶勶紝鍐欐硶鏇寸揣鍑� */
+.material-card {
+ transition: box-shadow 0.15s ease;
+
+ &:hover {
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+ }
+}
+
+/* :deep 绌块�� v-dompurify-html 娓叉煋鐨勫唴宓� DOM锛涚粺涓�鎺у埗瀵屾枃鏈噷鐨� img / p / hN 鎺掔増 */
+.article-content {
+ :deep(img) {
+ max-width: 100%;
+ height: auto;
+ }
+
+ :deep(p) {
+ margin: 12px 0;
+ }
+
+ :deep(h1),
+ :deep(h2),
+ :deep(h3) {
+ margin: 20px 0 12px;
+ font-weight: 600;
+ }
+}
+</style>
--
Gitblit v1.9.3