VUE元件遞迴實現自定義目錄及拖拽效果
阿新 • • 發佈:2020-10-10
最近在做一個類似語雀一樣的專案,自定義了一個目錄,無限層級,並有拖動等效果(與語雀裡知識庫目錄一樣),自己手寫,記錄下程式碼。
元件核心思想就是元件遞迴,很多外掛的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 屬性