上一章主要完善后台管理系统基础布局和项目结构 Vite2 + vue3 + TS + ElementPlus 从零搭建后台管理系统(三)
这一章开始完善基础布局组件
1. 完善 aside 菜单组件
新建 layout/component/logo/index.vue:
- logo 组件
<template>
<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
<img src="@/assets/logo.png" class="layout-logo-medium-img" />
<span>{{ getThemeConfig.globalTitle }}</span>
</div>
<div class="layout-logo-size" v-else @click="onThemeConfigChange">
<img src="@/assets/logo.png" class="layout-logo-size-img" />
</div>
</template>
<script lang="ts">
import { computed } from 'vue'
import { useStore } from 'store/index'
export default {
name: 'layoutLogo',
setup() {
const store = useStore()
// 获取布局配置信息
const getThemeConfig = computed(() => {
return store.state.themeConfig
})
return {
getThemeConfig
}
}
}
</script>
<style scoped lang="scss">
.layout-logo {
width: 220px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: rgb(0 21 41 / 2%) 0px 1px 4px;
color: var(--color-primary);
font-size: 16px;
cursor: pointer;
animation: logoAnimation 0.3s ease-in-out;
&:hover {
span {
color: var(--color-primary-light-2);
}
}
&-medium-img {
width: 20px;
margin-right: 5px;
}
}
.layout-logo-size {
width: 100%;
height: 50px;
display: flex;
cursor: pointer;
animation: logoAnimation 0.3s ease-in-out;
&-img {
width: 20px;
margin: auto;
}
&:hover {
img {
animation: logoAnimation 0.3s ease-in-out;
}
}
}
</style>
新建 layout/component/navMenu/subItem.vue:
- subItem 组件
<template>
<template v-for="val in chils">
<el-submenu
:index="val.path"
:key="val.path"
v-if="val.children && val.children.length > 0"
>
<template #title>
<i :class="val.meta.icon"></i>
<span>{{ val.meta.title }}</span>
</template>
<sub-item :chil="val.children" />
</el-submenu>
<el-menu-item :index="val.path" :key="val.path" v-else>
<template
v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)"
>
<i :class="val.meta.icon ? val.meta.icon : ''"></i>
<span>{{ val.meta.title }}</span>
</template>
<template v-else>
<a :href="val.meta.isLink" target="_blank">
<i :class="val.meta.icon ? val.meta.icon : ''"></i>
{{ val.meta.title }}
</a>
</template>
</el-menu-item>
</template>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
export default defineComponent({
name: 'navMenuSubItem',
props: {
chil: {
type: Array,
default: () => []
}
},
setup(props) {
// 获取父级菜单数据
const chils = computed(() => {
return props.chil
})
return {
chils
}
}
})
</script>
新建 layout/component/navMenu/subBar.vue:
- subBar 组件
<template>
<el-menu
router
background-color="transparent"
:collapse="setIsCollapse"
:default-active="defaultActive"
unique-opened="false"
>
<template v-for="val in menuLists">
<el-submenu
:index="val.path"
v-if="val.children && val.children.length > 0"
:key="val.path"
>
<template #title>
<i :class="val.meta.icon ? val.meta.icon : ''"></i>
<span>{{ val.meta.title }}</span>
</template>
<SubItem :chil="val.children" />
</el-submenu>
<el-menu-item :index="val.path" :key="val.path" v-else>
<i :class="val.meta.icon ? val.meta.icon : ''"></i>
<template
#title
v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)"
>
<span>{{ val.meta.title }}</span>
</template>
<template #title v-else>
<a :href="val.meta.isLink" target="_blank">
{{ val.meta.title }}
</a>
</template>
</el-menu-item>
</template>
</el-menu>
</template>
<script lang="ts">
import {
ref,
toRefs,
reactive,
computed,
defineComponent,
getCurrentInstance
} from 'vue'
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
import { useStore } from 'store/index'
import SubItem from './subItem.vue'
export default defineComponent({
name: 'navMenuSideBar',
components: { SubItem },
props: {
menuList: {
type: Array,
default: () => []
}
},
setup(props) {
const store = useStore()
const route = useRoute()
const state = reactive({
defaultActive: route.path
})
// 获取父级菜单数据
const menuLists = computed(() => {
return props.menuList
})
// 获取布局配置信息
const getThemeConfig = computed(() => {
return store.state.themeConfig
})
// 设置菜单的收起/展开
const setIsCollapse = computed(() => {
return document.body.clientWidth < 1000
? false
: getThemeConfig.value.isCollapse
})
// 路由更新时
onBeforeRouteUpdate((to) => {
const clientWidth = document.body.clientWidth
if (clientWidth < 1000) getThemeConfig.value.isCollapse = false
})
return {
getThemeConfig,
menuLists,
setIsCollapse,
...toRefs(state)
}
}
})
</script>
完善 layout/component/aside.vue:
<template>
<el-aside class="layout-aside" :class="setCollapseWidth">
<Logo v-if="setShowLogo" />
<el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef">
<SubBar :menuList="menuList" :class="setCollapseWidth" />
</el-scrollbar>
</el-aside>
</template>
<script lang="ts">
import {
ref,
toRefs,
reactive,
computed,
watch,
getCurrentInstance,
onBeforeMount,
onUnmounted
} from 'vue'
import { useStore } from 'store/index'
import Logo from './logo/index.vue'
import SubBar from './navMenu/subBar.vue'
export default {
name: 'layoutAside',
components: { Logo, SubBar },
setup() {
const store = useStore()
const state: any = reactive({
menuList: [
{
meta: {
auth: ['admin', 'test'],
icon: 'iconfont el-icon-menu',
isAffix: true,
isHide: false,
isIframe: false,
isKeepAlive: true,
title: '首页',
index: '1'
},
name: 'home',
path: '/home'
},
{
meta: {
auth: ['admin', 'test'],
icon: 'iconfont el-icon-s-grid',
isAffix: true,
isHide: false,
isIframe: false,
isKeepAlive: true,
title: '首页2',
index: '2'
},
name: 'home2',
path: '/home2'
}
],
clientWidth: ''
})
const setShowLogo = ref<boolean>(true)
const setCollapseWidth = ref<string>('layout-aside-width-default')
// 页面加载前
onBeforeMount(() => {})
// 页面卸载时
onUnmounted(() => {})
return {
setShowLogo,
setCollapseWidth,
...toRefs(state)
}
}
}
</script>
完善 layout/component/mainView.vue:
<template>
<el-container class="layout-container">
<Aside />
</el-container>
</template>
<script lang="ts">
import { getCurrentInstance, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useStore } from 'store/index'
import Aside from './component/aside.vue'
export default {
name: 'layoutDefaults',
components: { Aside },
setup() {
const { proxy } = getCurrentInstance() as any
const store = useStore()
const route = useRoute()
// 监听路由的变化
watch(
() => route.path,
() => {
proxy.$refs.layoutDefaultsScrollbarRef.wrap.scrollTop = 0
}
)
return {}
}
}
</script>
完善 layout/main/index.vue:
<template>
<el-container class="layout-container">
<Aside />
</el-container>
</template>
<script lang="ts">
import { getCurrentInstance, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useStore } from 'store/index'
import Aside from '../component/aside.vue'
export default {
name: 'layoutDefaults',
components: { Aside },
setup() {
return {}
}
}
</script>
到目前启动服务,就可以看见如下了:
如果细心看过代码很容易就猜测到,接下来要实现侧边菜单收起功能
2. logo 和 侧边菜单的收起以及动画
- store/interface/index.ts 的 ThemeConfigState 中添加相应的类型声明
export interface ThemeConfigState {
globalTitle:string;
layout:string;
menuBar:string;
animation:string;
isCollapse: boolean;
isShowLogo: boolean;
}
- 修改 store/modules/themeConfig.ts 中state:
state: {
/* --------- 界面设置 --------- */
// 网站主标题(菜单导航、浏览器当前网页标题)
globalTitle: 'Vue3-ElementPlus-Vite2',
// 是否开启侧边栏 Logo
isShowLogo: true,
// 是否开启菜单水平折叠效果
isCollapse: true,
// 默认布局,可选 1、默认 defaults 2、经典 classic 3、横向 transverse 4、分栏 columns
layout: 'defaults',
// 默认菜单导航背景颜色,请注意:需要同时修改 `/@/theme/common/var.scss` 对应的值
menuBar: '#545c64',
// 默认主页面切换动画,可选 1、 slide-right 2、 slide-left 3、 opacitys
animation: 'slide-right',
},
- 修改 layout/component/logo/index.vue 的 setup :
// 设置显示/隐藏 logo
const setShowLogo = computed(() => {
let { isShowLogo } = store.state.themeConfig
return isShowLogo
})
const onThemeConfigChange = () => {
store.state.themeConfig.isCollapse = !store.state.themeConfig.isCollapse
}
最后记得 return setShowLogo 和 onThemeConfigChange
- 修改 layout/component/aside.vue:
template:
<template>
<el-aside
class="layout-aside"
:class="setCollapseWidth"
v-if="clientWidth > 900"
>
<Logo v-if="setShowLogo" />
<el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef">
<SubBar :menuList="menuList" :class="setCollapseWidth" />
</el-scrollbar>
</el-aside>
<el-drawer
v-model="getThemeConfig.isCollapse"
:with-header="false"
direction="ltr"
size="220px"
v-else
>
<el-aside class="layout-aside w100 h100">
<Logo v-if="setShowLogo" />
<el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef">
<SubBar :menuList="menuList" />
</el-scrollbar>
</el-aside>
</el-drawer>
</template>
setup:
// 获取布局配置信息
const getThemeConfig = computed(() => {
return store.state.themeConfig
})
// 设置显示/隐藏 logo
const setShowLogo = computed(() => {
let { isShowLogo } = store.state.themeConfig
return isShowLogo
})
// 设置侧边栏宽度
const setCollapseWidth = computed(() => {
let { layout, isCollapse, menuBar } = store.state.themeConfig;
let asideBrColor = menuBar === '#FFFFFF' || menuBar === '#FFF' || menuBar === '#fff' || menuBar === '#ffffff' ? 'layout-el-aside-br-color' : '';
if (layout === 'columns') {
// 分栏布局,菜单收起时宽度给 1px
if (isCollapse) {
return ['layout-aside-width1', asideBrColor];
} else {
return ['layout-aside-width-default', asideBrColor];
}
} else {
// 其它布局给 64px
if (isCollapse) {
return ['layout-aside-width64', asideBrColor];
} else {
return ['layout-aside-width-default', asideBrColor];
}
}
});
// 设置菜单导航是否固定
const initMenuFixed = (clientWidth: number) => {
state.clientWidth = clientWidth
}
// 页面加载前
onBeforeMount(() => {
initMenuFixed(document.body.clientWidth)
})
最后记得 return getThemeConfig 和 setShowLogo
- 修改 layout/component/mainView.vue:
<template>
<div class="h100">
<router-view v-slot="{ Component }">
<transition :name="setTransitionName" mode="out-in">
<keep-alive :include="keepAliveNameList">
<component :is="Component" :key="refreshRouterViewKey" class="w100" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, toRefs, reactive} from 'vue';
import { useStore } from 'store/index';
export default defineComponent({
name: 'layoutAppMain',
setup() {
const store = useStore();
const state: any = reactive({
refreshRouterViewKey: null,
keepAliveNameList: [],
keepAliveNameNewList: [],
});
// 设置主界面切换动画
const setTransitionName = computed(() => {
return store.state.themeConfig.animation;
});
return {
setTransitionName,
...toRefs(state),
};
},
});
</script>
- 修改 layout/component/main/index.vue:
<template>
<el-container class="layout-container">
<Aside />
<el-container class="flex-center layout-backtop">
<el-scrollbar ref="layoutDefaultsScrollbarRef">
<MainView />
</el-scrollbar>
</el-container>
</el-container>
</template>
<script lang="ts">
import { getCurrentInstance, watch } from 'vue'
import { useRoute } from 'vue-router'
import Aside from '../component/aside.vue'
import MainView from '../component/mainView.vue'
export default {
name: 'layoutDefaults',
components: { Aside, MainView },
setup() {
const { proxy } = getCurrentInstance() as any
const route = useRoute()
// 监听路由的变化
watch(
() => route.path,
() => {
proxy.$refs.layoutDefaultsScrollbarRef.wrap.scrollTop = 0
}
)
return {
}
}
}
</script>
3. 添加 header 组件
在 store/modules/themeConfig.ts state新增 isFixedHeader 布尔类型状态
- 新建 layout/component/navBars/index.vue
<template>
<div class="layout-navbars-container">
<i
class="layout-navbars-breadcrumb-icon"
:class="getThemeConfig.isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold'"
@click="onThemeConfigChange"
></i>
</div>
</template>
<script lang="ts">
import { computed } from 'vue'
import { useStore } from 'store/index'
export default {
name: 'layoutNavBars',
components: { },
setup() {
const store = useStore()
// 获取布局配置信息
const getThemeConfig = computed(() => {
return store.state.themeConfig
})
// 展开/收起左侧菜单点击
const onThemeConfigChange = () => {
store.state.themeConfig.isCollapse = !store.state.themeConfig.isCollapse
}
return {
getThemeConfig,
onThemeConfigChange
}
}
}
</script>
<style scoped lang="scss">
.layout-navbars-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
</style>
- 新建 layout/component/header.vue
<template>
<el-header class="layout-header" :height="50">
<NavBarsIndex />
</el-header>
</template>
<script lang="ts">
import { computed } from 'vue';
import { useStore } from 'store/index';
import NavBarsIndex from './navBars/index.vue';
export default {
name: 'layoutHeader',
components: { NavBarsIndex },
setup() {
const store = useStore();
return {};
},
};
</script>
- 修改 layout/component/main/index.vue:
<template>
<el-container class="layout-container">
<Aside />
<el-container class="flex-center layout-backtop">
<Header v-if="isFixedHeader" />
<el-scrollbar ref="layoutDefaultsScrollbarRef">
<Header v-if="!isFixedHeader" />
<MainView />
</el-scrollbar>
</el-container>
<el-backtop target=".layout-backtop"></el-backtop>
</el-container>
</template>
<script lang="ts">
import { computed, getCurrentInstance, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useStore } from 'store/index'
import Aside from '../component/aside.vue'
import Header from '../component/header.vue'
import MainView from '../component/mainView.vue'
export default {
name: 'layoutDefaults',
components: { Aside, Header, MainView },
setup() {
const { proxy } = getCurrentInstance() as any
const store = useStore()
const route = useRoute()
const isFixedHeader = computed(() => {
return store.state.themeConfig.isFixedHeader;
});
// 监听路由的变化
watch(
() => route.path,
() => {
proxy.$refs.layoutDefaultsScrollbarRef.wrap.scrollTop = 0
}
)
return {
isFixedHeader
}
}
}
</script>
- 最后再修改 layout/index.vue 让 layout 从 store中获取
const store = useStore()
const layout = computed(() => store.state.themeConfig.layout)
至此侧边栏基本完善,如下显示: