diff --git a/src/layouts/components/DefaultLayout.vue b/src/layouts/components/DefaultLayout.vue index 18e3a24f..2f68c2b2 100644 --- a/src/layouts/components/DefaultLayout.vue +++ b/src/layouts/components/DefaultLayout.vue @@ -13,6 +13,7 @@ import { NavMenu } from '@/@layouts/types' import { useDisplay } from 'vuetify' import { useI18n } from 'vue-i18n' import { filterMenusByPermission } from '@/utils/permission' +import { checkUnreadOnStartup } from '@/utils/badge' const display = useDisplay() const appMode = inject('pwaMode') @@ -24,6 +25,9 @@ const userStore = useUserStore() // 是否超级用户 let superUser = userStore.superUser +// ShortcutBar 引用 +const shortcutBarRef = ref | null>(null) + // 获取用户权限信息 const userPermissions = computed(() => ({ is_superuser: userStore.superUser, @@ -58,6 +62,26 @@ function goBack() { 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(() => { // 获取菜单列表 startMenus.value = getMenuList(t('menu.start')) @@ -65,6 +89,9 @@ onMounted(() => { subscribeMenus.value = getMenuList(t('menu.subscribe')) organizeMenus.value = getMenuList(t('menu.organize')) systemMenus.value = getMenuList(t('menu.system')) + + // 检查未读消息 + checkUnreadMessages() }) @@ -86,7 +113,7 @@ onMounted(() => { - + diff --git a/src/layouts/components/ShortcutBar.vue b/src/layouts/components/ShortcutBar.vue index c74d1d6b..a9e2c1e8 100644 --- a/src/layouts/components/ShortcutBar.vue +++ b/src/layouts/components/ShortcutBar.vue @@ -9,6 +9,7 @@ import api from '@/api' import { useDisplay } from 'vuetify' import { getQueryValue } from '@/@core/utils' import { useI18n } from 'vue-i18n' +import { clearAppBadge } from '@/utils/badge' // 国际化 const { t } = useI18n() @@ -103,6 +104,15 @@ function openDialog(dialogRef: any) { dialogRef.value = true } +// 打开消息弹窗并清除徽章 +async function openMessageDialog() { + messageDialog.value = true + // 延迟清除徽章,确保对话框已经打开 + setTimeout(async () => { + await clearAppBadge() + }, 500) +} + // 滚动到底部 function scrollMessageToEnd() { // 使用更长的延迟确保DOM已更新 @@ -138,6 +148,16 @@ async function sendMessage() { } } +// 供外部调用的打开消息弹窗方法 +function openMessageDialogFromExternal() { + openMessageDialog() +} + +// 暴露方法给父组件 +defineExpose({ + openMessageDialog: openMessageDialogFromExternal, +}) + onMounted(() => { scrollMessageToEnd() const shortcut = getQueryValue('shortcut') @@ -187,7 +207,7 @@ onMounted(() => { flat class="pa-2 d-flex align-center cursor-pointer transition-transform duration-300 hover:-translate-y-1 border h-full" hover - @click="openDialog(item.dialogRef)" + @click="item.dialog === 'message' ? openMessageDialog() : openDialog(item.dialogRef)" > diff --git a/src/layouts/components/UserNotification.vue b/src/layouts/components/UserNotification.vue index 02894a3f..e0f7ae17 100644 --- a/src/layouts/components/UserNotification.vue +++ b/src/layouts/components/UserNotification.vue @@ -2,7 +2,6 @@ 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() @@ -18,22 +17,6 @@ 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 @@ -41,8 +24,6 @@ function markAllAsRead() { notificationList.value.forEach(item => { item.read = true }) - // 清除桌面图标徽章 - clearBadge() appsMenu.value = false } @@ -56,8 +37,6 @@ function startSSEMessager() { const noti: SystemNotification = JSON.parse(event.data) notificationList.value.unshift(noti) hasNewMessage.value = true - // 更新桌面图标徽章 - updateBadge() } }) }, 3000) diff --git a/src/service-worker.ts b/src/service-worker.ts index 73bc9ae2..3c5652c8 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -11,9 +11,6 @@ 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', @@ -21,6 +18,66 @@ const options = { actions: [{ action: 'close', title: '关闭' }], } +// 存储未读消息数量的键名 +const UNREAD_COUNT_KEY = 'mp_unread_count' + +// 从IndexedDB获取未读消息数量 +async function getStoredUnreadCount(): Promise { + 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 { + try { + await set(UNREAD_COUNT_KEY, count) + } catch (error) { + console.error('Failed to set stored unread count:', error) + } +} + +// 简单的IndexedDB包装器 +async function openDB(): Promise { + 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 { + 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 { + 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) { if ('setAppBadge' in navigator) { @@ -41,7 +98,7 @@ async function clearBadge() { if ('clearAppBadge' in navigator) { try { await navigator.clearAppBadge() - unreadCount = 0 + await setStoredUnreadCount(0) } catch (error) { console.error('Failed to clear app badge:', error) } @@ -74,11 +131,15 @@ self.addEventListener('push', function (event) { actions: options.actions, } - // 增加未读消息计数 - unreadCount++ - - // 更新桌面图标徽章 - event.waitUntil(Promise.all([self.registration.showNotification(payload.title, content), updateBadge(unreadCount)])) + // 增加未读消息计数并持久化存储 + event.waitUntil( + (async () => { + const currentCount = await getStoredUnreadCount() + const newCount = currentCount + 1 + await setStoredUnreadCount(newCount) + await Promise.all([self.registration.showNotification(payload.title, content), updateBadge(newCount)]) + })(), + ) } catch (e) { console.error(e) } @@ -114,12 +175,18 @@ self.addEventListener('message', function (event) { if (event.data && event.data.type === 'CLEAR_BADGE') { // 清除徽章 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') { // 更新徽章数量 const count = event.data.count || 0 - unreadCount = count - updateBadge(count) + setStoredUnreadCount(count) + .then(() => updateBadge(count)) .then(() => { event.ports[0]?.postMessage({ success: true }) }) @@ -127,5 +194,15 @@ self.addEventListener('message', function (event) { console.error('Failed to update badge:', error) 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 }) + }) } }) diff --git a/src/utils/badge.ts b/src/utils/badge.ts index ee3b7739..fdb9a798 100644 --- a/src/utils/badge.ts +++ b/src/utils/badge.ts @@ -2,6 +2,55 @@ * PWA 徽章管理工具 */ +// 等待Service Worker准备就绪 +export async function waitForServiceWorker(): Promise { + 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 { + 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 { try { @@ -62,6 +111,28 @@ export async function updateAppBadge(count: number): Promise { } } +// 获取Service Worker中的未读消息数量 +export async function getUnreadCount(): Promise { + 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 export function supportsBadgeAPI(): boolean { return 'setAppBadge' in navigator && 'clearAppBadge' in navigator