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', () => {
// ==================== 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)
* @param force - Force refresh from API
*/
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
if
(
publicSettingsLoaded
.
value
&&
!
force
)
{
if
(
cachedPublicSettings
.
value
)
{
...
...
@@ -300,6 +320,7 @@ export const useAppStore = defineStore('app', () => {
api_base_url
:
apiBaseUrl
.
value
,
contact_info
:
contactInfo
.
value
,
doc_url
:
docUrl
.
value
,
home_content
:
''
,
linuxdo_oauth_enabled
:
false
,
version
:
siteVersion
.
value
}
...
...
@@ -313,14 +334,7 @@ export const useAppStore = defineStore('app', () => {
publicSettingsLoading
.
value
=
true
try
{
const
data
=
await
fetchPublicSettingsAPI
()
cachedPublicSettings
.
value
=
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
applySettings
(
data
)
return
data
}
catch
(
error
)
{
console
.
error
(
'
Failed to fetch public settings:
'
,
error
)
...
...
@@ -338,6 +352,19 @@ export const useAppStore = defineStore('app', () => {
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
{
...
...
@@ -355,6 +382,7 @@ export const useAppStore = defineStore('app', () => {
contactInfo
,
apiBaseUrl
,
docUrl
,
cachedPublicSettings
,
// Version state
versionLoaded
,
...
...
@@ -391,6 +419,7 @@ export const useAppStore = defineStore('app', () => {
// Public settings actions
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 {
api_base_url
:
string
contact_info
:
string
doc_url
:
string
home_content
:
string
linuxdo_oauth_enabled
:
boolean
version
:
string
}
...
...
frontend/src/views/HomeView.vue
View file @
0c4f1762
<
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
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 -->
<div
class=
"pointer-events-none absolute inset-0 overflow-hidden"
>
...
...
@@ -96,7 +111,7 @@
</header>
<!-- 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"
>
<!-- Hero Section - Left/Right Layout -->
<div
class=
"mb-12 flex flex-col items-center justify-between gap-12 lg:flex-row lg:gap-16"
>
...
...
@@ -392,21 +407,27 @@
<
script
setup
lang=
"ts"
>
import
{
ref
,
computed
,
onMounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
getPublicSettings
}
from
'
@/api/auth
'
import
{
useAuthStore
}
from
'
@/stores
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
import
LocaleSwitcher
from
'
@/components/common/LocaleSwitcher.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
{
sanitizeUrl
}
from
'
@/utils/url
'
const
{
t
}
=
useI18n
()
const
authStore
=
useAuthStore
()
// Site settings
const
siteName
=
ref
(
'
Sub2API
'
)
const
siteLogo
=
ref
(
''
)
const
siteSubtitle
=
ref
(
'
AI API Gateway Platform
'
)
const
docUrl
=
ref
(
''
)
const
appStore
=
useAppStore
()
// Site settings - directly from appStore (already initialized from injected config)
const
siteName
=
computed
(()
=>
appStore
.
cachedPublicSettings
?.
site_name
||
appStore
.
siteName
||
'
Sub2API
'
)
const
siteLogo
=
computed
(()
=>
appStore
.
cachedPublicSettings
?.
site_logo
||
appStore
.
siteLogo
||
''
)
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
const
isDark
=
ref
(
document
.
documentElement
.
classList
.
contains
(
'
dark
'
))
...
...
@@ -446,20 +467,15 @@ function initTheme() {
}
}
onMounted
(
async
()
=>
{
onMounted
(()
=>
{
initTheme
()
// Check auth state
authStore
.
checkAuth
()
try
{
const
settings
=
await
getPublicSettings
()
siteName
.
value
=
settings
.
site_name
||
'
Sub2API
'
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
)
// Ensure public settings are loaded (will use cache if already loaded from injected config)
if
(
!
appStore
.
publicSettingsLoaded
)
{
appStore
.
fetchPublicSettings
()
}
})
</
script
>
...
...
frontend/src/views/admin/SettingsView.vue
View file @
0c4f1762
...
...
@@ -562,6 +562,26 @@
</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>
...
...
@@ -837,6 +857,7 @@ const form = reactive<SettingsForm>({
api_base_url
:
''
,
contact_info
:
''
,
doc_url
:
''
,
home_content
:
''
,
smtp_host
:
''
,
smtp_port
:
587
,
smtp_username
:
''
,
...
...
@@ -945,6 +966,7 @@ async function saveSettings() {
api_base_url
:
form
.
api_base_url
,
contact_info
:
form
.
contact_info
,
doc_url
:
form
.
doc_url
,
home_content
:
form
.
home_content
,
smtp_host
:
form
.
smtp_host
,
smtp_port
:
form
.
smtp_port
,
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
checker
from
'
vite-plugin-checker
'
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
({
plugins
:
[
...
...
@@ -10,7 +41,8 @@ export default defineConfig({
checker
({
typescript
:
true
,
vueTsc
:
true
})
}),
injectPublicSettings
()
],
resolve
:
{
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