323 lines
6.9 KiB
Vue
323 lines
6.9 KiB
Vue
<template>
|
||
<div class="main-layout" :class="layoutClasses">
|
||
|
||
<!-- 第一行:头部导航栏 -->
|
||
<header class="header-container">
|
||
<AppHeader
|
||
:sidebar-visible="sidebarVisible"
|
||
@toggle-sidebar="toggleSidebar"
|
||
/>
|
||
</header>
|
||
|
||
<!-- 第二行:侧边栏和主内容区域 -->
|
||
<div class="content-row">
|
||
<!-- 侧边栏容器 -->
|
||
<aside
|
||
class="sidebar-container"
|
||
>
|
||
<AppSidebar
|
||
:collapsed="!sidebarVisible"
|
||
@navigate="handleNavigate"
|
||
/>
|
||
</aside>
|
||
<!-- 主内容区域 -->
|
||
<div class="main-content" :class="{ 'sidebar-collapsed': !sidebarVisible && !isMobile }">
|
||
<div class="sidebar-overlay" :class="{ 'sidebar-overlay-active': loading }"></div>
|
||
<!-- 面包屑导航 -->
|
||
<!-- <BreadcrumbNavigation class="breadcrumb-container" /> -->
|
||
|
||
<!-- 页面内容 -->
|
||
<main class="page-content">
|
||
|
||
<router-view v-slot="{ Component, route }">
|
||
<keep-alive v-if="route.meta.keepAlive">
|
||
<component :is="Component" :key="route.name" />
|
||
</keep-alive>
|
||
<component v-else :is="Component" :key="route.name" />
|
||
</router-view>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
||
import AppHeader from './AppHeader.vue'
|
||
import AppSidebar from './AppSidebar.vue'
|
||
import BreadcrumbNavigation from './BreadcrumbNavigation.vue'
|
||
|
||
// 注册组件
|
||
defineOptions({
|
||
name: 'MainLayout'
|
||
})
|
||
const props = defineProps({
|
||
loading: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
})
|
||
// 响应式状态管理
|
||
const state = reactive({
|
||
screenWidth: window.innerWidth,
|
||
sidebarVisible: true,
|
||
isMobile: false,
|
||
isTablet: false,
|
||
isDesktop: false
|
||
})
|
||
|
||
// 计算属性:断点判断
|
||
const breakpoints = {
|
||
mobile: 768,
|
||
tablet: 1024
|
||
}
|
||
|
||
const layoutClasses = computed(() => ({
|
||
'mobile-layout': state.isMobile,
|
||
'tablet-layout': state.isTablet,
|
||
'desktop-layout': state.isDesktop
|
||
}))
|
||
|
||
// 导出需要在模板中使用的响应式数据
|
||
const sidebarVisible = computed(() => state.sidebarVisible)
|
||
const isMobile = computed(() => state.isMobile)
|
||
const isTablet = computed(() => state.isTablet)
|
||
const isDesktop = computed(() => state.isDesktop)
|
||
|
||
// 响应式断点更新
|
||
const updateBreakpoints = () => {
|
||
state.screenWidth = window.innerWidth
|
||
state.isMobile = state.screenWidth < breakpoints.mobile
|
||
state.isTablet = state.screenWidth >= breakpoints.mobile && state.screenWidth < breakpoints.tablet
|
||
state.isDesktop = state.screenWidth >= breakpoints.tablet
|
||
|
||
// 移动端默认隐藏侧边栏
|
||
if (state.isMobile) {
|
||
state.sidebarVisible = false
|
||
} else if (state.isDesktop) {
|
||
state.sidebarVisible = true
|
||
}
|
||
}
|
||
|
||
// 切换侧边栏显示
|
||
const toggleSidebar = () => {
|
||
if (state.isMobile) {
|
||
state.sidebarVisible = !state.sidebarVisible
|
||
} else {
|
||
state.sidebarVisible = !state.sidebarVisible
|
||
}
|
||
}
|
||
|
||
// 处理导航
|
||
const handleNavigate = (route) => {
|
||
// 移动端点击导航后隐藏侧边栏
|
||
if (state.isMobile) {
|
||
state.sidebarVisible = false
|
||
}
|
||
}
|
||
|
||
// 窗口大小变化监听
|
||
let resizeTimer = null
|
||
const handleResize = () => {
|
||
clearTimeout(resizeTimer)
|
||
resizeTimer = setTimeout(() => {
|
||
updateBreakpoints()
|
||
}, 150)
|
||
}
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
updateBreakpoints()
|
||
window.addEventListener('resize', handleResize)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('resize', handleResize)
|
||
if (resizeTimer) {
|
||
clearTimeout(resizeTimer)
|
||
}
|
||
})
|
||
|
||
// 监听路由变化,在移动端自动隐藏侧边栏
|
||
watch(() => window.location.pathname, () => {
|
||
if (state.isMobile) {
|
||
state.sidebarVisible = false
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 主布局容器 - 垂直布局:第一行头部,第二行内容 */
|
||
.main-layout {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 98vh;
|
||
/* 移除 overflow: hidden 以允许页面滚动 */
|
||
position: relative;
|
||
}
|
||
|
||
/* 第一行:头部容器样式 */
|
||
.header-container {
|
||
flex-shrink: 0;
|
||
height: 64px;
|
||
background: var(--header-bg, #ffffff);
|
||
border-bottom: 1px solid var(--border-color, #e5e7eb);
|
||
z-index: 100;
|
||
width: 100%;
|
||
}
|
||
|
||
/* 第二行:内容区域容器(侧边栏 + 主内容) */
|
||
.content-row {
|
||
display: flex;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 侧边栏容器 */
|
||
.sidebar-container {
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
width: 120px;
|
||
/* height: 100%; */
|
||
background: var(--sidebar-bg, #ffffff);
|
||
border-right: 1px solid var(--border-color, #e5e7eb);
|
||
z-index: 1000;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/* 主内容区域 */
|
||
.main-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
/* 移除 overflow: hidden 以允许页面滚动 */
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
position: relative;
|
||
}
|
||
|
||
.breadcrumb-container {
|
||
padding: 16px 24px 8px;
|
||
background-color: var(--breadcrumb-bg, #ffffff);
|
||
border-bottom: 1px solid var(--border-color, #e5e7eb);
|
||
}
|
||
|
||
|
||
|
||
/* 头部容器 */
|
||
.header-container {
|
||
height: 64px;
|
||
background: var(--header-bg, #ffffff);
|
||
border-bottom: 1px solid var(--border-color, #e5e7eb);
|
||
z-index: 999;
|
||
position: sticky;
|
||
top: 0;
|
||
}
|
||
|
||
/* 页面内容 */
|
||
.page-content {
|
||
/* overflow-y: auto; */
|
||
background: var(--content-bg, #f8fafc);
|
||
width: 100%;
|
||
height:90vh;
|
||
}
|
||
|
||
/* 移动端样式 */
|
||
@media (max-width: 767px) {
|
||
.content-row {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sidebar-container {
|
||
position: fixed;
|
||
top: 0;
|
||
left: -120px;
|
||
transform: translateX(0);
|
||
z-index: 999;
|
||
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.sidebar-container.sidebar-visible {
|
||
left: 0;
|
||
}
|
||
|
||
.main-content {
|
||
width: 100%;
|
||
margin-left: 0 !important;
|
||
}
|
||
}
|
||
|
||
/* 平板端样式 */
|
||
@media (min-width: 768px) and (max-width: 1024px) {
|
||
.sidebar-container {
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
width: 120px;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.main-content {
|
||
width: calc(100% - 120px);
|
||
padding: var(--content-padding-tablet, 20px);
|
||
}
|
||
}
|
||
|
||
/* 桌面端样式 */
|
||
@media (min-width: 1024px) {
|
||
.sidebar-container {
|
||
position: relative;
|
||
}
|
||
}
|
||
|
||
/* 主题变量 */
|
||
:host {
|
||
--sidebar-bg: #ffffff;
|
||
--header-bg: #ffffff;
|
||
--content-bg: #f8fafc;
|
||
--border-color: #e5e7eb;
|
||
--content-padding: 24px;
|
||
--content-padding-mobile: 16px;
|
||
}
|
||
|
||
/* 深色主题 */
|
||
.dark .main-layout {
|
||
--sidebar-bg: #1f2937;
|
||
--header-bg: #1f2937;
|
||
--content-bg: #111827;
|
||
--border-color: #374151;
|
||
--breadcrumb-bg: #111827;
|
||
}
|
||
|
||
/* 动画类 */
|
||
.fade-enter-active,
|
||
.fade-leave-active {
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.fade-enter-from,
|
||
.fade-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
.slide-enter-active,
|
||
.slide-leave-active {
|
||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.slide-enter-from {
|
||
transform: translateX(-100%);
|
||
}
|
||
|
||
.slide-leave-to {
|
||
transform: translateX(-100%);
|
||
}
|
||
</style> |