《vue3从零搭建一个后台》(六)、导航栏的配置

NavBar (导航栏)

创建 src/layout/components/NavBar.vue

<template>
  <div class="navbar">
    <!-- sidebar抽屉按钮 -->
    <div class="sidebar-switch" @click="switchSidebar">
      <i :class="open ? 'el-icon-s-fold':'el-icon-s-unfold'" />
    </div>
    <!-- breadcrumb -->
    <el-breadcrumb separator="/">
      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item><a href="/">活动管理</a></el-breadcrumb-item>
      <el-breadcrumb-item>活动列表</el-breadcrumb-item>
      <el-breadcrumb-item>活动详情</el-breadcrumb-item>
    </el-breadcrumb>
    <!-- nav menu -->
    <el-dropdown class="nav-menu">
      <div class="avatar-wrapper">
        <img :src="'https://upload.jianshu.io/users/upload_avatars/20351000/e6ae7017-e428-4e0d-819b-59c1ae535835.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/240/h/240'">
        <span>康言先森</span>
        <i class="el-icon-caret-bottom" />
      </div>
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item command="a">个人中心</el-dropdown-item>
          <el-dropdown-item command="e" divided>退出登录</el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
  </div>
</template>

<script>

export default {
  name: 'Navbar',
  props: {
    open: {
      type: Boolean,
      default: true
    }
  },
  methods: {
    switchSidebar() {
      this.$emit('switchSidebar')
    }
  }
}
</script>
<style lang="scss">
.navbar {
  height: 50px;
  -webkit-box-shadow: 0 1px 4px rgba(0,21,41,0.08);
  box-shadow: 0 1px 4px rgba(0,21,41,0.08);
  .sidebar-switch {
    height: 100%;
    width: 50px;
    padding: 0 15px;
    line-height: 50px;
    float: left;
    cursor: pointer;
    i {
      font-size: 22px;
      line-height: 50px;
    }
  }
  .el-breadcrumb {
    float: left;
    height: 100%;
    line-height: 50px;
  }
  .nav-menu {
    float: right;
    cursor: pointer;
    height: 50px;
    line-height: 50px;
    .avatar-wrapper {
      display: flex;
      align-items: center;
    }
    img {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      margin-right: 5px;
    }
  }
}
</style>

这里优化一下。/layout/components 文件夹的管理
创建src/layout/components/index.js

export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar'
// 引用方式
//import { Navbar, Sidebar } from './components/'
// './components/'  它会优先在 components 文件夹下寻找index.js文件
// 其次会是 index.vue 文件。这样做是方便维护
// 不再是
//import Navbar from './components/Navbar .vue'
//import Sidebar from './components/Sidebar.vue'

src/layout/index.vue。改造后 引入 Navbar组件

<template>
  <!-- 整体页面布局 -->
  <el-row class="app-wrapper">
    <el-container>
      <!-- 侧边栏 -->
      <el-aside :width="open ? '210px' : '0px'">
        <sidebar :open="open" />
      </el-aside>
      <el-container>
        <!-- 顶部 -->
        <el-header height="50px">
          <!-- 头部信息 -->
          <navbar @switchSidebar="switchSidebar" />
        </el-header>
        <!-- 主页面 -->
        <el-main>主页面</el-main>
      </el-container>
    </el-container>
  </el-row>
</template>

<script>
import { Navbar, Sidebar } from './components/'

export default {
  name: 'Layout',
  components: {
    Sidebar,
    Navbar
  },
  data() {
    return {
      open: true
    }
  },
  methods: {
    switchSidebar() {
      this.open = !this.open
    }
  }
}
</script>
...
样式
</style>

运行后的页面

接下来是数据的动态化处理

改造src/router/index.js 加入测试的三个路由
加入meta属性,用于传递参数

export const routes = [
  {
    path: '/',
    component: Layout,
    name: '主页',
    meta: {
      title: '主页'
    }
    // component: () => import('@/views/home/index')
  },
  {
    path: '/dog',
    component: Layout,
    name: '狗子世界',
    meta: {
      title: '狗子世界'
    },
    // component: () => import('@/views/home/index')
    children: [
      {
        path: '/erha',
        name: '哈士奇',
        meta: {
          title: '哈士奇'
        },
        component: () => import('@/views/home/index')
      },
      {
        path: '/jinmao',
        name: '金毛',
        meta: {
          title: '金毛'
        },
        component: () => import('@/views/home/index')
      },
      {
        path: '/taidi',
        name: '泰迪',
        meta: {
          title: '泰迪'
        },
        component: () => import('@/views/home/index')
      }
    ]
  }
]

NavBar.vue改造

  <!-- breadcrumb -->
    <el-breadcrumb separator="/">
      <el-breadcrumb-item v-for="item in levelList" :key="item.path">
        {{ item.meta.title }}
      </el-breadcrumb-item>
    </el-breadcrumb>

//script
export default {
  name: 'Navbar',
  props: {
    open: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      levelList: null
    }
  },
  watch: {
    $route() {
      this.getBreadcrumb()
    }
  },
  created() {
    this.getBreadcrumb()
  },
  methods: {
    switchSidebar() {
      this.$emit('switchSidebar')
    },
    getBreadcrumb() {
      // 获取路由对应title   && 存在返回右边,不存在返回左边
      const matched = this.$route.matched.filter(item => item.meta && item.meta.title)
      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
    }
  }
}
效果展示

接下来是标签

路由标签

创建/layout/components/ScrollPane.vue 标签多是启动滚动
ScrollPane.vue

<template>
<template>
  <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.prevent="handleScroll">
    <slot />
  </el-scrollbar>
</template>

<script>
export default {
  name: 'ScrollPane',
  data() {
    return {
      left: 0
    }
  },
  computed: {
    scrollWrapper() {
      return this.$refs.scrollContainer.$refs.wrap
    }
  },
  methods: {
    handleScroll(e) {
      const eventDelta = e.wheelDelta || -e.deltaY * 40
      const $scrollWrapper = this.scrollWrapper
      $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
    }
  }
}
</script>
<style lang="scss" scoped>
.scroll-container {
  white-space: nowrap;
  position: relative;
  overflow: hidden;
  width: 100%;
  /deep/ {
    .el-scrollbar__bar {
      bottom: 0px;
    }
    .el-scrollbar__wrap {
      height: 49px;
    }
  }
}
</style>

创建 src/store/modules/tabs.js。 需要配置Vuex 管理标签的信息
tabs.js

const state = {
  visitedViews: [], // 标签组
  cachedViews: [] // 需要缓存的标签组,根据这个数组,确定是否缓存页面,暂时没用到
}

const mutations = {
  ADD_VISITED_VIEW(state, view) {
    // 如果标签跳转的路由存在就不添加
    // 名字不同,路径相同的。也加入标签组
    if (state.visitedViews.some(v => v.path === view.path)) return
    state.visitedViews.push(
      Object.assign({}, view, {
        title: view.meta.title || 'no-name'
      })
    )
  },
  ADD_CACHED_VIEW(state, view) {
    // 已存在缓存就不缓存了
    if (state.cachedViews.includes(view.name)) return
    if (!view.meta.noCache) {
      state.cachedViews.push(view.name)
    }
  }
}
const actions = {
  addView({ commit }, view) {
    // view == this.$router
    commit('ADD_VISITED_VIEW', view)
    commit('ADD_CACHED_VIEW', view)
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

创建/layout/components/Tabs.vue 路由标签。用于关闭页面,刷新页面
Tabs.vue

<template>
  <div class="tabs">
    <scroll-pane ref="scrollPane" class="tabs-wrapper">
      <router-link
        v-for="tab in visitedViews"
        ref="tag"
        :key="tab.path"
        class="tabs-item"
        :class="isActive(tab)?'active':''"
        :to="{ path: tab.path, query: tab.query, fullPath: tab.fullPath }"
        tag="span"
      >
        {{ tab.title }}
        <span class="el-icon-close" />
      </router-link>
    </scroll-pane>
  </div>
</template>

<script>
import ScrollPane from './ScrollPane.vue'
export default {
  name: 'Tabs',
  components: { ScrollPane },
  computed: {
    visitedViews() {
      return this.$store.state.tabs.visitedViews
    }
  },
  watch: {
    $route() {
      this.addTab() // 路由一旦变化就会触发
    }
  },
  mounted() {
    this.addTab() 
  },
  methods: {
    addTab() {
      const { name } = this.$route
      // 已存在的标签就不更新tabs状态
      // 就是点击过的菜单,在点击不触发行为。
      if (name) {
        console.log('this.router:', this.$route)
        this.$store.dispatch('tabs/addView', this.$route)
      }
      return false
    },
    isActive(route) {
      return route.path === this.$route.path
    }
  }
}
</script>
<style lang="scss">
.tabs {
  position: relative;
  height: 34px;
  width: 100%;
  background: #fff;
  border-bottom: 1px solid #d8dce5;
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
  .tabs-wrapper {
    .tabs-item {
      display: inline-block;
      position: relative;
      cursor: pointer;
      height: 30px;
      line-height: 30px;
      border: 1px solid #d8dce5;
      color: #495060;
      background: #fff;
      padding: 0 8px;
      font-size: 12px;
      margin-left: 5px;
      margin-top: 2px;
      &:first-of-type {
        margin-left: 15px;
      }
      &:last-of-type {
        margin-right: 15px;
      }
      &.active {
        background-color: #42b983;
        color: #fff;
        border-color: #42b983;
        &::before {
          content: '';
          background: #fff;
          display: inline-block;
          width: 8px;
          height: 8px;
          border-radius: 50%;
          position: relative;
          margin-right: 2px;
        }
      }
    }
  }
}
</style>

由于加入了标签。所以头部高度将增加32px,所以
layout/index.vue 样式加入margin-top

.app-wrapper {
  position: relative;
  height: 100%;
  width: 100%;
  .el-aside{
    transition: .5s;
  }
  .el-header {
    padding: 0;
  }
  .el-main {
    margin-top: 32px;
  }
}

保存启动项目看看效果


效果展示

滚动效果

增已经完成了,接下来搭建删以及刷新的功能
删:
src/router/index.js 加入保护属性

 {
    path: '/',
    component: Layout,
    name: '主页',
    meta: {
      title: '主页',
      affix: true // 加入保护
    }
    // component: () => import('@/views/home/index')
  },

src/store/modules/tabs.js 加入删除方法

// action
  // 删除标签
  delView({ commit, state }, view) {
    return new Promise(resolve => {
      commit('DEL_VISITED_VIEW', view)
      commit('DEL_CACHED_VIEW', view)
      resolve({
        visitedViews: [...state.visitedViews],
        cachedViews: [...state.cachedViews]
      })
    })
  }
// mutations
  DEL_VISITED_VIEW(state, view) {
    for (const [i, v] of state.visitedViews.entries()) {
      if (v.path === view.path) {
        state.visitedViews.splice(i, 1)
      }
    }
  },
  DEL_CACHED_VIEW(state, view) {
    const index = state.cachedViews.indexOf(view.name)
    index > -1 && state.cachedViews.splice(index, 1)
  }
// 重构
<span class="el-icon-close" />
// ==>
<span v-if="!isAffix(tab)" class="el-icon-close" @click.prevent="closeTabs(tab)" />
// script 方法区加入  2个方法
methods: {
    isAffix(tab) { // 是否受保护,这里保护首页面不被删除。去掉X的按钮
      // 在保护的路由 meta 增加 affix属性
      return tab.meta && tab.meta.affix
    },
   closeTabs(tab) {
      this.$store.dispatch('tabs/delView', tab).then(({ visitedViews }) => {
        // 如果删除的是当前页面,则跳转去下一个页面。
        if (this.isActive(tab)) {
          if (visitedViews.length) {
            // 跳转到最后一个标签
            const lastTab = visitedViews[visitedViews.length - 1]
            this.$router.push(lastTab.fullPath)
          } else {
            // 如果没有标签了,则跳去首页
            this.$router.push('/')
          }
        }
      })
    }
  }
删除功能展示

标签拓展功能
刷新、关闭、关闭其他、关闭全部

// 加入 contextmenu属性。实现鼠标右键功能
<div class="tabs">
    <scroll-pane ref="scrollPane" class="tabs-wrapper">
      <router-link
        v-for="tab in visitedViews"
        ref="tab"
        :key="tab.path"
        class="tabs-item"
        :class="isActive(tab)?'active':''"
        :to="{ path: tab.path, query: tab.query, fullPath: tab.fullPath }"
        tag="span"
        @contextmenu.prevent="openMenu(tab,$event)"
      >
        {{ tab.title }}
        <span v-if="!isAffix(tab)" class="el-icon-close" @click.prevent="closeTabs(tab)" />
      </router-link>
    </scroll-pane>
    <!-- 加入菜单-->
    <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
      <li @click="menu('refresh')">刷新</li>
      <li v-if="!isAffix(selectedTab)" @click="menu('close')">关闭</li>
      <li @click="menu('other')">关闭其他</li>
      <li @click="menu('all')">全部关闭</li>
    </ul>
  </div>

方法区

...
  data() {
    return {
      visible: false, // 菜单显隐变量
      top: 0, // 菜单偏移量x
      left: 0, // 菜单偏移量x
      selectedTab: {} // 鼠标右击的tab
    }
  },
...
  methods: {
    openMenu(tab, e) {
      // 计算偏移量
      // this.$el = Tabs.vue 这个Dom
      // getBoundingClientRect().left 获取tabs 距离窗口左边距离。
      // 由于left 根据父元素进行偏移。
      // 所以 left = 鼠标在窗口的x坐标 - 侧边栏宽度     15为菜单离鼠标一段距离
      this.left = e.clientX - this.$el.getBoundingClientRect().left + 15 // 15: margin right
      // top 由于不用适配,所以采用 鼠标在当前元素的相对位置
      this.top = e.offsetY
      // 显示菜单
      this.visible = true
      // 功能操作的tab。
      this.selectedTab = tab
    },
    menu(select) {
      switch (select) {
        case 'refresh':
          // 清除该页面缓存,在跳转该路由 达到刷新效果。
          this.$store.dispatch('tabs/delCachedView', this.selectedTab).then(() => {
            const { fullPath } = this.selectedTab
            this.$nextTick(() => {
              const { query } = this.$route
              this.$router.replace({ path: fullPath, query })
            })
          })
          break
        case 'close':
          this.closeTabs(this.selectedTab)
          break
        case 'other':
          this.$store.dispatch('tabs/delOtherView', this.selectedTab).then(() => {
            // 不是当前激活,删除其他后,跳转到该页面
            if (!this.isActive(this.selectedTab)) this.$router.push(this.selectedTab.fullPath)
          })
          break
        case 'all':
          this.$store.dispatch('tabs/delAllView').then(() => {
            this.$router.push('/')
          })
          break
      }
      // 隐藏菜单
      this.visible = false
    }
  }
...
// 样式scss
  .contextmenu {
    margin: 0;
    background: #fff;
    z-index: 99;
    position: absolute;
    list-style-type: none;
    padding: 5px 0;
    border-radius: 4px;
    font-size: 12px;
    font-weight: 400;
    color: #333;
    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
    li {
      margin: 0;
      padding: 7px 16px;
      cursor: pointer;
      &:hover {
        background: #eee;
      }
    }
  }

src/store/modules/tabs.js 加入刷新、关闭等执行方法

// actions
  delCachedView({ commit }, view) {
    return new Promise(resolve => {
      commit('DEL_CACHED_VIEW', view)
      resolve()
    })
  },
  delOtherView({ commit }, view) {
    return new Promise(resolve => {
      commit('DEL_Other_VIEW', view)
      resolve()
    })
  },
  delAllView({ commit }) {
    return new Promise(resolve => {
      commit('DEL_ALL_VIEW')
      resolve()
    })
  }
// mutations
  DEL_CACHED_VIEW(state, view) {
    const index = state.cachedViews.indexOf(view.name)
    index > -1 && state.cachedViews.splice(index, 1)
  },
  DEL_Other_VIEW(state, view) {
    // 重置页面标签数组和缓存数组
    state.visitedViews = [
      Object.assign({}, view, {
        title: view.meta.title || 'no-name'
      })
    ]
    state.cachedViews = [view.name]
  },
  DEL_ALL_VIEW(state) {
    // 重置页面标签数组和缓存数组
    state.visitedViews = []
    state.cachedViews = []
  }
功能展示

最后再加上左键取消菜单的方法

...
watch: {
    $route() {
      this.addTab() // 路由一旦变化就会触发
    },
    visible(value) {
      if (value) {
        document.body.addEventListener('click', this.closeMenu)
      } else {
        document.body.removeEventListener('click', this.closeMenu)
      }
    }
  },
...
methods: {
   closeMenu() {
      this.visible = false
    }
}

到此导航栏 header 部分 配置完了。以上功能可以根据需求进行一些加工。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容