一.效果图

 二.代码

1.新建文件夹tree-picker,包含两个文件 ba-tree-picker.vue 和 scrollView.vue

(1)ba-tree-picker.vue

<!-- 树形层级选择器-->
<!-- 支持单选、多选 -->
<template>
	<div>
		<div class="tree-cover" :class="{'show':showDialog}" @click="_cancel"></div>
		<div class="tree-dialog" :class="{'show':showDialog}">
			<div class="tree-bar">
				<div class="tree-bar-cancel" :style="{'color':cancelColor}" 
                     hover-class="hover-c" @click="_cancel"
                >取消</div>
				<div class="tree-bar-title" :style="{'color':titleColor}">{{title}}</div>
				<div class="tree-bar-confirm" :style="{'color':confirmColor}" 
                   hover-class="hover-c" @click="_confirm"
                >
					{{multiple?'确定':''}}
				</div>
			</div>
			<div class="tree-div">
				<scroll-view class="tree-list" :scroll-y="true">		
					<div v-for="(item, index) in treeList" :key="index">
						<div class="tree-item" 
                          :style="[{paddingLeft: item.level*30 + 'px'}]" 
						  :class="{itemBorder: border === true,show: item.isShow}"
						>
						  <div class="item-label">
							  <div class="item-icon uni-inline-item" 
                                   @click="_onItemSwitch(item, index)"
                              >
									<div v-if="!item.isLastLevel&&item.isShowChild" 
                                         class="switch-on"
										:style="{'border-left-color':switchColor}">
									</div>
									<div v-else-if="!item.isLastLevel&&!item.isShowChild" 
                                        class="switch-off"
										:style="{'border-top-color':switchColor}"
                                    ></div>
									<div v-else class="item-last-dot" 
                                        :style="{'border-top-color':switchColor}">
									</div>
							 </div>
							 <div class="item-name"> 
                               {{item.name+(item.childCount?"("+item.childCount+")":'')}}
							 </div>
							 <div class="uni-flex-item uni-inline-item" 
                                 @click="_onItemSelect(item, index)"
                             >
								  <div class="item-check" 
                                     v-if="selectParent?true:item.isLastLevel"
                                  >
									 <div class="item-check-yes"
                                        v-if="item.checkStatus==1"
										:class="{'radio':!multiple}" 
                                        :style="{'border-color':confirmColor}"
                                     >
										<div class="item-check-yes-part"
											:style="{'background-color':confirmColor}"
                                        ></div>
									 </div>
									 <div class="item-check-yes" 
                                         v-else-if="item.checkStatus==2"
										 :class="{'radio':!multiple}" 
                                         :style="{'border-color':confirmColor}"
                                     >
										<div class="item-check-yes-all" 
                                             :style="{'background-color':confirmColor}"
                                        ></div>
									 </div>
									 <div class="item-check-no" v-else 
                                         :class="{'radio':!multiple}"
										 :style="{'border-color':confirmColor}"
                                     ></div>
								  </div>
							   </div>
							</div>
						</div>
					</div>
				</scroll-view>
			</div>
		</div>
	</div>
</template>

<script>
import scrollView from "@/components/tree-picker/scrollView.vue"

	export default {
		emits: ['select-change'],
		name: "ba-tree-picker",
		components: {
			scrollView
		},
		props: {
			valueKey: {
				type: String,
				default: 'id'
			},
			textKey: {
				type: String,
				default: 'name'
			},
			childrenKey: {
				type: String,
				default: 'children'
			},
			localdata: {
				type: Array,
				default: function() {
					return []
				}
			},
			localTreeList: { //在已经格式化好的数据
				type: Array,
				default: function() {
					return []
				}
			},
			selectedData: {
				type: Array,
				default: function() {
					return []
				}
			},
			title: {
				type: String,
				default: ''
			},
			multiple: { // 是否可以多选
				type: Boolean,
				default: true
			},
			selectParent: { //是否可以选父级
				type: Boolean,
				default: true
			},
			confirmColor: { // 确定按钮颜色
				type: String,
				default: '' // #0055ff
			},
			cancelColor: { // 取消按钮颜色
				type: String,
				default: '' // #757575
			},
			titleColor: { // 标题颜色
				type: String,
				default: '' //
			},
			switchColor: { // 节点切换图标颜色
				type: String,
				default: '' // #666
			},
			border: { // 是否有分割线
				type: Boolean,
				default: false
			},
		},
		data() {
			return {
				showDialog: false,
				treeList: []
			}
		},
		computed: {},
		methods: {
			_show() {
				this.showDialog = true
			},
			_hide() {
				this.showDialog = false
			},
			_cancel() {
				this._hide()
				this.$emit("cancel", '');
			},
			_confirm() { //多选
				let selectedList = []; //如果子集全部选中,只返回父级 id
				let selectedNames;
				let currentLevel = -1;
				this.treeList.forEach((item, index) => {
					if (currentLevel >= 0 && item.level > currentLevel) {

					} else {
						if (item.checkStatus === 2) {
							currentLevel = item.level;
							selectedList.push(item.id);
							selectedNames = selectedNames ? selectedNames + ' / ' + 
                            item.name : item.name;
						} else {
							currentLevel = -1;
						}
					}
				})
				//console.log('_confirm', selectedList);
				this._hide()
				this.$emit("select-change", selectedList, selectedNames);
			},
			//格式化原数据(原数据为tree结构)
			_formatTreeData(list = [], level = 0, parentItem, isShowChild = true) {
				let nextIndex = 0;
				let parentId = -1;
				let initCheckStatus = 0;
				if (parentItem) {
					nextIndex = this.treeList.findIndex(item =>
                    item.id === parentItem.id) + 1;
					parentId = parentItem.id;
					if (!this.multiple) { //单选
						initCheckStatus = 0;
					} else
						initCheckStatus = parentItem.checkStatus == 2 ? 2 : 0;
				}
				list.forEach(item => {
					let isLastLevel = true;
					if (item && item[this.childrenKey]) {
						let children = item[this.childrenKey];
						if (Array.isArray(children) && children.length > 0) {
							isLastLevel = false;
						}
					}

					let itemT = {
						id: item[this.valueKey],
						name: item[this.textKey],
						level,
						isLastLevel,
						isShow: isShowChild,
						isShowChild: false,
						checkStatus: initCheckStatus,
						orCheckStatus: 0,
						parentId,
						children: item[this.childrenKey],
					   childCount:item[this.childrenKey]?item[this.childrenKey].length:0,
					   childCheckCount: 0,
					   childCheckPCount: 0
					};

					if (this.selectedData.indexOf(itemT.id) >= 0) {
						itemT.checkStatus = 2;
						itemT.orCheckStatus = 2;
						itemT.childCheckCount = itemT.children?itemT.children.length :0;
						this._onItemParentSelect(itemT, nextIndex);
					}

					this.treeList.splice(nextIndex, 0, itemT);
					nextIndex++;
				})
				//console.log(this.treeList);
			},
			// 节点打开、关闭切换
			_onItemSwitch(item, index) {
				// console.log(item)
				//console.log('_itemSwitch')
				if (item.isLastLevel === true) {
					return;
				}
				item.isShowChild = !item.isShowChild;
				if (item.children) {
					this._formatTreeData(item.children, item.level + 1, item);
					item.children = undefined;
				} else {
					this._onItemChildSwitch(item, index);
				}
			},
			_onItemChildSwitch(item, index) {
				//console.log('_onItemChildSwitch')
				const firstChildIndex = index + 1;
				if (firstChildIndex > 0)
					for (var i = firstChildIndex; i < this.treeList.length; i++) {
						let itemChild = this.treeList[i];
						if (itemChild.level > item.level) {
							if (item.isShowChild) {
								if (itemChild.parentId === item.id) {
									itemChild.isShow = item.isShowChild;
									if (!itemChild.isShow) {
										itemChild.isShowChild = false;
									}
								}
							} else {
								itemChild.isShow = item.isShowChild;
								itemChild.isShowChild = false;
							}
						} else {
							return;
						}
					}
			},
			// 节点选中、取消选中
			_onItemSelect(item, index) {
				//console.log('_onItemSelect')
				//console.log(item)
				if (!this.multiple) { //单选
					item.checkStatus = item.checkStatus == 0 ? 2 : 0;

					this.treeList.forEach((v, i) => {
						if (i != index) {
							this.treeList[i].checkStatus = 0
						} else {
							this.treeList[i].checkStatus = 2
						}
					})

					let selectedList = [];
					let selectedNames;
					selectedList.push(item.id);
					selectedNames = item.name;
					this._hide()
					this.$emit("select-change", selectedList, selectedNames);
					return
				}

				let oldCheckStatus = item.checkStatus;
				switch (oldCheckStatus) {
					case 0:
						item.checkStatus = 2;
						item.childCheckCount = item.childCount;
						item.childCheckPCount = 0;
						break;
					case 1:
					case 2:
						item.checkStatus = 0;
						item.childCheckCount = 0;
						item.childCheckPCount = 0;
						break;
					default:
						break;
				}
				//子节点 全部选中
				this._onItemChildSelect(item, index);
				//父节点 选中状态变化
				this._onItemParentSelect(item, index, oldCheckStatus);
			},
			_onItemChildSelect(item, index) {
				//console.log('_onItemChildSelect')
				let allChildCount = 0;
				if (item.childCount && item.childCount > 0) {
					index++;
					while (index < this.treeList.length && 
                           this.treeList[index].level > item.level)
                    {
						let itemChild = this.treeList[index];
						itemChild.checkStatus = item.checkStatus;
						if (itemChild.checkStatus == 2) {
							itemChild.childCheckCount = itemChild.childCount;
							itemChild.childCheckPCount = 0;
						} else if (itemChild.checkStatus == 0) {
							itemChild.childCheckCount = 0;
							itemChild.childCheckPCount = 0;
						}

						index++;
					}
				}
			},
			_onItemParentSelect(item, index, oldCheckStatus) {
				//console.log('_onItemParentSelect')
				//console.log(item)
				const parentIndex = this.treeList.findIndex(itemP => itemP.id == 
                item.parentId);
				//console.log('parentIndex:' + parentIndex)
				if (parentIndex >= 0) {
					let itemParent = this.treeList[parentIndex];
					let count = itemParent.childCheckCount;
					let oldCheckStatusParent = itemParent.checkStatus;

					if (oldCheckStatus == 1) {
						itemParent.childCheckPCount -= 1;
					} else if (oldCheckStatus == 2) {
						itemParent.childCheckCount -= 1;
					}
					if (item.checkStatus == 1) {
						itemParent.childCheckPCount += 1;
					} else if (item.checkStatus == 2) {
						itemParent.childCheckCount += 1;
					}
					if (itemParent.childCheckCount<=0 && itemParent.childCheckPCount<=0){
						itemParent.childCheckCount = 0;
						itemParent.childCheckPCount = 0;
						itemParent.checkStatus = 0;
					} else if (itemParent.childCheckCount >= itemParent.childCount) {
						itemParent.childCheckCount = itemParent.childCount;
						itemParent.childCheckPCount = 0;
						itemParent.checkStatus = 2;
					} else {
						itemParent.checkStatus = 1;
					}
					//console.log('itemParent:', itemParent)
					this._onItemParentSelect(itemParent, parentIndex, 
                    oldCheckStatusParent);
				}
			},
			// 重置数据
			_reTreeList() {
				this.treeList.forEach((v, i) => {
					this.treeList[i].checkStatus = v.orCheckStatus
				})
			},
			_initTree() {
				this.treeList = [];
				this._formatTreeData(this.localdata);
			}
		},
		watch: {
			localdata() {
				this._initTree();
			},
			localTreeList() {
				this.treeList = this.localTreeList;
			}
		},
		mounted() {
			this._initTree();
		}
	}
</script>

<style scoped>
	.tree-cover {
		position: fixed;
		top: 0px;
		right: 0px;
		bottom: 0px;
		left: 0px;
		z-index: 100;
		background-color: rgba(0, 0, 0, .4);
		opacity: 0;
		transition: all 0.3s ease;
		visibility: hidden;
	}

	.tree-cover.show {
		visibility: visible;
		opacity: 1;
	}

	.tree-dialog {
		position: fixed;
		top: 0px;
		right: 0px;
		bottom: 0px;
		left: 0px;
		background-color: #fff;
		border-top-left-radius: 10px;
		border-top-right-radius: 10px;
		/* #ifndef APP-NVUE */
		display: flex;
		/* #endif */
		flex-direction: column;
		z-index: 102;
		top: 20%;
		transition: all 0.3s ease;
		transform: translateY(100%);
	}

	.tree-dialog.show {
		transform: translateY(0);
	}

	.tree-bar {
		/* background-color: #fff; */
		height: 40px;
		padding-left: 15px;
		padding-right: 25px;
		display: flex;
		justify-content: space-between;
		align-items: center;
		box-sizing: border-box;
		border-bottom-width: 1px !important;
		border-bottom-style: solid;
		border-bottom-color: #f5f5f5;
		font-size: 15px;
		color: #757575;
		line-height: 1;
	}

	.tree-bar-confirm {
		color: #0055ff;
		padding: 15px;
	}

	.tree-bar-title {
		color: #007aff
	}

	.tree-bar-cancel {
		color: #757575;
		padding: 10px;
	}

	.tree-div {
		flex: 1;
		padding: 20px 0 10px 0px;
		/* #ifndef APP-NVUE */
		display: flex;
		/* #endif */
		flex-direction: column;
		overflow: hidden;
		height: 100%;
	}

	.tree-list {
		flex: 1;
		height: 100%;
		overflow: hidden;
	}

	.tree-item {
		display: flex;
		justify-content: space-between;
		align-items: center;
		line-height: 1;
		height: 0;
		opacity: 0;
		transition: 0.2s;
		overflow: hidden;
	}

	.tree-item.show {
		height: 35px;
		opacity: 1;
		padding: 0 15px 0 0
	}

	.tree-item.showchild:before {
		transform: rotate(90deg);
	}

	.tree-item.last:before {
		opacity: 0;
	}

	.switch-on {
		width: 0;
		height: 0;
		border-left: 6px solid transparent;
		border-right: 6px solid transparent;
		border-top: 10px solid #666;
	}

	.switch-off {
		width: 0;
		height: 0;
		border-bottom: 6px solid transparent;
		border-top: 6px solid transparent;
		border-left: 10px solid #666;
	}

	.item-last-dot {
		position: absolute;
		width: 0px;
		height:0px;
		border-radius: 100%;
		background: #666;
	}

	.item-icon {
		width:0px;
		height: 8px;
		/* margin-right: 8px; */
		padding-right: 20px;
		padding-left: 20px;
	}

	.item-label {
		flex: 1;
		display: flex;
		align-items: center;
		height: 100%;
		line-height: 1.2;
	}

	.item-name {
		flex: 1;
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
		width: 100%;
		text-align: left;
		font-size: 14px;
	}

	.item-check {
		width: 40px;
		height: 40px;
		display: flex;
		justify-content: center;
		align-items: center;
	}

	.item-check-yes,
	.item-check-no {
		width: 20px;
		height: 20px;
		border-top-left-radius: 20%;
		border-top-right-radius: 20%;
		border-bottom-right-radius: 20%;
		border-bottom-left-radius: 20%;
		border-top-width: 1px;
		border-left-width: 1px;
		border-bottom-width: 1px;
		border-right-width: 1px;
		border-style: solid;
		border-color: #0055ff;
		display: flex;
		justify-content: center;
		align-items: center;
		box-sizing: border-box;
	}

	.item-check-yes-part {
		width: 12px;
		height: 12px;
		border-top-left-radius: 20%;
		border-top-right-radius: 20%;
		border-bottom-right-radius: 20%;
		border-bottom-left-radius: 20%;
		background-color: #0055ff;
	}

	.item-check-yes-all {
		margin-bottom: 5px;
		border: 2px solid #007aff;
		border-left: 0;
		border-top: 0;
		height: 12px;
		width: 6px;
		transform-origin: center;
		/* #ifndef APP-NVUE */
		transition: all 0.3s;
		/* #endif */
		transform: rotate(45deg);
	}

	.item-check .radio {
		border-top-left-radius: 50%;
		border-top-right-radius: 50%;
		border-bottom-right-radius: 50%;
		border-bottom-left-radius: 50%;
	}

	.item-check .radio .item-check-yes-b {
		border-top-left-radius: 50%;
		border-top-right-radius: 50%;
		border-bottom-right-radius: 50%;
		border-bottom-left-radius: 50%;
	}

	.hover-c {
		opacity: 0.6;
	}

	.itemBorder {
		border-bottom: 1px solid #e5e5e5;
	}
</style>

(2)scrollView.vue

<template>
    <div v-bind:class="{'my_scroll_container':true}" 
       @scroll="getScroll" 
       :style="`${scrollX==='true'||
            scrollX===true?'overflow-x:scroll;overflow-y:hidden':''};${scrollY==='true'||
            scrollY===true?'overflow-x:hidden;overflow-y:scroll':''};`"
    >
        <slot></slot>
    </div>
</template>
<script>
export default {
    props: {
        scrollX:{
        type:[String,Boolean],
        value:false
        },
        scrollY:{
        type:[String,Boolean],
        value:false
        }
    },
    data() {
        return {
        scrollTop: 0
        };
    },
    methods: {
        //滚动事件
        getScroll(e) {
          let wScrollY = e.target.scrollTop; // 当前滚动条位置
          this.scrollTop = wScrollY;
          let wInnerH = e.target.clientHeight; // 设备窗口的高度(不会变)
          let bScrollH = e.target.scrollHeight; // 元素总高度
          if (wScrollY + wInnerH >= bScrollH) {
             this.$emit("reachBottom");
          }
        }
    },
    activated() {
        this.$el.scrollTop = this.scrollTop;
        this.$emit('getScrollTop',{scrollTop:this.scrollTop});
    },
    updated() {},
    mounted() {}
};
</script>
<style scoped>
.my_scroll_container{
   width: 100%;
  height: 250px;
}
</style>

2.使用

<template>
  <ba-tree-picker 
    ref="treePicker" 
	:multiple='false'
	@select-change="selectChange" 
	title="选择属地"
	:localdata="copyAreaOptions" 
    valueKey="id" 
    textKey="label" 
    childrenKey="children" 
  />  
</template>

<script>
import baTreePicker from "@/components/tree-picker/ba-tree-picker.vue"
export default {
  components: {
	baTreePicker
  },
  data() {
	return {
	  copyAreaOptions:[],
    }
  }.
  created() {
	this.getTreeselect()
  },
  methods: {
    // 显示选择器
	showPicker(){
		this.$refs.treePicker._show();
	},
    /** 查询下拉树结构 */
	getTreeselect() {
         //接口数据
		 this.copyAreaOptions= XXX
	},
    //监听选择(ids为数组)
	selectChange(ids, names) {
		console.log(ids, names)
	}
  }
}
</script>

在我的项目中使用

ps:找了好久才整好,暂时用着没有什么问题

附:数据格式

Logo

前往低代码交流专区

更多推荐