refactor(layout): 重构顶部菜单导航实现逻辑
- 修改Settings组件中的导航类型监听逻辑,修正响应式值访问方式 - 重写TopBar组件的菜单渲染结构,实现更灵活的子菜单展示 - 添加菜单选择事件处理器,支持多种路由跳转模式 - 优化菜单激活状态计算逻辑,改进侧边栏路由过滤机制 - 调整样式布局,适配顶部菜单与内容区域的定位关系 - 移除旧的SidebarItem组件引用,简化代码结构
This commit is contained in:
@@ -167,20 +167,20 @@ function handleNavType(val) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 菜单导航设置 */
|
/** 菜单导航设置 */
|
||||||
watch(() => navType, val => {
|
watch(() => navType.value, val => {
|
||||||
if (val.value == 1) {
|
if (val == 1) {
|
||||||
// 纯左侧菜单
|
// 纯左侧菜单:显示侧边栏,使用全部路由
|
||||||
appStore.sidebar.opened = true
|
appStore.sidebar.opened = true
|
||||||
appStore.toggleSideBarHide(false)
|
appStore.toggleSideBarHide(false)
|
||||||
permissionStore.setSidebarRouters(permissionStore.defaultRoutes)
|
permissionStore.setSidebarRouters(permissionStore.defaultRoutes)
|
||||||
}
|
}
|
||||||
if (val.value == 2) {
|
if (val == 2) {
|
||||||
// 混合菜单:顶部显示一级菜单,左侧显示子菜单
|
// 混合菜单:顶部显示一级菜单,侧边栏由 TopNav activeRoutes 自动过滤
|
||||||
appStore.sidebar.opened = true
|
appStore.sidebar.opened = true
|
||||||
appStore.toggleSideBarHide(false)
|
appStore.toggleSideBarHide(false)
|
||||||
permissionStore.setSidebarRouters(permissionStore.defaultRoutes)
|
// 保留当前 sidebarRouters,TopNav 组件会根据当前路由自动筛选
|
||||||
}
|
}
|
||||||
if (val.value == 3) {
|
if (val == 3) {
|
||||||
// 纯顶部菜单:隐藏侧边栏
|
// 纯顶部菜单:隐藏侧边栏
|
||||||
appStore.sidebar.opened = false
|
appStore.sidebar.opened = false
|
||||||
appStore.toggleSideBarHide(true)
|
appStore.toggleSideBarHide(true)
|
||||||
|
|||||||
@@ -1,68 +1,267 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-menu class="topbar-menu" :ellipsis="false" :default-active="activeMenu" :active-text-color="theme" mode="horizontal">
|
<el-menu
|
||||||
<sidebar-item :key="route.path + index" v-for="(route, index) in topMenus" :item="route" :base-path="route.path" />
|
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>
|
<template #title>
|
||||||
<span>更多菜单</span>
|
<span>更多菜单</span>
|
||||||
</template>
|
</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-sub-menu>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import SidebarItem from '../Sidebar/SidebarItem'
|
import {constantRoutes} from "@/router"
|
||||||
import useAppStore from '@/store/modules/app'
|
import {isHttp} from "@/utils/validate"
|
||||||
import useSettingsStore from '@/store/modules/settings'
|
import useAppStore from "@/store/modules/app"
|
||||||
import usePermissionStore from '@/store/modules/permission'
|
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 appStore = useAppStore()
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
const permissionStore = usePermissionStore()
|
const permissionStore = usePermissionStore()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const sidebarRouters = computed(() => permissionStore.sidebarRouters)
|
|
||||||
const theme = computed(() => settingsStore.theme)
|
const theme = computed(() => settingsStore.theme)
|
||||||
const device = computed(() => appStore.device)
|
const routers = computed(() => permissionStore.topbarRouters)
|
||||||
const activeMenu = computed(() => {
|
|
||||||
const { meta, path } = route
|
const topMenus = computed(() => {
|
||||||
if (meta.activeMenu) {
|
let menus = []
|
||||||
return meta.activeMenu
|
routers.value.map((menu) => {
|
||||||
}
|
if (menu.hidden !== true) {
|
||||||
return path
|
if (menu.path === "/") {
|
||||||
|
menus.push(menu.children[0])
|
||||||
|
} else {
|
||||||
|
menus.push(menu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return menus
|
||||||
})
|
})
|
||||||
|
|
||||||
const visibleNumber = ref(5)
|
const childrenMenus = computed(() => {
|
||||||
const topMenus = computed(() => {
|
let children = []
|
||||||
return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(0, visibleNumber.value)
|
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() {
|
function setVisibleNumber() {
|
||||||
const width = document.body.getBoundingClientRect().width / 3
|
const width = document.body.getBoundingClientRect().width / 3
|
||||||
visibleNumber.value = Math.max(1, parseInt(width / 85))
|
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(() => {
|
onMounted(() => {
|
||||||
|
setVisibleNumber()
|
||||||
window.addEventListener('resize', setVisibleNumber)
|
window.addEventListener('resize', setVisibleNumber)
|
||||||
})
|
})
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('resize', setVisibleNumber)
|
window.removeEventListener('resize', setVisibleNumber)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
setVisibleNumber()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<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 {
|
.topbar-menu.el-menu--horizontal > .el-menu-item {
|
||||||
float: left;
|
float: left;
|
||||||
height: 50px !important;
|
height: 50px !important;
|
||||||
@@ -72,28 +271,35 @@ onMounted(() => {
|
|||||||
margin: 0 10px !important;
|
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 {
|
.topbar-menu.el-menu--horizontal > .el-menu-item.is-active,
|
||||||
color: v-bind(theme);
|
.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 {
|
.topbar-menu.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
|
||||||
float: left;
|
float: left;
|
||||||
|
height: 50px !important;
|
||||||
line-height: 50px !important;
|
line-height: 50px !important;
|
||||||
color: #303133 !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 {
|
.topbar-menu .el-sub-menu .el-sub-menu__icon-arrow {
|
||||||
position: static;
|
position: static;
|
||||||
|
vertical-align: middle;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
margin-top: 0px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<TopBar v-if="topNav === 2 || topNav === 3" />
|
<TopBar v-if="topNav === 2 || topNav === 3" />
|
||||||
<!-- 内容区 -->
|
<!-- 内容区 -->
|
||||||
<div
|
<div
|
||||||
:class="{ 'hasTagsView': needTagsView, 'sidebar-hidden': sidebar.hide }"
|
:class="{ 'hasTagsView': needTagsView, 'sidebar-hidden': sidebar.hide, 'has-topbar': topNav === 2 || topNav === 3 }"
|
||||||
class="content-wrapper"
|
class="content-wrapper"
|
||||||
>
|
>
|
||||||
<!-- 标签栏 -->
|
<!-- 标签栏 -->
|
||||||
@@ -149,6 +149,10 @@ defineExpose({
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.has-topbar .fixed-header {
|
||||||
|
top: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-hidden {
|
.sidebar-hidden {
|
||||||
.fixed-header {
|
.fixed-header {
|
||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user