1. 程式人生 > 實用技巧 >VUE元件遞迴實現自定義目錄及拖拽效果

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="width: 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="width: 300px;" placeholder="標題" @keyup.enter.native="submitAdd"> </a-input> <a-input v-model="cataInfo.link" style="width: 300px;" placeholder="連結" @keyup.enter.native="submitAdd"> </a-input> </template> <a-input v-else v-model="cataInfo.title" placeholder="標題" style="width: 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="width: 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="width: 300px;"
        placeholder="標題"
        @keyup.enter.native="submitAdd">
        </a-input>
        <a-input v-model="cataInfo.link"
        style="width: 300px;"
        placeholder="連結"
        @keyup.enter.native="submitAdd">
        </a-input>
      </template>
      <a-input v-else v-model="cataInfo.title"
      placeholder="標題"
      style="width: 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 屬性