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

时间:2022-04-15 04:48:23
结构示意图
  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/

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

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

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

index.html
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width,initial-scale=1.0">
  6. <title>Vue 实现树形菜单(多级菜单)功能模块- 云库网</title>
  7. </head>
  8. <body>
  9. <div id="app"></div>
  10. </body>
  11. </html>
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: '<App/>'
  15. })

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

App.vue
  1. <template>
  2. <div id="app">
  3. <Home></Home>
  4. </div>
  5. </template>
  6. <script>
  7. import Home from "./Home";
  8. export default {
  9. components: {
  10. Home
  11. },
  12. name: "App"
  13. };
  14. </script>
  15. <style>
  16. * {
  17. padding: 0;
  18. margin: 0;
  19. }
  20. #app {
  21. font-family: "Avenir", Helvetica, Arial, sans-serif;
  22. -webkit-font-smoothing: antialiased;
  23. -moz-osx-font-smoothing: grayscale;
  24. color: #2c3e50;
  25. }
  26. html,
  27. body,
  28. #app,
  29. .home {
  30. height: 100%;
  31. }
  32. html,
  33. body {
  34. overflow: hidden;
  35. }
  36. </style>
Home.vue
  1. <template>
  2. <div class="home">
  3. <div class="side-bar">
  4. <Tree-view></Tree-view>
  5. </div>
  6. <div class="continer">
  7. <router-view></router-view>
  8. </div>
  9. </div>
  10. </template>
  11. <script>
  12. import TreeView from "./TreeView";
  13. export default {
  14. components: {
  15. TreeView
  16. },
  17. name: "Home"
  18. };
  19. </script>
  20. <style scoped>
  21. .side-bar {
  22. width: 300px;
  23. height: 100%;
  24. overflow-y: auto;
  25. overflow-x: hidden;
  26. font-size: 14px;
  27. position: absolute;
  28. top: 0;
  29. left: 0;
  30. }
  31. .continer {
  32. padding-left: 320px;
  33. }
  34. </style>

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

TreeView.vue
  1. <template>
  2. <div class="tree-view-menu">
  3. <Tree-view-item :menus='menus'></Tree-view-item>
  4. </div>
  5. </template>
  6. <script>
  7. import TreeViewItem from "./TreeViewItem";
  8. const menusData = [];
  9. export default {
  10. components: {
  11. TreeViewItem
  12. },
  13. name: "TreeViewMenu",
  14. data() {
  15. return {
  16. menus: this.$store.state.menusModule.menus
  17. };
  18. }
  19. };
  20. </script>
  21. <style scoped>
  22. .tree-view-menu {
  23. width: 300px;
  24. height: 100%;
  25. overflow-y: auto;
  26. overflow-x: hidden;
  27. }
  28. .tree-view-menu::-webkit-scrollbar {
  29. height: 6px;
  30. width: 6px;
  31. }
  32. .tree-view-menu::-webkit-scrollbar-trac {
  33. -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
  34. box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
  35. }
  36. .tree-view-menu::-webkit-scrollbar-thumb {
  37. background-color: #6e6e6e;
  38. outline: 1px solid #333;
  39. }
  40. .tree-view-menu::-webkit-scrollbar {
  41. height: 4px;
  42. width: 4px;
  43. }
  44. .tree-view-menu::-webkit-scrollbar-track {
  45. -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
  46. box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
  47. }
  48. .tree-view-menu::-webkit-scrollbar-thumb {
  49. background-color: #6e6e6e;
  50. outline: 1px solid #708090;
  51. }
  52. </style>

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

TreeViewItem.vue
  1. <template>
  2. <div class="tree-view-item">
  3. <div class="level" :class="'level-'+ menu.level" v-for="menu in menus" :key="menu.id">
  4. <div v-if="menu.type === 'link'">
  5. <router-link class="link" v-bind:to="menu.url" @click.native="toggle(menu)">{{menu.name}}</router-link>
  6. </div>
  7. <div v-if="menu.type === 'button'">
  8. <div class="button heading" :class="{selected: menu.isSelected,expand:menu.isExpanded}" @click="toggle(menu)">
  9. {{menu.name}}
  10. <div class="icon">
  11. <svg xmlns="http://www.w3.org/2000/svg" focusable="false" viewBox="0 0 24 24">
  12. <path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z "></path>
  13. </svg>
  14. </div>
  15. </div>
  16. <transition name="fade">
  17. <div class="heading-children" v-show="menu.isExpanded" v-if="menu.subMenu">
  18. <Tree-view-item :menus='menu.subMenu'></Tree-view-item>
  19. </div>
  20. </transition>
  21. </div>
  22. </div>
  23. </div>
  24. </template>
  25. <script>
  26. export default {
  27. name: "TreeViewItem",
  28. props: ["menus"],
  29. created() {
  30. this.$store.commit("firstInit", { url: this.$route.path });
  31. },
  32. methods: {
  33. toggle(menu) {
  34. this.$store.commit("findParents", { menu });
  35. }
  36. }
  37. };
  38. </script>
  39. <style scoped>
  40. a {
  41. text-decoration: none;
  42. color: #333;
  43. }
  44. .link,
  45. .button {
  46. display: block;
  47. padding: 10px 15px;
  48. transition: background-color 0.2s ease-in-out 0s, color 0.3s ease-in-out 0.1s;
  49. -moz-user-select: none;
  50. -webkit-user-select: none;
  51. -ms-user-select: none;
  52. -khtml-user-select: none;
  53. user-select: none;
  54. }
  55. .button {
  56. position: relative;
  57. }
  58. .link:hover,
  59. .button:hover {
  60. color: #1976d2;
  61. background-color: #eee;
  62. cursor: pointer;
  63. }
  64. .icon {
  65. position: absolute;
  66. right: 0;
  67. display: inline-block;
  68. height: 24px;
  69. width: 24px;
  70. fill: currentColor;
  71. transition: -webkit-transform 0.15s;
  72. transition: transform 0.15s;
  73. transition: transform 0.15s, -webkit-transform 0.15s;
  74. transition-timing-function: ease-in-out;
  75. }
  76. .heading-children {
  77. padding-left: 14px;
  78. overflow: hidden;
  79. }
  80. .expand {
  81. display: block;
  82. }
  83. .collapsed {
  84. display: none;
  85. }
  86. .expand .icon {
  87. -webkit-transform: rotate(90deg);
  88. transform: rotate(90deg);
  89. }
  90. .selected {
  91. color: #1976d2;
  92. }
  93. .fade-enter-active {
  94. transition: all 0.5s ease 0s;
  95. }
  96. .fade-enter {
  97. opacity: 0;
  98. }
  99. .fade-enter-to {
  100. opacity: 1;
  101. }
  102. .fade-leave-to {
  103. height: 0;
  104. }
  105. </style>

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

TreeViewDetail.vue
  1. <template>
  2. <h3>
  3. 这里是{{currentRoute}}导航详情
  4. </h3>
  5. </template>
  6. <script>
  7. export default {
  8. name: "TreeViewDetail",
  9. data() {
  10. return {
  11. currentRoute: this.$route.path
  12. };
  13. },
  14. watch: {
  15. //监听路由,只要路由有变化(路径,参数等变化)都有执行下面的函数
  16. $route: {
  17. handler: function(val, oldVal) {
  18. this.currentRoute = val.name;
  19. },
  20. deep: true
  21. }
  22. }
  23. };
  24. </script>
  25. <style scoped>
  26. h3 {
  27. margin-top: 10px;
  28. font-weight: normal;
  29. }
  30. </style>
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 实现树形菜单功能模块之旅只能带你到这里了。