# 页面布局

基础布局包含侧边栏、导航栏面包屑、标签页、以及内容区域等。

如下图所示:

image-20210104153623651

<template>
  <div :class="classObj" class="app-wrapper">
    <div
      v-if="device === 'mobile' && sidebar.opened"
      class="drawer-bg"
      @click="handleClickOutside"
    />
    <sidebar class="sidebar-container" />
    <div :class="{ hasTagsView: needTagsView }" class="main-container">
      <div :class="{ 'fixed-header': fixedHeader }">
        <navbar />
        <tags-view v-if="needTagsView" />
      </div>
      <app-main />
      <right-panel v-if="showSettings">
        <settings />
      </right-panel>
    </div>
  </div>
</template>

项目中除了个别页面如:login , 404, 401 等页面,大部分页面都是基于这个 layout 的。因此,这些页面在一级路由中需要选择layout组件。当然,如果需要别的布局样式,你可以自定义并引用扩展的layout。

// 无布局
{
  path: '/401',
  component: () => import('errorPage/401')
}

// 使用当前layout组件
{
  path: '/profile',

  component: Layout,

  // 这里开始对应的路由都会显示在app-main中 如上图所示
  children: [{
    path: 'index',
    component: () => import('profile/index'),
    name: 'Profile'
  }]
}

这里使用了 vue-router 路由嵌套 (opens new window), 所以一般情况下,你增加或者修改页面只会影响 app-main这个主体区域。其它配置在 layout 中的内容如:侧边栏或者导航栏都是不会随着你主体页面变化而变化的。

/foo                                  /bar
+------------------+                  +-----------------+
| layout           |                  | layout          |
| +--------------+ |                  | +-------------+ |
| | foo.vue      | |  +------------>  | | bar.vue     | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+

当然你也可以一个项目里面使用多个不同的 layout,只要在你想作用的路由父级上引用它就可以了。

# 侧边栏

路由和侧边栏是组织起一个后台应用的关键骨架,结合后端项目的菜单配置功能,可以自定义左侧的层级菜单项,并自动生成菜单的路由。

<template>
  <div :class="{ 'has-logo': showLogo }">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="variables.menuBg"
        :text-color="variables.menuText"
        :unique-opened="false"
        :active-text-color="variables.menuActiveText"
        :collapse-transition="false"
        mode="vertical"
      >
        <sidebar-item
          v-for="route in permission_routes"
          :key="route.path"
          :item="route"
          :base-path="route.path"
        />
      </el-menu>
    </el-scrollbar>
  </div>
</template>

src/router/index.js 下面配置对应的路由,侧边栏就能动态的生成了。大大减轻了手动重复编辑侧边栏的工作量。当然这样就需要在配置路由的时候遵循一些约定的规则。

首先我们了解一下本项目配置路由时提供了哪些配置项。

// 当设置 true 的时候该路由不会在侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
hidden: true // (默认 false)

//当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
redirect: 'noRedirect'

// 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
// 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
// 若你想不管路由下面的 children 声明的个数都显示你的根路由
// 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
alwaysShow: true

name: 'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
meta: {
  permissions: ['user:list'] // 设置该路由进入的权限,支持多个权限叠加
  title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
  icon: 'svg-name' // 设置该路由的图标,支持 svg-class,也支持 el-icon-x element-ui 的 icon
  noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
  breadcrumb: false //  如果设置为false,则不会在breadcrumb面包屑中显示(默认 true)
  affix: true // 如果设置为true,它则会固定在tags-view中(默认 false)

  // 当路由设置了该属性,则会高亮相对应的侧边栏。
  // 这在某些场景非常有用,比如:一个文章的列表页路由为:/article/list
  // 点击文章进入文章详情页,这时候路由为/article/1,但你想在侧边栏高亮文章列表的路由,
  // 就可以进行如下设置
  activeMenu: '/article/list'
}

# 导航栏

导航栏包括左侧的面包屑、右侧的工具栏按钮(包括功能搜索、全屏缩放、文本大小设置以及个人中心和布局设置),这里可以支持用户对前端UI的全局设置进行统一的设置。

<template>
  <div class="navbar">
    <hamburger
      id="hamburger-container"
      :is-active="sidebar.opened"
      class="hamburger-container"
      @toggleClick="toggleSideBar"
    />

    <breadcrumb id="breadcrumb-container" class="breadcrumb-container" />

    <div class="right-menu">
      <template v-if="device!=='mobile'">
        <search id="header-search" class="right-menu-item" />

        <error-log class="errLog-container right-menu-item hover-effect" />

        <el-tooltip content="全屏缩放" effect="dark" placement="bottom">
          <screenfull id="screenfull" class="right-menu-item hover-effect" />
        </el-tooltip>

        <el-tooltip content="文本大小" effect="dark" placement="bottom">
          <size-select id="size-select" class="right-menu-item hover-effect" />
        </el-tooltip>
      </template>

      <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
        <div class="avatar-wrapper">
          <img
            :src="user.avatarName ? baseApi + '/avatar/' + user.avatarName : avatar"
            class="user-avatar"
          >
          <i class="el-icon-caret-bottom" />
        </div>
        <el-dropdown-menu slot="dropdown">
          <router-link to="/profile/index">
            <el-dropdown-item>个人信息</el-dropdown-item>
          </router-link>
          <span style="display:block;" @click="showSettings = true">
            <el-dropdown-item>布局设置</el-dropdown-item>
          </span>
          <el-dropdown-item divided @click.native="confirmLogout">
            <span style="display:block;">注销</span>
          </el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </div>
  </div>
</template>

# 面包屑

项目中封装了一个面包屑导航,它也是监控路由 变化动态生成的。它和侧边栏菜单 一样,可以通过之前那些配置项控制一些路由在面包屑中的展现,用户可以通过点击面包屑菜单项来快速定位到相关的上一级的功能模块。

<template>
  <el-breadcrumb class="app-breadcrumb" separator="/">
    <transition-group name="breadcrumb">
      <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
        <span
          v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
          class="no-redirect"
          >{{ item.meta.title }}</span
        >
        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
      </el-breadcrumb-item>
    </transition-group>
  </el-breadcrumb>
</template>

# 标签页

实现标签导航方案大致为: 运用 keep-aliverouter-view 的结合。

顶部标签栏导航实际作用相当于 nav 的另一种展现形式,其实说白了都是一个个 router-link,点击跳转到相应的页面。然后我们再来监听路由 $route 的变化,来判断当前页面是否需要重新加载或者已被缓存。

目前 tags-view 维护了两个数组。

  • visitedViews : 用户访问过的页面 就是标签栏导航显示的一个个 tag 数组集合
  • cachedViews : 实际 keep-alive 的路由。可以在配置路由的时候通过 meta.noCache 来设置是否需要缓存这个路由 默认都缓存。
<template>
  <div id="tags-view-container" class="tags-view-container">
    <scroll-pane
      ref="scrollPane"
      class="tags-view-wrapper"
      @scroll="handleScroll"
    >
      <router-link
        v-for="tag in visitedViews"
        ref="tag"
        :key="tag.path"
        :class="isActive(tag) ? 'active' : ''"
        :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
        tag="span"
        class="tags-view-item"
        @click.middle.native="!isAffix(tag) ? closeSelectedTag(tag) : ''"
        @contextmenu.prevent.native="openMenu(tag, $event)"
      >
        {{ tag.title }}
        <span
          v-if="!isAffix(tag)"
          class="el-icon-close"
          @click.prevent.stop="closeSelectedTag(tag)"
        />
      </router-link>
    </scroll-pane>
    <ul
      v-show="visible"
      :style="{ left: left + 'px', top: top + 'px' }"
      class="contextmenu"
    >
      <li @click="refreshSelectedTag(selectedTag)">刷新</li>
      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
        关闭
      </li>
      <li @click="closeOthersTags">关闭其他</li>
      <li @click="closeAllTags(selectedTag)">关闭全部</li>
    </ul>
  </div>
</template>

# 内容区域

内容区域用来展示用户自定义的业务视图,一般我们都统一放在src/views/目录中,通过用户自定义的路由来动态加载业务视图。

<template>
  <section class="app-main">
    <transition name="fade-transform" mode="out-in">
      <keep-alive :include="cachedViews">
        <router-view :key="key" />
      </keep-alive>
    </transition>
  </section>
</template>

#