diff --git a/frontend/src/components/layout/AppHeader.vue b/frontend/src/components/layout/AppHeader.vue index ffc7c5e2..76bf684f 100644 --- a/frontend/src/components/layout/AppHeader.vue +++ b/frontend/src/components/layout/AppHeader.vue @@ -211,6 +211,7 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import { useAppStore, useAuthStore, useOnboardingStore } from '@/stores' +import { useAdminSettingsStore } from '@/stores/adminSettings' import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue' import SubscriptionProgressMini from '@/components/common/SubscriptionProgressMini.vue' import AnnouncementBell from '@/components/common/AnnouncementBell.vue' @@ -221,6 +222,7 @@ const route = useRoute() const { t } = useI18n() const appStore = useAppStore() const authStore = useAuthStore() +const adminSettingsStore = useAdminSettingsStore() const onboardingStore = useOnboardingStore() const user = computed(() => authStore.user) @@ -257,8 +259,9 @@ const pageTitle = computed(() => { // For custom pages, use the menu item's label instead of generic "自定义页面" if (route.name === 'CustomPage') { const id = route.params.id as string - const items = appStore.cachedPublicSettings?.custom_menu_items ?? [] - const menuItem = items.find((item) => item.id === id) + const publicItems = appStore.cachedPublicSettings?.custom_menu_items ?? [] + const menuItem = publicItems.find((item) => item.id === id) + ?? (authStore.isAdmin ? adminSettingsStore.customMenuItems.find((item) => item.id === id) : undefined) if (menuItem?.label) return menuItem.label } const titleKey = route.meta.titleKey as string diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index dcfc60bb..3c384e64 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -579,8 +579,7 @@ const customMenuItemsForUser = computed(() => { }) const customMenuItemsForAdmin = computed(() => { - const items = appStore.cachedPublicSettings?.custom_menu_items ?? [] - return items + return adminSettingsStore.customMenuItems .filter((item) => item.visibility === 'admin') .sort((a, b) => a.sort_order - b.sort_order) }) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 08f492d4..8aa9cfff 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -6,6 +6,7 @@ import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' import { useAuthStore } from '@/stores/auth' import { useAppStore } from '@/stores/app' +import { useAdminSettingsStore } from '@/stores/adminSettings' import { useNavigationLoadingState } from '@/composables/useNavigationLoading' import { useRoutePrefetch } from '@/composables/useRoutePrefetch' import { resolveDocumentTitle } from './title' @@ -431,8 +432,10 @@ router.beforeEach((to, _from, next) => { // For custom pages, use menu item label as document title if (to.name === 'CustomPage') { const id = to.params.id as string - const items = appStore.cachedPublicSettings?.custom_menu_items ?? [] - const menuItem = items.find((item) => item.id === id) + 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}` diff --git a/frontend/src/stores/adminSettings.ts b/frontend/src/stores/adminSettings.ts index 460cc92b..76010c5e 100644 --- a/frontend/src/stores/adminSettings.ts +++ b/frontend/src/stores/adminSettings.ts @@ -1,6 +1,7 @@ import { defineStore } from 'pinia' import { ref } from 'vue' import { adminAPI } from '@/api' +import type { CustomMenuItem } from '@/types' export const useAdminSettingsStore = defineStore('adminSettings', () => { const loaded = ref(false) @@ -47,6 +48,7 @@ export const useAdminSettingsStore = defineStore('adminSettings', () => { const opsMonitoringEnabled = ref(readCachedBool('ops_monitoring_enabled_cached', true)) const opsRealtimeMonitoringEnabled = ref(readCachedBool('ops_realtime_monitoring_enabled_cached', true)) const opsQueryModeDefault = ref(readCachedString('ops_query_mode_default_cached', 'auto')) + const customMenuItems = ref([]) async function fetch(force = false): Promise { if (loaded.value && !force) return @@ -64,6 +66,8 @@ export const useAdminSettingsStore = defineStore('adminSettings', () => { opsQueryModeDefault.value = settings.ops_query_mode_default || 'auto' writeCachedString('ops_query_mode_default_cached', opsQueryModeDefault.value) + customMenuItems.value = Array.isArray(settings.custom_menu_items) ? settings.custom_menu_items : [] + loaded.value = true } catch (err) { // Keep cached/default value: do not "flip" the UI based on a transient fetch failure. @@ -122,6 +126,7 @@ export const useAdminSettingsStore = defineStore('adminSettings', () => { opsMonitoringEnabled, opsRealtimeMonitoringEnabled, opsQueryModeDefault, + customMenuItems, fetch, setOpsMonitoringEnabledLocal, setOpsRealtimeMonitoringEnabledLocal, diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index e3f89ac9..32bbeb75 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -1363,9 +1363,11 @@ import Toggle from '@/components/common/Toggle.vue' import ImageUpload from '@/components/common/ImageUpload.vue' import { useClipboard } from '@/composables/useClipboard' import { useAppStore } from '@/stores' +import { useAdminSettingsStore } from '@/stores/adminSettings' const { t } = useI18n() const appStore = useAppStore() +const adminSettingsStore = useAdminSettingsStore() const { copyToClipboard } = useClipboard() const loading = ref(true) @@ -1661,8 +1663,9 @@ async function saveSettings() { form.smtp_password = '' form.turnstile_secret_key = '' form.linuxdo_connect_client_secret = '' - // Refresh cached public settings so sidebar/header update immediately + // Refresh cached settings so sidebar/header update immediately await appStore.fetchPublicSettings(true) + await adminSettingsStore.fetch(true) appStore.showSuccess(t('admin.settings.settingsSaved')) } catch (error: any) { appStore.showError( diff --git a/frontend/src/views/user/CustomPageView.vue b/frontend/src/views/user/CustomPageView.vue index ed1c11d7..532830a5 100644 --- a/frontend/src/views/user/CustomPageView.vue +++ b/frontend/src/views/user/CustomPageView.vue @@ -70,6 +70,7 @@ import { useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores' import { useAuthStore } from '@/stores/auth' +import { useAdminSettingsStore } from '@/stores/adminSettings' import AppLayout from '@/components/layout/AppLayout.vue' import Icon from '@/components/icons/Icon.vue' import { buildEmbeddedUrl, detectTheme } from '@/utils/embedded-url' @@ -78,6 +79,7 @@ const { t } = useI18n() const route = useRoute() const appStore = useAppStore() const authStore = useAuthStore() +const adminSettingsStore = useAdminSettingsStore() const loading = ref(false) const pageTheme = ref<'light' | 'dark'>('light') @@ -86,12 +88,16 @@ let themeObserver: MutationObserver | null = null const menuItemId = computed(() => route.params.id as string) const menuItem = computed(() => { - const items = appStore.cachedPublicSettings?.custom_menu_items ?? [] - const found = items.find((item) => item.id === menuItemId.value) ?? null - if (found && found.visibility === 'admin' && !authStore.isAdmin) { - return null + const id = menuItemId.value + // Try public settings first (contains user-visible items) + const publicItems = appStore.cachedPublicSettings?.custom_menu_items ?? [] + const found = publicItems.find((item) => item.id === id) ?? null + if (found) return found + // For admin users, also check admin settings (contains admin-only items) + if (authStore.isAdmin) { + return adminSettingsStore.customMenuItems.find((item) => item.id === id) ?? null } - return found + return null }) const embeddedUrl = computed(() => {