163 lines
3.9 KiB
Vue
163 lines
3.9 KiB
Vue
<template>
|
||
<section class="layout">
|
||
<!-- 顶部导航栏:固定在顶部 -->
|
||
<Header :isSmallScreen="screenWidth < COLLAPSE_BREAKPOINT" @toggle-sidebar="handleToggleSidebar" />
|
||
|
||
<!-- 主体内容区域 -->
|
||
<section class="main-container" :class="{ 'is-collapsed': sidebarCollapsed }">
|
||
<!-- 左侧侧边菜单:固定在左侧 -->
|
||
<Sidebar :collapsed="sidebarCollapsed" />
|
||
|
||
<!-- 右侧内容区:页签 + 主体内容 -->
|
||
<section class="content-container">
|
||
<!-- 页签标签栏 -->
|
||
<Tabs />
|
||
|
||
<!-- 主体内容区 -->
|
||
<main class="main-content">
|
||
<transition
|
||
name="fade-transform"
|
||
mode="out-in"
|
||
style="height: 100%; display: flex; flex-direction: column"
|
||
>
|
||
<router-view v-slot="{ Component }">
|
||
<!-- 使用 :key 确保每次路由变化都会创建新的 ErrorBoundary 实例 -->
|
||
<ErrorBoundary :key="route.path + '-' + refreshKey">
|
||
<KeepAlive v-if="route.meta.keepAlive">
|
||
<component :is="Component" :key="refreshKey" />
|
||
</KeepAlive>
|
||
<component v-else :is="Component" :key="refreshKey" />
|
||
</ErrorBoundary>
|
||
</router-view>
|
||
</transition>
|
||
</main>
|
||
</section>
|
||
</section>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
defineOptions({
|
||
name: 'Layout',
|
||
})
|
||
import Header from './Header.vue'
|
||
import Sidebar from './Sidebar.vue'
|
||
import Tabs from './Tabs.vue'
|
||
import ErrorBoundary from '@/components/common/ErrorBoundary.vue'
|
||
import { useTabsStore } from '@/stores/modules/tabs'
|
||
|
||
const route = useRoute()
|
||
const tabsStore = useTabsStore()
|
||
|
||
// 用于组件重新渲染的 key
|
||
const refreshKey = computed(() => {
|
||
// 当 refreshKey 包含当前路由时,重新渲染组件
|
||
if (tabsStore.refreshKey.startsWith(route.path)) {
|
||
return tabsStore.refreshKey
|
||
}
|
||
return route.path
|
||
})
|
||
|
||
// 侧边栏收缩状态
|
||
const sidebarCollapsed = ref(false)
|
||
|
||
// 屏幕宽度监听
|
||
const screenWidth = ref(window.innerWidth)
|
||
|
||
// 响应式收起侧边栏的阈值(单位:px)
|
||
const COLLAPSE_BREAKPOINT = 992
|
||
|
||
// 监听屏幕宽度变化
|
||
const handleResize = () => {
|
||
screenWidth.value = window.innerWidth
|
||
// 当屏幕宽度小于阈值时自动收起侧边栏
|
||
if (screenWidth.value < COLLAPSE_BREAKPOINT) {
|
||
sidebarCollapsed.value = true
|
||
}else {
|
||
// sidebarCollapsed.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
// 初始化时判断屏幕宽度
|
||
handleResize()
|
||
window.addEventListener('resize', handleResize)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('resize', handleResize)
|
||
})
|
||
|
||
// 切换侧边栏
|
||
const handleToggleSidebar = (collapsed: boolean) => {
|
||
sidebarCollapsed.value = collapsed
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.layout {
|
||
width: 100%;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
background: var(--el-bg-color-page);
|
||
|
||
.main-container {
|
||
display: flex;
|
||
height: calc(100vh - 50px);
|
||
margin-top: 50px;
|
||
transition: margin-left 0.3s ease;
|
||
|
||
&.is-collapsed {
|
||
.content-container {
|
||
margin-left: 64px;
|
||
}
|
||
}
|
||
|
||
.content-container {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
margin-left: 200px;
|
||
transition: margin-left 0.3s ease;
|
||
|
||
.main-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
// background: white;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 响应式侧边栏收起
|
||
@media screen and (max-width: 992px) {
|
||
.layout {
|
||
.main-container {
|
||
&.is-collapsed {
|
||
.content-container {
|
||
margin-left: 64px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 页面切换动画
|
||
.fade-transform-enter-active,
|
||
.fade-transform-leave-active {
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.fade-transform-enter-from {
|
||
opacity: 0;
|
||
transform: translateX(30px);
|
||
}
|
||
|
||
.fade-transform-leave-to {
|
||
opacity: 0;
|
||
transform: translateX(-30px);
|
||
}
|
||
</style>
|