diff --git a/frontend/src/App.vue b/frontend/src/App.vue index ece3bf76802..14c364c3b29 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -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() }) diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index 5dab65e8097..79bf77e8bad 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -70,12 +70,20 @@ export async function setLocale(locale: string): Promise { 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 { diff --git a/frontend/src/router/__tests__/title.spec.ts b/frontend/src/router/__tests__/title.spec.ts index 3a89283748a..7cd3812b65c 100644 --- a/frontend/src/router/__tests__/title.spec.ts +++ b/frontend/src/router/__tests__/title.spec.ts @@ -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') + }) +}) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index bcc5f42197e..8721efd70a0 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -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 diff --git a/frontend/src/router/title.ts b/frontend/src/router/title.ts index 89ec9276a64..be5cf9d39fc 100644 --- a/frontend/src/router/title.ts +++ b/frontend/src/router/title.ts @@ -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, + 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) +}