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
+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();