deotalandAi/apps/frontend/src/components/layout/MainLayout.vue

323 lines
6.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>