Vue2 实现树形菜单(多级菜单)功能模块

结构示意图
  1. ├── index.html
  2. ├── main.js
  3. ├── router
  4. └── index.js # 路由配置文件
  5. ├── components # 组件目录
  6. ├── App.vue # 根组件
  7. ├── Home.vue # 大的框架结构组件
  8. ├── TreeView.vue
  9. ├── TreeViewItem.vue
  10. └── TreeDetail.vue
  11. ├── store
  12. ├── index.js # 我们组装模块并导出 store 的地方
  13. ├── modules # 模块目录
  14. └── menusModule.js # 菜单模块

这个多级菜单实现的功能如下:

  • 1、可展示多级菜单,理论上可以展无限级菜单
  • 2、当前菜单高亮功能
  • 3、刷新后依然会自动定位到上一次点击的菜单,即使这个是子菜单,并且父菜单会自动展开
  • 4、子菜单的显示隐藏有收起、展开,同时带有淡入效果

这个例子用到的知识点:路由、状态管理、组件。

状态管理安装:

  1. npm install --save vuex

更多关于 vuex 的介绍可以看官方文档:https://vuex.vuejs.org/zh-cn/。

我们先来看看效果演示图:

程序员是用代码来沟通的,所以费话不多说,直接上码:

index.html
  1. charset="utf-8">
  2. name="viewport" content="width=device-width,initial-scale=1.0">
  3. <span class="pln">Vue 实现树形菜单(多级菜单)功能模块- 云库网<span class="tag">
  4. id="app">
  • main.js
    1. import Vue from 'vue'
    2. import App from './components/App'
    3. import router from './router'
    4. import store from './store/index'
    5. Vue.config.productionTip = false
    6. /* eslint-disable no-new */
    7. new Vue({
    8. el: '#app',
    9. router,
    10. store,
    11. components: {
    12. App
    13. },
    14. template: ''
    15. })

    在 main.js 中引入 路由和状态管理配置

    App.vue
    Home.vue
    1. scoped>
    2. .side-bar {
    3. width: 300px;
    4. height: 100%;
    5. overflow-y: auto;
    6. overflow-x: hidden;
    7. font-size: 14px;
    8. position: absolute;
    9. top: 0;
    10. left: 0;
    11. }
    12. .continer {
    13. padding-left: 320px;
    14. }

    这个 Home.vue 主要是用来完成页面的大框架结构。

    TreeView.vue
    1. scoped>
    2. .tree-view-menu {
    3. width: 300px;
    4. height: 100%;
    5. overflow-y: auto;
    6. overflow-x: hidden;
    7. }
    8. .tree-view-menu::-webkit-scrollbar {
    9. height: 6px;
    10. width: 6px;
    11. }
    12. .tree-view-menu::-webkit-scrollbar-trac {
    13. -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
    14. box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
    15. }
    16. .tree-view-menu::-webkit-scrollbar-thumb {
    17. background-color: #6e6e6e;
    18. outline: 1px solid #333;
    19. }
    20. .tree-view-menu::-webkit-scrollbar {
    21. height: 4px;
    22. width: 4px;
    23. }
    24. .tree-view-menu::-webkit-scrollbar-track {
    25. -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
    26. box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
    27. }
    28. .tree-view-menu::-webkit-scrollbar-thumb {
    29. background-color: #6e6e6e;
    30. outline: 1px solid #708090;
    31. }

    这个组件也非常地简单,拿到菜单数据,传给子组件,并把菜单的滚动条样式修改了下。

    TreeViewItem.vue
    1. scoped>
    2. a {
    3. text-decoration: none;
    4. color: #333;
    5. }
    6. .link,
    7. .button {
    8. display: block;
    9. padding: 10px 15px;
    10. transition: background-color 0.2s ease-in-out 0s, color 0.3s ease-in-out 0.1s;
    11. -moz-user-select: none;
    12. -webkit-user-select: none;
    13. -ms-user-select: none;
    14. -khtml-user-select: none;
    15. user-select: none;
    16. }
    17. .button {
    18. position: relative;
    19. }
    20. .link:hover,
    21. .button:hover {
    22. color: #1976d2;
    23. background-color: #eee;
    24. cursor: pointer;
    25. }
    26. .icon {
    27. position: absolute;
    28. right: 0;
    29. display: inline-block;
    30. height: 24px;
    31. width: 24px;
    32. fill: currentColor;
    33. transition: -webkit-transform 0.15s;
    34. transition: transform 0.15s;
    35. transition: transform 0.15s, -webkit-transform 0.15s;
    36. transition-timing-function: ease-in-out;
    37. }
    38. .heading-children {
    39. padding-left: 14px;
    40. overflow: hidden;
    41. }
    42. .expand {
    43. display: block;
    44. }
    45. .collapsed {
    46. display: none;
    47. }
    48. .expand .icon {
    49. -webkit-transform: rotate(90deg);
    50. transform: rotate(90deg);
    51. }
    52. .selected {
    53. color: #1976d2;
    54. }
    55. .fade-enter-active {
    56. transition: all 0.5s ease 0s;
    57. }
    58. .fade-enter {
    59. opacity: 0;
    60. }
    61. .fade-enter-to {
    62. opacity: 1;
    63. }
    64. .fade-leave-to {
    65. height: 0;
    66. }

    上面的这个组件才是这个树型结构重点代码,用了递归的思想来实现这个树型菜单。

    TreeViewDetail.vue
    1. scoped>
    2. h3 {
    3. margin-top: 10px;
    4. font-weight: normal;
    5. }
    router/index.js
    1. import Vue from 'vue';
    2. import Router from 'vue-router';
    3. import App from '@/components/App';
    4. import TreeViewDetail from '@/components/TreeViewDetail';
    5. Vue.use(Router)
    6. export default new Router({
    7. linkActiveClass: 'selected',
    8. routes: [{
    9. path: '/',
    10. name: 'App',
    11. component: App
    12. },
    13. {
    14. path: '/detail/quickstart',
    15. name: 'quickstart',
    16. component: TreeViewDetail
    17. },
    18. {
    19. path: '/detail/tutorial',
    20. name: 'tutorial',
    21. component: TreeViewDetail
    22. },
    23. {
    24. path: '/detail/toh-pt1',
    25. name: 'toh-pt1',
    26. component: TreeViewDetail
    27. },
    28. {
    29. path: '/detail/toh-pt2',
    30. name: 'toh-pt2',
    31. component: TreeViewDetail
    32. },
    33. {
    34. path: '/detail/toh-pt3',
    35. name: 'toh-pt3',
    36. component: TreeViewDetail
    37. },
    38. {
    39. path: '/detail/toh-pt4',
    40. name: 'toh-pt4',
    41. component: TreeViewDetail
    42. },
    43. {
    44. path: '/detail/toh-pt5',
    45. name: 'toh-pt5',
    46. component: TreeViewDetail
    47. },
    48. {
    49. path: '/detail/toh-pt6',
    50. name: 'toh-pt6',
    51. component: TreeViewDetail
    52. },
    53. {
    54. path: '/detail/architecture',
    55. name: 'architecture',
    56. component: TreeViewDetail
    57. },
    58. {
    59. path: '/detail/displaying-data',
    60. name: 'displaying-data',
    61. component: TreeViewDetail
    62. },
    63. {
    64. path: '/detail/template-syntax',
    65. name: 'template-syntax',
    66. component: TreeViewDetail
    67. },
    68. {
    69. path: '/detail/lifecycle-hooks',
    70. name: 'lifecycle-hooks',
    71. component: TreeViewDetail
    72. },
    73. {
    74. path: '/detail/component-interaction',
    75. name: 'component-interaction',
    76. component: TreeViewDetail
    77. },
    78. {
    79. path: '/detail/component-styles',
    80. name: 'component-styles',
    81. component: TreeViewDetail
    82. },
    83. {
    84. path: '/detail/dynamic-component-loader',
    85. name: 'dynamic-component-loader',
    86. component: TreeViewDetail
    87. },
    88. {
    89. path: '/detail/attribute-directives',
    90. name: 'attribute-directives',
    91. component: TreeViewDetail
    92. },
    93. {
    94. path: '/detail/structural-directives',
    95. name: 'structural-directives',
    96. component: TreeViewDetail
    97. },
    98. {
    99. path: '/detail/pipes',
    100. name: 'pipes',
    101. component: TreeViewDetail
    102. },
    103. {
    104. path: '/detail/animations',
    105. name: 'animations',
    106. component: TreeViewDetail
    107. },
    108. {
    109. path: '/detail/user-input',
    110. name: 'user-input',
    111. component: TreeViewDetail
    112. },
    113. {
    114. path: '/detail/forms',
    115. name: 'forms',
    116. component: TreeViewDetail
    117. },
    118. {
    119. path: '/detail/form-validation',
    120. name: 'form-validation',
    121. component: TreeViewDetail
    122. },
    123. {
    124. path: '/detail/reactive-forms',
    125. name: 'reactive-forms',
    126. component: TreeViewDetail
    127. },
    128. {
    129. path: '/detail/dynamic-form',
    130. name: 'dynamic-form',
    131. component: TreeViewDetail
    132. },
    133. {
    134. path: '/detail/bootstrapping',
    135. name: 'bootstrapping',
    136. component: TreeViewDetail
    137. },
    138. {
    139. path: '/detail/ngmodule',
    140. name: 'ngmodule',
    141. component: TreeViewDetail
    142. },
    143. {
    144. path: '/detail/ngmodule-faq',
    145. name: 'ngmodule-faq',
    146. component: TreeViewDetail
    147. },
    148. {
    149. path: '/detail/dependency-injection',
    150. name: 'dependency-injection',
    151. component: TreeViewDetail
    152. },
    153. {
    154. path: '/detail/hierarchical-dependency-injection',
    155. name: 'hierarchical-dependency-injection',
    156. component: TreeViewDetail
    157. },
    158. {
    159. path: '/detail/dependency-injection-in-action',
    160. name: 'dependency-injection-in-action',
    161. component: TreeViewDetail
    162. },
    163. {
    164. path: '/detail/http',
    165. name: 'http',
    166. component: TreeViewDetail
    167. },
    168. {
    169. path: '/detail/router',
    170. name: 'router',
    171. component: TreeViewDetail
    172. },
    173. {
    174. path: '/detail/testing',
    175. name: 'testing',
    176. component: TreeViewDetail
    177. },
    178. {
    179. path: '/detail/cheatsheet',
    180. name: 'cheatsheet',
    181. component: TreeViewDetail
    182. },
    183. {
    184. path: '/detail/i18n',
    185. name: 'i18n',
    186. component: TreeViewDetail
    187. },
    188. {
    189. path: '/detail/language-service',
    190. name: 'language-service',
    191. component: TreeViewDetail
    192. },
    193. {
    194. path: '/detail/security',
    195. name: 'security',
    196. component: TreeViewDetail
    197. },
    198. {
    199. path: '/detail/setup',
    200. name: 'setup',
    201. component: TreeViewDetail
    202. },
    203. {
    204. path: '/detail/setup-systemjs-anatomy',
    205. name: 'setup-systemjs-anatomy',
    206. component: TreeViewDetail
    207. },
    208. {
    209. path: '/detail/browser-support',
    210. name: 'browser-support',
    211. component: TreeViewDetail
    212. },
    213. {
    214. path: '/detail/npm-packages',
    215. name: 'npm-packages',
    216. component: TreeViewDetail
    217. },
    218. {
    219. path: '/detail/typescript-configuration',
    220. name: 'typescript-configuration',
    221. component: TreeViewDetail
    222. },
    223. {
    224. path: '/detail/aot-compiler',
    225. name: 'aot-compiler',
    226. component: TreeViewDetail
    227. },
    228. {
    229. path: '/detail/metadata',
    230. name: 'metadata',
    231. component: TreeViewDetail
    232. },
    233. {
    234. path: '/detail/deployment',
    235. name: 'deployment',
    236. component: TreeViewDetail
    237. },
    238. {
    239. path: '/detail/upgrade',
    240. name: 'upgrade',
    241. component: TreeViewDetail
    242. },
    243. {
    244. path: '/detail/ajs-quick-reference',
    245. name: 'ajs-quick-reference',
    246. component: TreeViewDetail
    247. },
    248. {
    249. path: '/detail/visual-studio-2015',
    250. name: 'visual-studio-2015',
    251. component: TreeViewDetail
    252. },
    253. {
    254. path: '/detail/styleguide',
    255. name: 'styleguide',
    256. component: TreeViewDetail
    257. },
    258. {
    259. path: '/detail/glossary',
    260. name: 'glossary',
    261. component: TreeViewDetail
    262. },
    263. {
    264. path: '/detail/api',
    265. name: 'api',
    266. component: TreeViewDetail
    267. }
    268. ]
    269. })
    store/module/menusModule.js
    1. let menus = [
    2. { id: 1, level: 1, name: '快速上手', type: "link", url: "/detail/quickstart" },
    3. {
    4. id: 2,
    5. level: 1,
    6. name: '教程',
    7. type: "button",
    8. isExpanded: false,
    9. isSelected: false,
    10. subMenu: [
    11. { id: 21, level: 2, name: '简介', type: "link", url: "/detail/tutorial" },
    12. { id: 22, level: 2, name: '英雄编辑器', type: "link", url: "/detail/toh-pt1" },
    13. { id: 23, level: 2, name: '主从结构', type: "link", url: "/detail/toh-pt2" },
    14. { id: 24, level: 2, name: '多个组件', type: "link", url: "/detail/toh-pt3" },
    15. { id: 25, level: 2, name: '服务', type: "link", url: "/detail/toh-pt4" },
    16. { id: 26, level: 2, name: '路由', type: "link", url: "/detail/toh-pt5" },
    17. { id: 27, level: 2, name: 'HTTP', type: "link", url: "/detail/toh-pt6" },
    18. ]
    19. },
    20. {
    21. id: 3,
    22. level: 1,
    23. name: '核心知识',
    24. type: "button",
    25. isExpanded: false,
    26. isSelected: false,
    27. subMenu: [
    28. { id: 31, level: 2, name: '架构', type: "link", url: "/detail/architecture" },
    29. {
    30. id: 32,
    31. level: 2,
    32. name: '模板与数据绑定',
    33. type: "button",
    34. isExpanded: false,
    35. isSelected: false,
    36. subMenu: [
    37. { id: 321, level: 3, name: '显示数据', type: "link", url: "/detail/displaying-data" },
    38. { id: 322, level: 3, name: '模板语法', type: "link", url: "/detail/template-syntax" },
    39. { id: 323, level: 3, name: '生命周期钩子', type: "link", url: "/detail/lifecycle-hooks" },
    40. { id: 324, level: 3, name: '组件交互', type: "link", url: "/detail/component-interaction" },
    41. { id: 325, level: 3, name: '组件样式', type: "link", url: "/detail/component-styles" },
    42. { id: 326, level: 3, name: '动态组件', type: "link", url: "/detail/dynamic-component-loader" },
    43. { id: 327, level: 3, name: '属性型指令', type: "link", url: "/detail/attribute-directives" },
    44. { id: 328, level: 3, name: '结构型指令', type: "link", url: "/detail/structural-directives" },
    45. { id: 329, level: 3, name: '管道', type: "link", url: "/detail/pipes" },
    46. { id: 3210, level: 3, name: '动画', type: "link", url: "/detail/animations" },
    47. ]
    48. },
    49. {
    50. id: 33,
    51. level: 2,
    52. name: '表单',
    53. type: "button",
    54. isExpanded: false,
    55. isSelected: false,
    56. subMenu: [
    57. { name: '用户输入', type: "link", url: "/detail/user-input" },
    58. { name: '模板驱动表单', type: "link", url: "/detail/forms" },
    59. { name: '表单验证', type: "link", url: "/detail/form-validation" },
    60. { name: '响应式表单', type: "link", url: "/detail/reactive-forms" },
    61. { name: '动态表单', type: "link", url: "/detail/dynamic-form" }
    62. ]
    63. },
    64. { id: 34, level: 2, name: '引用启动', type: "link", url: "/detail/bootstrapping" },
    65. {
    66. id: 35,
    67. level: 2,
    68. name: 'NgModules',
    69. type: "button",
    70. isExpanded: false,
    71. isSelected: false,
    72. subMenu: [
    73. { id: 341, level: 3, name: 'NgModule', type: "link", url: "/detail/ngmodule" },
    74. { id: 342, level: 3, name: 'NgModule 常见问题', type: "link", url: "/detail/ngmodule-faq" }
    75. ]
    76. },
    77. {
    78. id: 36,
    79. level: 2,
    80. name: '依赖注入',
    81. type: "button",
    82. isExpanded: false,
    83. isSelected: false,
    84. subMenu: [
    85. { id: 361, level: 3, name: '依赖注入', type: "link", url: "/detail/dependency-injection" },
    86. { id: 362, level: 3, name: '多级注入器', type: "link", url: "/detail/hierarchical-dependency-injection" },
    87. { id: 363, level: 3, name: 'DI 实例技巧', type: "link", url: "/detail/dependency-injection-in-action" }
    88. ]
    89. },
    90. { id: 37, level: 2, name: 'HttpClient', type: "link", url: "/detail/http" },
    91. { id: 38, level: 2, name: '路由与导航', type: "link", url: "/detail/router" },
    92. { id: 39, level: 2, name: '测试', type: "link", url: "/detail/testing" },
    93. { id: 310, level: 2, name: '速查表', type: "link", url: "/detail/cheatsheet" },
    94. ]
    95. },
    96. {
    97. id: 4,
    98. level: 1,
    99. name: '其它技术',
    100. type: "button",
    101. isExpanded: false,
    102. isSelected: false,
    103. subMenu: [
    104. { id: 41, level: 2, name: '国际化(i18n)', type: "link", url: "/detail/i18n" },
    105. { id: 42, level: 2, name: '语言服务', type: "link", url: "/detail/language-service" },
    106. { id: 43, level: 2, name: '安全', type: "link", url: "/detail/security" },
    107. {
    108. id: 44,
    109. level: 2,
    110. name: '环境设置与部署',
    111. type: "button",
    112. isExpanded: false,
    113. isSelected: false,
    114. subMenu: [
    115. { id: 441, level: 3, name: '搭建本地开发环境', type: "link", url: "/detail/setup" },
    116. { id: 442, level: 3, name: '搭建方式剖析', type: "link", url: "/detail/setup-systemjs-anatomy" },
    117. { id: 443, level: 3, name: '浏览器支持', type: "link", url: "/detail/browser-support" },
    118. { id: 444, level: 3, name: 'npm 包', type: "link", url: "/detail/npm-packages" },
    119. { id: 445, level: 3, name: 'TypeScript 配置', type: "link", url: "/detail/typescript-configuration" },
    120. { id: 446, level: 3, name: '预 (AoT) 编译器', type: "link", url: "/detail/aot-compiler" },
    121. { id: 447, level: 3, name: '预 (AoT) 编译器', type: "link", url: "/detail/metadata" },
    122. { id: 448, level: 3, name: '部署', type: "link", url: "/detail/deployment" }
    123. ]
    124. },
    125. {
    126. id: 45,
    127. level: 2,
    128. name: '升级',
    129. type: "button",
    130. isExpanded: false,
    131. isSelected: false,
    132. subMenu: [
    133. { id: 451, level: 3, name: '从 AngularJS 升级', type: "link", url: "/detail/upgrade" },
    134. { id: 452, level: 3, name: '升级速查表', type: "link", url: "/detail/ajs-quick-reference" }
    135. ]
    136. },
    137. { id: 46, level: 2, name: 'Visual Studio 2015 快速上手', type: "link", url: "/detail/visual-studio-2015" },
    138. { id: 47, level: 2, name: '风格指南', type: "link", url: "/detail/styleguide" },
    139. { id: 48, level: 2, name: '词汇表', type: "link", url: "/detail/glossary" }
    140. ]
    141. },
    142. { id: 5, level: 1, name: 'API 参考手册', type: "link", url: "/detail/api" }
    143. ];
    144. let levelNum = 1;
    145. let startExpand = []; // 保存刷新后当前要展开的菜单项
    146. function setExpand(source, url) {
    147. let sourceItem = '';
    148. for (let i = 0; i < source.length; i++) {
    149. sourceItem = JSON.stringify(source[i]); // 把菜单项转为字符串
    150. if (sourceItem.indexOf(url) > -1) { // 查找当前 URL 所对应的子菜单属于哪一个祖先菜单
    151. if (source[i].type === 'button') { // 导航菜单为按钮
    152. source[i].isSelected = true; // 设置选中高亮
    153. source[i].isExpanded = true; // 设置为展开
    154. startExpand.push(source[i]);
    155. // 递归下一级菜单,以此类推
    156. setExpand(source[i].subMenu, url);
    157. }
    158. break;
    159. }
    160. }
    161. }
    162. const state = {
    163. menus,
    164. levelNum
    165. };
    166. const mutations = {
    167. findParents(state, payload) {
    168. if (payload.menu.type === "button") {
    169. payload.menu.isExpanded = !payload.menu.isExpanded;
    170. } else if (payload.menu.type === "link") {
    171. if (startExpand.length > 0) {
    172. for (let i = 0; i < startExpand.length; i++) {
    173. startExpand[i].isSelected = false;
    174. }
    175. }
    176. startExpand = []; // 清空展开菜单记录项
    177. setExpand(state.menus, payload.menu.url);
    178. };
    179. },
    180. firstInit(state, payload) {
    181. setExpand(state.menus, payload.url);
    182. }
    183. }
    184. export default {
    185. state,
    186. mutations
    187. };

    在使用状态管理时,我们一定要记住,一旦数据写到了 state 中时,就不能再添加其它属性了,什么时间?就拿上面的 menus 数据来说,比如,本来菜单数据中没有 isExpanded 这个字段的,然后你在 mutations 的方法中给 menus 对象添加了一个 isExpanded 属性,但你会发现属性是不会被状态管理追踪到的,所以我们一开始就给这个数据添加了 isExpanded 和 isSelected 。

    store/index.js
    1. import Vue from 'vue'
    2. import Vuex from 'vuex'
    3. import menusModule from './module/menusModule'
    4. Vue.use(Vuex);
    5. const store = new Vuex.Store({
    6. modules: {
    7. menusModule
    8. }
    9. })
    10. export default store;

    上面这个例子在使用状态管理时,把菜单的相关配置封装成模块,然后再引入。如果把状态管理写成模块的形式的话,在调用这个模块中的状态时就需要注意了,写法可以参数示例中的代码。

    上面这个例子可以直接用到自己的项目中,只要你理解了其中的思想,其他的都不是问题。Vue 实现树形菜单功能模块之旅只能带你到这里了。

    你可能感兴趣的:(Vue2 实现树形菜单(多级菜单)功能模块)