• VUE组件递归实现自定义目录及拖拽效果


      最近在做一个类似语雀一样的项目,自定义了一个目录,无限层级,并有拖动等效果(与语雀里知识库目录一样),自己手写,记录下代码。

      组件核心思想就是组件递归,很多插件的tree结构,其核心原理也就是用的组件递归。

    一、我们来看看实例代码,不使用组件递归的话怎么写

    1、组件

    <template>
    <div :class="{'expand': expand}">
      <div class="cata-item flex" :class="{'draggable': editing}"
      :draggable="editing"
      @dragstart="dragstart($event, item.id)">
        <template v-if="editing">
          <div class="sort-line" :class="{'b-color': peerShow}"
          @dragenter="peerShow = true"
          @dragleave="peerShow = false"
          @dragover.prevent
          @drop.prevent="dragDropPeer($event, item)">
            <div class="before"></div>
          </div>
          <div class="sort-line child" :class="{'b-color': childShow}"
          @dragenter="childShow = true"
          @dragleave="childShow = false"
          @dragover.prevent
          @drop.prevent="dragDropChild($event, item)">
            <div class="before"></div>
          </div>
        </template>
        <i class="nb-pull-down" :class="{'expand': expand}"
        v-if="item.childrenList.length > 0 || adding"
        @click="expand = !expand"></i>
        <a-input v-if="renaming"
        v-model="cataInfo.title"
        style=" 500px;"
        @keyup.enter.native="submitAdd">
        </a-input>
        <div v-else class="title" @click="jump(item)">{{item.title}}</div>
        <div class="dot" :class="{'edit': editing}"></div>
        <div class="time" v-if="!editing">{{item.createdTime.substring(0,11)}}</div>
        <div class="operate" v-else>
          <a-popover v-if="!lastChild" overlayClassName="nav-popover" placement="bottom">
            <div class="popover-nav-box cata" slot="content">
              <div class="popover-item" @click="addType(item.id, 'article')">添加文章</div>
              <div class="popover-item" @click="addType(item.id, 'resource')">添加资源</div>
              <div class="popover-item" @click="addType(item.id, 'link')">添加链接</div>
              <div class="popover-item" @click="addType(item.id, '')">添加分组</div>
            </div>
            <i class="nb-add"></i>
          </a-popover>
          <a-popover overlayClassName="nav-popover" placement="bottom">
            <div class="popover-nav-box cata" slot="content">
              <a-popconfirm
                title="是否删除子级目录?"
                ok-text=""
                cancel-text=""
                @confirm="delCata(item.id, true)"
                @cancel="delCata(item.id, false)">
                <div class="popover-item">移除目录</div>
              </a-popconfirm>
              <div class="popover-item" @click="rename">重命名</div>
            </div>
            <i class="nb-more"></i>
          </a-popover>
        </div>
      </div>
      <div class="add-box" v-if="adding">
        <template v-if="cataInfo.type === 'link'">
          <a-input v-model="cataInfo.title"
          style=" 300px;"
          placeholder="标题"
          @keyup.enter.native="submitAdd">
          </a-input>
          <a-input v-model="cataInfo.link"
          style=" 300px;"
          placeholder="链接"
          @keyup.enter.native="submitAdd">
          </a-input>
        </template>
        <a-input v-else v-model="cataInfo.title"
        placeholder="标题"
        style=" 500px;"
        @keyup.enter.native="submitAdd">
        </a-input>
      </div>
    </div>
    </template>
    <script>
    import { saveCatalogApi, delCatalogApi, sortCatalogApi } from '@/apis'
    import { cbSuccess } from '@/utils'
    export default {
      props: ['item', 'editing', 'dragId', 'lastChild'],
      data () {
        return {
          expand: false,
          renaming: false,
          adding: false,
          cataInfo: {
            type: '',
            title: '',
            link: ''
          },
          peerShow: false,
          childShow: false
        }
      },
      methods: {
        rename () {
          this.renaming = true
          this.cataInfo = this.item
        },
        addType (id, type) {
          this.adding = true
          this.cataInfo.type = type
          this.cataInfo.parentId = id
        },
        async submitAdd () {
          let _cata = this.cataInfo
          if (!_cata.title) {
            this.$message.error('标题不能为空')
            return
          }
          if (_cata.type === 'link' && !_cata.link) {
            this.$message.error('链接不能为空')
            return
          }
          let { data } = await saveCatalogApi(_cata)
          cbSuccess(data, _ => {
            this.$emit('refresh')
            this.adding = false
            this.cataInfo = {
              type: '',
              title: '',
              link: '',
              knowledgeId: this.$route.params.id
            }
            this.renaming = false
          })
        },
        async delCata (id, isAll) {
          let { data } = await delCatalogApi(id, isAll)
          cbSuccess(data, _ => {
            this.$emit('refresh')
          })
        },
        // 拖动排序
        dragstart (e, id) {
          this.$emit('start', id)
        },
        dragDropPeer (e, item) { // 拖动目标的同级
          if (this.dragId === item.id) return
          let _data = {
            id: this.dragId,
            newParentId: item.parentId,
            newSort: item.sort + 1
          }
          this.dragDrop(_data)
          this.peerShow = false
        },
        dragDropChild (e, item) { // 拖动目标的子级
          if (this.dragId === item.id) return
          let _data = {
            id: this.dragId,
            newParentId: item.id
          }
          this.dragDrop(_data)
          this.childShow = false
        },
        async dragDrop (_data) {
          let { data } = await sortCatalogApi(_data)
          cbSuccess(data, _ => {
            this.$emit('refresh')
          })
        },
        jump (item) {
          if (item.link) {
            window.open(item.link, '_blank')
          } else if (item.baseId) {
            let _routePath = this.$router.resolve(`/blog/${item.baseId}`)
            window.open(_routePath.href, '_blank')
          }
        }
      },
      mounted () {
        this.cataInfo.knowledgeId = this.$route.params.id
      }
    }
    </script>
    <style lang="stylus" scoped>
    .sort-line{
      width 100%
      box-sizing border-box
      margin-left 8px
      height 34px
      position absolute
      bottom 0
      border-bottom 2px solid transparent
      .before{
        width 8px
        height 8px
        border 2px solid #4882fc
        border-radius 50%
        position absolute
        left -7px
        bottom -5px
        display none
      }
      &.b-color{
        border-color #4882fc
        & > .before{
          display block
        }
      }
    }
    .sort-line.child{
      width calc(100% - 25px)
      margin-left 25px
    }
    .cata-item{
      position relative
      height 34px
      line-height 34px
      &:hover{
        background #F7F8FC
      }
      i{
        font-size 13px
        color #B8BECC
        opacity 0.5
        margin-right 10px
        cursor pointer
        &.expand{
          transform rotate(270deg)
        }
      }
      .dot{
        flex 1
        width 200px
        height 1px
        border-top: 1px dashed #B8BECC;
        opacity: 0.5;
        margin 0 30px
        &.edit{
          border-top none
        }
      }
    }
    .title{
      font-size 14px
      line-height 34px
      color #37393D
      cursor pointer
    }
    .ml23{
      .title{
        color #858A94
      }
    }
    .c3 .title{
      font-size 12px
    }
    .add-box{
      padding-left 23px
      text-align left
      &:hover{
        background #F7F8FC
      }
    }
    </style>

    2、调用

        <div v-for="item in cts" :key="item.id">
          <CataItem :item="item" :editing="editing" @refresh="fetchData" :dragId="dragId" @start="dragstart"></CataItem>
          <template v-if="item.childrenList && item.childrenList.length > 0">
            <div class="ml23" v-for="c1 in item.childrenList" :key="c1.id">
              <CataItem :item="c1" :editing="editing" @refresh="fetchData" :dragId="dragId" @start="dragstart"></CataItem>
              <template v-if="c1.childrenList.length > 0">
                <div class="ml23" v-for="c2 in c1.childrenList" :key="c2.id">
                  <CataItem :item="c2" :editing="editing" @refresh="fetchData" :dragId="dragId" @start="dragstart"></CataItem>
                  <template v-if="c2.childrenList.length > 0">
                    <div class="ml23" v-for="c3 in c2.childrenList" :key="c3.id">
                      <CataItem :item="c3" :editing="editing" @refresh="fetchData" :dragId="dragId" @start="dragstart"></CataItem>
                      <template v-if="c3.childrenList.length > 0">
                        <div class="ml23" v-for="c4 in c3.childrenList" :key="c4.id">
                          <CataItem :item="c4" :editing="editing" :lastChild="true" @refresh="fetchData" :dragId="dragId" @start="dragstart"></CataItem>
                        </div>
                      </template>
                    </div>
                  </template>
                </div>
              </template>
            </div>
          </template>
        </div>

      我们看到这调用简直就是噩梦啊,而且不能做到无限层级,想要多少级,就得写多少次,而且很容易写错。

    二、使用组件递归思想优化

      通过观察发现很多层级都是一样的

    <CataItem :item="c3" :editing="editing" @refresh="fetchData" :dragId="dragId" @start="dragstart"></CataItem>
    <template v-if="c3.childrenList.length > 0">
      <div class="ml23" v-for="c4 in c3.childrenList" :key="c4.id">
      ...   </div> </template>

      每个这一级都是一样的,那么我们就来通过组件递归来优化一下组件和调用

    1、组件优化

    <template>
    <div>
      <div :class="{'expand': expand}">
        <div class="cata-item flex" :class="{'draggable': editing}"
        :draggable="editing"
        @dragstart="dragstart($event, item.id)">
          <template v-if="editing">
            <div class="sort-line" :class="{'b-color': peerShow}"
            @dragenter="peerShow = true"
            @dragleave="peerShow = false"
            @dragover.prevent
            @drop.prevent="dragDropPeer($event, item)">
              <div class="before"></div>
            </div>
            <div class="sort-line child" :class="{'b-color': childShow}"
            @dragenter="childShow = true"
            @dragleave="childShow = false"
            @dragover.prevent
            @drop.prevent="dragDropChild($event, item)">
              <div class="before"></div>
            </div>
          </template>
          <i class="nb-pull-down" :class="{'expand': expand}"
          v-if="item.childrenList.length > 0 || adding"
          @click="expand = !expand"></i>
          <a-input v-if="renaming"
          v-model="cataInfo.title"
          style=" 500px;"
          @keyup.enter.native="submitAdd">
          </a-input>
          <div v-else class="title" @click="jump(item)">{{item.title}}</div>
          <div class="dot" :class="{'edit': editing}"></div>
          <div class="time" v-if="!editing">{{item.createdTime.substring(0,11)}}</div>
          <div class="operate" v-else>
            <a-popover overlayClassName="nav-popover" placement="bottom">
              <div class="popover-nav-box cata" slot="content">
                <div class="popover-item" @click="addType(item.id, 'article')">添加文章</div>
                <div class="popover-item" @click="addType(item.id, 'resource')">添加资源</div>
                <div class="popover-item" @click="addType(item.id, 'link')">添加链接</div>
                <div class="popover-item" @click="addType(item.id, '')">添加分组</div>
              </div>
              <i class="nb-add"></i>
            </a-popover>
            <a-popover overlayClassName="nav-popover" placement="bottom">
              <div class="popover-nav-box cata" slot="content">
                <a-popconfirm
                  title="是否删除子级目录?"
                  ok-text=""
                  cancel-text=""
                  @confirm="delCata(item.id, true)"
                  @cancel="delCata(item.id, false)">
                  <div class="popover-item">移除目录</div>
                </a-popconfirm>
                <div class="popover-item" @click="rename">重命名</div>
              </div>
              <i class="nb-more"></i>
            </a-popover>
          </div>
        </div>
        <div class="add-box" v-if="adding">
          <template v-if="cataInfo.type === 'link'">
            <a-input v-model="cataInfo.title"
            style=" 300px;"
            placeholder="标题"
            @keyup.enter.native="submitAdd">
            </a-input>
            <a-input v-model="cataInfo.link"
            style=" 300px;"
            placeholder="链接"
            @keyup.enter.native="submitAdd">
            </a-input>
          </template>
          <a-input v-else v-model="cataInfo.title"
          placeholder="标题"
          style=" 500px;"
          @keyup.enter.native="submitAdd">
          </a-input>
        </div>
      </div>
      <div class="ml23" v-for="cata in item.childrenList" :key="cata.id">
        <cataTree
        :item="cata"
        :editing="editing"
        :dragId="dragId"
        @refresh="$emit('refresh')"
        @start="$emit('start', arguments[0])">
        </cataTree>
      </div>
    </div>
    </template>
    <script>
    import { saveCatalogApi, delCatalogApi, sortCatalogApi } from '@/apis'
    import { cbSuccess } from '@/utils'
    export default {
      name: 'cataTree',
      props: ['item', 'editing', 'dragId'],
      data () {
        return {
          expand: false,
          renaming: false,
          adding: false,
          cataInfo: {
            type: '',
            title: '',
            link: ''
          },
          peerShow: false,
          childShow: false
        }
      },
      methods: {
        rename () {
          this.renaming = true
          this.cataInfo = this.item
        },
        addType (id, type) {
          this.adding = true
          this.cataInfo.type = type
          this.cataInfo.parentId = id
        },
        async submitAdd () {
          let _cata = this.cataInfo
          if (!_cata.title) {
            this.$message.error('标题不能为空')
            return
          }
          if (_cata.type === 'link' && !_cata.link) {
            this.$message.error('链接不能为空')
            return
          }
          let { data } = await saveCatalogApi(_cata)
          cbSuccess(data, _ => {
            this.$emit('refresh')
            this.adding = false
            this.cataInfo = {
              type: '',
              title: '',
              link: '',
              knowledgeId: this.$route.params.id
            }
            this.renaming = false
          })
        },
        async delCata (id, isAll) {
          let { data } = await delCatalogApi(id, isAll)
          cbSuccess(data, _ => {
            this.$emit('refresh')
          })
        },
        // 拖动排序
        dragstart (e, id) {
          this.$emit('start', id)
        },
        dragDropPeer (e, item) { // 拖动目标的同级
          if (this.dragId === item.id) {
            this.peerShow = false
            return
          }
          let _data = {
            id: this.dragId,
            newParentId: item.parentId,
            newSort: item.sort + 1
          }
          this.dragDrop(_data)
          this.peerShow = false
        },
        dragDropChild (e, item) { // 拖动目标的子级
          if (this.dragId === item.id){
            this.childShow = false
            return
          }
          let _data = {
            id: this.dragId,
            newParentId: item.id
          }
          this.dragDrop(_data)
          this.childShow = false
        },
        async dragDrop (_data) {
          let { data } = await sortCatalogApi(_data)
          cbSuccess(data, _ => {
            this.$emit('refresh')
          })
        },
        jump (item) {
          if (item.link) {
            window.open(item.link, '_blank')
          } else if (item.baseId) {
            let _routePath = this.$router.resolve(`/blog/${item.baseId}`)
            window.open(_routePath.href, '_blank')
          }
        }
      },
      mounted () {
        this.cataInfo.knowledgeId = this.$route.params.id
      }
    }
    </script>
    <style lang="stylus" scoped>
    .expand + .ml23{
      display none
    }
    .ml23{
      margin-left 23px
    }
    .sort-line{
      width 100%
      box-sizing border-box
      margin-left 8px
      height 34px
      position absolute
      bottom 0
      border-bottom 2px solid transparent
      .before{
        width 8px
        height 8px
        border 2px solid #4882fc
        border-radius 50%
        position absolute
        left -7px
        bottom -5px
        display none
      }
      &.b-color{
        border-color #4882fc
        & > .before{
          display block
        }
      }
    }
    .sort-line.child{
      width calc(100% - 25px)
      margin-left 25px
    }
    .cata-item{
      position relative
      height 34px
      line-height 34px
      &:hover{
        background #F7F8FC
      }
      i{
        font-size 13px
        color #B8BECC
        opacity 0.5
        margin-right 10px
        cursor pointer
        &.expand{
          transform rotate(270deg)
        }
      }
      .dot{
        flex 1
        width 200px
        height 1px
        border-top: 1px dashed #B8BECC;
        opacity: 0.5;
        margin 0 30px
        &.edit{
          border-top none
        }
      }
    }
    .title{
      font-size 14px
      line-height 34px
      color #37393D
      cursor pointer
    }
    .ml23{
      .title{
        color #858A94
      }
    }
    .c3 .title{
      font-size 12px
    }
    .add-box{
      padding-left 23px
      text-align left
      &:hover{
        background #F7F8FC
      }
    }
    </style>

      主要修改的就是其中标红的,我们看到修改的内容很少。

      需要特别注意的就是:组件递归调用自己的时候,其 props 和 方法传递,均需要捕获并触发一下

    2、调用优化

    // 目录组件
    <CataItem v-for="item in cts" :key="item.id"
    :item="item"
    :editing="editing"
    :dragId="dragId"
    @refresh="fetchData"
    @start="dragstart">
    </CataItem>

      调用优化就直接循环使用即可。我们看到组件递归优化之后,调用就比较方便美观了。

    三、简单实例

      其实没啥好说的,就是组件递归,这里呢简单写个例子,面试被问到的时候直接拿来手写代码也行,没多少代码量,主要是让还没懂组件递归的同学好理解,核心就这个,组件自己调用自己

    1、组件

    <template>
      <ul>
        <li v-for="(item,index) in list " :key="index">
          <p>{{item.name}}</p>
          <treeMenus :list="item.children"></treeMenus>
        </li>
      </ul>
    </template>
    <script>
    export default {
      name: "treeMenus",
      props: {
        list: Array
      }
    };
    </script>
    <style>
        ul {
        padding-left: 20px !important;
        }
    </style>

    2、html调用

    <treeMenus :list="treeMenusData"></treeMenus>
    // 数据格式
    treeMenusData: [
      {
        name: "菜单1",
        children: [
          {
            name: "菜单1-1",
            children: []
          }
        ]
      }
    ]

      这个简单的例子就比较好理解,主要就是利用了 组件的 name 属性

  • 相关阅读:
    cf 811c Vladik and Memorable Trip
    Codeforces 837D--Round Subset (DP)
    codeforces798C
    Codeforces 814C
    CodeForces 610D Vika and Segments
    CodeForces 593D Happy Tree Party
    hdu 5564 Clarke and digits
    hdu 5517 Triple
    codeforces 584E Anton and Ira [想法题]
    codeforces 582A GCD Table
  • 原文地址:https://www.cnblogs.com/goloving/p/13794531.html
Copyright © 2020-2023  润新知