This commit is contained in:
2026-08-03 10:51:53 +08:00
parent beb86d53e7
commit f098610a6c
15 changed files with 125 additions and 115 deletions
+21 -1
View File
@@ -3,13 +3,15 @@ import { readFileSync } from 'node:fs'
import test from 'node:test'
const layoutSource = readFileSync(new URL('./AppLayout.vue', import.meta.url), 'utf8')
const shellStyles = readFileSync(new URL('../styles/shell.css', import.meta.url), 'utf8')
const globalStyles = readFileSync(new URL('../styles/index.css', import.meta.url), 'utf8')
const routerSource = readFileSync(new URL('../router/index.ts', import.meta.url), 'utf8')
const sidebarMenuSource = readFileSync(new URL('../components/ClientSidebarMenu.vue', import.meta.url), 'utf8')
const sidebarMenuNodeSource = readFileSync(new URL('../components/ClientSidebarMenuNode.vue', import.meta.url), 'utf8')
test('source contract: renders the grouped sidebar menu and mobile drawer controls', () => {
assert.match(layoutSource, /import \{ computed, nextTick, onBeforeUnmount, onMounted, ref, watch \} from 'vue'/)
assert.match(layoutSource, /import \{[\s\S]*?\bX,[\s\S]*?\} from '@lucide\/vue'/)
assert.match(layoutSource, /import \{[\s\S]*?\bX\s*\} from '@lucide\/vue'/)
assert.match(layoutSource, /import ClientSidebarMenu from '@\/components\/ClientSidebarMenu\.vue'/)
assert.match(layoutSource, /:class="\{ 'mobile-menu-open': mobileMenuOpen \}"/)
assert.match(layoutSource, /<button\s+v-if="mobileMenuOpen"[^>]*class="client-sidebar-scrim"[^>]*aria-label="关闭导航"[^>]*@click="closeMobileMenu"/s)
@@ -42,6 +44,24 @@ test('source contract: removes the header and main content from the accessibilit
assert.match(layoutSource, /<main\s+class="client-main"\s+:inert="mobileMenuOpen"\s+:aria-hidden="mobileMenuOpen \? 'true' : undefined"\s*>/)
})
test('source contract: keeps route context and user actions in the compact header without tenant identity', () => {
assert.match(layoutSource, /const pageTitle = computed\(\(\) => route\.meta\.title \|\| '业务总览'\)/)
assert.match(layoutSource, /const pageDescription = computed\(\(\) => route\.meta\.description \|\| ''\)/)
assert.match(layoutSource, /<div class="client-header-heading">\s*<h1>\{\{ pageTitle \}\}<\/h1>\s*<p v-if="pageDescription">\{\{ pageDescription \}\}<\/p>/s)
assert.match(layoutSource, /<div class="client-user">[\s\S]*\{\{ auth\.user\?\.name \}\}[\s\S]*aria-label="退出登录"/)
assert.doesNotMatch(layoutSource, /Building2|tenant-name|tenantName/)
assert.doesNotMatch(routerSource, /showPageHeading|showTenantName/)
})
test('source contract: uses compact desktop padding and a bounded mobile header', () => {
assert.match(shellStyles, /\.top-header \{[^}]*padding: 0 20px;/)
assert.match(shellStyles, /\.client-main \{[^}]*padding: 88px 20px 40px;/)
assert.match(globalStyles, /@media \(max-width: 720px\) \{[\s\S]*\.top-header \{ padding: 0 14px; \}/)
assert.match(globalStyles, /\.client-brand \{ display: none; \}/)
assert.match(globalStyles, /\.client-header-menu-button \{ display: grid; \}/)
assert.match(globalStyles, /\.client-header-heading p, \.client-user-copy \{ display: none; \}/)
})
test('source contract: closes the mobile menu on route changes and Escape', () => {
assert.match(layoutSource, /watch\(\(\) => route\.fullPath, closeMobileMenu\)/)
assert.match(layoutSource, /event\.key === 'Escape'/)
+17 -25
View File
@@ -2,12 +2,7 @@
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import {
Building2,
LogOut,
Menu,
X,
} from '@lucide/vue'
import { LogOut, Menu, X } from '@lucide/vue'
import baiyueLogoDark from '@/assets/brand/baiyue-logo-dark.png'
import ClientSidebarMenu from '@/components/ClientSidebarMenu.vue'
import { useAuthStore } from '@/stores/auth'
@@ -100,15 +95,25 @@ async function handleLogout() {
<template>
<div class="client-shell" :class="{ 'mobile-menu-open': mobileMenuOpen }">
<header
class="top-header"
:inert="mobileMenuOpen"
:aria-hidden="mobileMenuOpen ? 'true' : undefined"
>
<header class="top-header" :inert="mobileMenuOpen" :aria-hidden="mobileMenuOpen ? 'true' : undefined">
<RouterLink class="client-brand" to="/">
<img class="client-brand-logo" :src="baiyueLogoDark" alt="柏悦物流" />
</RouterLink>
<div class="tenant-name"><Building2 :size="17" /> {{ auth.user?.tenantName }}</div>
<button
ref="menuButtonRef"
type="button"
class="client-header-menu-button"
aria-label="打开导航"
aria-controls="client-sidebar"
:aria-expanded="mobileMenuOpen"
@click="openMobileMenu"
>
<Menu :size="20" />
</button>
<div class="client-header-heading">
<h1>{{ pageTitle }}</h1>
<p v-if="pageDescription">{{ pageDescription }}</p>
</div>
<div class="client-user">
<span class="client-avatar">{{ auth.user?.name.slice(0, 1) }}</span>
<span class="client-user-copy"><strong>{{ auth.user?.name }}</strong><small>{{ auth.user?.roles.join('、') }}</small></span>
@@ -156,19 +161,6 @@ async function handleLogout() {
:inert="mobileMenuOpen"
:aria-hidden="mobileMenuOpen ? 'true' : undefined"
>
<div class="client-page-heading">
<button
ref="menuButtonRef"
type="button"
aria-label="打开导航"
aria-controls="client-sidebar"
:aria-expanded="mobileMenuOpen"
@click="openMobileMenu"
>
<Menu :size="20" />
</button>
<div><h1>{{ pageTitle }}</h1><p>{{ pageDescription }}</p></div>
</div>
<RouterView />
</main>
</div>