Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
0c4f1762
Unverified
Commit
0c4f1762
authored
Jan 11, 2026
by
Wesley Liddick
Committed by
GitHub
Jan 11, 2026
Browse files
Merge pull request #232 from Edric-Li/feat/api-key-ip-restriction
feat(settings): 首页自定义内容 & 配置注入优化
parents
24d19a5f
0fa5a601
Changes
26
Show whitespace changes
Inline
Side-by-side
frontend/src/stores/app.ts
View file @
0c4f1762
...
@@ -279,11 +279,31 @@ export const useAppStore = defineStore('app', () => {
...
@@ -279,11 +279,31 @@ export const useAppStore = defineStore('app', () => {
// ==================== Public Settings Management ====================
// ==================== Public Settings Management ====================
/**
* Apply settings to store state (internal helper to avoid code duplication)
*/
function
applySettings
(
config
:
PublicSettings
):
void
{
cachedPublicSettings
.
value
=
config
siteName
.
value
=
config
.
site_name
||
'
Sub2API
'
siteLogo
.
value
=
config
.
site_logo
||
''
siteVersion
.
value
=
config
.
version
||
''
contactInfo
.
value
=
config
.
contact_info
||
''
apiBaseUrl
.
value
=
config
.
api_base_url
||
''
docUrl
.
value
=
config
.
doc_url
||
''
publicSettingsLoaded
.
value
=
true
}
/**
/**
* Fetch public settings (uses cache unless force=true)
* Fetch public settings (uses cache unless force=true)
* @param force - Force refresh from API
* @param force - Force refresh from API
*/
*/
async
function
fetchPublicSettings
(
force
=
false
):
Promise
<
PublicSettings
|
null
>
{
async
function
fetchPublicSettings
(
force
=
false
):
Promise
<
PublicSettings
|
null
>
{
// Check for injected config from server (eliminates flash)
if
(
!
publicSettingsLoaded
.
value
&&
!
force
&&
window
.
__APP_CONFIG__
)
{
applySettings
(
window
.
__APP_CONFIG__
)
return
window
.
__APP_CONFIG__
}
// Return cached data if available and not forcing refresh
// Return cached data if available and not forcing refresh
if
(
publicSettingsLoaded
.
value
&&
!
force
)
{
if
(
publicSettingsLoaded
.
value
&&
!
force
)
{
if
(
cachedPublicSettings
.
value
)
{
if
(
cachedPublicSettings
.
value
)
{
...
@@ -300,6 +320,7 @@ export const useAppStore = defineStore('app', () => {
...
@@ -300,6 +320,7 @@ export const useAppStore = defineStore('app', () => {
api_base_url
:
apiBaseUrl
.
value
,
api_base_url
:
apiBaseUrl
.
value
,
contact_info
:
contactInfo
.
value
,
contact_info
:
contactInfo
.
value
,
doc_url
:
docUrl
.
value
,
doc_url
:
docUrl
.
value
,
home_content
:
''
,
linuxdo_oauth_enabled
:
false
,
linuxdo_oauth_enabled
:
false
,
version
:
siteVersion
.
value
version
:
siteVersion
.
value
}
}
...
@@ -313,14 +334,7 @@ export const useAppStore = defineStore('app', () => {
...
@@ -313,14 +334,7 @@ export const useAppStore = defineStore('app', () => {
publicSettingsLoading
.
value
=
true
publicSettingsLoading
.
value
=
true
try
{
try
{
const
data
=
await
fetchPublicSettingsAPI
()
const
data
=
await
fetchPublicSettingsAPI
()
cachedPublicSettings
.
value
=
data
applySettings
(
data
)
siteName
.
value
=
data
.
site_name
||
'
Sub2API
'
siteLogo
.
value
=
data
.
site_logo
||
''
siteVersion
.
value
=
data
.
version
||
''
contactInfo
.
value
=
data
.
contact_info
||
''
apiBaseUrl
.
value
=
data
.
api_base_url
||
''
docUrl
.
value
=
data
.
doc_url
||
''
publicSettingsLoaded
.
value
=
true
return
data
return
data
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'
Failed to fetch public settings:
'
,
error
)
console
.
error
(
'
Failed to fetch public settings:
'
,
error
)
...
@@ -338,6 +352,19 @@ export const useAppStore = defineStore('app', () => {
...
@@ -338,6 +352,19 @@ export const useAppStore = defineStore('app', () => {
cachedPublicSettings
.
value
=
null
cachedPublicSettings
.
value
=
null
}
}
/**
* Initialize settings from injected config (window.__APP_CONFIG__)
* This is called synchronously before Vue app mounts to prevent flash
* @returns true if config was found and applied, false otherwise
*/
function
initFromInjectedConfig
():
boolean
{
if
(
window
.
__APP_CONFIG__
)
{
applySettings
(
window
.
__APP_CONFIG__
)
return
true
}
return
false
}
// ==================== Return Store API ====================
// ==================== Return Store API ====================
return
{
return
{
...
@@ -355,6 +382,7 @@ export const useAppStore = defineStore('app', () => {
...
@@ -355,6 +382,7 @@ export const useAppStore = defineStore('app', () => {
contactInfo
,
contactInfo
,
apiBaseUrl
,
apiBaseUrl
,
docUrl
,
docUrl
,
cachedPublicSettings
,
// Version state
// Version state
versionLoaded
,
versionLoaded
,
...
@@ -391,6 +419,7 @@ export const useAppStore = defineStore('app', () => {
...
@@ -391,6 +419,7 @@ export const useAppStore = defineStore('app', () => {
// Public settings actions
// Public settings actions
fetchPublicSettings
,
fetchPublicSettings
,
clearPublicSettingsCache
clearPublicSettingsCache
,
initFromInjectedConfig
}
}
})
})
frontend/src/types/global.d.ts
0 → 100644
View file @
0c4f1762
import
type
{
PublicSettings
}
from
'
@/types
'
declare
global
{
interface
Window
{
__APP_CONFIG__
?:
PublicSettings
}
}
export
{}
frontend/src/types/index.ts
View file @
0c4f1762
...
@@ -74,6 +74,7 @@ export interface PublicSettings {
...
@@ -74,6 +74,7 @@ export interface PublicSettings {
api_base_url
:
string
api_base_url
:
string
contact_info
:
string
contact_info
:
string
doc_url
:
string
doc_url
:
string
home_content
:
string
linuxdo_oauth_enabled
:
boolean
linuxdo_oauth_enabled
:
boolean
version
:
string
version
:
string
}
}
...
...
frontend/src/views/HomeView.vue
View file @
0c4f1762
<
template
>
<
template
>
<!-- Custom Home Content: Full Page Mode -->
<div
v-if=
"homeContent"
class=
"min-h-screen"
>
<!-- iframe mode -->
<iframe
v-if=
"isHomeContentUrl"
:src=
"homeContent.trim()"
class=
"h-screen w-full border-0"
allowfullscreen
></iframe>
<!-- HTML mode - SECURITY: homeContent is admin-only setting, XSS risk is acceptable -->
<div
v-else
v-html=
"homeContent"
></div>
</div>
<!-- Default Home Page -->
<div
<div
class=
"relative min-h-screen overflow-hidden bg-gradient-to-br from-gray-50 via-primary-50/30 to-gray-100 dark:from-dark-950 dark:via-dark-900 dark:to-dark-950"
v-else
class=
"relative flex min-h-screen flex-col overflow-hidden bg-gradient-to-br from-gray-50 via-primary-50/30 to-gray-100 dark:from-dark-950 dark:via-dark-900 dark:to-dark-950"
>
>
<!-- Background Decorations -->
<!-- Background Decorations -->
<div
class=
"pointer-events-none absolute inset-0 overflow-hidden"
>
<div
class=
"pointer-events-none absolute inset-0 overflow-hidden"
>
...
@@ -96,7 +111,7 @@
...
@@ -96,7 +111,7 @@
</header>
</header>
<!-- Main Content -->
<!-- Main Content -->
<main
class=
"relative z-10 px-6 py-16"
>
<main
class=
"relative z-10
flex-1
px-6 py-16"
>
<div
class=
"mx-auto max-w-6xl"
>
<div
class=
"mx-auto max-w-6xl"
>
<!-- Hero Section - Left/Right Layout -->
<!-- Hero Section - Left/Right Layout -->
<div
class=
"mb-12 flex flex-col items-center justify-between gap-12 lg:flex-row lg:gap-16"
>
<div
class=
"mb-12 flex flex-col items-center justify-between gap-12 lg:flex-row lg:gap-16"
>
...
@@ -392,21 +407,27 @@
...
@@ -392,21 +407,27 @@
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
computed
,
onMounted
}
from
'
vue
'
import
{
ref
,
computed
,
onMounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
getPublicSettings
}
from
'
@/api/auth
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
import
{
useAuthStore
}
from
'
@/stores
'
import
LocaleSwitcher
from
'
@/components/common/LocaleSwitcher.vue
'
import
LocaleSwitcher
from
'
@/components/common/LocaleSwitcher.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
{
sanitizeUrl
}
from
'
@/utils/url
'
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
authStore
=
useAuthStore
()
const
authStore
=
useAuthStore
()
const
appStore
=
useAppStore
()
// Site settings
const
siteName
=
ref
(
'
Sub2API
'
)
// Site settings - directly from appStore (already initialized from injected config)
const
siteLogo
=
ref
(
''
)
const
siteName
=
computed
(()
=>
appStore
.
cachedPublicSettings
?.
site_name
||
appStore
.
siteName
||
'
Sub2API
'
)
const
siteSubtitle
=
ref
(
'
AI API Gateway Platform
'
)
const
siteLogo
=
computed
(()
=>
appStore
.
cachedPublicSettings
?.
site_logo
||
appStore
.
siteLogo
||
''
)
const
docUrl
=
ref
(
''
)
const
siteSubtitle
=
computed
(()
=>
appStore
.
cachedPublicSettings
?.
site_subtitle
||
'
AI API Gateway Platform
'
)
const
docUrl
=
computed
(()
=>
appStore
.
cachedPublicSettings
?.
doc_url
||
appStore
.
docUrl
||
''
)
const
homeContent
=
computed
(()
=>
appStore
.
cachedPublicSettings
?.
home_content
||
''
)
// Check if homeContent is a URL (for iframe display)
const
isHomeContentUrl
=
computed
(()
=>
{
const
content
=
homeContent
.
value
.
trim
()
return
content
.
startsWith
(
'
http://
'
)
||
content
.
startsWith
(
'
https://
'
)
})
// Theme
// Theme
const
isDark
=
ref
(
document
.
documentElement
.
classList
.
contains
(
'
dark
'
))
const
isDark
=
ref
(
document
.
documentElement
.
classList
.
contains
(
'
dark
'
))
...
@@ -446,20 +467,15 @@ function initTheme() {
...
@@ -446,20 +467,15 @@ function initTheme() {
}
}
}
}
onMounted
(
async
()
=>
{
onMounted
(()
=>
{
initTheme
()
initTheme
()
// Check auth state
// Check auth state
authStore
.
checkAuth
()
authStore
.
checkAuth
()
try
{
// Ensure public settings are loaded (will use cache if already loaded from injected config)
const
settings
=
await
getPublicSettings
()
if
(
!
appStore
.
publicSettingsLoaded
)
{
siteName
.
value
=
settings
.
site_name
||
'
Sub2API
'
appStore
.
fetchPublicSettings
()
siteLogo
.
value
=
sanitizeUrl
(
settings
.
site_logo
||
''
,
{
allowRelative
:
true
})
siteSubtitle
.
value
=
settings
.
site_subtitle
||
'
AI API Gateway Platform
'
docUrl
.
value
=
sanitizeUrl
(
settings
.
doc_url
||
''
,
{
allowRelative
:
true
})
}
catch
(
error
)
{
console
.
error
(
'
Failed to load public settings:
'
,
error
)
}
}
})
})
</
script
>
</
script
>
...
...
frontend/src/views/admin/SettingsView.vue
View file @
0c4f1762
...
@@ -562,6 +562,26 @@
...
@@ -562,6 +562,26 @@
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Home Content -->
<div>
<label
class=
"mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.settings.site.homeContent
'
)
}}
</label>
<textarea
v-model=
"form.home_content"
rows=
"6"
class=
"input font-mono text-sm"
:placeholder=
"t('admin.settings.site.homeContentPlaceholder')"
></textarea>
<p
class=
"mt-1.5 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.settings.site.homeContentHint
'
)
}}
</p>
<!-- iframe CSP Warning -->
<p
class=
"mt-2 text-xs text-amber-600 dark:text-amber-400"
>
{{
t
(
'
admin.settings.site.homeContentIframeWarning
'
)
}}
</p>
</div>
</div>
</div>
</div>
</div>
...
@@ -837,6 +857,7 @@ const form = reactive<SettingsForm>({
...
@@ -837,6 +857,7 @@ const form = reactive<SettingsForm>({
api_base_url
:
''
,
api_base_url
:
''
,
contact_info
:
''
,
contact_info
:
''
,
doc_url
:
''
,
doc_url
:
''
,
home_content
:
''
,
smtp_host
:
''
,
smtp_host
:
''
,
smtp_port
:
587
,
smtp_port
:
587
,
smtp_username
:
''
,
smtp_username
:
''
,
...
@@ -945,6 +966,7 @@ async function saveSettings() {
...
@@ -945,6 +966,7 @@ async function saveSettings() {
api_base_url
:
form
.
api_base_url
,
api_base_url
:
form
.
api_base_url
,
contact_info
:
form
.
contact_info
,
contact_info
:
form
.
contact_info
,
doc_url
:
form
.
doc_url
,
doc_url
:
form
.
doc_url
,
home_content
:
form
.
home_content
,
smtp_host
:
form
.
smtp_host
,
smtp_host
:
form
.
smtp_host
,
smtp_port
:
form
.
smtp_port
,
smtp_port
:
form
.
smtp_port
,
smtp_username
:
form
.
smtp_username
,
smtp_username
:
form
.
smtp_username
,
...
...
frontend/vite.config.ts
View file @
0c4f1762
import
{
defineConfig
}
from
'
vite
'
import
{
defineConfig
,
Plugin
}
from
'
vite
'
import
vue
from
'
@vitejs/plugin-vue
'
import
vue
from
'
@vitejs/plugin-vue
'
import
checker
from
'
vite-plugin-checker
'
import
checker
from
'
vite-plugin-checker
'
import
{
resolve
}
from
'
path
'
import
{
resolve
}
from
'
path
'
/**
* Vite 插件:开发模式下注入公开配置到 index.html
* 与生产模式的后端注入行为保持一致,消除闪烁
*/
function
injectPublicSettings
():
Plugin
{
const
backendUrl
=
process
.
env
.
VITE_DEV_PROXY_TARGET
||
'
http://localhost:8080
'
return
{
name
:
'
inject-public-settings
'
,
transformIndexHtml
:
{
order
:
'
pre
'
,
async
handler
(
html
)
{
try
{
const
response
=
await
fetch
(
`
${
backendUrl
}
/api/v1/settings/public`
,
{
signal
:
AbortSignal
.
timeout
(
2000
)
})
if
(
response
.
ok
)
{
const
data
=
await
response
.
json
()
if
(
data
.
code
===
0
&&
data
.
data
)
{
const
script
=
`<script>window.__APP_CONFIG__=
${
JSON
.
stringify
(
data
.
data
)}
;</script>`
return
html
.
replace
(
'
</head>
'
,
`
${
script
}
\n</head>`
)
}
}
}
catch
(
e
)
{
console
.
warn
(
'
[vite] 无法获取公开配置,将回退到 API 调用:
'
,
(
e
as
Error
).
message
)
}
return
html
}
}
}
}
export
default
defineConfig
({
export
default
defineConfig
({
plugins
:
[
plugins
:
[
...
@@ -10,7 +41,8 @@ export default defineConfig({
...
@@ -10,7 +41,8 @@ export default defineConfig({
checker
({
checker
({
typescript
:
true
,
typescript
:
true
,
vueTsc
:
true
vueTsc
:
true
})
}),
injectPublicSettings
()
],
],
resolve
:
{
resolve
:
{
alias
:
{
alias
:
{
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment