mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 07:28:37 +08:00
feat: optimize notification center controls
This commit is contained in:
@@ -55,7 +55,9 @@ const globalSettingsStore = useGlobalSettingsStore()
|
|||||||
// 获取用户权限信息
|
// 获取用户权限信息
|
||||||
const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions))
|
const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions))
|
||||||
const canAdmin = computed(() => hasPermission(userPermissions.value, 'admin'))
|
const canAdmin = computed(() => hasPermission(userPermissions.value, 'admin'))
|
||||||
const showAgentAssistant = computed(() => globalSettingsStore.get('AI_AGENT_ENABLE') === true)
|
const showAgentAssistant = computed(
|
||||||
|
() => globalSettingsStore.get('AI_AGENT_ENABLE') === true && globalSettingsStore.get('AI_AGENT_HIDE_ENTRY') !== true,
|
||||||
|
)
|
||||||
|
|
||||||
// 开始菜单项
|
// 开始菜单项
|
||||||
const startMenus = ref<NavMenu[]>([])
|
const startMenus = ref<NavMenu[]>([])
|
||||||
|
|||||||
@@ -4,26 +4,49 @@ import api from '@/api'
|
|||||||
import { clearUnreadMessages } from '@/utils/badge'
|
import { clearUnreadMessages } from '@/utils/badge'
|
||||||
import { formatDateDifference } from '@core/utils/formatters'
|
import { formatDateDifference } from '@core/utils/formatters'
|
||||||
import { useBackground } from '@/composables/useBackground'
|
import { useBackground } from '@/composables/useBackground'
|
||||||
|
import { useToast } from 'vue-toastification'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { useDelayedSSE } = useBackground()
|
const { useDelayedSSE } = useBackground()
|
||||||
|
const $toast = useToast()
|
||||||
|
const createConfirm = useConfirm()
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
// 固定通知项高度,配合 VVirtualScroll 避免历史通知过多时一次性渲染全部 DOM。
|
// 固定通知项高度,配合 VVirtualScroll 避免历史通知过多时一次性渲染全部 DOM。
|
||||||
const NOTIFICATION_ITEM_HEIGHT = 104
|
const NOTIFICATION_ITEM_HEIGHT = 104
|
||||||
const MEDIA_NOTIFICATION_TYPES = ['资源下载', '整理入库', '订阅', '媒体服务器', '手动处理']
|
const CLEAR_NOTIFICATION_ENDPOINTS = ['message/notification', 'message/notification/clear']
|
||||||
|
const NOTIFICATION_CLEAR_BEFORE_STORAGE_KEY = 'moviepilot-notification-clear-before'
|
||||||
|
|
||||||
const appsMenu = ref(false)
|
const appsMenu = ref(false)
|
||||||
const hasNewMessage = ref(false)
|
const hasNewMessage = ref(false)
|
||||||
const notificationList = ref<SystemNotification[]>([])
|
const notificationList = ref<SystemNotification[]>([])
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
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 hasUnreadNotifications = computed(() => notificationList.value.some(item => item.read === false))
|
const hasUnreadNotifications = computed(() => notificationList.value.some(item => item.read === false))
|
||||||
|
|
||||||
|
/** 从本地存储读取通知清理时间戳,用于过滤已清理的历史通知。 */
|
||||||
|
function readNotificationClearBefore() {
|
||||||
|
if (typeof localStorage === 'undefined') return 0
|
||||||
|
|
||||||
|
return Number(localStorage.getItem(NOTIFICATION_CLEAR_BEFORE_STORAGE_KEY) || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 写入通知清理时间戳,使清理结果在刷新后仍然生效。 */
|
||||||
|
function writeNotificationClearBefore(value: number) {
|
||||||
|
notificationClearBefore.value = value
|
||||||
|
if (typeof localStorage === 'undefined') return
|
||||||
|
|
||||||
|
localStorage.setItem(NOTIFICATION_CLEAR_BEFORE_STORAGE_KEY, String(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将通知备注统一转换成稳定字符串,用于生成去重 key。 */
|
||||||
function normalizeNote(note: SystemNotification['note']) {
|
function normalizeNote(note: SystemNotification['note']) {
|
||||||
if (note == null) return ''
|
if (note == null) return ''
|
||||||
if (typeof note === 'string') return note
|
if (typeof note === 'string') return note
|
||||||
@@ -31,26 +54,31 @@ function normalizeNote(note: SystemNotification['note']) {
|
|||||||
return JSON.stringify(note)
|
return JSON.stringify(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取通知时间字段,兼容历史数据中的不同命名。 */
|
||||||
function getNotificationTime(item: SystemNotification) {
|
function getNotificationTime(item: SystemNotification) {
|
||||||
return item.reg_time || item.date || ''
|
return item.reg_time || item.date || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 归一化文本内容,避免空白差异影响通知去重。 */
|
||||||
function normalizeText(value: unknown) {
|
function normalizeText(value: unknown) {
|
||||||
return String(value ?? '')
|
return String(value ?? '')
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
.trim()
|
.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取通知分类,统一插件、系统等历史字段差异。 */
|
||||||
function getNotificationKind(item: SystemNotification) {
|
function getNotificationKind(item: SystemNotification) {
|
||||||
if (item.type === 'plugin' || item.mtype === '插件') return 'plugin'
|
if (item.type === 'plugin' || item.mtype === '插件') return 'plugin'
|
||||||
if (item.type === 'system' || item.mtype === '其它') return 'system'
|
if (item.type === 'system' || item.mtype === '其它') return 'system'
|
||||||
return item.mtype || item.type || ''
|
return item.mtype || item.type || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 按分钟生成时间桶,降低同一通知秒级差异导致的重复展示。 */
|
||||||
function getNotificationTimeBucket(item: SystemNotification) {
|
function getNotificationTimeBucket(item: SystemNotification) {
|
||||||
return getNotificationTime(item).slice(0, 16)
|
return getNotificationTime(item).slice(0, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 基于主要展示字段生成内容去重 key。 */
|
||||||
function getNotificationContentKey(item: SystemNotification) {
|
function getNotificationContentKey(item: SystemNotification) {
|
||||||
return [
|
return [
|
||||||
getNotificationKind(item),
|
getNotificationKind(item),
|
||||||
@@ -63,25 +91,39 @@ function getNotificationContentKey(item: SystemNotification) {
|
|||||||
].join('::')
|
].join('::')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 生成通知可用于去重的全部 key。 */
|
||||||
function getNotificationKeys(item: SystemNotification) {
|
function getNotificationKeys(item: SystemNotification) {
|
||||||
return [item.id ? `id:${item.id}` : '', `content:${getNotificationContentKey(item)}`].filter(Boolean)
|
return [item.id ? `id:${item.id}` : '', `content:${getNotificationContentKey(item)}`].filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取用于虚拟列表渲染的稳定 key。 */
|
||||||
function getNotificationKey(item: SystemNotification) {
|
function getNotificationKey(item: SystemNotification) {
|
||||||
return item.id ? `id:${item.id}` : `content:${getNotificationContentKey(item)}`
|
return item.id ? `id:${item.id}` : `content:${getNotificationContentKey(item)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将通知时间解析成时间戳,用于列表降序排序。 */
|
||||||
function parseNotificationTime(value: string) {
|
function parseNotificationTime(value: string) {
|
||||||
if (!value) return 0
|
if (!value) return 0
|
||||||
return new Date(value.includes('T') ? value : value.replaceAll(/-/g, '/')).getTime() || 0
|
return new Date(value.includes('T') ? value : value.replaceAll(/-/g, '/')).getTime() || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 判断历史通知是否早于本地清理时间,需要从列表中过滤。 */
|
||||||
|
function isClearedHistoryNotification(item: SystemNotification) {
|
||||||
|
const clearBefore = notificationClearBefore.value
|
||||||
|
if (!clearBefore) return false
|
||||||
|
|
||||||
|
const notificationTime = parseNotificationTime(getNotificationTime(item))
|
||||||
|
return notificationTime > 0 && notificationTime <= clearBefore
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按通知时间倒序重排当前列表。 */
|
||||||
function sortNotifications() {
|
function sortNotifications() {
|
||||||
notificationList.value = [...notificationList.value].sort(
|
notificationList.value = [...notificationList.value].sort(
|
||||||
(a, b) => parseNotificationTime(getNotificationTime(b)) - parseNotificationTime(getNotificationTime(a)),
|
(a, b) => parseNotificationTime(getNotificationTime(b)) - parseNotificationTime(getNotificationTime(a)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 压缩当前通知列表,移除同一内容或同一 ID 的重复项。 */
|
||||||
function compactNotifications(items: SystemNotification[]) {
|
function compactNotifications(items: SystemNotification[]) {
|
||||||
const contentKeys = new Set<string>()
|
const contentKeys = new Set<string>()
|
||||||
const idKeys = new Set<string>()
|
const idKeys = new Set<string>()
|
||||||
@@ -101,6 +143,7 @@ function compactNotifications(items: SystemNotification[]) {
|
|||||||
return compactedItems
|
return compactedItems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 规范化通知展示字段,并补齐默认标题、类型和已读状态。 */
|
||||||
function normalizeNotification(item: SystemNotification, read = true): SystemNotification {
|
function normalizeNotification(item: SystemNotification, read = true): SystemNotification {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
@@ -110,6 +153,7 @@ function normalizeNotification(item: SystemNotification, read = true): SystemNot
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 合并新通知到当前列表,并维护去重集合、排序和已读状态。 */
|
||||||
function mergeNotifications(items: SystemNotification[], options: { prepend?: boolean; read?: boolean } = {}) {
|
function mergeNotifications(items: SystemNotification[], options: { prepend?: boolean; read?: boolean } = {}) {
|
||||||
const normalizedItems = items.map(item => normalizeNotification(item, options.read ?? true))
|
const normalizedItems = items.map(item => normalizeNotification(item, options.read ?? true))
|
||||||
const acceptedItems: SystemNotification[] = []
|
const acceptedItems: SystemNotification[] = []
|
||||||
@@ -133,6 +177,76 @@ function mergeNotifications(items: SystemNotification[], options: { prepend?: bo
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 重置通知分页状态,用于清理后重新进入空列表状态。 */
|
||||||
|
function resetNotifications() {
|
||||||
|
notificationList.value = []
|
||||||
|
notificationKeys.clear()
|
||||||
|
page.value = 1
|
||||||
|
hasMore.value = true
|
||||||
|
hasNewMessage.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通过后端接口清理通知历史,兼容新旧后端可能暴露的清理路径。 */
|
||||||
|
async function deleteNotificationHistory() {
|
||||||
|
let lastError: unknown = null
|
||||||
|
|
||||||
|
for (const endpoint of CLEAR_NOTIFICATION_ENDPOINTS) {
|
||||||
|
try {
|
||||||
|
return await api.delete(endpoint)
|
||||||
|
} catch (error: any) {
|
||||||
|
lastError = error
|
||||||
|
if (error?.response?.status !== 404 && error?.response?.status !== 405) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 尝试调用后端清理接口,不支持时回退为本地清理。 */
|
||||||
|
async function tryDeleteNotificationHistory() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await deleteNotificationHistory()
|
||||||
|
return result?.success !== false
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.response?.status === 404 || error?.response?.status === 405) return true
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确认并清空通知中心历史,同时同步清理未读角标。 */
|
||||||
|
async function clearNotifications() {
|
||||||
|
if (clearing.value || notificationList.value.length === 0) return
|
||||||
|
|
||||||
|
const confirmed = await createConfirm({
|
||||||
|
type: 'warn',
|
||||||
|
title: t('notification.clear'),
|
||||||
|
content: t('notification.clearConfirm'),
|
||||||
|
confirmText: t('notification.clear'),
|
||||||
|
})
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
clearing.value = true
|
||||||
|
try {
|
||||||
|
const cleared = await tryDeleteNotificationHistory()
|
||||||
|
if (!cleared) {
|
||||||
|
$toast.error(t('notification.clearFailed'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotificationClearBefore(Date.now())
|
||||||
|
resetNotifications()
|
||||||
|
await clearUnreadMessages()
|
||||||
|
appsMenu.value = false
|
||||||
|
hasMore.value = false
|
||||||
|
$toast.success(t('notification.clearSuccess'))
|
||||||
|
} catch (error: any) {
|
||||||
|
$toast.error(error?.response?.data?.message || error?.message || t('notification.clearFailed'))
|
||||||
|
} finally {
|
||||||
|
clearing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按页加载历史通知,并合并到当前虚拟列表。 */
|
||||||
async function loadNotifications({ done }: { done: (status: 'ok' | 'empty' | 'error') => void }) {
|
async function loadNotifications({ done }: { done: (status: 'ok' | 'empty' | 'error') => void }) {
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
done('ok')
|
done('ok')
|
||||||
@@ -159,9 +273,10 @@ async function loadNotifications({ done }: { done: (status: 'ok' | 'empty' | 'er
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeNotifications(items, { read: true })
|
const visibleItems = items.filter(item => !isClearedHistoryNotification(item))
|
||||||
|
mergeNotifications(visibleItems, { read: true })
|
||||||
page.value += 1
|
page.value += 1
|
||||||
hasMore.value = items.length >= PAGE_SIZE
|
hasMore.value = visibleItems.length === items.length && items.length >= PAGE_SIZE
|
||||||
done(hasMore.value ? 'ok' : 'empty')
|
done(hasMore.value ? 'ok' : 'empty')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载通知失败:', error)
|
console.error('加载通知失败:', error)
|
||||||
@@ -171,6 +286,7 @@ async function loadNotifications({ done }: { done: (status: 'ok' | 'empty' | 'er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 处理 SSE 推送的新通知,并置为未读状态展示红点。 */
|
||||||
function handleMessage(event: MessageEvent) {
|
function handleMessage(event: MessageEvent) {
|
||||||
if (!event.data) return
|
if (!event.data) return
|
||||||
|
|
||||||
@@ -194,6 +310,7 @@ function markAllAsRead() {
|
|||||||
void clearUnreadMessages()
|
void clearUnreadMessages()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 根据通知分类和业务类型选择列表图标。 */
|
||||||
function getNotificationIcon(item: SystemNotification) {
|
function getNotificationIcon(item: SystemNotification) {
|
||||||
if (getNotificationKind(item) === 'plugin') return 'mdi-puzzle-outline'
|
if (getNotificationKind(item) === 'plugin') return 'mdi-puzzle-outline'
|
||||||
if (item.mtype === '资源下载') return 'mdi-download'
|
if (item.mtype === '资源下载') return 'mdi-download'
|
||||||
@@ -203,6 +320,7 @@ function getNotificationIcon(item: SystemNotification) {
|
|||||||
return getNotificationKind(item) === 'system' ? 'mdi-alert-circle-outline' : 'mdi-bell-outline'
|
return getNotificationKind(item) === 'system' ? 'mdi-alert-circle-outline' : 'mdi-bell-outline'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 根据通知分类和业务类型选择图标颜色。 */
|
||||||
function getNotificationColor(item: SystemNotification) {
|
function getNotificationColor(item: SystemNotification) {
|
||||||
if (getNotificationKind(item) === 'system') return 'error'
|
if (getNotificationKind(item) === 'system') return 'error'
|
||||||
if (getNotificationKind(item) === 'plugin') return 'warning'
|
if (getNotificationKind(item) === 'plugin') return 'warning'
|
||||||
@@ -212,10 +330,12 @@ function getNotificationColor(item: SystemNotification) {
|
|||||||
return 'secondary'
|
return 'secondary'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 判断通知是否有真实媒体图,决定是否使用媒体缩略图样式。 */
|
||||||
function isMediaNotification(item: SystemNotification) {
|
function isMediaNotification(item: SystemNotification) {
|
||||||
return Boolean(item.image) || MEDIA_NOTIFICATION_TYPES.includes(item.mtype || '')
|
return Boolean(item.image)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 打开通知链接,并在需要时同步清理未读角标。 */
|
||||||
function openNotification(item: SystemNotification) {
|
function openNotification(item: SystemNotification) {
|
||||||
item.read = true
|
item.read = true
|
||||||
hasNewMessage.value = hasUnreadNotifications.value
|
hasNewMessage.value = hasUnreadNotifications.value
|
||||||
@@ -249,11 +369,11 @@ useDelayedSSE(
|
|||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<VBadge v-if="hasNewMessage" dot color="error" :offset-x="5" :offset-y="5" v-bind="props">
|
<VBadge v-if="hasNewMessage" dot color="error" :offset-x="5" :offset-y="5" v-bind="props">
|
||||||
<IconBtn>
|
<IconBtn>
|
||||||
<VIcon icon="mdi-bell-outline" />
|
<VIcon icon="mdi-bell-outline" size="22" />
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</VBadge>
|
</VBadge>
|
||||||
<IconBtn v-else v-bind="props">
|
<IconBtn v-else v-bind="props">
|
||||||
<VIcon icon="mdi-bell-outline" />
|
<VIcon icon="mdi-bell-outline" size="22" />
|
||||||
</IconBtn>
|
</IconBtn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -261,13 +381,27 @@ useDelayedSSE(
|
|||||||
<VCardItem class="py-3">
|
<VCardItem class="py-3">
|
||||||
<VCardTitle>{{ t('notification.center') }}</VCardTitle>
|
<VCardTitle>{{ t('notification.center') }}</VCardTitle>
|
||||||
<template #append>
|
<template #append>
|
||||||
<VTooltip :text="t('notification.markRead')">
|
<div class="notification-actions">
|
||||||
<template #activator="{ props }">
|
<VTooltip :text="t('notification.clear')">
|
||||||
<IconBtn v-bind="props" @click.stop="markAllAsRead">
|
<template #activator="{ props }">
|
||||||
<VIcon icon="mdi-email-check-outline" size="20" />
|
<IconBtn
|
||||||
</IconBtn>
|
v-bind="props"
|
||||||
</template>
|
:disabled="notificationList.length === 0 || clearing"
|
||||||
</VTooltip>
|
@click.stop="clearNotifications"
|
||||||
|
>
|
||||||
|
<VProgressCircular v-if="clearing" indeterminate size="18" width="2" />
|
||||||
|
<VIcon v-else icon="mdi-trash-can-outline" size="20" />
|
||||||
|
</IconBtn>
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
|
<VTooltip :text="t('notification.markRead')">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<IconBtn v-bind="props" :disabled="!hasUnreadNotifications" @click.stop="markAllAsRead">
|
||||||
|
<VIcon icon="mdi-email-check-outline" size="20" />
|
||||||
|
</IconBtn>
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
<VDivider />
|
<VDivider />
|
||||||
@@ -290,7 +424,9 @@ useDelayedSSE(
|
|||||||
{{ t('message.noMoreData') }}
|
{{ t('message.noMoreData') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="notification-empty">
|
<div v-else class="notification-empty">
|
||||||
<VIcon icon="mdi-bell-sleep-outline" size="40" class="mb-3" />
|
<div class="notification-empty__icon">
|
||||||
|
<VIcon icon="mdi-bell-sleep-outline" size="22" />
|
||||||
|
</div>
|
||||||
<div>{{ t('notification.empty') }}</div>
|
<div>{{ t('notification.empty') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -312,15 +448,12 @@ useDelayedSSE(
|
|||||||
}"
|
}"
|
||||||
@click="openNotification(item)"
|
@click="openNotification(item)"
|
||||||
>
|
>
|
||||||
<div v-if="isMediaNotification(item)" 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">
|
||||||
<template #placeholder>
|
<template #placeholder>
|
||||||
<VSkeletonLoader class="h-100 w-100" />
|
<VSkeletonLoader class="h-100 w-100" />
|
||||||
</template>
|
</template>
|
||||||
</VImg>
|
</VImg>
|
||||||
<div v-else class="notification-media__fallback">
|
|
||||||
<VIcon :icon="getNotificationIcon(item)" size="24" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="notification-icon" :class="`text-${getNotificationColor(item)}`">
|
<div v-else class="notification-icon" :class="`text-${getNotificationColor(item)}`">
|
||||||
<VIcon :icon="getNotificationIcon(item)" size="22" />
|
<VIcon :icon="getNotificationIcon(item)" size="22" />
|
||||||
@@ -354,6 +487,12 @@ useDelayedSSE(
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.notification-list-container {
|
.notification-list-container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-block-size: min(560px, 62vh);
|
max-block-size: min(560px, 62vh);
|
||||||
@@ -410,13 +549,11 @@ useDelayedSSE(
|
|||||||
block-size: 84px;
|
block-size: 84px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-media__image,
|
.notification-media__image {
|
||||||
.notification-media__fallback {
|
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-media__fallback,
|
|
||||||
.notification-icon {
|
.notification-icon {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@@ -495,4 +632,14 @@ useDelayedSSE(
|
|||||||
padding-inline: 16px;
|
padding-inline: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-empty__icon {
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(var(--v-theme-on-surface), 0.06);
|
||||||
|
block-size: 40px;
|
||||||
|
inline-size: 40px;
|
||||||
|
margin-block-end: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -462,6 +462,10 @@ export default {
|
|||||||
notification: {
|
notification: {
|
||||||
center: 'Notification Center',
|
center: 'Notification Center',
|
||||||
markRead: 'Mark as Read',
|
markRead: 'Mark as Read',
|
||||||
|
clear: 'Clear Notifications',
|
||||||
|
clearConfirm: 'Clear all notification history from Notification Center?',
|
||||||
|
clearSuccess: 'Notifications cleared',
|
||||||
|
clearFailed: 'Failed to clear notifications',
|
||||||
empty: 'No Notifications',
|
empty: 'No Notifications',
|
||||||
channel: 'Notification Channel',
|
channel: 'Notification Channel',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
@@ -1616,6 +1620,9 @@ export default {
|
|||||||
'Set the check interval for scheduled wake. Select "Disabled" to disable scheduled tasks.',
|
'Set the check interval for scheduled wake. Select "Disabled" to disable scheduled tasks.',
|
||||||
aiAgentVerbose: 'Verbose Mode',
|
aiAgentVerbose: 'Verbose Mode',
|
||||||
aiAgentVerboseHint: 'When enabled, tool call process will be displayed in AI agent responses',
|
aiAgentVerboseHint: 'When enabled, tool call process will be displayed in AI agent responses',
|
||||||
|
aiAgentHideEntry: 'Hide Global Entry',
|
||||||
|
aiAgentHideEntryHint:
|
||||||
|
'Only hide the floating AI assistant entry in the bottom-right corner. Message channels and background assistant features are not affected.',
|
||||||
aiAgentJobIntervalDisabled: 'Disabled',
|
aiAgentJobIntervalDisabled: 'Disabled',
|
||||||
aiAgentJobInterval1h: '1 Hour',
|
aiAgentJobInterval1h: '1 Hour',
|
||||||
aiAgentJobInterval3h: '3 Hours',
|
aiAgentJobInterval3h: '3 Hours',
|
||||||
|
|||||||
@@ -460,6 +460,10 @@ export default {
|
|||||||
notification: {
|
notification: {
|
||||||
center: '通知中心',
|
center: '通知中心',
|
||||||
markRead: '设为已读',
|
markRead: '设为已读',
|
||||||
|
clear: '清理通知',
|
||||||
|
clearConfirm: '是否确认清理通知中心内的全部历史消息?',
|
||||||
|
clearSuccess: '通知已清理',
|
||||||
|
clearFailed: '通知清理失败',
|
||||||
empty: '暂无通知',
|
empty: '暂无通知',
|
||||||
channel: '通知渠道',
|
channel: '通知渠道',
|
||||||
name: '名称',
|
name: '名称',
|
||||||
@@ -1602,6 +1606,8 @@ export default {
|
|||||||
aiAgentJobIntervalHint: '设置定时唤醒的检查间隔,选择"不启用"则不执行定时任务',
|
aiAgentJobIntervalHint: '设置定时唤醒的检查间隔,选择"不启用"则不执行定时任务',
|
||||||
aiAgentVerbose: '啰嗦模式',
|
aiAgentVerbose: '啰嗦模式',
|
||||||
aiAgentVerboseHint: '开启后会在智能体回复时显示工具调用过程',
|
aiAgentVerboseHint: '开启后会在智能体回复时显示工具调用过程',
|
||||||
|
aiAgentHideEntry: '隐藏全局入口',
|
||||||
|
aiAgentHideEntryHint: '仅隐藏页面右下角的智能助手浮动入口,不影响消息渠道和后台智能助手功能',
|
||||||
aiAgentJobIntervalDisabled: '不启用',
|
aiAgentJobIntervalDisabled: '不启用',
|
||||||
aiAgentJobInterval1h: '1小时',
|
aiAgentJobInterval1h: '1小时',
|
||||||
aiAgentJobInterval3h: '3小时',
|
aiAgentJobInterval3h: '3小时',
|
||||||
|
|||||||
@@ -460,6 +460,10 @@ export default {
|
|||||||
notification: {
|
notification: {
|
||||||
center: '通知中心',
|
center: '通知中心',
|
||||||
markRead: '設為已讀',
|
markRead: '設為已讀',
|
||||||
|
clear: '清理通知',
|
||||||
|
clearConfirm: '是否確認清理通知中心內的全部歷史消息?',
|
||||||
|
clearSuccess: '通知已清理',
|
||||||
|
clearFailed: '通知清理失敗',
|
||||||
empty: '暫無通知',
|
empty: '暫無通知',
|
||||||
channel: '通知渠道',
|
channel: '通知渠道',
|
||||||
name: '名稱',
|
name: '名稱',
|
||||||
@@ -1603,6 +1607,8 @@ export default {
|
|||||||
aiAgentJobIntervalHint: '設置定時喚醒的檢查間隔,選擇「不啟用」則不執行定時任務',
|
aiAgentJobIntervalHint: '設置定時喚醒的檢查間隔,選擇「不啟用」則不執行定時任務',
|
||||||
aiAgentVerbose: '囉嗦模式',
|
aiAgentVerbose: '囉嗦模式',
|
||||||
aiAgentVerboseHint: '開啟後會在智能體回覆時顯示工具調用過程',
|
aiAgentVerboseHint: '開啟後會在智能體回覆時顯示工具調用過程',
|
||||||
|
aiAgentHideEntry: '隱藏全域入口',
|
||||||
|
aiAgentHideEntryHint: '僅隱藏頁面右下角的智能助手浮動入口,不影響消息渠道和後台智能助手功能',
|
||||||
aiAgentJobIntervalDisabled: '不啟用',
|
aiAgentJobIntervalDisabled: '不啟用',
|
||||||
aiAgentJobInterval1h: '1小時',
|
aiAgentJobInterval1h: '1小時',
|
||||||
aiAgentJobInterval3h: '3小時',
|
aiAgentJobInterval3h: '3小時',
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const SystemSettings = ref<any>({
|
|||||||
CUSTOMIZE_WALLPAPER_API_URL: null,
|
CUSTOMIZE_WALLPAPER_API_URL: null,
|
||||||
AI_AGENT_ENABLE: false,
|
AI_AGENT_ENABLE: false,
|
||||||
AI_AGENT_GLOBAL: false,
|
AI_AGENT_GLOBAL: false,
|
||||||
|
AI_AGENT_HIDE_ENTRY: false,
|
||||||
AI_AGENT_VERBOSE: false,
|
AI_AGENT_VERBOSE: false,
|
||||||
AI_AGENT_JOB_INTERVAL: 24,
|
AI_AGENT_JOB_INTERVAL: 24,
|
||||||
LLM_PROVIDER: 'deepseek',
|
LLM_PROVIDER: 'deepseek',
|
||||||
@@ -1160,7 +1161,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
|
|||||||
<VExpandTransition>
|
<VExpandTransition>
|
||||||
<VCardText v-show="!aiAgentSettingsCollapsed" class="pt-2">
|
<VCardText v-show="!aiAgentSettingsCollapsed" class="pt-2">
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol cols="12" md="4">
|
<VCol cols="12" md="6">
|
||||||
<VSwitch
|
<VSwitch
|
||||||
v-model="SystemSettings.Basic.AI_AGENT_ENABLE"
|
v-model="SystemSettings.Basic.AI_AGENT_ENABLE"
|
||||||
:label="t('setting.system.aiAgentEnable')"
|
:label="t('setting.system.aiAgentEnable')"
|
||||||
@@ -1168,7 +1169,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
|
|||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="4">
|
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||||
<VSwitch
|
<VSwitch
|
||||||
v-model="SystemSettings.Basic.AI_AGENT_GLOBAL"
|
v-model="SystemSettings.Basic.AI_AGENT_GLOBAL"
|
||||||
:label="t('setting.system.aiAgentGlobal')"
|
:label="t('setting.system.aiAgentGlobal')"
|
||||||
@@ -1176,7 +1177,7 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
|
|||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="4">
|
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||||
<VSwitch
|
<VSwitch
|
||||||
v-model="SystemSettings.Basic.AI_AGENT_VERBOSE"
|
v-model="SystemSettings.Basic.AI_AGENT_VERBOSE"
|
||||||
:label="t('setting.system.aiAgentVerbose')"
|
:label="t('setting.system.aiAgentVerbose')"
|
||||||
@@ -1184,6 +1185,14 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
|
|||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||||
|
<VSwitch
|
||||||
|
v-model="SystemSettings.Basic.AI_AGENT_HIDE_ENTRY"
|
||||||
|
:label="t('setting.system.aiAgentHideEntry')"
|
||||||
|
:hint="t('setting.system.aiAgentHideEntryHint')"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||||
<VAutocomplete
|
<VAutocomplete
|
||||||
v-model="SystemSettings.Basic.LLM_PROVIDER"
|
v-model="SystemSettings.Basic.LLM_PROVIDER"
|
||||||
|
|||||||
Reference in New Issue
Block a user