mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-18 06:00:31 +08:00
调整未读消息入口提示
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
// 监控缓存大小
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user