最近的项目要求是做一个树状的多选功能,然而该项目是使用vant4作为前端的框架,vant4只有树状单选,没有多选的,那只能自己写一个了。
借鉴博主 https://blog.csdn.net/m0_68428581/article/details/130641982,
我将他的代码转成了vue3的语法,并且根据自己的项目需求进了相关改动,终于实现了这个需求。
首先需要完成两个封装组件的文件:
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 setup> import scrollView from './scrollView.vue' import { ref,onMounted,computed } from 'vue' const emit = defineEmits(['selectChange']) const props = defineProps({ 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 }, showState: { type: Boolean, default: false } }) //const showDialog = ref(false) const treeList = ref([]) const _show = () =>{ showDialog.value = true } const _hide = () =>{ showDialog.value = false } const _cancel =() => { emit("canceled", false) } const _confirm = () => { let selectedList = []; //如果子集全部选中,只返回父级 id let selectedNames; let currentLevel = -1; treeList.value.forEach((item, index) => { //console.log(item) 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); _cancel() emit("selectChange", selectedList, selectedNames); } //格式化原数据(原数据为tree结构) const _formatTreeData = (list = [], level = 0, parentItem, isShowChild = true) => { let nextIndex = 0; let parentId = -1; let initCheckStatus = 0; if (parentItem) { nextIndex = treeList.value.findIndex(item => item.id === parentItem.id) + 1; parentId = parentItem.id; if (!props.multiple) { //单选 initCheckStatus = 0; } else initCheckStatus = parentItem.checkStatus == 2 ? 2 : 0; } list.forEach(item => { let isLastLevel = true; if (item && item[props.childrenKey]) { let children = item[props.childrenKey]; if (Array.isArray(children) && children.length > 0) { isLastLevel = false; } } let itemT = { id: item[props.valueKey], name: item[props.textKey], level, isLastLevel, isShow: isShowChild, isShowChild: false, checkStatus: initCheckStatus, orCheckStatus: 0, parentId, children: item[props.childrenKey], childCount:item[props.childrenKey]?item[props.childrenKey].length:0, childCheckCount: 0, childCheckPCount: 0 } if (props.selectedData.indexOf(itemT.id) >= 0) { itemT.checkStatus = 2; itemT.orCheckStatus = 2; itemT.childCheckCount = itemT.children?itemT.children.length :0; _onItemParentSelect(itemT, nextIndex); } treeList.value.splice(nextIndex, 0, itemT); nextIndex++; }) //console.log(this.treeList); } const _onItemSwitch = (item, index) => {// 节点打开、关闭切换 // console.log(item) //console.log('_itemSwitch') if (item.isLastLevel === true) { return; } item.isShowChild = !item.isShowChild; if (item.children) { _formatTreeData(item.children, item.level + 1, item); item.children = undefined; } else { _onItemChildSwitch(item, index); } } const _onItemChildSwitch = (item, index) => { //console.log('_onItemChildSwitch') const firstChildIndex = index + 1; if (firstChildIndex > 0) for (var i = firstChildIndex; i < treeList.value.length; i++) { let itemChild = treeList.value[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; } } } // 节点选中、取消选中 const _onItemSelect = (item, index) => { //console.log('_onItemSelect') //console.log(item) if (!props.multiple) { //单选 item.checkStatus = item.checkStatus == 0 ? 2 : 0; treeList.value.forEach((v, i) => { if (i != index) { treeList.value[i].checkStatus = 0 } else { treeList.value[i].checkStatus = 2 } }) let selectedList = []; let selectedNames; selectedList.push(item.id); selectedNames = item.name; _hide() emit("selectChange", 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; } //子节点 全部选中 _onItemChildSelect(item, index); //父节点 选中状态变化 _onItemParentSelect(item, index, oldCheckStatus); } const _onItemChildSelect = (item, index) => { //console.log('_onItemChildSelect') let allChildCount = 0; if (item.childCount && item.childCount > 0) { index++; while (index < treeList.value.length && treeList.value[index].level > item.level) { let itemChild = treeList.value[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++; } } } const _onItemParentSelect = (item, index, oldCheckStatus) => { //console.log('_onItemParentSelect') //console.log(item) const parentIndex = treeList.value.findIndex(itemP => itemP.id == item.parentId); //console.log('parentIndex:' + parentIndex) if (parentIndex >= 0) { let itemParent = treeList.value[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) _onItemParentSelect(itemParent, parentIndex, oldCheckStatusParent); } } // 重置数据 const _reTreeList = () => { treeList.value.forEach((v, i) => { treeList.value[i].checkStatus = v.orCheckStatus }) } const _initTree = () => { treeList.value = []; _formatTreeData(props.localdata); } // watch([localdata,localTreeList],(newValue, oldValue)=>{ // _initTree() // treeList.value = props.localTreeList; // }) const showDialog = computed(()=>{ return props.showState; }) onMounted(()=>{ _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 { /* */ 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>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 setup> import { ref,onActivated } from 'vue' const props = defineProps({ scrollX:{ type:[String,Boolean], value:false }, scrollY:{ type:[String,Boolean], value:false } }) const scrollTop = ref(0) const getScroll = (e) => { //滚动事件 let wScrollY = e.target.scrollTop; // 当前滚动条位置 scrollTop.value = wScrollY; let wInnerH = e.target.clientHeight; // 设备窗口的高度(不会变) let bScrollH = e.target.scrollHeight; // 元素总高度 // if (wScrollY + wInnerH >= bScrollH) { // emit("reachBottom"); // } } // onActivated(()=>{ // emit('getScrollTop',{scrollTop:scrollTop.value}) // }) </script> <style scoped> .my_scroll_container{ width: 100%; height: 250px; } </style>父组件使用:
<template> ...... <van-field v-model="fieldValue" is-link readonly label="地区" placeholder="请选择所在地区" @click="showTree" /> <baTreePicker :multiple='true' @selectChange="selectChange" title="选择所在地区" :localdata="copyAreaOptions" valueKey="areaId" textKey="areaName" childrenKey="children" @canceled="cancelTree" :showState="show" /> ...... </template> //multiple可以选择单选和多选,copyAreaOptions是需要显示的树状数据,valueKey需要改成你的数据id,textKey需要改成显示的文字,showState是显示状态(显示or隐藏) <script setup> import baTreePicker from "../../component/treePicker/ba-tree-picker.vue" const fieldValue = ref('') const show = ref(false) const copyAreaOptions = ref([]) const cancelTree = (val) => {//关闭弹窗 show.value = val } const showTree = () => {//显示弹窗 show.value = true } const selectChange = (ids, names) => {//监听选择 注意,这里 如果选择的是树的父级,返回的ids也只是一个父级id,如需要获取父级下面的所以子集,则需要对数据进行操作 //console.log(ids, names) fieldValue.value = names } </script>
标签:vue,const,多选,树状,value,item,let,checkStatus,border From: https://www.cnblogs.com/luzanzan/p/17761841.html