Vue 实现展开折叠效果

一、目标实现效果

image.png
除了使用jQuery的方式实现上述效果,同样可以在Vue实现,下面是解决办法:

二、步骤

1.创建collapse.js文件

  1. const elTransition =
  2. "0.3s height ease-in-out, 0.3s padding-top ease-in-out, 0.3s padding-bottom ease-in-out";
  3. const Transition = {
  4. "before-enter"(el) {
  5. el.style.transition = elTransition;
  6. if (!el.dataset) el.dataset = {};
  7. el.dataset.oldPaddingTop = el.style.paddingTop;
  8. el.dataset.oldPaddingBottom = el.style.paddingBottom;
  9. el.style.height = 0;
  10. el.style.paddingTop = 0;
  11. el.style.paddingBottom = 0;
  12. },
  13. enter(el) {
  14. el.dataset.oldOverflow = el.style.overflow;
  15. if (el.scrollHeight !== 0) {
  16. el.style.height = el.scrollHeight + "px";
  17. el.style.paddingTop = el.dataset.oldPaddingTop;
  18. el.style.paddingBottom = el.dataset.oldPaddingBottom;
  19. } else {
  20. el.style.height = "";
  21. el.style.paddingTop = el.dataset.oldPaddingTop;
  22. el.style.paddingBottom = el.dataset.oldPaddingBottom;
  23. }
  24. el.style.overflow = "hidden";
  25. },
  26. "after-enter"(el) {
  27. el.style.transition = "";
  28. el.style.height = "";
  29. el.style.overflow = el.dataset.oldOverflow;
  30. },
  31. "before-leave"(el) {
  32. if (!el.dataset) el.dataset = {};
  33. el.dataset.oldPaddingTop = el.style.paddingTop;
  34. el.dataset.oldPaddingBottom = el.style.paddingBottom;
  35. el.dataset.oldOverflow = el.style.overflow;
  36. el.style.height = el.scrollHeight + "px";
  37. el.style.overflow = "hidden";
  38. },
  39. leave(el) {
  40. if (el.scrollHeight !== 0) {
  41. el.style.transition = elTransition;
  42. el.style.height = 0;
  43. el.style.paddingTop = 0;
  44. el.style.paddingBottom = 0;
  45. }
  46. },
  47. "after-leave"(el) {
  48. el.style.transition = "";
  49. el.style.height = "";
  50. el.style.overflow = el.dataset.oldOverflow;
  51. el.style.paddingTop = el.dataset.oldPaddingTop;
  52. el.style.paddingBottom = el.dataset.oldPaddingBottom;
  53. }
  54. };
  55. export default {
  56. name: "collapseTransition",
  57. functional: true,
  58. render(h, { children }) {
  59. const data = {
  60. on: Transition
  61. };
  62. return h("transition", data, children);
  63. }
  64. };

2.再次封装组件,提供slot插槽

  1. <template>
  2. <view>
  3. <collapse>
  4. <div class="collapse-wrap" v-show="isActive">
  5. <!-- @slot default -->
  6. <slot></slot>
  7. </div>
  8. </collapse>
  9. </view>
  10. </template>
  11. <script>
  12. /**
  13. * @description 折叠收缩组件
  14. * @created by lishengsong
  15. * @date 2020-04-24
  16. */
  17. import collapse from '@/plugins/uiTools/collapse.js'
  18. export default {
  19. components: {
  20. collapse
  21. },
  22. props: {
  23. isActive: {
  24. type: Boolean,
  25. default: true
  26. }
  27. },
  28. onLoad() {
  29. console.log(collapse);
  30. },
  31. watch: {
  32. isActive(val) {
  33. return val
  34. }
  35. }
  36. }
  37. </script>
  38. <style>
  39. </style>

递归组件-树形控件预览及问题

交互特效001 - 图2

在编写树形组件时遇到的问题:
**

  • 组件如何才能递归调用?
  • 递归组件点击事件如何传递?

一、树形控件基本结构及样式

  1. <template>
  2. <ul class="vue-tree">
  3. <li class="tree-item">
  4. <div class="tree-content"><!--节点内容-->
  5. <div class="expand-arrow"></div><!--展开或收缩节点按钮-->
  6. <div class="tree-label">小学</div><!--节点文本内容-->
  7. </div>
  8. <ul class="sub-tree"><!--子节点-->
  9. <li class="tree-item expand">
  10. <div class="tree-content">
  11. <div class="expand-arrow"></div>
  12. <div class="tree-label">语文</div>
  13. </div>
  14. </li>
  15. <li class="tree-item">
  16. <div class="tree-content">
  17. <div class="expand-arrow"></div>
  18. <div class="tree-label">数学</div>
  19. </div>
  20. </li>
  21. </ul>
  22. </li>
  23. </ul>
  24. </template>
  25. <style lang="stylus">
  26. .vue-tree{
  27. list-style: none;
  28. padding: 0;
  29. margin: 0;
  30. .tree-item{
  31. cursor: pointer;
  32. transition: background-color .2s;
  33. .tree-content{
  34. position: relative;
  35. padding-left: 28px;
  36. &:hover{
  37. background-color: #f0f7ff;
  38. }
  39. }
  40. .expand-arrow{
  41. position: absolute;
  42. top: 0;
  43. left: 0;
  44. width: 28px;
  45. height: 28px;
  46. cursor: pointer;
  47. &::after{
  48. position: absolute;
  49. top: 50%;
  50. left: 50%;
  51. display: block;
  52. content: ' ';
  53. border-width: 5px;
  54. border-style: solid;
  55. border-color: transparent;
  56. border-left-color: #ccc;
  57. margin: -5px 0 0 -2.5px;
  58. transition: all .2s;
  59. }
  60. }
  61. &.expand{
  62. &>.tree-content{
  63. background-color: #f0f7ff;
  64. &>.expand-arrow{
  65. &::after{
  66. transform: rotate(90deg);
  67. margin: -2.5px 0 0 -5px;
  68. }
  69. }
  70. }
  71. }
  72. .tree-label{
  73. height: 28px;
  74. line-height: 28px;
  75. font-size: 14px;
  76. }
  77. .sub-tree{
  78. display: none;
  79. list-style: none;
  80. padding: 0 0 0 28px;
  81. margin: 0;
  82. }
  83. &.expand>.sub-tree{
  84. display: block;
  85. }
  86. &.no-child{
  87. &>.tree-content{
  88. &>.expand-arrow{
  89. display: none;
  90. }
  91. }
  92. }
  93. }
  94. }
  95. </style>

二、组件目录及数据结构

目录结构
vue-tree

  • VueTree.vue 树形控件父组件
  • TreeItem.vue 树形控件递归组件

树形控件数据结构
**

  1. let treeData = [
  2. {
  3. text: "一级", // 显示的文字
  4. expand: false, // 默认是否展开
  5. children: [ // 子节点
  6. {
  7. text: "一级-1",
  8. expand: false,
  9. },
  10. {
  11. text: "一级-2",
  12. expand: false,
  13. children: [
  14. {
  15. text: "一级-2-1",
  16. expand: false,
  17. },
  18. {
  19. text: "一级-2-2",
  20. expand: false,
  21. }
  22. ]
  23. }
  24. ]
  25. }
  26. ];

1.TreeItem.vue 代码

  1. <template>
  2. <li class="tree-item" :class="{expand: isExpand, 'no-child': !treeItemData.children || treeItemData.children.length === 0}">
  3. <div class="tree-content" @click="_clickEvent">
  4. <div class="expand-arrow" @click.stop="expandTree()"></div>
  5. <div class="tree-label">{{treeItemData.text}}</div>
  6. </div>
  7. <ul class="sub-tree" v-if="treeItemData.children && treeItemData.children.length > 0">
  8. <!--TreeItem组件中调用TreeItem组件-->
  9. <TreeItem
  10. v-for="item in treeItemData.children"
  11. :tree-item-data="item"
  12. :key="uuid()"
  13. :tree-click-event="treeClickEvent"></TreeItem>
  14. </ul>
  15. </li>
  16. </template>
  17. <script>
  18. export default {
  19. name: "TreeItem",
  20. props: {
  21. treeItemData: {
  22. type: Object,
  23. default(){
  24. return {};
  25. }
  26. },
  27. // 节点点击事件
  28. treeClickEvent: {
  29. type: Function,
  30. default() {
  31. return function () {};
  32. }
  33. }
  34. },
  35. data(){
  36. return {
  37. // 节点是否展开
  38. isExpand: this.treeItemData.expand || false
  39. }
  40. },
  41. methods: {
  42. // 展开/收缩
  43. expandTree(flag){
  44. if(!this.treeItemData.children || this.treeItemData.children.length === 0){
  45. return;
  46. }
  47. if(typeof flag === 'undefined'){
  48. flag = !this.isExpand;
  49. }else{
  50. flag = !!flag;
  51. }
  52. this.isExpand = flag;
  53. },
  54. // 创建一个唯一id
  55. uuid(){
  56. let str = Math.random().toString(32);
  57. str = str.substr(2);
  58. return str;
  59. },
  60. // 节点点击事件
  61. _clickEvent(){
  62. // 如果有传递事件函数,则调用事件函数并传递当前节点数据及组件
  63. if(this.treeClickEvent && typeof this.treeClickEvent === 'function'){
  64. this.treeClickEvent(this.treeItemData, this);
  65. }
  66. }
  67. }
  68. }
  69. </script>

(一)解决组件如何才能递归调用问题

在组件模板内调用自身必须明确定义组件的name属性,并且递归调用时组件名称就是name属性。如在TreeItem.vue组件中组件的name名称为’TreeItem’,那么在template中调用时组件名称就必须是
当然也可以全局注册组件,具体可以查看vue官方文档 递归组件

(二)解决递归组件点击事件如何传递问题

我这里的解决方案是使用props将事件函数传递进来,在点击节点的时候调用事件函数,并把相应的数据传递进去。
之前也尝试过使用$emit的形式并把数据传递过去,由于是递归组件,这样一直$emit,到最外层时传递的数据就变了,比如传递是第3层节点的数据,到最后执行时数据就变成第1层节点的数据了

2.VueTree.vue 组件

  1. <template>
  2. <ul class="vue-tree">
  3. <TreeItem
  4. v-for="(item, index) in treeData"
  5. :key="index"
  6. :treeItemData="item"
  7. :tree-click-event="treeClickEvent"></TreeItem>
  8. </ul>
  9. </template>
  10. <script>
  11. import TreeItem from "./TreeItem";
  12. export default {
  13. name: "VueTreeMenu",
  14. components: {
  15. TreeItem
  16. },
  17. props: {
  18. // 树形控件数据
  19. treeData: {
  20. type: Array,
  21. default(){
  22. return [];
  23. }
  24. },
  25. // 节点点击事件
  26. treeClickEvent: {
  27. type: Function,
  28. default() {
  29. return function () {};
  30. }
  31. }
  32. }
  33. }
  34. </script>
  35. <style lang="stylus">
  36. .vue-tree{
  37. list-style: none;
  38. padding: 0;
  39. margin: 0;
  40. .tree-item{
  41. cursor: pointer;
  42. transition: background-color .2s;
  43. .tree-content{
  44. position: relative;
  45. padding-left: 28px;
  46. &:hover{
  47. background-color: #f0f7ff;
  48. }
  49. }
  50. .expand-arrow{
  51. position: absolute;
  52. top: 0;
  53. left: 0;
  54. width: 28px;
  55. height: 28px;
  56. cursor: pointer;
  57. &::after{
  58. position: absolute;
  59. top: 50%;
  60. left: 50%;
  61. display: block;
  62. content: ' ';
  63. border-width: 5px;
  64. border-style: solid;
  65. border-color: transparent;
  66. border-left-color: #ccc;
  67. margin: -5px 0 0 -2.5px;
  68. transition: all .2s;
  69. }
  70. }
  71. &.expand{
  72. &>.tree-content{
  73. background-color: #f0f7ff;
  74. &>.expand-arrow{
  75. &::after{
  76. transform: rotate(90deg);
  77. margin: -2.5px 0 0 -5px;
  78. }
  79. }
  80. }
  81. }
  82. .tree-label{
  83. height: 28px;
  84. line-height: 28px;
  85. font-size: 14px;
  86. }
  87. .sub-tree{
  88. display: none;
  89. list-style: none;
  90. padding: 0 0 0 28px;
  91. margin: 0;
  92. }
  93. &.expand>.sub-tree{
  94. display: block;
  95. }
  96. &.no-child{
  97. &>.tree-content{
  98. /*padding-left: 0;*/
  99. &>.expand-arrow{
  100. display: none;
  101. }
  102. }
  103. }
  104. }
  105. }
  106. </style>

3.使用树形组件

  1. <template>
  2. <div class="app" id="app">
  3. <VueTree :tree-data="treeData2" :tree-click-event="treeClickEvent"></VueTree>
  4. </div>
  5. </template>
  6. <script>
  7. import VueTree from "./components/vue-tree/VueTree";
  8. export default {
  9. name: 'app',
  10. data(){
  11. return {
  12. treeData2: [
  13. {
  14. text: "一级", // 显示的文字
  15. expand: false, // 默认是否展开
  16. children: [
  17. {
  18. text: "二级-1",
  19. expand: false,
  20. },
  21. {
  22. text: "二级-2",
  23. expand: false,
  24. children: [
  25. {
  26. text: "三级-1",
  27. expand: false,
  28. },
  29. {
  30. text: "三级-2",
  31. expand: false,
  32. children: [
  33. {
  34. text: "四级-1",
  35. expand: false,
  36. }
  37. ]
  38. }
  39. ]
  40. }
  41. ]
  42. },
  43. {
  44. text: "一级-2",
  45. expand: false
  46. }
  47. ]
  48. }
  49. },
  50. methods: {
  51. treeClickEvent(item, treeItem){
  52. console.log(item);
  53. }
  54. },
  55. components: {
  56. VueTree
  57. }
  58. }
  59. </script>