记录checkbox 树形组件

77 阅读4分钟

顾问要求展示多层级的地区选择,市面上有的不是结构不符合就是不能多选,没办法,只能自己写一个组件。

树形组件还是第一次做,好多地方用到了递归,参考了blog.csdn.net/bushanyanta… 可惜样式不符合,只能借鉴里面的思路

IMG20240816-152037.png

IMG20240816-152044.png

IMG20240816-152049.png

贴一个动图

_VID_20240816160627.gif

效果如上,记录下代码和思路:
// CascaderCheckboxAddr.vue
<template>
    <div class="bu">
        <div class="bu-box">
            <div class="bu-col left">
                <!-- 国家 -->
                <div v-for="item in addrData" :key="item.text" class="item"
                    :class="active1 === item.value ? 'active' : ''">
                    <div class="title" @click="changeTitle(item, 1)"> {{ item.text }}</div>
                    <!-- {{ active1 }} -->
                    <!-- {{ item.check }} -->


                    <!-- <van-checkbox icon-size="14px" class="checkbox" @click.native="changeCheckbox(item, 1)"
                        v-model="item.check" shape="square">
                    </van-checkbox> -->

                    <div class="van-checkbox__icon van-checkbox__icon--square"
                        :class="item.check || item.halfCheck ? 'van-checkbox__icon--checked' : ''"
                        style="font-size: 14px;" @click="changeCheckbox(item, 1)">
                        <i class="van-icon" :class="item.halfCheck ? 'van-icon-minus' : 'van-icon-success'"></i>
                    </div>
                </div>

            </div>
            <div class="bu-col right grey1" v-show="active2">
                <!-- 省份 -->
                <div v-for="item in secondData" :key="item.value" class="item"
                    :class="active2 === item.value ? 'active' : ''">
                    <div class="title" @click="changeTitle(item, 2)"> {{ item.text }}</div>

                    <!-- <van-checkbox icon-size="14px" class="checkbox" @click.native="changeCheckbox(item, 2)"
                        v-model="item.check" shape="square">
                    </van-checkbox> -->
                    <div class="van-checkbox__icon van-checkbox__icon--square"
                        :class="item.check || item.halfCheck ? 'van-checkbox__icon--checked' : ''"
                        style="font-size: 14px;" @click="changeCheckbox(item, 2)">
                        <i class="van-icon" :class="item.halfCheck ? 'van-icon-minus' : 'van-icon-success'"></i>
                    </div>
                </div>
            </div>
            <div class="bu-col right grey2" v-show="active3">
                <!-- 城市 -->
                <div v-for="item in thirdData" :key="item.value" class="item"
                    :class="active3 === item.value ? 'active' : ''">
                    <div class="title" @click="changeTitle(item, 3)"> {{ item.text }}</div>
                    <!-- <van-checkbox icon-size="14px" class="checkbox" @click.native="changeCheckbox(item, 3)"
                        v-model="item.check" shape="square">
                    </van-checkbox> -->
                    <div class="van-checkbox__icon van-checkbox__icon--square"
                        :class="item.check || item.halfCheck ? 'van-checkbox__icon--checked' : ''"
                        style="font-size: 14px;" @click="changeCheckbox(item, 3)">
                        <i class="van-icon" :class="item.halfCheck ? 'van-icon-minus' : 'van-icon-success'"></i>
                    </div>
                </div>
            </div>
            <div class="bu-col right grey3" v-show="active4">
                <!-- 区 -->
                <div v-for="item in forthData" :key="item.value" class="item"
                    :class="active4 === item.value ? 'active' : ''">
                    <div class="title" @click="changeTitle(item, 4)"> {{ item.text }}</div>
                    <!-- <van-checkbox icon-size="14px" class="checkbox" @click.native="changeCheckbox(item, 4)"
                        v-model="item.check" shape="square">
                    </van-checkbox> -->
                    <div class="van-checkbox__icon van-checkbox__icon--square"
                        :class="item.check || item.halfCheck ? 'van-checkbox__icon--checked' : ''"
                        style="font-size: 14px;" @click="changeCheckbox(item, 4)">
                        <i class="van-icon" :class="item.halfCheck ? 'van-icon-minus' : 'van-icon-success'"></i>
                    </div>
                </div>
            </div>
        </div>
        <div class="footer-box">
            <div @click="reset(addrData, false, true)">重置</div>
            <div class="finally" @click="getData">完成</div>
        </div>
    </div>
</template>
<script>
import Vue from 'vue';
import { Tabbar, TabbarItem, Toast } from 'vant';

Vue.use(Toast);
Vue.use(Tabbar);
Vue.use(TabbarItem);
export default {
    name: 'CascaderCheckboxAddr',
    components: {},
    mixins: [],
    props: {
        addrData: {
            type: Array,
            default: () => []
        }
    },
    data() {
        return {
            secondData: [],
            thirdData: [],
            forthData: [],

            active1: "",
            active2: "",
            active3: "",
            active4: "",
            activeItem1: "",
            activeItem2: "",
            activeItem3: "",
            activeItem4: "",
        }
    },
    computed: {

    },
    watch: {
    },
    mounted() {
        this.addCheck(this.addrData)
        this.addrData[0].check = true
    },
    methods: {
        // 递归函数在每一项中添加check/halfCheck
        addCheck(arr) {
            for (var i = 0; i < arr.length; i++) {
                this.$set(arr[i], 'check', false)
                this.$set(arr[i], 'halfCheck', false)
                this.$set(arr[i], 'children', [])
                if (arr[i].children && arr[i].children.length > 0) {
                    this.addCheck(arr[i].children)
                }
            }
        },
        reset(arr, check, isInit) {
            for (var i = 0; i < arr.length; i++) {
                this.$set(arr[i], 'check', check)
                this.$set(arr[i], 'halfCheck', false)
                if (arr[i].children && arr[i].children.length > 0) {
                    this.reset(arr[i].children, check)
                }
            }
            if (isInit) {
                this.addrData[0].check = true
            }
        },

        // 递归函数 修改子级状态
        setChildrenChecked(arr, check, halfCheck) {
            for (var i = 0; i < arr.length; i++) {
                this.$set(arr[i], 'check', check)
                this.$set(arr[i], 'halfCheck', halfCheck)
                if (arr[i].children && arr[i].children.length > 0) {
                    this.setChildrenChecked(arr[i].children, check, halfCheck)
                }
            }
        },
        // 递归函数 修改父级状态
        setFatherChecked(father, fatherId) {
            // console.log(father, fatherId, "father,fatherId");
            let flag = false;
            let someFlag = false

            someFlag = father.children.some(n => n.check || n.halfCheck)
            flag = father.children.every(n => n.check)
            someFlag = flag ? false : someFlag

            this.$set(father, "halfCheck", someFlag)
            this.$set(father, "check", flag)

            if (fatherId - 1 > 0) {
                this.setFatherChecked(this[`activeItem${fatherId - 1}`], fatherId - 1)
            }
        },
        setFirstChange() {

        },
        changeCheckbox(item, col, type = 'box') {
            console.log(item, col, type, "====");

            if (type === 'box') {
                item.check = !item.check
                item.halfCheck = false
            }
            switch (col) {

                case 1:  // 省份
                    this.active1 = item.value

                    if (item.value !== 'CN') {
                        // 其他国家隐藏二三四列
                        this.active2 = ""
                        this.active3 = ""
                        this.active4 = ""
                        return
                    }
                    // activeItem1 只能为中国
                    this.activeItem1 = item
                    if (!this.activeItem1.hasLoad) {
                        this.queryDictionaryType('CUX_ADDRESS_PROVINCE', item.value).then(res => {
                            // res = res.slice(0, 3)
                            this.secondData = res.map(n => ({ ...n, check: item.check, type: "province" }))
                            this.$set(this.activeItem1, "children", this.secondData)
                            this.activeItem2 = this.secondData[0]
                            this.active2 = this.secondData[0].value

                            this.queryDictionaryType('CUX_ADDRESS_CITY', this.secondData[0].value).then(res1 => {
                                res1 = res1.slice(0, 3)
                                this.thirdData = res1.map(n => ({ ...n, check: item.check, type: "city" }))
                                this.$set(this.activeItem2, "children", this.thirdData)
                                this.activeItem3 = this.thirdData[0]
                                this.active3 = this.thirdData[0].value

                                this.queryDictionaryType('CUX_ADDRESS_AREA', this.thirdData[0].value).then(res2 => {
                                    this.forthData = res2.map(n => ({ ...n, check: item.check, type: "district" }))
                                    this.$set(this.activeItem3, "children", this.forthData)
                                    this.activeItem4 = this.forthData[0]
                                    this.active4 = this.forthData[0].value
                                }).catch(() => {

                                })
                            }).catch(() => {

                            })


                        }).catch(() => {

                        })
                        this.$set(item, "hasLoad", true)
                    } else {
                        this.setChildrenChecked(item.children, item.check, item.halfCheck)

                        this.secondData = item.children
                        this.active2 = this.secondData[0].value
                        this.activeItem2 = this.secondData[0]

                        this.thirdData = this.activeItem2.children
                        this.active3 = this.thirdData[0].value
                        this.activeItem3 = this.thirdData[0]

                        this.forthData = this.activeItem3.children
                        this.active4 = this.forthData[0].value
                        this.activeItem4 = this.forthData[0]
                        this.$set(this.activeItem1, "children", this.secondData)
                    }

                    break;
                case 2: // 城市
                    this.active2 = item.value
                    this.activeItem2 = item
                    this.setFatherChecked(this.activeItem1, 1)

                    // console.log(this.activeItem1, "this.activeItem1");
                    if (!this.activeItem2.hasLoad) {
                        this.queryDictionaryType('CUX_ADDRESS_CITY', item.value).then(res => {
                            // res = res.slice(0, 3)
                            this.thirdData = res.map(n => ({ ...n, check: item.check, type: "city" }))
                            this.$set(this.activeItem2, "children", this.thirdData)
                            this.activeItem3 = this.thirdData[0]
                            this.active3 = this.thirdData[0].value

                            this.queryDictionaryType('CUX_ADDRESS_AREA', this.thirdData[0].value).then(res2 => {
                                this.forthData = res2.map(n => ({ ...n, check: item.check, type: "district" }))
                                this.$set(this.activeItem3, "children", this.forthData)
                                this.activeItem4 = this.forthData[0]
                                this.active4 = this.forthData[0].value
                            }).catch(() => {

                            })

                        }).catch(() => {

                        })
                        this.$set(item, "hasLoad", true)
                    } else {
                        this.thirdData = item.children
                        this.active3 = this.thirdData[0].value
                        this.activeItem3 = this.thirdData[0]

                        this.forthData = this.activeItem3.children
                        this.active4 = this.forthData[0].value
                        this.activeItem4 = this.forthData[0]

                        if (type === 'box') {
                            this.setChildrenChecked(item.children, item.check, item.halfCheck)
                        }
                        this.$set(this.activeItem2, "children", this.thirdData)

                    }

                    break;
                case 3: // 县城区域
                    this.active3 = item.value
                    this.activeItem3 = item
                    this.setFatherChecked(this.activeItem2, 2)
                    if (!this.activeItem3.hasLoad) {
                        this.queryDictionaryType('CUX_ADDRESS_AREA', item.value).then(res => {
                            this.forthData = res.map(n => ({ ...n, check: item.check, type: "district" }))
                            this.$set(this.activeItem3, "children", this.forthData)
                            this.activeItem4 = this.forthData[0]
                            this.active4 = this.forthData[0].value
                        }).catch(() => {

                        })
                        this.$set(item, "hasLoad", true)
                    } else {
                        this.forthData = item.children
                        this.activeItem4 = this.forthData[0]
                        this.active4 = this.forthData[0].value

                        if (type === 'box') {
                            this.setChildrenChecked(item.children, item.check, item.halfCheck)
                        }
                        this.$set(this.activeItem3, "children", this.forthData)

                    }
                    break;
                case 4: // 街道
                    this.active4 = item.value
                    this.activeItem4 = item
                    this.setFatherChecked(this.activeItem3, 3)

                    break;

                default:
                    break;
            }
        },
        changeTitle(item, col) {
            this.changeCheckbox(item, col, "title")
        },

        getData() {
            this.$emit("getData", this.addrData)
        },
        // 字典接口
        queryDictionaryType(type, flag) {
            return new Promise(async (resolve, reject) => {
                const res = await this.baseService.queryDictionaryType({
                    language: "zh-CN",
                    dictionaryCode: type,
                    entryFlag: flag,
                })
                if (res.data.length > 0) {
                    res.data = res.data.map(n => {
                        return {
                            text: n.entryName, value: n.entryCode
                        }
                    })

                    resolve(res.data)
                } else {
                    reject()
                }
            })
        },

    }
}
</script>
<style lang="less" scoped>
// @headerHeight: 50px;
// @avatarHeight: 42px;
// @avatarFontSize: 18px;
.bu {
    position: relative;
    height: 400px;
    // overflow: hidden;
    // border-radius: 0 10px 10px 0;
}

.footer-box {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 40px;
    border-top: 1px solid #eee;

    >div {
        flex: 1;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100%;
        font-size: 16px;
    }

    .finally {
        background-color: var(--custom-primary-color);
        color: #fff;
    }
}

.grey1{
    background-color: #fafafa;
}
.grey2{
    background-color: #f5f5f5;
}
.grey3{
    background-color: #f0f0f0;
}
.bu-box {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    font-size: 14px;
    height: calc(100% - 40px);
    overflow: hidden;
    overflow-x: scroll;

    .bu-col {
        flex: 1;
        padding: 5px 0;
        min-width: 40vw;
        height: 100%;
        // overflow: hidden;
        overflow-y: scroll;

    }

    .left {
        background-color: #fff;
        // border-right: 1px solid #eee;


    }

    .right {
        .item {
            // background-color: #e8f0fd !important;
            // color: var(--custom-primary-color) !important;
        }


    }

    .item {
        padding: 10px;
        // margin-bottom: 5px;
        display: flex;
        justify-content: space-between;
        align-items: center;

        .title {
            flex: 1;
        }

        .checkbox {
            margin-left: 10px;
        }
    }

    .active {
        background-color: #e8f0fd;
        color: var(--custom-primary-color);
    }
}
</style>

父组件
 <CascaderCheckboxAddr :addrData="addrData" @getData="getAddrsData"></CascaderCheckboxAddr>
     async mounted() {
        // 异步获取并处理国家字典项,区域 国家
        this.queryDictionaryType('CUX_ADDRESS_COUNTRY').then(res => {
            this.addrData = res.map(n => ({ ...n, type: "country" }))
            this.addrData.unshift({
                text: '全部国家', value: "全部国家"
            });
        });
    },
       methods: {
       
        // 递归函数获取addrs
        getAddrs(data) {

            let countryList = []
            let provinceList = []
            let cityList = []
            let districtList = []
            function traverseData(item) {
                // 判断当前组织是否为BU类型,如果是,则计数器加一
                if ((item.check === true || item.halfCheck === true) && item.type === "country") {
                    countryList.push(item);
                }
                if ((item.check === true || item.halfCheck === true) && item.type === "province") {
                    provinceList.push(item);
                }
                if ((item.check === true || item.halfCheck === true) && item.type === "city") {
                    cityList.push(item);
                }
                if ((item.check === true || item.halfCheck === true) && item.type === "district") {
                    districtList.push(item);
                }

                if (item.children && Array.isArray(item.children)) {
                    for (let subItem of item.children) {
                        traverseData(subItem);
                    }
                }
            }

            for (let i = 0; i < data.length; i++) {
                traverseData(data[i]); // 从根节点开始遍历
            }


            // console.log("countryList: ", countryList);
            // console.log("provinceList: ", provinceList);
            // console.log("cityList : ", cityList);
            // console.log("districtList : ", districtList);
            return {
                countryList,
                provinceList,
                cityList,
                districtList,
            }


        },
        // 切换组织 
        getAddrsData(data) {
            // console.log(data, "getAddrs");
            // countryCode: null,
            // provinceCode: null,
            // cityCode: null,
            // countyCode: null,
            let { countryList, provinceList, cityList, districtList } = this.getAddrs(data)

            if (countryList.length == 0 && provinceList.length == 0 && cityList.length == 0 && districtList.length == 0) {

                this.addrsTitle = "全部区域"
                this.searchForm.countryCode = null
                this.searchForm.provinceCode = null
                this.searchForm.cityCode = null
                this.searchForm.countyCode = null
                this.getPageData('search')
                this.$refs.addrs.toggle()
                return;
            }


            this.searchForm.countryCode = countryList.filter(n => n.check).map(n => n.value)

            this.searchForm.provinceCode = provinceList.filter(n => n.check).map(n => n.value)

            this.searchForm.cityCode = cityList.filter(n => n.check).map(n => n.value)

            this.searchForm.countyCode = districtList.filter(n => n.check).map(n => n.value)

            // 显示名称------
            if (countryList && countryList.length > 1) {
                this.addrsTitle = "多个国家"
            } else if (countryList && countryList.length === 1) {
                if (countryList[0].value !== 'CN') {
                    this.addrsTitle = countryList[0].text
                } else if (countryList && countryList.length === 1) {
                    if (countryList[0].check) {
                        this.addrsTitle = countryList[0].text
                    } else {
                        if (provinceList && provinceList.length > 1) {
                            this.addrsTitle = "多个省份"
                        } else if (provinceList && provinceList.length === 1) {
                            if (provinceList[0].check) {
                                this.addrsTitle = provinceList[0].text
                            } else {
                                if (cityList && cityList.length > 1) {
                                    this.addrsTitle = "多个城市"
                                } else if (cityList && cityList.length === 1) {
                                    if (cityList[0].check) {
                                        this.addrsTitle = cityList[0].text
                                    } else {
                                        if (districtList && districtList.length > 1) {
                                            this.addrsTitle = "多个区域"
                                        } else if (districtList && districtList.length === 1) {
                                            if (districtList[0].check) {
                                                this.addrsTitle = districtList[0].text
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            this.getPageData('search')
            this.$refs.addrs.toggle()
        },
      
        // 字典接口
        queryDictionaryType(type, flag) {
            return new Promise(async (resolve, reject) => {
                const res = await this.baseService.queryDictionaryType({
                    language: "zh-CN",
                    dictionaryCode: type,
                    entryFlag: flag,
                })
                if (res.data.length > 0) {
                    res.data = res.data.map(n => {
                        return {
                            text: n.entryName, value: n.entryCode
                        }
                    })

                    resolve(res.data)
                } else {
                    reject()
                }
            })
        },

    },
    
业务要求是:
  1. 国家-省份-城市-地区四个层级(都是用字典接口查询的,所以总的数据结构需要自己组装,后面要求除了中国别的国家不展开其他几个层级)
  2. 组件可以多选,展示效果是大于一个国家显示多个国家,只有一个国家展示那个国家名称,省份城市区域也是这样的显示逻辑
实现的思路主要是:
  1. 点击的是checkbox框还是只是点击title,如果是点击的title只作为展开操作,点击box,会更改check/halfCheck的状态;
  2. 第一次点击的时候从接口获取值,赋值之后将item的hasLoad设置为true,并且当前层级有下一层的时候需要继续调用下一层的接口,一直到最后一个层级结束,首次点击的不是第一层级国家的时候,也需要递归函数 修改父级状态
  3. 非首次点击的时候,相当于是切换子数据,需要递归函数 修改子级状态;

写这个组件的时候,顾问还要求写一个需求和这个类似,多选且树形结构,不同的是,那个多选组织的需求,只有两个层级,并且只有一个接口直接返回整个树形结构,比较简单,不用考虑点击item之后父级和子级的状态更改问题,也不用考虑数据接口的组装问题,算是比较顺利,到这个层级比较多的时候,思路已经不能使用了,本来开始的思路是点击第一层设置第二层数据,即打开上一层设置下一层数据,过程中发现上下层的数据一直对不上,比如说北京对上了天津的下一层数据,因为开始的时候没做树形的想法,控制页面展开那个变量做不了,后面才用的active1234做的绑定显示,到最后就成了一点击后面的几个层级全部展开,这算是代码自己的想法吧。