使用vue在移动端显示树状多选功能
最近的项目要求是做一个树状的多选功能,然而该项目是使用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 = []; //如果子集全部选中,只返回父级 idlet 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;} elseinitCheckStatus = 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-fieldv-model="fieldValue"is-linkreadonlylabel="地区"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>
参考 https://blog.csdn.net/m0_68428581/article/details/130641982
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律