mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-21 15:43:51 +08:00
feat(notification): add support for grouped notification display and localization
This commit is contained in:
@@ -8,6 +8,10 @@ import { useToast } from 'vue-toastification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
|
||||
type NotificationDisplayItem =
|
||||
| { kind: 'section'; key: string; title: string; count: number }
|
||||
| { kind: 'notification'; key: string; notification: SystemNotification }
|
||||
|
||||
const { t } = useI18n()
|
||||
const { useDelayedSSE } = useBackground()
|
||||
const $toast = useToast()
|
||||
@@ -31,6 +35,7 @@ const notificationClearBefore = ref(readNotificationClearBefore())
|
||||
const expandedNotificationKeys = ref(new Set<string>())
|
||||
|
||||
const hasUnreadNotifications = computed(() => notificationList.value.some(item => item.read === false))
|
||||
const notificationDisplayList = computed(() => buildNotificationDisplayList(notificationList.value))
|
||||
|
||||
/** 从本地存储读取通知清理时间戳,用于过滤已清理的历史通知。 */
|
||||
function readNotificationClearBefore() {
|
||||
@@ -342,6 +347,37 @@ function isMediaNotification(item: SystemNotification) {
|
||||
return Boolean(item.image)
|
||||
}
|
||||
|
||||
/** 按系统类消息和媒体消息生成带分组标题的虚拟列表数据。 */
|
||||
function buildNotificationDisplayList(items: SystemNotification[]) {
|
||||
const systemItems = items.filter(item => !isMediaNotification(item))
|
||||
const mediaItems = items.filter(isMediaNotification)
|
||||
const sections = [
|
||||
{ key: 'system', title: t('notification.systemMessages'), items: systemItems },
|
||||
{ key: 'media', title: t('notification.mediaMessages'), items: mediaItems },
|
||||
]
|
||||
const displayItems: NotificationDisplayItem[] = []
|
||||
|
||||
sections.forEach(section => {
|
||||
if (section.items.length === 0) return
|
||||
|
||||
displayItems.push({
|
||||
kind: 'section',
|
||||
key: `section:${section.key}`,
|
||||
title: section.title,
|
||||
count: section.items.length,
|
||||
})
|
||||
section.items.forEach(item => {
|
||||
displayItems.push({
|
||||
kind: 'notification',
|
||||
key: `notification:${getNotificationKey(item)}`,
|
||||
notification: item,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return displayItems
|
||||
}
|
||||
|
||||
/** 判断通知正文是否已经展开。 */
|
||||
function isNotificationExpanded(item: SystemNotification) {
|
||||
return expandedNotificationKeys.value.has(getNotificationExpansionKey(item))
|
||||
@@ -457,50 +493,65 @@ useDelayedSSE(
|
||||
<VVirtualScroll
|
||||
v-if="notificationList.length > 0"
|
||||
renderless
|
||||
:items="notificationList"
|
||||
:items="notificationDisplayList"
|
||||
:item-height="NOTIFICATION_ITEM_HEIGHT"
|
||||
>
|
||||
<template #default="{ item, itemRef }">
|
||||
<div :ref="itemRef" :key="getNotificationKey(item)" class="notification-virtual-item">
|
||||
<div
|
||||
:ref="itemRef"
|
||||
:key="item.key"
|
||||
class="notification-virtual-item"
|
||||
:class="{ 'notification-virtual-item--section': item.kind === 'section' }"
|
||||
>
|
||||
<div v-if="item.kind === 'section'" class="notification-section-heading">
|
||||
<span class="notification-section-heading__title">{{ item.title }}</span>
|
||||
<span class="notification-section-heading__count">{{ item.count }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="notification-row"
|
||||
:class="{
|
||||
'notification-row--unread': item.read === false,
|
||||
'notification-row--media': isMediaNotification(item),
|
||||
'notification-row--unread': item.notification.read === false,
|
||||
'notification-row--media': isMediaNotification(item.notification),
|
||||
}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-expanded="item.text ? isNotificationExpanded(item) : undefined"
|
||||
@click="toggleNotificationExpanded(item)"
|
||||
@keydown.enter.prevent="toggleNotificationExpanded(item)"
|
||||
@keydown.space.prevent="toggleNotificationExpanded(item)"
|
||||
:aria-expanded="item.notification.text ? isNotificationExpanded(item.notification) : undefined"
|
||||
@click="toggleNotificationExpanded(item.notification)"
|
||||
@keydown.enter.prevent="toggleNotificationExpanded(item.notification)"
|
||||
@keydown.space.prevent="toggleNotificationExpanded(item.notification)"
|
||||
>
|
||||
<div v-if="item.image" class="notification-media">
|
||||
<VImg v-if="item.image" :src="item.image" cover class="notification-media__image">
|
||||
<div v-if="item.notification.image" class="notification-media">
|
||||
<VImg
|
||||
v-if="item.notification.image"
|
||||
:src="item.notification.image"
|
||||
cover
|
||||
class="notification-media__image"
|
||||
>
|
||||
<template #placeholder>
|
||||
<VSkeletonLoader class="h-100 w-100" />
|
||||
</template>
|
||||
</VImg>
|
||||
</div>
|
||||
<div v-else class="notification-icon" :class="`text-${getNotificationColor(item)}`">
|
||||
<VIcon :icon="getNotificationIcon(item)" size="22" />
|
||||
<div v-else class="notification-icon" :class="`text-${getNotificationColor(item.notification)}`">
|
||||
<VIcon :icon="getNotificationIcon(item.notification)" size="22" />
|
||||
</div>
|
||||
|
||||
<div class="notification-content">
|
||||
<div class="notification-title-row">
|
||||
<span class="notification-title">{{ item.title }}</span>
|
||||
<span v-if="item.read === false" class="notification-unread-dot" />
|
||||
<span class="notification-title">{{ item.notification.title }}</span>
|
||||
<span v-if="item.notification.read === false" class="notification-unread-dot" />
|
||||
</div>
|
||||
<div
|
||||
v-if="item.text"
|
||||
v-if="item.notification.text"
|
||||
class="notification-text"
|
||||
:class="{ 'notification-text--expanded': isNotificationExpanded(item) }"
|
||||
:class="{ 'notification-text--expanded': isNotificationExpanded(item.notification) }"
|
||||
>
|
||||
{{ item.text }}
|
||||
{{ item.notification.text }}
|
||||
</div>
|
||||
<div class="notification-meta">
|
||||
<span v-if="item.mtype" class="notification-type">{{ item.mtype }}</span>
|
||||
<span>{{ formatDateDifference(getNotificationTime(item)) }}</span>
|
||||
<span v-if="item.notification.mtype" class="notification-type">{{ item.notification.mtype }}</span>
|
||||
<span>{{ formatDateDifference(getNotificationTime(item.notification)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -540,6 +591,34 @@ useDelayedSSE(
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.notification-virtual-item--section {
|
||||
padding-block: 10px 2px;
|
||||
}
|
||||
|
||||
.notification-section-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
|
||||
gap: 8px;
|
||||
letter-spacing: 0;
|
||||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
.notification-section-heading__title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.notification-section-heading__count {
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--v-theme-on-surface), 0.08);
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1;
|
||||
padding-block: 3px;
|
||||
padding-inline: 6px;
|
||||
}
|
||||
|
||||
.notification-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
@@ -467,6 +467,8 @@ export default {
|
||||
clearSuccess: 'Notifications cleared',
|
||||
clearFailed: 'Failed to clear notifications',
|
||||
empty: 'No Notifications',
|
||||
systemMessages: 'System Messages',
|
||||
mediaMessages: 'Media Messages',
|
||||
channel: 'Notification Channel',
|
||||
name: 'Name',
|
||||
nameHint: 'Name of notification channel',
|
||||
|
||||
@@ -465,6 +465,8 @@ export default {
|
||||
clearSuccess: '通知已清理',
|
||||
clearFailed: '通知清理失败',
|
||||
empty: '暂无通知',
|
||||
systemMessages: '系统类消息',
|
||||
mediaMessages: '媒体消息',
|
||||
channel: '通知渠道',
|
||||
name: '名称',
|
||||
nameHint: '通知渠道名称',
|
||||
|
||||
@@ -465,6 +465,8 @@ export default {
|
||||
clearSuccess: '通知已清理',
|
||||
clearFailed: '通知清理失敗',
|
||||
empty: '暫無通知',
|
||||
systemMessages: '系統類消息',
|
||||
mediaMessages: '媒體消息',
|
||||
channel: '通知渠道',
|
||||
name: '名稱',
|
||||
nameHint: '通知渠道名稱',
|
||||
|
||||
Reference in New Issue
Block a user