feat: 支持系统设置中自定义全站名称和副标题

新增 site_name 和 site_subtitle 两个系统配置项,允许管理员在后台自定义
站点品牌名称(默认 Aether)和副标题(默认 AI Gateway)。

- 后端:DEFAULT_CONFIGS 新增配置项,公开 /api/public/site-info 端点
- 后端:邮件发送 app_name 回退链增加 site_name
- 前端:新增 useSiteInfo composable 全局缓存站点信息
- 前端:首页、后台布局、登录页、指南页面全部改为动态读取
- 前端:系统设置页新增「站点信息」配置区块
- 前端:配置导出文件名跟随站点名称
- 优化:请求失败允许重试,保存后即时生效无需刷新页面
- 优化:document.title 随站点名称动态更新

Closes #176

Co-authored-by: LewisPen <LewisPen@nyadoo.com>
This commit is contained in:
fawney19
2026-02-13 21:50:05 +08:00
parent c171d65d3f
commit f464f32e48
14 changed files with 223 additions and 34 deletions

View File

@@ -6,6 +6,60 @@
/>
<div class="mt-6 space-y-6">
<!-- 站点信息 -->
<CardSection
title="站点信息"
description="自定义站点名称和副标题,影响导航栏、登录页、指南页面和邮件等全站显示"
>
<template #actions>
<Button
size="sm"
:disabled="siteInfoLoading || !hasSiteInfoChanges"
@click="saveSiteInfo"
>
{{ siteInfoLoading ? '保存中...' : '保存' }}
</Button>
</template>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label
for="site-name"
class="block text-sm font-medium"
>
站点名称
</Label>
<Input
id="site-name"
v-model="systemConfig.site_name"
type="text"
placeholder="Aether"
class="mt-1"
/>
<p class="mt-1 text-xs text-muted-foreground">
显示在导航栏登录页标题和邮件中
</p>
</div>
<div>
<Label
for="site-subtitle"
class="block text-sm font-medium"
>
站点副标题
</Label>
<Input
id="site-subtitle"
v-model="systemConfig.site_subtitle"
type="text"
placeholder="AI Gateway"
class="mt-1"
/>
<p class="mt-1 text-xs text-muted-foreground">
显示在导航栏品牌名称下方
</p>
</div>
</div>
</CardSection>
<!-- 配置导出/导入 -->
<CardSection
title="配置管理"
@@ -1055,11 +1109,16 @@ import { useToast } from '@/composables/useToast'
import { adminApi, type ConfigExportData, type ConfigImportResponse, type UsersExportData, type UsersImportResponse } from '@/api/admin'
import { log } from '@/utils/logger'
import { useProxyNodesStore } from '@/stores/proxy-nodes'
import { useSiteInfo } from '@/composables/useSiteInfo'
const { success, error } = useToast()
const { refreshSiteInfo } = useSiteInfo()
const proxyNodesStore = useProxyNodesStore()
interface SystemConfig {
// 站点信息
site_name: string
site_subtitle: string
// 网络代理
system_proxy_node_id: string | null
// 基础配置
@@ -1092,6 +1151,7 @@ interface SystemConfig {
enable_oauth_token_refresh: boolean
}
const siteInfoLoading = ref(false)
const proxyConfigLoading = ref(false)
const basicConfigLoading = ref(false)
const logConfigLoading = ref(false)
@@ -1123,6 +1183,9 @@ const usersMergeModeSelectOpen = ref(false)
const systemVersion = ref<string>('')
const systemConfig = ref<SystemConfig>({
// 站点信息
site_name: 'Aether',
site_subtitle: 'AI Gateway',
// 网络代理
system_proxy_node_id: null,
// 基础配置
@@ -1159,6 +1222,14 @@ const systemConfig = ref<SystemConfig>({
const originalConfig = ref<SystemConfig | null>(null)
// 检测各模块是否有变动
const hasSiteInfoChanges = computed(() => {
if (!originalConfig.value) return false
return (
systemConfig.value.site_name !== originalConfig.value.site_name ||
systemConfig.value.site_subtitle !== originalConfig.value.site_subtitle
)
})
const hasBasicConfigChanges = computed(() => {
if (!originalConfig.value) return false
return (
@@ -1238,6 +1309,9 @@ async function loadSystemVersion() {
async function loadSystemConfig() {
try {
const configs = [
// 站点信息
'site_name',
'site_subtitle',
// 网络代理
'system_proxy_node_id',
// 基础配置
@@ -1298,6 +1372,32 @@ const hasProxyConfigChanges = computed(() => {
return systemConfig.value.system_proxy_node_id !== originalConfig.value.system_proxy_node_id
})
async function saveSiteInfo() {
siteInfoLoading.value = true
try {
const configItems = [
{ key: 'site_name', value: systemConfig.value.site_name, description: '站点名称' },
{ key: 'site_subtitle', value: systemConfig.value.site_subtitle, description: '站点副标题' },
]
await Promise.all(
configItems.map(item =>
adminApi.updateSystemConfig(item.key, item.value, item.description)
)
)
if (originalConfig.value) {
originalConfig.value.site_name = systemConfig.value.site_name
originalConfig.value.site_subtitle = systemConfig.value.site_subtitle
}
await refreshSiteInfo()
success('站点信息已保存')
} catch (err) {
error('保存站点信息失败')
log.error('保存站点信息失败:', err)
} finally {
siteInfoLoading.value = false
}
}
async function saveProxyConfig() {
proxyConfigLoading.value = true
try {
@@ -1755,7 +1855,7 @@ async function handleExportConfig() {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `aether-config-${new Date().toISOString().slice(0, 10)}.json`
a.download = `${systemConfig.value.site_name.toLowerCase()}-config-${new Date().toISOString().slice(0, 10)}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
@@ -1847,7 +1947,7 @@ async function handleExportUsers() {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `aether-users-${new Date().toISOString().slice(0, 10)}.json`
a.download = `${systemConfig.value.site_name.toLowerCase()}-users-${new Date().toISOString().slice(0, 10)}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)