refactor(layout): 重构顶部菜单导航实现逻辑
- 修改Settings组件中的导航类型监听逻辑,修正响应式值访问方式 - 重写TopBar组件的菜单渲染结构,实现更灵活的子菜单展示 - 添加菜单选择事件处理器,支持多种路由跳转模式 - 优化菜单激活状态计算逻辑,改进侧边栏路由过滤机制 - 调整样式布局,适配顶部菜单与内容区域的定位关系 - 移除旧的SidebarItem组件引用,简化代码结构
This commit is contained in:
@@ -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)
|
||||
// 保留当前 sidebarRouters,TopNav 组件会根据当前路由自动筛选
|
||||
}
|
||||
if (val.value == 3) {
|
||||
if (val == 3) {
|
||||
// 纯顶部菜单:隐藏侧边栏
|
||||
appStore.sidebar.opened = false
|
||||
appStore.toggleSideBarHide(true)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user