mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
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:
44
frontend/src/composables/useSiteInfo.ts
Normal file
44
frontend/src/composables/useSiteInfo.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import apiClient from '@/api/client'
|
||||||
|
|
||||||
|
interface SiteInfo {
|
||||||
|
site_name: string
|
||||||
|
site_subtitle: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模块级缓存,所有组件共享同一份数据
|
||||||
|
const siteName = ref('Aether')
|
||||||
|
const siteSubtitle = ref('AI Gateway')
|
||||||
|
const loaded = ref(false)
|
||||||
|
let fetchPromise: Promise<void> | null = null
|
||||||
|
|
||||||
|
async function fetchSiteInfo() {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<SiteInfo>('/api/public/site-info')
|
||||||
|
siteName.value = response.data.site_name
|
||||||
|
siteSubtitle.value = response.data.site_subtitle
|
||||||
|
loaded.value = true
|
||||||
|
} catch {
|
||||||
|
// 加载失败时保持默认值,允许后续重试
|
||||||
|
fetchPromise = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshSiteInfo() {
|
||||||
|
fetchPromise = null
|
||||||
|
loaded.value = false
|
||||||
|
fetchPromise = fetchSiteInfo()
|
||||||
|
await fetchPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSiteInfo() {
|
||||||
|
if (!loaded.value && !fetchPromise) {
|
||||||
|
fetchPromise = fetchSiteInfo()
|
||||||
|
}
|
||||||
|
return { siteName, siteSubtitle, refreshSiteInfo }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 站点名称变化时同步更新 document.title
|
||||||
|
watch(siteName, (name) => {
|
||||||
|
document.title = name
|
||||||
|
}, { immediate: true })
|
||||||
@@ -9,11 +9,11 @@
|
|||||||
<div class="flex flex-col items-center text-center mb-8">
|
<div class="flex flex-col items-center text-center mb-8">
|
||||||
<img
|
<img
|
||||||
src="/aether_adaptive.svg"
|
src="/aether_adaptive.svg"
|
||||||
alt="Aether"
|
:alt="siteName"
|
||||||
class="h-16 w-16 mb-4"
|
class="h-16 w-16 mb-4"
|
||||||
>
|
>
|
||||||
<h2 class="text-2xl font-semibold text-foreground">
|
<h2 class="text-2xl font-semibold text-foreground">
|
||||||
登录到 Aether
|
登录到 {{ siteName }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -236,6 +236,7 @@ import Input from '@/components/ui/input.vue'
|
|||||||
import Label from '@/components/ui/label.vue'
|
import Label from '@/components/ui/label.vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
|
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||||
import { isDemoMode, DEMO_ACCOUNTS } from '@/config/demo'
|
import { isDemoMode, DEMO_ACCOUNTS } from '@/config/demo'
|
||||||
import RegisterDialog from './RegisterDialog.vue'
|
import RegisterDialog from './RegisterDialog.vue'
|
||||||
import { authApi } from '@/api/auth'
|
import { authApi } from '@/api/auth'
|
||||||
@@ -254,6 +255,7 @@ const emit = defineEmits<{
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const { success: showSuccess, warning: showWarning, error: showError } = useToast()
|
const { success: showSuccess, warning: showWarning, error: showError } = useToast()
|
||||||
|
const { siteName } = useSiteInfo()
|
||||||
|
|
||||||
const isOpen = ref(props.modelValue)
|
const isOpen = ref(props.modelValue)
|
||||||
const isDemo = computed(() => isDemoMode())
|
const isDemo = computed(() => isDemoMode())
|
||||||
|
|||||||
@@ -41,9 +41,9 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">
|
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">
|
||||||
Aether
|
{{ siteName }}
|
||||||
</h1>
|
</h1>
|
||||||
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1.5 font-medium tracking-wide">Multi Private 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>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,9 +105,9 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">
|
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">
|
||||||
Aether
|
{{ siteName }}
|
||||||
</h1>
|
</h1>
|
||||||
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1.5 font-medium tracking-wide">Multi Private 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>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
@@ -333,6 +333,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useModuleStore } from '@/stores/modules'
|
import { useModuleStore } from '@/stores/modules'
|
||||||
import { useDarkMode } from '@/composables/useDarkMode'
|
import { useDarkMode } from '@/composables/useDarkMode'
|
||||||
|
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||||
import { isDemoMode } from '@/config/demo'
|
import { isDemoMode } from '@/config/demo'
|
||||||
import { adminApi, type CheckUpdateResponse } from '@/api/admin'
|
import { adminApi, type CheckUpdateResponse } from '@/api/admin'
|
||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
@@ -378,6 +379,7 @@ const route = useRoute()
|
|||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const moduleStore = useModuleStore()
|
const moduleStore = useModuleStore()
|
||||||
const { themeMode, toggleDarkMode } = useDarkMode()
|
const { themeMode, toggleDarkMode } = useDarkMode()
|
||||||
|
const { siteName, siteSubtitle } = useSiteInfo()
|
||||||
const isDemo = computed(() => isDemoMode())
|
const isDemo = computed(() => isDemoMode())
|
||||||
|
|
||||||
const showAuthError = ref(false)
|
const showAuthError = ref(false)
|
||||||
|
|||||||
@@ -6,6 +6,60 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mt-6 space-y-6">
|
<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
|
<CardSection
|
||||||
title="配置管理"
|
title="配置管理"
|
||||||
@@ -1055,11 +1109,16 @@ import { useToast } from '@/composables/useToast'
|
|||||||
import { adminApi, type ConfigExportData, type ConfigImportResponse, type UsersExportData, type UsersImportResponse } from '@/api/admin'
|
import { adminApi, type ConfigExportData, type ConfigImportResponse, type UsersExportData, type UsersImportResponse } from '@/api/admin'
|
||||||
import { log } from '@/utils/logger'
|
import { log } from '@/utils/logger'
|
||||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||||
|
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||||
|
|
||||||
const { success, error } = useToast()
|
const { success, error } = useToast()
|
||||||
|
const { refreshSiteInfo } = useSiteInfo()
|
||||||
const proxyNodesStore = useProxyNodesStore()
|
const proxyNodesStore = useProxyNodesStore()
|
||||||
|
|
||||||
interface SystemConfig {
|
interface SystemConfig {
|
||||||
|
// 站点信息
|
||||||
|
site_name: string
|
||||||
|
site_subtitle: string
|
||||||
// 网络代理
|
// 网络代理
|
||||||
system_proxy_node_id: string | null
|
system_proxy_node_id: string | null
|
||||||
// 基础配置
|
// 基础配置
|
||||||
@@ -1092,6 +1151,7 @@ interface SystemConfig {
|
|||||||
enable_oauth_token_refresh: boolean
|
enable_oauth_token_refresh: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const siteInfoLoading = ref(false)
|
||||||
const proxyConfigLoading = ref(false)
|
const proxyConfigLoading = ref(false)
|
||||||
const basicConfigLoading = ref(false)
|
const basicConfigLoading = ref(false)
|
||||||
const logConfigLoading = ref(false)
|
const logConfigLoading = ref(false)
|
||||||
@@ -1123,6 +1183,9 @@ const usersMergeModeSelectOpen = ref(false)
|
|||||||
const systemVersion = ref<string>('')
|
const systemVersion = ref<string>('')
|
||||||
|
|
||||||
const systemConfig = ref<SystemConfig>({
|
const systemConfig = ref<SystemConfig>({
|
||||||
|
// 站点信息
|
||||||
|
site_name: 'Aether',
|
||||||
|
site_subtitle: 'AI Gateway',
|
||||||
// 网络代理
|
// 网络代理
|
||||||
system_proxy_node_id: null,
|
system_proxy_node_id: null,
|
||||||
// 基础配置
|
// 基础配置
|
||||||
@@ -1159,6 +1222,14 @@ const systemConfig = ref<SystemConfig>({
|
|||||||
const originalConfig = ref<SystemConfig | null>(null)
|
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(() => {
|
const hasBasicConfigChanges = computed(() => {
|
||||||
if (!originalConfig.value) return false
|
if (!originalConfig.value) return false
|
||||||
return (
|
return (
|
||||||
@@ -1238,6 +1309,9 @@ async function loadSystemVersion() {
|
|||||||
async function loadSystemConfig() {
|
async function loadSystemConfig() {
|
||||||
try {
|
try {
|
||||||
const configs = [
|
const configs = [
|
||||||
|
// 站点信息
|
||||||
|
'site_name',
|
||||||
|
'site_subtitle',
|
||||||
// 网络代理
|
// 网络代理
|
||||||
'system_proxy_node_id',
|
'system_proxy_node_id',
|
||||||
// 基础配置
|
// 基础配置
|
||||||
@@ -1298,6 +1372,32 @@ const hasProxyConfigChanges = computed(() => {
|
|||||||
return systemConfig.value.system_proxy_node_id !== originalConfig.value.system_proxy_node_id
|
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() {
|
async function saveProxyConfig() {
|
||||||
proxyConfigLoading.value = true
|
proxyConfigLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -1755,7 +1855,7 @@ async function handleExportConfig() {
|
|||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
a.href = url
|
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)
|
document.body.appendChild(a)
|
||||||
a.click()
|
a.click()
|
||||||
document.body.removeChild(a)
|
document.body.removeChild(a)
|
||||||
@@ -1847,7 +1947,7 @@ async function handleExportUsers() {
|
|||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
a.href = url
|
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)
|
document.body.appendChild(a)
|
||||||
a.click()
|
a.click()
|
||||||
document.body.removeChild(a)
|
document.body.removeChild(a)
|
||||||
|
|||||||
@@ -34,9 +34,9 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
<h1 class="text-base sm:text-lg font-bold text-[#191919] dark:text-white leading-none">
|
<h1 class="text-base sm:text-lg font-bold text-[#191919] dark:text-white leading-none">
|
||||||
Aether
|
{{ siteName }}
|
||||||
</h1>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -104,9 +104,9 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">
|
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">
|
||||||
Aether
|
{{ siteName }}
|
||||||
</h1>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -458,6 +458,7 @@ import GithubIcon from '@/components/icons/GithubIcon.vue'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useDarkMode } from '@/composables/useDarkMode'
|
import { useDarkMode } from '@/composables/useDarkMode'
|
||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
|
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||||
import LoginDialog from '@/features/auth/components/LoginDialog.vue'
|
import LoginDialog from '@/features/auth/components/LoginDialog.vue'
|
||||||
import RippleLogo from '@/components/RippleLogo.vue'
|
import RippleLogo from '@/components/RippleLogo.vue'
|
||||||
import HeaderLogo from '@/components/HeaderLogo.vue'
|
import HeaderLogo from '@/components/HeaderLogo.vue'
|
||||||
@@ -483,6 +484,7 @@ import {
|
|||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const { isDark, themeMode, toggleDarkMode } = useDarkMode()
|
const { isDark, themeMode, toggleDarkMode } = useDarkMode()
|
||||||
const { copyToClipboard } = useClipboard()
|
const { copyToClipboard } = useClipboard()
|
||||||
|
const { siteName, siteSubtitle } = useSiteInfo()
|
||||||
|
|
||||||
const dashboardPath = computed(() =>
|
const dashboardPath = computed(() =>
|
||||||
authStore.user?.role === 'admin' ? '/admin/dashboard' : '/dashboard'
|
authStore.user?.role === 'admin' ? '/admin/dashboard' : '/dashboard'
|
||||||
@@ -566,10 +568,10 @@ const { claudeConfig, codexConfig, codexAuthConfig, geminiEnvConfig, geminiSetti
|
|||||||
// Dialog state
|
// Dialog state
|
||||||
const showLoginDialog = ref(false)
|
const showLoginDialog = ref(false)
|
||||||
|
|
||||||
// Typewriter effect for "Aether"
|
// Typewriter effect for site name
|
||||||
const aetherText = ref('')
|
const aetherText = ref('')
|
||||||
const showCursor = ref(true)
|
const showCursor = ref(true)
|
||||||
const typewriterFullText = 'Aether'
|
const typewriterFullText = computed(() => siteName.value)
|
||||||
let typewriterTimer: ReturnType<typeof setTimeout> | null = null
|
let typewriterTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
const hasTypewriterStarted = ref(false)
|
const hasTypewriterStarted = ref(false)
|
||||||
|
|
||||||
@@ -578,19 +580,20 @@ const startTypewriter = () => {
|
|||||||
hasTypewriterStarted.value = true
|
hasTypewriterStarted.value = true
|
||||||
aetherText.value = ''
|
aetherText.value = ''
|
||||||
showCursor.value = true
|
showCursor.value = true
|
||||||
|
|
||||||
const typeSpeed = 200
|
const typeSpeed = 200
|
||||||
const deleteSpeed = 120
|
const deleteSpeed = 120
|
||||||
const pauseAfterType = 3500
|
const pauseAfterType = 3500
|
||||||
const pauseAfterDelete = 1000
|
const pauseAfterDelete = 1000
|
||||||
|
|
||||||
const typeLoop = () => {
|
const typeLoop = () => {
|
||||||
let index = 0
|
let index = 0
|
||||||
|
const fullText = typewriterFullText.value
|
||||||
|
|
||||||
// Type phase
|
// Type phase
|
||||||
const typeNextChar = () => {
|
const typeNextChar = () => {
|
||||||
if (index < typewriterFullText.length) {
|
if (index < fullText.length) {
|
||||||
aetherText.value = typewriterFullText.slice(0, index + 1)
|
aetherText.value = fullText.slice(0, index + 1)
|
||||||
index++
|
index++
|
||||||
typewriterTimer = setTimeout(typeNextChar, typeSpeed)
|
typewriterTimer = setTimeout(typeNextChar, typeSpeed)
|
||||||
} else {
|
} else {
|
||||||
@@ -598,7 +601,7 @@ const startTypewriter = () => {
|
|||||||
typewriterTimer = setTimeout(deleteChars, pauseAfterType)
|
typewriterTimer = setTimeout(deleteChars, pauseAfterType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete phase
|
// Delete phase
|
||||||
const deleteChars = () => {
|
const deleteChars = () => {
|
||||||
if (aetherText.value.length > 0) {
|
if (aetherText.value.length > 0) {
|
||||||
@@ -609,7 +612,7 @@ const startTypewriter = () => {
|
|||||||
typewriterTimer = setTimeout(typeLoop, pauseAfterDelete)
|
typewriterTimer = setTimeout(typeLoop, pauseAfterDelete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typeNextChar()
|
typeNextChar()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { ArrowRight, Shuffle, FileCode, Globe, Shield, Check, Info, AlertTriangle, Settings } from 'lucide-vue-next'
|
import { ArrowRight, Shuffle, FileCode, Globe, Shield, Check, Info, AlertTriangle, Settings } from 'lucide-vue-next'
|
||||||
import { panelClasses } from './guide-config'
|
import { panelClasses } from './guide-config'
|
||||||
|
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||||
|
|
||||||
|
const { siteName } = useSiteInfo()
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -162,7 +165,7 @@ const systemSettings = [
|
|||||||
高级功能
|
高级功能
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
|
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
|
||||||
Aether 提供了一些高级功能,帮助你实现更灵活的 API 管理。
|
{{ siteName }} 提供了一些高级功能,帮助你实现更灵活的 API 管理。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { Search, ChevronDown, ExternalLink, HelpCircle } from 'lucide-vue-next'
|
import { Search, ChevronDown, ExternalLink, HelpCircle } from 'lucide-vue-next'
|
||||||
import { faqItems, panelClasses } from './guide-config'
|
import { faqItems, panelClasses } from './guide-config'
|
||||||
|
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||||
|
|
||||||
|
const { siteName } = useSiteInfo()
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -70,7 +73,7 @@ function toggleAll() {
|
|||||||
常见问题
|
常见问题
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
|
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
|
||||||
关于 Aether 使用和配置的常见问题解答。
|
关于 {{ siteName }} 使用和配置的常见问题解答。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
/>
|
/>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
<h1 class="text-base sm:text-lg font-bold text-[#191919] dark:text-white leading-none">
|
<h1 class="text-base sm:text-lg font-bold text-[#191919] dark:text-white leading-none">
|
||||||
Aether
|
{{ siteName }}
|
||||||
</h1>
|
</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>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
<!-- Base URL Input -->
|
<!-- Base URL Input -->
|
||||||
<div class="p-4 border-t border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)]">
|
<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">
|
<label class="block text-xs font-medium text-[#666663] dark:text-muted-foreground mb-2">
|
||||||
Aether Base URL
|
{{ siteName }} Base URL
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
v-model="baseUrl"
|
v-model="baseUrl"
|
||||||
@@ -200,10 +200,12 @@ import {
|
|||||||
import GithubIcon from '@/components/icons/GithubIcon.vue'
|
import GithubIcon from '@/components/icons/GithubIcon.vue'
|
||||||
import HeaderLogo from '@/components/HeaderLogo.vue'
|
import HeaderLogo from '@/components/HeaderLogo.vue'
|
||||||
import { useDarkMode } from '@/composables/useDarkMode'
|
import { useDarkMode } from '@/composables/useDarkMode'
|
||||||
|
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||||
import { guideNavItems } from './guide-config'
|
import { guideNavItems } from './guide-config'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { themeMode, toggleDarkMode } = useDarkMode()
|
const { themeMode, toggleDarkMode } = useDarkMode()
|
||||||
|
const { siteName, siteSubtitle } = useSiteInfo()
|
||||||
|
|
||||||
const showMobileNav = ref(false)
|
const showMobileNav = ref(false)
|
||||||
const baseUrl = ref(typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com')
|
const baseUrl = ref(typeof window !== 'undefined' ? window.location.origin : 'https://your-aether.com')
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { ArrowRight, Server, Layers, Key, Box, ChevronRight } from 'lucide-vue-next'
|
import { ArrowRight, Server, Layers, Key, Box, ChevronRight } from 'lucide-vue-next'
|
||||||
import { coreConcepts, apiFormats, configSteps, panelClasses } from './guide-config'
|
import { coreConcepts, apiFormats, configSteps, panelClasses } from './guide-config'
|
||||||
|
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||||
|
|
||||||
|
const { siteName } = useSiteInfo()
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -41,10 +44,10 @@ const conceptIconColors = {
|
|||||||
<!-- 标题区域 -->
|
<!-- 标题区域 -->
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
|
<h1 class="text-3xl font-bold text-[#262624] dark:text-[#f1ead8]">
|
||||||
欢迎使用 Aether
|
欢迎使用 {{ siteName }}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
|
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
|
||||||
Aether 是一个 AI API 网关,帮助你统一管理多个 AI 服务供应商,实现负载均衡、访问控制和用量统计。
|
{{ siteName }} 是一个 AI API 网关,帮助你统一管理多个 AI 服务供应商,实现负载均衡、访问控制和用量统计。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -54,7 +57,7 @@ const conceptIconColors = {
|
|||||||
核心概念
|
核心概念
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-[#666663] dark:text-[#a3a094]">
|
<p class="text-[#666663] dark:text-[#a3a094]">
|
||||||
理解这四个核心概念,是配置 Aether 的基础:
|
理解这四个核心概念,是配置 {{ siteName }} 的基础:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
@@ -161,7 +164,7 @@ const conceptIconColors = {
|
|||||||
支持的 API 格式
|
支持的 API 格式
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-[#666663] dark:text-[#a3a094]">
|
<p class="text-[#666663] dark:text-[#a3a094]">
|
||||||
Aether 支持多种 API 格式,可以作为不同客户端的统一入口:
|
{{ siteName }} 支持多种 API 格式,可以作为不同客户端的统一入口:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { ArrowRight, Server, Settings, Check, AlertTriangle, Info } from 'lucide-vue-next'
|
import { ArrowRight, Server, Settings, Check, AlertTriangle, Info } from 'lucide-vue-next'
|
||||||
import { apiFormats, panelClasses } from './guide-config'
|
import { apiFormats, panelClasses } from './guide-config'
|
||||||
|
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||||
|
|
||||||
|
const { siteName } = useSiteInfo()
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -73,7 +76,7 @@ const providerExamples = [
|
|||||||
供应商管理
|
供应商管理
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
|
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
|
||||||
供应商和端点是 Aether 的基础配置,决定了系统可以调用哪些 AI 服务。
|
供应商和端点是 {{ siteName }} 的基础配置,决定了系统可以调用哪些 AI 服务。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { ArrowRight, Users, Key, Shield, Check, Info, AlertTriangle, Clock } from 'lucide-vue-next'
|
import { ArrowRight, Users, Key, Shield, Check, Info, AlertTriangle, Clock } from 'lucide-vue-next'
|
||||||
import { panelClasses } from './guide-config'
|
import { panelClasses } from './guide-config'
|
||||||
|
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||||
|
|
||||||
|
const { siteName } = useSiteInfo()
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -46,7 +49,7 @@ const roleComparison = [
|
|||||||
用户与密钥
|
用户与密钥
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
|
<p class="text-lg text-[#666663] dark:text-[#a3a094]">
|
||||||
用户和 API Key 是 Aether 的访问控制核心。通过用户管理分配角色权限,通过 Key 管理控制 API 访问。
|
用户和 API Key 是 {{ siteName }} 的访问控制核心。通过用户管理分配角色权限,通过 Key 管理控制 API 访问。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -192,7 +195,7 @@ const roleComparison = [
|
|||||||
<ul class="space-y-3 text-sm text-[#666663] dark:text-[#a3a094]">
|
<ul class="space-y-3 text-sm text-[#666663] dark:text-[#a3a094]">
|
||||||
<li class="flex items-start gap-2">
|
<li class="flex items-start gap-2">
|
||||||
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
|
<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>
|
||||||
<li class="flex items-start gap-2">
|
<li class="flex items-start gap-2">
|
||||||
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
|
<Check class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
|||||||
@@ -39,11 +39,21 @@ from src.models.endpoint_models import (
|
|||||||
PublicHealthEvent,
|
PublicHealthEvent,
|
||||||
)
|
)
|
||||||
from src.services.health.endpoint import EndpointHealthService
|
from src.services.health.endpoint import EndpointHealthService
|
||||||
|
from src.services.system.config import SystemConfigService
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/public", tags=["System Catalog"])
|
router = APIRouter(prefix="/api/public", tags=["System Catalog"])
|
||||||
pipeline = ApiRequestPipeline()
|
pipeline = ApiRequestPipeline()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/site-info")
|
||||||
|
def get_site_info(db: Session = Depends(get_db)) -> dict[str, str]:
|
||||||
|
"""获取站点基本信息(公开接口,无需认证)"""
|
||||||
|
return {
|
||||||
|
"site_name": SystemConfigService.get_config(db, "site_name", default="Aether"),
|
||||||
|
"site_subtitle": SystemConfigService.get_config(db, "site_subtitle", default="AI Gateway"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/providers", response_model=list[PublicProviderResponse])
|
@router.get("/providers", response_model=list[PublicProviderResponse])
|
||||||
async def get_public_providers(
|
async def get_public_providers(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -130,8 +130,10 @@ class EmailSenderService:
|
|||||||
return False, error
|
return False, error
|
||||||
|
|
||||||
# 生成邮件内容
|
# 生成邮件内容
|
||||||
# 优先使用 email_app_name,否则回退到 smtp_from_name
|
# 优先使用 email_app_name,否则回退到 site_name,最后回退到 smtp_from_name
|
||||||
app_name = SystemConfigService.get_config(db, "email_app_name", default=None)
|
app_name = SystemConfigService.get_config(db, "email_app_name", default=None)
|
||||||
|
if not app_name:
|
||||||
|
app_name = SystemConfigService.get_config(db, "site_name", default=None)
|
||||||
if not app_name:
|
if not app_name:
|
||||||
app_name = SystemConfigService.get_config(db, "smtp_from_name", default="Aether")
|
app_name = SystemConfigService.get_config(db, "smtp_from_name", default="Aether")
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,15 @@ class SystemConfigService:
|
|||||||
|
|
||||||
# 默认配置
|
# 默认配置
|
||||||
DEFAULT_CONFIGS = {
|
DEFAULT_CONFIGS = {
|
||||||
|
# 站点信息
|
||||||
|
"site_name": {
|
||||||
|
"value": "Aether",
|
||||||
|
"description": "站点名称,显示在页面标题、导航栏、登录页面和邮件中",
|
||||||
|
},
|
||||||
|
"site_subtitle": {
|
||||||
|
"value": "AI Gateway",
|
||||||
|
"description": "站点副标题,显示在导航栏品牌名称下方",
|
||||||
|
},
|
||||||
REQUEST_RECORD_LEVEL_KEY: {
|
REQUEST_RECORD_LEVEL_KEY: {
|
||||||
"value": RequestRecordLevel.BASIC.value,
|
"value": RequestRecordLevel.BASIC.value,
|
||||||
"description": "请求记录级别:basic(基本信息), headers(含请求/响应头), full(完整请求/响应)",
|
"description": "请求记录级别:basic(基本信息), headers(含请求/响应头), full(完整请求/响应)",
|
||||||
|
|||||||
Reference in New Issue
Block a user