| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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('') |
| | | |
| | | // ç¹å»è¡ä¸ºï¼url é空跳å¤é¾ï¼ä¸ºç©ºåæ payload.materialId æå¯ææ¬æ£æï¼å
¨å± 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] æåæ£æå¤±è´¥', 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" |
| | | > |
| | | ææ æ£æ |
| | | </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> |