使用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 = []; //如果子集全部选中,只返回父级 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>

 

 参考 https://blog.csdn.net/m0_68428581/article/details/130641982
 
posted @   妞妞猪  阅读(1655)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示