Vue 實現樹形菜單(多級菜單)功能模塊

##前言 以前博客裏分享過一篇用《Angular 實現樹形菜單(多級菜單)功能模塊》,而在因爲戰略轉移,因此我給 Vue 也來了一份。功能差很少,不過因爲這個是第二次作,會對以前的一些不足做一個修正。下面來看看 Vue 樹形菜單。html

在作這個 DEMO 的時候我是用了vue 的腳手架(vue-cli)的,關於腳手架能夠看這裏《Vue 腳手架(vue-cli)安裝及詳解》,上面的兩篇文章到時也會發到掘金上,若是有須要的朋友能夠收藏本文,以備不時之需。因此結構看起來就像下面這樣(結構示意圖):vue

├── index.html
├── main.js
├── router
│ └── index.js # 路由配置文件
├── components # 組件目錄
│ ├── App.vue # 根組件
│ ├── Home.vue # 大的框架結構組件
│ ├── TreeView.vue
│ ├── TreeViewItem.vue
│ └── TreeDetail.vue
├── store
├── index.js # 咱們組裝模塊並導出 store 的地方
├── modules # 模塊目錄
└── menusModule.js # 菜單模塊
複製代碼

這個多級菜單實現的功能以下:react

  • 一、可展現多級菜單,理論上能夠展無限級菜單
  • 二、當前菜單高亮功能
  • 三、刷新後依然會自動定位到上一次點擊的菜單,即便這個是子菜單,而且父菜單會自動展開
  • 四、子菜單的顯示隱藏有收起、展開,同時帶有淡入效果

這個例子用到的知識點:路由、狀態管理、組件。程序員

狀態管理安裝:web

npm install --save vuex
複製代碼

更多關於 vuex 的介紹能夠看官方文檔:https://vuex.vuejs.org/zh-cn/。vue-router

咱們先來看看效果演示圖:vuex

程序員是用代碼來溝通的,因此費話很少說,直接上碼:vue-cli

index.htmltypescript

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>Vue 實現樹形菜單(多級菜單)功能模塊- 雲庫網</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
複製代碼

main.jsnpm

import Vue from 'vue'
import App from './components/App'
import router from './router'
import store from './store/index'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  store,
  components: {
    App
  },
  template: '<App/>'
})
複製代碼

在 main.js 中引入 路由和狀態管理配置

App.vue

<template>
  <div id="app">
    <Home></Home>
  </div>
</template>
<script>
import Home from "./Home";
export default {
  components: {
    Home
  },
  name: "App"
};
</script>
<style>
* {
  padding: 0;
  margin: 0;
}
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
html,
body,
#app,
.home {
  height: 100%;
}
html,
body {
 overflow: hidden;
}
</style>
複製代碼

Home.vue

<template>
  <div class="home">
       <div class="side-bar">
            <Tree-view></Tree-view>
       </div>
        <div class="continer">
            <router-view></router-view>
        </div>
  </div>
</template>
<script>
import TreeView from "./TreeView";
export default {
  components: {
    TreeView
  },
  name: "Home"
};
</script>
<style scoped>
.side-bar {
  width: 300px;
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  font-size: 14px;
  position: absolute;
  top: 0;
  left: 0;
}
.continer {
  padding-left: 320px;
}
</style>
複製代碼

這個 Home.vue 主要是用來完成頁面的大框架結構。

TreeView.vue

<template>
  <div class="tree-view-menu">
      <Tree-view-item :menus='menus'></Tree-view-item>
  </div>
</template>
<script>
import TreeViewItem from "./TreeViewItem";
const menusData = [];
export default {
  components: {
    TreeViewItem
  },
  name: "TreeViewMenu",
  data() {
    return {
      menus: this.$store.state.menusModule.menus
    };
  }
};
</script>
<style scoped>
.tree-view-menu {
  width: 300px;
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
}
.tree-view-menu::-webkit-scrollbar {
  height: 6px;
  width: 6px;
}
.tree-view-menu::-webkit-scrollbar-trac {
  -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
  box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
}
.tree-view-menu::-webkit-scrollbar-thumb {
  background-color: #6e6e6e;
  outline: 1px solid #333;
}
.tree-view-menu::-webkit-scrollbar {
  height: 4px;
  width: 4px;
}
.tree-view-menu::-webkit-scrollbar-track {
  -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
  box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
}
.tree-view-menu::-webkit-scrollbar-thumb {
  background-color: #6e6e6e;
  outline: 1px solid #708090;
}
</style>
複製代碼

這個組件也很是地簡單,拿到菜單數據,傳給子組件,並把菜單的滾動條樣式修改了下。

TreeViewItem.vue

<template>
  <div class="tree-view-item">
    <div class="level" :class="'level-'+ menu.level" v-for="menu in menus" :key="menu.id">
      <div v-if="menu.type === 'link'">
        <router-link class="link" v-bind:to="menu.url" @click.native="toggle(menu)">{{menu.name}}</router-link>
      </div>
      <div v-if="menu.type === 'button'">
        <div class="button heading" :class="{selected: menu.isSelected,expand:menu.isExpanded}" @click="toggle(menu)">
          {{menu.name}}
          <div class="icon">
            <svg xmlns="http://www.w3.org/2000/svg" focusable="false" viewBox="0 0 24 24">
              <path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z "></path>
            </svg>
          </div>
        </div>
         <transition name="fade">
          <div class="heading-children" v-show="menu.isExpanded" v-if="menu.subMenu">
            <Tree-view-item :menus='menu.subMenu'></Tree-view-item>
          </div>
        </transition>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: "TreeViewItem",
  props: ["menus"],
  created() {
    this.$store.commit("firstInit", { url: this.$route.path });
  },
  methods: {
    toggle(menu) {
      this.$store.commit("findParents", { menu });
    }
  }
};
</script>
<style scoped>
a {
  text-decoration: none;
  color: #333;
}
.link,
.button {
  display: block;
  padding: 10px 15px;
  transition: background-color 0.2s ease-in-out 0s, color 0.3s ease-in-out 0.1s;
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
  -khtml-user-select: none;
  user-select: none;
}
.button {
  position: relative;
}
.link:hover,
.button:hover {
  color: #1976d2;
  background-color: #eee;
  cursor: pointer;
}
.icon {
  position: absolute;
  right: 0;
  display: inline-block;
  height: 24px;
  width: 24px;
  fill: currentColor;
  transition: -webkit-transform 0.15s;
  transition: transform 0.15s;
  transition: transform 0.15s, -webkit-transform 0.15s;
  transition-timing-function: ease-in-out;
}
.heading-children {
  padding-left: 14px;
  overflow: hidden;
}
.expand {
  display: block;
}
.collapsed {
  display: none;
}
.expand .icon {
  -webkit-transform: rotate(90deg);
  transform: rotate(90deg);
}
.selected {
  color: #1976d2;
}
.fade-enter-active {
  transition: all 0.5s ease 0s;
}
.fade-enter {
  opacity: 0;
}
.fade-enter-to {
  opacity: 1;
}
.fade-leave-to {
  height: 0;
}
</style>
複製代碼

上面的這個組件纔是這個樹型結構重點代碼,用了遞歸的思想來實現這個樹型菜單。

TreeViewDetail.vue

<template>
    <h3>
        這裏是{{currentRoute}}導航詳情
    </h3>
</template>
<script>
export default {
  name: "TreeViewDetail",
  data() {
    return {
      currentRoute: this.$route.path
    };
  },
  watch: {
    //監聽路由,只要路由有變化(路徑,參數等變化)都有執行下面的函數
    $route: {
      handler: function(val, oldVal) {
        this.currentRoute = val.name;
      },
      deep: true
    }
  }
};
</script>
<style scoped>
h3 {
  margin-top: 10px;
  font-weight: normal;
}
</style>
複製代碼

router/index.js

import Vue from 'vue';
import Router from 'vue-router';
import App from '@/components/App';
import TreeViewDetail from '@/components/TreeViewDetail';
Vue.use(Router)
export default new Router({
  linkActiveClass: 'selected',
  routes: [{
      path: '/',
      name: 'App',
      component: App
    },
    {
      path: '/detail/quickstart',
      name: 'quickstart',
      component: TreeViewDetail
    },
    {
      path: '/detail/tutorial',
      name: 'tutorial',
      component: TreeViewDetail
    },
    {
      path: '/detail/toh-pt1',
      name: 'toh-pt1',
      component: TreeViewDetail
    },
    {
      path: '/detail/toh-pt2',
      name: 'toh-pt2',
      component: TreeViewDetail
    },
    {
      path: '/detail/toh-pt3',
      name: 'toh-pt3',
      component: TreeViewDetail
    },
    {
      path: '/detail/toh-pt4',
      name: 'toh-pt4',
      component: TreeViewDetail
    },
    {
      path: '/detail/toh-pt5',
      name: 'toh-pt5',
      component: TreeViewDetail
    },
    {
      path: '/detail/toh-pt6',
      name: 'toh-pt6',
      component: TreeViewDetail
    },
    {
      path: '/detail/architecture',
      name: 'architecture',
      component: TreeViewDetail
    },
    {
      path: '/detail/displaying-data',
      name: 'displaying-data',
      component: TreeViewDetail
    },
    {
      path: '/detail/template-syntax',
      name: 'template-syntax',
      component: TreeViewDetail
    },
    {
      path: '/detail/lifecycle-hooks',
      name: 'lifecycle-hooks',
      component: TreeViewDetail
    },
    {
      path: '/detail/component-interaction',
      name: 'component-interaction',
      component: TreeViewDetail
    },
    {
      path: '/detail/component-styles',
      name: 'component-styles',
      component: TreeViewDetail
    },
    {
      path: '/detail/dynamic-component-loader',
      name: 'dynamic-component-loader',
      component: TreeViewDetail
    },
    {
      path: '/detail/attribute-directives',
      name: 'attribute-directives',
      component: TreeViewDetail
    },
    {
      path: '/detail/structural-directives',
      name: 'structural-directives',
      component: TreeViewDetail
    },
    {
      path: '/detail/pipes',
      name: 'pipes',
      component: TreeViewDetail
    },
    {
      path: '/detail/animations',
      name: 'animations',
      component: TreeViewDetail
    },
    {
      path: '/detail/user-input',
      name: 'user-input',
      component: TreeViewDetail
    },
    {
      path: '/detail/forms',
      name: 'forms',
      component: TreeViewDetail
    },
    {
      path: '/detail/form-validation',
      name: 'form-validation',
      component: TreeViewDetail
    },
    {
      path: '/detail/reactive-forms',
      name: 'reactive-forms',
      component: TreeViewDetail
    },
    {
      path: '/detail/dynamic-form',
      name: 'dynamic-form',
      component: TreeViewDetail
    },
    {
      path: '/detail/bootstrapping',
      name: 'bootstrapping',
      component: TreeViewDetail
    },
    {
      path: '/detail/ngmodule',
      name: 'ngmodule',
      component: TreeViewDetail
    },
    {
      path: '/detail/ngmodule-faq',
      name: 'ngmodule-faq',
      component: TreeViewDetail
    },
    {
      path: '/detail/dependency-injection',
      name: 'dependency-injection',
      component: TreeViewDetail
    },
    {
      path: '/detail/hierarchical-dependency-injection',
      name: 'hierarchical-dependency-injection',
      component: TreeViewDetail
    },
    {
      path: '/detail/dependency-injection-in-action',
      name: 'dependency-injection-in-action',
      component: TreeViewDetail
    },
    {
      path: '/detail/http',
      name: 'http',
      component: TreeViewDetail
    },
    {
      path: '/detail/router',
      name: 'router',
      component: TreeViewDetail
    },
    {
      path: '/detail/testing',
      name: 'testing',
      component: TreeViewDetail
    },
    {
      path: '/detail/cheatsheet',
      name: 'cheatsheet',
      component: TreeViewDetail
    },
    {
      path: '/detail/i18n',
      name: 'i18n',
      component: TreeViewDetail
    },
    {
      path: '/detail/language-service',
      name: 'language-service',
      component: TreeViewDetail
    },
    {
      path: '/detail/security',
      name: 'security',
      component: TreeViewDetail
    },
    {
      path: '/detail/setup',
      name: 'setup',
      component: TreeViewDetail
    },
    {
      path: '/detail/setup-systemjs-anatomy',
      name: 'setup-systemjs-anatomy',
      component: TreeViewDetail
    },
    {
      path: '/detail/browser-support',
      name: 'browser-support',
      component: TreeViewDetail
    },
    {
      path: '/detail/npm-packages',
      name: 'npm-packages',
      component: TreeViewDetail
    },
    {
      path: '/detail/typescript-configuration',
      name: 'typescript-configuration',
      component: TreeViewDetail
    },
    {
      path: '/detail/aot-compiler',
      name: 'aot-compiler',
      component: TreeViewDetail
    },
    {
      path: '/detail/metadata',
      name: 'metadata',
      component: TreeViewDetail
    },
    {
      path: '/detail/deployment',
      name: 'deployment',
      component: TreeViewDetail
    },
    {
      path: '/detail/upgrade',
      name: 'upgrade',
      component: TreeViewDetail
    },
    {
      path: '/detail/ajs-quick-reference',
      name: 'ajs-quick-reference',
      component: TreeViewDetail
    },
    {
      path: '/detail/visual-studio-2015',
      name: 'visual-studio-2015',
      component: TreeViewDetail
    },
    {
      path: '/detail/styleguide',
      name: 'styleguide',
      component: TreeViewDetail
    },
    {
      path: '/detail/glossary',
      name: 'glossary',
      component: TreeViewDetail
    },
    {
      path: '/detail/api',
      name: 'api',
      component: TreeViewDetail
    }
  ]
})
複製代碼

store/module/menusModule.js

let menus = [
  { id: 1, level: 1, name: '快速上手', type: "link", url: "/detail/quickstart" },
  {
    id: 2,
    level: 1,
    name: '教程',
    type: "button",
    isExpanded: false,
    isSelected: false,
    subMenu: [
      { id: 21, level: 2, name: '簡介', type: "link", url: "/detail/tutorial" },
      { id: 22, level: 2, name: '英雄編輯器', type: "link", url: "/detail/toh-pt1" },
      { id: 23, level: 2, name: '主從結構', type: "link", url: "/detail/toh-pt2" },
      { id: 24, level: 2, name: '多個組件', type: "link", url: "/detail/toh-pt3" },
      { id: 25, level: 2, name: '服務', type: "link", url: "/detail/toh-pt4" },
      { id: 26, level: 2, name: '路由', type: "link", url: "/detail/toh-pt5" },
      { id: 27, level: 2, name: 'HTTP', type: "link", url: "/detail/toh-pt6" },
    ]
  },
  {
    id: 3,
    level: 1,
    name: '核心知識',
    type: "button",
    isExpanded: false,
    isSelected: false,
    subMenu: [
      { id: 31, level: 2, name: '架構', type: "link", url: "/detail/architecture" },
      {
        id: 32,
        level: 2,
        name: '模板與數據綁定',
        type: "button",
        isExpanded: false,
        isSelected: false,
        subMenu: [
          { id: 321, level: 3, name: '顯示數據', type: "link", url: "/detail/displaying-data" },
          { id: 322, level: 3, name: '模板語法', type: "link", url: "/detail/template-syntax" },
          { id: 323, level: 3, name: '生命週期鉤子', type: "link", url: "/detail/lifecycle-hooks" },
          { id: 324, level: 3, name: '組件交互', type: "link", url: "/detail/component-interaction" },
          { id: 325, level: 3, name: '組件樣式', type: "link", url: "/detail/component-styles" },
          { id: 326, level: 3, name: '動態組件', type: "link", url: "/detail/dynamic-component-loader" },
          { id: 327, level: 3, name: '屬性型指令', type: "link", url: "/detail/attribute-directives" },
          { id: 328, level: 3, name: '結構型指令', type: "link", url: "/detail/structural-directives" },
          { id: 329, level: 3, name: '管道', type: "link", url: "/detail/pipes" },
          { id: 3210, level: 3, name: '動畫', type: "link", url: "/detail/animations" },
        ]
      },
      {
        id: 33,
        level: 2,
        name: '表單',
        type: "button",
        isExpanded: false,
        isSelected: false,
        subMenu: [
          { name: '用戶輸入', type: "link", url: "/detail/user-input" },
          { name: '模板驅動表單', type: "link", url: "/detail/forms" },
          { name: '表單驗證', type: "link", url: "/detail/form-validation" },
          { name: '響應式表單', type: "link", url: "/detail/reactive-forms" },
          { name: '動態表單', type: "link", url: "/detail/dynamic-form" }
        ]
      },
      { id: 34, level: 2, name: '引用啓動', type: "link", url: "/detail/bootstrapping" },
      {
        id: 35,
        level: 2,
        name: 'NgModules',
        type: "button",
        isExpanded: false,
        isSelected: false,
        subMenu: [
          { id: 341, level: 3, name: 'NgModule', type: "link", url: "/detail/ngmodule" },
          { id: 342, level: 3, name: 'NgModule 常見問題', type: "link", url: "/detail/ngmodule-faq" }
        ]
      },
      {
        id: 36,
        level: 2,
        name: '依賴注入',
        type: "button",
        isExpanded: false,
        isSelected: false,
        subMenu: [
          { id: 361, level: 3, name: '依賴注入', type: "link", url: "/detail/dependency-injection" },
          { id: 362, level: 3, name: '多級注入器', type: "link", url: "/detail/hierarchical-dependency-injection" },
          { id: 363, level: 3, name: 'DI 實例技巧', type: "link", url: "/detail/dependency-injection-in-action" }
        ]
      },
      { id: 37, level: 2, name: 'HttpClient', type: "link", url: "/detail/http" },
      { id: 38, level: 2, name: '路由與導航', type: "link", url: "/detail/router" },
      { id: 39, level: 2, name: '測試', type: "link", url: "/detail/testing" },
      { id: 310, level: 2, name: '速查表', type: "link", url: "/detail/cheatsheet" },
    ]
  },
  {
    id: 4,
    level: 1,
    name: '其它技術',
    type: "button",
    isExpanded: false,
    isSelected: false,
    subMenu: [
      { id: 41, level: 2, name: '國際化(i18n)', type: "link", url: "/detail/i18n" },
      { id: 42, level: 2, name: '語言服務', type: "link", url: "/detail/language-service" },
      { id: 43, level: 2, name: '安全', type: "link", url: "/detail/security" },
      {
        id: 44,
        level: 2,
        name: '環境設置與部署',
        type: "button",
        isExpanded: false,
        isSelected: false,
        subMenu: [
          { id: 441, level: 3, name: '搭建本地開發環境', type: "link", url: "/detail/setup" },
          { id: 442, level: 3, name: '搭建方式剖析', type: "link", url: "/detail/setup-systemjs-anatomy" },
          { id: 443, level: 3, name: '瀏覽器支持', type: "link", url: "/detail/browser-support" },
          { id: 444, level: 3, name: 'npm 包', type: "link", url: "/detail/npm-packages" },
          { id: 445, level: 3, name: 'TypeScript 配置', type: "link", url: "/detail/typescript-configuration" },
          { id: 446, level: 3, name: '預 (AoT) 編譯器', type: "link", url: "/detail/aot-compiler" },
          { id: 447, level: 3, name: '預 (AoT) 編譯器', type: "link", url: "/detail/metadata" },
          { id: 448, level: 3, name: '部署', type: "link", url: "/detail/deployment" }
        ]
      },
      {
        id: 45,
        level: 2,
        name: '升級',
        type: "button",
        isExpanded: false,
        isSelected: false,
        subMenu: [
          { id: 451, level: 3, name: '從 AngularJS 升級', type: "link", url: "/detail/upgrade" },
          { id: 452, level: 3, name: '升級速查表', type: "link", url: "/detail/ajs-quick-reference" }
        ]
      },
      { id: 46, level: 2, name: 'Visual Studio 2015 快速上手', type: "link", url: "/detail/visual-studio-2015" },
      { id: 47, level: 2, name: '風格指南', type: "link", url: "/detail/styleguide" },
      { id: 48, level: 2, name: '詞彙表', type: "link", url: "/detail/glossary" }
    ]
  },
  { id: 5, level: 1, name: 'API 參考手冊', type: "link", url: "/detail/api" }
];
let levelNum = 1;
let startExpand = []; // 保存刷新後當前要展開的菜單項
function setExpand(source, url) {
  let sourceItem = '';
  for (let i = 0; i < source.length; i++) {
    sourceItem = JSON.stringify(source[i]); // 把菜單項轉爲字符串
    if (sourceItem.indexOf(url) > -1) { // 查找當前 URL 所對應的子菜單屬於哪個祖先菜單
      if (source[i].type === 'button') { // 導航菜單爲按鈕
        source[i].isSelected = true; // 設置選中高亮
        source[i].isExpanded = true; // 設置爲展開
        startExpand.push(source[i]);
        // 遞歸下一級菜單,以此類推
        setExpand(source[i].subMenu, url);
      }
      break;
    }
  }
}
const state = {
  menus,
  levelNum
};
const mutations = {
  findParents(state, payload) {
    if (payload.menu.type === "button") {
      payload.menu.isExpanded = !payload.menu.isExpanded;
    } else if (payload.menu.type === "link") {
      if (startExpand.length > 0) {
        for (let i = 0; i < startExpand.length; i++) {
          startExpand[i].isSelected = false;
        }
      }
      startExpand = []; // 清空展開菜單記錄項
      setExpand(state.menus, payload.menu.url);
    };
  },
  firstInit(state, payload) {
    setExpand(state.menus, payload.url);
  }
}
export default {
  state,
  mutations
};
複製代碼

在使用狀態管理時,咱們必定要記住,一旦數據寫到了 state 中時,就不能再添加其它屬性了,什麼時間?就拿上面的 menus 數據來講,好比,原本菜單數據中沒有 isExpanded 這個字段的,而後你在 mutations 的方法中給 menus 對象添加了一個 isExpanded 屬性,但你會發現屬性是不會被狀態管理追蹤到的,因此咱們一開始就給這個數據添加了 isExpanded 和 isSelected 。

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import menusModule from './module/menusModule'
Vue.use(Vuex);
const store = new Vuex.Store({
  modules: {
    menusModule
  }
})
export default store;
複製代碼

上面這個例子在使用狀態管理時,把菜單的相關配置封裝成模塊,而後再引入。若是把狀態管理寫成模塊的形式的話,在調用這個模塊中的狀態時就須要注意了,寫法能夠參數示例中的代碼。

上面這個例子能夠直接用到本身的項目中,只要你理解了其中的思想,其餘的都不是問題。Vue 實現樹形菜單功能模塊之旅只能帶你到這裏了。

相關文章
相關標籤/搜索