优化
This commit is contained in:
@@ -1,11 +1,20 @@
|
||||
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 { ElMessageBox } from 'element-plus'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import AppLayout from './AppLayout.vue'
|
||||
|
||||
const CachedPage = defineComponent({
|
||||
setup() {
|
||||
const count = ref(0)
|
||||
return { count }
|
||||
},
|
||||
template: '<button class="cached-count" @click="count += 1">{{ count }}</button>',
|
||||
})
|
||||
|
||||
vi.mock('element-plus', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('element-plus')>()
|
||||
return { ...actual, ElMessage: { warning: vi.fn(), error: vi.fn() }, ElMessageBox: { confirm: vi.fn() } }
|
||||
@@ -21,15 +30,16 @@ async function mountLayout(path = '/') {
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<p>page</p>' } },
|
||||
{ path: '/customers', component: { template: '<p>customers</p>' }, meta: { title: '客户管理', showPageHeading: false } },
|
||||
{ path: '/', name: 'home', component: { template: '<p>page</p>' }, meta: { requiresAuth: true, workspaceTab: false, keepAlive: false } },
|
||||
{ path: '/customers', name: 'customers', component: CachedPage, meta: { title: '客户管理', showPageHeading: false, requiresAuth: true } },
|
||||
{ path: '/warehouses', name: 'warehouses', component: { template: '<p>warehouses</p>' }, meta: { title: '仓库管理', requiresAuth: true } },
|
||||
{ path: '/login', component: { template: '<p>login</p>' } },
|
||||
],
|
||||
})
|
||||
await router.push(path)
|
||||
await router.isReady()
|
||||
const wrapper = mount(AppLayout, {
|
||||
global: { plugins: [pinia, router], stubs: { AdminSidebarMenu: { template: '<nav />' }, 'el-button': { emits: ['click'], template: '<button @click="$emit(\'click\')"><slot /></button>' } } },
|
||||
global: { plugins: [pinia, router], stubs: { AdminSidebarMenu: { template: '<nav />' }, WorkspaceTabs: { template: '<div class="workspace-tabs-stub" />' }, 'el-button': { emits: ['click'], template: '<button @click="$emit(\'click\')"><slot /></button>' } } },
|
||||
})
|
||||
return { wrapper, router }
|
||||
}
|
||||
@@ -56,4 +66,18 @@ describe('AppLayout', () => {
|
||||
await flushPromises()
|
||||
expect(router.currentRoute.value.path).toBe('/login')
|
||||
})
|
||||
|
||||
it('opens business routes as workspace tabs and keeps route component state while switching', async () => {
|
||||
const { wrapper, router } = await mountLayout('/customers')
|
||||
expect(wrapper.find('.workspace-tabs-stub').exists()).toBe(true)
|
||||
await wrapper.get('.cached-count').trigger('click')
|
||||
expect(wrapper.get('.cached-count').text()).toBe('1')
|
||||
|
||||
await router.push('/warehouses')
|
||||
await flushPromises()
|
||||
await router.push('/customers')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.get('.cached-count').text()).toBe('1')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,15 +7,19 @@ import { getApiErrorDetails } from '@/shared/api/http'
|
||||
import baiyueLogoWhite from '@/assets/brand/baiyue-logo-white.png'
|
||||
import baiyueMarkWhite from '@/assets/brand/baiyue-mark-white.png'
|
||||
import AdminSidebarMenu from '@/app/components/AdminSidebarMenu.vue'
|
||||
import WorkspaceTabs from '@/app/components/WorkspaceTabs.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
|
||||
import { canAccessWorkspaceRoute, workspaceRouteId, workspaceTabCandidate } from '@/core/workspace-tabs/routeWorkspace'
|
||||
import { MAX_CACHED_WORKSPACE_VIEWS, useWorkspaceTabsStore } from '@/core/workspace-tabs/workspaceTabs'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const access = useAccessPreviewStore()
|
||||
const unsaved = useUnsavedWorkStore()
|
||||
const workspace = useWorkspaceTabsStore()
|
||||
const collapsed = ref(false)
|
||||
const mobileMenuOpen = ref(false)
|
||||
const pageTitle = computed(() => route.meta.title || '管理后台')
|
||||
@@ -24,10 +28,38 @@ const showPageHeading = computed(() => route.meta.showPageHeading !== false)
|
||||
const menuCollapsed = computed(() => collapsed.value && !mobileMenuOpen.value)
|
||||
|
||||
access.initializeAfterLogin()
|
||||
workspace.initialize(auth.user?.id || auth.user?.account || '')
|
||||
|
||||
function syncCurrentRoute() {
|
||||
const candidate = workspaceTabCandidate(route)
|
||||
if (candidate) workspace.open(candidate, unsaved.dirtyWorkspaceIds)
|
||||
}
|
||||
|
||||
async function pruneInaccessibleTabs() {
|
||||
const inaccessible = workspace.tabs.filter((tab) => !canAccessWorkspaceRoute(router.resolve(tab.path), access.permissionCodes))
|
||||
const discardedDirtyWork = inaccessible.flatMap((tab) => unsaved.findForWorkspace(tab.id))
|
||||
if (discardedDirtyWork.length) {
|
||||
await ElMessageBox.alert(
|
||||
`账号权限已变化,以下未保存内容无法继续编辑:${[...new Set(discardedDirtyWork.map((entry) => entry.label))].join('、')}。相关页签将关闭。`,
|
||||
'权限已变化',
|
||||
{ confirmButtonText: '知道了', type: 'warning' },
|
||||
).catch(() => undefined)
|
||||
}
|
||||
const previousActiveId = workspace.activeId
|
||||
const result = workspace.prune((tab) => canAccessWorkspaceRoute(router.resolve(tab.path), access.permissionCodes))
|
||||
if (result.removedIds.length === 0) return
|
||||
unsaved.removeWorkspaces(result.removedIds)
|
||||
if (!previousActiveId || !result.removedIds.includes(previousActiveId)) return
|
||||
const destination = result.fallback?.path || access.firstAccessiblePath || '/403'
|
||||
if (destination !== route.fullPath) await router.replace(destination)
|
||||
else syncCurrentRoute()
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定退出管理后台吗?', '退出登录', {
|
||||
const dirtyLabels = unsaved.entries.filter((entry) => entry.dirty).map((entry) => entry.label)
|
||||
const dirtyNotice = dirtyLabels.length ? ` 当前还有未保存内容:${[...new Set(dirtyLabels)].join('、')},退出后将丢失。` : ''
|
||||
await ElMessageBox.confirm(`确定退出管理后台吗?${dirtyNotice}`, '退出登录', {
|
||||
confirmButtonText: '退出',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
@@ -37,6 +69,7 @@ async function handleLogout() {
|
||||
|
||||
access.reset()
|
||||
unsaved.clear()
|
||||
workspace.reset(true)
|
||||
try {
|
||||
await auth.logout()
|
||||
} catch (error) {
|
||||
@@ -59,7 +92,17 @@ function handleMenuToggle() {
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
|
||||
watch(() => route.fullPath, () => { mobileMenuOpen.value = false })
|
||||
watch(() => route.fullPath, () => {
|
||||
mobileMenuOpen.value = false
|
||||
syncCurrentRoute()
|
||||
}, { immediate: true })
|
||||
watch(() => access.permissionSignature, () => { void pruneInaccessibleTabs() }, { immediate: true })
|
||||
watch(() => auth.user?.id || auth.user?.account || '', (userId, previousUserId) => {
|
||||
if (!userId || userId === previousUserId) return
|
||||
workspace.reset(false)
|
||||
workspace.initialize(userId)
|
||||
void pruneInaccessibleTabs().then(syncCurrentRoute)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -91,7 +134,19 @@ watch(() => route.fullPath, () => { mobileMenuOpen.value = false })
|
||||
<el-button text aria-label="退出登录" @click="handleLogout"><LogOut :size="18" /></el-button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="page-container"><RouterView /></main>
|
||||
<WorkspaceTabs v-if="workspace.tabs.length" />
|
||||
<main class="page-container">
|
||||
<RouterView v-slot="{ Component, route: renderedRoute }">
|
||||
<KeepAlive :max="MAX_CACHED_WORKSPACE_VIEWS">
|
||||
<component
|
||||
:is="Component"
|
||||
v-if="renderedRoute.meta.keepAlive !== false"
|
||||
:key="workspace.cacheKey(workspaceRouteId(renderedRoute))"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component :is="Component" v-if="renderedRoute.meta.keepAlive === false" />
|
||||
</RouterView>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user