fix: 修复系统设置的动态注入问题

- 创建 useSiteSettings composable 在应用启动时加载系统设置
- 从 window.__SITE_CONFIG__(后端注入)和 localStorage 初始化设置
- 修复 favicon 更新逻辑,正确移除旧 favicon 并添加新的
- 在路由守卫中设置页面标题,格式为 '页面名 - 网站名'
- 简化 admin.vue 和 admin-sidebar 组件,使用统一的设置管理

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 13:15:31 +08:00
parent 7eb232fd8e
commit 53ab7fd778
6 changed files with 245 additions and 115 deletions
+14 -3
View File
@@ -5,22 +5,33 @@
<link rel="icon" type="image/svg+xml" media="(prefers-color-scheme: dark)" href="/logo.svg" /> <link rel="icon" type="image/svg+xml" media="(prefers-color-scheme: dark)" href="/logo.svg" />
<link rel="icon" type="image/svg+xml" media="(prefers-color-scheme: light)" href="/logo-black.svg" /> <link rel="icon" type="image/svg+xml" media="(prefers-color-scheme: light)" href="/logo-black.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{.SiteName}}</title> <title>加载中...</title>
<meta name="description" content="{{.SiteDescription}}" />
<meta name="keywords" content="{{.SiteKeywords}}" />
<!-- Google Fonts - Inter --> <!-- Google Fonts - Inter -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- 后端注入的系统配置(生产环境由后端渲染) -->
<script> <script>
// 后端渲染时会注入这些变量,开发环境使用默认值
window.__SITE_CONFIG__ = { window.__SITE_CONFIG__ = {
siteName: '{{.SiteName}}', siteName: '{{.SiteName}}',
siteDescription: '{{.SiteDescription}}', siteDescription: '{{.SiteDescription}}',
logoLight: '{{.LogoLight}}', logoLight: '{{.LogoLight}}',
logoDark: '{{.LogoDark}}', logoDark: '{{.LogoDark}}',
favicon: '{{.Favicon}}', favicon: '{{.Favicon}}',
};
// 检测是否是开发环境(模板变量未被替换)
if (window.__SITE_CONFIG__.siteName.indexOf('{{') === 0) {
window.__SITE_CONFIG__ = {
siteName: '',
siteDescription: '',
logoLight: '',
logoDark: '',
favicon: '',
};
} }
</script> </script>
</head> </head>
+5
View File
@@ -4,8 +4,13 @@ import { ConfigProvider } from 'reka-ui'
import Loading from '@/components/loading.vue' import Loading from '@/components/loading.vue'
import { Toaster } from '@/components/ui/sonner' import { Toaster } from '@/components/ui/sonner'
import { useSystemTheme } from '@/composables/use-system-theme' import { useSystemTheme } from '@/composables/use-system-theme'
import { useSiteSettings } from '@/composables/use-site-settings'
useSystemTheme() useSystemTheme()
const { initSiteSettings } = useSiteSettings()
// 在应用启动时初始化系统设置
initSiteSettings()
</script> </script>
<template> <template>
+19 -37
View File
@@ -1,13 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Boxes, Code, CreditCard, DollarSign, FileLock, Gauge, GitBranch, HardDrive, Hash, Key, KeyRound, Mail, Megaphone, MessageSquare, Monitor, Network, Plug, ScrollText, Settings, Shield, Smartphone, Users, Variable } from 'lucide-vue-next' import { Boxes, Code, CreditCard, DollarSign, FileLock, Gauge, GitBranch, HardDrive, Hash, Key, KeyRound, Mail, Megaphone, MessageSquare, Monitor, Network, Plug, ScrollText, Settings, Shield, Smartphone, Users, Variable } from 'lucide-vue-next'
import { computed, onMounted, onUnmounted, reactive } from 'vue' import { computed, onMounted, onUnmounted, reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import NavTeam from '@/components/app-sidebar/nav-team.vue' import NavTeam from '@/components/app-sidebar/nav-team.vue'
import TeamSwitcher from '@/components/app-sidebar/team-switcher.vue' import TeamSwitcher from '@/components/app-sidebar/team-switcher.vue'
import NavFooter from '@/components/admin-sidebar/nav-footer.vue' import NavFooter from '@/components/admin-sidebar/nav-footer.vue'
import { useSiteSettings } from '@/composables/use-site-settings'
const { t } = useI18n() const { t } = useI18n()
const { siteSettings } = useSiteSettings()
const user = reactive({ const user = reactive({
name: t('nav.admin'), name: t('nav.admin'),
@@ -16,48 +18,32 @@ const user = reactive({
role: 'admin', role: 'admin',
}) })
const siteSettings = reactive({
name: t('nav.adminDashboard'),
logo: '',
})
const teams = reactive([ const teams = reactive([
{ {
name: t('nav.adminDashboard'), name: t('nav.adminDashboard'),
logo: Code, logo: Code as any,
}, },
]) ])
function loadSystemSettings() { // 监听系统设置变化
const storedSettings = localStorage.getItem('systemSettings') watch(() => siteSettings.siteName, (newName) => {
if (storedSettings) { if (newName) {
try { teams[0].name = newName
const parsed = JSON.parse(storedSettings)
if (parsed.site_name) {
siteSettings.name = parsed.site_name
teams[0].name = parsed.site_name
}
if (parsed.site_logo) {
siteSettings.logo = parsed.site_logo
teams[0].logo = parsed.site_logo
}
}
catch (e) {
console.error('Failed to parse systemSettings from localStorage', e)
}
}
} }
})
function handleSettingsChange(event: CustomEvent) { watch(() => siteSettings.siteLogo, (newLogo) => {
const settings = event.detail if (newLogo) {
if (settings.site_name) { teams[0].logo = newLogo
siteSettings.name = settings.site_name
teams[0].name = settings.site_name
} }
if (settings.site_logo) { })
siteSettings.logo = settings.site_logo
teams[0].logo = settings.site_logo // 初始化时设置
if (siteSettings.siteName) {
teams[0].name = siteSettings.siteName
} }
if (siteSettings.siteLogo) {
teams[0].logo = siteSettings.siteLogo
} }
onMounted(() => { onMounted(() => {
@@ -74,13 +60,9 @@ onMounted(() => {
console.error('Failed to parse user from localStorage', e) console.error('Failed to parse user from localStorage', e)
} }
} }
loadSystemSettings()
window.addEventListener('system-settings-changed', handleSettingsChange as EventListener)
}) })
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('system-settings-changed', handleSettingsChange as EventListener)
}) })
const navMain = computed(() => [ const navMain = computed(() => [
@@ -0,0 +1,194 @@
import { reactive } from 'vue'
import { getFileUrl } from '@/utils/config'
interface SiteSettings {
siteName: string
siteDescription: string
siteLogo: string
siteFavicon: string
logoLight: string
logoDark: string
loaded: boolean
}
const siteSettings = reactive<SiteSettings>({
siteName: '',
siteDescription: '',
siteLogo: '',
siteFavicon: '',
logoLight: '',
logoDark: '',
loaded: false,
})
function updateFavicon(favicon: string) {
if (!favicon)
return
const head = document.getElementsByTagName('head')[0]
// 移除旧的 favicon
const existingLinks = head.querySelectorAll('link[rel*="icon"]')
existingLinks.forEach((link) => link.remove())
// 添加新的 favicon
const link = document.createElement('link')
link.type = 'image/x-icon'
link.rel = 'shortcut icon'
link.href = getFileUrl(favicon)
head.appendChild(link)
// 同时添加 icon 类型的 link
const linkIcon = document.createElement('link')
linkIcon.type = 'image/x-icon'
linkIcon.rel = 'icon'
linkIcon.href = getFileUrl(favicon)
head.appendChild(linkIcon)
}
function updateTitle(title?: string) {
if (title) {
document.title = title
} else if (siteSettings.siteName) {
document.title = siteSettings.siteName
}
}
function applySettings(settings: Partial<SiteSettings>) {
if (settings.siteName) {
siteSettings.siteName = settings.siteName
updateTitle(settings.siteName)
}
if (settings.siteDescription) {
siteSettings.siteDescription = settings.siteDescription
}
if (settings.siteLogo) {
siteSettings.siteLogo = settings.siteLogo
}
if (settings.siteFavicon) {
siteSettings.siteFavicon = settings.siteFavicon
updateFavicon(settings.siteFavicon)
}
if (settings.logoLight) {
siteSettings.logoLight = settings.logoLight
}
if (settings.logoDark) {
siteSettings.logoDark = settings.logoDark
}
siteSettings.loaded = true
}
// 从 window.__SITE_CONFIG__ 获取初始配置(由后端注入)
function initFromServerConfig() {
const config = (window as any).__SITE_CONFIG__
if (config) {
applySettings({
siteName: config.siteName || '',
siteDescription: config.siteDescription || '',
siteLogo: config.logoLight || config.logoDark || '',
logoLight: config.logoLight || '',
logoDark: config.logoDark || '',
favicon: config.favicon || '',
})
return true
}
return false
}
// 从 localStorage 获取缓存的设置
function initFromLocalStorage() {
const storedSettings = localStorage.getItem('systemSettings')
if (storedSettings) {
try {
const parsed = JSON.parse(storedSettings)
applySettings({
siteName: parsed.site_name || '',
siteDescription: parsed.site_description || '',
siteLogo: parsed.site_logo || '',
siteFavicon: parsed.site_favicon || '',
})
return true
}
catch (e) {
console.error('Failed to parse systemSettings from localStorage', e)
}
}
return false
}
// 从 API 获取最新设置
async function fetchSettings() {
try {
const response = await fetch('/api/v1/dev/system-settings', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`,
},
})
const data = await response.json()
if (data.code === 200 && data.data) {
const settings = {
site_name: data.data.site_name || '',
site_description: data.data.site_description || '',
site_logo: data.data.site_logo || '',
site_favicon: data.data.site_favicon || '',
}
localStorage.setItem('systemSettings', JSON.stringify(settings))
applySettings({
siteName: settings.site_name,
siteDescription: settings.site_description,
siteLogo: settings.site_logo,
siteFavicon: settings.site_favicon,
})
// 触发事件通知其他组件
window.dispatchEvent(new CustomEvent('system-settings-changed', { detail: settings }))
}
}
catch (error) {
console.error('Failed to fetch system settings:', error)
}
}
// 初始化系统设置(在应用启动时调用)
async function initSiteSettings() {
// 优先从后端注入的配置获取
const hasServerConfig = initFromServerConfig()
// 然后从 localStorage 获取缓存
const hasLocalConfig = initFromLocalStorage()
// 如果都没有,尝试从 API 获取
if (!hasServerConfig && !hasLocalConfig) {
await fetchSettings()
}
// 监听设置变化
window.addEventListener('system-settings-changed', ((event: CustomEvent) => {
applySettings({
siteName: event.detail.site_name,
siteDescription: event.detail.site_description,
siteLogo: event.detail.site_logo,
siteFavicon: event.detail.site_favicon,
})
}) as EventListener)
}
// 获取带页面标题的完整标题
function getPageTitle(pageTitle?: string): string {
if (pageTitle && siteSettings.siteName) {
return `${pageTitle} - ${siteSettings.siteName}`
}
if (siteSettings.siteName) {
return siteSettings.siteName
}
return pageTitle || ''
}
export function useSiteSettings() {
return {
siteSettings,
initSiteSettings,
updateTitle,
getPageTitle,
fetchSettings,
}
}
+1 -71
View File
@@ -1,13 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import AdminSidebar from '@/components/admin-sidebar/index.vue' import AdminSidebar from '@/components/admin-sidebar/index.vue'
import LanguageChange from '@/components/language-change.vue' import LanguageChange from '@/components/language-change.vue'
import ToggleTheme from '@/components/toggle-theme.vue' import ToggleTheme from '@/components/toggle-theme.vue'
import api from '@/services/api'
import { getFileUrl } from '@/utils/config'
const router = useRouter() const router = useRouter()
const { t } = useI18n() const { t } = useI18n()
@@ -73,74 +71,6 @@ const breadcrumbs = computed(() => {
return crumbs return crumbs
}) })
function getAssetUrl(path: string) {
return getFileUrl(path)
}
function updateFavicon(favicon: string) {
if (!favicon)
return
const link: HTMLLinkElement = document.querySelector('link[rel*=\'icon\']') || document.createElement('link')
link.type = 'image/x-icon'
link.rel = 'shortcut icon'
link.href = getAssetUrl(favicon)
document.getElementsByTagName('head')[0].appendChild(link)
}
function updateSiteName(name: string) {
if (name)
document.title = name
}
function applySettings(settings: { site_name?: string, site_logo?: string, site_favicon?: string }) {
if (settings.site_favicon)
updateFavicon(settings.site_favicon)
if (settings.site_name)
updateSiteName(settings.site_name)
}
function handleSettingsChange(event: CustomEvent) {
applySettings(event.detail)
}
async function loadSystemSettings() {
const storedSettings = localStorage.getItem('systemSettings')
if (storedSettings) {
try {
applySettings(JSON.parse(storedSettings))
}
catch (e) {
console.error('Failed to parse systemSettings', e)
}
return
}
try {
const data = await api.get<any>('/dev/system-settings')
if (data) {
const settings = {
site_name: data.site_name || '',
site_logo: data.site_logo || '',
site_favicon: data.site_favicon || '',
}
localStorage.setItem('systemSettings', JSON.stringify(settings))
window.dispatchEvent(new CustomEvent('system-settings-changed', { detail: settings }))
}
}
catch (error) {
console.error('加载系统设置失败:', error)
}
}
onMounted(() => {
loadSystemSettings()
window.addEventListener('system-settings-changed', handleSettingsChange as EventListener)
})
onUnmounted(() => {
window.removeEventListener('system-settings-changed', handleSettingsChange as EventListener)
})
</script> </script>
<template> <template>
+8
View File
@@ -1,4 +1,5 @@
import type { Router } from 'vue-router' import type { Router } from 'vue-router'
import { useSiteSettings } from '@/composables/use-site-settings'
const INSTALL_CACHE_KEY = 'app_installed' const INSTALL_CACHE_KEY = 'app_installed'
const INSTALL_CACHE_EXPIRY = 24 * 60 * 60 * 1000 const INSTALL_CACHE_EXPIRY = 24 * 60 * 60 * 1000
@@ -93,4 +94,11 @@ export function createRouterGuard(router: Router) {
return '/admin' return '/admin'
} }
}) })
// 设置页面标题
router.afterEach((to) => {
const { siteSettings, getPageTitle } = useSiteSettings()
const pageTitle = to.meta?.title as string | undefined
document.title = getPageTitle(pageTitle)
})
} }