refactor(layout): 重构顶部菜单导航实现逻辑

- 修改Settings组件中的导航类型监听逻辑,修正响应式值访问方式
- 重写TopBar组件的菜单渲染结构,实现更灵活的子菜单展示
- 添加菜单选择事件处理器,支持多种路由跳转模式
- 优化菜单激活状态计算逻辑,改进侧边栏路由过滤机制
- 调整样式布局,适配顶部菜单与内容区域的定位关系
- 移除旧的SidebarItem组件引用,简化代码结构
This commit is contained in:
2026-06-04 15:07:38 +08:00
parent 1e76eb005d
commit 03d03649df
3 changed files with 260 additions and 50 deletions

View File

@@ -167,20 +167,20 @@ function handleNavType(val) {
}
/** 菜单导航设置 */
watch(() => navType, val => {
if (val.value == 1) {
// 纯左侧菜单
watch(() => navType.value, val => {
if (val == 1) {
// 纯左侧菜单:显示侧边栏,使用全部路由
appStore.sidebar.opened = true
appStore.toggleSideBarHide(false)
permissionStore.setSidebarRouters(permissionStore.defaultRoutes)
}
if (val.value == 2) {
// 混合菜单:顶部显示一级菜单,左侧显示子菜单
if (val == 2) {
// 混合菜单:顶部显示一级菜单,侧边栏由 TopNav activeRoutes 自动过滤
appStore.sidebar.opened = true
appStore.toggleSideBarHide(false)
permissionStore.setSidebarRouters(permissionStore.defaultRoutes)
// 保留当前 sidebarRoutersTopNav 组件会根据当前路由自动筛选
}
if (val.value == 3) {
if (val == 3) {
// 纯顶部菜单:隐藏侧边栏
appStore.sidebar.opened = false
appStore.toggleSideBarHide(true)

View File

@@ -1,68 +1,267 @@
<template>
<el-menu class="topbar-menu" :ellipsis="false" :default-active="activeMenu" :active-text-color="theme" mode="horizontal">
<sidebar-item :key="route.path + index" v-for="(route, index) in topMenus" :item="route" :base-path="route.path" />
<el-menu
class="topbar-menu"
:default-active="activeMenu"
mode="horizontal"
:ellipsis="false"
:active-text-color="theme"
@select="handleSelect"
>
<template v-for="(item, index) in topMenus">
<template v-if="item.children && item.children.length > 0 && index < visibleNumber">
<el-sub-menu
:key="index"
:style="{'--theme': theme}"
:index="item.path"
>
<template #title>
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"
/>
<span class="menu-title">{{ item.meta.title }}</span>
</template>
<template
v-for="(child, childIndex) in item.children"
:key="childIndex"
>
<el-menu-item :index="item.path + '/' + (child.path || '')">
<svg-icon
v-if="child.meta && child.meta.icon && child.meta.icon !== '#'"
:icon-class="child.meta.icon"
/>
<template #title>
<span class="menu-title">{{ child.meta.title }}</span>
</template>
</el-menu-item>
</template>
</el-sub-menu>
</template>
<template v-else-if="index < visibleNumber">
<el-menu-item
:key="index"
:style="{'--theme': theme}"
:index="item.path"
>
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"
/>
<template #title>
<span class="menu-title">{{ item.meta.title }}</span>
</template>
</el-menu-item>
</template>
</template>
<el-sub-menu index="more" class="el-sub-menu__hide-arrow" v-if="moreRoutes.length > 0">
<el-sub-menu
v-if="topMenus.length > visibleNumber"
:style="{'--theme': theme}"
index="more"
>
<template #title>
<span>更多菜单</span>
</template>
<sidebar-item :key="route.path + index" v-for="(route, index) in moreRoutes" :item="route" :base-path="route.path" />
<template
v-for="(item, index) in topMenus"
:key="index"
>
<template v-if="item.children && item.children.length > 0 && index >= visibleNumber">
<el-sub-menu :index="item.path">
<template #title>
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"
/>
<span class="menu-title">{{ item.meta.title }}</span>
</template>
<template
v-for="(child, childIndex) in item.children"
:key="childIndex"
>
<el-menu-item :index="item.path + '/' + (child.path || '')">
<svg-icon
v-if="child.meta && child.meta.icon && child.meta.icon !== '#'"
:icon-class="child.meta.icon"
/>
<template #title>
<span class="menu-title">{{ child.meta.title }}</span>
</template>
</el-menu-item>
</template>
</el-sub-menu>
</template>
<template v-else-if="index >= visibleNumber">
<el-menu-item :index="item.path">
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"
/>
<template #title>
<span class="menu-title">{{ item.meta.title }}</span>
</template>
</el-menu-item>
</template>
</template>
</el-sub-menu>
</el-menu>
</template>
<script setup>
import SidebarItem from '../Sidebar/SidebarItem'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
import {constantRoutes} from "@/router"
import {isHttp} from "@/utils/validate"
import useAppStore from "@/store/modules/app"
import useSettingsStore from "@/store/modules/settings"
import usePermissionStore from "@/store/modules/permission"
const visibleNumber = ref(5)
const currentIndex = ref(null)
const hideList = ['/index', '/user/profile']
const route = useRoute()
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const route = useRoute()
const router = useRouter()
const sidebarRouters = computed(() => permissionStore.sidebarRouters)
const theme = computed(() => settingsStore.theme)
const device = computed(() => appStore.device)
const activeMenu = computed(() => {
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
const routers = computed(() => permissionStore.topbarRouters)
const topMenus = computed(() => {
let menus = []
routers.value.map((menu) => {
if (menu.hidden !== true) {
if (menu.path === "/") {
menus.push(menu.children[0])
} else {
menus.push(menu)
}
}
})
return menus
})
const visibleNumber = ref(5)
const topMenus = computed(() => {
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(0, visibleNumber.value)
const childrenMenus = computed(() => {
let children = []
routers.value.map((rt) => {
for (let item in rt.children) {
if (rt.children[item].parentPath === undefined) {
if (rt.path === "/") {
rt.children[item].path = "/" + rt.children[item].path
} else {
if (!isHttp(rt.children[item].path)) {
rt.children[item].path = rt.path + "/" + rt.children[item].path
}
}
rt.children[item].parentPath = rt.path
}
children.push(rt.children[item])
}
})
return constantRoutes.concat(children)
})
const moreRoutes = computed(() => {
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(visibleNumber.value)
const activeMenu = computed(() => {
const path = route.path
let activePath = path
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
const tmpPath = path.substring(1, path.length)
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"))
if (!route.meta.link) {
appStore.toggleSideBarHide(false)
}
} else if (!route.children) {
activePath = path
appStore.toggleSideBarHide(true)
}
let isChildRoute = false
for (const item of routers.value) {
if (item.children && item.children.length > 0) {
const childRoute = item.children.find(child => (item.path + '/' + (child.path || '')) === path)
if (childRoute) {
isChildRoute = true
activePath = item.path
break
}
}
}
activeRoutes(activePath)
return activePath
})
function setVisibleNumber() {
const width = document.body.getBoundingClientRect().width / 3
visibleNumber.value = Math.max(1, parseInt(width / 85))
}
function handleSelect(key, keyPath) {
currentIndex.value = key
const rt = routers.value.find(item => item.path === key)
if (isHttp(key)) {
window.open(key, "_blank")
} else {
let isChildRoute = false
let parentRoute = null
for (const item of routers.value) {
if (item.children && item.children.length > 0) {
const childRoute = item.children.find(child => (item.path + '/' + (child.path || '')) === key)
if (childRoute) {
isChildRoute = true
parentRoute = item
break
}
}
}
if (isChildRoute) {
router.push({ path: key })
appStore.toggleSideBarHide(true)
} else if (!rt || !rt.children) {
const routeMenu = childrenMenus.value.find(item => item.path === key)
if (routeMenu && routeMenu.query) {
let query = JSON.parse(routeMenu.query)
router.push({ path: key, query: query })
} else {
router.push({ path: key })
}
appStore.toggleSideBarHide(true)
} else {
activeRoutes(key)
appStore.toggleSideBarHide(false)
}
}
}
function activeRoutes(key) {
let routes = []
if (childrenMenus.value && childrenMenus.value.length > 0) {
childrenMenus.value.map((item) => {
if (key == item.parentPath || (key == "index" && "" == item.path)) {
routes.push(item)
}
})
}
if (routes.length > 0) {
permissionStore.setSidebarRouters(routes)
} else {
appStore.toggleSideBarHide(true)
}
return routes
}
onMounted(() => {
setVisibleNumber()
window.addEventListener('resize', setVisibleNumber)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', setVisibleNumber)
})
onMounted(() => {
setVisibleNumber()
})
</script>
<style lang="scss">
/* menu item */
.topbar-menu.el-menu--horizontal .el-submenu__title, .topbar-menu.el-menu--horizontal .el-menu-item {
padding: 0 10px !important;
}
.topbar-menu.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px !important;
@@ -72,28 +271,35 @@ onMounted(() => {
margin: 0 10px !important;
}
.el-sub-menu.is-active .svg-icon, .el-menu-item.is-active .svg-icon + span, .el-sub-menu.is-active .svg-icon + span, .el-sub-menu.is-active .el-sub-menu__title span {
color: v-bind(theme);
.topbar-menu.el-menu--horizontal > .el-menu-item.is-active,
.topbar-menu.el-menu--horizontal > .el-sub-menu.is-active > .el-sub-menu__title {
border-bottom: 2px solid #{'var(--theme)'} !important;
color: #303133;
}
/* sub-menu item */
.topbar-menu.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #303133 !important;
margin: 0 15px -3px!important;
padding: 0 5px !important;
margin: 0 10px !important;
}
.topbar-menu.el-menu--horizontal > .el-menu-item:not(.is-disabled):focus,
.topbar-menu.el-menu--horizontal > .el-menu-item:not(.is-disabled):hover,
.topbar-menu.el-menu--horizontal > .el-sub-menu .el-sub-menu__title:hover {
background-color: #ffffff !important;
}
.topbar-menu .svg-icon {
margin-right: 4px;
}
/* topbar more arrow */
.topbar-menu .el-sub-menu .el-sub-menu__icon-arrow {
position: static;
vertical-align: middle;
margin-left: 8px;
margin-top: 0px;
display: block !important;
}
/* menu__title el-menu-item */
.topbar-menu.el-menu--horizontal .el-sub-menu__title, .topbar-menu.el-menu--horizontal .el-menu-item {
height: 60px;
}
</style>

View File

@@ -16,7 +16,7 @@
<TopBar v-if="topNav === 2 || topNav === 3" />
<!-- 内容区 -->
<div
:class="{ 'hasTagsView': needTagsView, 'sidebar-hidden': sidebar.hide }"
:class="{ 'hasTagsView': needTagsView, 'sidebar-hidden': sidebar.hide, 'has-topbar': topNav === 2 || topNav === 3 }"
class="content-wrapper"
>
<!-- 标签栏 -->
@@ -149,6 +149,10 @@ defineExpose({
box-sizing: border-box;
}
.has-topbar .fixed-header {
top: 100px;
}
.sidebar-hidden {
.fixed-header {
left: 0 !important;