1. 无限级树形组件1
(1) 组件源码 tree-item.vue
<template>
<view>
<view class="tree-ul" v-for="(item,i) in data" :key="item.code+i">
<view class="isTrue" v-if="item.children">
<view class="tree-item">
<label>{{item.name}}</label>
<span v-if="!opends[i]" @tap="onOpends(item,i)">∨</span>
<span v-if="opends[i]" @tap="onOpends(item,i)">∧</span>
</view>
<tree-item class="children-box" :data="item.children" v-show="opends[i]"></tree-item>
</view>
<view class="tree-item isTrue" v-if="!item.children">
<label>{{item.name}}</label>
</view>
</view></view>
</template>
<script>
export default {
name: "tree-item",
props:{
data: {
type: Array,
default: function(e) {
return []
}
}
},
data() {
return {
opends: [],
};
},
onLoad() {
console.log("tree-item onLoad")
},
methods:{
onOpends(item,i){
this.$nextTick(function(){
this.$set(this.opends,i,!this.opends[i])
})
}
}
}
</script>
<style>
.tree-box{
display: inline-block;
width: 220px;
border: 1px solid #eee;
overflow: hidden;
border-radius: 4px;
}
.tree-item{
display: flex;
overflow: hidden;
height: 32px;
border-bottom: 1px solid #eee;
}
.tree-item>label{
flex: 1;
line-height: 32px;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tree-item>span{
width: 32px;
height: 32px;
text-align: center;
line-height: 32px;
}
.isTrue{
padding-left: 15px;
}
</style>
(2) index.vue中调用
<template>
<view>
<view>该组件只能在h5端实现树状列表;无法实现微信小程序和android端的树状列表,只能显示一级而无法展开下级列表</view>
<view position="middle" mode="fixed" style="background-color: #FFFFFF;">
<scroll-view class="uni-bg-white" style="height:500upx;width:300upx" scroll-y="true">
<tree-folder :data="lists"></tree-folder>
</scroll-view>
</view>
</view>
</template>
<script>
import treeFolder from "@/components/tree/tree-item.vue";
export default {
components: {
treeFolder
},
data() {
return {
lists:[
{
name:"椒江新厂",
code:"001",
children:[
{
name:"生产部门A",
code:"021",
children:[
{
name:"A-01",
code:"031",
},
{
name:"A-02",
code:"032",
},
{
name:"A-03",
code:"033",
}
]
},
{
name:"生产部门B",
code:"022",
children:[
{
name:"B-01",
code:"034",
}
]
},
{
name:"生产部门C",
code:"023",
}
]
},
{
name:"杭州工厂",
code:"002",
},
{
name:"西安工厂",
code:"003",
}
]
}
},
methods: {
}
}
</script>
<style>
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #fff;
}
</style>
备注
:uni-app使用vue递归组件做无限级树形列表时,在H5页面能正常显示(H5兼容递归组件本身),但是在微信小程序和android端只能显示第一级(小程序和安卓app无法递归)。该组件无法实现微信小程序和android端的树状列表,只能显示一级而无法展开下级列表。
2. 无限级树形组件2
(1) 组件源码mix-tree.vue
<template>
<view class="content">
<view class="mix-tree-list">
<block v-for="(item, index) in treeList" :key="index">
<view
class="mix-tree-item"
:style="[{
paddingLeft: item.rank*15 + 'px',
zIndex: item.rank*-1 +50
}]"
:class="{
border: treeParams.border === true,
show: item.show,
last: item.lastRank,
showchild: item.showChild
}"
@click.stop="treeItemTap(item, index)"
>
<image class="mix-tree-icon" :src="item.lastRank ? treeParams.lastIcon : item.showChild ? treeParams.currentIcon : treeParams.defaultIcon"></image>
{{item.name}}
</view>
</block>
</view>
</view>
</template>
<script>
export default {
props: {
list: {
type: Array,
default(){
return [];
}
},
params: {
type: Object,
default(){
return {}
}
}
},
data() {
return {
treeList: [],
treeParams: {
defaultIcon: '/static/mix-tree/defaultIcon.png',
currentIcon: '/static/mix-tree/currentIcon.png',
lastIcon: '',
border: false
}
}
},
watch: {
list(list){
this.treeParams = Object.assign(this.treeParams, this.params);
console.log(this.treeParams, this.params);
this.renderTreeList(list);
}
},
onLoad() {
console.log("mix-tree onLoad")
},
methods: {
//扁平化树结构
renderTreeList(list=[], rank=0, parentId=[]){
list.forEach(item=>{
this.treeList.push({
id: item.id,
name: item.name,
parentId, // 父级id数组
rank, // 层级
showChild: false, //子级是否显示
show: rank === 0 // 自身是否显示
})
if(Array.isArray(item.children) && item.children.length > 0){
let parents = [...parentId];
parents.push(item.id);
this.renderTreeList(item.children, rank+1, parents);
}else{
this.treeList[this.treeList.length-1].lastRank = true;
}
})
},
// 点击
treeItemTap(item){
let list = this.treeList;
let id = item.id;
if(item.lastRank === true){
//点击最后一级时触发事件
this.$emit('treeItemClick', item);
return;
}
item.showChild = !item.showChild;
list.forEach(childItem=>{
if(item.showChild === false){
//隐藏所有子级
if(!childItem.parentId.includes(id)){
return;
}
if(childItem.lastRank !== true){
childItem.showChild = false;
}
childItem.show = false;
}else{
if(childItem.parentId[childItem.parentId.length-1] === id){
childItem.show = true;
}
}
})
}
}
}
</script>
<style>
.mix-tree-list{
display: flex;
flex-direction: column;
padding-left: 30upx;
}
.mix-tree-item{
display: flex;
align-items: center;
font-size: 30upx;
color: #333;
height: 0;
opacity: 0;
transition: .2s;
position: relative;
}
.mix-tree-item.border{
border-bottom: 1px solid #eee;
}
.mix-tree-item.show{
height: 80upx;
opacity: 1;
}
.mix-tree-icon{
width: 26upx;
height: 26upx;
margin-right: 8upx;
opacity: .9;
}
.mix-tree-item.showchild:before{
transform: rotate(90deg);
}
.mix-tree-item.last:before{
opacity: 0;
}
</style>
(2) index.vue中调用
<template>
<view>
<view>该组件可实现树状列表</view>
<view position="middle" mode="fixed" style="background-color: #FFFFFF;">
<scroll-view class="uni-bg-white" style="height:500upx;width:300upx" scroll-y="true">
<mix-tree :list="lists" @treeItemClick="treeItemClick"></mix-tree>
</scroll-view>
</view>
</view>
</template>
<script>
import mixTree from '@/components/mix-tree/mix-tree';
let testList = [
{
name:"椒江新厂",
id:"001",
children:[
{
name:"生产部门A",
id:"021",
children:[
{
name:"A-01",
id:"031",
},
{
name:"A-02",
id:"032",
},
{
name:"A-03",
id:"033",
}
]
},
{
name:"生产部门B",
id:"022",
children:[
{
name:"B-01",
id:"034",
}
]
},
{
name:"生产部门C",
id:"023",
}
]
},
{
name:"杭州工厂",
id:"002",
},
{
name:"西安工厂",
id:"003",
}
]
export default {
components: {
mixTree
},
data() {
return {
lists: [],
}
},
onLoad:function(){
//测试树状列表
setTimeout(()=>{
this.lists = testList;
}, 500);
console.log(this.lists);
},
methods: {
//点击最后一级时触发该事件
treeItemClick(item) {
let {
id,
name,
parentId
} = item;
uni.showModal({
content: `点击了${parentId.length+1}级菜单, ${name}, id为${id}, 父id为${parentId.toString()}`
})
console.log(item)
}
}
}
</script>
<style>
</style>
备注
:源码中的id和name字段需根据数据结构定义的字段名进行对应的修改。如网络获取的列表数据为:
"departmentList": [
{
"name": "椒江新厂",
"id": "001",
"children": [
{
"name": "生产部门A",
"id": "021",
"children": [
{
"name": "A-01",
"id": "031"
},
{
"name": "A-02",
"id": "032"
},
{
"name": "A-03",
"id": "033"
}
]
},
{
"name": "生产部门B",
"id": "022",
"children": [
{
"name": "B-01",
"id": "034"
}
]
},
{
"name": "生产部门C",
"id": "023"
}
]
},
{
"name": "杭州工厂",
"id": "002"
},
{
"name": "西安工厂",
"id": "003"
}
]
则源码mix-tree.vue修改为
<template>
<view class="content">
<view class="mix-tree-list">
<block v-for="(item, index) in treeList" :key="index">
<view
class="mix-tree-item"
:style="[{
paddingLeft: item.rank*15 + 'px',
zIndex: item.rank*-1 +50
}]"
:class="{
border: treeParams.border === true,
show: item.show,
last: item.lastRank,
showchild: item.showChild
}"
@click.stop="treeItemTap(item, index)"
>
<image class="mix-tree-icon" :src="item.lastRank ? treeParams.lastIcon : item.showChild ? treeParams.currentIcon : treeParams.defaultIcon"></image>
{{item.department_name}}
</view>
</block>
</view>
</view>
</template>
<script>
export default {
props: {
list: {
type: Array,
default(){
return [];
}
},
params: {
type: Object,
default(){
return {}
}
}
},
data() {
return {
treeList: [],
treeParams: {
defaultIcon: '/static/mix-tree/defaultIcon.png',
currentIcon: '/static/mix-tree/currentIcon.png',
lastIcon: '',
border: false
}
}
},
watch: {
list(list){
this.treeParams = Object.assign(this.treeParams, this.params);
// console.log(this.treeParams, this.params);
this.renderTreeList(list);
}
},
onLoad() {
console.log("mix-tree-dep onLoad")
},
methods: {
//扁平化树结构
renderTreeList(list=[], rank=0, parentId=[]){
//liy:若无该if代码块的判断与处理,网络数据每刷新一次一级目录就会重复一次
if(rank == 0) {
this.treeList = [];
}
list.forEach(item=>{
this.treeList.push({
department_id: item.department_id,
department_name: item.department_name,
parentId, // 父级id数组
rank, // 层级
showChild: false, //子级是否显示
show: rank === 0 // 自身是否显示
})
if(Array.isArray(item.children) && item.children.length > 0){
let parents = [...parentId];
parents.push(item.department_id);
this.renderTreeList(item.children, rank+1, parents);
}else{
this.treeList[this.treeList.length-1].lastRank = true;
}
})
},
// 点击
treeItemTap(item){
let list = this.treeList;
let id = item.department_id;
if(item.lastRank === true){
//点击最后一级时触发事件
this.$emit('treeItemClick', item);
return;
}
item.showChild = !item.showChild;
list.forEach(childItem=>{
if(item.showChild === false){
//隐藏所有子级
if(!childItem.parentId.includes(id)){
return;
}
if(childItem.lastRank !== true){
childItem.showChild = false;
}
childItem.show = false;
}else{
if(childItem.parentId[childItem.parentId.length-1] === id){
childItem.show = true;
}
}
})
}
}
}
</script>
<style>
.mix-tree-list{
display: flex;
flex-direction: column;
padding-left: 30upx;
}
.mix-tree-item{
display: flex;
align-items: center;
font-size: 30upx;
color: #333;
height: 0;
opacity: 0;
transition: .2s;
position: relative;
}
.mix-tree-item.border{
border-bottom: 1px solid #eee;
}
.mix-tree-item.show{
height: 80upx;
opacity: 1;
}
.mix-tree-icon{
width: 26upx;
height: 26upx;
margin-right: 8upx;
opacity: .9;
}
.mix-tree-item.showchild:before{
transform: rotate(90deg);
}
.mix-tree-item.last:before{
opacity: 0;
}
</style>
备注
:若无该if代码块的判断与处理if(rank == 0) {this.treeList = [];}
,网络数据每刷新一次一级目录就会重复一次 。
index.vue修改为:
<template>
<view>
<view @click="getDepartList">点击此处获取树状列表</view>
<view position="middle" mode="fixed" style="background-color: #FFFFFF;">
<scroll-view class="uni-bg-white" style="height:500upx;width:300upx" scroll-y="true">
<mix-tree :list="departmentList" @treeItemClick="treeItemClick"></mix-tree>
</scroll-view>
</view>
</view>
</template>
<script>
import mixTree from '@/components/mix-tree/mix-tree';
export default {
components: {
mixTree
},
data() {
return {
departmentList: [],
}
},
onLoad:function(){
this.getDepartList();
},
methods: {
//网络获取树状列表
getDepartList() {
var _this = this;
uni.request({
url: "接口url地址",
header: {
"content-type": "application/x-www-form-urlencoded"
},
method: "POST",
data: {
userName: "liy"
},
success: res => {
if (res.data.result === "1") {
_this.departmentList = res.data.departmentList;
}
},
fail: res => {},
complete: () => {}
});
},
//点击最后一级时触发该事件
treeItemClick(item) {
let {
department_id,
department_name,
parentId
} = item;
uni.showModal({
content: `点击了${parentId.length+1}级菜单, ${department_name}, id为${department_id}, 父id为${parentId.toString()}`
})
console.log(item)
},
}
}
</script>
<style>
</style>
2.1 功能拓展 — 实现选中中间项
说明:第二部分的无限级树形列表点击某个菜单会进行下级菜单的展开或收缩,最终只对最后一级的点击事件进行逻辑处理;若需求是实现每一个菜单项都能被选中,即中间菜单项不仅能实现展开或收缩子菜单,还要能实现当前中间菜单项自身被选中,此时可对第二部分的无限级树形列表的功能进行拓展。源码mix-tree.vue修改为:
<template>
<view class="content">
<view class="mix-tree-list">
<block v-for="(item, index) in treeList" :key="index">
<view
class="mix-tree-item"
:style="[{
paddingLeft: item.rank*15 + 'px',
zIndex: item.rank*-1 +50
}]"
:class="{
border: treeParams.border === true,
show: item.show,
last: item.lastRank,
showchild: item.showChild
}"
@click.stop="treeItemTap(item, index)"
>
<image class="mix-tree-icon" :src="item.lastRank ? treeParams.lastIcon : item.showChild ? treeParams.currentIcon : treeParams.defaultIcon"></image>
{{item.department_name}}
<!-- 新增选中当前项的功能 -->
<!-- <view @click.stop="currentItemTap(item, index)">{{item.department_name}}</view> -->
<view style="width: 60upx;text-align: center;margin-left: 5upx;" @click.stop="currentItemTap(item, index)">
<image class="mix-tree-icon" src="/static/mix-tree/currentNote.png"></image>
</view>
</view>
</block>
</view>
</view>
</template>
<script>
export default {
props: {
list: {
type: Array,
default(){
return [];
}
},
params: {
type: Object,
default(){
return {}
}
}
},
data() {
return {
treeList: [],
treeParams: {
defaultIcon: '/static/mix-tree/defaultIcon.png',
currentIcon: '/static/mix-tree/currentIcon.png',
lastIcon: '',
border: false
}
}
},
watch: {
list(list){
this.treeParams = Object.assign(this.treeParams, this.params);
// console.log(this.treeParams, this.params);
this.renderTreeList(list);
}
},
onLoad() {
console.log("mix-tree-dep onLoad")
},
methods: {
//扁平化树结构
renderTreeList(list=[], rank=0, parentId=[]){
//liy:若无该if代码块的判断与处理,网络数据每刷新一次一级目录就会重复一次
if(rank == 0) {
this.treeList = [];
}
list.forEach(item=>{
this.treeList.push({
department_id: item.department_id,
department_name: item.department_name,
parentId, // 父级id数组
rank, // 层级
showChild: false, //子级是否显示
show: rank === 0 // 自身是否显示
})
if(Array.isArray(item.children) && item.children.length > 0){
let parents = [...parentId];
parents.push(item.department_id);
this.renderTreeList(item.children, rank+1, parents);
}else{
this.treeList[this.treeList.length-1].lastRank = true;
}
})
},
// 点击
treeItemTap(item){
let list = this.treeList;
let id = item.department_id;
if(item.lastRank === true){
//点击最后一级时触发事件
this.$emit('treeItemClick', item);
return;
}
item.showChild = !item.showChild;
list.forEach(childItem=>{
if(item.showChild === false){
//隐藏所有子级
if(!childItem.parentId.includes(id)){
return;
}
if(childItem.lastRank !== true){
childItem.showChild = false;
}
childItem.show = false;
}else{
if(childItem.parentId[childItem.parentId.length-1] === id){
childItem.show = true;
}
}
})
},
// 实现选中中间item项
currentItemTap(item){
let list = this.treeList;
let id = item.department_id;
console.log("currentItemTap:" + id);
this.$emit('treeItemClick', item);
return;
}
}
}
</script>
<style>
.mix-tree-list{
display: flex;
flex-direction: column;
padding-left: 30upx;
}
.mix-tree-item{
display: flex;
align-items: center;
font-size: 30upx;
color: #333;
height: 0;
opacity: 0;
transition: .2s;
position: relative;
}
.mix-tree-item.border{
border-bottom: 1px solid #eee;
}
.mix-tree-item.show{
height: 80upx;
opacity: 1;
}
.mix-tree-icon{
width: 26upx;
height: 26upx;
margin-right: 8upx;
opacity: .9;
}
.mix-tree-item.showchild:before{
transform: rotate(90deg);
}
.mix-tree-item.last:before{
opacity: 0;
}
</style>
主要新增代码为:
<!-- <view @click.stop="currentItemTap(item, index)">{{item.department_name}}</view> -->
<view style="width: 60upx;text-align: center;margin-left: 5upx;" @click.stop="currentItemTap(item, index)">
<image class="mix-tree-icon" src="/static/mix-tree/currentNote.png"></image>
</view>
currentItemTap(item){
let list = this.treeList;
let id = item.department_id;
console.log("currentItemTap:" + id);
this.$emit('treeItemClick', item);
return;
}
2.2 功能拓展 — 实现选中中间项实现默认列表子项全展开
<template>
<view class="content">
<view class="mix-tree-list">
<block v-for="(item, index) in treeList" :key="index">
<view
class="mix-tree-item"
:style="[{
paddingLeft: item.rank*15 + 'px',
zIndex: item.rank*-1 +50
}]"
:class="{
border: treeParams.border === true,
show: item.show,
last: item.lastRank,
showchild: item.showChild
}"
@click.stop="treeItemTap(item, index)"
>
<image class="mix-tree-icon" :src="item.lastRank ? treeParams.lastIcon : item.showChild ? treeParams.currentIcon : treeParams.defaultIcon"></image>
{{item.department_name}}
<!-- 新增选中当前项的功能 -->
<!-- <view @click.stop="currentItemTap(item, index)">{{item.department_name}}</view> -->
<view style="width: 60upx;text-align: center;margin-left: 5upx;" @click.stop="currentItemTap(item, index)">
<image class="mix-tree-icon" src="/static/mix-tree/currentNote.png"></image>
</view>
</view>
</block>
</view>
</view>
</template>
<script>
export default {
props: {
list: {
type: Array,
default(){
return [];
}
},
params: {
type: Object,
default(){
return {}
}
}
},
data() {
return {
treeList: [],
treeParams: {
defaultIcon: '/static/mix-tree/defaultIcon.png',
currentIcon: '/static/mix-tree/currentIcon.png',
lastIcon: '',
border: false
}
}
},
watch: {
list(list){
this.treeParams = Object.assign(this.treeParams, this.params);
// console.log(this.treeParams, this.params);
this.renderTreeList(list);
}
},
onLoad() {
console.log("mix-tree-dep onLoad")
},
methods: {
//扁平化树结构
renderTreeList(list=[], rank=0, parentId=[]){
//liy:若无该if代码块的判断与处理,网络数据每刷新一次一级目录就会重复一次
if(rank == 0) {
this.treeList = [];
}
list.forEach(item=>{
this.treeList.push({
department_id: item.department_id,
department_name: item.department_name,
parentId, // 父级id数组
rank, // 层级
showChild: false, //子级是否显示(liy:默认所有子级都不显示)
show: rank === 0 // 自身是否显示
})
if(Array.isArray(item.children) && item.children.length > 0){
let parents = [...parentId];
parents.push(item.department_id);
this.renderTreeList(item.children, rank+1, parents);
}else{
this.treeList[this.treeList.length-1].lastRank = true;
}
});
//liy:实现默认展开所有子级
this.treeList.forEach(childItem=>{
childItem.show = true;
//解决第一次点击条目不会收缩子项的问题(默认所有子级都不显示,所有这里要重新赋值),或者showChild默认值该为true即可
if(childItem.rank != 0){
this.treeList[childItem.rank -1].showChild = true;
}
});
},
// 点击
treeItemTap(item){
let list = this.treeList;
let id = item.department_id;
if(item.lastRank === true){
//点击最后一级时触发事件
this.$emit('treeItemClick', item);
return;
}
item.showChild = !item.showChild;
list.forEach(childItem=>{
if(item.showChild === false){
//隐藏所有子级
if(!childItem.parentId.includes(id)){
return;
}
if(childItem.lastRank !== true){
childItem.showChild = false;
}
childItem.show = false;
}else{
if(childItem.parentId[childItem.parentId.length-1] === id){
childItem.show = true;
}
}
})
},
// 实现选中中间item项
currentItemTap(item){
let list = this.treeList;
let id = item.department_id;
console.log("currentItemTap:" + id);
this.$emit('treeItemClick', item);
return;
}
}
}
</script>
<style>
.mix-tree-list{
display: flex;
flex-direction: column;
padding-left: 30upx;
}
.mix-tree-item{
display: flex;
align-items: center;
font-size: 30upx;
color: #333;
height: 0;
opacity: 0;
transition: .2s;
position: relative;
}
.mix-tree-item.border{
border-bottom: 1px solid #eee;
}
.mix-tree-item.show{
height: 80upx;
opacity: 1;
}
.mix-tree-icon{
width: 26upx;
height: 26upx;
margin-right: 8upx;
opacity: .9;
}
.mix-tree-item.showchild:before{
transform: rotate(90deg);
}
.mix-tree-item.last:before{
opacity: 0;
}
</style>
- 主要新增代码为:
//liy:实现默认展开所有子级
this.treeList.forEach(childItem=>{
childItem.show = true;
});
问题
:因为代表子级是否显示的标志位showChild默认为false(showChild: false
),即默认所有子级都不显示;所以如果仅实现展开每个子级而不重新设置其父级的showChild标志位,那么第一次点击条目则不会收缩子项。
解决方案1
:还需将标志位showChild默认设置为true(showChild: true
)
解决方案2
:
//liy:实现默认展开所有子级
this.treeList.forEach(childItem=>{
childItem.show = true;
//解决第一次点击条目不会收缩子项的问题(默认所有子级都不显示,所有这里要重新赋值),或者showChild默认值该为true即可
if(childItem.rank != 0){
this.treeList[childItem.rank -1].showChild = true;
}
});
2.3 功能拓展 — 实现选中中间项实现默认仅展开第二级
在 2.2 功能拓展 — 实现默认列表子项全展开 的基础上修改代码:
//liy:实现默认展开所有子级
/**this.treeList.forEach(childItem=>{
childItem.show = true;
//解决第一次点击条目不会收缩子项的问题(默认所有子级都不显示,所有这里要重新赋值),或者showChild默认值该为true即可
if(childItem.rank != 0){
this.treeList[childItem.rank -1].showChild = true;
}
});**/
//liy:实现默认仅展开第二级
this.treeList.forEach(childItem=>{
if(childItem.rank == 1){//层级从0开始,1代表第二级
childItem.show = true;//展开第二级
this.treeList[0].showChild = true;//解决第一次点击条目不会收缩子项的问题(默认所有子级都不显示,所有这里要重新赋值)
}
});
。