【vue】封装树形下拉框组件 el-popover+el-tree+el-select

父组件使用

javascript 复制代码
<template>
    <div>
        {{ array }}  更多属性详见wgyTreeSelect组件
        <wgyTreeSelect
             v-model="array"
             :list="list"
             :multiple="true"
             :disabled-ids="[111,113,2]"
         />
    </div>
</template>


<script>
/* 
    注意: 默认是这种结构
                id: 'id', // ID
                label: 'name', // 显示名称
                children: 'children', // 子级字段名
                path: 'path', // 路径
                content: 'content', // 描述
                pid: 'pid', // 父id
    如果不是这种结构传入obj定义
*/
export default {
    data() {
        return {
            array: [],
            //数据源
            list: [
                {
                    id: '1', // ID
                    name: '上海市', // 显示名称
                    children: [
                        {
                            id: '2', // ID
                            name: '嘉定区', // 显示名称
                            children: [
                                {
                                    id: '3', // ID
                                    name: '江桥镇11111111111111111111111111111', // 显示名称
                                    content: '嘉定', // 描述
                                    pid: '2', // 父id
                                },
                                {
                                    id: '4', // ID
                                    name: '安亭镇', // 显示名称
                                    content: '安亭', // 描述
                                    pid: '2', // 父id
                                },
                            ], // 子级字段名
                            content: '嘉定', // 描述
                            pid: '1', // 父id
                        },
                        {
                            id: '2000000', // ID
                            name: '嘉定2区', // 显示名称
                            children: [
                                {
                                    id: '3876543', // ID
                                    name: '江桥2镇11111111111111111111111111111', // 显示名称
                                    content: '嘉定', // 描述
                                    pid: '2000000', // 父id
                                },
                            ], // 子级字段名
                            content: '嘉定', // 描述
                            pid: '1', // 父id
                        },
                    ], // 子级字段名
                    content: '上海魔都', // 描述
                    pid: '0', // 父id
                },
                {
                    id: '11', // ID
                    name: '北京市', // 显示名称
                    children: [
                        {
                            id: '12', // ID
                            name: '朝阳区', // 显示名称
                            children: [
                                {
                                    id: '13', // ID
                                    name: '三里屯', // 显示名称
                                    content: '三里屯', // 描述
                                    pid: '12', // 父id
                                },
                                {
                                    id: '111', // ID
                                    name: '四里屯--------------', // 显示名称
                                    pid: '12', // 父id
                                },
                                {
                                    id: '113', // ID
                                    name: '五里屯--------------', // 显示名称
                                    pid: '12', // 父id
                                },
                                {
                                    id: '123', // ID
                                    name: '六里屯--------------', // 显示名称
                                    pid: '12', // 父id
                                },
                                {
                                    id: '11111', // ID
                                    name: '七里屯--------------', // 显示名称
                                    pid: '12', // 父id
                                },
                                {
                                    id: '11311', // ID
                                    name: '八里屯--------------', // 显示名称
                                    pid: '12', // 父id
                                },
                                {
                                    id: '12312', // ID
                                    name: '九里屯--------------', // 显示名称
                                    pid: '12', // 父id
                                },
                                {
                                    id: '11113331', // ID
                                    name: '十里屯--------------', // 显示名称
                                    pid: '12', // 父id
                                },
                                {
                                    id: '11344411', // ID
                                    name: '十一里屯--------------', // 显示名称
                                    pid: '12', // 父id
                                },
                                {
                                    id: '12555312', // ID
                                    name: '十二里屯--------------', // 显示名称
                                    pid: '12', // 父id
                                },
                                {
                                    id: '14', // ID
                                    name: '左家庄--------------', // 显示名称
                                    content: '左家庄', // 描述
                                    pid: '12', // 父id
                                },
                            ], // 子级字段名
                            content: '朝阳', // 描述
                            pid: '11', // 父id
                        },
                    ], // 子级字段名
                    content: '北京', // 描述
                    pid: '0', // 父id
                },
            ],
            
        };
    },
}
</script>

子组件

javascript 复制代码
<template>
    <div>
        <el-popover
            v-model="isShowSelect"
            placement="bottom-start"
            :width="popoverWidth"
            :close-on-click-modal="false"
            trigger="manual"
            @hide="popoverHide"
        >
            <el-tree
                ref="tree"
                v-bind="$attrs"
                class="common-tree"
                :width="width"
                :data="treeData"
                :props="obj"
                :show-checkbox="multiple"
                :node-key="obj.id"
                :check-strictly="checkStrictly"
                :default-expanded-keys="defaultKeys"
                :expand-on-click-node="multiple&&expandClickNode"
                :check-on-click-node="checkClickNode"
                :highlight-current="true"
                @check-change="nodeClick"
                @node-click="nodeClick"
            />
            <el-select
                slot="reference"
                ref="select"
                v-bind="$attrs"
                v-model="returnDataKeys"
                :size="size"
                :width="width"
                :multiple="multiple"
                :clearable="clearable"
                :collapse-tags="collapseTags"
                class="tree-select"
                @click.native="selectClick"
                @remove-tag="removeTag"
                @clear="clear"
                @mouseenter.native="showCloseIcon = true"
                @mouseleave.native="showCloseIcon = false"
            >
                <el-option
                    v-for="item in options"
                    :key="item.value"
                    :label="item.label"
                    :value="item.value"
                />
                <!-- 这里是删除整个下拉框的内容,多选的时候,是没有这个功能的,所有自己弄了一个icon -->
                <template
                    slot="prefix"
                >
                    <i
                        v-if="multiple && clearable && returnDataKeys.length && showCloseIcon"
                        class="el-icon-close"
                        @click.stop="clearSelectedNodes"
                    ></i>
                    <i></i>
                </template>
            </el-select>
        </el-popover>
    </div>
</template>

<script>
export default {
    name: 'TreeSelect',
    props: {
        // 绑定的值
        value: {
            tyep: Object,
        },
        // 树结构数据
        list: {
            type: Array,
            default: () => ([]),
        },
        obj: {
            type: Object,
            required: false,
            default: () => ({
                id: 'id', // ID
                label: 'name', // 显示名称
                children: 'children', // 子级字段名
                path: 'path', // 路径
                content: 'content', // 描述
                pid: 'pid', // 父id
            }),
        },
        // 配置是否可多选
        multiple: {
            type: Boolean,
            default: false,
        },
        // 配置是否可清空选择,只对单选时生效, 多选时自定义清空按钮
        clearable: {
            type: Boolean,
            default: true,
        },
        // 配置多选时是否将选中值按文字的形式展示
        collapseTags: {
            type: Boolean,
            default: false,
        },
        // 显示复选框情况下,是否严格遵循父子不互相关联
        checkStrictly: {
            type: Boolean,
            default: false,
        },
        // 多选时设置点击节点是否可以选中,false是只有点击多选框才选中,点击元素不选中
        checkClickNode: {
            type: Boolean,
            default: false,
        },
        // 多选时:点击节点展开还是点三角标,true是点击节点展开,false是点击三角形展开
        expandClickNode: {
            type: Boolean,
            default: false,
        },
        size: {
            type: String,
            default: 'small',
        },
        width: {
            type: String,
            default: '200px',
        },
        // 父节点全选时, 是否只展示子节点
        onlyLeaf: {
            type: Boolean,
            default: false,
        },
        // 父节点全选时,是否只展示父节点
        mergeTag: {
            type: Boolean,
            default: false,
        },
        // 外部点击是否收起树形, 默认收起
        closeOnOutsideClick: {
            type: Boolean,
            default: true,
        },
        // 禁止选中的数据
        disabledIds: {
            type: Array,
            default: () => ([]),
        },
    },
    // 上面是父组件可传入参数
    data() {
        return {
            defaultKeys: [], // 默认展开的节点
            defaultKey: '',
            first: false, //
            popoverWidth: '0px', // 下拉框大小
            isShowSelect: false, // 是否显示树状选择器
            options: [], // select option选项
            returnDatas: [], // 返回给父组件数组对象
            returnDataKeys: [], // 返回父组件数组主键值
            showCloseIcon: false, // 清空icon
        };
    },
    computed: {
        treeData() {
            // 判断传入的数据是不是树形结构
            const isTreeStructure = JSON.stringify(this.list).indexOf(this.obj.children) !== -1;
            // 是树形结构,就返回传入的数据, 不是的话格式化成树形结构
            return isTreeStructure ? this.list : this.toTreeStructure(this.list);
        },
    },
    watch: {
        // 是否显示树状选择器
        isShowSelect() {
            // 隐藏select自带的下拉框
            this.$refs.select.blur();
        },
        // 监听tree数据
        treeData() {
            this.$nextTick(() => {
                this.init();
            });
        },
        // 监听value从新赋值
        value: {
            handler(val) {
                this.$nextTick(() => {
                    if (this.multiple) {
                        this.defaultKeys = val;
                    } else {
                        this.defaultKey = val;
                    }
                    this.init();
                });
            },
            immediate: true,
        },
        // 监听选中的值
        returnDataKeys: {
            handler(val) {
                if (this.first || val) {
                    this.first = true;
                    this.$emit('input', val);
                } else {
                    this.first = true;
                }
            },
        },
        // 监听禁用数组
        disabledIds: {
            handler(val) {
                console.log(val);
                this.addDisabledProperty(this.list, val);
            },
        },
    },
    mounted() {
        if (this.closeOnOutsideClick) {
            document.addEventListener('click', this.handleClickOutside);
        }
    },
    beforeDestroy() {
        if (this.closeOnOutsideClick) {
            document.removeEventListener('click', this.handleClickOutside);
        }
    },
    methods: {
        // 传入数据类型不是树形结构,转成树形结构
        toTreeStructure(list) {
            console.log(list);
            const map = {}; let node; const roots = []; let
                i;
            for (i = 0; i < list.length; i += 1) {
                map[list[i].id] = i; // 初始化map
                list[i].children = []; // 初始化children
            }
            for (i = 0; i < list.length; i += 1) {
                node = list[i];
                if (node.pid !== '0') {
                    // 如果有父级
                    list[map[node.pid]].children.push(node);
                } else {
                    // 如果没有父级,则为根节点
                    roots.push(node);
                }
            }
            return roots;
        },

        // 点击其他元素, 树隐藏
        handleClickOutside(event) {
            if (!this.$refs.select?.$el?.contains(event.target)) {
                this.isShowSelect = false;
            }
        },

        init() {
            //  如果是多选,
            if (this.multiple) {
                // 且默认展开的节点大于0
                if (Array.isArray(this.defaultKeys) && this.defaultKeys.length > 0) {
                    // 检测this.defaultKeys[0]是否是一个对象。
                    if (Object.prototype.toString.call(this.defaultKeys[0]).indexOf('Object') !== -1) { // 对象
                        this.setDatas(this.defaultKeys);
                    // 检测this.defaultKeys[0]是否是一个数字或者字符串。
                    } else if (Object.prototype.toString.call(this.defaultKeys[0]).indexOf('Number') !== -1
                            || Object.prototype.toString.call(this.defaultKeys[0]).indexOf('String') !== -1) {
                        this.setKeys(this.defaultKeys);
                    } else {
                        console.log('多选:传入参数类型不匹配');
                    }
                }
            } else {
                // 单选
                if (Object.prototype.toString.call(this.defaultKey).indexOf('Number') !== -1
                    || Object.prototype.toString.call(this.defaultKey).indexOf('String') !== -1
                    || Object.prototype.toString.call(this.defaultKey).indexOf('Object') !== -1) {
                    this.setKey(this.defaultKey);
                } else {
                    console.log('单选:传入参数类型不匹配');
                }
            }
        },

        // 下拉框select点击[入口]
        selectClick() {
            this.isShowSelect = !this.isShowSelect;
        },

        // 节点被点击  
        nodeClick(a, node) {
            // 单选
            if (!this.multiple) {
                this.isShowSelect = false;
                this.setKey(node.key);
            // 多选
            } else {
                // 所有被选中的节点的 key 所组成的数组数据
                const checkedKeys = this.$refs.tree.getCheckedKeys();
                let selectedNodes = checkedKeys.map((item) => {
                    // 所有被选中的节点对应的node
                    const { data, label, key } = this.$refs.tree.getNode(item);
                    return { label, value: key, data };
                });

                // 如果onlyLeaf为true,mergeTag为false只保留叶子节点
                if (this.onlyLeaf && !this.mergeTag) {
                    selectedNodes = selectedNodes.filter((n) => !n.data.children);
                }

                // 如果mergeTag为true,onlyLeaf为false只保留父节点
                if (this.mergeTag && !this.onlyLeaf) {
                    selectedNodes = selectedNodes.filter((n) => {
                        // 判断当前节点是否有父节点
                        if (n.data.pid) {
                            // 判断当前节点的父节点是否也在selectedNodes中
                            const parentInSelectedNodes = selectedNodes.some((upN) => upN.data.id === n.data.pid);
                            // 如果父节点也在selectedNodes中,就将当前节点从selectedNodes中移除
                            return !parentInSelectedNodes;
                        }
                        return true;
                    });
                }

                // 设置option选项
                this.options = selectedNodes;
                this.returnDataKeys = selectedNodes.map((item) => item.value);
                this.returnDatas = selectedNodes.map((n) => n.data);
            }
        },

        // 单选:清空选中
        clear() {
            this.$refs.tree.setCurrentKey(null);// 清除树选中key
            this.returnDatas = null;
            this.returnDataKeys = '';
            this.popoverHide();
            this.isShowSelect = false;
        },

        // 单选:设置、初始化值 key
        setKey(thisKey) {
            if (thisKey) {
                // 设置当前选中的值, 获取当前值对应的节点, 设置当前节点
                this.$refs.tree.setCurrentKey(thisKey);
                const node = this.$refs.tree.getNode(thisKey);
                this.setData(node.data);
            }
        },

        // 单选:设置、初始化对象
        setData(data) {
            this.options = [];
            this.options.push({ label: data[this.obj.label], value: data[this.obj.id] });
            this.returnDatas = data;
            this.returnDataKeys = data[this.obj.id];
        },

        // 多选:设置、初始化值 keys
        setKeys(checkedKeys) {
            // 给树状选择器设置选中的节点。 给select赋值
            this.$refs.tree.setCheckedKeys(checkedKeys);
            this.returnDataKeys = checkedKeys;

            const selectedNodes = checkedKeys.map((item) => {
                // 所有被选中的节点对应的node
                const node = this.$refs.tree.getNode(item);
                return { label: node.label, value: node.key, data: node.data };
            });
            this.returnDatas = selectedNodes.map((node) => node.data);
            this.popoverHide();
        },

        // 多选:设置、初始化对象  
        setDatas(data) {
            // 获取选中节点的主键值
            const checkedKeys = data.map((item) => item[this.obj.id]);
            // 设置树状选择器的选中节点
            this.$refs.tree.setCheckedKeys(checkedKeys);
            // 设置返回给父组件的数组对象
            this.returnDatas = data;
            this.returnDataKeys = checkedKeys;
            this.popoverHide();
        },

        // 多选模式下,当删除选中的节点时,将该节点及其子节点设置为未选中状态,并更新选中的节点。 ok
        removeTag(val) {
            // 获取删除的节点
            const node = this.$refs.tree.getNode(val);
            // 判断获取的节点是否为叶子节点
            const isLeafNode = node.childNodes.length === 0;
            // 是叶子节点,那么只需要将该节点设置为未选中状态
            // 不是叶子节点,那么需要将该节点及其所有子节点设置为未选中状态
            const nodesToUncheck = isLeafNode ? [node] : this.treeToList(node);

            // 将每个节点设置为未选中状态。
            nodesToUncheck.forEach((n) => this.$refs.tree.setChecked(n, false));

            // 更新选中的节点。
            this.nodeClick();
            this.popoverHide();
        },

        // 下拉框关闭执行
        popoverHide() {
            this.$emit('getValue', this.returnDataKeys, this.returnDatas);
        },

        // 多选,清空所有被选中的节点
        clearSelectedNodes() {
            this.$refs.tree.setCheckedKeys([]);
        },

        // 树形转为集合
        treeToList(tree) {
            let queen = [];
            const out = [];
            queen = queen.concat(tree);
            while (queen.length) {
                const first = queen.shift();
                if (first.childNodes) {
                    queen = queen.concat(first.childNodes);
                }
                out.push(first);
            }
            return out;
        },

        // 禁用某个节点
        addDisabledProperty(data, ids) {
            data.forEach((item) => {
                if (ids.includes(+item.id)) {
                    item.disabled = true;
                }
                if (item.children) {
                    this.addDisabledProperty(item.children, ids);
                }
            });
        },

    },
};
</script>

<style scoped lang="scss">
::v-deep .el-input__prefix{
    position: absolute;
    height: 100%;
    right: 5px;
    .el-icon-close {
        position: absolute;
        right: 6px;
        top: 11px;
        z-index: 1;
        border-radius:50%;
        color: #fff;
        background-color: #B0B3B8;
    }
}
    .mask{
        height: 100%;
        position: fixed;
        top: 0;
        left: 0;
        opacity: 0;
        z-index: 11;
    }
    .common-tree{
        overflow: auto;
    }
    .tree-select{
        position: relative;
        z-index: 111;
    }
    .ok{
        float: right;
    }
    .el-row{
        padding-top: 0px !important;
    }
</style>
相关推荐
万叶学编程2 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js
前端李易安4 小时前
Web常见的攻击方式及防御方法
前端
PythonFun4 小时前
Python技巧:如何避免数据输入类型错误
前端·python
知否技术4 小时前
为什么nodejs成为后端开发者的新宠?
前端·后端·node.js
hakesashou4 小时前
python交互式命令时如何清除
java·前端·python
天涯学馆4 小时前
Next.js与NextAuth:身份验证实践
前端·javascript·next.js
HEX9CF5 小时前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
ConardLi5 小时前
Chrome:新的滚动捕捉事件助你实现更丝滑的动画效果!
前端·javascript·浏览器
ConardLi5 小时前
安全赋值运算符,新的 JavaScript 提案让你告别 trycatch !
前端·javascript