Vite2 + vue3 + TS + ElementPlus 从零搭建后台管理系统(四)

4,314 阅读1分钟

上一章主要完善后台管理系统基础布局和项目结构 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>

到目前启动服务,就可以看见如下了: image.png

如果细心看过代码很容易就猜测到,接下来要实现侧边菜单收起功能

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)

至此侧边栏基本完善,如下显示:

image.png