优化代码,更新布局

This commit is contained in:
2026-01-16 22:44:31 +08:00
parent 865f7fd9d6
commit 8cf5b446cf
2 changed files with 380 additions and 18 deletions

View File

@@ -1,5 +1,54 @@
<template>
<template v-for="item in menuItems" :key="item.path || item.name">
<!-- 有子菜单 - 使用递归 -->
<a-sub-menu v-if="item.children && item.children.length > 0" :key="`submenu-${item.path}`">
<template #icon v-if="item.meta?.icon">
<component :is="getIconComponent(item.meta.icon)" />
</template>
<template #title>{{ item.meta?.title || item.name }}</template>
<navMenu :menu-items="item.children" :active-path="activePath" :parent-path="item.path" />
</a-sub-menu>
<!-- 无子菜单的菜单项 -->
<a-menu-item v-else :key="item.path" :class="{ 'ant-menu-item-selected': item.path === activePath }"
@click="handleMenuClick(item)">
<template #icon v-if="item.meta?.icon">
<component :is="getIconComponent(item.meta.icon)" />
</template>
{{ item.meta?.title || item.name }}
</a-menu-item>
</template>
</template>
<script setup>
import { useRouter } from 'vue-router'
import * as icons from '@ant-design/icons-vue'
defineProps({
menuItems: {
type: Array,
default: () => []
},
activePath: {
type: String,
default: ''
},
parentPath: {
type: String,
default: ''
}
})
const router = useRouter()
// 获取图标组件
const getIconComponent = (iconName) => {
return icons[iconName] || icons.MenuOutlined
}
// 处理菜单点击
const handleMenuClick = (item) => {
if (item.path) {
router.push(item.path)
}
}
</script>

View File

@@ -3,13 +3,15 @@
<!-- 默认布局左侧双栏布局 -->
<template v-if="layoutMode === 'default'">
<!-- 第一个侧边栏显示一级菜单 -->
<a-layout-sider theme="dark" width="70" class="level1-sidebar">
<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">
<component :is="item.meta?.icon" />
<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>
@@ -18,9 +20,12 @@
<!-- 第二个侧边栏显示选中的父菜单的子菜单 -->
<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">
<a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline" :items="menuList">
<navMenu />
theme="light" :collapsed="sidebarCollapsed" :collapsible="true" @collapse="handleCollapse" width="200"
:collapsed-width="64" class="right-sidebar">
<div class="parent-title">{{ selectedParentMenu.meta?.title }}</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>
@@ -51,8 +56,9 @@
<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" :items="menuList">
<navMenu />
<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">
@@ -80,8 +86,8 @@
<div class="logo-box-top">
<span class="logo-text">VUE ADMIN</span>
</div>
<a-menu v-model:selectedKeys="selectedKeys" mode="horizontal" :items="menuList">
<navMenu />
<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 />
@@ -109,10 +115,12 @@
</template>
<script setup>
import { computed, defineOptions, ref } from 'vue'
import { computed, defineOptions, ref, watch, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useLayoutStore } from '@/stores/modules/layout'
import { SettingOutlined } from '@ant-design/icons-vue'
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'
@@ -122,10 +130,13 @@ import setting from './components/setting.vue'
// 定义组件名称(多词命名)
defineOptions({
name: 'AppLayouts'
name: 'AppLayouts'
});
const route = useRoute()
const router = useRouter()
const layoutStore = useLayoutStore()
const userStore = useUserStore()
const settingRef = ref(null)
@@ -153,9 +164,31 @@ const layoutClass = computed(() => {
const openKeys = ref([])
const selectedKeys = ref([])
const menuList = computed(() => {
return useUserStore().menu
return userStore.menu
})
// 获取图标组件
const getIconComponent = (iconName) => {
return icons[iconName] || icons.MenuOutlined
}
// 处理父菜单点击(默认布局的第一级菜单)
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
@@ -165,6 +198,105 @@ const handleCollapse = (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)
console.log(currentMenu)
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, () => {
console.log('布局模式变化:', 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">
@@ -207,7 +339,7 @@ const openSetting = () => {
/* 默认布局 - 双栏菜单 */
&.layout-default {
.level1-sidebar {
.left-sidebar {
background-color: #001529;
display: flex;
flex-direction: column;
@@ -228,12 +360,122 @@ const openSetting = () => {
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;
&::before {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
border-width: 6px;
border-style: solid;
border-color: transparent #ffffff transparent transparent;
}
}
: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;
}
}
}
}
.level2-sidebar {
.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;
height: 60px;
font-size: 18px;
font-weight: 500;
color: #262626;
border-bottom: 1px solid #f0f0f0;
background-color: #fafafa;
}
: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 {
@@ -257,6 +499,7 @@ const openSetting = () => {
.full-menu-sidebar {
background-color: #ffffff;
z-index: 10;
transition: all 0.2s;
.logo-box-full {
width: 100%;
@@ -265,6 +508,7 @@ const openSetting = () => {
align-items: center;
justify-content: center;
border-bottom: 1px solid #f0f0f0;
transition: all 0.2s;
.logo-text {
color: #1890ff;
@@ -279,6 +523,68 @@ const openSetting = () => {
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 {
@@ -306,15 +612,17 @@ const openSetting = () => {
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: 20px;
gap: 30px;
.logo-box-top {
.logo-text {
color: #1890ff;
font-size: 20px;
font-weight: bold;
letter-spacing: 1px;
@@ -327,5 +635,10 @@ const openSetting = () => {
height: calc(100vh - 116px);
}
}
/* 通用菜单样式优化 */
:deep(.ant-menu-item-icon) {
font-size: 16px;
}
}
</style>