feat: 新增二级菜单子导航系统(SubNav + redirectMenu)
- 新增 SubNav 组件:Navbar 下方水平子导航条,展示当前二级菜单下的三级页面,支持滚动和下拉分组 - 新增 redirectMenu 页面:点击二级菜单时卡片式展示子页面列表 - SidebarItem 深度 >=1 时改为叶子节点渲染(小圆点标记),激活高亮回退到二级菜单 - Navbar 布局重构为 Flexbox,面包屑/SubNav 根据场景切换 - 新增 productionLine Vuex 模块,轧辊研磨页改为 store 方式读取产线数据 - Sidebar activeMenu 逻辑增强:自动定位当前页所属二级菜单
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
<div class="navbar">
|
||||
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
||||
|
||||
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav"/>
|
||||
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-show="topNav || !showSubNav"/>
|
||||
<sub-nav id="subnav-container" class="subnav-container" v-if="!topNav" @has-items="showSubNav = $event"/>
|
||||
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav"/>
|
||||
|
||||
<div class="right-menu">
|
||||
@@ -52,6 +53,7 @@
|
||||
import { mapGetters } from 'vuex'
|
||||
import Breadcrumb from '@/components/Breadcrumb'
|
||||
import TopNav from '@/components/TopNav'
|
||||
import SubNav from './SubNav'
|
||||
import Hamburger from '@/components/Hamburger'
|
||||
import Screenfull from '@/components/Screenfull'
|
||||
// import SizeSelect from '@/components/SizeSelect'
|
||||
@@ -64,6 +66,7 @@ export default {
|
||||
components: {
|
||||
Breadcrumb,
|
||||
TopNav,
|
||||
SubNav,
|
||||
Hamburger,
|
||||
Screenfull,
|
||||
// SizeSelect,
|
||||
@@ -74,7 +77,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
hasWarning: false,
|
||||
warningTimer: null
|
||||
warningTimer: null,
|
||||
showSubNav: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -153,6 +157,8 @@ export default {
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// 金属质感渐变背景
|
||||
// background: #454c51;
|
||||
// border-bottom: 1px solid #8d939b;
|
||||
@@ -162,7 +168,7 @@ export default {
|
||||
.hamburger-container {
|
||||
line-height: 46px;
|
||||
height: 100%;
|
||||
float: left;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
transition: all .3s ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
@@ -177,10 +183,16 @@ export default {
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
float: left;
|
||||
flex-shrink: 0;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.subnav-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.topmenu-container {
|
||||
position: absolute;
|
||||
left: 50px;
|
||||
@@ -192,7 +204,8 @@ export default {
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
float: right;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
height: 100%;
|
||||
line-height: 50px;
|
||||
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
<template>
|
||||
<div v-if="!item.hidden">
|
||||
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
|
||||
<template v-if="depth >= 1">
|
||||
<template v-if="hasVisibleChild">
|
||||
<app-link :to="subMenuRoute">
|
||||
<el-menu-item :index="basePath" :class="{'submenu-title-noDropdown':!isNest}">
|
||||
<item :icon="item.meta && item.meta.icon" :title="item.meta && item.meta.title" />
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<app-link :to="selfRoute">
|
||||
<el-menu-item :index="basePath" :class="{'submenu-title-noDropdown':!isNest, 'is-page-item': true}">
|
||||
<item :icon="item.meta && item.meta.icon" :title="item.meta && item.meta.title" />
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
|
||||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
|
||||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
|
||||
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
|
||||
@@ -18,6 +35,7 @@
|
||||
:is-nest="true"
|
||||
:item="child"
|
||||
:base-path="resolvePath(child.path)"
|
||||
:depth="depth + 1"
|
||||
class="nest-menu"
|
||||
/>
|
||||
</el-submenu>
|
||||
@@ -48,12 +66,38 @@ export default {
|
||||
basePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
depth: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
this.onlyOneChild = null
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
hasVisibleChild() {
|
||||
if (!this.item.children || !this.item.children.length) return false
|
||||
return this.item.children.some(c => !c.hidden)
|
||||
},
|
||||
selfRoute() {
|
||||
if (this.item.query) {
|
||||
return { path: this.basePath, query: JSON.parse(this.item.query) }
|
||||
}
|
||||
return this.basePath
|
||||
},
|
||||
subMenuRoute() {
|
||||
const title = (this.item.meta && this.item.meta.title) || this.item.name || ''
|
||||
return {
|
||||
path: '/redirect/subMenu',
|
||||
query: {
|
||||
parent: this.basePath,
|
||||
title: title
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hasOneShowingChild(children = [], parent) {
|
||||
if (!children) {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import path from 'path'
|
||||
import { mapGetters, mapState } from "vuex";
|
||||
import Logo from "./Logo";
|
||||
import SidebarItem from "./SidebarItem";
|
||||
@@ -36,12 +37,15 @@ export default {
|
||||
...mapGetters(["sidebarRouters", "sidebar"]),
|
||||
activeMenu() {
|
||||
const route = this.$route;
|
||||
const { meta, path } = route;
|
||||
// if set path, the sidebar will highlight the path you set
|
||||
const { meta, path: routePath } = route;
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu;
|
||||
}
|
||||
return path;
|
||||
const level2 = this.findLevel2Parent(routePath);
|
||||
if (level2) {
|
||||
return level2;
|
||||
}
|
||||
return routePath;
|
||||
},
|
||||
showLogo() {
|
||||
return this.$store.state.settings.sidebarLogo;
|
||||
@@ -53,5 +57,28 @@ export default {
|
||||
return !this.sidebar.opened;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
findLevel2Parent(currentPath) {
|
||||
return this.walkTree(this.sidebarRouters, currentPath, 0, '');
|
||||
},
|
||||
walkTree(routes, currentPath, depth, basePath) {
|
||||
for (const route of routes) {
|
||||
if (route.hidden) continue;
|
||||
const fullPath = basePath ? path.resolve(basePath, route.path) : route.path;
|
||||
if (currentPath !== fullPath && !currentPath.startsWith(fullPath + '/')) continue;
|
||||
if (depth >= 1) {
|
||||
return fullPath;
|
||||
}
|
||||
if (route.children && route.children.length) {
|
||||
const result = this.walkTree(route.children, currentPath, depth + 1, fullPath);
|
||||
if (result) return result;
|
||||
}
|
||||
if (currentPath === fullPath) {
|
||||
return depth >= 1 ? fullPath : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
306
klp-ui/src/layout/components/SubNav.vue
Normal file
306
klp-ui/src/layout/components/SubNav.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<div v-if="subNavItems.length > 0" class="sub-nav">
|
||||
<span class="sub-nav__arrow" :class="{ 'is-disabled': scrollLeft <= 0 }" @click="scrollTo('left')">
|
||||
<i class="el-icon-arrow-left" />
|
||||
</span>
|
||||
<div ref="subNavWrap" class="sub-nav__wrap" @mousewheel="handleWheel">
|
||||
<div ref="subNavInner" class="sub-nav__inner">
|
||||
<template v-for="item in subNavItems">
|
||||
<el-dropdown
|
||||
v-if="item._hasChildren"
|
||||
:key="item._ownPath"
|
||||
trigger="click"
|
||||
class="sub-nav__dropdown"
|
||||
@command="handleCommand"
|
||||
>
|
||||
<span class="sub-nav__item sub-nav__item--dropdown" :class="{ 'is-active': isActive(item) }">
|
||||
{{ item._title }}
|
||||
<i class="el-icon-arrow-down el-icon--right" />
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item
|
||||
v-for="child in item._children"
|
||||
:key="child._ownPath"
|
||||
:command="child._fullPath"
|
||||
:class="{ 'is-active': isChildActive(child) }"
|
||||
>
|
||||
{{ child._title }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<router-link
|
||||
v-else
|
||||
:key="item._ownPath"
|
||||
:to="item._fullPath"
|
||||
class="sub-nav__item"
|
||||
:class="{ 'is-active': isActive(item) }"
|
||||
>
|
||||
{{ item._title }}
|
||||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<span class="sub-nav__arrow" :class="{ 'is-disabled': !canScrollRight }" @click="scrollTo('right')">
|
||||
<i class="el-icon-arrow-right" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import path from 'path'
|
||||
|
||||
export default {
|
||||
name: 'SubNav',
|
||||
data() {
|
||||
return {
|
||||
scrollLeft: 0,
|
||||
canScrollRight: false,
|
||||
resizeObserver: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
subNavItems() {
|
||||
const sidebarRouters = this.$store.state.permission.sidebarRouters
|
||||
const items = this.getThirdLevelMenus(sidebarRouters)
|
||||
return items
|
||||
},
|
||||
hasItems() {
|
||||
return this.subNavItems.length > 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.$nextTick(() => {
|
||||
this.checkArrows()
|
||||
this.scrollToActive()
|
||||
})
|
||||
},
|
||||
hasItems(val) {
|
||||
this.$emit('has-items', val)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.checkArrows()
|
||||
this.scrollToActive()
|
||||
})
|
||||
if (window.ResizeObserver) {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.checkArrows()
|
||||
})
|
||||
if (this.$refs.subNavWrap) {
|
||||
this.resizeObserver.observe(this.$refs.subNavWrap)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCommand(command) {
|
||||
this.$router.push(command)
|
||||
},
|
||||
isActive(item) {
|
||||
return this.$route.path === item._ownPath || this.$route.path.startsWith(item._ownPath + '/')
|
||||
},
|
||||
isChildActive(child) {
|
||||
return this.$route.path === child._ownPath || this.$route.path.startsWith(child._ownPath + '/')
|
||||
},
|
||||
getThirdLevelMenus(routes) {
|
||||
let currentPath = this.$route.path
|
||||
if (currentPath === '/redirect/subMenu' && this.$route.query.parent) {
|
||||
currentPath = this.$route.query.parent
|
||||
}
|
||||
return this.findActiveLevel2(routes, currentPath, 0, '') || []
|
||||
},
|
||||
findActiveLevel2(routes, currentPath, depth, basePath) {
|
||||
for (const route of routes) {
|
||||
if (route.hidden) continue
|
||||
if (!route.children || route.children.length === 0) continue
|
||||
|
||||
const fullPath = basePath ? path.resolve(basePath, route.path) : route.path
|
||||
if (currentPath !== fullPath && !currentPath.startsWith(fullPath + '/')) continue
|
||||
|
||||
if (currentPath === fullPath && depth >= 1) {
|
||||
if (depth === 1) {
|
||||
return this.buildItems(route.children, fullPath)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const nested = this.findActiveLevel2(route.children, currentPath, depth + 1, fullPath)
|
||||
if (nested) return nested
|
||||
|
||||
if (depth === 1) {
|
||||
return this.buildItems(route.children, fullPath)
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
buildItems(children, parentPath) {
|
||||
return children.filter(c => !c.hidden).map(c => this.buildItem(c, parentPath))
|
||||
},
|
||||
buildItem(c, parentPath) {
|
||||
const ownPath = c.path.startsWith('/') ? c.path : path.resolve(parentPath, c.path)
|
||||
const title = (c.meta && c.meta.title) || c.name || c.path
|
||||
const hasChildren = c.children && c.children.length > 0 && c.children.some(gc => !gc.hidden)
|
||||
const item = {
|
||||
...c,
|
||||
_ownPath: ownPath,
|
||||
_fullPath: ownPath,
|
||||
_title: title,
|
||||
_hasChildren: hasChildren
|
||||
}
|
||||
if (hasChildren) {
|
||||
item._children = this.buildItems(c.children, ownPath)
|
||||
}
|
||||
return item
|
||||
},
|
||||
handleWheel(e) {
|
||||
if (!this.$refs.subNavWrap) return
|
||||
const delta = e.deltaY || e.wheelDelta
|
||||
this.$refs.subNavWrap.scrollLeft += delta
|
||||
this.scrollLeft = this.$refs.subNavWrap.scrollLeft
|
||||
this.checkArrows()
|
||||
},
|
||||
scrollTo(direction) {
|
||||
if (!this.$refs.subNavWrap) return
|
||||
const wrap = this.$refs.subNavWrap
|
||||
const scrollAmount = 200
|
||||
if (direction === 'left') {
|
||||
wrap.scrollLeft = Math.max(0, wrap.scrollLeft - scrollAmount)
|
||||
} else {
|
||||
wrap.scrollLeft = Math.min(wrap.scrollWidth - wrap.clientWidth, wrap.scrollLeft + scrollAmount)
|
||||
}
|
||||
this.scrollLeft = wrap.scrollLeft
|
||||
this.checkArrows()
|
||||
},
|
||||
checkArrows() {
|
||||
if (!this.$refs.subNavWrap) return
|
||||
const wrap = this.$refs.subNavWrap
|
||||
this.scrollLeft = wrap.scrollLeft
|
||||
this.canScrollRight = wrap.scrollWidth > wrap.clientWidth + wrap.scrollLeft + 1
|
||||
},
|
||||
scrollToActive() {
|
||||
if (!this.$refs.subNavWrap) return
|
||||
const activeEl = this.$refs.subNavWrap.querySelector('.sub-nav__item.is-active')
|
||||
if (activeEl) {
|
||||
const wrap = this.$refs.subNavWrap
|
||||
const activeLeft = activeEl.offsetLeft
|
||||
const activeRight = activeLeft + activeEl.offsetWidth
|
||||
if (activeLeft < wrap.scrollLeft) {
|
||||
wrap.scrollLeft = activeLeft
|
||||
} else if (activeRight > wrap.scrollLeft + wrap.clientWidth) {
|
||||
wrap.scrollLeft = activeRight - wrap.clientWidth
|
||||
}
|
||||
this.scrollLeft = wrap.scrollLeft
|
||||
this.checkArrows()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sub-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0 4px;
|
||||
|
||||
&__arrow {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #111;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&__wrap {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
min-width: 0;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__dropdown {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__item {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 14px;
|
||||
margin: 0 2px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
color: #111;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: var(--current-color, #409EFF);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.is-active:hover {
|
||||
background: var(--current-color, #409EFF);
|
||||
color: #fff;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&--dropdown {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.el-icon--right {
|
||||
margin-left: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .sub-nav__dropdown .el-dropdown-menu__item.is-active {
|
||||
color: var(--current-color, #409EFF);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user