【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>
相关推荐
weixin_382395238 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__8 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.9 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~10 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华11 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子13 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅13 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni14 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学15 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端