177 lines
5.1 KiB
Vue
177 lines
5.1 KiB
Vue
<script setup lang="ts">
|
||||
|
|
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 { PRODUCT_NAME } from '@chongchong/shared'
|
|||
|
|
import ClientSidebarMenu from '@/components/ClientSidebarMenu.vue'
|
|||
|
|
import { useAuthStore } from '@/stores/auth'
|
|||
|
|
|
|||
|
|
const route = useRoute()
|
|||
|
|
const router = useRouter()
|
|||
|
|
const auth = useAuthStore()
|
|||
|
|
const pageTitle = computed(() => route.meta.title || '业务总览')
|
|||
|
|
const pageDescription = computed(() => route.meta.description || '')
|
|||
|
|
const mobileMenuOpen = ref(false)
|
|||
|
|
const desktopViewport = ref(true)
|
|||
|
|
const menuButtonRef = ref<HTMLButtonElement>()
|
|||
|
|
const sidebarRef = ref<HTMLElement>()
|
|||
|
|
const sidebarCloseButtonRef = ref<HTMLButtonElement>()
|
|||
|
|
let previousBodyOverflow: string | undefined
|
|||
|
|
let desktopMediaQuery: MediaQueryList | undefined
|
|||
|
|
|
|||
|
|
function openMobileMenu() {
|
|||
|
|
mobileMenuOpen.value = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeMobileMenu() {
|
|||
|
|
mobileMenuOpen.value = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function restoreBodyOverflow() {
|
|||
|
|
if (previousBodyOverflow === undefined) return
|
|||
|
|
|
|||
|
|
document.body.style.overflow = previousBodyOverflow
|
|||
|
|
previousBodyOverflow = undefined
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleKeydown(event: KeyboardEvent) {
|
|||
|
|
if (event.key === 'Escape') closeMobileMenu()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleDesktopBreakpoint(event: MediaQueryList | MediaQueryListEvent) {
|
|||
|
|
desktopViewport.value = event.matches
|
|||
|
|
if (event.matches) closeMobileMenu()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
watch(() => route.fullPath, closeMobileMenu)
|
|||
|
|
|
|||
|
|
watch(mobileMenuOpen, async (open, wasOpen) => {
|
|||
|
|
if (open) {
|
|||
|
|
previousBodyOverflow = document.body.style.overflow
|
|||
|
|
document.body.style.overflow = 'hidden'
|
|||
|
|
|
|||
|
|
await nextTick()
|
|||
|
|
if (mobileMenuOpen.value) sidebarCloseButtonRef.value?.focus()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
restoreBodyOverflow()
|
|||
|
|
|
|||
|
|
if (wasOpen) {
|
|||
|
|
await nextTick()
|
|||
|
|
if (!mobileMenuOpen.value && !desktopViewport.value) menuButtonRef.value?.focus()
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
document.addEventListener('keydown', handleKeydown)
|
|||
|
|
desktopMediaQuery = window.matchMedia('(min-width: 721px)')
|
|||
|
|
desktopMediaQuery.addEventListener('change', handleDesktopBreakpoint)
|
|||
|
|
handleDesktopBreakpoint(desktopMediaQuery)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onBeforeUnmount(() => {
|
|||
|
|
document.removeEventListener('keydown', handleKeydown)
|
|||
|
|
desktopMediaQuery?.removeEventListener('change', handleDesktopBreakpoint)
|
|||
|
|
restoreBodyOverflow()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
async function handleLogout() {
|
|||
|
|
try {
|
|||
|
|
await ElMessageBox.confirm('确定退出客户工作台吗?', '退出登录', {
|
|||
|
|
confirmButtonText: '退出',
|
|||
|
|
cancelButtonText: '取消',
|
|||
|
|
})
|
|||
|
|
} catch (error) {
|
|||
|
|
if (error === 'cancel' || error === 'close') return
|
|||
|
|
throw error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await auth.logout()
|
|||
|
|
await router.replace('/login')
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div class="client-shell" :class="{ 'mobile-menu-open': mobileMenuOpen }">
|
|||
|
|
<header
|
|||
|
|
class="top-header"
|
|||
|
|
:inert="mobileMenuOpen"
|
|||
|
|
:aria-hidden="mobileMenuOpen ? 'true' : undefined"
|
|||
|
|
>
|
|||
|
|
<RouterLink class="client-brand" to="/">
|
|||
|
|
<span class="client-brand-mark">CC</span>
|
|||
|
|
<span><strong>{{ PRODUCT_NAME }}</strong><small>客户工作台</small></span>
|
|||
|
|
</RouterLink>
|
|||
|
|
<div class="tenant-name"><Building2 :size="17" /> {{ auth.user?.tenantName }}</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>
|
|||
|
|
<el-button text aria-label="退出登录" @click="handleLogout"><LogOut :size="18" /></el-button>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
v-if="mobileMenuOpen"
|
|||
|
|
type="button"
|
|||
|
|
class="client-sidebar-scrim"
|
|||
|
|
aria-label="关闭导航"
|
|||
|
|
@click="closeMobileMenu"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<aside
|
|||
|
|
ref="sidebarRef"
|
|||
|
|
id="client-sidebar"
|
|||
|
|
class="client-sidebar"
|
|||
|
|
:inert="!desktopViewport && !mobileMenuOpen"
|
|||
|
|
:aria-hidden="!desktopViewport && !mobileMenuOpen ? 'true' : undefined"
|
|||
|
|
>
|
|||
|
|
<div class="client-sidebar-header">
|
|||
|
|
<strong>功能导航</strong>
|
|||
|
|
<button
|
|||
|
|
ref="sidebarCloseButtonRef"
|
|||
|
|
type="button"
|
|||
|
|
aria-label="关闭导航"
|
|||
|
|
@click="closeMobileMenu"
|
|||
|
|
>
|
|||
|
|
<X :size="20" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<nav aria-label="客户工作台导航">
|
|||
|
|
<ClientSidebarMenu
|
|||
|
|
:active-path="route.path"
|
|||
|
|
@navigate="closeMobileMenu"
|
|||
|
|
/>
|
|||
|
|
</nav>
|
|||
|
|
<div class="sidebar-help"><strong>需要帮助?</strong><span>联系您的专属客户经理</span></div>
|
|||
|
|
</aside>
|
|||
|
|
|
|||
|
|
<main
|
|||
|
|
class="client-main"
|
|||
|
|
: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>
|
|||
|
|
</template>
|