优化
This commit is contained in:
@@ -2,7 +2,7 @@ import { createPinia, setActivePinia } from 'pinia'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import AppLayout from './AppLayout.vue'
|
||||
@@ -44,8 +44,53 @@ async function mountLayout(path = '/') {
|
||||
return { wrapper, router }
|
||||
}
|
||||
|
||||
function mockViewport(isMobile: boolean) {
|
||||
const mediaQuery = {
|
||||
matches: isMobile,
|
||||
media: '(max-width: 720px)',
|
||||
onchange: null,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
} as unknown as MediaQueryList
|
||||
vi.stubGlobal('matchMedia', vi.fn(() => mediaQuery))
|
||||
}
|
||||
|
||||
describe('AppLayout', () => {
|
||||
beforeEach(() => { vi.mocked(ElMessageBox.confirm).mockReset() })
|
||||
beforeEach(() => {
|
||||
vi.mocked(ElMessageBox.confirm).mockReset()
|
||||
mockViewport(false)
|
||||
})
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
|
||||
it('reports the desktop navigation expanded and collapsed states', async () => {
|
||||
const { wrapper } = await mountLayout()
|
||||
const menuButton = wrapper.get('.shell-menu-button')
|
||||
|
||||
expect(menuButton.attributes('aria-label')).toBe('收起导航')
|
||||
expect(menuButton.attributes('aria-expanded')).toBe('true')
|
||||
|
||||
await menuButton.trigger('click')
|
||||
|
||||
expect(menuButton.attributes('aria-label')).toBe('展开导航')
|
||||
expect(menuButton.attributes('aria-expanded')).toBe('false')
|
||||
})
|
||||
|
||||
it('reports the mobile menu closed and open states', async () => {
|
||||
mockViewport(true)
|
||||
const { wrapper } = await mountLayout()
|
||||
const menuButton = wrapper.get('.shell-menu-button')
|
||||
|
||||
expect(menuButton.attributes('aria-label')).toBe('打开菜单')
|
||||
expect(menuButton.attributes('aria-expanded')).toBe('false')
|
||||
|
||||
await menuButton.trigger('click')
|
||||
|
||||
expect(menuButton.attributes('aria-label')).toBe('关闭菜单')
|
||||
expect(menuButton.attributes('aria-expanded')).toBe('true')
|
||||
})
|
||||
|
||||
it('shows the authenticated identity without a role preview switcher', async () => {
|
||||
const { wrapper } = await mountLayout()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Bell, FileText, LogOut, Mail, Menu } from '@lucide/vue'
|
||||
@@ -20,12 +20,22 @@ const auth = useAuthStore()
|
||||
const access = useAccessPreviewStore()
|
||||
const unsaved = useUnsavedWorkStore()
|
||||
const workspace = useWorkspaceTabsStore()
|
||||
const mobileViewportQuery = '(max-width: 720px)'
|
||||
const collapsed = ref(false)
|
||||
const mobileMenuOpen = ref(false)
|
||||
const isMobileViewport = ref(false)
|
||||
let mobileViewportMedia: MediaQueryList | undefined
|
||||
const pageTitle = computed(() => route.meta.title || '管理后台')
|
||||
const pageDescription = computed(() => route.meta.description || '')
|
||||
const showPageHeading = computed(() => route.meta.showPageHeading !== false)
|
||||
const menuCollapsed = computed(() => collapsed.value && !mobileMenuOpen.value)
|
||||
const menuButtonLabel = computed(() => {
|
||||
if (isMobileViewport.value) return mobileMenuOpen.value ? '关闭菜单' : '打开菜单'
|
||||
return collapsed.value ? '展开导航' : '收起导航'
|
||||
})
|
||||
const menuExpanded = computed(() => (
|
||||
isMobileViewport.value ? mobileMenuOpen.value : !collapsed.value
|
||||
))
|
||||
const canOpenNotificationManagement = computed(() => (
|
||||
access.can('platform:notification:task:list')
|
||||
|| access.can('platform:notification:template:list')
|
||||
@@ -89,15 +99,27 @@ async function handleLogout() {
|
||||
}
|
||||
|
||||
function handleMenuToggle() {
|
||||
if (window.matchMedia('(max-width: 720px)').matches) {
|
||||
if (isMobileViewport.value) {
|
||||
mobileMenuOpen.value = !mobileMenuOpen.value
|
||||
return
|
||||
}
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
|
||||
function syncMobileViewport(event: MediaQueryList | MediaQueryListEvent) {
|
||||
isMobileViewport.value = event.matches
|
||||
if (!event.matches) mobileMenuOpen.value = false
|
||||
}
|
||||
|
||||
function openNotificationPage(path: string) { void router.push(path) }
|
||||
|
||||
onMounted(() => {
|
||||
mobileViewportMedia = window.matchMedia(mobileViewportQuery)
|
||||
syncMobileViewport(mobileViewportMedia)
|
||||
mobileViewportMedia.addEventListener('change', syncMobileViewport)
|
||||
})
|
||||
onBeforeUnmount(() => mobileViewportMedia?.removeEventListener('change', syncMobileViewport))
|
||||
|
||||
watch(() => route.fullPath, () => {
|
||||
mobileMenuOpen.value = false
|
||||
syncCurrentRoute()
|
||||
@@ -129,8 +151,8 @@ watch(() => auth.user?.id || auth.user?.account || '', (userId, previousUserId)
|
||||
<button
|
||||
class="shell-menu-button"
|
||||
type="button"
|
||||
:aria-label="mobileMenuOpen ? '关闭菜单' : collapsed ? '展开导航' : '收起导航'"
|
||||
:aria-expanded="mobileMenuOpen || !collapsed"
|
||||
:aria-label="menuButtonLabel"
|
||||
:aria-expanded="menuExpanded"
|
||||
@click="handleMenuToggle"
|
||||
><Menu :size="22" /></button>
|
||||
<div v-if="showPageHeading" class="page-heading"><h1>{{ pageTitle }}</h1><p v-if="pageDescription">{{ pageDescription }}</p></div>
|
||||
|
||||
Reference in New Issue
Block a user