更新桌面图标徽章的逻辑

This commit is contained in:
jxxghp
2025-06-14 07:52:55 +08:00
parent 05ebd48f09
commit fe17986b2a
5 changed files with 209 additions and 35 deletions
+28 -1
View File
@@ -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 -->
+21 -1
View File
@@ -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
View File
@@ -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 })
})
} }
}) })
+71
View File
@@ -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