fix(notification): improve notification expansion handling and styling

This commit is contained in:
jxxghp
2026-06-19 10:02:16 +08:00
parent 9eed2fea87
commit 68f2f010d1

View File

@@ -14,8 +14,8 @@ const $toast = useToast()
const createConfirm = useConfirm() const createConfirm = useConfirm()
const PAGE_SIZE = 20 const PAGE_SIZE = 20
// 固定通知项高度,配合 VVirtualScroll 避免历史通知过多时一次性渲染全部 DOM // 虚拟滚动的默认通知项高度,展开后的实际高度由 VVirtualScroll 的 itemRef 动态测量
const NOTIFICATION_ITEM_HEIGHT = 104 const NOTIFICATION_ITEM_HEIGHT = 136
const CLEAR_NOTIFICATION_ENDPOINTS = ['message/notification', 'message/notification/clear'] const CLEAR_NOTIFICATION_ENDPOINTS = ['message/notification', 'message/notification/clear']
const NOTIFICATION_CLEAR_BEFORE_STORAGE_KEY = 'moviepilot-notification-clear-before' const NOTIFICATION_CLEAR_BEFORE_STORAGE_KEY = 'moviepilot-notification-clear-before'
@@ -28,6 +28,7 @@ const clearing = ref(false)
const hasMore = ref(true) const hasMore = ref(true)
const notificationKeys = new Set<string>() const notificationKeys = new Set<string>()
const notificationClearBefore = ref(readNotificationClearBefore()) const notificationClearBefore = ref(readNotificationClearBefore())
const expandedNotificationKeys = ref(new Set<string>())
const hasUnreadNotifications = computed(() => notificationList.value.some(item => item.read === false)) const hasUnreadNotifications = computed(() => notificationList.value.some(item => item.read === false))
@@ -101,6 +102,11 @@ function getNotificationKey(item: SystemNotification) {
return item.id ? `id:${item.id}` : `content:${getNotificationContentKey(item)}` return item.id ? `id:${item.id}` : `content:${getNotificationContentKey(item)}`
} }
/** 获取通知正文展开状态使用的稳定 key。 */
function getNotificationExpansionKey(item: SystemNotification) {
return getNotificationKey(item)
}
/** 将通知时间解析成时间戳,用于列表降序排序。 */ /** 将通知时间解析成时间戳,用于列表降序排序。 */
function parseNotificationTime(value: string) { function parseNotificationTime(value: string) {
if (!value) return 0 if (!value) return 0
@@ -181,6 +187,7 @@ function mergeNotifications(items: SystemNotification[], options: { prepend?: bo
function resetNotifications() { function resetNotifications() {
notificationList.value = [] notificationList.value = []
notificationKeys.clear() notificationKeys.clear()
expandedNotificationKeys.value = new Set()
page.value = 1 page.value = 1
hasMore.value = true hasMore.value = true
hasNewMessage.value = false hasNewMessage.value = false
@@ -335,12 +342,28 @@ function isMediaNotification(item: SystemNotification) {
return Boolean(item.image) return Boolean(item.image)
} }
/** 打开通知链接,并在需要时同步清理未读角标。 */ /** 判断通知正文是否已经展开。 */
function openNotification(item: SystemNotification) { function isNotificationExpanded(item: SystemNotification) {
return expandedNotificationKeys.value.has(getNotificationExpansionKey(item))
}
/** 标记单条通知为已读,并在全部已读时同步清理未读角标。 */
function markNotificationAsRead(item: SystemNotification) {
item.read = true item.read = true
hasNewMessage.value = hasUnreadNotifications.value hasNewMessage.value = hasUnreadNotifications.value
if (!hasUnreadNotifications.value) void clearUnreadMessages() if (!hasUnreadNotifications.value) void clearUnreadMessages()
if (item.link) window.open(item.link, '_blank') }
/** 切换通知正文展开状态。 */
function toggleNotificationExpanded(item: SystemNotification) {
markNotificationAsRead(item)
if (!item.text) return
const key = getNotificationExpansionKey(item)
const expandedKeys = new Set(expandedNotificationKeys.value)
if (expandedKeys.has(key)) expandedKeys.delete(key)
else expandedKeys.add(key)
expandedNotificationKeys.value = expandedKeys
} }
useDelayedSSE( useDelayedSSE(
@@ -362,7 +385,7 @@ useDelayedSSE(
width="420" width="420"
max-width="calc(100vw - 24px)" max-width="calc(100vw - 24px)"
transition="scale-transition" transition="scale-transition"
close-on-content-click :close-on-content-click="false"
class="notification-menu" class="notification-menu"
scrim scrim
> >
@@ -439,14 +462,18 @@ useDelayedSSE(
> >
<template #default="{ item, itemRef }"> <template #default="{ item, itemRef }">
<div :ref="itemRef" :key="getNotificationKey(item)" class="notification-virtual-item"> <div :ref="itemRef" :key="getNotificationKey(item)" class="notification-virtual-item">
<button <div
type="button"
class="notification-row" class="notification-row"
:class="{ :class="{
'notification-row--unread': item.read === false, 'notification-row--unread': item.read === false,
'notification-row--media': isMediaNotification(item), 'notification-row--media': isMediaNotification(item),
}" }"
@click="openNotification(item)" role="button"
tabindex="0"
:aria-expanded="item.text ? isNotificationExpanded(item) : undefined"
@click="toggleNotificationExpanded(item)"
@keydown.enter.prevent="toggleNotificationExpanded(item)"
@keydown.space.prevent="toggleNotificationExpanded(item)"
> >
<div v-if="item.image" class="notification-media"> <div v-if="item.image" class="notification-media">
<VImg v-if="item.image" :src="item.image" cover class="notification-media__image"> <VImg v-if="item.image" :src="item.image" cover class="notification-media__image">
@@ -464,7 +491,11 @@ useDelayedSSE(
<span class="notification-title">{{ item.title }}</span> <span class="notification-title">{{ item.title }}</span>
<span v-if="item.read === false" class="notification-unread-dot" /> <span v-if="item.read === false" class="notification-unread-dot" />
</div> </div>
<div v-if="item.text" class="notification-text"> <div
v-if="item.text"
class="notification-text"
:class="{ 'notification-text--expanded': isNotificationExpanded(item) }"
>
{{ item.text }} {{ item.text }}
</div> </div>
<div class="notification-meta"> <div class="notification-meta">
@@ -472,7 +503,7 @@ useDelayedSSE(
<span>{{ formatDateDifference(getNotificationTime(item)) }}</span> <span>{{ formatDateDifference(getNotificationTime(item)) }}</span>
</div> </div>
</div> </div>
</button> </div>
</div> </div>
</template> </template>
</VVirtualScroll> </VVirtualScroll>
@@ -505,7 +536,6 @@ useDelayedSSE(
} }
.notification-virtual-item { .notification-virtual-item {
block-size: 110px;
padding-block: 4px; padding-block: 4px;
padding-inline: 8px; padding-inline: 8px;
} }
@@ -518,7 +548,6 @@ useDelayedSSE(
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
background: transparent; background: transparent;
block-size: 100%;
color: inherit; color: inherit;
cursor: pointer; cursor: pointer;
gap: 12px; gap: 12px;
@@ -573,18 +602,22 @@ useDelayedSSE(
.notification-title-row { .notification-title-row {
display: flex; display: flex;
align-items: center; align-items: flex-start;
gap: 8px; gap: 8px;
min-block-size: 20px; min-block-size: 24px;
} }
.notification-title { .notification-title {
display: -webkit-box;
overflow: hidden; overflow: hidden;
flex: 1 1 auto;
-webkit-box-orient: vertical;
font-size: 0.925rem; font-size: 0.925rem;
font-weight: 600; font-weight: 600;
line-height: 1.35; line-height: 1.35;
-webkit-line-clamp: 2;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: normal;
} }
.notification-unread-dot { .notification-unread-dot {
@@ -593,20 +626,31 @@ useDelayedSSE(
background: rgb(var(--v-theme-error)); background: rgb(var(--v-theme-error));
block-size: 7px; block-size: 7px;
inline-size: 7px; inline-size: 7px;
margin-block-start: 0.45rem;
} }
.notification-text { .notification-text {
display: -webkit-box; display: block;
overflow: hidden; overflow: hidden;
-webkit-box-orient: vertical; padding: 0;
border: 0;
background: transparent;
block-size: auto;
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
cursor: pointer;
font-size: 0.8125rem; font-size: 0.8125rem;
-webkit-line-clamp: 2;
line-height: 1.45; line-height: 1.45;
margin-block-start: 4px; margin-block-start: 4px;
max-block-size: calc(0.8125rem * 1.45 * 3);
text-align: start;
white-space: pre-wrap; white-space: pre-wrap;
} }
.notification-text--expanded {
max-block-size: none;
overflow: visible;
}
.notification-meta { .notification-meta {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;