mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-17 17:07:35 +08:00
新增未读消息计数和桌面图标徽章更新功能
This commit is contained in:
@@ -1008,6 +1008,8 @@ export interface SystemNotification {
|
||||
text: string
|
||||
// 通知时间
|
||||
date: string
|
||||
// 是否已读
|
||||
read?: boolean
|
||||
}
|
||||
|
||||
// 下载器配置
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { formatDateDifference } from '@core/utils/formatters'
|
||||
import { SystemNotification } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { updateAppBadge, clearAppBadge } from '@/utils/badge'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -17,6 +18,34 @@ let eventSource: EventSource | null = null
|
||||
// 弹窗
|
||||
const appsMenu = ref(false)
|
||||
|
||||
// 未读消息数量
|
||||
const unreadCount = computed(() => {
|
||||
return notificationList.value.filter(item => !item.read).length
|
||||
})
|
||||
|
||||
// 更新桌面图标徽章
|
||||
async function updateBadge() {
|
||||
const count = unreadCount.value
|
||||
await updateAppBadge(count)
|
||||
}
|
||||
|
||||
// 清除桌面图标徽章
|
||||
async function clearBadge() {
|
||||
await clearAppBadge()
|
||||
}
|
||||
|
||||
// 标记所有消息为已读
|
||||
function markAllAsRead() {
|
||||
hasNewMessage.value = false
|
||||
// 标记所有消息为已读
|
||||
notificationList.value.forEach(item => {
|
||||
item.read = true
|
||||
})
|
||||
// 清除桌面图标徽章
|
||||
clearBadge()
|
||||
appsMenu.value = false
|
||||
}
|
||||
|
||||
// SSE持续接收消息
|
||||
function startSSEMessager() {
|
||||
// 延迟 3 秒启动 SSE,避免相关认证信息尚未写入 Cookie 导致 403
|
||||
@@ -27,6 +56,8 @@ function startSSEMessager() {
|
||||
const noti: SystemNotification = JSON.parse(event.data)
|
||||
notificationList.value.unshift(noti)
|
||||
hasNewMessage.value = true
|
||||
// 更新桌面图标徽章
|
||||
updateBadge()
|
||||
}
|
||||
})
|
||||
}, 3000)
|
||||
@@ -70,15 +101,7 @@ onBeforeUnmount(() => {
|
||||
<template #append>
|
||||
<VTooltip :text="t('notification.markRead')">
|
||||
<template #activator="{ props }">
|
||||
<IconBtn
|
||||
v-bind="props"
|
||||
@click="
|
||||
() => {
|
||||
hasNewMessage = false
|
||||
appsMenu = false
|
||||
}
|
||||
"
|
||||
>
|
||||
<IconBtn v-bind="props" @click="markAllAsRead">
|
||||
<VIcon icon="mdi-email-check-outline" size="20" />
|
||||
</IconBtn>
|
||||
</template>
|
||||
|
||||
@@ -11,6 +11,9 @@ precacheAndRoute(self.__WB_MANIFEST)
|
||||
// to allow work offline
|
||||
registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'), { denylist: [/^(\/[\w-]+)*\/api/] }))
|
||||
|
||||
// 消息计数器,用于存储未读消息数量
|
||||
let unreadCount = 0
|
||||
|
||||
// 通知选项
|
||||
const options = {
|
||||
icon: '/logo.png',
|
||||
@@ -18,6 +21,33 @@ const options = {
|
||||
actions: [{ action: 'close', title: '关闭' }],
|
||||
}
|
||||
|
||||
// 更新桌面图标徽章
|
||||
async function updateBadge(count: number) {
|
||||
if ('setAppBadge' in navigator) {
|
||||
try {
|
||||
if (count > 0) {
|
||||
await navigator.setAppBadge(count)
|
||||
} else {
|
||||
await navigator.clearAppBadge()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update app badge:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清除桌面图标徽章
|
||||
async function clearBadge() {
|
||||
if ('clearAppBadge' in navigator) {
|
||||
try {
|
||||
await navigator.clearAppBadge()
|
||||
unreadCount = 0
|
||||
} catch (error) {
|
||||
console.error('Failed to clear app badge:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 push 事件,显示通知
|
||||
self.addEventListener('push', function (event) {
|
||||
console.log('notification push')
|
||||
@@ -43,7 +73,12 @@ self.addEventListener('push', function (event) {
|
||||
data: { url: payload.url },
|
||||
actions: options.actions,
|
||||
}
|
||||
event.waitUntil(self.registration.showNotification(payload.title, content))
|
||||
|
||||
// 增加未读消息计数
|
||||
unreadCount++
|
||||
|
||||
// 更新桌面图标徽章
|
||||
event.waitUntil(Promise.all([self.registration.showNotification(payload.title, content), updateBadge(unreadCount)]))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
@@ -71,3 +106,26 @@ self.addEventListener('notificationclick', function (event) {
|
||||
event.waitUntil(self.clients.openWindow(info.data?.url))
|
||||
}
|
||||
})
|
||||
|
||||
// 监听来自主应用的消息,用于清除徽章或更新徽章数量
|
||||
self.addEventListener('message', function (event) {
|
||||
console.log('service worker received message:', event.data)
|
||||
|
||||
if (event.data && event.data.type === 'CLEAR_BADGE') {
|
||||
// 清除徽章
|
||||
clearBadge()
|
||||
event.ports[0]?.postMessage({ success: true })
|
||||
} else if (event.data && event.data.type === 'UPDATE_BADGE') {
|
||||
// 更新徽章数量
|
||||
const count = event.data.count || 0
|
||||
unreadCount = count
|
||||
updateBadge(count)
|
||||
.then(() => {
|
||||
event.ports[0]?.postMessage({ success: true })
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to update badge:', error)
|
||||
event.ports[0]?.postMessage({ success: false, error: error.message })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
17
src/types/global.d.ts
vendored
Normal file
17
src/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
// PWA Badge API 类型定义
|
||||
declare global {
|
||||
interface Navigator {
|
||||
/**
|
||||
* 设置应用徽章数量
|
||||
* @param contents 要显示的数量,可选
|
||||
*/
|
||||
setAppBadge(contents?: number): Promise<void>
|
||||
|
||||
/**
|
||||
* 清除应用徽章
|
||||
*/
|
||||
clearAppBadge(): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
68
src/utils/badge.ts
Normal file
68
src/utils/badge.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* PWA 徽章管理工具
|
||||
*/
|
||||
|
||||
// 清除桌面图标徽章
|
||||
export async function clearAppBadge(): Promise<boolean> {
|
||||
try {
|
||||
// 如果浏览器支持原生Badge API,直接调用
|
||||
if ('clearAppBadge' in navigator) {
|
||||
await navigator.clearAppBadge()
|
||||
}
|
||||
|
||||
// 向service worker发送清除徽章消息
|
||||
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||||
const messageChannel = new MessageChannel()
|
||||
|
||||
return new Promise(resolve => {
|
||||
messageChannel.port1.onmessage = event => {
|
||||
resolve(event.data.success)
|
||||
}
|
||||
|
||||
navigator.serviceWorker.controller?.postMessage({ type: 'CLEAR_BADGE' }, [messageChannel.port2])
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to clear app badge:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新桌面图标徽章数量
|
||||
export async function updateAppBadge(count: number): Promise<boolean> {
|
||||
try {
|
||||
// 如果浏览器支持原生Badge API,直接调用
|
||||
if ('setAppBadge' in navigator) {
|
||||
if (count > 0) {
|
||||
await navigator.setAppBadge(count)
|
||||
} else {
|
||||
await navigator.clearAppBadge()
|
||||
}
|
||||
}
|
||||
|
||||
// 向service worker发送更新徽章消息
|
||||
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||||
const messageChannel = new MessageChannel()
|
||||
|
||||
return new Promise(resolve => {
|
||||
messageChannel.port1.onmessage = event => {
|
||||
resolve(event.data.success)
|
||||
}
|
||||
|
||||
navigator.serviceWorker.controller?.postMessage({ type: 'UPDATE_BADGE', count }, [messageChannel.port2])
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to update app badge:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查浏览器是否支持Badge API
|
||||
export function supportsBadgeAPI(): boolean {
|
||||
return 'setAppBadge' in navigator && 'clearAppBadge' in navigator
|
||||
}
|
||||
Reference in New Issue
Block a user