mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 16:26:30 +00:00
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:
@@ -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">
|
||||
© {{ 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()
|
||||
})
|
||||
})
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user