feat: 🎸 重构sider组件中

This commit is contained in:
chenkl 2020-12-15 17:22:34 +08:00
parent 26d4c7c568
commit 51313d7116
17 changed files with 613 additions and 139 deletions

View File

@ -1,27 +1,23 @@
<template> <template>
<div> <div>
<menu-unfold-outlined <svg
v-if="collapsed" :class="{'is-active': !collapsed}"
class="trigger" class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
@click="toggleCollapsed(!collapsed)" @click="toggleCollapsed(!collapsed)"
/> >
<menu-fold-outlined <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
v-else </svg>
class="trigger"
@click="toggleCollapsed(!collapsed)"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue' import { defineComponent, PropType } from 'vue'
// import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue'
export default defineComponent({ export default defineComponent({
name: 'Hamburger', name: 'Hamburger',
// components: {
// MenuUnfoldOutlined,
// MenuFoldOutlined
// },
props: { props: {
collapsed: { collapsed: {
type: Boolean as PropType<boolean>, type: Boolean as PropType<boolean>,
@ -43,13 +39,13 @@ export default defineComponent({
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.trigger { .hamburger {
display: inline-block; display: inline-block;
transition: color 0.3s; cursor: pointer;
height: @navbarHeight; width: 20px;
line-height: @navbarHeight; height: 20px;
} }
.trigger:hover { .hamburger.is-active {
color: #1890ff; transform: rotate(180deg);
} }
</style> </style>

View File

@ -50,7 +50,7 @@ export default defineComponent({
padding-left: 18px; padding-left: 18px;
cursor: pointer; cursor: pointer;
height: @topSilderHeight; height: @topSilderHeight;
max-width: 200px; width: 100%;
img { img {
width: 37px; width: 37px;
height: 37px; height: 37px;

View File

@ -0,0 +1,34 @@
<template>
<div>
<i v-if="icon.includes('el-icon')" :class="[icon, 'sub-el-icon', 'anticon']" />
<svg-icon v-else :icon-class="icon" class="anticon" />
<slot name="title">
<span class="anticon-item">{{ title }}</span>
</slot>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'Item',
props: {
icon: {
type: String as PropType<string>,
default: ''
},
title: {
type: String as PropType<string>,
default: ''
}
}
})
</script>
<style lang="less" scoped>
.anticon-item {
opacity: 1;
transition: opacity .3s cubic-bezier(.645,.045,.355,1),width .3s cubic-bezier(.645,.045,.355,1);
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener">
<slot />
</a>
<router-link v-else :to="to">
<slot />
</router-link>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { isExternal } from '@/utils/validate'
export default defineComponent({
props: {
to: {
type: String as PropType<string>,
required: true
}
},
setup() {
return {
isExternal
}
}
})
</script>

View File

@ -1,11 +1,94 @@
<template> <template>
<div /> <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)">
<item v-if="onlyOneChild.meta" :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" />
<template #title>
{{ onlyOneChild.meta.title }}
</template>
</el-menu-item>
</template>
<el-submenu v-else :index="resolvePath(item.path)">
<template #title>
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
</template>
<sider-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
/>
</el-submenu>
</template>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent, PropType, ref } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item.vue'
import AppLink from './Link.vue'
export default defineComponent({ export default defineComponent({
name: 'SilderItem' name: 'SiderItem',
components: { Item, AppLink },
props: {
// route object
item: {
type: Object as PropType<object>,
required: true
},
isNest: {
type: Boolean as PropType<boolean>,
default: false
},
basePath: {
type: String as PropType<string>,
default: ''
}
},
setup(props) {
const onlyOneChild = ref<any>(null)
function hasOneShowingChild(children: RouteRecordRaw[] = [], parent: RouteRecordRaw): boolean {
const showingChildren: RouteRecordRaw[] = children.filter((item: RouteRecordRaw) => {
if (item.meta && item.meta.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
onlyOneChild.value = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
return true
}
return false
}
function resolvePath(routePath: string): string {
if (isExternal(routePath)) {
return routePath
}
return path.resolve(props.basePath, routePath)
}
return {
onlyOneChild,
hasOneShowingChild,
resolvePath
}
}
}) })
</script> </script>

View File

@ -1,21 +1,27 @@
<template> <template>
<div :class="{'has-logo':show_logo}"> <!-- <div :class="{'has-logo': showLogo}" class="sidebar-container"> -->
<el-scrollbar wrap-class="scrollbar-wrapper"> <!-- <el-scrollbar class="menu-wrap"> -->
<el-menu <el-menu
:default-active="activeMenu" :default-active="activeMenu"
:collapse="collapsed" :collapse="collapsed"
:unique-opened="false" :unique-opened="false"
mode="vertical" :background-color="variables.menuBg"
> :text-color="variables.menuText"
<sider-item :active-text-color="variables.menuActiveText"
v-for="route in routers" mode="vertical"
:key="route.path" class="sidebar-container"
:item="route" :class="{'sidebar__wrap--collapsed': collapsed}"
:base-path="route.path" @select="selectMenu"
/> >
</el-menu> <sider-item
</el-scrollbar> v-for="route in routers"
</div> :key="route.path"
:item="route"
:base-path="route.path"
/>
</el-menu>
<!-- </el-scrollbar> -->
<!-- </div> -->
</template> </template>
<script lang="ts"> <script lang="ts">
@ -25,9 +31,8 @@ import { permissionStore } from '_p/index/store/modules/permission'
import { appStore } from '_p/index/store/modules/app' import { appStore } from '_p/index/store/modules/app'
import type { RouteRecordRaw, RouteLocationNormalizedLoaded } from 'vue-router' import type { RouteRecordRaw, RouteLocationNormalizedLoaded } from 'vue-router'
import SiderItem from './SiderItem.vue' import SiderItem from './SiderItem.vue'
// import variables from '@/styles/variables.less' import variables from '@/styles/variables.less'
import config from '_p/index/config' import { isExternal } from '@/utils/validate'
const { show_logo } = config
export default defineComponent({ export default defineComponent({
components: { SiderItem }, components: { SiderItem },
@ -45,12 +50,23 @@ export default defineComponent({
return path return path
}) })
const collapsed = computed(() => appStore.collapsed) const collapsed = computed(() => appStore.collapsed)
const showLogo = computed(() => appStore.showLogo)
function selectMenu(path: string) {
if (isExternal(path)) {
window.open(path)
} else {
push(path)
}
}
return { return {
routers, routers,
activeMenu, activeMenu,
collapsed, collapsed,
show_logo showLogo,
variables,
selectMenu
} }
} }
}) })
@ -59,12 +75,24 @@ export default defineComponent({
<style lang="less" scoped> <style lang="less" scoped>
.sidebar-container { .sidebar-container {
height: 100%; height: 100%;
background: @menuBg;
@{deep}(.svg-icon) {
margin-right: 16px;
}
} }
.has-logo { .has-logo {
height: calc(~"100% - @{topSilderHeight}"); height: calc(~"100% - @{topSilderHeight}");
} }
.menu-wrap { // .menu-wrap {
height: 100%; // height: 100%;
overflow: hidden; // overflow: hidden;
} // @{deep}(.el-scrollbar__wrap) {
// overflow-x: hidden;
// overflow-y: auto;
// }
// @{deep}(.el-menu) {
// border-right: none;
// width: 100%;
// }
// }
</style> </style>

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<!-- <i v-if="icon.includes('el-icon')" :class="[icon, 'sub-el-icon', 'anticon']" /> --> <i v-if="icon.includes('el-icon')" :class="[icon, 'sub-el-icon', 'anticon']" />
<svg-icon :icon-class="icon" class="anticon" /> <svg-icon v-else :icon-class="icon" class="anticon" />
<slot name="title"> <slot name="title">
<span class="anticon-item">{{ title }}</span> <span class="anticon-item">{{ title }}</span>
</slot> </slot>

View File

@ -1,25 +1,29 @@
<template> <template>
<div class="app__wrap"> <el-container class="app__wrap">
<div class="sidebar__wrap"> <div class="sidebar__wrap" :class="{'sidebar__wrap--collapsed': collapsed}">
<logo <logo
v-if="show_logo" v-if="showLogo"
:collapsed="collapsed" :collapsed="collapsed"
/> />
<sider /></div> <sider />
</div>
<el-main><hamburger :collapsed="collapsed" class="hamburger-container" @toggleClick="setCollapsed" /></el-main>
</el-container>
<!-- <div class="app__wrap">
<div class="sidebar__wrap" :class="{'sidebar__wrap--collapsed': collapsed}">
<logo
v-if="showLogo"
:collapsed="collapsed"
/>
<sider />
</div>
<div class="main__wrap"> <div class="main__wrap">
<div class="navbar__wrap" /> <div class="navbar__wrap">
</div>
<div class="tags__wrap" /> <div class="tags__wrap" />
<div class="main__wrap" /> <div class="main__wrap" />
</div> </div>
<!-- <sidebar class="sidebar-wrap" /> </div> -->
<div :class="{hasTagsView: has_tags}" class="main-wrap">
<div>
<navbar />
<tags-view v-if="has_tags" />
</div>
<app-main />
</div> -->
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -31,32 +35,63 @@ import TagsView from '../components/TagsView.vue'
import Logo from '_c/Logo/index.vue' import Logo from '_c/Logo/index.vue'
import Scrollbar from '_c/Scrollbar/index.vue' import Scrollbar from '_c/Scrollbar/index.vue'
import Sider from '_c/Sider/index.vue' import Sider from '_c/Sider/index.vue'
import Navbar from '../components/Navbar.vue' import Hamburger from '_c/Hamburger/index.vue'
import config from '_p/index/config'
const { show_logo, has_tags } = config
export default defineComponent({ export default defineComponent({
name: 'Layout', name: 'Layout',
components: { components: {
Sider, Sider,
Navbar, Hamburger,
// Navbar,
AppMain, AppMain,
TagsView, TagsView,
Logo, Logo,
Scrollbar Scrollbar
}, },
setup() { setup() {
const collapsed = computed(() => appStore.collapsed)
const showLogo = computed(() => appStore.showLogo)
const showTags = computed(() => appStore.showTags)
function setCollapsed(collapsed: boolean): void {
appStore.SetCollapsed(collapsed)
}
return { return {
show_logo, has_tags collapsed,
showLogo,
showTags,
setCollapsed
} }
} }
}) })
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.app-wrap { .app__wrap {
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
// .sidebar__wrap {
// position: absolute;
// width: @menuWidth;
// top: 0;
// left: 0;
// height: 100%;
// transition: all 0.2s;
// }
// .sidebar__wrap--collapsed {
// width: @menuMinWidth;
// }
// .main__wrap {
// position: absolute;
// width: calc(~"100% - @{menuWidth}");
// height: 100%;
// top: 0;
// left: @menuWidth;
// .navbar__wrap {
// height: @navbarHeight;
// }
// }
} }
</style> </style>

View File

@ -357,65 +357,65 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
// } // }
// ] // ]
// }, // },
// { {
// path: '/level', path: '/level',
// component: Layout, component: Layout,
// redirect: '/level/menu1/menu1-1/menu1-1-1', redirect: '/level/menu1/menu1-1/menu1-1-1',
// name: 'Level', name: 'Level',
// meta: { meta: {
// title: '多级菜单缓存', title: '多级菜单缓存',
// icon: 'nested' icon: 'nested'
// }, },
// children: [ children: [
// { {
// path: 'menu1', path: 'menu1',
// name: 'Menu1Demo', name: 'Menu1Demo',
// component: getParentLayout('Menu1Demo'), component: getParentLayout('Menu1Demo'),
// redirect: '/level/menu1/menu1-1/menu1-1-1', redirect: '/level/menu1/menu1-1/menu1-1-1',
// meta: { meta: {
// title: 'Menu1' title: 'Menu1'
// }, },
// children: [ children: [
// { {
// path: 'menu1-1', path: 'menu1-1',
// name: 'Menu11Demo', name: 'Menu11Demo',
// component: getParentLayout('Menu11Demo'), component: getParentLayout('Menu11Demo'),
// redirect: '/level/menu1/menu1-1/menu1-1-1', redirect: '/level/menu1/menu1-1/menu1-1-1',
// meta: { meta: {
// title: 'Menu1-1', title: 'Menu1-1',
// alwaysShow: true alwaysShow: true
// }, },
// children: [ children: [
// { {
// path: 'menu1-1-1', path: 'menu1-1-1',
// name: 'Menu111Demo', name: 'Menu111Demo',
// component: () => import('_p/index/views/level/Menu111.vue'), component: () => import('_p/index/views/level/Menu111.vue'),
// meta: { meta: {
// title: 'Menu1-1-1' title: 'Menu1-1-1'
// } }
// } }
// ] ]
// }, },
// { {
// path: 'menu1-2', path: 'menu1-2',
// name: 'Menu12Demo', name: 'Menu12Demo',
// component: () => import('_p/index/views/level/Menu12.vue'), component: () => import('_p/index/views/level/Menu12.vue'),
// meta: { meta: {
// title: 'Menu1-2' title: 'Menu1-2'
// } }
// } }
// ] ]
// }, },
// { {
// path: 'menu2', path: 'menu2',
// name: 'Menu2Demo', name: 'Menu2Demo',
// component: () => import('_p/index/views/level/Menu2.vue'), component: () => import('_p/index/views/level/Menu2.vue'),
// meta: { meta: {
// title: 'Menu2' title: 'Menu2'
// } }
// } }
// ] ]
// }, },
// { // {
// path: '/example-demo', // path: '/example-demo',
// component: Layout, // component: Layout,

View File

@ -14,7 +14,7 @@ export interface AppState {
@Module({ dynamic: true, namespaced: true, store, name: 'app' }) @Module({ dynamic: true, namespaced: true, store, name: 'app' })
class App extends VuexModule implements AppState { class App extends VuexModule implements AppState {
public collapsed = false // 菜单栏是否栏缩收 public collapsed = false // 菜单栏是否栏缩收
public showLogo = true // 是否显示logo public showLogo = false // 是否显示logo
public showTags = true // 是否显示标签栏 public showTags = true // 是否显示标签栏
public showNavbar = true // 是否显示navbar public showNavbar = true // 是否显示navbar
public fixedTags = true // 是否固定标签栏 public fixedTags = true // 是否固定标签栏

View File

@ -1,7 +1,7 @@
<template> <template>
<div style="padding: 20px; background: #fff;display: flex;align-items: center;"> <div style="padding: 20px; background: #fff;display: flex;align-items: center;">
<div style="min-width: 200px;">多层级缓存-页面1-1-1</div> <div style="min-width: 200px;">多层级缓存-页面1-1-1</div>
<a-input /> <el-input />
</div> </div>
</template> </template>

View File

@ -1,7 +1,7 @@
<template> <template>
<div style="padding: 20px; background: #fff;display: flex;align-items: center;"> <div style="padding: 20px; background: #fff;display: flex;align-items: center;">
<div style="min-width: 200px;">多层级缓存-页面1-2</div> <div style="min-width: 200px;">多层级缓存-页面1-2</div>
<a-input /> <el-input />
</div> </div>
</template> </template>

View File

@ -1,7 +1,7 @@
<template> <template>
<div style="padding: 20px; background: #fff;display: flex;align-items: center;"> <div style="padding: 20px; background: #fff;display: flex;align-items: center;">
<div style="min-width: 200px;">多层级缓存-页面2</div> <div style="min-width: 200px;">多层级缓存-页面2</div>
<a-input /> <el-input />
</div> </div>
</template> </template>

View File

@ -1,4 +1,5 @@
@import '~element-plus/lib/theme-chalk/index.css'; @import '~element-plus/lib/theme-chalk/index.css';
// @import './sidebar.less';
@import './transition.less'; @import './transition.less';
@import './silder.less'; @import './silder.less';
@import './glob.less'; @import './glob.less';

234
src/styles/sidebar.less Normal file
View File

@ -0,0 +1,234 @@
#app {
// 主体区域 Main container
.main-container {
min-height: 100%;
transition: margin-left .28s;
margin-left: @menuWidth;
position: relative;
}
// 侧边栏 Sidebar container
.sidebar-container {
transition: width 0.28s;
width: @menuWidth !important;
height: 100%;
position: fixed;
font-size: 0px;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
overflow: hidden;
//reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
.el-scrollbar__view {
height: 100%;
}
}
.el-scrollbar__bar.is-vertical {
right: 0px;
}
.el-scrollbar {
height: 100%;
}
&.has-logo {
.el-scrollbar {
height: calc(100% - 70px);
}
}
.is-horizontal {
display: none;
}
a {
display: inline-block;
width: 100%;
overflow: hidden;
}
.svg-icon {
margin-right: 16px;
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
}
// menu hover
.submenu-title-noDropdown,
.el-submenu__title {
color: hsla(0,0%,100%,.7) !important;
&:hover {
// background-color: @menuHover !important;
color: @subMenuActiveText !important;
}
}
.is-active>.el-submenu__title {
color: @subMenuActiveText !important;
}
.is-active {
color: @subMenuActiveText !important;
background-color: @menuActiveBg !important;
&:hover {
color: @subMenuActiveText !important;
background-color: @menuActiveBg !important;
}
& .el-menu-item {
background-color: @menuActiveBg !important;
&:hover {
color: @subMenuActiveText !important;
}
}
}
& .nest-menu .el-submenu>.el-submenu__title,
& .el-submenu .el-menu-item {
min-width: @menuWidth !important;
background-color: @subMenuBg !important;
&:hover {
color: @subMenuActiveText !important;
background-color: @subMenuHover !important;
}
}
& .nest-menu {
& .is-active {
background-color: @menuActiveBg !important;
&:hover {
color: @subMenuActiveText !important;
background-color: @menuActiveBg !important;
}
}
}
}
.hideSidebar {
.sidebar-container {
width: 36px !important;
}
.main-container {
margin-left: 36px;
}
.submenu-title-noDropdown {
padding-left: 10px !important;
position: relative;
.el-tooltip {
padding: 0 10px !important;
}
}
.el-submenu {
overflow: hidden;
&>.el-submenu__title {
padding-left: 10px !important;
.el-submenu__icon-arrow {
display: none;
}
}
}
.el-menu--collapse {
.el-submenu {
&>.el-submenu__title {
&>span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
}
.el-menu--collapse .el-menu .el-submenu {
min-width: @menuWidth !important;
}
// 适配移动端, Mobile responsive
.mobile {
.main-container {
margin-left: 0px;
}
.sidebar-container {
transition: transform .28s;
width: @menuWidth !important;
}
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transition-duration: 0.3s;
transform: translate3d(-@menuWidth, 0, 0);
}
}
}
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
}
// when menu collapsed
.el-menu--vertical {
&>.el-menu {
.svg-icon {
margin-right: 16px;
}
}
.nest-menu .el-submenu>.el-submenu__title,
.el-menu-item {
&:hover {
// you can use @subMenuHover
// background-color: @menuHover !important;
}
}
// the scroll bar appears when the subMenu is too long
>.el-menu--popup {
max-height: 100vh;
overflow-y: auto;
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
}

View File

@ -1,15 +1,25 @@
// Silder // Silder
@menuText: #bfcbd9;
@menuActiveText: #409EFF;
@menuActiveBg: #2d8cf0;
@menuBg: #001529;
@subMenuBg: #1f2d3d;
@subMenuHover: #1f2d3d;
@subMenuActiveText: #fff;
@menuWidth: 200px;
@menuMinWidth: 64px;
@menuLightActiveText: #1890ff; @menuLightActiveText: #1890ff;
@menuLightActiveBg: #e6f7ff; @menuLightActiveBg: #e6f7ff;
@menuDarkActiveText: #fff; @menuDarkActiveText: #fff;
@menuDarkActiveBg: #1890ff; @menuDarkActiveBg: #1890ff;
@menuBg: #001529;
@menuLightBg: #fff; @menuLightBg: #fff;
@menuWidth: 200px;
// topSilder // topSilder
@topSilderHeight: 50px; @topSilderHeight: 50px;
@ -27,3 +37,15 @@
// deep // deep
@deep: ~'::v-deep'; @deep: ~'::v-deep';
// the :export directive is the magic sauce for webpack
:export {
menuText: @menuText;
menuActiveText: @menuActiveText;
menuActiveBg: @menuActiveBg;
menuBg: @menuBg;
subMenuBg: @subMenuBg;
subMenuHover: @subMenuHover;
menuWidth: @menuWidth;
menuMinWidth: @menuMinWidth;
}

14
src/styles/variables.less.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
export interface IScssVariables {
menuText: string
menuActiveText: string
menuActiveBg: string
menuBg: string
subMenuBg: string
subMenuHover: string
menuWidth: string
menuMinWidth: string
}
export const variables: IScssVariables
export default variables