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

@@ -34,9 +34,9 @@
/>
<div class="flex flex-col justify-center">
<h1 class="text-base sm:text-lg font-bold text-[#191919] dark:text-white leading-none">
Aether
{{ siteName }}
</h1>
<span class="text-[9px] sm:text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1 sm:mt-1.5 font-medium tracking-wide">AI Gateway</span>
<span class="text-[9px] sm:text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1 sm:mt-1.5 font-medium tracking-wide">{{ siteSubtitle }}</span>
</div>
</div>
@@ -104,9 +104,9 @@
/>
<div class="flex flex-col justify-center">
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">
Aether
{{ siteName }}
</h1>
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1.5 font-medium tracking-wide">AI Gateway</span>
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1.5 font-medium tracking-wide">{{ siteSubtitle }}</span>
</div>
</div>
@@ -458,6 +458,7 @@ import GithubIcon from '@/components/icons/GithubIcon.vue'
import { useAuthStore } from '@/stores/auth'
import { useDarkMode } from '@/composables/useDarkMode'
import { useClipboard } from '@/composables/useClipboard'
import { useSiteInfo } from '@/composables/useSiteInfo'
import LoginDialog from '@/features/auth/components/LoginDialog.vue'
import RippleLogo from '@/components/RippleLogo.vue'
import HeaderLogo from '@/components/HeaderLogo.vue'
@@ -483,6 +484,7 @@ import {
const authStore = useAuthStore()
const { isDark, themeMode, toggleDarkMode } = useDarkMode()
const { copyToClipboard } = useClipboard()
const { siteName, siteSubtitle } = useSiteInfo()
const dashboardPath = computed(() =>
authStore.user?.role === 'admin' ? '/admin/dashboard' : '/dashboard'
@@ -566,10 +568,10 @@ const { claudeConfig, codexConfig, codexAuthConfig, geminiEnvConfig, geminiSetti
// Dialog state
const showLoginDialog = ref(false)
// Typewriter effect for "Aether"
// Typewriter effect for site name
const aetherText = ref('')
const showCursor = ref(true)
const typewriterFullText = 'Aether'
const typewriterFullText = computed(() => siteName.value)
let typewriterTimer: ReturnType<typeof setTimeout> | null = null
const hasTypewriterStarted = ref(false)
@@ -578,19 +580,20 @@ const startTypewriter = () => {
hasTypewriterStarted.value = true
aetherText.value = ''
showCursor.value = true
const typeSpeed = 200
const deleteSpeed = 120
const pauseAfterType = 3500
const pauseAfterDelete = 1000
const typeLoop = () => {
let index = 0
const fullText = typewriterFullText.value
// Type phase
const typeNextChar = () => {
if (index < typewriterFullText.length) {
aetherText.value = typewriterFullText.slice(0, index + 1)
if (index < fullText.length) {
aetherText.value = fullText.slice(0, index + 1)
index++
typewriterTimer = setTimeout(typeNextChar, typeSpeed)
} else {
@@ -598,7 +601,7 @@ const startTypewriter = () => {
typewriterTimer = setTimeout(deleteChars, pauseAfterType)
}
}
// Delete phase
const deleteChars = () => {
if (aetherText.value.length > 0) {
@@ -609,7 +612,7 @@ const startTypewriter = () => {
typewriterTimer = setTimeout(typeLoop, pauseAfterDelete)
}
}
typeNextChar()
}

View File

@@ -2,6 +2,9 @@
import { RouterLink } from 'vue-router'
import { ArrowRight, Shuffle, FileCode, Globe, Shield, Check, Info, AlertTriangle, Settings } from 'lucide-vue-next'
import { panelClasses } from './guide-config'
import { useSiteInfo } from '@/composables/useSiteInfo'
const { siteName } = useSiteInfo()
withDefaults(
defineProps<{
@@ -162,7 +165,7 @@ const systemSettings = [
高级功能
</h1>
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
Aether 提供了一些高级功能帮助你实现更灵活的 API 管理
{{ siteName }} 提供了一些高级功能帮助你实现更灵活的 API 管理
</p>
</div>

View File

@@ -2,6 +2,9 @@
import { ref, computed } from 'vue'
import { Search, ChevronDown, ExternalLink, HelpCircle } from 'lucide-vue-next'
import { faqItems, panelClasses } from './guide-config'
import { useSiteInfo } from '@/composables/useSiteInfo'
const { siteName } = useSiteInfo()
withDefaults(
defineProps<{
@@ -70,7 +73,7 @@ function toggleAll() {
常见问题
</h1>
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
关于 Aether 使用和配置的常见问题解答
关于 {{ siteName }} 使用和配置的常见问题解答
</p>
</div>

View File

@@ -14,9 +14,9 @@
/>
<div class="flex flex-col justify-center">
<h1 class="text-base sm:text-lg font-bold text-[#191919] dark:text-white leading-none">
Aether
{{ siteName }}
</h1>
<span class="text-[9px] sm:text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1 sm:mt-1.5 font-medium tracking-wide">AI Gateway</span>
<span class="text-[9px] sm:text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1 sm:mt-1.5 font-medium tracking-wide">{{ siteSubtitle }}</span>
</div>
</RouterLink>
@@ -157,7 +157,7 @@
<!-- Base URL Input -->
<div class="p-4 border-t border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)]">
<label class="block text-xs font-medium text-[#666663] dark:text-muted-foreground mb-2">
Aether Base URL
{{ siteName }} Base URL
</label>
<input
v-model="baseUrl"
@@ -200,10 +200,12 @@ import {
import GithubIcon from '@/components/icons/GithubIcon.vue'
import HeaderLogo from '@/components/HeaderLogo.vue'
import { useDarkMode } from '@/composables/useDarkMode'
import { useSiteInfo } from '@/composables/useSiteInfo'
import { guideNavItems } from './guide-config'
const route = useRoute()
const { themeMode, toggleDarkMode } = useDarkMode()
const { siteName, siteSubtitle } = useSiteInfo()
const showMobileNav = ref(false)
const baseUrl = ref(typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com')

View File

@@ -2,6 +2,9 @@
import { RouterLink } from 'vue-router'
import { ArrowRight, Server, Layers, Key, Box, ChevronRight } from 'lucide-vue-next'
import { coreConcepts, apiFormats, configSteps, panelClasses } from './guide-config'
import { useSiteInfo } from '@/composables/useSiteInfo'
const { siteName } = useSiteInfo()
withDefaults(
defineProps<{
@@ -41,10 +44,10 @@ const conceptIconColors = {
<!-- 标题区域 -->
<div class="space-y-4">
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
欢迎使用 Aether
欢迎使用 {{ siteName }}
</h1>
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
Aether 是一个 AI API 网关帮助你统一管理多个 AI 服务供应商实现负载均衡访问控制和用量统计
{{ siteName }} 是一个 AI API 网关帮助你统一管理多个 AI 服务供应商实现负载均衡访问控制和用量统计
</p>
</div>
@@ -54,7 +57,7 @@ const conceptIconColors = {
核心概念
</h2>
<p class="text-[#666663] dark:text-[#a3a094]">
理解这四个核心概念是配置 Aether 的基础
理解这四个核心概念是配置 {{ siteName }} 的基础
</p>
<div class="grid gap-4 sm:grid-cols-2">
@@ -161,7 +164,7 @@ const conceptIconColors = {
支持的 API 格式
</h2>
<p class="text-[#666663] dark:text-[#a3a094]">
Aether 支持多种 API 格式可以作为不同客户端的统一入口
{{ siteName }} 支持多种 API 格式可以作为不同客户端的统一入口
</p>
<div

View File

@@ -2,6 +2,9 @@
import { RouterLink } from 'vue-router'
import { ArrowRight, Server, Settings, Check, AlertTriangle, Info } from 'lucide-vue-next'
import { apiFormats, panelClasses } from './guide-config'
import { useSiteInfo } from '@/composables/useSiteInfo'
const { siteName } = useSiteInfo()
withDefaults(
defineProps<{
@@ -73,7 +76,7 @@ const providerExamples = [
供应商管理
</h1>
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
供应商和端点是 Aether 的基础配置决定了系统可以调用哪些 AI 服务
供应商和端点是 {{ siteName }} 的基础配置决定了系统可以调用哪些 AI 服务
</p>
</div>

View File

@@ -2,6 +2,9 @@
import { RouterLink } from 'vue-router'
import { ArrowRight, Users, Key, Shield, Check, Info, AlertTriangle, Clock } from 'lucide-vue-next'
import { panelClasses } from './guide-config'
import { useSiteInfo } from '@/composables/useSiteInfo'
const { siteName } = useSiteInfo()
withDefaults(
defineProps<{
@@ -46,7 +49,7 @@ const roleComparison = [
用户与密钥
</h1>
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
用户和 API Key Aether 的访问控制核心通过用户管理分配角色权限通过 Key 管理控制 API 访问
用户和 API Key {{ siteName }} 的访问控制核心通过用户管理分配角色权限通过 Key 管理控制 API 访问
</p>
</div>
@@ -192,7 +195,7 @@ const roleComparison = [
<ul class="space-y-3 text-sm text-[#666663] dark:text-[#a3a094]">
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>API Key 是用户调用 Aether API 的凭证</span>
<span>API Key 是用户调用 {{ siteName }} API 的凭证</span>
</li>
<li class="flex items-start gap-2">
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />