feat(settings): add home content customization and config injection
- Add home_content setting for custom homepage (HTML or iframe URL) - Inject public settings into index.html to eliminate page flash - Support ETag caching with automatic invalidation on settings update - Add Vite plugin for dev mode settings injection - Refactor HomeView to use appStore instead of local API calls
This commit is contained in:
@@ -1,6 +1,21 @@
|
||||
<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()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// Site settings
|
||||
const siteName = ref('Sub2API')
|
||||
const siteLogo = ref('')
|
||||
const siteSubtitle = ref('AI API Gateway Platform')
|
||||
const docUrl = ref('')
|
||||
// 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>
|
||||
|
||||
@@ -562,6 +562,22 @@
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -837,6 +853,7 @@ const form = reactive<SettingsForm>({
|
||||
api_base_url: '',
|
||||
contact_info: '',
|
||||
doc_url: '',
|
||||
home_content: '',
|
||||
smtp_host: '',
|
||||
smtp_port: 587,
|
||||
smtp_username: '',
|
||||
@@ -945,6 +962,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,
|
||||
|
||||
Reference in New Issue
Block a user