2026-07-30 17:59:43 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-07-31 15:34:35 +08:00
|
|
|
|
import { computed, ref, watch } from 'vue'
|
2026-07-30 17:59:43 +08:00
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
2026-07-31 15:34:35 +08:00
|
|
|
|
import { ChevronLeft, ChevronRight, LogOut, Menu } from '@lucide/vue'
|
2026-07-31 13:43:16 +08:00
|
|
|
|
import { getApiErrorDetails } from '@/api/http'
|
2026-07-31 15:34:35 +08:00
|
|
|
|
import baiyueLogoWhite from '@/assets/brand/baiyue-logo-white.png'
|
|
|
|
|
|
import baiyueMarkWhite from '@/assets/brand/baiyue-mark-white.png'
|
2026-07-30 17:59:43 +08:00
|
|
|
|
import AdminSidebarMenu from '@/components/AdminSidebarMenu.vue'
|
|
|
|
|
|
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
|
|
|
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
|
|
|
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
|
|
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const auth = useAuthStore()
|
|
|
|
|
|
const access = useAccessPreviewStore()
|
|
|
|
|
|
const unsaved = useUnsavedWorkStore()
|
|
|
|
|
|
const collapsed = ref(false)
|
|
|
|
|
|
const mobileMenuOpen = ref(false)
|
|
|
|
|
|
const pageTitle = computed(() => route.meta.title || '管理后台')
|
|
|
|
|
|
const pageDescription = computed(() => route.meta.description || '')
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const showPageHeading = computed(() => route.meta.showPageHeading !== false)
|
2026-07-31 15:34:35 +08:00
|
|
|
|
const menuCollapsed = computed(() => collapsed.value && !mobileMenuOpen.value)
|
2026-07-30 17:59:43 +08:00
|
|
|
|
|
2026-07-31 15:34:35 +08:00
|
|
|
|
access.initializeAfterLogin()
|
2026-07-30 17:59:43 +08:00
|
|
|
|
|
|
|
|
|
|
async function handleLogout() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await ElMessageBox.confirm('确定退出管理后台吗?', '退出登录', {
|
|
|
|
|
|
confirmButtonText: '退出',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
access.reset()
|
|
|
|
|
|
unsaved.clear()
|
2026-07-31 13:43:16 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await auth.logout()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
const details = getApiErrorDetails(error, '服务器会话撤销失败')
|
|
|
|
|
|
const trackingId = details.requestId || details.traceId
|
|
|
|
|
|
ElMessage.warning(`本地已退出,但服务器会话撤销失败${trackingId ? `(追踪标识:${trackingId})` : ''}`)
|
|
|
|
|
|
}
|
2026-07-30 17:59:43 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await router.replace('/login')
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
ElMessage.error('已退出登录,但登录页跳转失败,请刷新页面')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(() => route.fullPath, () => { mobileMenuOpen.value = false })
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="app-shell" :class="{ 'is-collapsed': collapsed, 'is-mobile-open': mobileMenuOpen }">
|
|
|
|
|
|
<button v-if="mobileMenuOpen" class="sidebar-scrim" type="button" aria-label="关闭菜单" @click="mobileMenuOpen = false" />
|
|
|
|
|
|
<aside class="app-sidebar">
|
|
|
|
|
|
<div class="brand-block">
|
2026-07-31 15:34:35 +08:00
|
|
|
|
<img class="brand-logo brand-logo-full" :src="baiyueLogoWhite" alt="柏悦物流" />
|
|
|
|
|
|
<img class="brand-logo brand-logo-mark" :src="baiyueMarkWhite" alt="柏悦物流" />
|
2026-07-30 17:59:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<nav class="side-nav" aria-label="主导航">
|
|
|
|
|
|
<AdminSidebarMenu :items="access.filteredNavigation" :active-path="route.path" :collapsed="menuCollapsed" />
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
<button class="sidebar-toggle" type="button" @click="collapsed = !collapsed">
|
|
|
|
|
|
<ChevronRight v-if="collapsed" :size="18" />
|
|
|
|
|
|
<ChevronLeft v-else :size="18" />
|
|
|
|
|
|
<span v-if="!collapsed">收起导航</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-main">
|
2026-08-01 12:17:33 +08:00
|
|
|
|
<header class="app-header" :class="{ 'app-header--without-page-heading': !showPageHeading }">
|
2026-07-31 15:34:35 +08:00
|
|
|
|
<button class="mobile-menu-button" type="button" aria-label="打开菜单" :aria-expanded="mobileMenuOpen" @click="mobileMenuOpen = true"><Menu :size="20" /></button>
|
2026-08-01 12:17:33 +08:00
|
|
|
|
<div v-if="showPageHeading" class="page-heading"><h1>{{ pageTitle }}</h1><p v-if="pageDescription">{{ pageDescription }}</p></div>
|
2026-07-30 17:59:43 +08:00
|
|
|
|
<div class="header-user">
|
|
|
|
|
|
<div class="user-avatar">{{ auth.user?.name.slice(0, 1) }}</div>
|
2026-07-31 15:34:35 +08:00
|
|
|
|
<div class="user-copy" :aria-label="`登录身份:${auth.user?.name ?? '-'}(${auth.user?.account ?? '-'})`"><strong>登录身份:{{ auth.user?.name }}({{ auth.user?.account }})</strong></div>
|
|
|
|
|
|
<el-button text aria-label="退出登录" @click="handleLogout"><LogOut :size="18" /></el-button>
|
2026-07-30 17:59:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
2026-07-31 15:34:35 +08:00
|
|
|
|
<main class="page-container"><RouterView /></main>
|
2026-07-30 17:59:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-08-01 12:17:33 +08:00
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.app-header--without-page-heading .header-user { margin-left: auto; }
|
|
|
|
|
|
</style>
|