feat: 🎸 新增固定一级菜单配置

This commit is contained in:
陈凯龙 2021-03-29 17:40:04 +08:00
parent 62eeb55330
commit 4c4903e806
25 changed files with 362 additions and 59 deletions

View File

@ -1,6 +1,6 @@
{
"name": "vue-element-plus-admin",
"version": "0.0.5",
"version": "0.0.6",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",

View File

@ -36,23 +36,23 @@ export default defineComponent({
const levelList = ref<RouteRecordRaw[]>([])
function getBreadcrumb() {
let matched: any[] = currentRoute.value.matched.filter((item: RouteLocationMatched) => item.meta && item.meta.title)
const first = matched[0]
const matched: any[] = currentRoute.value.matched.filter((item: RouteLocationMatched) => item.meta && item.meta.title)
// const first = matched[0]
if (!isDashboard(first)) {
matched = [{ path: '/dashboard', meta: { title: '首页', icon: 'dashboard' }}].concat(matched)
}
// if (!isDashboard(first)) {
// matched = [{ path: '/dashboard', meta: { title: '', icon: 'dashboard' }}].concat(matched)
// }
levelList.value = matched.filter((item: RouteLocationMatched) => item.meta && item.meta.title && item.meta.breadcrumb !== false)
}
function isDashboard(route: RouteLocationMatched) {
const name = route && route.name
if (!name) {
return false
}
return (name as any).trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
}
// function isDashboard(route: RouteLocationMatched) {
// const name = route && route.name
// if (!name) {
// return false
// }
// return (name as any).trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
// }
function pathCompile(path: string): string {
const { params } = currentRoute.value

View File

@ -0,0 +1,125 @@
<template>
<el-tabs
v-model="activeName"
:tab-position="tabPosition"
@tab-click="changeTab"
>
<el-tab-pane
v-for="(item, $index) in tabRouters"
:key="$index"
:name="item.path === '/' ? '/dashboard' : item.path"
>
<template #label>
<div class="label-item">
<svg-icon :icon-class="filterTab(item, 'icon')" />
<div class="title-item">{{ filterTab(item, 'title') }}</div>
</div>
</template>
</el-tab-pane>
</el-tabs>
</template>
<script lang="ts">
import { defineComponent, ref, watch, onMounted, computed } from 'vue'
import { appStore } from '_@/store/modules/app'
import { permissionStore } from '_@/store/modules/permission'
import type { RouteRecordRaw } from 'vue-router'
import { useRouter } from 'vue-router'
import { findIndex } from '@/utils'
import { isExternal } from '@/utils/validate'
export default defineComponent({
name: 'MenuTab',
setup() {
const { currentRoute, push } = useRouter()
const activeName = ref<string>('')
const routers = computed((): RouteRecordRaw[] => permissionStore.routers)
const tabRouters = computed((): RouteRecordRaw[] => routers.value.filter(v => !v.meta?.hidden))
const layout = computed(() => appStore.layout)
const tabPosition = computed(() => layout.value === 'Classic' ? 'left' : 'top')
function init() {
const currentPath = currentRoute.value.fullPath.split('/')
const index = findIndex(tabRouters.value, (v: RouteRecordRaw) => {
if (v.path === '/') {
return `/${currentPath[1]}` === '/dashboard'
} else {
return v.path === `/${currentPath[1]}`
}
})
if (index > -1) {
activeName.value = `/${currentPath[1]}`
setActive(index)
permissionStore.SetAcitveTab(activeName.value)
}
}
function filterTab(item: RouteRecordRaw | any, key: string): any {
return item.meta && item.meta[key] ? item.meta[key] : item.children[0].meta[key]
}
function setActive(index: number): void {
const currRoute: any = tabRouters.value[index]
permissionStore.SetMenuTabRouters(currRoute.children)
}
function changeTab(item: any) {
const currRoute: any = tabRouters.value[item.index]
permissionStore.SetMenuTabRouters(currRoute.children)
if (isExternal(currRoute.children[0].path)) {
window.open(currRoute.children[0].path)
} else {
push(`${activeName.value === '/dashboard' ? '' : activeName.value}/${currRoute.children[0].path}`)
permissionStore.SetAcitveTab(activeName.value)
}
}
onMounted(() => {
init()
})
watch(
() => currentRoute.value,
() => {
init()
}
)
watch(
() => activeName.value,
(val) => {
permissionStore.SetAcitveTab(val)
}
)
return {
activeName,
tabRouters,
tabPosition,
filterTab,
setActive,
changeTab
}
}
})
</script>
<style lang="less" scoped>
.label-item {
height: 100%;
display: flex;
justify-content: center;
flex-wrap: wrap;
align-items: center;
&>div {
width: 100%;
}
.title-item {
position: relative;
top: -5px;
}
}
</style>

View File

@ -37,6 +37,11 @@
<!-- <div class="setting__title">界面功能</div> -->
<div class="setting__title">界面显示</div>
<div v-if="layout !== 'Top'" class="setting__item">
<span>固定一级菜单</span>
<el-switch v-model="showMenuTab" @change="setShowMenuTab" />
</div>
<div class="setting__item">
<span>固定Header</span>
<el-switch v-model="fixedHeader" @change="setFixedHeader" />
@ -117,6 +122,7 @@ export default defineComponent({
if (mode === layout.value) return
appStore.SetLayout(mode)
appStore.SetCollapsed(false)
mode === 'Top' && appStore.SetShowMenuTab(false)
}
const fixedHeader = ref<boolean>(appStore.fixedHeader)
@ -179,6 +185,11 @@ export default defineComponent({
appStore.SetShowBackTop(showBackTop)
}
const showMenuTab = ref<boolean>(appStore.showMenuTab)
function setShowMenuTab(showMenuTab: boolean) {
appStore.SetShowMenuTab(showMenuTab)
}
return {
drawer, toggleClick,
layout, setLayout,
@ -193,7 +204,8 @@ export default defineComponent({
title, setTitle,
logoTitle, setLogoTitle,
greyMode, setGreyMode,
showBackTop, setShowBackTop
showBackTop, setShowBackTop,
showMenuTab, setShowMenuTab
}
}
})

View File

@ -1,7 +1,7 @@
<template>
<template v-if="!item.meta?.hidden">
<template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.meta?.alwaysShow">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown': !isNest}">
<el-menu-item :index="resolvePath(onlyOneChild.path, showMenuTab ? `${activeTab === '/dashboard' ? '' : activeTab}/${basePath}` : '')" :class="{'submenu-title-noDropdown': !isNest}">
<item v-if="onlyOneChild.meta" :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
<template #title>
<span class="anticon-item">{{ onlyOneChild.meta.title }}</span>
@ -14,7 +14,7 @@
:popper-class="layout !== 'Top'
? 'nest-popper-menu'
: 'top-popper-menu'"
:index="resolvePath(item.path)"
:index="resolvePath(item.path, showMenuTab ? `${activeTab === '/dashboard' ? '' : activeTab}/${basePath}` : '')"
>
<template #title>
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
@ -32,11 +32,13 @@
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from 'vue'
import { defineComponent, PropType, ref, computed } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item.vue'
import { permissionStore } from '_@/store/modules/permission'
import { appStore } from '_@/store/modules/app'
export default defineComponent({
name: 'SiderItem',
components: { Item },
@ -62,6 +64,9 @@ export default defineComponent({
setup(props) {
const onlyOneChild = ref<any>(null)
const activeTab = computed(() => permissionStore.activeTab)
const showMenuTab = computed(() => appStore.showMenuTab)
function hasOneShowingChild(children: RouteRecordRaw[] = [], parent: RouteRecordRaw): boolean {
const showingChildren: RouteRecordRaw[] = children.filter((item: RouteRecordRaw) => {
if (item.meta && item.meta.hidden) {
@ -87,14 +92,16 @@ export default defineComponent({
return false
}
function resolvePath(routePath: string): string {
function resolvePath(routePath: string, otherPath: string): string {
if (isExternal(routePath)) {
return routePath
}
return path.resolve(props.basePath, routePath)
return path.resolve(otherPath || props.basePath, routePath)
}
return {
onlyOneChild,
activeTab,
showMenuTab,
hasOneShowingChild,
resolvePath
}

View File

@ -12,7 +12,7 @@
@select="selectMenu"
>
<sider-item
v-for="route in routers"
v-for="route in showMenuTab ? menuTabRouters : routers"
:key="route.path"
:item="route"
:layout="layout"
@ -62,7 +62,12 @@ export default defineComponent({
const collapsed = computed(() => appStore.collapsed)
const showLogo = computed(() => appStore.showLogo)
const showMenuTab = computed(() => appStore.showMenuTab)
const menuTabRouters = computed(() => permissionStore.menuTabRouters)
const activeTab = computed(() => permissionStore.activeTab)
function selectMenu(path: string) {
if (currentRoute.value.fullPath === path) return
if (isExternal(path)) {
window.open(path)
} else {
@ -75,6 +80,9 @@ export default defineComponent({
activeMenu,
collapsed,
showLogo,
showMenuTab,
menuTabRouters,
activeTab,
variables,
selectMenu
}

View File

@ -1,10 +1,13 @@
<template>
<div :class="classObj" class="app__wrap">
<!-- Classic -->
<div v-if="showMenuTab" class="menu__tab">
<menu-tab />
</div>
<div
id="sidebar__wrap"
class="sidebar__wrap"
:class="{'sidebar__wrap--collapsed': collapsed}"
:class="{'sidebar__wrap--collapsed': collapsed, 'sidebar__wrap--tab': showMenuTab}"
>
<logo
v-if="showLogo && layout === 'Classic'"
@ -16,7 +19,9 @@
<div
class="main__wrap"
:class="{
'main__wrap--collapsed': collapsed
'main__wrap--collapsed': collapsed,
'main__wrap--tab': showMenuTab,
'main__wrap--tab--collapsed': showMenuTab && collapsed
}"
>
<el-scrollbar
@ -31,7 +36,10 @@
class="header__wrap"
:class="{
'header__wrap--fixed': fixedHeader,
'header__wrap--collapsed': fixedHeader && collapsed
'header__wrap--tab--fixed': fixedHeader && showMenuTab,
'header__wrap--collapsed': fixedHeader && collapsed,
'header__wrap--tab': showMenuTab,
'header__wrap--tab--collapsed': showMenuTab && collapsed
}"
>
<div
@ -75,17 +83,18 @@
import { defineComponent, computed } from 'vue'
import { appStore } from '_@/store/modules/app'
import AppMain from '../components/AppMain.vue'
import TagsView from '_c/TagsView/index.vue'
import Logo from '_c/Logo/index.vue'
import Sider from '_c/Sider/index.vue'
import Hamburger from '_c/Hamburger/index.vue'
import Breadcrumb from '_c/Breadcrumb/index.vue'
import Screenfull from '_c/Screenfull/index.vue'
import UserInfo from '_c/UserInfo/index.vue'
import AppMain from '../components/AppMain/index.vue'
import TagsView from '../components/TagsView/index.vue'
import Logo from '../components/Logo/index.vue'
import Sider from '../components/Sider/index.vue'
import Hamburger from '../components/Hamburger/index.vue'
import Breadcrumb from '../components/Breadcrumb/index.vue'
import Screenfull from '../components/Screenfull/index.vue'
import UserInfo from '../components/UserInfo/index.vue'
import MenuTab from '../components/MenuTab/index.vue'
import Setting from '_c/Setting/index.vue'
import Backtop from '_c/Backtop/index.vue'
import Setting from '../components/Setting/index.vue'
import Backtop from '../components/Backtop/index.vue'
export default defineComponent({
name: 'Classic',
components: {
@ -98,7 +107,8 @@ export default defineComponent({
TagsView,
Logo,
Setting,
Backtop
Backtop,
MenuTab
},
setup() {
const layout = computed(() => appStore.layout)
@ -114,6 +124,7 @@ export default defineComponent({
// const fixedTags = computed(() => appStore.fixedTags)
const fixedHeader = computed(() => appStore.fixedHeader)
const showBackTop = computed(() => appStore.showBackTop)
const showMenuTab = computed(() => appStore.showMenuTab)
const classObj = computed(() => {
const obj = {}
@ -140,7 +151,8 @@ export default defineComponent({
// fixedNavbar,
// fixedTags,
setCollapsed,
showBackTop
showBackTop,
showMenuTab
}
}
})

View File

@ -22,8 +22,8 @@
:collapsed="collapsed"
/>
</div>
<div v-if="layout === 'Top'" class="sidebar__item--Top">
<sider :layout="layout" mode="horizontal" />
<div v-if="showMenuTab" class="menu__tab--top sidebar__item--Top">
<menu-tab />
</div>
<div>
<div v-if="showScreenfull || showUserInfo" class="navbar__wrap--right">
@ -96,17 +96,18 @@
import { defineComponent, computed } from 'vue'
import { appStore } from '_@/store/modules/app'
import AppMain from '../components/AppMain.vue'
import TagsView from '_c/TagsView/index.vue'
import Logo from '_c/Logo/index.vue'
import Sider from '_c/Sider/index.vue'
import Hamburger from '_c/Hamburger/index.vue'
import Breadcrumb from '_c/Breadcrumb/index.vue'
import Screenfull from '_c/Screenfull/index.vue'
import UserInfo from '_c/UserInfo/index.vue'
import AppMain from '../components/AppMain/index.vue'
import TagsView from '../components/TagsView/index.vue'
import Logo from '../components/Logo/index.vue'
import Sider from '../components/Sider/index.vue'
import Hamburger from '../components/Hamburger/index.vue'
import Breadcrumb from '../components/Breadcrumb/index.vue'
import Screenfull from '../components/Screenfull/index.vue'
import UserInfo from '../components/UserInfo/index.vue'
import MenuTab from '../components/MenuTab/index.vue'
import Setting from '_c/Setting/index.vue'
import Backtop from '_c/Backtop/index.vue'
import Setting from '../components/Setting/index.vue'
import Backtop from '../components/Backtop/index.vue'
export default defineComponent({
name: 'LeftTop',
components: {
@ -119,7 +120,8 @@ export default defineComponent({
TagsView,
Logo,
Setting,
Backtop
Backtop,
MenuTab
},
setup() {
const layout = computed(() => appStore.layout)
@ -135,6 +137,7 @@ export default defineComponent({
// const fixedTags = computed(() => appStore.fixedTags)
const fixedHeader = computed(() => appStore.fixedHeader)
const showBackTop = computed(() => appStore.showBackTop)
const showMenuTab = computed(() => appStore.showMenuTab)
const classObj = computed(() => {
const obj = {}
@ -161,7 +164,8 @@ export default defineComponent({
// fixedNavbar,
// fixedTags,
setCollapsed,
showBackTop
showBackTop,
showMenuTab
}
}
})

View File

@ -81,17 +81,17 @@
import { defineComponent, computed } from 'vue'
import { appStore } from '_@/store/modules/app'
import AppMain from '../components/AppMain.vue'
import TagsView from '_c/TagsView/index.vue'
import Logo from '_c/Logo/index.vue'
import Sider from '_c/Sider/index.vue'
// import Hamburger from '_c/Hamburger/index.vue'
// import Breadcrumb from '_c/Breadcrumb/index.vue'
import Screenfull from '_c/Screenfull/index.vue'
import UserInfo from '_c/UserInfo/index.vue'
import AppMain from '../components/AppMain/index.vue'
import TagsView from '../components/TagsView/index.vue'
import Logo from '../components/Logo/index.vue'
import Sider from '../components/Sider/index.vue'
// import Hamburger from '../components/Hamburger/index.vue'
// import Breadcrumb from '../components/Breadcrumb/index.vue'
import Screenfull from '../components/Screenfull/index.vue'
import UserInfo from '../components/UserInfo/index.vue'
import Setting from '_c/Setting/index.vue'
import Backtop from '_c/Backtop/index.vue'
import Setting from '../components/Setting/index.vue'
import Backtop from '../components/Backtop/index.vue'
export default defineComponent({
name: 'Top',
components: {

View File

@ -2,6 +2,74 @@
position: relative;
height: 100%;
width: 100%;
.menu__tab {
width: @menuTabWidth;
height: 100%;
background: @menuTabBg;
@{deep}(.is-left::after),
@{deep}(.el-tabs__active-bar) {
display: none;
}
@{deep}(.el-tabs) {
height: 100%;
.el-tabs__header {
height: 100%;
margin-right: 0;
width: @menuTabWidth;
.el-tabs__nav-wrap {
height: 100%;
.el-tabs__item {
padding: 0;
line-height: 0;
height: @menuTabItemHeight;
text-align: center;
color: @menuTabText;
transition: all .3s cubic-bezier(.645,.045,.355,1);
}
.el-tabs__item:hover {
color: @menuTabActiveText;
background: @menuTabActiveBg;
}
.is-active {
color: @menuTabActiveText;
background: @menuTabActiveBg;
}
}
}
}
&--top {
width: auto;
@{deep}(.el-tabs),
@{deep}(.is-top),
@{deep}(.el-tabs__nav-scroll) {
height: 100%;
}
@{deep}(.is-top::after),
@{deep}(.el-tabs__active-bar) {
display: none;
}
@{deep}(.is-top) {
margin-bottom: 0;
}
@{deep}(.el-tabs__item) {
padding: 0;
width: @menuTopTabWidth;
text-align: center;
height: 100%;
padding-top: 10px;
color: @menuTabText;
transition: all .3s cubic-bezier(.645,.045,.355,1);
}
@{deep}(.el-tabs__item:hover) {
color: @menuTopTabActiveText;
background: @menuTopTabActiveBg;
}
@{deep}(.is-active) {
color: @menuTopTabActiveText;
background: @menuTopTabActiveBg;
}
}
}
.sidebar__wrap {
position: fixed;
width: @menuWidth;
@ -10,6 +78,9 @@
height: 100%;
transition: width 0.2s;
}
.sidebar__wrap--tab {
left: @menuTabWidth;
}
.sidebar__wrap--collapsed {
width: @menuMinWidth;
@{deep}(.anticon-item) {
@ -76,10 +147,18 @@
}
// content样式
}
.main__wrap--tab {
width: calc(~"100% - @{menuWidth} - @{menuTabWidth}");
left: @menuWidth + @menuTabWidth;
}
.main__wrap--collapsed {
width: calc(~"100% - @{menuMinWidth}");
left: @menuMinWidth;
}
.main__wrap--tab--collapsed {
width: calc(~"100% - @{menuMinWidth} - @{menuTabWidth}");
left: @menuMinWidth + @menuTabWidth;
}
}
// LeftTop模式
@ -105,10 +184,20 @@
left: @menuWidth !important;
z-index: 200;
}
.header__wrap--tab--fixed {
width: calc(~"100% - @{menuWidth} - @{menuTabWidth}") !important;
}
.header__wrap--tab {
left: @menuWidth + @menuTabWidth !important;
}
.header__wrap--collapsed {
width: calc(~"100% - @{menuMinWidth}") !important;
left: @menuMinWidth !important;
}
.header__wrap--tab--collapsed {
width: calc(~"100% - @{menuMinWidth} - @{menuTabWidth}") !important;
left: @menuMinWidth + @menuTabWidth !important;
}
}
.app__wrap--Classic {
.header__wrap--fixed {

View File

@ -17,6 +17,7 @@ export interface AppState {
userInfo: String
greyMode: Boolean
showBackTop: Boolean
showMenuTab: Boolean
}
@Module({ dynamic: true, namespaced: true, store, name: 'app' })
@ -36,6 +37,7 @@ class App extends VuexModule implements AppState {
public userInfo = 'userInfo' // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突
public greyMode = false // 是否开始灰色模式,用于特殊悼念日
public showBackTop = true // 是否显示回到顶部
public showMenuTab = false // 是否固定一级菜单
@Mutation
private SET_COLLAPSED(collapsed: boolean): void {
@ -93,6 +95,10 @@ class App extends VuexModule implements AppState {
private SET_SHOWBACKTOP(showBackTop: boolean): void {
this.showBackTop = showBackTop
}
@Mutation
private SET_SHOWMENUTAB(showMenuTab: boolean): void {
this.showMenuTab = showMenuTab
}
@Action
public SetCollapsed(collapsed: boolean): void {
@ -150,6 +156,10 @@ class App extends VuexModule implements AppState {
public SetShowBackTop(showBackTop: boolean): void {
this.SET_SHOWBACKTOP(showBackTop)
}
@Action
public SetShowMenuTab(showMenuTab: boolean): void {
this.SET_SHOWMENUTAB(showMenuTab)
}
}
export const appStore = getModule<App>(App)

View File

@ -17,6 +17,8 @@ export interface PermissionState {
routers: AppRouteRecordRaw[]
addRouters: AppRouteRecordRaw[]
isAddRouters: boolean
activeTab: string
menuTabRouters: AppRouteRecordRaw[]
}
@Module({ dynamic: true, namespaced: true, store, name: 'permission' })
@ -24,6 +26,8 @@ class Permission extends VuexModule implements PermissionState {
public routers = [] as any[]
public addRouters = [] as any[]
public isAddRouters = false
public menuTabRouters = [] as any[]
public activeTab = ''
@Mutation
private SET_ROUTERS(routers: AppRouteRecordRaw[]): void {
@ -44,6 +48,14 @@ class Permission extends VuexModule implements PermissionState {
private SET_ISADDROUTERS(state: boolean): void {
this.isAddRouters = state
}
@Mutation
private SET_MENUTABROUTERS(routers: AppRouteRecordRaw[]): void {
this.menuTabRouters = routers
}
@Mutation
private SET_ACTIVETAB(activeTab: string): void {
this.activeTab = activeTab
}
@Action
public GenerateRoutes(): Promise<unknown> {
@ -66,6 +78,14 @@ class Permission extends VuexModule implements PermissionState {
public SetIsAddRouters(state: boolean): void {
this.SET_ISADDROUTERS(state)
}
@Action
public SetMenuTabRouters(routers: AppRouteRecordRaw[]): void {
this.SET_MENUTABROUTERS(routers)
}
@Action
public SetAcitveTab(activeTab: string): void {
this.SET_ACTIVETAB(activeTab)
}
}
// 路由过滤,主要用于权限控制

View File

@ -25,6 +25,21 @@
@topSMenuHover: #2d8cf0;
@topSMenuActiveText: #2d8cf0;
// meunTab
@menuTabWidth: 90px;
@menuTabItemHeight: 70px;
@menuTabBg: #fff;
@menuTabText: black;
@menuTabActiveBg: #2d8cf0;
@menuTabActiveText: #fff;
// menuTopTab
@menuTopTabWidth: 120px;
@menuTopTabBg: #fff;
@menuTopTabText: black;
@menuTopTabActiveBg: #2d8cf0;
@menuTopTabActiveText: #fff;
// navbar
@navbarHeight: 40px;

View File

@ -7,6 +7,7 @@ export interface IScssVariables {
subMenuHover: string
menuWidth: string
menuMinWidth: string
}
export const variables: IScssVariables