《vue3從零搭建一個后臺》(六)、導(dǎo)航欄的配置

NavBar (導(dǎo)航欄)

創(chuàng)建 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>

這里優(yōu)化一下。/layout/components 文件夾的管理
創(chuàng)建src/layout/components/index.js

export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar'
// 引用方式
//import { Navbar, Sidebar } from './components/'
// './components/'  它會優(yōu)先在 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>
      <!-- 側(cè)邊欄 -->
      <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>

運行后的頁面

接下來是數(shù)據(jù)的動態(tài)化處理

改造src/router/index.js 加入測試的三個路由
加入meta屬性,用于傳遞參數(shù)

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() {
      // 獲取路由對應(yīng)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)
    }
  }
}
效果展示

接下來是標(biāo)簽

路由標(biāo)簽

創(chuàng)建/layout/components/ScrollPane.vue 標(biāo)簽多是啟動滾動
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>

創(chuàng)建 src/store/modules/tabs.js。 需要配置Vuex 管理標(biāo)簽的信息
tabs.js

const state = {
  visitedViews: [], // 標(biāo)簽組
  cachedViews: [] // 需要緩存的標(biāo)簽組,根據(jù)這個數(shù)組,確定是否緩存頁面,暫時沒用到
}

const mutations = {
  ADD_VISITED_VIEW(state, view) {
    // 如果標(biāo)簽跳轉(zhuǎn)的路由存在就不添加
    // 名字不同,路徑相同的。也加入標(biāo)簽組
    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
}

創(chuàng)建/layout/components/Tabs.vue 路由標(biāo)簽。用于關(guān)閉頁面,刷新頁面
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() // 路由一旦變化就會觸發(fā)
    }
  },
  mounted() {
    this.addTab() 
  },
  methods: {
    addTab() {
      const { name } = this.$route
      // 已存在的標(biāo)簽就不更新tabs狀態(tài)
      // 就是點擊過的菜單,在點擊不觸發(fā)行為。
      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>

由于加入了標(biāo)簽。所以頭部高度將增加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;
  }
}

保存啟動項目看看效果


效果展示

滾動效果

增已經(jīng)完成了,接下來搭建刪以及刷新的功能
刪:
src/router/index.js 加入保護屬性

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

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

// action
  // 刪除標(biāo)簽
  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)
  }
// 重構(gòu)
<span class="el-icon-close" />
// ==>
<span v-if="!isAffix(tab)" class="el-icon-close" @click.prevent="closeTabs(tab)" />
// script 方法區(qū)加入  2個方法
methods: {
    isAffix(tab) { // 是否受保護,這里保護首頁面不被刪除。去掉X的按鈕
      // 在保護的路由 meta 增加 affix屬性
      return tab.meta && tab.meta.affix
    },
   closeTabs(tab) {
      this.$store.dispatch('tabs/delView', tab).then(({ visitedViews }) => {
        // 如果刪除的是當(dāng)前頁面,則跳轉(zhuǎn)去下一個頁面。
        if (this.isActive(tab)) {
          if (visitedViews.length) {
            // 跳轉(zhuǎn)到最后一個標(biāo)簽
            const lastTab = visitedViews[visitedViews.length - 1]
            this.$router.push(lastTab.fullPath)
          } else {
            // 如果沒有標(biāo)簽了,則跳去首頁
            this.$router.push('/')
          }
        }
      })
    }
  }
刪除功能展示

標(biāo)簽拓展功能
刷新、關(guān)閉、關(guān)閉其他、關(guān)閉全部

// 加入 contextmenu屬性。實現(xiàn)鼠標(biāo)右鍵功能
<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')">關(guān)閉</li>
      <li @click="menu('other')">關(guān)閉其他</li>
      <li @click="menu('all')">全部關(guān)閉</li>
    </ul>
  </div>

方法區(qū)

...
  data() {
    return {
      visible: false, // 菜單顯隱變量
      top: 0, // 菜單偏移量x
      left: 0, // 菜單偏移量x
      selectedTab: {} // 鼠標(biāo)右擊的tab
    }
  },
...
  methods: {
    openMenu(tab, e) {
      // 計算偏移量
      // this.$el = Tabs.vue 這個Dom
      // getBoundingClientRect().left 獲取tabs 距離窗口左邊距離。
      // 由于left 根據(jù)父元素進行偏移。
      // 所以 left = 鼠標(biāo)在窗口的x坐標(biāo) - 側(cè)邊欄寬度     15為菜單離鼠標(biāo)一段距離
      this.left = e.clientX - this.$el.getBoundingClientRect().left + 15 // 15: margin right
      // top 由于不用適配,所以采用 鼠標(biāo)在當(dāng)前元素的相對位置
      this.top = e.offsetY
      // 顯示菜單
      this.visible = true
      // 功能操作的tab。
      this.selectedTab = tab
    },
    menu(select) {
      switch (select) {
        case 'refresh':
          // 清除該頁面緩存,在跳轉(zhuǎn)該路由 達(dá)到刷新效果。
          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(() => {
            // 不是當(dāng)前激活,刪除其他后,跳轉(zhuǎn)到該頁面
            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 加入刷新、關(guān)閉等執(zhí)行方法

// 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) {
    // 重置頁面標(biāo)簽數(shù)組和緩存數(shù)組
    state.visitedViews = [
      Object.assign({}, view, {
        title: view.meta.title || 'no-name'
      })
    ]
    state.cachedViews = [view.name]
  },
  DEL_ALL_VIEW(state) {
    // 重置頁面標(biāo)簽數(shù)組和緩存數(shù)組
    state.visitedViews = []
    state.cachedViews = []
  }
功能展示

最后再加上左鍵取消菜單的方法

...
watch: {
    $route() {
      this.addTab() // 路由一旦變化就會觸發(fā)
    },
    visible(value) {
      if (value) {
        document.body.addEventListener('click', this.closeMenu)
      } else {
        document.body.removeEventListener('click', this.closeMenu)
      }
    }
  },
...
methods: {
   closeMenu() {
      this.visible = false
    }
}

到此導(dǎo)航欄 header 部分 配置完了。以上功能可以根據(jù)需求進行一些加工。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容