Vue2 实现树形菜单(多级菜单)功能模块
结构示意图
- ├── 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 # 菜单模块
这个多级菜单实现的功能如下:
- 1、可展示多级菜单,理论上可以展无限级菜单
- 2、当前菜单高亮功能
- 3、刷新后依然会自动定位到上一次点击的菜单,即使这个是子菜单,并且父菜单会自动展开
- 4、子菜单的显示隐藏有收起、展开,同时带有淡入效果
这个例子用到的知识点:路由、状态管理、组件。
状态管理安装:
- npm install --save vuex
更多关于 vuex 的介绍可以看官方文档:https://vuex.vuejs.org/zh-cn/。
我们先来看看效果演示图:

程序员是用代码来沟通的,所以费话不多说,直接上码:
index.html
- <!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.js
- 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 实现树形菜单功能模块之旅只能带你到这里了。
 
                    
                 
 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号