前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【vite+vue3+Ts+element-plus】肩并肩带你写后台管理之主页面开发(侧边栏菜单生成、标签栏开发)

【vite+vue3+Ts+element-plus】肩并肩带你写后台管理之主页面开发(侧边栏菜单生成、标签栏开发)

作者头像
十里青山
发布2023-04-28 15:59:55
4.1K0
发布2023-04-28 15:59:55
举报
文章被收录于专栏:我的前端之路


github: https://github.com/heyongsheng/hevue3-admin 码云: https://gitee.com/ihope_top/hevue3-admin 线上体验地址 https://ihope_top.gitee.io/hevue3-admin

本章知识点:

  • layout页面开发
  • 侧边栏菜单开发
  • 标签栏开发
  • 页面切换过渡效果及页面缓存

layout页面开发

我们先来看一下主页面长什么样子。

页面比较简单,主要分为左侧的菜单栏,顶部的导航栏(折叠左侧菜单,切换暗黑模式,员工账号名,退出登录),再下面的标签栏,之后就是主页面显示区域。

我们在layout目录下创建一个index.vue来作为我们的入口文件

代码语言:javascript
复制
<template>
  <div class="app-wrapper">
    <!-- 左侧menu -->
    <sidebar
      id="guide-sidebar"
      class="sidebar"
      :class="{ 'sidebar-container': !isCollapse }"
    ></sidebar>
    <div class="main-container">
      <div class="fixed-header">
        <!-- 顶部 navbar -->
        <navbar>
          <template #collapse>
            <div class="collapse-btn" :class="{ row: isCollapse }">
              <svg-icon
                name="gengduo-heng"
                @click="isCollapse = !isCollapse"
              ></svg-icon>
            </div>
          </template>
        </navbar>
        <!-- 标签 -->
        <tags-view></tags-view>
      </div>
      <!-- 内容区 -->
      <app-main></app-main>
    </div>
  </div>
</template>

<script setup lang="ts">
import Navbar from './components/Navbar.vue'
import Sidebar from './components/Sidebar/index.vue'
import TagsView from './components/TagsView/index.vue'
import AppMain from './components/AppMain.vue'

const isCollapse = ref(false)
provide('isCollapse', isCollapse)
</script>
<style lang="scss">
.app-wrapper {
  @include globalScss.clearfix;
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  display: flex;
}
.sidebar {
  position: relative;
  z-index: 2;
  background: var(--color-menu-bg);
}
.sidebar-container {
  min-width: globalScss.$sideBarWidth;
}
.main-container {
  overflow: hidden;
  flex: 1;
  display: flex;
  flex-direction: column;
}
.collapse-btn {
  cursor: pointer;
  margin-left: 16px;
  transition: all 0.3s;
  &amp;.row {
    transform: rotate(90deg);
  }
}
</style>

这里控制侧边栏折叠的按钮我是通过slot的方式传入的顶部导航栏,因为左侧的菜单组件也需要接收这个属性,并且层级较深,所以这里我们使用provide发送一下,在菜单组件那里使用inject进行接收。

这里需要讲的内容主要就是左侧的菜单和标签栏,我们先来讲一下左侧的菜单开发。

侧边菜单栏开发

我们之前讲权限的地方已经给大家看过了返回的菜单数据,并封装成了树形结构,所以我们这里菜单就根据保存的菜单数据渲染菜单就可以了。

我们在按照以下层级创建侧边栏需要用到的组件

layout -> components -> Sidebar -> index.vue , SidebarItem.vue, SidebarMenu.vue

代码语言:javascript
复制
// index.vue
<template>
  <div>
    <el-scrollbar>
      <sidebar-menu></sidebar-menu>
    </el-scrollbar>
  </div>
</template>

<script setup lang="ts">
import SidebarMenu from './SidebarMenu.vue'
</script>
<style scoped></style>

index.vue比较简单,我们这里就是引用了一下element-plus的滚动条组件,然后再引入SidebarMenu

代码语言:javascript
复制
// SidebarMenu
<template>
  <el-menu
    :default-openeds="defaultOpeneds"
    :default-active="$route.fullPath"
    class="el-menu-vertical-demo"
    :unique-opened="true"
    :active-text-color="themeColor"
    router
    :collapse="isCollapse"
  >
    <template v-for="item in menus" :key="item._id">
      <sidebar-item :item-data="item" v-if="!item?.meta?.hidden"></sidebar-item>
    </template>
  </el-menu>
</template>

<script setup lang="ts">
import { usePermissionStore } from '@/stores/permission'
import SidebarItem from './SidebarItem.vue'
import { reactive, inject } from 'vue'
import { compareVersion } from '@/utils/util'

const permissionStore = usePermissionStore()
const menus = permissionStore.routes
const router = useRouter()
const isCollapse: boolean | undefined = inject('isCollapse')

const themeColor = ref('')
onMounted(() => {
  themeColor.value =
    document.documentElement.style.getPropertyValue('--color-primary')
})

// 默认展开
const defaultOpeneds: any[] = reactive([])
const findActive = (menus: any) => {
  menus.forEach((item: any) => {
    if (item.children &amp;&amp; item.children[0]) {
      findActive(item.children)
    } else {
      if (item.path === router.currentRoute.value.path) {
        defaultOpeneds.push(item.parentId)
      }
    }
  })
}
findActive(menus)

// 菜单排序
const sortMenus = (menus: any) => {
  menus.sort((a: any, b: any) =>
    compareVersion(b.meta.sort || '0', a.meta.sort || '0')
  )
  menus.forEach((item: any) => {
    if (item.children) {
      sortMenus(item.children)
    }
  })
}
sortMenus(menus)
</script>
<style scoped></style>

这个页面主要就是几个操作

第一个就是从pinia中获取一下菜单数据,并传递给子组件进行遍历渲染。

第二个就是设置了默认展开项

第三个对菜单进行了排序,这里的排序我用的事版本号对比的方式,这里贴一下代码

代码语言:javascript
复制
// 版本号大小对比
export function compareVersion(v1: string = '0', v2: string = '0'): number {
  let v1Arr = v1.split('.')
  let v2Arr = v2.split('.')
  const len = Math.max(v1.length, v2.length)

  while (v1Arr.length < len) {
    v1Arr.push('0')
  }
  while (v2Arr.length < len) {
    v2Arr.push('0')
  }

  for (let i = 0; i < len; i++) {
    const num1 = parseInt(v1Arr[i])
    const num2 = parseInt(v2Arr[i])

    if (num1 > num2) {
      return 1
    } else if (num1 < num2) {
      return -1
    }
  }

  return 0
}

什么是版本号对比方式呢?就是 1.11 是比 1.2大的,因为我这里除了菜单要排序,标签栏那里也要排序,采用版本号对比的方式会方便一点。

之后再来看看SidebarItem.vue

代码语言:javascript
复制
<template>
  <!-- 此处注意,不要多嵌套层级,否则可能导致菜单样式错乱,建议直接在父级组件v-for时直接判断 -->
  <!-- <div v-if="!itemData?.meta?.hidden"> -->
  <el-sub-menu
    v-if="
      itemData?.children &amp;&amp;
      (itemData.meta.alwaysShow || itemData?.children?.length > 1)
    "
    :index="itemData._id"
  >
    <template #title>
      <!-- 此处不嵌套el-icon也可正常显示,嵌套了之后可以使用el-menu预设的样式,且在折叠的时候不会闪动 -->
      <el-icon
        ><svg-icon class="menu-icon" :name="itemData.meta.icon"></svg-icon
      ></el-icon>
      <span>{{ itemData.meta.title }}</span>
    </template>
    <!-- <el-menu-item-group> -->
    <sidebar-item
      v-for="item in itemData.children"
      :key="item._id"
      :item-data="item"
    ></sidebar-item>
    <!-- </el-menu-item-group> -->
  </el-sub-menu>
  <sidebar-item
    v-else-if="itemData?.children"
    :item-data="itemData?.children[0]"
  ></sidebar-item>
  <el-menu-item v-else :index="itemData.path">
    <el-icon
      ><svg-icon class="menu-icon" :name="itemData.meta.icon"></svg-icon
    ></el-icon>
    <span>{{ itemData.meta.title }}</span>
  </el-menu-item>
  <!-- </div> -->
</template>

<script setup lang="ts">
defineProps(['itemData'])
</script>
<style scoped lang="scss">
.menu-icon {
  font-size: 16px;
}
</style>

这里首先会判断该菜单是否要在菜单栏隐藏,之后会判断这是个菜单(一级菜单)还是个页面(二级菜单),同时也支持一些只有一个二级菜单的一级菜单直接显示二级菜单,这个是否直接显示根据我们在编辑菜单时配置的alwaysShow决定,后面也会简单的说一下菜单管理的配置项。

菜单栏其实就这么多东西,这里写的比较粗糙,如果有问题欢迎评论区指出。

标签栏开发

现在我们来开发标签栏,这里也参考了花裤衩大佬的标签方案,首先创建文件layout/components/TagsView/index.vue

这里就不放全篇的代码了,只讲一下注意的点吧。

首先说一下标签的数据从哪里来,我这里是监听的route,在route变化时,将新的路由信息添加到标签列表。

代码语言:javascript
复制
const route = useRoute()

watch(
  () => route.path,
  () => {
    addTags()
  }
)

const addTags = () => {
  const { name } = route
  if (name) {
    tagsViewStore.addView(route)
  }
}

这里我们把添加工作放到pinia里,需要注意的点我都在代码里备注上了

代码语言:javascript
复制
// stores/tagView.ts

  state: () => ({
    visitedViews: new Array<any>(), // 标签列表,每一项存储的路由信息
    cachedViews: new Array<string>(), // 缓存列表,每一项存储的路由name
  }),
  
  actions: {
    addView(view: any) {
      this.addVisitedView(view)
      this.addCachedView(view)
    },
    addVisitedView(view: any) {
      // 判断是否已添加
      if (this.visitedViews.some((v) => v.path === view.path)) return
      this.visitedViews.push(
        Object.assign({}, view, {
          title: view.meta.title || 'no-name',
        })
      )
    },
    addCachedView(view: any) {
      // 判断是否已添加
      if (this.cachedViews.includes(view.name)) return
      // 判断该页面是否需要缓存
      if (view.meta.cache) {
        this.cachedViews.push(view.name)
      }
    },
  }

之后我们用router-link渲染一下数据

代码语言:javascript
复制
    <el-scrollbar
      ref="scrollPane"
      class="scroll-pane"
      @wheel.native.prevent="handleScroll"
    >
      <div class="tag-list">
        <router-link
          :to="item.path"
          class="tag-item"
          :class="{ checked: isCheck(item) }"
          v-for="item in tagsViewStore.visitedViews"
          ref="tagItem"
        >
          {{ item.meta.title }}
          <i-ep-close
            class="close-icon"
            v-if="!item.meta.affix"
            @click.prevent="closeTag(item)"
          />
        </router-link>
      </div>
    </el-scrollbar>

这里我们使用了el-scrollbar来进行横向滚动,由于el-scrollbar也不支持鼠标滚动的时候横向滚动,所以我们只能监听鼠标滚动事件,自己写一个横向滚动的方法。

代码语言:javascript
复制
// 标签栏横向滚动
const handleScroll = (e: any) => {
  const eventDelta = e.wheelDelta || -e.deltaY * 40
  let scrollLeft = scrollPane.value?.wrapRef.scrollLeft
  scrollLeft += eventDelta / 8
  scrollPane.value.setScrollLeft(scrollLeft)
}

关于滚动,还有一个小细节,就是当标签比较多了之后,我们通过侧边栏或者其他方式跳转到已经访问过的页面,如果该页面的标签被超出屏幕被隐藏了,我们需要把标签栏滚动到该标签的位置。

代码语言:javascript
复制
// 滚动到当前tag
const tagItem = ref<any[]>()
const moveToCurrentTag = async () => {
  tagItem.value?.forEach((item: any) => {
    if (item.to === route.path) {
      // 判断当前元素是否超出屏幕
      const isOut =
        item.$el.offsetLeft + item.$el.offsetWidth >
          scrollPane.value?.wrapRef.offsetWidth +
            scrollPane.value?.wrapRef.scrollLeft ||
        item.$el.offsetLeft < scrollPane.value?.wrapRef.scrollLeft + 20

      if (isOut) {
        scrollPane.value?.scrollTo(item.$el.offsetLeft - 20, 0)
      }
      // when query is different then update
      if (item.to.fullPath !== route.fullPath) {
        tagsViewStore.updateVisitedView(route)
      }
    }
  })
}

有时候我们会需要某些标签一直固定在标签栏,比如首页,固定的标签栏不可关闭,这里是通过在菜单管理时候配置的是否固定标签栏,固定标签的排序顺序跟菜单排序顺序一样。如果是公共路由,我们也可以给路由的meta配置affix: true来实现。

固定标签没有关闭按钮

页面切换过渡效果

刚才说标签的时候提到了缓存页面,不过没有说怎么写,这里和过渡效果一起说。

我们需要切换过渡效果的地方其实就是主界面显示区域那一块,文件是layout/components/AppMain.vue,这里需要注意的是,在vue3中router-view嵌套使用的时候写法发生了改变

代码语言:javascript
复制
<template>
  <div class="main-wrap">
    <router-view v-slot="{ Component, route }">
      <transition name="fade-transform" appea mode="default">
        <keep-alive :include="tagsViewStore.cachedViews">
          <component :is="Component" :key="route.path" class="app-main" />
        </keep-alive>
      </transition>
    </router-view>
  </div>
</template>

这里还需要注意两个点:

  • transition和keep-alive嵌套使用时,transition的mode不能为out-in模式,否则可能会导致页面空白或者过渡效果不生效的问题
  • 虽然vue3不再显示单个的页面根节点,但是transition和keep-alive都要求必须接受一个根节点,所以如果我们要使用这两个,建议vue页面还是乖乖的写单个根标签的好。

下面附上过渡效果的css

代码语言:javascript
复制
.fade-transform-enter-from {
  opacity: 0;
  transform: translateX(60px);
}
.fade-transform-leave-to {
  opacity: 0;
  transform: translateX(-60px);
}
.fade-transform-enter-active,
.fade-transform-leave-active {
  transition: all 0.5s;
}

这样我们的页面切换过渡效果就做好了。

可以看到,我们是通过keep-alive的include参数,把我们刚才的缓存页面name列表告诉它哪些页面需要缓存的,我们这里设置角色管理为缓存页面测试一下效果

可以看到角色管理页面被成功的缓存了。

本章就到这里,下一章讲一下前端字典项,还有一些前面遗漏的地方。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-04-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • layout页面开发
    • 侧边菜单栏开发
      • 标签栏开发
      • 页面切换过渡效果
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档