fix(frontend): refresh custom page document title

This commit is contained in:
milesians
2026-06-18 12:30:21 +08:00
parent 4a5665da5b
commit 952be8711b
5 changed files with 85 additions and 24 deletions
+27 -4
View File
@@ -4,9 +4,9 @@ import { onMounted, onBeforeUnmount, watch } from 'vue'
import Toast from '@/components/common/Toast.vue'
import NavigationProgress from '@/components/common/NavigationProgress.vue'
import AdminComplianceDialog from '@/components/admin/AdminComplianceDialog.vue'
import { resolveDocumentTitle } from '@/router/title'
import { resolveRouteDocumentTitle } from '@/router/title'
import AnnouncementPopup from '@/components/common/AnnouncementPopup.vue'
import { useAppStore, useAuthStore, useSubscriptionStore, useAnnouncementStore, useAdminComplianceStore } from '@/stores'
import { useAppStore, useAuthStore, useSubscriptionStore, useAnnouncementStore, useAdminComplianceStore, useAdminSettingsStore } from '@/stores'
import { getSetupStatus } from '@/api/setup'
const router = useRouter()
@@ -16,6 +16,15 @@ const authStore = useAuthStore()
const subscriptionStore = useSubscriptionStore()
const announcementStore = useAnnouncementStore()
const adminComplianceStore = useAdminComplianceStore()
const adminSettingsStore = useAdminSettingsStore()
function updateDocumentTitle() {
const customMenuItems = [
...(appStore.cachedPublicSettings?.custom_menu_items ?? []),
...(authStore.isAdmin ? adminSettingsStore.customMenuItems : []),
]
document.title = resolveRouteDocumentTitle(route, appStore.siteName, customMenuItems)
}
/**
* Update favicon dynamically
@@ -44,6 +53,20 @@ watch(
{ immediate: true }
)
watch(
[
() => route.fullPath,
() => route.meta.title,
() => route.meta.titleKey,
() => appStore.siteName,
() => appStore.cachedPublicSettings?.custom_menu_items,
() => authStore.isAdmin,
() => adminSettingsStore.customMenuItems,
],
updateDocumentTitle,
{ deep: true }
)
// Watch for authentication state and manage subscription data + announcements
function onVisibilityChange() {
if (document.visibilityState === 'visible' && authStore.isAuthenticated) {
@@ -123,8 +146,8 @@ onMounted(async () => {
// Load public settings into appStore (will be cached for other components)
await appStore.fetchPublicSettings()
// Re-resolve document title now that siteName is available
document.title = resolveDocumentTitle(route.meta.title, appStore.siteName, route.meta.titleKey as string)
// Re-resolve document title now that site settings are available
updateDocumentTitle()
})
</script>
+10 -2
View File
@@ -70,12 +70,20 @@ export async function setLocale(locale: string): Promise<void> {
document.documentElement.setAttribute('lang', locale)
// 同步更新浏览器页签标题,使其跟随语言切换
const { resolveDocumentTitle } = await import('@/router/title')
const { resolveRouteDocumentTitle } = await import('@/router/title')
const { default: router } = await import('@/router')
const { useAppStore } = await import('@/stores/app')
const { useAuthStore } = await import('@/stores/auth')
const { useAdminSettingsStore } = await import('@/stores/adminSettings')
const route = router.currentRoute.value
const appStore = useAppStore()
document.title = resolveDocumentTitle(route.meta.title, appStore.siteName, route.meta.titleKey as string)
const authStore = useAuthStore()
const adminSettingsStore = useAdminSettingsStore()
const customMenuItems = [
...(appStore.cachedPublicSettings?.custom_menu_items ?? []),
...(authStore.isAdmin ? adminSettingsStore.customMenuItems : []),
]
document.title = resolveRouteDocumentTitle(route, appStore.siteName, customMenuItems)
}
export function getLocale(): LocaleCode {
+25 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { resolveDocumentTitle } from '@/router/title'
import { resolveDocumentTitle, resolveRouteDocumentTitle } from '@/router/title'
describe('resolveDocumentTitle', () => {
it('路由存在标题时,使用“路由标题 - 站点名”格式', () => {
@@ -23,3 +23,27 @@ describe('resolveDocumentTitle', () => {
expect(after).toBe('Admin Dashboard - Beta')
})
})
describe('resolveRouteDocumentTitle', () => {
it('自定义页面菜单加载后,使用菜单名称作为标题', () => {
const route = {
name: 'CustomPage',
params: { id: 'scheduler' },
meta: {
title: 'Custom Page'
}
}
expect(resolveRouteDocumentTitle(route, 'EzouAPI')).toBe('Custom Page - EzouAPI')
expect(resolveRouteDocumentTitle(route, 'EzouAPI', [
{
id: 'scheduler',
label: '账号调度器',
icon_svg: '',
url: 'https://example.com',
visibility: 'admin',
sort_order: 0
}
])).toBe('账号调度器 - EzouAPI')
})
})
+7 -17
View File
@@ -12,7 +12,7 @@ import { useNavigationLoadingState } from '@/composables/useNavigationLoading'
import { useRoutePrefetch } from '@/composables/useRoutePrefetch'
import { getSetupStatus } from '@/api/setup'
import { resolveCompletedSetupRedirectPath } from './setupRedirect'
import { resolveDocumentTitle } from './title'
import { resolveRouteDocumentTitle } from './title'
/**
* Route definitions with lazy loading
@@ -732,22 +732,12 @@ router.beforeEach(async (to, _from, next) => {
// Set page title
const appStore = useAppStore()
// For custom pages, use menu item label as document title
if (to.name === 'CustomPage') {
const id = to.params.id as string
const publicItems = appStore.cachedPublicSettings?.custom_menu_items ?? []
const adminSettingsStore = useAdminSettingsStore()
const menuItem = publicItems.find((item) => item.id === id)
?? (authStore.isAdmin ? adminSettingsStore.customMenuItems.find((item) => item.id === id) : undefined)
if (menuItem?.label) {
const siteName = appStore.siteName || 'Sub2API'
document.title = `${menuItem.label} - ${siteName}`
} else {
document.title = resolveDocumentTitle(to.meta.title, appStore.siteName, to.meta.titleKey as string)
}
} else {
document.title = resolveDocumentTitle(to.meta.title, appStore.siteName, to.meta.titleKey as string)
}
const adminSettingsStore = useAdminSettingsStore()
const customMenuItems = [
...(appStore.cachedPublicSettings?.custom_menu_items ?? []),
...(authStore.isAdmin ? adminSettingsStore.customMenuItems : []),
]
document.title = resolveRouteDocumentTitle(to, appStore.siteName, customMenuItems)
// Check if route requires authentication
const requiresAuth = to.meta.requiresAuth !== false // Default to true
+16
View File
@@ -1,4 +1,6 @@
import { i18n } from '@/i18n'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import type { CustomMenuItem } from '@/types'
/**
* 统一生成页面标题,避免多处写入 document.title 产生覆盖冲突。
@@ -20,3 +22,17 @@ export function resolveDocumentTitle(routeTitle: unknown, siteName?: string, tit
return normalizedSiteName
}
export function resolveRouteDocumentTitle(
route: Pick<RouteLocationNormalizedLoaded, 'name' | 'params' | 'meta'>,
siteName: string | undefined,
customMenuItems: CustomMenuItem[] = [],
): string {
const id = typeof route.params.id === 'string' ? route.params.id : ''
const menuItem = route.name === 'CustomPage' && id
? customMenuItems.find((item) => item.id === id)
: undefined
const menuTitle = menuItem?.label.trim()
return resolveDocumentTitle(menuTitle || route.meta.title, siteName, menuTitle ? undefined : route.meta.titleKey as string)
}