Merge pull request #5094 from wucm667/feat/issue-5065-compact-homepage

feat(home): add compact home page preset to avoid abuse classification
This commit is contained in:
Wesley Liddick
2026-07-31 21:51:33 +08:00
committed by GitHub
26 changed files with 300 additions and 1 deletions
+2
View File
@@ -440,6 +440,7 @@ export interface SystemSettings {
contact_info: string;
doc_url: string;
home_content: string;
compact_home_enabled: boolean;
hide_ccs_import_button: boolean;
table_default_page_size: number;
table_page_size_options: number[];
@@ -752,6 +753,7 @@ export interface UpdateSettingsRequest {
contact_info?: string;
doc_url?: string;
home_content?: string;
compact_home_enabled?: boolean;
hide_ccs_import_button?: boolean;
table_default_page_size?: number;
table_page_size_options?: number[];
@@ -73,6 +73,7 @@ function buildPublicSettings(overrides: Partial<WeChatPublicSettings> = {}): WeC
contact_info: '',
doc_url: '',
home_content: '',
compact_home_enabled: false,
hide_ccs_import_button: false,
payment_enabled: false,
table_default_page_size: 20,
@@ -250,6 +250,7 @@ describe('ProfileIdentityBindingsSection', () => {
contact_info: '',
doc_url: '',
home_content: '',
compact_home_enabled: false,
hide_ccs_import_button: false,
payment_enabled: false,
table_default_page_size: 20,
@@ -549,6 +549,8 @@ export default {
homeContentPlaceholder: 'Enter custom content for the home page. Supports Markdown & HTML. If a URL is entered, it will be displayed as an iframe.',
homeContentHint: 'Customize the home page content. Supports Markdown/HTML. If you enter a URL (starting with http:// or https://), it will be used as an iframe src to embed an external page. When set, the default status information will no longer be displayed.',
homeContentIframeWarning: '⚠️ iframe mode note: Some websites have X-Frame-Options or CSP security policies that prevent embedding in iframes. If the page appears blank or shows an error, please verify the target website allows embedding, or consider using HTML mode to build your own content.',
compactHome: 'Compact Home Page',
compactHomeHint: 'Show a restrained site identity page when no custom home page content is set.',
hideCcsImportButton: 'Hide CCS Import Button',
hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page'
},
@@ -544,6 +544,8 @@ export default {
'自定义首页内容,支持 Markdown/HTML。如果输入的是链接(以 http:// 或 https:// 开头),则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。设置后首页的状态信息将不再显示。',
homeContentIframeWarning:
'⚠️ iframe 模式提示:部分网站设置了 X-Frame-Options 或 CSP 安全策略,禁止被嵌入到 iframe 中。如果页面显示空白或报错,请确认目标网站允许被嵌入,或考虑使用 HTML 模式自行构建页面内容。',
compactHome: '简洁首页',
compactHomeHint: '未设置自定义首页内容时,展示简洁的站点信息页面。',
hideCcsImportButton: '隐藏 CCS 导入按钮',
hideCcsImportButtonHint: '启用后将在 API Keys 页面隐藏"导入 CCS"按钮'
},
@@ -33,6 +33,7 @@ function createPublicSettings(overrides: Partial<PublicSettings> = {}): PublicSe
contact_info: '',
doc_url: '',
home_content: '',
compact_home_enabled: false,
hide_ccs_import_button: false,
payment_enabled: false,
risk_control_enabled: false,
@@ -452,6 +453,7 @@ describe('useAppStore', () => {
contact_info: '',
doc_url: '',
home_content: '',
compact_home_enabled: false,
hide_ccs_import_button: false,
purchase_subscription_enabled: false,
purchase_subscription_url: '',
+1
View File
@@ -342,6 +342,7 @@ export const useAppStore = defineStore('app', () => {
contact_info: contactInfo.value,
doc_url: docUrl.value,
home_content: '',
compact_home_enabled: false,
hide_ccs_import_button: false,
payment_enabled: false,
table_default_page_size: 20,
+1
View File
@@ -210,6 +210,7 @@ export interface PublicSettings {
contact_info: string
doc_url: string
home_content: string
compact_home_enabled: boolean
hide_ccs_import_button: boolean
payment_enabled: boolean
risk_control_enabled: boolean
+72 -1
View File
@@ -1,6 +1,6 @@
<template>
<!-- Custom Home Content: Full Page Mode -->
<div v-if="homeContent" class="min-h-screen">
<div v-if="hasHomeContent" class="min-h-screen">
<!-- iframe mode -->
<iframe
v-if="isHomeContentUrl"
@@ -12,6 +12,75 @@
<div v-else v-html="homeContent"></div>
</div>
<!-- Compact Home Page -->
<div
v-else-if="compactHomeEnabled"
data-testid="compact-home"
class="flex min-h-screen flex-col bg-gray-50 text-gray-900 dark:bg-dark-950 dark:text-white"
>
<header class="border-b border-gray-200 px-4 py-4 sm:px-6 dark:border-dark-800">
<nav class="mx-auto flex max-w-5xl flex-wrap items-center justify-between gap-3 sm:gap-4">
<div class="flex min-w-0 flex-1 items-center gap-3">
<img
:src="siteLogo || '/logo.svg'"
alt="Logo"
class="h-9 w-9 shrink-0 rounded-lg object-contain"
/>
<span class="min-w-0 truncate text-base font-semibold">{{ siteName }}</span>
</div>
<div class="flex max-w-full shrink-0 flex-wrap items-center justify-end gap-2">
<LocaleSwitcher />
<a
v-if="docUrl"
:href="docUrl"
target="_blank"
rel="noopener noreferrer"
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-gray-500 hover:bg-gray-100 dark:text-dark-400 dark:hover:bg-dark-800"
:title="t('home.viewDocs')"
>
<Icon name="book" size="md" />
</a>
<button
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-gray-500 hover:bg-gray-100 dark:text-dark-400 dark:hover:bg-dark-800"
:title="isDark ? t('home.switchToLight') : t('home.switchToDark')"
@click="toggleTheme"
>
<Icon v-if="isDark" name="sun" size="md" />
<Icon v-else name="moon" size="md" />
</button>
<router-link
:to="isAuthenticated ? dashboardPath : '/login'"
class="inline-flex min-h-10 shrink-0 items-center justify-center rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-200"
>
{{ isAuthenticated ? t('home.dashboard') : t('home.login') }}
</router-link>
</div>
</nav>
</header>
<main class="flex min-w-0 flex-1 items-center justify-center px-4 py-16 sm:px-6">
<div class="min-w-0 max-w-2xl text-center">
<img
:src="siteLogo || '/logo.svg'"
alt="Logo"
class="mx-auto mb-6 h-20 w-20 rounded-2xl object-contain"
/>
<h1 class="[overflow-wrap:anywhere] text-3xl font-bold md:text-4xl">{{ siteName }}</h1>
<p class="mt-4 whitespace-pre-wrap [overflow-wrap:anywhere] text-base text-gray-600 dark:text-dark-300">{{ siteSubtitle }}</p>
<router-link
:to="isAuthenticated ? dashboardPath : '/login'"
class="mt-8 inline-flex min-h-10 items-center justify-center rounded-lg bg-primary-600 px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-700"
>
{{ isAuthenticated ? t('home.goToDashboard') : t('home.login') }}
</router-link>
</div>
</main>
<footer class="min-w-0 border-t border-gray-200 px-4 py-5 text-center text-sm text-gray-500 [overflow-wrap:anywhere] sm:px-6 dark:border-dark-800 dark:text-dark-400">
&copy; {{ currentYear }} {{ siteName }}
</footer>
</div>
<!-- Default Home Page -->
<div
v-else
@@ -423,6 +492,8 @@ const siteLogo = computed(() => sanitizeUrl(appStore.cachedPublicSettings?.site_
const siteSubtitle = computed(() => appStore.cachedPublicSettings?.site_subtitle || 'AI API Gateway Platform')
const docUrl = computed(() => sanitizeUrl(appStore.cachedPublicSettings?.doc_url || appStore.docUrl || ''))
const homeContent = computed(() => appStore.cachedPublicSettings?.home_content || '')
const hasHomeContent = computed(() => homeContent.value.trim().length > 0)
const compactHomeEnabled = computed(() => appStore.cachedPublicSettings?.compact_home_enabled === true)
// Check if homeContent is a URL (for iframe display)
const isHomeContentUrl = computed(() => {
@@ -0,0 +1,122 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mount, RouterLinkStub } from '@vue/test-utils'
import HomeView from '../HomeView.vue'
const { appStore, authStore } = vi.hoisted(() => ({
appStore: {
cachedPublicSettings: {} as Record<string, unknown>,
siteName: 'Fallback site',
siteLogo: '',
docUrl: '',
publicSettingsLoaded: true,
fetchPublicSettings: vi.fn(),
},
authStore: {
isAuthenticated: false,
isAdmin: false,
user: null as { email?: string } | null,
checkAuth: vi.fn(),
},
}))
vi.mock('@/stores', () => ({
useAppStore: () => appStore,
useAuthStore: () => authStore,
}))
vi.mock('vue-i18n', async (importOriginal) => {
const actual = await importOriginal<typeof import('vue-i18n')>()
return {
...actual,
useI18n: () => ({ t: (key: string) => key }),
}
})
function mountHome(settings: Record<string, unknown> = {}) {
appStore.cachedPublicSettings = {
site_name: 'Test site',
site_subtitle: 'Test subtitle',
...settings,
}
return mount(HomeView, {
global: {
stubs: {
RouterLink: RouterLinkStub,
LocaleSwitcher: { template: '<div data-testid="locale-switcher" />' },
Icon: { template: '<span data-testid="icon" />' },
},
},
})
}
function compactDestination(wrapper: ReturnType<typeof mountHome>) {
return wrapper.get('[data-testid="compact-home"]').findComponent(RouterLinkStub).props('to')
}
describe('HomeView compact mode', () => {
beforeEach(() => {
authStore.isAuthenticated = false
authStore.isAdmin = false
authStore.user = null
authStore.checkAuth.mockClear()
appStore.fetchPublicSettings.mockClear()
localStorage.clear()
vi.spyOn(window, 'matchMedia').mockReturnValue({ matches: false } as MediaQueryList)
})
it('renders custom HTML ahead of compact mode', () => {
const wrapper = mountHome({
compact_home_enabled: true,
home_content: '<section id="custom-home">Custom home</section>',
})
expect(wrapper.get('#custom-home').text()).toBe('Custom home')
expect(wrapper.find('[data-testid="compact-home"]').exists()).toBe(false)
})
it('renders custom URL content ahead of compact mode', () => {
const wrapper = mountHome({
compact_home_enabled: true,
home_content: ' https://example.com/home ',
})
expect(wrapper.get('iframe').attributes('src')).toBe('https://example.com/home')
expect(wrapper.find('[data-testid="compact-home"]').exists()).toBe(false)
})
it('treats whitespace-only custom content as empty and selects compact mode', () => {
const wrapper = mountHome({ compact_home_enabled: true, home_content: ' \n\t ' })
expect(wrapper.get('[data-testid="compact-home"]').text()).toContain('Test site')
})
it.each([undefined, false])('selects the default home when compact mode is %s', (enabled) => {
const settings = enabled === undefined ? {} : { compact_home_enabled: enabled }
const wrapper = mountHome(settings)
expect(wrapper.find('[data-testid="compact-home"]').exists()).toBe(false)
expect(wrapper.find('.terminal-container').exists()).toBe(true)
})
it('links unauthenticated visitors to login', () => {
expect(compactDestination(mountHome({ compact_home_enabled: true }))).toBe('/login')
})
it('links authenticated users to their dashboard', () => {
authStore.isAuthenticated = true
expect(compactDestination(mountHome({ compact_home_enabled: true }))).toBe('/dashboard')
})
it('links administrators to the admin dashboard', () => {
authStore.isAuthenticated = true
authStore.isAdmin = true
const wrapper = mountHome({ compact_home_enabled: true })
expect(compactDestination(wrapper)).toBe('/admin/dashboard')
expect(authStore.checkAuth).toHaveBeenCalledOnce()
expect(appStore.fetchPublicSettings).not.toHaveBeenCalled()
})
})
+15
View File
@@ -5977,6 +5977,19 @@
</p>
</div>
<!-- Compact Home Page -->
<div class="flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700">
<div>
<label class="font-medium text-gray-900 dark:text-white">{{
t("admin.settings.site.compactHome")
}}</label>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t("admin.settings.site.compactHomeHint") }}
</p>
</div>
<Toggle v-model="form.compact_home_enabled" data-testid="compact-home-toggle" />
</div>
<!-- Hide CCS Import Button -->
<div
class="flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
@@ -8837,6 +8850,7 @@ const form = reactive<SettingsForm>({
contact_info: "",
doc_url: "",
home_content: "",
compact_home_enabled: false,
backend_mode_enabled: false,
hide_ccs_import_button: false,
payment_enabled: false,
@@ -10361,6 +10375,7 @@ async function saveSettings() {
contact_info: form.contact_info,
doc_url: form.doc_url,
home_content: form.home_content,
compact_home_enabled: form.compact_home_enabled,
backend_mode_enabled: form.backend_mode_enabled,
hide_ccs_import_button: form.hide_ccs_import_button,
table_default_page_size: form.table_default_page_size,
@@ -378,6 +378,7 @@ const baseSettingsResponse = {
contact_info: "",
doc_url: "",
home_content: "",
compact_home_enabled: false,
hide_ccs_import_button: false,
table_default_page_size: 20,
table_page_size_options: [10, 20, 50, 100],
@@ -680,6 +681,22 @@ describe("admin SettingsView payment visible method controls", () => {
adminSettingsFetch.mockResolvedValue(undefined);
});
it("submits the compact home page toggle", async () => {
const wrapper = mountView();
await flushPromises();
const toggle = wrapper.get('[data-testid="compact-home-toggle"]');
expect((toggle.element as HTMLInputElement).checked).toBe(false);
await toggle.setValue(true);
await wrapper.find("form").trigger("submit.prevent");
await flushPromises();
expect(updateSettings).toHaveBeenCalledWith(
expect.objectContaining({ compact_home_enabled: true }),
);
});
it("renders panel rate limit card and saves settings", async () => {
getPanelRateLimitSettings.mockClear();
updatePanelRateLimitSettings.mockClear();