Files
vueadmin/src/layouts/index.vue
2026-01-23 22:05:09 +08:00

648 lines
14 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>
<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>