最近在做一个类似语雀一样的项目,自定义了一个目录,无限层级,并有拖动等效果(与语雀里知识库目录一样),自己手写,记录下代码。
组件核心思想就是组件递归,很多插件的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 属性