优化
This commit is contained in:
@@ -60,14 +60,14 @@ test('source contract: aligns the light top bar beside the prototype-inspired da
|
||||
assert.match(shellStyles, /\.top-header \{[^}]*background: rgba\(255, 255, 255, \.96\)/)
|
||||
assert.match(shellStyles, /\.client-sidebar \{[^}]*inset: 0 auto 0 0;[^}]*width: 250px;[^}]*background: linear-gradient\(180deg, #0b1f3a 0%, #0c2a52 100%\)/)
|
||||
assert.match(shellStyles, /\.client-sidebar-brand \{[^}]*border-bottom: 1px solid rgba\(255, 255, 255, \.09\)/)
|
||||
assert.match(shellStyles, /\.client-main \{[^}]*padding: 88px 20px 40px;/)
|
||||
assert.match(shellStyles, /\.client-main \{[^}]*padding: 134px 20px 40px;/)
|
||||
assert.match(globalStyles, /@media \(max-width: 720px\) \{[\s\S]*\.top-header \{ inset: 0;[^}]*padding: 0 14px; \}/)
|
||||
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, /watch\(\(\) => route\.fullPath, \(\) => \{\s*closeMobileMenu\(\)\s*registerCurrentRoute\(\)\s*\}\)/)
|
||||
assert.match(layoutSource, /event\.key === 'Escape'/)
|
||||
assert.match(layoutSource, /onMounted\(\(\) => \{[\s\S]*document\.addEventListener\('keydown', handleKeydown\)[\s\S]*\}\)/)
|
||||
assert.match(layoutSource, /document\.removeEventListener\('keydown', handleKeydown\)/)
|
||||
|
||||
+108
-3
@@ -5,11 +5,22 @@ import { ElMessageBox } from 'element-plus'
|
||||
import { LogOut, Menu, X } from '@lucide/vue'
|
||||
import baiyueLogoDark from '@/assets/brand/baiyue-logo-dark.png'
|
||||
import ClientSidebarMenu from '@/components/ClientSidebarMenu.vue'
|
||||
import WorkspaceTabs from '@/components/WorkspaceTabs.vue'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { getFirstAccessiblePath } from '@/navigation/clientNavigation'
|
||||
import { useUnsavedWorkStore } from '@/navigation/workspace-tabs/unsavedWork'
|
||||
import {
|
||||
MAX_CACHED_WORKSPACE_VIEWS,
|
||||
isWorkspaceRoute,
|
||||
useWorkspaceTabsStore,
|
||||
type WorkspaceTab,
|
||||
} from '@/navigation/workspace-tabs/workspaceTabs'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const workspace = useWorkspaceTabsStore()
|
||||
const unsaved = useUnsavedWorkStore()
|
||||
const pageTitle = computed(() => route.meta.title || '业务总览')
|
||||
const pageDescription = computed(() => route.meta.description || '')
|
||||
const mobileMenuOpen = ref(false)
|
||||
@@ -19,6 +30,25 @@ const sidebarRef = ref<HTMLElement>()
|
||||
const sidebarCloseButtonRef = ref<HTMLButtonElement>()
|
||||
let previousBodyOverflow: string | undefined
|
||||
let desktopMediaQuery: MediaQueryList | undefined
|
||||
const scrollPositions = new Map<string, number>()
|
||||
|
||||
function isAllowedTab(tab: WorkspaceTab) {
|
||||
const resolved = router.resolve(tab.fullPath)
|
||||
if (String(resolved.name ?? '') !== tab.routeName || !isWorkspaceRoute(resolved)) return false
|
||||
const permission = typeof resolved.meta.permission === 'string' ? resolved.meta.permission : undefined
|
||||
return auth.hasPermission(permission)
|
||||
}
|
||||
|
||||
function registerCurrentRoute() {
|
||||
workspace.openRoute(route, unsaved.dirtyRouteKeys)
|
||||
}
|
||||
|
||||
const restoredActivePath = workspace.initialize(auth.user?.id ?? '', isAllowedTab)
|
||||
if (route.fullPath === '/' && restoredActivePath && restoredActivePath !== '/') {
|
||||
void router.replace(restoredActivePath)
|
||||
} else {
|
||||
registerCurrentRoute()
|
||||
}
|
||||
|
||||
function openMobileMenu() {
|
||||
mobileMenuOpen.value = true
|
||||
@@ -44,7 +74,51 @@ function handleDesktopBreakpoint(event: MediaQueryList | MediaQueryListEvent) {
|
||||
if (event.matches) closeMobileMenu()
|
||||
}
|
||||
|
||||
watch(() => route.fullPath, closeMobileMenu)
|
||||
watch(() => route.fullPath, () => {
|
||||
closeMobileMenu()
|
||||
registerCurrentRoute()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => String(route.name ?? ''),
|
||||
async (routeName, previousRouteName) => {
|
||||
if (previousRouteName && workspace.tabs.some((tab) => tab.key === previousRouteName)) {
|
||||
scrollPositions.set(previousRouteName, window.scrollY)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
window.requestAnimationFrame(() => {
|
||||
window.scrollTo(0, scrollPositions.get(routeName) ?? 0)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => workspace.tabs.map((tab) => tab.key),
|
||||
(tabKeys) => {
|
||||
const retainedKeys = new Set(tabKeys)
|
||||
for (const routeKey of scrollPositions.keys()) {
|
||||
if (!retainedKeys.has(routeKey)) scrollPositions.delete(routeKey)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => auth.permissionCodes.join('|'),
|
||||
() => {
|
||||
const result = workspace.reconcile(isAllowedTab)
|
||||
if (result.closedActive) void router.replace(result.fallbackPath ?? getFirstAccessiblePath(auth.permissionCodes))
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => auth.isAuthenticated,
|
||||
(authenticated) => {
|
||||
if (authenticated) return
|
||||
workspace.clear()
|
||||
unsaved.clear()
|
||||
},
|
||||
)
|
||||
|
||||
watch(mobileMenuOpen, async (open, wasOpen) => {
|
||||
if (open) {
|
||||
@@ -66,6 +140,7 @@ watch(mobileMenuOpen, async (open, wasOpen) => {
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
desktopMediaQuery = window.matchMedia('(min-width: 721px)')
|
||||
desktopMediaQuery.addEventListener('change', handleDesktopBreakpoint)
|
||||
handleDesktopBreakpoint(desktopMediaQuery)
|
||||
@@ -73,21 +148,33 @@ onMounted(() => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
desktopMediaQuery?.removeEventListener('change', handleDesktopBreakpoint)
|
||||
restoreBodyOverflow()
|
||||
})
|
||||
|
||||
function handleBeforeUnload(event: BeforeUnloadEvent) {
|
||||
if (!unsaved.hasDirty) return
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定退出客户工作台吗?', '退出登录', {
|
||||
await ElMessageBox.confirm(unsaved.hasDirty
|
||||
? '当前还有未保存的修改,退出后这些内容将丢失。确定继续吗?'
|
||||
: '确定退出客户工作台吗?', '退出登录', {
|
||||
confirmButtonText: '退出',
|
||||
cancelButtonText: '取消',
|
||||
type: unsaved.hasDirty ? 'warning' : undefined,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') return
|
||||
throw error
|
||||
}
|
||||
|
||||
workspace.clear()
|
||||
unsaved.clear()
|
||||
await auth.logout()
|
||||
await router.replace('/login')
|
||||
}
|
||||
@@ -158,12 +245,30 @@ async function handleLogout() {
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<WorkspaceTabs
|
||||
:inert="mobileMenuOpen"
|
||||
:aria-hidden="mobileMenuOpen ? 'true' : undefined"
|
||||
/>
|
||||
|
||||
<main
|
||||
class="client-main"
|
||||
:inert="mobileMenuOpen"
|
||||
:aria-hidden="mobileMenuOpen ? 'true' : undefined"
|
||||
>
|
||||
<RouterView />
|
||||
<RouterView v-slot="{ Component, route: viewRoute }">
|
||||
<KeepAlive :max="MAX_CACHED_WORKSPACE_VIEWS">
|
||||
<component
|
||||
:is="Component"
|
||||
v-if="viewRoute.meta.keepAlive !== false"
|
||||
:key="workspace.cacheKey(viewRoute.name)"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component
|
||||
:is="Component"
|
||||
v-if="viewRoute.meta.keepAlive === false"
|
||||
:key="String(viewRoute.name ?? viewRoute.fullPath)"
|
||||
/>
|
||||
</RouterView>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user