调整未读消息入口提示

This commit is contained in:
jxxghp
2026-06-14 21:11:36 +08:00
parent 342c62c085
commit da0cd14af8
5 changed files with 108 additions and 50 deletions

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import api from '@/api'
import { clearAppBadge } from '@/utils/badge'
import { clearUnreadMessages } from '@/utils/badge'
import { useI18n } from 'vue-i18n'
import { useDisplay } from 'vuetify'
@@ -67,15 +67,20 @@ async function sendMessage() {
}
}
/** 清除未读消息计数和桌面角标。 */
function clearUnreadMessageState() {
window.setTimeout(() => {
void clearUnreadMessages()
}, 500)
}
watch(visible, async newValue => {
if (newValue) {
await nextTick()
messageViewRef.value?.resumeSSE?.()
messageViewRef.value?.forceScrollToEnd?.()
window.setTimeout(() => {
void clearAppBadge()
}, 500)
clearUnreadMessageState()
return
}
@@ -88,9 +93,7 @@ onMounted(async () => {
messageViewRef.value?.resumeSSE?.()
messageViewRef.value?.forceScrollToEnd?.()
window.setTimeout(() => {
void clearAppBadge()
}, 500)
clearUnreadMessageState()
})
onUnmounted(() => {

View File

@@ -24,7 +24,6 @@ import {
hasPermission,
type UserPermissionKey,
} from '@/utils/permission'
import { onUnreadMessage } from '@/utils/badge'
import { usePullDownGesture } from '@/composables/usePullDownGesture'
import { usePWA } from '@/composables/usePWA'
import OfflinePage from '@/layouts/components/OfflinePage.vue'
@@ -48,9 +47,6 @@ const themeLayout = ref(readThemeCustomizerSettings().layout)
const userStore = useUserStore()
const pluginSidebarNavStore = usePluginSidebarNavStore()
// ShortcutBar 引用
const shortcutBarRef = ref<InstanceType<typeof ShortcutBar> | null>(null)
// 获取用户权限信息
const userPermissions = computed(() => buildUserPermissionContext(userStore.superUser, userStore.permissions))
const canAdmin = computed(() => hasPermission(userPermissions.value, 'admin'))
@@ -382,18 +378,6 @@ function applyPendingHorizontalTab() {
pendingHorizontalTab.value = null
}
// 处理未读消息事件
function handleUnreadMessage(count: number) {
if (canAdmin.value && count > 0) {
// 延迟一点时间确保组件已渲染
setTimeout(() => {
if (shortcutBarRef.value && typeof shortcutBarRef.value.openMessageDialog === 'function') {
shortcutBarRef.value.openMessageDialog()
}
}, 500)
}
}
// 关闭插件快速访问
function handleClosePluginQuickAccess() {
showPluginQuickAccess.value = false
@@ -442,9 +426,6 @@ onMounted(async () => {
await pluginSidebarNavStore.ensureSidebarNav()
appendPluginSidebarMenus()
// 监听全局未读消息事件
const unsubscribe = onUnreadMessage(handleUnreadMessage)
// 监听Service Worker消息
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', handleServiceWorkerMessage)
@@ -454,7 +435,6 @@ onMounted(async () => {
// 组件卸载时清理监听
onBeforeUnmount(() => {
unsubscribe()
window.removeEventListener(THEME_CUSTOMIZER_CHANGE_EVENT, handleThemeCustomizerChange)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.removeEventListener('message', handleServiceWorkerMessage)
@@ -521,7 +501,7 @@ onMounted(async () => {
<!-- 👉 Horizontal Search Icon -->
<SearchBar v-if="showHorizontalThemeNav" icon-only />
<!-- 👉 Shortcuts -->
<ShortcutBar v-if="canAdmin" ref="shortcutBarRef" />
<ShortcutBar v-if="canAdmin" />
<!-- 👉 Notification -->
<UserNofification />
<!-- 👉 UserProfile -->

View File

@@ -5,6 +5,7 @@ import { openSharedDialog } from '@/composables/useSharedDialog'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores'
import { buildUserPermissionContext, filterItemsByPermission, hasItemPermission, type PermissionProtectedItem } from '@/utils/permission'
import { clearUnreadMessages, getUnreadCount, onUnreadMessage } from '@/utils/badge'
// 国际化
const { t } = useI18n()
@@ -43,6 +44,12 @@ const appsMenu = ref(false)
// 菜单最大宽度
const menuMaxWidth = ref(420)
// 未读消息数量,用于控制消息捷径卡片上的红点。
const unreadMessageCount = ref(0)
const hasUnreadMessages = computed(() => unreadMessageCount.value > 0)
let unreadStateRevision = 0
let stopUnreadMessageListener: (() => void) | null = null
// 定义捷径列表
const shortcuts: ShortcutItem[] = [
{
@@ -127,12 +134,44 @@ const shortcuts: ShortcutItem[] = [
const visibleShortcuts = computed(() => filterItemsByPermission(shortcuts, userPermissions.value))
/** 设置消息捷径卡片的未读数量。 */
function setUnreadMessageCount(count: number) {
unreadMessageCount.value = Math.max(0, count)
}
/** 同步全局未读消息数量到消息捷径卡片。 */
function handleUnreadMessage(count: number) {
unreadStateRevision += 1
setUnreadMessageCount(count)
}
/** 从 Service Worker 读取当前未读数量,避免错过启动早期事件。 */
async function syncUnreadMessageStateFromBadge() {
const revision = unreadStateRevision
const count = await getUnreadCount()
if (revision === unreadStateRevision) {
setUnreadMessageCount(count)
}
}
/** 清空未读消息数量和 PWA 桌面角标。 */
function clearUnreadMessageState() {
unreadStateRevision += 1
setUnreadMessageCount(0)
void clearUnreadMessages()
}
/** 打开快捷工具对应的共享弹窗。 */
function openShortcutDialog(item: (typeof shortcuts)[number]) {
if (!hasItemPermission(item, userPermissions.value)) return
appsMenu.value = false
if (item.dialog === 'message') {
clearUnreadMessageState()
}
if (item.customDialog) {
openSharedDialog(item.customDialog, {}, {}, { closeOn: ['close', 'update:modelValue'] })
return
@@ -168,6 +207,9 @@ defineExpose({
})
onMounted(() => {
stopUnreadMessageListener = onUnreadMessage(handleUnreadMessage)
void syncUnreadMessageStateFromBadge()
const shortcut = getQueryValue('shortcut')
if (shortcut) {
const found = visibleShortcuts.value.find(item => item.dialog === shortcut)
@@ -176,6 +218,10 @@ onMounted(() => {
}
}
})
onBeforeUnmount(() => {
stopUnreadMessageListener?.()
})
</script>
<template>
@@ -211,20 +257,30 @@ onMounted(() => {
<div class="grid grid-cols-2 gap-3">
<!-- 循环渲染快捷方式 -->
<div v-for="(item, index) in visibleShortcuts" :key="index">
<VCard
flat
class="pa-2 d-flex align-center cursor-pointer transition-transform duration-300 hover:-translate-y-1 border h-full"
hover
@click="openShortcutDialog(item)"
<VBadge
:model-value="item.dialog === 'message' && hasUnreadMessages"
dot
color="error"
location="top end"
offset-x="8"
offset-y="8"
class="d-block h-full w-100"
>
<VAvatar variant="text" size="48" rounded="lg">
<VIcon color="primary" :icon="item.icon" size="24" />
</VAvatar>
<div>
<div class="text-body-1 text-high-emphasis font-weight-medium">{{ item.title }}</div>
<div class="text-caption text-medium-emphasis">{{ item.subtitle }}</div>
</div>
</VCard>
<VCard
flat
class="pa-2 d-flex align-center cursor-pointer transition-transform duration-300 hover:-translate-y-1 border h-full w-100"
hover
@click="openShortcutDialog(item)"
>
<VAvatar variant="text" size="48" rounded="lg">
<VIcon color="primary" :icon="item.icon" size="24" />
</VAvatar>
<div>
<div class="text-body-1 text-high-emphasis font-weight-medium">{{ item.title }}</div>
<div class="text-caption text-medium-emphasis">{{ item.subtitle }}</div>
</div>
</VCard>
</VBadge>
</div>
</div>
</div>

View File

@@ -297,15 +297,21 @@ async function updateBadge(count: number) {
}
}
// 清除桌面角标和本地未读计数,确保不支持 Badge API 时也能归零。
async function clearBadge() {
if ('clearAppBadge' in self.navigator) {
try {
await self.navigator.clearAppBadge()
await setStoredUnreadCount(0)
} catch (error) {
console.error('Failed to clear app badge:', error)
console.error('Failed to clear native app badge:', error)
}
}
try {
await setStoredUnreadCount(0)
} catch (error) {
console.error('Failed to clear unread count:', error)
}
}
// 监控缓存大小

View File

@@ -96,28 +96,41 @@ export async function checkAndEmitUnreadMessages() {
}
}
// 清除未读消息计数,并通知前端同步隐藏未读红点。
export async function clearUnreadMessages(): Promise<boolean> {
emitUnreadMessageEvent(0)
return clearAppBadge()
}
// 清除桌面图标徽章
export async function clearAppBadge(): Promise<boolean> {
try {
// 如果浏览器支持原生Badge API直接调用
if ('clearAppBadge' in navigator) {
await navigator.clearAppBadge()
}
let nativeBadgeCleared = true
// 如果浏览器支持原生Badge API直接调用
if ('clearAppBadge' in navigator) {
try {
await navigator.clearAppBadge()
} catch (error) {
nativeBadgeCleared = false
console.error('Failed to clear native app badge:', error)
}
}
try {
// 向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)
resolve(Boolean(event.data.success) && nativeBadgeCleared)
}
navigator.serviceWorker.controller?.postMessage({ type: 'CLEAR_BADGE' }, [messageChannel.port2])
})
}
return true
return nativeBadgeCleared
} catch (error) {
console.error('Failed to clear app badge:', error)
return false