mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-28 19:47:49 +08:00
更新桌面图标徽章的逻辑
This commit is contained in:
@@ -13,6 +13,7 @@ import { NavMenu } from '@/@layouts/types'
|
|||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { filterMenusByPermission } from '@/utils/permission'
|
import { filterMenusByPermission } from '@/utils/permission'
|
||||||
|
import { checkUnreadOnStartup } from '@/utils/badge'
|
||||||
|
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
const appMode = inject('pwaMode')
|
const appMode = inject('pwaMode')
|
||||||
@@ -24,6 +25,9 @@ const userStore = useUserStore()
|
|||||||
// 是否超级用户
|
// 是否超级用户
|
||||||
let superUser = userStore.superUser
|
let superUser = userStore.superUser
|
||||||
|
|
||||||
|
// ShortcutBar 引用
|
||||||
|
const shortcutBarRef = ref<InstanceType<typeof ShortcutBar> | null>(null)
|
||||||
|
|
||||||
// 获取用户权限信息
|
// 获取用户权限信息
|
||||||
const userPermissions = computed(() => ({
|
const userPermissions = computed(() => ({
|
||||||
is_superuser: userStore.superUser,
|
is_superuser: userStore.superUser,
|
||||||
@@ -58,6 +62,26 @@ function goBack() {
|
|||||||
history.back()
|
history.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查未读消息并自动打开消息弹窗
|
||||||
|
async function checkUnreadMessages() {
|
||||||
|
if (!superUser) {
|
||||||
|
return // 只有超级用户才能看到消息
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const unreadCount = await checkUnreadOnStartup()
|
||||||
|
|
||||||
|
if (unreadCount > 0) {
|
||||||
|
// 延迟2秒打开消息弹窗,确保页面和组件都已完全加载
|
||||||
|
setTimeout(() => {
|
||||||
|
shortcutBarRef.value?.openMessageDialog()
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 静默处理错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 获取菜单列表
|
// 获取菜单列表
|
||||||
startMenus.value = getMenuList(t('menu.start'))
|
startMenus.value = getMenuList(t('menu.start'))
|
||||||
@@ -65,6 +89,9 @@ onMounted(() => {
|
|||||||
subscribeMenus.value = getMenuList(t('menu.subscribe'))
|
subscribeMenus.value = getMenuList(t('menu.subscribe'))
|
||||||
organizeMenus.value = getMenuList(t('menu.organize'))
|
organizeMenus.value = getMenuList(t('menu.organize'))
|
||||||
systemMenus.value = getMenuList(t('menu.system'))
|
systemMenus.value = getMenuList(t('menu.system'))
|
||||||
|
|
||||||
|
// 检查未读消息
|
||||||
|
checkUnreadMessages()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -86,7 +113,7 @@ onMounted(() => {
|
|||||||
<!-- 👉 Spacer -->
|
<!-- 👉 Spacer -->
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
<!-- 👉 Shortcuts -->
|
<!-- 👉 Shortcuts -->
|
||||||
<ShortcutBar v-if="superUser" />
|
<ShortcutBar v-if="superUser" ref="shortcutBarRef" />
|
||||||
<!-- 👉 Notification -->
|
<!-- 👉 Notification -->
|
||||||
<UserNofification />
|
<UserNofification />
|
||||||
<!-- 👉 UserProfile -->
|
<!-- 👉 UserProfile -->
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import api from '@/api'
|
|||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { getQueryValue } from '@/@core/utils'
|
import { getQueryValue } from '@/@core/utils'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { clearAppBadge } from '@/utils/badge'
|
||||||
|
|
||||||
// 国际化
|
// 国际化
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -103,6 +104,15 @@ function openDialog(dialogRef: any) {
|
|||||||
dialogRef.value = true
|
dialogRef.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开消息弹窗并清除徽章
|
||||||
|
async function openMessageDialog() {
|
||||||
|
messageDialog.value = true
|
||||||
|
// 延迟清除徽章,确保对话框已经打开
|
||||||
|
setTimeout(async () => {
|
||||||
|
await clearAppBadge()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部
|
||||||
function scrollMessageToEnd() {
|
function scrollMessageToEnd() {
|
||||||
// 使用更长的延迟确保DOM已更新
|
// 使用更长的延迟确保DOM已更新
|
||||||
@@ -138,6 +148,16 @@ async function sendMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 供外部调用的打开消息弹窗方法
|
||||||
|
function openMessageDialogFromExternal() {
|
||||||
|
openMessageDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
openMessageDialog: openMessageDialogFromExternal,
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
scrollMessageToEnd()
|
scrollMessageToEnd()
|
||||||
const shortcut = getQueryValue('shortcut')
|
const shortcut = getQueryValue('shortcut')
|
||||||
@@ -187,7 +207,7 @@ onMounted(() => {
|
|||||||
flat
|
flat
|
||||||
class="pa-2 d-flex align-center cursor-pointer transition-transform duration-300 hover:-translate-y-1 border h-full"
|
class="pa-2 d-flex align-center cursor-pointer transition-transform duration-300 hover:-translate-y-1 border h-full"
|
||||||
hover
|
hover
|
||||||
@click="openDialog(item.dialogRef)"
|
@click="item.dialog === 'message' ? openMessageDialog() : openDialog(item.dialogRef)"
|
||||||
>
|
>
|
||||||
<VAvatar variant="text" size="48" rounded="lg">
|
<VAvatar variant="text" size="48" rounded="lg">
|
||||||
<VIcon color="primary" :icon="item.icon" size="24" />
|
<VIcon color="primary" :icon="item.icon" size="24" />
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { formatDateDifference } from '@core/utils/formatters'
|
import { formatDateDifference } from '@core/utils/formatters'
|
||||||
import { SystemNotification } from '@/api/types'
|
import { SystemNotification } from '@/api/types'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { updateAppBadge, clearAppBadge } from '@/utils/badge'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -18,22 +17,6 @@ let eventSource: EventSource | null = null
|
|||||||
// 弹窗
|
// 弹窗
|
||||||
const appsMenu = ref(false)
|
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() {
|
function markAllAsRead() {
|
||||||
hasNewMessage.value = false
|
hasNewMessage.value = false
|
||||||
@@ -41,8 +24,6 @@ function markAllAsRead() {
|
|||||||
notificationList.value.forEach(item => {
|
notificationList.value.forEach(item => {
|
||||||
item.read = true
|
item.read = true
|
||||||
})
|
})
|
||||||
// 清除桌面图标徽章
|
|
||||||
clearBadge()
|
|
||||||
appsMenu.value = false
|
appsMenu.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,8 +37,6 @@ function startSSEMessager() {
|
|||||||
const noti: SystemNotification = JSON.parse(event.data)
|
const noti: SystemNotification = JSON.parse(event.data)
|
||||||
notificationList.value.unshift(noti)
|
notificationList.value.unshift(noti)
|
||||||
hasNewMessage.value = true
|
hasNewMessage.value = true
|
||||||
// 更新桌面图标徽章
|
|
||||||
updateBadge()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
|||||||
+89
-12
@@ -11,9 +11,6 @@ precacheAndRoute(self.__WB_MANIFEST)
|
|||||||
// to allow work offline
|
// to allow work offline
|
||||||
registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'), { denylist: [/^(\/[\w-]+)*\/api/] }))
|
registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'), { denylist: [/^(\/[\w-]+)*\/api/] }))
|
||||||
|
|
||||||
// 消息计数器,用于存储未读消息数量
|
|
||||||
let unreadCount = 0
|
|
||||||
|
|
||||||
// 通知选项
|
// 通知选项
|
||||||
const options = {
|
const options = {
|
||||||
icon: '/logo.png',
|
icon: '/logo.png',
|
||||||
@@ -21,6 +18,66 @@ const options = {
|
|||||||
actions: [{ action: 'close', title: '关闭' }],
|
actions: [{ action: 'close', title: '关闭' }],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 存储未读消息数量的键名
|
||||||
|
const UNREAD_COUNT_KEY = 'mp_unread_count'
|
||||||
|
|
||||||
|
// 从IndexedDB获取未读消息数量
|
||||||
|
async function getStoredUnreadCount(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const count = await get(UNREAD_COUNT_KEY)
|
||||||
|
return count || 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get stored unread count:', error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存未读消息数量到IndexedDB
|
||||||
|
async function setStoredUnreadCount(count: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
await set(UNREAD_COUNT_KEY, count)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set stored unread count:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单的IndexedDB包装器
|
||||||
|
async function openDB(): Promise<IDBDatabase> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open('mp_badge_db', 1)
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
request.onsuccess = () => resolve(request.result)
|
||||||
|
request.onupgradeneeded = event => {
|
||||||
|
const db = (event.target as IDBOpenDBRequest).result
|
||||||
|
if (!db.objectStoreNames.contains('badge')) {
|
||||||
|
db.createObjectStore('badge')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(key: string): Promise<any> {
|
||||||
|
const db = await openDB()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(['badge'], 'readonly')
|
||||||
|
const store = transaction.objectStore('badge')
|
||||||
|
const request = store.get(key)
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
request.onsuccess = () => resolve(request.result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function set(key: string, value: any): Promise<void> {
|
||||||
|
const db = await openDB()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction(['badge'], 'readwrite')
|
||||||
|
const store = transaction.objectStore('badge')
|
||||||
|
const request = store.put(value, key)
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
request.onsuccess = () => resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 更新桌面图标徽章
|
// 更新桌面图标徽章
|
||||||
async function updateBadge(count: number) {
|
async function updateBadge(count: number) {
|
||||||
if ('setAppBadge' in navigator) {
|
if ('setAppBadge' in navigator) {
|
||||||
@@ -41,7 +98,7 @@ async function clearBadge() {
|
|||||||
if ('clearAppBadge' in navigator) {
|
if ('clearAppBadge' in navigator) {
|
||||||
try {
|
try {
|
||||||
await navigator.clearAppBadge()
|
await navigator.clearAppBadge()
|
||||||
unreadCount = 0
|
await setStoredUnreadCount(0)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to clear app badge:', error)
|
console.error('Failed to clear app badge:', error)
|
||||||
}
|
}
|
||||||
@@ -74,11 +131,15 @@ self.addEventListener('push', function (event) {
|
|||||||
actions: options.actions,
|
actions: options.actions,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 增加未读消息计数
|
// 增加未读消息计数并持久化存储
|
||||||
unreadCount++
|
event.waitUntil(
|
||||||
|
(async () => {
|
||||||
// 更新桌面图标徽章
|
const currentCount = await getStoredUnreadCount()
|
||||||
event.waitUntil(Promise.all([self.registration.showNotification(payload.title, content), updateBadge(unreadCount)]))
|
const newCount = currentCount + 1
|
||||||
|
await setStoredUnreadCount(newCount)
|
||||||
|
await Promise.all([self.registration.showNotification(payload.title, content), updateBadge(newCount)])
|
||||||
|
})(),
|
||||||
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
@@ -114,12 +175,18 @@ self.addEventListener('message', function (event) {
|
|||||||
if (event.data && event.data.type === 'CLEAR_BADGE') {
|
if (event.data && event.data.type === 'CLEAR_BADGE') {
|
||||||
// 清除徽章
|
// 清除徽章
|
||||||
clearBadge()
|
clearBadge()
|
||||||
event.ports[0]?.postMessage({ success: true })
|
.then(() => {
|
||||||
|
event.ports[0]?.postMessage({ success: true })
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to clear badge:', error)
|
||||||
|
event.ports[0]?.postMessage({ success: false, error: error.message })
|
||||||
|
})
|
||||||
} else if (event.data && event.data.type === 'UPDATE_BADGE') {
|
} else if (event.data && event.data.type === 'UPDATE_BADGE') {
|
||||||
// 更新徽章数量
|
// 更新徽章数量
|
||||||
const count = event.data.count || 0
|
const count = event.data.count || 0
|
||||||
unreadCount = count
|
setStoredUnreadCount(count)
|
||||||
updateBadge(count)
|
.then(() => updateBadge(count))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
event.ports[0]?.postMessage({ success: true })
|
event.ports[0]?.postMessage({ success: true })
|
||||||
})
|
})
|
||||||
@@ -127,5 +194,15 @@ self.addEventListener('message', function (event) {
|
|||||||
console.error('Failed to update badge:', error)
|
console.error('Failed to update badge:', error)
|
||||||
event.ports[0]?.postMessage({ success: false, error: error.message })
|
event.ports[0]?.postMessage({ success: false, error: error.message })
|
||||||
})
|
})
|
||||||
|
} else if (event.data && event.data.type === 'GET_UNREAD_COUNT') {
|
||||||
|
// 获取未读消息数量
|
||||||
|
getStoredUnreadCount()
|
||||||
|
.then(count => {
|
||||||
|
event.ports[0]?.postMessage({ count })
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to get unread count:', error)
|
||||||
|
event.ports[0]?.postMessage({ count: 0 })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,55 @@
|
|||||||
* PWA 徽章管理工具
|
* PWA 徽章管理工具
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 等待Service Worker准备就绪
|
||||||
|
export async function waitForServiceWorker(): Promise<ServiceWorker | null> {
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经有激活的Service Worker,直接返回
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
return navigator.serviceWorker.controller
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待Service Worker注册和激活
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const checkServiceWorker = () => {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
resolve(navigator.serviceWorker.controller)
|
||||||
|
} else {
|
||||||
|
setTimeout(checkServiceWorker, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听Service Worker变化
|
||||||
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||||
|
resolve(navigator.serviceWorker.controller)
|
||||||
|
})
|
||||||
|
|
||||||
|
checkServiceWorker()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用启动时检查未读消息数量
|
||||||
|
export async function checkUnreadOnStartup(): Promise<number> {
|
||||||
|
try {
|
||||||
|
// 等待Service Worker准备就绪
|
||||||
|
const sw = await waitForServiceWorker()
|
||||||
|
if (!sw) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟500ms确保Service Worker完全准备好
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
const unreadCount = await getUnreadCount()
|
||||||
|
return unreadCount
|
||||||
|
} catch (error) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 清除桌面图标徽章
|
// 清除桌面图标徽章
|
||||||
export async function clearAppBadge(): Promise<boolean> {
|
export async function clearAppBadge(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
@@ -62,6 +111,28 @@ export async function updateAppBadge(count: number): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取Service Worker中的未读消息数量
|
||||||
|
export async function getUnreadCount(): Promise<number> {
|
||||||
|
try {
|
||||||
|
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||||||
|
const messageChannel = new MessageChannel()
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
messageChannel.port1.onmessage = event => {
|
||||||
|
resolve(event.data.count || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.serviceWorker.controller?.postMessage({ type: 'GET_UNREAD_COUNT' }, [messageChannel.port2])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get unread count:', error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 检查浏览器是否支持Badge API
|
// 检查浏览器是否支持Badge API
|
||||||
export function supportsBadgeAPI(): boolean {
|
export function supportsBadgeAPI(): boolean {
|
||||||
return 'setAppBadge' in navigator && 'clearAppBadge' in navigator
|
return 'setAppBadge' in navigator && 'clearAppBadge' in navigator
|
||||||
|
|||||||
Reference in New Issue
Block a user