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">
|
||||
<img
|
||||
src="/aether_adaptive.svg"
|
||||
alt="Aether"
|
||||
:alt="siteName"
|
||||
class="h-16 w-16 mb-4"
|
||||
>
|
||||
<h2 class="text-2xl font-semibold text-foreground">
|
||||
登录到 Aether
|
||||
登录到 {{ siteName }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -236,6 +236,7 @@ import Input from '@/components/ui/input.vue'
|
||||
import Label from '@/components/ui/label.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||
import { isDemoMode, DEMO_ACCOUNTS } from '@/config/demo'
|
||||
import RegisterDialog from './RegisterDialog.vue'
|
||||
import { authApi } from '@/api/auth'
|
||||
@@ -254,6 +255,7 @@ const emit = defineEmits<{
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const { success: showSuccess, warning: showWarning, error: showError } = useToast()
|
||||
const { siteName } = useSiteInfo()
|
||||
|
||||
const isOpen = ref(props.modelValue)
|
||||
const isDemo = computed(() => isDemoMode())
|
||||
|
||||
@@ -41,9 +41,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">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>
|
||||
</RouterLink>
|
||||
</div>
|
||||
@@ -105,9 +105,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">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>
|
||||
</RouterLink>
|
||||
|
||||
@@ -333,6 +333,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useModuleStore } from '@/stores/modules'
|
||||
import { useDarkMode } from '@/composables/useDarkMode'
|
||||
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||
import { isDemoMode } from '@/config/demo'
|
||||
import { adminApi, type CheckUpdateResponse } from '@/api/admin'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
@@ -378,6 +379,7 @@ const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const moduleStore = useModuleStore()
|
||||
const { themeMode, toggleDarkMode } = useDarkMode()
|
||||
const { siteName, siteSubtitle } = useSiteInfo()
|
||||
const isDemo = computed(() => isDemoMode())
|
||||
|
||||
const showAuthError = ref(false)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user