Optimize comment notification template to support rich text rendering (#7683)

#### What type of PR is this?

/area core
/area ui
/milestone 2.21.x
/kind feature

#### What this PR does / why we need it:

Optimize comment notification template to support rich text rendering

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/7685/head
Ryan Wang 2025-08-13 15:32:50 +08:00 committed by GitHub
parent eddcb5bc38
commit 2bcfbbc371
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 53 additions and 38 deletions

View File

@ -25,7 +25,7 @@ spec:
<a th:href="${postUrl}" target="_blank" th:text="|《${postTitle}》|"></a>
<span>,以下是评论的具体内容:</span>
</p>
<pre class="content" th:text="${content}"></pre>
<div class="content" th:utext="${content}"></div>
</div>
<div></div>
</div>
@ -58,7 +58,7 @@ spec:
<a th:href="${pageUrl}" target="_blank" th:text="|《${pageTitle}》|"></a>
<span>,以下是评论的具体内容:</span>
</p>
<pre class="content" th:text="${content}"></pre>
<div class="content" th:utext="${content}"></div>
</div>
<div></div>
</div>
@ -87,15 +87,18 @@ spec:
</div>
<div class="body">
<p>
<span th:text="${replier}"></span> 在评论
<span th:text="${replier}"></span> 在
<a
th:href="${commentSubjectUrl}"
target="_blank"
th:text="|”${isQuoteReply ? quoteContent : commentContent}”|"
th:text="|《${commentSubjectTitle}》|"
></a>
<span>中回复了你,以下是回复的具体内容:</span>
<span>中回复了你</span>
</p>
<pre class="content" th:text="${content}"></pre>
<p>你的评论:</p>
<div class="content" th:utext="${isQuoteReply ? quoteContent : commentContent}"></div>
<p>回复的内容:</p>
<div class="content" th:utext="${content}"></div>
</div>
<div></div>
</div>

View File

@ -14,6 +14,7 @@ import {
} from "@halo-dev/components";
import { useQuery } from "@tanstack/vue-query";
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
import sanitize from "sanitize-html";
const { currentUser } = useUserStore();
@ -81,7 +82,12 @@ function handleRouteToNotification(notification: Notification) {
<template #start>
<VEntityField
:title="notification.spec?.title"
:description="notification.spec?.rawContent"
:description="
sanitize(notification.spec?.htmlContent || '', {
allowedTags: [],
allowedAttributes: {},
})
"
@click="handleRouteToNotification(notification)"
/>
</template>

View File

@ -231,7 +231,9 @@ function handleMarkAllAsRead() {
</Transition>
</OverlayScrollbarsComponent>
</div>
<div class="col-span-12 sm:col-span-6 lg:col-span-7 xl:col-span-9">
<div
class="col-span-12 overflow-auto sm:col-span-6 lg:col-span-7 xl:col-span-9"
>
<NotificationContent :notification="selectedNotification" />
</div>
</div>

View File

@ -1,6 +1,7 @@
<script lang="ts" setup>
import type { Notification } from "@halo-dev/api-client";
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
import sanitize from "sanitize-html";
import { computed } from "vue";
const props = withDefaults(
@ -10,34 +11,28 @@ const props = withDefaults(
{ notification: undefined }
);
const htmlContent = computed(() => {
const styles = `
<style>
html {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 0.875rem;
line-height: 1.25rem;
}
</style>
`;
if (!props.notification?.spec?.htmlContent) {
return "";
}
return styles + props.notification?.spec?.htmlContent;
const content = computed(() => {
return sanitize(props.notification?.spec?.htmlContent || "");
});
</script>
<template>
<div class="h-full w-full overflow-auto">
<OverlayScrollbarsComponent
element="div"
:options="{ scrollbars: { autoHide: 'scroll' } }"
class="h-full w-full"
defer
>
<iframe class="h-full w-full p-2" :srcdoc="htmlContent"></iframe>
</OverlayScrollbarsComponent>
</div>
<OverlayScrollbarsComponent
element="div"
:options="{ scrollbars: { autoHide: 'scroll' } }"
class="h-full w-full"
defer
>
<div class="markdown-body h-full w-full p-2 text-sm" v-html="content"></div>
</OverlayScrollbarsComponent>
</template>
<style scoped lang="scss">
.markdown-body :deep(ul) {
list-style: disc !important;
}
.markdown-body :deep(ol) {
list-style: decimal !important;
}
</style>

View File

@ -5,7 +5,8 @@ import type { Notification } from "@halo-dev/api-client";
import { ucApiClient } from "@halo-dev/api-client";
import { Dialog, Toast, VStatusDot } from "@halo-dev/components";
import { useMutation, useQueryClient } from "@tanstack/vue-query";
import { ref, watch } from "vue";
import sanitize from "sanitize-html";
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
const queryClient = useQueryClient();
@ -74,6 +75,14 @@ watch(
immediate: true,
}
);
const content = computed(() => {
// Clean html tags
return sanitize(props.notification.spec?.htmlContent || "", {
allowedTags: [],
allowedAttributes: {},
});
});
</script>
<template>
<div
@ -99,10 +108,10 @@ watch(
/>
</div>
<div
v-if="notification.spec?.rawContent"
class="truncate text-xs text-gray-600"
v-if="notification.spec?.htmlContent"
class="line-clamp-1 text-xs text-gray-600"
>
{{ notification.spec.rawContent }}
{{ content }}
</div>
<div class="flex h-6 items-end justify-between">
<div class="text-xs text-gray-600">