mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-12 16:01:35 +08:00
fix(notification): improve notification expansion handling and styling
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user