648 lines
14 KiB
Vue
648 lines
14 KiB
Vue
<template>
|
||
<a-layout class="app-wrapper" :class="layoutClass">
|
||
<!-- 默认布局:左侧双栏布局 -->
|
||
<template v-if="layoutMode === 'default'">
|
||
<!-- 第一个侧边栏:显示一级菜单 -->
|
||
<a-layout-sider theme="dark" width="70" class="left-sidebar">
|
||
<div class="logo-box">
|
||
<span class="logo-text">VUE</span>
|
||
</div>
|
||
<ul class="left-nav">
|
||
<li v-for="(item, index) in menuList" :key="index"
|
||
:class="{ active: selectedParentMenu?.path === item.path }"
|
||
@click="handleParentMenuClick(item)">
|
||
<component :is="getIconComponent(item.meta?.icon)" />
|
||
<span>{{ item.meta?.title }}</span>
|
||
</li>
|
||
</ul>
|
||
</a-layout-sider>
|
||
|
||
<!-- 第二个侧边栏:显示选中的父菜单的子菜单 -->
|
||
<a-layout-sider
|
||
v-if="selectedParentMenu && selectedParentMenu.children && selectedParentMenu.children.length > 0"
|
||
theme="light" :collapsed="sidebarCollapsed" :collapsible="true" @collapse="handleCollapse" width="200"
|
||
:collapsed-width="64" class="right-sidebar">
|
||
<div class="parent-title">
|
||
<component :is="getIconComponent(selectedParentMenu.meta?.icon)" />
|
||
<span v-if="!sidebarCollapsed">{{ selectedParentMenu.meta?.title }}</span>
|
||
</div>
|
||
<a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline"
|
||
:selected-keys="[route.path]">
|
||
<navMenu :menu-items="selectedParentMenu.children" :active-path="route.path" />
|
||
</a-menu>
|
||
</a-layout-sider>
|
||
|
||
<a-layout class="main-layout">
|
||
<a-layout-header class="app-header">
|
||
<div class="header-left">
|
||
<breadcrumb />
|
||
</div>
|
||
<userbar />
|
||
</a-layout-header>
|
||
<tags />
|
||
<a-layout-content class="app-main">
|
||
<router-view v-slot="{ Component }">
|
||
<keep-alive :include="cachedViews">
|
||
<component :is="Component" :key="refreshKey" />
|
||
</keep-alive>
|
||
</router-view>
|
||
</a-layout-content>
|
||
</a-layout>
|
||
</template>
|
||
|
||
<!-- Menu布局:左侧菜单栏布局 -->
|
||
<template v-else-if="layoutMode === 'menu'">
|
||
<a-layout-sider theme="light" style="border-right: 1px solid #f0f0f0" :collapsed="sidebarCollapsed"
|
||
:collapsible="true" @collapse="handleCollapse" class="full-menu-sidebar" width="200"
|
||
:collapsed-width="64">
|
||
<div class="logo-box-full">
|
||
<span v-if="!sidebarCollapsed" class="logo-text">VUE ADMIN</span>
|
||
<span v-else class="logo-text-mini">V</span>
|
||
</div>
|
||
<a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline"
|
||
:selected-keys="[route.path]">
|
||
<navMenu :menu-items="menuList" :active-path="route.path" />
|
||
</a-menu>
|
||
</a-layout-sider>
|
||
<a-layout class="main-layout">
|
||
<a-layout-header class="app-header">
|
||
<div class="header-left">
|
||
<breadcrumb />
|
||
</div>
|
||
<userbar />
|
||
</a-layout-header>
|
||
<tags />
|
||
<a-layout-content class="app-main">
|
||
<router-view v-slot="{ Component }">
|
||
<keep-alive :include="cachedViews">
|
||
<component :is="Component" :key="$route.fullPath" />
|
||
</keep-alive>
|
||
</router-view>
|
||
</a-layout-content>
|
||
</a-layout>
|
||
</template>
|
||
|
||
<!-- Top布局:顶部菜单栏布局 -->
|
||
<template v-else-if="layoutMode === 'top'">
|
||
<a-layout-header class="app-header top-header">
|
||
<div class="top-header-left">
|
||
<div class="logo-box-top">
|
||
<span class="logo-text">VUE ADMIN</span>
|
||
</div>
|
||
<a-menu v-model:selectedKeys="selectedKeys" mode="horizontal" :selected-keys="[route.path]"
|
||
style="line-height: 60px">
|
||
<navMenu :menu-items="menuList" :active-path="route.path" />
|
||
</a-menu>
|
||
</div>
|
||
<userbar />
|
||
</a-layout-header>
|
||
<tags />
|
||
<a-layout-content class="app-main top-content">
|
||
<router-view v-slot="{ Component }">
|
||
<keep-alive :include="cachedViews">
|
||
<component :is="Component" :key="refreshKey" />
|
||
</keep-alive>
|
||
</router-view>
|
||
</a-layout-content>
|
||
</template>
|
||
|
||
<!-- 漂浮的设置按钮 -->
|
||
<a-float-button type="primary" @click="openSetting">
|
||
<template #icon>
|
||
<SettingOutlined />
|
||
</template>
|
||
</a-float-button>
|
||
|
||
<!-- 布局设置组件 -->
|
||
<setting ref="settingRef" />
|
||
</a-layout>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref, watch, onMounted } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { useLayoutStore } from '@/stores/modules/layout'
|
||
import { useUserStore } from '@/stores/modules/user'
|
||
import { SettingOutlined } from '@ant-design/icons-vue'
|
||
import * as icons from '@ant-design/icons-vue'
|
||
|
||
import userbar from './components/userbar.vue'
|
||
import navMenu from './components/navMenu.vue'
|
||
import breadcrumb from './components/breadcrumb.vue'
|
||
import tags from './components/tags.vue'
|
||
import setting from './components/setting.vue'
|
||
|
||
// 定义组件名称(多词命名)
|
||
defineOptions({
|
||
name: 'AppLayouts',
|
||
})
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const layoutStore = useLayoutStore()
|
||
const userStore = useUserStore()
|
||
|
||
const settingRef = ref(null)
|
||
|
||
const layoutMode = computed(() => layoutStore.layoutMode)
|
||
const sidebarCollapsed = computed(() => layoutStore.sidebarCollapsed)
|
||
const selectedParentMenu = computed(() => layoutStore.selectedParentMenu)
|
||
|
||
// 缓存的视图列表
|
||
const cachedViews = computed(() => {
|
||
return layoutStore.viewTags.filter((tag) => !tag.meta?.noCache).map((tag) => tag.name)
|
||
})
|
||
|
||
// 布局类名
|
||
const layoutClass = computed(() => {
|
||
return {
|
||
'layout-default': layoutMode.value === 'default',
|
||
'layout-menu': layoutMode.value === 'menu',
|
||
'layout-top': layoutMode.value === 'top',
|
||
'is-collapse': sidebarCollapsed.value,
|
||
}
|
||
})
|
||
|
||
// 获取刷新 key
|
||
const refreshKey = computed(() => layoutStore.refreshKey)
|
||
|
||
const openKeys = ref([])
|
||
const selectedKeys = ref([])
|
||
const menuList = computed(() => {
|
||
return userStore.menu
|
||
})
|
||
|
||
// 获取图标组件
|
||
const getIconComponent = (iconName) => {
|
||
return icons[iconName] || icons.FileTextOutlined
|
||
}
|
||
|
||
// 处理父菜单点击(默认布局的第一级菜单)
|
||
const handleParentMenuClick = (item) => {
|
||
// 设置选中的父菜单
|
||
layoutStore.setSelectedParentMenu(item)
|
||
// 如果没有子菜单,直接跳转
|
||
if (!item.children || item.children.length === 0) {
|
||
if (item.path) {
|
||
router.push(item.path)
|
||
}
|
||
} else {
|
||
// 默认展开第一个子菜单
|
||
if (item.children.length > 0 && item.children[0].path) {
|
||
router.push(item.children[0].path)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理折叠
|
||
const handleCollapse = (collapsed) => {
|
||
layoutStore.sidebarCollapsed = collapsed
|
||
}
|
||
|
||
// 打开设置抽屉
|
||
const openSetting = () => {
|
||
settingRef.value?.openDrawer()
|
||
}
|
||
|
||
// 更新选中的菜单和展开的菜单
|
||
const updateMenuState = () => {
|
||
selectedKeys.value = [route.path]
|
||
|
||
// 获取所有父级路径
|
||
const matched = route.matched.filter((item) => item.path !== '/' && item.path !== route.path)
|
||
const parentPaths = matched.map((item) => item.path)
|
||
|
||
// 对于不同的布局模式,处理方式不同
|
||
if (layoutMode.value === 'default') {
|
||
// 默认布局:找到当前路由对应的父菜单
|
||
const currentMenu = findMenuByPath(menuList.value, route.path)
|
||
if (currentMenu) {
|
||
// 如果当前菜单有子菜单,设置为选中的父菜单
|
||
if (currentMenu.children && currentMenu.children.length > 0) {
|
||
layoutStore.setSelectedParentMenu(currentMenu)
|
||
} else {
|
||
// 如果当前菜单是子菜单,找到它的父菜单
|
||
const parentMenu = findParentMenu(menuList.value, route.path)
|
||
if (parentMenu) {
|
||
layoutStore.setSelectedParentMenu(parentMenu)
|
||
} else {
|
||
layoutStore.setSelectedParentMenu(currentMenu)
|
||
}
|
||
}
|
||
}
|
||
} else if (!sidebarCollapsed.value) {
|
||
// 其他布局模式:展开所有父级菜单
|
||
openKeys.value = parentPaths
|
||
}
|
||
}
|
||
|
||
// 根据路径查找菜单
|
||
const findMenuByPath = (menus, path) => {
|
||
for (const menu of menus) {
|
||
if (menu.path === path) {
|
||
return menu
|
||
}
|
||
if (menu.children && menu.children.length > 0) {
|
||
const found = findMenuByPath(menu.children, path)
|
||
if (found) {
|
||
return found
|
||
}
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
// 查找父菜单
|
||
const findParentMenu = (menus, path) => {
|
||
for (const menu of menus) {
|
||
if (menu.children && menu.children.length > 0) {
|
||
for (const child of menu.children) {
|
||
if (child.path === path) {
|
||
return menu
|
||
}
|
||
if (child.children && child.children.length > 0) {
|
||
const found = findParentMenu([child], path)
|
||
if (found) {
|
||
return menu
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
// 监听路由变化,更新菜单状态
|
||
watch(
|
||
() => route.path,
|
||
(newPath) => {
|
||
console.log('路由变化:', newPath)
|
||
updateMenuState()
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
|
||
// 监听布局模式变化,确保菜单状态正确
|
||
watch(
|
||
() => layoutMode.value,
|
||
() => {
|
||
updateMenuState()
|
||
},
|
||
)
|
||
|
||
// 监听折叠状态
|
||
watch(
|
||
() => sidebarCollapsed.value,
|
||
(val) => {
|
||
if (val) {
|
||
openKeys.value = []
|
||
} else {
|
||
updateMenuState()
|
||
}
|
||
},
|
||
)
|
||
|
||
// 初始化
|
||
onMounted(() => {
|
||
// 如果还没有选中的父菜单,默认选中第一个
|
||
if (layoutMode.value === 'default' && !selectedParentMenu.value && menuList.value.length > 0) {
|
||
layoutStore.setSelectedParentMenu(menuList.value[0])
|
||
}
|
||
updateMenuState()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.app-wrapper {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100vh;
|
||
display: flex;
|
||
overflow: hidden;
|
||
|
||
.app-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
background-color: #ffffff;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||
height: 60px;
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
.collapse-btn {
|
||
font-size: 16px;
|
||
padding: 0 8px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.app-main {
|
||
background-color: #f0f2f5;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
height: calc(100vh - 106px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* 默认布局 - 双栏菜单 */
|
||
&.layout-default {
|
||
.left-sidebar {
|
||
background-color: #001529;
|
||
display: flex;
|
||
flex-direction: column;
|
||
z-index: 10;
|
||
|
||
.logo-box {
|
||
width: 100%;
|
||
height: 64px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
|
||
.logo-text {
|
||
color: #ffffff;
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
letter-spacing: 2px;
|
||
}
|
||
}
|
||
|
||
.left-nav {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
|
||
&::-webkit-scrollbar {
|
||
width: 4px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
li {
|
||
height: 70px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: rgba(255, 255, 255, 0.65);
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
border-left: 3px solid transparent;
|
||
position: relative;
|
||
|
||
&:hover {
|
||
color: #ffffff;
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
&.active {
|
||
color: #ffffff;
|
||
background-color: #1890ff;
|
||
border-left-color: #ffffff;
|
||
}
|
||
|
||
:deep(.anticon) {
|
||
font-size: 20px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
span {
|
||
font-size: 12px;
|
||
text-align: center;
|
||
line-height: 1.2;
|
||
word-break: break-all;
|
||
padding: 0 4px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.right-sidebar {
|
||
background-color: #ffffff;
|
||
box-shadow: 1px 0 6px rgba(0, 0, 0, 0.1);
|
||
z-index: 9;
|
||
|
||
.parent-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
gap: 5px;
|
||
height: 60px;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: #262626;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
:deep(.ant-menu) {
|
||
border-right: none;
|
||
|
||
.ant-menu-item {
|
||
&:hover {
|
||
color: #1890ff;
|
||
background-color: #e6f7ff;
|
||
}
|
||
|
||
&.ant-menu-item-selected {
|
||
background-color: #e6f7ff;
|
||
|
||
&::after {
|
||
border-color: #1890ff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.ant-menu-submenu {
|
||
>.ant-menu-submenu-title {
|
||
&:hover {
|
||
color: #1890ff;
|
||
}
|
||
}
|
||
|
||
&.ant-menu-submenu-open {
|
||
>.ant-menu-submenu-title {
|
||
color: #1890ff;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.main-layout {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.app-header {
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.app-main {
|
||
height: calc(100vh - 106px);
|
||
}
|
||
}
|
||
|
||
/* Menu布局 */
|
||
&.layout-menu {
|
||
.full-menu-sidebar {
|
||
background-color: #ffffff;
|
||
z-index: 10;
|
||
transition: all 0.2s;
|
||
|
||
.logo-box-full {
|
||
width: 100%;
|
||
height: 60px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
transition: all 0.2s;
|
||
|
||
.logo-text {
|
||
color: #1890ff;
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.logo-text-mini {
|
||
color: #1890ff;
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-menu) {
|
||
border-right: none;
|
||
height: calc(100% - 60px);
|
||
overflow-y: auto;
|
||
|
||
&::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.ant-menu-item {
|
||
margin: 0;
|
||
height: 44px;
|
||
line-height: 44px;
|
||
padding-left: 20px !important;
|
||
|
||
&:hover {
|
||
color: #1890ff;
|
||
background-color: #e6f7ff;
|
||
}
|
||
|
||
&.ant-menu-item-selected {
|
||
background-color: #e6f7ff;
|
||
|
||
&::after {
|
||
border-color: #1890ff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.ant-menu-submenu {
|
||
>.ant-menu-submenu-title {
|
||
height: 44px;
|
||
line-height: 44px;
|
||
margin: 0;
|
||
padding-left: 20px !important;
|
||
|
||
&:hover {
|
||
color: #1890ff;
|
||
}
|
||
}
|
||
|
||
&.ant-menu-submenu-open {
|
||
>.ant-menu-submenu-title {
|
||
color: #1890ff;
|
||
}
|
||
}
|
||
|
||
.ant-menu-sub {
|
||
background-color: #fafafa;
|
||
|
||
.ant-menu-item {
|
||
padding-left: 40px !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.main-layout {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.app-header {
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.app-main {
|
||
height: calc(100vh - 106px);
|
||
}
|
||
}
|
||
|
||
/* Top布局 */
|
||
&.layout-top {
|
||
flex-direction: column;
|
||
|
||
.top-header {
|
||
padding: 0 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
height: 60px;
|
||
border-bottom: none;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||
|
||
.top-header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 30px;
|
||
|
||
.logo-box-top {
|
||
.logo-text {
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
letter-spacing: 1px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.top-content {
|
||
height: calc(100vh - 116px);
|
||
}
|
||
}
|
||
|
||
/* 通用菜单样式优化 */
|
||
:deep(.ant-menu-item-icon) {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
</style>
|