Bootstrap

uniapp - 树选择组件

效果图:

在这里插入图片描述

前言

组件引用 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"
}
 -->
;