Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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>

Expand Down
12 changes: 10 additions & 2 deletions frontend/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 25 additions & 1 deletion frontend/src/router/__tests__/title.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { resolveDocumentTitle } from '@/router/title'
import { resolveDocumentTitle, resolveRouteDocumentTitle } from '@/router/title'

describe('resolveDocumentTitle', () => {
it('路由存在标题时,使用“路由标题 - 站点名”格式', () => {
Expand All @@ -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')
})
})
24 changes: 7 additions & 17 deletions frontend/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/router/title.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { i18n } from '@/i18n'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import type { CustomMenuItem } from '@/types'

/**
* 统一生成页面标题,避免多处写入 document.title 产生覆盖冲突。
Expand All @@ -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)
}
Loading