Make enabled model plaza discoverable from /home

The model plaza route already supports public access, but both built-in /home headers omit its entry. Add the link to compact and default headers while keeping the existing feature and authentication settings authoritative, then cover the visibility matrix with focused component tests.

Constraint: Keep the change frontend-only and preserve router-owned access control

Rejected: Add the link to AppHeader only | /home renders its own headers and never mounts AppHeader

Confidence: high

Scope-risk: narrow

Reversibility: clean

Directive: Keep the model plaza entry gated by the existing opt-in flag and require-auth setting

Tested: HomeView focused Vitest, full frontend Vitest (223 files / 1554 tests), ESLint, vue-tsc, production build

Not-tested: Manual browser click-through against a running backend

Related: #5524
This commit is contained in:
yan9651688
2026-08-17 10:22:32 +08:00
committed by shaw
parent 219368ec6d
commit d9d2854d27
2 changed files with 90 additions and 0 deletions
+28
View File
@@ -40,6 +40,15 @@
>
<Icon name="book" size="md" />
</a>
<router-link
v-if="showModelPlazaEntry"
to="/model-plaza"
class="flex h-10 shrink-0 items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-dark-400 dark:hover:bg-dark-800 dark:hover:text-white"
:title="t('nav.modelPlaza')"
>
<Icon name="grid" size="md" />
<span class="hidden sm:inline">{{ t('nav.modelPlaza') }}</span>
</router-link>
<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')"
@@ -132,6 +141,17 @@
<Icon name="book" size="md" />
</a>
<!-- Model Plaza Link -->
<router-link
v-if="showModelPlazaEntry"
to="/model-plaza"
class="inline-flex items-center gap-1.5 rounded-lg p-2 text-sm text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:text-dark-400 dark:hover:bg-dark-800 dark:hover:text-white"
:title="t('nav.modelPlaza')"
>
<Icon name="grid" size="md" />
<span class="hidden sm:inline">{{ t('nav.modelPlaza') }}</span>
</router-link>
<!-- Theme Toggle -->
<button
@click="toggleTheme"
@@ -480,6 +500,7 @@ import { useAuthStore, useAppStore } from '@/stores'
import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue'
import Icon from '@/components/icons/Icon.vue'
import { sanitizeUrl } from '@/utils/url'
import { FeatureFlags, isFeatureFlagEnabled } from '@/utils/featureFlags'
const { t } = useI18n()
@@ -494,6 +515,7 @@ const docUrl = computed(() => sanitizeUrl(appStore.cachedPublicSettings?.doc_url
const homeContent = computed(() => appStore.cachedPublicSettings?.home_content || '')
const hasHomeContent = computed(() => homeContent.value.trim().length > 0)
const compactHomeEnabled = computed(() => appStore.cachedPublicSettings?.compact_home_enabled === true)
const modelPlazaEnabled = computed(() => isFeatureFlagEnabled(FeatureFlags.modelPlaza))
// Check if homeContent is a URL (for iframe display)
const isHomeContentUrl = computed(() => {
@@ -509,6 +531,12 @@ const githubUrl = 'https://github.com/Wei-Shaw/sub2api'
// Auth state
const isAuthenticated = computed(() => authStore.isAuthenticated)
const modelPlazaRequiresAuth = computed(
() => appStore.cachedPublicSettings?.model_plaza_require_auth === true,
)
const showModelPlazaEntry = computed(
() => modelPlazaEnabled.value && (isAuthenticated.value || !modelPlazaRequiresAuth.value),
)
const isAdmin = computed(() => authStore.isAdmin)
const dashboardPath = computed(() => isAdmin.value ? '/admin/dashboard' : '/dashboard')
const userInitial = computed(() => {
@@ -25,6 +25,10 @@ vi.mock('@/stores', () => ({
useAuthStore: () => authStore,
}))
vi.mock('@/stores/app', () => ({
useAppStore: () => appStore,
}))
vi.mock('vue-i18n', async (importOriginal) => {
const actual = await importOriginal<typeof import('vue-i18n')>()
return {
@@ -55,6 +59,13 @@ function compactDestination(wrapper: ReturnType<typeof mountHome>) {
return wrapper.get('[data-testid="compact-home"]').findComponent(RouterLinkStub).props('to')
}
function modelPlazaDestination(wrapper: ReturnType<typeof mountHome>) {
return wrapper
.findAllComponents(RouterLinkStub)
.find((link) => link.props('to') === '/model-plaza')
?.props('to')
}
describe('HomeView compact mode', () => {
beforeEach(() => {
authStore.isAuthenticated = false
@@ -119,4 +130,55 @@ describe('HomeView compact mode', () => {
expect(authStore.checkAuth).toHaveBeenCalledOnce()
expect(appStore.fetchPublicSettings).not.toHaveBeenCalled()
})
it('shows the model plaza link to anonymous visitors when public access is enabled', () => {
const wrapper = mountHome({
compact_home_enabled: true,
model_plaza_enabled: true,
model_plaza_require_auth: false,
})
expect(modelPlazaDestination(wrapper)).toBe('/model-plaza')
})
it('hides the model plaza link from anonymous visitors when sign-in is required', () => {
const wrapper = mountHome({
compact_home_enabled: true,
model_plaza_enabled: true,
model_plaza_require_auth: true,
})
expect(modelPlazaDestination(wrapper)).toBeUndefined()
})
it('shows the model plaza link to authenticated visitors when sign-in is required', () => {
authStore.isAuthenticated = true
const wrapper = mountHome({
compact_home_enabled: true,
model_plaza_enabled: true,
model_plaza_require_auth: true,
})
expect(modelPlazaDestination(wrapper)).toBe('/model-plaza')
})
it('shows the model plaza link in the default home header', () => {
const wrapper = mountHome({
model_plaza_enabled: true,
model_plaza_require_auth: false,
})
expect(modelPlazaDestination(wrapper)).toBe('/model-plaza')
})
it('hides the model plaza link when the feature is disabled', () => {
const wrapper = mountHome({
compact_home_enabled: true,
model_plaza_enabled: false,
model_plaza_require_auth: false,
})
expect(modelPlazaDestination(wrapper)).toBeUndefined()
})
})