效果图:
前言
组件引用 uni-app 中 uni-drawer,uni-icons 组件,请自行引入。
代码
treeSelect.vue
<template>
<view class="tree">
<view class="tree-meta">
<scroll-view scroll-with-animation :show-scrollbar="false" scroll-x class="tree-meta-breadcrumb"
:scroll-into-view="intoView">
<span @click="clearBreadCrumb">银河系</span>
<span :id="'breadCrumb' + item[props.value]" v-for="(item,index) in breadCrumb" :key="index"
@click="clickBreadCrumb(item)">{{item.title}}</span>
</scroll-view>
</view>
<scroll-view scroll-y class="tree-list">
<view class="tree-list-item" v-for="(item,index) in list" :key="index" v-if="list.length > 0">
<checkbox-group @change="selectRow($event, item)">
<checkbox :value="item[props.value]" :checked="isCheck(item[props.value])" />
</checkbox-group>
<view class="tree-list-info" @click="handleClick(item)">
<image src="./dept.png" mode="widthFix"></image>
<view>
{{item[props.label]}}
</view>
</view>
<uni-icons type="forward" size="20" v-if="item[props.children].length > 0"></uni-icons>
</view>
<view class="nodata" style="padding-top:40rpx;" v-if="list.length == 0">
<image src="./nodata.png" mode="widthFix"></image>
<view class="nodata-text">暂无数据</view>
</view>
</scroll-view>
<view class="tree-box">
<view class="tree-box-info" @click="chooseVisible = !chooseVisible">
<template v-if="chooseVisible">
收起 <uni-icons type="bottom" size="20"></uni-icons>
</template>
<template v-else>
已选中({{selectList.length}}) <uni-icons type="top" size="20"></uni-icons>
</template>
</view>
<view class="tree-box-btn success" @click="confirm">确认</view>
</view>
<view class="tree-choose" :class="{on:chooseVisible}">
<view class="tree-choose-mask" @click="chooseVisible = false"></view>
<view class="tree-choose-inner">
<view class="tree-choose-title">
<view>已选中({{selectList.length}})</view>
<view @click="clearSelect">清空</view>
</view>
<view class="tree-list" v-if="selectList.length > 0">
<view class="tree-list-item" v-for="(item,index) in selectList" :key="index">
<view class="tree-list-info">
<image src="./dept.png" mode="widthFix"></image>
<view>{{item[props.label]}}</view>
</view>
<uni-icons type="closeempty" size="20" @click="removeSelectRow(index)"></uni-icons>
</view>
</view>
<view class="nodata" style="padding-top:40rpx;" v-else>
<image src="./nodata.png" mode="widthFix"></image>
<view class="nodata-text">暂无选中地区</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
// 回显值
value: {
type: Array,
default: () => []
},
// 树数据
treeData: {
type: Array,
default: () => []
},
// 树字段值
props: {
type: Object,
default: () => ({
label: "label",
value: "id",
children: "children"
})
},
},
data() {
return {
// 面包屑
breadCrumb: [],
// 设置面包屑位置
intoView: "",
// 当前展示数据
list: this.treeData,
// 选中列表
selectList: this.value || [],
// 是否显示已选地区列表
chooseVisible: false
};
},
watch: {
// 异步请求回显
value(val) {
this.selectList = val;
}
},
methods: {
// 添加面包屑
addBreadCrumb(row) {
this.breadCrumb.push({
id: row[this.props.value],
title: row[this.props.label],
list: row[this.props.children]
});
this.autoLower(row[this.props.value]);
},
// 点击面包屑
clickBreadCrumb(row) {
this.autoLower(row[this.props.value]);
const index = this.breadCrumb.findIndex(item => item[this.props.value] == row[this.props.value]);
// 删除点击索引后的所有数据
this.breadCrumb.splice(index + 1, this.breadCrumb.length - 1);
this.list = this.breadCrumb[index].list;
},
// 清空面包屑
clearBreadCrumb() {
this.breadCrumb = [];
this.list = this.treeData;
},
// 面包屑自动靠右
autoLower(id) {
this.$nextTick(() => {
this.intoView = `breadCrumb${id}`
});
},
// 点击列表项
handleClick(row) {
// 没有子级则不需要执行操作
if (row[this.props.children].length == 0) return;
this.addBreadCrumb(row);
this.list = row[this.props.children];
},
// 判断列表项是否选中
isCheck(id) {
return this.selectList.findIndex(item => item[this.props.value] === id) > -1;
},
// 选中
selectRow(e, row) {
// 选中
if (e.detail.value.length > 0) {
this.selectList.push(row);
}
// 取消选中
else {
const index = this.list.findIndex(item => item[this.props.value] == row[this.props.value]);
this.selectList.splice(index, 1);
}
},
// 删除选中
removeSelectRow(index) {
this.selectList.splice(index, 1);
},
// 清空选中
clearSelect() {
uni.showModal({
title: "提示",
content: "确认清空已选中数据吗?",
success: (res) => {
if (res.confirm) {
this.selectList = [];
}
}
})
},
// 点击确认
confirm() {
const selectIds = this.selectList.map(item => item[this.props.value]).join(",");
const selectNames = this.selectList.map(item => item[this.props.label]).join(",");
const result = {
selectIds,
selectNames,
selectList: this.selectList
}
this.$emit("confirm", result);
}
}
}
</script>
<style lang="less">
.tree {
height: 100vh;
background: #f2f6fa;
display: flex;
flex-direction: column;
&-meta {
background: #fff;
display: flex;
line-height: 96rpx;
margin-bottom: 20rpx;
&-breadcrumb {
flex: 1;
padding: 0 40rpx;
white-space: nowrap;
overflow: auto;
span {
color: #8c8c8c;
&::after {
font-family: "uniicons" !important;
content: "\e6ba";
margin: 0 16rpx;
}
&:last-child {
color: #000;
&::after {
display: none;
}
}
}
}
}
// 主体区域
&-list {
flex: 1;
overflow: auto;
background: #fff;
&-item {
display: flex;
align-items: center;
padding: 24rpx 40rpx;
border-bottom: 1px solid #e6e6e6;
i {
font-size: 40rpx;
color: #666;
}
}
&-info {
flex: 1;
display: flex;
align-items: center;
uni-checkbox {
margin-right: 20rpx;
}
image {
width: 64rpx;
height: 64rpx;
margin-right: 16rpx;
}
view {
font-size: 32rpx;
&.red {
color: #dd524d;
}
}
}
}
// 结果集
&-box {
position: relative;
z-index: 3;
border-top: 1px solid #e6e6e6;
background: #fff;
display: flex;
font-size: 36rpx;
&-info {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
line-height: 116rpx;
span {
color: #004986;
margin: 0 16rpx;
}
}
&-btn {
height: 116rpx;
line-height: 116rpx;
width: 216rpx;
text-align: center;
&.success {
background: #004986;
color: #fff;
}
&.danger {
background: #dd524d;
color: #fff;
}
}
}
// 当前选中
&-choose {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 2;
visibility: hidden;
&.on {
visibility: initial;
.tree-choose-mask {
opacity: 1;
}
.tree-choose-inner {
bottom: 116rpx;
}
}
&-mask {
opacity: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
transition: all .3s ease;
}
&-inner {
position: absolute;
bottom: -820rpx;
width: 100%;
background: #fff;
height: 700rpx;
display: flex;
flex-direction: column;
transition: all .3s ease;
}
&-title {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 40rpx;
line-height: 96rpx;
border-bottom: 1px solid #e3e3e3;
view {
font-size: 32rpx;
color: #8c8c8c;
}
span {
color: #004986;
}
}
&-content {}
}
}
.nodata {
padding: 30rpx 0;
text-align: center;
image {
width: 500rpx;
}
&-text {
font-size: 28rpx;
color: #999;
}
}
</style>
注:引用 dept.png 和 nodata.png 两张图片,请自备图片资源。
treeSelectDrawer.vue
<template>
<view class="tree-input">
<uni-drawer ref="uniDrawer" mode="right" :mask-click="true">
<selectTree :value="value" :treeData="treeData" :props="props" @confirm="handleClick"></selectTree>
</uni-drawer>
</view>
</template>
<script>
export default {
name: "selectTreeDrawer",
props: {
treeData: {
type: Array,
default: () => []
},
// 回显值
value: {
type: Array,
default: () => []
},
props: {
type: Object,
default: () => ({
label: "label",
value: "id",
children: "children"
})
},
},
methods: {
open() {
this.$refs.uniDrawer.open();
},
close() {
this.$refs.uniDrawer.close();
},
handleClick(result) {
this.close();
this.$emit("confirm", result);
}
}
}
</script>
注:uni-drawer组件默认宽度被我改成了100%。
基本用法
<template>
<view class="content">
<text @click="selectTree">
{{ name || "请选择" }}
</text>
<view v-if="id">选中项ids:{{ id }}</view>
<selectTreeDrawer ref="selectTree" :value="selectList" :treeData="treeData" @confirm="confirm">
</selectTreeDrawer>
</view>
</template>
<script>
import selectTreeDrawer from "path/selectTreeDrawer.vue";
export default {
components:{
selectTreeDrawer
},
data() {
return {
id: "",
name: "",
// 选中列表
selectList: [],
treeData: [{
id: "1",
label: "太阳系",
children: [{
id: "10",
label: "水星",
children: []
}, {
id: "11",
label: "金星",
children: []
}, {
id: "12",
label: "地球",
children: [{
id: "121",
label: "中国",
children: [{
id: "1211",
label: "北京市",
children: [{
id: "12111",
label: "东城区",
children: []
}, {
id: "12112",
label: "西城区",
children: []
}]
}, {
id: "1212",
label: "上海市",
children: []
}, {
id: "1213",
label: "深圳市",
children: []
}, {
id: "1214",
label: "广州市",
children: []
}, ]
}, {
id: "122",
label: "俄罗斯",
children: []
}, {
id: "123",
label: "法国",
children: []
}, {
id: "124",
label: "美国",
children: []
}, {
id: "125",
label: "英国",
children: []
}, ]
}, {
id: "13",
label: "火星",
children: []
}, {
id: "14",
label: "木星",
children: []
}, {
id: "15",
label: "土星",
children: []
}, {
id: "16",
label: "天王星",
children: []
}, {
id: "17",
label: "海王星",
children: []
}]
}]
}
},
methods: {
selectTree() {
this.$refs.selectTree.open();
},
// 点击确认
confirm(e) {
const {
selectIds,
selectNames,
selectList
} = e;
this.id = selectIds;
this.name = selectNames;
this.selectList = selectList;
}
}
}
</script>
可通过传递 props ,修改树结构对应的 label,value,children参数名
treeData:[{
deptId:"1",
deptName:"北京市",
deptChild:[{
deptId:"11",
deptName:"东城区",
deptChild:[]
}]
}]
组件使用:
<selectTreeDrawer ref="selectTree" :value="selectList" :treeData="treeData" :props="props" @confirm="confirm"></selectTreeDrawer>
<!--
props:{
value: "deptId",
label:"deptName",
children: "deptChild"
}
-->