从0开始搭建后台管理系统-02(Layout布局)(上篇)

1. 介绍

后台管理系统通常都是统一的布局结构,通常有:sideBar、headerBar、main,其中:headerBar包括navBar等,sideBar分为左侧和右侧等。简单结构如下图:

如何让所有的页面都会通过Layout结构,展现在mian中呢?
答:我们会在Layout中使用router-view组件。并且在路由中,每个父路由都是指向的Layout组件,再通过重定向来跳转至具体页面,这样可以实现每个路由都必须通过Layout结构。

2. 详解

注:Layout组件的路径为 src/layout

layout

  • components
    • header
      • breadcrumb
      • tagBar
      • themeSwitch
      • userDropdown
  • sideBar
  • index.vue

2.1 headerBar介绍

本框架中haderBar的功能有:

  1. 控制sideBar收缩按钮,v-model绑定isCollapsed控制侧边栏。
  2. breadBrumb面包屑,用于记录当前页面的位置
  3. screenfull全屏按钮,将管理系统全屏展示
  4. themeSwitch主题切换按钮,用于切换封装的主题风格,主题后面单独文章介绍
  5. 用户头像,包含外链的跳转和退出登录等功能
  6. tagsBar用于记录打开过的页面,并实现来回切换、并实现部分页面保活功能。

2.1.1 收缩按钮

在Layout的入口文件index.vue中将两个组件绑定isCollapsed属性来控制sideBar进行收缩。
在header入口中定义一个icon按钮去控制isCollapsed的切换。

// layout>index.vue
<my-side :is-collapse="isCollapsed"/>
<my-header v-model:isCollapsed="isCollapsed"></my-header>


 // header>index.vue
 <template>
  <div class="header-top">
    <el-icon @click="()=>emit('update:isCollapsed',!isCollapsed)" class="header-icon" :size="20">
      <component :is="isCollapsed ?  Expand:Fold"/> // 切换图标
    </el-icon>
    ...
  </div>
</template>

<script setup lang="ts">
  import {Fold, Expand} from '@element-plus/icons-vue'
  defineProps({
  isCollapsed: {
    type: Boolean,
  }
})
const emit = defineEmits(['update:isCollapsed'])
</script>

2.1.2 breadCrumb面包屑

面包屑的功能就是一个路由展示。如:当前在首页的仪表盘页面,那么breadCrumb就是 首页 > 仪表盘
通过elementPlus中的el-breadcrumb,和route中的route.matched的路由记录来实现,如下:

// > header>components>breadcrumb>index.vue
<template>
    <el-breadcrumb :separator-icon="ArrowRight">
      <el-breadcrumb-item class="breadcrumb-item" v-for="item in breadcrumbItems" :to="{ path: item.path }">{{item.meta.title}}</el-breadcrumb-item>
    </el-breadcrumb>
</template>

<script setup lang="ts">
import {ref, watch} from 'vue'
import {useRoute,type RouteLocationMatched} from 'vue-router'
import {ArrowRight} from '@element-plus/icons-vue'

const route = useRoute()

const breadcrumbItems = ref<RouteLocationMatched[]>([])

const refreshBreadcrumbItems = ()=>{
  breadcrumbItems.value = route.matched.filter(r=>r.meta.title)
}

// 监听路由改变
watch(
    ()=>route.path,
    (newPath)=>{
      refreshBreadcrumbItems()
    },{
      immediate:true
    }
)

</script>
<style lang="less" scoped>
.breadcrumb-item{
  :deep(.is-link){
    color: inherit;
  }
}
</style>

2.1.3 screenfull全屏按钮

这个功能是使用了 screenfull 包实现的,我们把他封装在全局组件中使用。
添加一个el-tooltip,这个是elemenPlus中用于提示的一个组件,提高用户体验。

// > src>components>screenfull>index.vue
<template>
  <el-icon @click="trigger" >
    <el-tooltip :content="isFull? '退出全屏' : '全屏'" placement="bottom" effect="dark">
      <svg-icon :name=" isFull? 'closeScreenfull': 'openScreenfull'"/>
    </el-tooltip>
  </el-icon>
</template>

<script setup lang="ts">
import {ref} from 'vue'
import {ElMessage } from 'element-plus'
import screenfull from 'screenfull'

const props = defineProps({
  el:{
    type:HTMLElement,
    default:document.documentElement
  },
})

const isFull = ref<boolean>(false)

const trigger = ()=>{
  if(screenfull.isEnabled){ // 判断是否支持全屏
    screenfull.toggle(props.el)
    screenfull.isFullscreen
        ? isFull.value = false
        : isFull.value = true
  }else{
    ElMessage.error('此设备不支持全屏操作!')
  }
}
</script>

2.1.4 theme-switch 主题切换

在一般文档页面、后台管理页面、官网等用户长时间使用的页面中,通常会加上主题切换。可以避免视觉疲劳。主题中最少有:两种风格。
本文章也封装了两套:明亮、黑暗,由于主题也是一大块,这里不做细解。具体实现请看本系列文章(自定义主题详解)

// header > themeSwitch > index.vue
<template>
    <el-dropdown trigger="click" @command="setTheme">
      <el-icon>
        <el-tooltip effect="dark" content="主题" placement="bottom">
          <svg-icon name="palette"/>
        </el-tooltip>
      </el-icon>
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item
              v-for="theme in themeList"
              :command="theme"
          >{{theme.title}}</el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
</template>

<script setup lang="ts">
import {useTheme} from '@/hooks/useTheme'
const {initTheme,themeList,setTheme} = useTheme()
initTheme()
</script>

<style lang="less" scoped>
.el-icon{
  height: 100%;
  svg{
    cursor: pointer;
  }
}
.el-dropdown{
  color:inherit;
}
</style>

2.1.5 头像

头像使用的是elementPlus组件-el-dropdown,同时伴有el-dropdown-menu做了一个下拉菜单,用来做登出操作。

// header > userDropdown > index.vue
<template>
    <el-dropdown >
    <span>
      <el-avatar :size="30" :fit="'cover'" :src="circleUrl" />
    </span>
      <template #dropdown>
        <el-dropdown-menu>
          <span @click="toAbout">
          <el-dropdown-item>关于</el-dropdown-item>
            </span>
          <el-dropdown-item>GitHub</el-dropdown-item>
          <span @click="logout">
          <el-dropdown-item >退出登录</el-dropdown-item>
            </span>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
</template>

<script setup lang="ts">
import {ref} from 'vue'
import {useUserStore} from '@/store/user'
const userStore = useUserStore()

const circleUrl = ref<string>('https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png')
const {logout} = userStore // 删除用户缓存信息,跳转登录页面
</script>

// store > user.ts
...
actions:{
  logout(){
            this.$reset()
            cache.removeCookie(ACCESS_TOKEN_KEY)
            cache.clear()
            resetRoutes()
            router.push({name:'login'})
        }
}
...

2.1.6 tagsBar

tagsBar中功能相对较多。我们需要实现:获取、添加、删除、选择并跳转、固定等功能。
思路:首页的tagBar不能关闭,这样实现会更加简单些,毕竟不可能一个页面都不展示,所以默认是展示首页。其他页面添加进来都会进行一个保存,保存其路由信息。当前的标签还要进行样式区别,增加体验感。
函数解释:
computeRoutes:,正常路由都是父路由嵌套子路由,我们只需要显示具体的子路由。所以需要将路由进行一个扁平化操作,获取全部的子路由。这里使用了递归的思想。

<template>
  <section class="tag-wrapper">
    <el-tag
        v-for="tag in displayTags"
        class="tag"
        :class="{isActive:tag.name===currentTag?.name}"
        effect="plain"
        :closable="tag.name !== defaultTagName"
        @close="closeTag(tag)"
        @click='selectTag(tag)'
    >
      {{ tag?.meta?.title }}
    </el-tag>
  </section>
</template>

<script setup lang="ts">
import {onMounted, ref, watch} from 'vue'
import {RouteRecordRaw, useRoute, useRouter} from "vue-router";
import {useUserStore} from '@/store/user'

const route = useRoute()
const router = useRouter()
const {routes} = useUserStore()

// 默认tag,不能关闭
const defaultTagName = 'dashboard'
// 当前tag
const currentTag = ref<RouteRecordRaw>()
// tagBar中全部tag
const displayTags = ref<RouteRecordRaw[]>([])

/** 获取当前全部可用路由信息 */
const computeRoutes = (targetList: RouteRecordRaw[]) => {
  let result: RouteRecordRaw[] = []
  targetList.forEach(item => {
    if (item.children) {
      const childResult = computeRoutes(item.children) as RouteRecordRaw[]
      if (childResult.length > 0) {
        result = result.concat(childResult)
      }
    }
    result.push(item)
  })
  return result
}
const usableRoutes = computeRoutes(routes)

/** 获取标签 */
const getDisplayTag = (tagName)=>{
  return displayTags.value.find(route=>route.name === tagName)
}
const getUsableTag = (tagName)=>{
  return usableRoutes.find(route=>route.name === tagName)
}


/** 选择标签 */
const selectTag = (tag) => {
  router.push({path:tag.path,replace:true})
}

/** 关闭标签 */
const closeTag = (tag) => {
  const index = displayTags.value.findIndex(item => item.name == tag.name)
  if (index !== -1) {
    displayTags.value.splice(index, 1)
    if (tag.name === currentTag?.value?.name) {
      const chooseIndex = index - 1 >= 0 ? index - 1 : 0
      selectTag(displayTags.value[chooseIndex])
    }
  }
}

/** 初始化tag,保证有个默认的tag存在 */
onMounted(()=>{
  const hasDefault = getDisplayTag(defaultTagName)
  if(!hasDefault){
    const defaultTag = getUsableTag(defaultTagName)
    if(defaultTag){
      displayTags.value.splice(0,0,defaultTag)
    }
  }
})

/** 检测路由 */
watch(
    () => route.name,
    (newVal) => {
      const currentPathName = newVal
      const chosenTag = getDisplayTag(currentPathName)
      if (chosenTag) {
        currentTag.value = chosenTag
      } else {
        const route = getUsableTag(currentPathName)
        if (route && route?.meta?.title) {
          displayTags.value.push(route)
          currentTag.value = route
        }
      }
    }, {
      immediate: true
    }
)


</script>

<style lang="less" scoped>
.tag {
  margin: 5px;
  background-color: @theme-color;
  border: 1px solid @theme-line-color;
  color: @theme-font-color;
  cursor: pointer;

  :deep(.el-icon) {
    color: @theme-font-color;
  }
}

.isActive {
  background-color: @theme-label-active-color;
  color: @theme-label-active-font-color;

  :deep(.el-icon) {
    color: @theme-label-active-font-color;
  }
}
</style>

本篇内容过多,拆分为两篇,下篇讲解sideBar和整体Layout。

3. 本框架其他文章链接

GitHub开源链接:GitHub - grxynl/vue3-admin-template: vue3+TypeScript+pinia 后台管理系统模板

其他文章
从0开始搭建后台管理系统(首篇)
从0开始搭建后台管理系统-01(Login登录)

从0开始搭建后台管理系统-02(Layout布局)(下篇)

4. 结束语

本框架完全免费,框架尚有不足,供前端爱好者一起讨论,一起学习。
源码和文档都制作不易,如果觉得您还可以的话,求一个stars,这是对我最大的支持,也是本框架前进的最大动力。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用 ElementUI 组件库与 Vue CLI 工具快速搭建一个基于 Vue2 的后台管理系统。Vue CLI 工具可以帮助我们创建一个基础项目,然后使用 ElementUI 组件库进行页面布局和编写组件。我们还可以使用 Vue Router 和 Vuex 管理路由和状态。 具体实现步骤如下: 1. 安装 Vue CLI 工具:执行命令 npm i -g @vue/cli 2. 创建项目:在命令行中执行 vue create project-name,根据提示选择 Manually select features 选项,然后选择 Babel、Router、Vuex、CSS Pre-processors、Linter / Formatter 这些选项。 3. 安装 ElementUI:执行命令 npm i element-ui -S 4. 在 main.js 中引入 ElementUI:在 main.js 中添加以下代码: ``` import Vue from 'vue' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' Vue.use(ElementUI) ``` 5. 在 App.vue 中布局页面:使用 ElementUI 提供的组件进行页面布局,如 Layout、Menu、Breadcrumb、Table、Form 等。具体可以参考 ElementUI 官方文档:https://element.eleme.cn/#/zh-CN/component/installation。 6. 编写组件:根据实际需求编写自定义组件,封装复用性高的业务逻辑。 7. 使用 Vue Router 管理路由:使用 Vue Router 来管理页面之间的路由,使得页面跳转更加方便和灵活。 8. 使用 Vuex 管理状态:使用 Vuex 来管理全局状态,使得不同页面间能够共享数据。 以上是一个基本的搭建步骤,您可以根据自己的需求来选择其他组件库或 UI 框架,以及根据实际情况进行细节调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值