feat: 新增二级菜单子导航系统(SubNav + redirectMenu)
- 新增 SubNav 组件:Navbar 下方水平子导航条,展示当前二级菜单下的三级页面,支持滚动和下拉分组 - 新增 redirectMenu 页面:点击二级菜单时卡片式展示子页面列表 - SidebarItem 深度 >=1 时改为叶子节点渲染(小圆点标记),激活高亮回退到二级菜单 - Navbar 布局重构为 Flexbox,面包屑/SubNav 根据场景切换 - 新增 productionLine Vuex 模块,轧辊研磨页改为 store 方式读取产线数据 - Sidebar activeMenu 逻辑增强:自动定位当前页所属二级菜单
This commit is contained in:
@@ -303,4 +303,17 @@
|
|||||||
|
|
||||||
#app .sidebar-container .nest-menu .el-submenu .el-submenu__title {
|
#app .sidebar-container .nest-menu .el-submenu .el-submenu__title {
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二级菜单叶子页面小圆点
|
||||||
|
.el-menu-item.is-page-item::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--current-color, #409EFF);
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
<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"/>
|
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav"/>
|
||||||
|
|
||||||
<div class="right-menu">
|
<div class="right-menu">
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import Breadcrumb from '@/components/Breadcrumb'
|
import Breadcrumb from '@/components/Breadcrumb'
|
||||||
import TopNav from '@/components/TopNav'
|
import TopNav from '@/components/TopNav'
|
||||||
|
import SubNav from './SubNav'
|
||||||
import Hamburger from '@/components/Hamburger'
|
import Hamburger from '@/components/Hamburger'
|
||||||
import Screenfull from '@/components/Screenfull'
|
import Screenfull from '@/components/Screenfull'
|
||||||
// import SizeSelect from '@/components/SizeSelect'
|
// import SizeSelect from '@/components/SizeSelect'
|
||||||
@@ -64,6 +66,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
TopNav,
|
TopNav,
|
||||||
|
SubNav,
|
||||||
Hamburger,
|
Hamburger,
|
||||||
Screenfull,
|
Screenfull,
|
||||||
// SizeSelect,
|
// SizeSelect,
|
||||||
@@ -74,7 +77,8 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hasWarning: false,
|
hasWarning: false,
|
||||||
warningTimer: null
|
warningTimer: null,
|
||||||
|
showSubNav: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -153,6 +157,8 @@ export default {
|
|||||||
height: 50px;
|
height: 50px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
// 金属质感渐变背景
|
// 金属质感渐变背景
|
||||||
// background: #454c51;
|
// background: #454c51;
|
||||||
// border-bottom: 1px solid #8d939b;
|
// border-bottom: 1px solid #8d939b;
|
||||||
@@ -162,7 +168,7 @@ export default {
|
|||||||
.hamburger-container {
|
.hamburger-container {
|
||||||
line-height: 46px;
|
line-height: 46px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
float: left;
|
flex-shrink: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all .3s ease;
|
transition: all .3s ease;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
@@ -177,10 +183,16 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-container {
|
.breadcrumb-container {
|
||||||
float: left;
|
flex-shrink: 0;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subnav-container {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.topmenu-container {
|
.topmenu-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50px;
|
left: 50px;
|
||||||
@@ -192,7 +204,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.right-menu {
|
.right-menu {
|
||||||
float: right;
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!item.hidden">
|
<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)">
|
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
|
||||||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
|
<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" />
|
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
|
||||||
@@ -18,6 +35,7 @@
|
|||||||
:is-nest="true"
|
:is-nest="true"
|
||||||
:item="child"
|
:item="child"
|
||||||
:base-path="resolvePath(child.path)"
|
:base-path="resolvePath(child.path)"
|
||||||
|
:depth="depth + 1"
|
||||||
class="nest-menu"
|
class="nest-menu"
|
||||||
/>
|
/>
|
||||||
</el-submenu>
|
</el-submenu>
|
||||||
@@ -48,12 +66,38 @@ export default {
|
|||||||
basePath: {
|
basePath: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
|
},
|
||||||
|
depth: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
this.onlyOneChild = null
|
this.onlyOneChild = null
|
||||||
return {}
|
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: {
|
methods: {
|
||||||
hasOneShowingChild(children = [], parent) {
|
hasOneShowingChild(children = [], parent) {
|
||||||
if (!children) {
|
if (!children) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import path from 'path'
|
||||||
import { mapGetters, mapState } from "vuex";
|
import { mapGetters, mapState } from "vuex";
|
||||||
import Logo from "./Logo";
|
import Logo from "./Logo";
|
||||||
import SidebarItem from "./SidebarItem";
|
import SidebarItem from "./SidebarItem";
|
||||||
@@ -36,12 +37,15 @@ export default {
|
|||||||
...mapGetters(["sidebarRouters", "sidebar"]),
|
...mapGetters(["sidebarRouters", "sidebar"]),
|
||||||
activeMenu() {
|
activeMenu() {
|
||||||
const route = this.$route;
|
const route = this.$route;
|
||||||
const { meta, path } = route;
|
const { meta, path: routePath } = route;
|
||||||
// if set path, the sidebar will highlight the path you set
|
|
||||||
if (meta.activeMenu) {
|
if (meta.activeMenu) {
|
||||||
return meta.activeMenu;
|
return meta.activeMenu;
|
||||||
}
|
}
|
||||||
return path;
|
const level2 = this.findLevel2Parent(routePath);
|
||||||
|
if (level2) {
|
||||||
|
return level2;
|
||||||
|
}
|
||||||
|
return routePath;
|
||||||
},
|
},
|
||||||
showLogo() {
|
showLogo() {
|
||||||
return this.$store.state.settings.sidebarLogo;
|
return this.$store.state.settings.sidebarLogo;
|
||||||
@@ -53,5 +57,28 @@ export default {
|
|||||||
return !this.sidebar.opened;
|
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>
|
</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>
|
||||||
@@ -35,6 +35,12 @@ export const constantRoutes = [
|
|||||||
component: Layout,
|
component: Layout,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: 'subMenu',
|
||||||
|
component: () => import('@/views/redirectMenu'),
|
||||||
|
name: 'RedirectMenu',
|
||||||
|
meta: { title: '子菜单' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/redirect/:path(.*)',
|
path: '/redirect/:path(.*)',
|
||||||
component: () => import('@/views/redirect')
|
component: () => import('@/views/redirect')
|
||||||
|
|||||||
@@ -23,5 +23,6 @@ const getters = {
|
|||||||
bomMap: state => state.category.bomMap,
|
bomMap: state => state.category.bomMap,
|
||||||
financialAccounts: state => state.finance.financialAccounts,
|
financialAccounts: state => state.finance.financialAccounts,
|
||||||
processList: state => state.craft.processList,
|
processList: state => state.craft.processList,
|
||||||
|
productionLines: state => state.productionLine.productionLines,
|
||||||
}
|
}
|
||||||
export default getters
|
export default getters
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import settings from './modules/settings'
|
|||||||
import category from './modules/category'
|
import category from './modules/category'
|
||||||
import finance from './modules/finance'
|
import finance from './modules/finance'
|
||||||
import craft from './modules/craft'
|
import craft from './modules/craft'
|
||||||
|
import productionLine from './modules/productionLine'
|
||||||
import getters from './getters'
|
import getters from './getters'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
@@ -23,7 +24,8 @@ const store = new Vuex.Store({
|
|||||||
settings,
|
settings,
|
||||||
category,
|
category,
|
||||||
finance,
|
finance,
|
||||||
craft
|
craft,
|
||||||
|
productionLine
|
||||||
},
|
},
|
||||||
getters
|
getters
|
||||||
})
|
})
|
||||||
|
|||||||
27
klp-ui/src/store/modules/productionLine.js
Normal file
27
klp-ui/src/store/modules/productionLine.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { listProductionLine } from "@/api/wms/productionLine";
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
productionLines: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
SET_PRODUCTION_LINES(state, list) {
|
||||||
|
state.productionLines = list;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
getProductionLines({ commit, state }) {
|
||||||
|
if (state.productionLines.length > 0) return Promise.resolve()
|
||||||
|
return listProductionLine({ pageNum: 1, pageSize: 100 }).then(response => {
|
||||||
|
commit('SET_PRODUCTION_LINES', response.rows || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
mutations,
|
||||||
|
actions
|
||||||
|
}
|
||||||
@@ -494,9 +494,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import { listRollInfo, getRollInfo, addRollInfo, updateRollInfo, delRollInfo } from '@/api/mes/roll/rollInfo'
|
import { listRollInfo, getRollInfo, addRollInfo, updateRollInfo, delRollInfo } from '@/api/mes/roll/rollInfo'
|
||||||
import { listRollGrind, addRollGrind, updateRollGrind, delRollGrind, getMonthlyStats } from '@/api/mes/roll/rollGrind'
|
import { listRollGrind, addRollGrind, updateRollGrind, delRollGrind, getMonthlyStats } from '@/api/mes/roll/rollGrind'
|
||||||
import { listProductionLine } from '@/api/wms/productionLine'
|
|
||||||
import { listData, addData, updateData, delData } from '@/api/system/dict/data'
|
import { listData, addData, updateData, delData } from '@/api/system/dict/data'
|
||||||
import rollLineMixin from '../rollLineMixin'
|
import rollLineMixin from '../rollLineMixin'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'GrindRoom',
|
name: 'GrindRoom',
|
||||||
@@ -504,7 +504,6 @@ export default {
|
|||||||
dicts: ['mes_roll_operator'],
|
dicts: ['mes_roll_operator'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
productionLines: [],
|
|
||||||
filterLineId: null,
|
filterLineId: null,
|
||||||
lineTabOrder: [],
|
lineTabOrder: [],
|
||||||
|
|
||||||
@@ -549,6 +548,8 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters(['productionLines']),
|
||||||
|
|
||||||
currentUserName() {
|
currentUserName() {
|
||||||
return this.$store.state.user.name || this.$store.getters.name || ''
|
return this.$store.state.user.name || this.$store.getters.name || ''
|
||||||
},
|
},
|
||||||
@@ -606,9 +607,7 @@ export default {
|
|||||||
this.filterLineId = this.lineId
|
this.filterLineId = this.lineId
|
||||||
const uid = this.$store.state.user?.userId || 0
|
const uid = this.$store.state.user?.userId || 0
|
||||||
try { this.lineTabOrder = JSON.parse(localStorage.getItem(`grind_line_order_${uid}`) || '[]') } catch { /* ignore */ }
|
try { this.lineTabOrder = JSON.parse(localStorage.getItem(`grind_line_order_${uid}`) || '[]') } catch { /* ignore */ }
|
||||||
listProductionLine({ pageNum: 1, pageSize: 100 }).then(res => {
|
this.$store.dispatch('productionLine/getProductionLines')
|
||||||
this.productionLines = res.rows || []
|
|
||||||
})
|
|
||||||
this.loadRolls()
|
this.loadRolls()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
252
klp-ui/src/views/redirectMenu.vue
Normal file
252
klp-ui/src/views/redirectMenu.vue
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
<template>
|
||||||
|
<div class="submenu-page">
|
||||||
|
<div class="submenu-page__header">
|
||||||
|
<span class="submenu-page__parent-title">{{ parentTitle }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="tree.length" class="submenu-page__list">
|
||||||
|
<div v-for="group in tree" :key="group._ownPath" class="submenu-group">
|
||||||
|
<div class="submenu-group__title" @click="goTo(group)">
|
||||||
|
<span class="submenu-group__icon">
|
||||||
|
<svg-icon v-if="group.meta && group.meta.icon" :icon-class="group.meta.icon" />
|
||||||
|
<i v-else class="el-icon-folder-opened" />
|
||||||
|
</span>
|
||||||
|
<span class="submenu-group__text">{{ group._title }}</span>
|
||||||
|
<span v-if="group._hasChildren" class="submenu-group__count">{{ group._children.length }}项</span>
|
||||||
|
<i v-if="group._hasChildren" class="el-icon-arrow-right submenu-group__arrow" />
|
||||||
|
</div>
|
||||||
|
<div v-if="group._hasChildren" class="submenu-group__items">
|
||||||
|
<div
|
||||||
|
v-for="child in group._children"
|
||||||
|
:key="child._ownPath"
|
||||||
|
class="submenu-item"
|
||||||
|
:class="{ 'submenu-item--leaf': !child._hasChildren }"
|
||||||
|
@click="goTo(child)"
|
||||||
|
>
|
||||||
|
<i v-if="child._hasChildren" class="el-icon-folder submenu-item__folder" />
|
||||||
|
<i v-else class="el-icon-document submenu-item__doc" />
|
||||||
|
<span class="submenu-item__text">{{ child._title }}</span>
|
||||||
|
<i v-if="child._hasChildren" class="el-icon-arrow-right submenu-item__arrow" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="submenu-page__empty">暂无可访问的菜单</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RedirectMenu',
|
||||||
|
computed: {
|
||||||
|
parentPath() {
|
||||||
|
return this.$route.query.parent || ''
|
||||||
|
},
|
||||||
|
parentTitle() {
|
||||||
|
return this.$route.query.title || this.parentPath
|
||||||
|
},
|
||||||
|
tree() {
|
||||||
|
const sidebarRouters = this.$store.state.permission.sidebarRouters
|
||||||
|
const children = this.findChildren(sidebarRouters, this.parentPath, '')
|
||||||
|
if (!children) return []
|
||||||
|
return this.buildTree(children, this.parentPath)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
findChildren(routes, targetPath, basePath) {
|
||||||
|
for (const route of routes) {
|
||||||
|
if (route.hidden) continue
|
||||||
|
const fullPath = basePath ? path.resolve(basePath, route.path) : route.path
|
||||||
|
if (fullPath === targetPath) {
|
||||||
|
return route.children || []
|
||||||
|
}
|
||||||
|
if (route.children && route.children.length > 0) {
|
||||||
|
const result = this.findChildren(route.children, targetPath, fullPath)
|
||||||
|
if (result) return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
buildTree(children, parentPath) {
|
||||||
|
return children
|
||||||
|
.filter(c => !c.hidden)
|
||||||
|
.map(c => {
|
||||||
|
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.buildTree(c.children, ownPath)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goTo(item) {
|
||||||
|
if (item._hasChildren) {
|
||||||
|
this.$router.push({
|
||||||
|
path: '/redirect/subMenu',
|
||||||
|
query: { parent: item._ownPath, title: item._title }
|
||||||
|
})
|
||||||
|
} else if (item.path) {
|
||||||
|
this.$router.push(item._fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.submenu-page {
|
||||||
|
padding: 16px 20px;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-page__header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-page__parent-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-page__list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-page__empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-group {
|
||||||
|
width: calc(50% - 6px);
|
||||||
|
min-width: 280px;
|
||||||
|
background: #fafbfc;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #c0c4cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-group__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
transition: background 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-group__icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--current-color, #409EFF);
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-group__text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-group__count {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #909399;
|
||||||
|
margin-right: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-group__arrow {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-group__items {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 16px 8px 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-item__folder {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #e6a23c;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-item__doc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-item__text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-item__arrow {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-item--leaf {
|
||||||
|
.submenu-item__doc {
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user