vue封装一个多选级联选择器组件


此级联选择器有三种状态:未选,半选,全选。半选就是此树结构的下方有选中的,全选就是此树结构的下方全部选中。实现上下级联动。

手写一个多选级联选择器,我们首先要创建一个外壳组件,用来处理默认数据增加一些显示隐藏,选中属性和回显功能。

我们把外壳组件起名为multipleChoice.vue,这里先放上template代码,已选显示的列表我没做,已选的ID会放在数据selected里,你们可以自行寻找。
    <div v-click-outside="close" class="department-option" @click.stop="show = true">
        <div class="now-text">
            选择部门
        </div>
        <transition name="transition-drop">
            <div class="department-box" v-show="show">
                <MultipleChoiceItem v-model="listData" @child-change="childChange" :selectKey="selectKey" />
            </div>
        </transition>
    </div>


接下来就是script代码。我们要引入组件 MultipleChoiceItem 组件,这个组件会放在下面。这是用来递归显示的组件。
注意:一下代码是multipleChoice.vue的script代码。内部实现了对数据的处理,给已选中的定义选中状态,给原始数据增加是否选中字段,显示隐藏字段,给每条数据都赋值一个父级原型链。为的是当前状态修改后,所有父级需要变更状态如(半选,全选,未选)。
props主要传入列表数据,是否需要自定义key包括返回的id,要显示的name,树结构的子级字段,v-model需要绑定的数据。
<script>
import MultipleChoiceItem from "./multipleChoiceItem.vue"
export default {
    name: 'multipleChoice',
    data() {
        return {
            show: false,
            listData: [], // 列表数据
            selected: [], // 最终选中
        }
    },
    props: {
        data: {
            type: Array,
            default: () => []
        },
        // 未选提示
        placeholder: {
            type: String,
            default: '请选择'
        },
        /**
         * 用到的key
         * 默认 id    name    child
         */
        selectKey: {
            type: Object,
            default: () => {
                return { value: 'id', lable: 'name', child: 'child' }
            }
        },
        value: {
            type: Array,
            default: () => []
        }
    },
    watch: {
        show(newVal) {
            if(!newVal) {
                this.updateData()   
            }
        }
    },
    created() {
        this.init();
    },
    methods: {
        childChange(val) {
            // console.log(val)
        },
        openCascader() {
            this.show = true;
            this.init()
        },
        init() {
            if (this.data && this.data.length) {
                let arr = JSON.parse(JSON.stringify(this.data))
                this.washData(arr, null); // 洗数据
                this.listData = arr;
            }
        },
        washData(treeList, parent, flag) {
            for (let i = 0; i < treeList.length; i++) {
                let flag_n = false; // 父级有选中子级不显示 标识符
                let jsonTree = JSON.parse(JSON.stringify(treeList[i])); // 赋值已选
                const tree = treeList[i];
                tree.parent = parent;
                if(this.value.includes(tree[this.selectKey.value])) {
                    flag_n = true;
                    // 默认回选
                    if (tree.parent) {
                        const diffParnet = (child_item) => {
                            if(child_item.check != 1) { // 只有没有选中的父级才能是半选状态
                                child_item.check = 2;
                            }
                            if(child_item.parent) {
                                diffParnet(child_item.parent);
                            }
                        }
                        diffParnet(tree.parent)
                    }
                    tree.check = 1;
                    tree.show = true;
                    if(!flag) {
                        this.selected_item.push(jsonTree); // 回源默认选中
                    }
                } else {
                    // 默认为0
                    tree.check = 0;
                    tree.show = false;
                }
                if (tree[this.selectKey.child]) {
                    this.washData(tree[this.selectKey.child], tree, flag_n);
                }
            }
        },
        // 弹窗关闭
        updateData() {
            this.selected = [];
            this.findSelected(this.listData);
            this.$emit('input', this.selected);
            this.$emit('on-change', this.selected);
        },
        close() {
            this.show = false;
        },
        // 查询已被选中的数据
        findSelected(arr) {
            arr.forEach(v => {
                if (v.check == 1) {
                    this.selected.push(v.id)
                }
                if (v[this.selectKey.child] && v[this.selectKey.child].length) {
                    this.findSelected(v[this.selectKey.child])
                }
            })
        }
    },
    components: {
        MultipleChoiceItem
    }
}
</script>


接下来是multipleChoice.vue的style代码
<style lang="less" scoped>
.department-option {
    width: 300px;
    height: 32px;
    margin-top: 20px;
    position: relative;
    border: 1px solid #d9d9d9;
    cursor: pointer;


    .department-box {
        width: 298px;
        max-height: 180px;
        overflow-y: auto;
    }


    .select-list {
        display: inline-block;
        background-color: #f5f5f5;
        color: rgba(0, 0, 0, .85);
        margin: 3px 0 3px 3px;
        height: 24px;
        line-height: 24px;
        padding: 0 10px;


        .icon-close {
            margin-left: 6px;
            font-size: 10px;
            color: rgba(0, 0, 0, .45);
        }
    }


    .now-text {
        overflow: hidden;
        color: #bfbfbf;
        white-space: nowrap;
        text-overflow: ellipsis;
        pointer-events: none;
        padding: 0 6px;
        line-height: 30px;
    }


    input {
        position: absolute;
        width: 0;
        border: 0;
        left: 6px;
        top: 0;
        height: 30px;
        padding: 6px 6px;
    }
}
</style>


我们写好multipleChoice.vue组件后,要继续创建multipleChoiceItem.vue组件

multipleChoiceItem.vue组件是为了递归显示每条数据的,里面包括了多选,上级下级联动的逻辑。
下方是template代码,重复使用组件multipleChoiceItem(自己)来实现递归。
<template>
    <div>
        <div class="list" :class="{ 'child-list': is_child }" v-for="(item, index) in dataList">
            <div class="main-body">
                <span class="more-icon" :class="{ 'body-icon': item[selectKey.child], 'active': item.show }"
                    @click="changeShow(item, index)"></span>
                <span class="check-box" @click="changeCheckStatus(item, index)">
                    <span class="chech-item" :class="isCheckClass(item.check)"></span>
                    {{ item[selectKey.lable] }}
                </span>
            </div>
            <multipleChoiceItem v-if="item[selectKey.child] && item.show" v-model="item[selectKey.child]" :is_child="true" />
        </div>
    </div>
</template>


下方是script代码,内部实现了选中后递归所有子级选中,父级递归检测是全选还是半选状态。
<script>
// 多选级联选择器
export default {
    name: 'multipleChoiceItem',
    data() {
        return {
            dataList: []
        }
    },
    props: {
        value: {
            type: Array,
            default: () => []
        },
        is_child: {
            type: Boolean,
            default: false
        },
        /**
         * 用到的key
         * 默认 id    name    child
         */
         selectKey: {
            type: Object,
            default: () => {
                return { value: 'id', lable: 'name', child: 'child' }
            }
        },
    },
    created() {
        this.dataList = this.value;
    },
    watch: {


    },
    methods: {
        isCheckClass(val) {
            if(val == 0) { // 未选中
                return ''
            } else if(val == 1) {
                return 'pitch-on'; // 选中
            } else if (val == 2) {
                return 'half-select'; // 半选中
            }
        },
        // 修改选中状态
        changeCheckStatus(item, i) {
            if(item.check == 0 || item.check == 2) {
                item.check = 1;
            } else {
                item.check = 0;
            }
            try {
                this.changeStatus(item, i)
            } catch(err) {
                console.log('err=', err)
            }
        },
        // 父级自己联动
        changeStatus(item, i) {
            // console.log(item)
            // 子级全部修改相同
            this.diffChangeStatus(item)
            // 父级联动
            if (item.parent) {
                const diffParnet = (child_item) => {
                    child_item.check = this.findParent(child_item);
                    if(child_item.parent) {
                        diffParnet(child_item.parent);
                    }
                }
                diffParnet(item.parent)
            }
        },
        diffChangeStatus(item, i) {
            item[this.selectKey.child] && item[this.selectKey.child].forEach(v => {
                v.check = item.check;
                if (v[this.selectKey.child] && v[this.selectKey.child].length) {
                    this.diffChangeStatus(v)
                }
            })
        },
        // 查找父级的选中的状态
        findParent(item) {
            let flag = 0;
            let flag_arr = [];
            let THIS = this;
            function filterData(arr) {
                arr.forEach(v => {
                    if (v.check == 1) {
                        flag = 1;
                    }
                    if(!flag_arr.includes(v.check)) {
                        flag_arr.push(v.check)
                    }
                    // if (v[THIS.selectKey.child]) {
                    //     filterData(v[THIS.selectKey.child])
                    // }
                })
            }
            filterData(item[THIS.selectKey.child]);
            if(flag_arr.length > 1) {
                return 2
            } else {
                return flag
            }
        },
        // 修改当前显示
        changeShow(item, i) {
            item.show = !item.show;
        },
    },
}
</script>


下方是style代码
<style lang="less" scoped>
.check-box {
    display: flex;
    align-items: center;
}
.chech-item {
    display: inline-block;
    width: 14px;
    height: 14px;
    border: 1px solid #D7D7D7;
    position: relative;
    margin-right: 4px;
    transition: all .3s;
    // 半选中
    &.half-select {
        border-color: #2d8cf0;
        background-color: #2d8cf0;
        color: #2d8cf0;
        &::after {
            content: "";
            width: 8px;
            height: 2px;
            background-color: #fff;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-4px, -1px);
        }
    }
    // 全选
    &.pitch-on {
        border-color: #2d8cf0;
        background-color: #2d8cf0;
        color: #2d8cf0;
        &::after {
            content: "";
            width: 4px;
            height: 8px;
            position: absolute;
            top: 1px;
            left: 4px;
            border: 2px solid #fff;
            border-top: 0;
            border-left: 0;
            transition: all 0.2s ease-in-out;
            transform: rotate(45deg) scale(1);
        }
    }
}
.list {
    width: 100%;


    &.child-list {
        padding-left: 20px;
    }


    .more-icon {
        display: inline-block;
        position: relative;
        width: 30px;
        height: 100%;


        &.body-icon::after {
            content: "";
            position: absolute;
            left: 50%;
            top: 50%;
            margin-top: -4px;
            margin-left: 0px;
            width: 0;
            height: 0;
            border-left: 4px solid #787878;
            border-right: 4px solid transparent;
            border-top: 4px solid transparent;
            border-bottom: 4px solid transparent;
        }


        &.active::after {
            transform: rotate(90deg);
        }
    }


    .main-body {
        display: flex;
        align-items: center;
        height: 30px;
    }
}
</style>



368

声明:Web前端小站 - 前端博客 - 王搏的个人博客|版权所有,违者必究|如未注明,均为原创

转载:转载请注明原文链接 - vue封装一个多选级联选择器组件

评论
孙瑞杰生日