TreeSelect 是一个基于 Element UI 的树形选择器组件,结合了 el-select 和 el-tree 的功能,支持单选和多选模式,支持树形

看效果图: 多选模式:

单选模式:

直接上复制代码就能用,在代码中找到这个方法getCompanyList;然后替换成自己的树形结构的接口,返回的数据结构需要根据自身的结构来处理

使用例子

js 复制代码
<template>
  <TreeSelect 
    v-model="selectedValue"
    :tree-data="treeData"
    :multiple="true"
    placeholder="请选择部门"
  />
</template>

<script>
export default {
  data() {
    return {
      selectedValue: [],
      treeData: [
        {
          deptName: '总公司',
          rcmOrgId: '1',
          children: [
            { deptName: '技术部', rcmOrgId: '2' },
            { deptName: '市场部', rcmOrgId: '3' }
          ]
        }
      ]
    }
  }
}
</script>

动态加载数据

js 复制代码
<template>
  <TreeSelect 
    v-model="selectedDept"
    resource-code="cmrmp:dept"
    :is-dept="true"
    placeholder="选择部门"
    @tree-done="handleTreeDone"
  />
</template>

<script>
export default {
  data() {
    return {
      selectedDept: ''
    }
  },
  methods: {
    handleTreeDone(treeData) {
      console.log('树数据加载完成:', treeData)
    }
  }
}
</script>

自定义配置

js 复制代码
<template>
  <TreeSelect 
    v-model="selectedValues"
    :tree-data="orgData"
    :multiple="true"
    :check-strictly="true"
    :default-props="{ 
      children: 'children', 
      label: 'name', 
      value: 'id' 
    }"
    :clearable="false"
    size="medium"
    @change="handleSelectionChange"
  />
</template>

<script>
export default {
  data() {
    return {
      selectedValues: [],
      orgData: [
        {
          name: '组织架构',
          id: 'org-1',
          children: [
            { name: '研发中心', id: 'dept-1' },
            { name: '产品部', id: 'dept-2' }
          ]
        }
      ]
    }
  },
  methods: {
    handleSelectionChange(values, nodes) {
      console.log('选中值:', values)
      console.log('节点数据:', nodes)
    }
  }
}
</script>

获取选中数据

js 复制代码
<template>
  <div>
    <TreeSelect 
      ref="treeSelect"
      v-model="selectedIds"
      :tree-data="companyData"
      :multiple="true"
    />
    <button @click="getSelectedData">获取选中数据</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selectedIds: [],
      companyData: [] // 树数据
    }
  },
  methods: {
    getSelectedData() {
      const nodes = this.$refs.treeSelect.getCheckedNodes()
      const keys = this.$refs.treeSelect.getCheckedKeys()
      console.log('选中节点:', nodes)
      console.log('选中键值:', keys)
    }
  }
}
</script>

组件代码如下: 在components文件夹下建立treeSelect文件夹,然后创建index.vue文件;把下面代码复制进去

js 复制代码
<template>
  <div
    class="treeSelectWrap"
    @mouseleave="onmouseleave"
    @mouseenter="onmouseenter"
  >
    <!-- 禁用状态,单选 -->
    <el-input
      v-if="disabled && !multiple"
      disabled
      style="width: 100%"
      placeholder="请输入"
      :size="size"
      :value="selectedDisabledValue"
    ></el-input>
    <!-- 禁用状态,多选 -->
    <el-select
      v-if="disabled && multiple"
      :value="selectedDisabledValue"
      :size="size"
      style="width: 100%"
      disabled
      multiple
      collapse-tags
    >
      <el-option
        v-for="item in options"
        :key="item.value"
        :label="item.label"
        :value="item.value"
      ></el-option>
    </el-select>
    <template v-if="!disabled">
      <!-- 下拉树形选择器 -->
      <el-popover
        :trigger="trigger"
        ref="popoverRef"
        popper-class="treeSelectPopover"
        :width="popoverSlotWidth"
        :visible-arrow="false"
        :placement="placement"
        :disabled="disabled"
        :append-to-body="appendToBody"
      >
        <el-select
          v-model="selectedValue"
          ref="treeSelect"
          slot="reference"
          filterable
          popper-class="displayNone"
          placeholder="请选择(可输入名称搜索)"
          style="width: 100%"
          :multiple="multiple"
          :clearable="clearable"
          :collapse-tags="multiple"
          :filter-method="filterMethod"
          :size="size"
          :disabled="disabled"
          @clear="clearSelected"
          @remove-tag="removeTag"
          @visible-change="visbleChange"
        >
          <el-option
            v-for="item in options"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          ></el-option>
        </el-select>
        <el-tree
          v-if="!disabled"
          ref="tree"
          :data="treeDataList"
          :props="defaultProps"
          :node-key="treeNodeKey"
          :show-checkbox="multiple"
          :filter-node-method="filterNode"
          :check-strictly="!checkStrictly"
          :default-expanded-keys="defaultExpandedKeys"
          :highlight-current="!multiple"
          :expand-on-click-node="false"
          @check="handleTreeCheck"
          @node-click="handleTreeNodeClick"
          @current-change="handleCurrentChange"
          @node-expand="nodeExpand"
          @node-collapse="nodeCollapse"
        >
          <!-- 多种勾选选项 -->
          <div
            class="span-ellipsis"
            slot-scope="{ node, data }"
            v-if="multiple"
          >
            <TreeSeletLabelPopover
              v-if="node.data.children && node.data.children.length"
              v-bind="refs"
              :node="node"
              :node-key="treeNodeKey"
              :selectedValue="selectedValue"
              @popoverChangeValue="popoverChangeValue"
            >
              <div
                class="text-ellipsis"
                :style="{
                  backgroundColor: data.orgStatus === '0' ? '#DDD' : '',
                  textDecoration: data.orgStatus === '0' ? 'line-through' : '',
                }"
              >
                <span>{{ node.label }}</span>
              </div>
            </TreeSeletLabelPopover>
            <div
              v-else
              class="text-ellipsis"
              :style="{
                backgroundColor: data.orgStatus === '0' ? '#DDD' : '',
                textDecoration: data.orgStatus === '0' ? 'line-through' : '',
              }"
              :title="node.label"
            >
              <span>{{ node.label }}</span>
            </div>
          </div>
          <div
            v-else
            slot-scope="{ node, data }"
            :class="{ 'text-ellipsis': true, 'node-disabled': !data.open }"
            :style="{
              backgroundColor: data.orgStatus === '0' ? '#DDD' : '',
              textDecoration: data.orgStatus === '0' ? 'line-through' : '',
            }"
            :title="node.label"
          >
            <span>{{ node.label }}</span>
          </div>
        </el-tree>
      </el-popover>
    </template>
  </div>
</template>

<script>
  import { getOpenNode, getTreeAllLength } from "@/utils";
  import TreeSeletLabelPopover from "./TreeSeletLabelPopover";
  import axios from "axios";

  export default {
    name: "CMR_TreeSelect",
    components: {
      TreeSeletLabelPopover,
    },
    props: {
      // select的size
      size: {
        type: String,
        default: "small",
      },
      // 树形数据
      treeData: {
        type: Array,
        default: () => [],
      },
      // 是否多选
      multiple: {
        type: Boolean,
        default: false,
      },
      // 是否可清空
      clearable: {
        type: Boolean,
        default: true,
      },
      // 父子节点关联(多选时有效)
      checkStrictly: {
        type: Boolean,
        default: false,
      },
      // 节点配置选项
      defaultProps: {
        type: Object,
        default: () => ({
          children: "children",
          label: "deptName",
          value: "rcmOrgId",
        }),
      },
      // 默认选中的值
      value: {
        type: [String, Number, Array],
        default: () => [],
      },
      // 是否默认选中第一个
      defaultOne: {
        type: Boolean,
        default: false,
      },
      resourceCode: {
        type: String,
        default: "",
      },
      isDept: {
        type: Boolean,
        default: false,
      },
      requestUrl: {
        type: String,
        default: "",
      },
      // 是否挂在body上
      appendToBody: {
        type: Boolean,
        default: false,
      },
      // popover的位置
      placement: {
        type: String,
        default: "bottom-start",
      },
      disabled: {
        type: Boolean,
        default: false,
      },
      // 禁用状态下,以文本的方式展示
      /**
       * 为何需要单独设置禁用状态的值?
       * 1.禁用下不走树结构,减少数的接口请求
       * 2.回显值不一定在树结构中
       */
      disabledValue: {
        type: [String, Array],
        default: "",
      },
      trigger: {
        type: String,
        default: "click",
      },
    },
    data() {
      return {
        selectedValue: this.multiple ? [] : "", // 选中的值
        selectedDisabledValue: this.multiple ? [] : "", // 禁用状态的值
        treeDataList: [], // 数结构数据
        treeNodeKey: "", // 节点唯一标识字段
        options: [], // 下拉选择的option
        popoverSlotWidth: 350, // popover的宽度
        isInit: true,
        refs: {
          companyTreeRef: {},
          companySelectRef: {},
        },
      };
    },
    watch: {
      value: {
        handler(newVal) {
          if (!this.disabled) {
            this.selectedValue = newVal;
            this.initSelectedNodes();
          }
        },
        deep: true,
        immediate: true,
      },
      disabled: {
        handler(newVal) {
          if (newVal) {
            this.initDisabledText();
          }
        },
        deep: true,
        immediate: true,
      },
      disabledValue: {
        handler() {
          this.initDisabledText();
        },
        immediate: true,
        deep: true,
      },
      treeData: {
        handler(val) {
          if (val.length && !this.disabled) {
            this.treeDataList = val;
          }
        },
        deep: true,
        immediate: true,
      },
      treeDataList: {
        handler(val) {
          if (!this.disabled) {
            this.addAttr(val);
            if (this.defaultOne) {
              this.setDefaultOne();
            }
            this.initOptions();
            this.initSelectedNodes();
          }
        },
        deep: true,
        immediate: true,
      },
      resourceCode: {
        handler(val) {
          if (val) {
            this.getCompanyList(val);
          }
        },
        immediate: true,
      },
      defaultProps: {
        handler(val) {
          if (val && typeof val === "object") {
            this.treeNodeKey = val.value;
          }
        },
        deep: true,
        immediate: true,
      },
      // 动态改变展开节点
      defaultExpandedKeys: {
        handler(keys) {
          if (this.$refs.tree && this.$refs.tree.store) {
            const nodes = this.$refs.tree.store._getAllNodes();
            nodes.forEach((item) => {
              item.expanded = keys.includes(item.data[this.defaultProps.value]);
            });
          }
        },
        deep: true,
      },
    },
    computed: {
      // 默认展开的节点
      defaultExpandedKeys() {
        if (this.disabled) {
          return [];
        }
        const findOpen = getOpenNode(this.treeDataList || []);
        let findNode = [];
        if (findOpen.length) {
          findNode = this.getExpandChild(this.treeDataList, findOpen[0].level);
        }

        const data = Array.isArray(this.value) ? this.value : [this.value];
        const valueLength = getTreeAllLength(this.treeDataList);
        // 当全选时,只展开根节点
        if (valueLength === data.length) {
          return findNode;
        }
        // 初始化时, 不为第一级时,展开第一个open的项
        if (this.isInit && findOpen[0]?.level > 1) {
          // eslint-disable-next-line vue/no-side-effects-in-computed-properties
          this.isInit = false;
          return [findOpen[0][this.defaultProps.value]];
        }
        const checkedParentKeys = data
          .filter(Boolean)
          .map((item) => {
            return this.findParentOrSelf(
              this.treeDataList || [],
              (node) => node[this.defaultProps.value] === item
            );
          })
          .filter(Boolean)
          .map((i) => i[this.defaultProps.value]);

        const res = [...findNode, ...checkedParentKeys].filter(Boolean);
        return this.$lodash.uniq(res);
      },
    },
    mounted() {
      if (!this.disabled) {
        this.refs.companySelectRef = this.$refs.treeSelect;
        this.refs.companyTreeRef = this.$refs.tree;
        const treeSelectPopoverEl =
          this.$refs.popoverRef.$el.querySelector(".treeSelectPopover");
        if (treeSelectPopoverEl) {
          treeSelectPopoverEl.addEventListener("mouseenter", this.onmouseenter);
          treeSelectPopoverEl.addEventListener("mouseleave", this.onmouseleave);
        }
        window.addEventListener("resize", this.setPopoverSlotWidth);
        this.setPopoverSlotWidth();
      }
    },
    beforeDestroy() {
      if (!this.disabled) {
        window.removeEventListener("resize", this.setPopoverSlotWidth);
        const treeSelectPopoverEl =
          this.$refs.popoverRef.$el.querySelector(".treeSelectPopover");
        if (treeSelectPopoverEl) {
          treeSelectPopoverEl.removeEventListener(
            "mouseenter",
            this.onmouseenter
          );
          treeSelectPopoverEl.removeEventListener(
            "mouseleave",
            this.onmouseleave
          );
        }
      }
    },
    methods: {
      // 处理hover模式下的交互样式问题
      onmouseleave() {
        if (!this.disabled && this.trigger === "hover") {
          this.$refs.treeSelect.visible = false;
        }
      },
      onmouseenter() {
        if (!this.disabled && this.trigger === "hover") {
          this.$refs.treeSelect.visible = true;
        }
      },
      // 设置popover的宽度
      setPopoverSlotWidth() {
        setTimeout(() => {
          this.popoverSlotWidth =
            this.$refs.treeSelect?.$el?.offsetWidth || 350;
        });
      },
      // 添加权限禁用节点
      addAttr(tree) {
        tree.map((item) => {
          if (!item.open) {
            item.disabled = true;
          } else {
            item.disabled = false;
          }
          if (item.children) {
            this.addAttr(item.children);
          }
        });
      },
      // 重置过滤显示
      visbleChange(visible) {
        if (!visible) {
          this.$refs.tree.filter();
        }
      },
      // 输入过滤寻找
      filterMethod(val) {
        if (val) {
          this.$refs.tree.filter(val);
        } else {
          this.$refs.tree.filter();
        }
      },
      filterNode(value, data) {
        if (!value) return true;
        return data.deptName.indexOf(value) !== -1;
      },
      // 节点悬浮选择
      popoverChangeValue(ids) {
        this.$emit("change", ids);
        this.$emit("input", ids);
      },
      // 获取数结构数据
      async getCompanyList(val) {
        if (this.disabled || (this.treeDataList && this.treeDataList.length)) {
          return;
        }
        let url = "/cmrmp/api/commonComponent/queryCompanyTree";
        if (this.isDept) url = "/cmrmp/api/commonComponent/queryOrgTree";
        if (this.requestUrl) url = this.requestUrl;

        const res = await axios.get(url, {
          params: {
            resourceCode: val,
          },
        });

        if (res.data.code === "1") {
          const data = res.data.result || [];
          this.addAttr(data);
          this.treeDataList = data;
          if (this.defaultOne) {
            this.setDefaultOne();
          }
          this.$emit("treeDone", data);
        }
      },
      // 找寻父节点或自己
      findParentOrSelf(tree, condition, parent = null) {
        // 如果 tree 是数组,遍历每个子节点
        if (Array.isArray(tree)) {
          for (let node of tree) {
            const result = this.findParentOrSelf(node, condition, parent);
            if (result) return result;
          }
          return null;
        }

        // 如果当前节点满足条件
        if (condition(tree)) {
          // 如果有父节点,返回父节点;否则返回自己
          return parent || tree;
        }

        // 递归查找子节点
        if (tree.children && Array.isArray(tree.children)) {
          for (let child of tree.children) {
            const result = this.findParentOrSelf(child, condition, tree);
            if (result) return result;
          }
        }

        return null;
      },
      // 找到需要展开的节点层级
      getExpandChild(node, level, result = []) {
        if (Array.isArray(node)) {
          node.forEach((o, ind) => {
            if (ind === 0 && level === 1 && o.level === 1)
              result.push(o[this.defaultProps.value]);
            else if (o.level < level) result.push(o[this.defaultProps.value]);
            if (o.children) this.getExpandChild(o.children, level, result);
          });
        } else {
          if (node.children) this.getExpandChild(node.children, level, result);
        }
        return result;
      },
      // 递归找到第一个有权限(open)的节点
      findFirstOpenNode(list) {
        if (!list) return;
        let arr = JSON.parse(JSON.stringify(list));
        while (arr.length) {
          let cur = arr[0];
          if (cur.open) return cur;
          if (cur.children?.length) {
            arr = arr.concat(cur.children);
          }
          arr.shift();
        }
      },
      // 默认选中第一个
      setDefaultOne() {
        if (!this.treeDataList.length) return;
        const curNode = this.findFirstOpenNode(this.treeDataList);
        if (curNode) {
          const data = this.multiple
            ? [curNode[this.defaultProps.value]]
            : curNode[this.defaultProps.value];
          this.$emit("change", data);
          this.$emit("input", data);
        }
      },
      // 初始化已选中的节点
      initSelectedNodes() {
        if (!this.treeDataList.length) return;

        this.$nextTick(() => {
          if (this.multiple) {
            // 多选模式
            this.$refs.tree?.setCheckedKeys(this.selectedValue || []);
          } else {
            // 单选模式
            this.$refs.tree?.setCurrentKey(this.selectedValue || "");
          }
        });
      },
      // 初始化禁用状态下的文本
      initDisabledText() {
        if (!this.disabled) {
          return;
        }
        const val = this.disabledValue;
        if (this.multiple) {
          // 多选模式
          if (typeof val == "string" && val.includes(",")) {
            this.selectedDisabledValue = val.split(",");
          } else if (Array.isArray(val)) {
            this.selectedDisabledValue = val;
          } else {
            this.selectedDisabledValue = [val];
          }
          this.options = this.selectedDisabledValue.map((d) => {
            return { value: d, label: d };
          });
        } else {
          // 单选模式
          this.selectedDisabledValue = val;
        }
      },
      // 树结构平铺成el-select的一维option
      flattenTree(
        treeData,
        idKey = this.defaultProps.value,
        nameKey = this.defaultProps.label,
        childrenKey = this.defaultProps.children
      ) {
        const result = [];

        // 确保输入是数组,如果不是则包装成数组
        const treeArray = Array.isArray(treeData) ? treeData : [treeData];

        function traverse(node) {
          if (!node) return;

          // 将当前节点转换为目标格式并添加到结果中
          result.push({
            label: node[nameKey],
            value: node[idKey],
          });

          // 如果存在子节点,则递归遍历
          if (node[childrenKey] && Array.isArray(node[childrenKey])) {
            node[childrenKey].forEach(traverse);
          }
        }

        // 遍历树的每个根节点
        treeArray.forEach(traverse);

        return result;
      },
      // 初始化options
      initOptions() {
        if (!this.treeDataList.length) return;
        const list = this.flattenTree(this.treeDataList);
        this.options = list;
      },

      // 节点展开
      nodeExpand() {
        this.$refs.treeSelect.visible = true;
      },
      // 节点收起
      nodeCollapse() {
        this.$refs.treeSelect.visible = true;
      },
      // 处理树节点点击(单选)
      handleTreeNodeClick(data, node) {
        if (this.multiple) {
          this.$refs.treeSelect.visible = true;
          return;
        }
        if (!this.multiple && data.open) {
          this.selectedValue =
            data[this.defaultProps.value] || data[this.treeNodeKey];
          this.$emit("input", this.selectedValue);
          this.$emit("change", this.selectedValue, data, node);
          this.$refs.popoverRef.doClose();
        }
      },

      // 处理树节点选择变化(多选)
      handleTreeCheck(data, checkedInfo) {
        if (this.multiple) {
          this.$refs.treeSelect.visible = true;
          const checkedKeys = checkedInfo.checkedKeys;
          this.selectedValue = checkedKeys;
          this.$emit("input", this.selectedValue);
          this.$emit("change", this.selectedValue, checkedInfo.checkedNodes);
        }
      },

      // 处理当前节点变化(单选)
      handleCurrentChange(data, node) {
        if (this.multiple) {
          this.$refs.treeSelect.visible = true;
          return;
        }
        if (!this.multiple && data.open) {
          this.selectedValue =
            data[this.defaultProps.value] || data[this.treeNodeKey];
        }
      },

      // 清空选择
      clearSelected() {
        // if (this.defaultOne) {
        //   this.setDefaultOne();
        //   return;
        // }
        if (this.multiple) {
          this.selectedValue = [];
          this.$refs.tree.setCheckedKeys([]);
        } else {
          this.selectedValue = "";
          this.$refs.tree.setCurrentKey(null);
        }
        this.$emit("input", this.selectedValue);
        this.$emit("change", this.selectedValue);
      },

      // 移除标签(多选时)
      removeTag(tag) {
        // 查找要移除的节点
        const nodes = this.$refs.tree.getCheckedNodes();
        const nodeToRemove = nodes.find(
          (node) => node[this.defaultProps.value] === tag
        );

        if (nodeToRemove) {
          const key =
            nodeToRemove[this.defaultProps.value] ||
            nodeToRemove[this.treeNodeKey];
          this.$refs.tree.setChecked(key, false);

          // 更新选中值
          const index = this.selectedValue.indexOf(key);
          if (index > -1) {
            this.selectedValue.splice(index, 1);
          }

          // if (this.selectedValue.length === 0) {
          //   if (this.defaultOne) {
          //     this.setDefaultOne();
          //     return;
          //   }
          // }
          this.$emit("input", this.selectedValue);
          this.$emit("change", this.selectedValue);
        }
      },

      // 获取选中的节点数据
      getCheckedNodes() {
        return this.multiple
          ? this.$refs.tree.getCheckedNodes()
          : [this.$refs.tree.getCurrentNode()].filter(Boolean);
      },

      // 获取选中的键值
      getCheckedKeys() {
        return this.multiple
          ? this.$refs.tree.getCheckedKeys()
          : this.selectedValue
          ? [this.selectedValue]
          : [];
      },
    },
  };
</script>

<style lang="less">
  .treeSelectPopover {
    max-height: 350px;
    overflow-y: auto;
    padding: 0;
    .el-tree {
      padding: 5px;
    }
  }
  .treeSelectPopover[x-placement] {
    margin: 0;
  }
</style>
<style lang="less" scoped>
  /deep/ .el-select-dropdown__item {
    padding: 0;
  }

  /deep/ .el-tree {
    // max-height: 350px;
    // padding: 5px;
    /* max-height: 300px; */
    /* overflow-y: auto; */
    // overflow: unset;
  }
  /deep/ .el-popover {
    // overflow-y: auto;
    // padding: 0;
    // width: 100%;
  }
  .treeSelectWrap {
    width: 100%;
  }
  .el-select-dropdown__item.selected {
    font-weight: normal;
  }
  .span-ellipsis {
    color: #333;
  }
  .text-ellipsis {
    max-width: 280px;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .node-disabled {
    color: #999;
    cursor: not-allowed;
  }
</style>

在treeSelect文件夹下再建立TreeSeletLabelPopover.vue文件,代码如下:

js 复制代码
<template>
  <el-popover placement="right" :open-delay="600" trigger="hover">
    <div class="optional-list-wrapper">
      <div :title="node.label" class="popover-title">
        {{ node.label || node.data.deptName }}
      </div>
      <el-link
        v-for="item in optionalList"
        :key="item"
        type="primary"
        :underline="false"
        style="display: block; margin: 1px 0"
        @click="handlePopoverClick(node.data, item)"
      >
        {{ item }}
      </el-link>
    </div>

    <template #reference>
      <slot></slot>
    </template>
  </el-popover>
</template>
<script>
  export default {
    props: {
      node: {
        // 树节点
        type: Object,
        default: () => ({}),
      },
      selectedValue: {
        type: Array,
        default: () => [],
      },
      companyTreeRef: {
        // 树组件的ref,必填
        type: Object,
        default: () => ({}),
      },
      companySelectRef: {
        type: Object,
        default: () => ({}),
      },
      nodeKey: {
        type: String,
        default: "rcmOrgId",
      },
    },
    data() {
      return {
        optionalList: [
          "勾选本级和直属下级",
          "勾选本级和全部下级",
          "取消勾选全部下级",
        ],
      };
    },
    methods: {
      getAllChildrenIds(node) {
        // 存储结果的数组
        const ids = [];

        // 如果当前节点有children且是一个数组
        if (node.children && Array.isArray(node.children)) {
          for (const child of node.children) {
            // 将当前子节点的id加入结果
            if (child[this.nodeKey] && child.open) {
              ids.push(child[this.nodeKey]);
            }
            // 递归处理该子节点的后代
            ids.push(...this.getAllChildrenIds(child));
          }
        }

        return ids;
      },
      handlePopoverClick(companyNode, type) {
        this.companySelectRef.visible = true;
        const allIds = this.getAllChildrenIds(companyNode);
        if (type === this.optionalList[0]) {
          const data = [...this.selectedValue];
          if (companyNode.open) {
            data.push(companyNode[this.nodeKey]);
          }
          const ids = data.filter((item) => !allIds.includes(item));
          if (companyNode.children?.length) {
            companyNode.children.forEach((element) => {
              if (element.open) {
                ids.push(element[this.nodeKey]);
              }
            });
          }
          this.$emit("popoverChangeValue", this.$lodash.uniq(ids));
          return;
        }
        if (type === this.optionalList[1]) {
          const ids = [...this.selectedValue];
          if (companyNode.open) {
            ids.push(companyNode[this.nodeKey]);
          }
          ids.push(...allIds);
          this.$emit("popoverChangeValue", this.$lodash.uniq(ids));
          return;
        }
        if (type === this.optionalList[2]) {
          const ids = this.selectedValue.filter(
            (item) => !allIds.includes(item)
          );
          this.$emit("popoverChangeValue", this.$lodash.uniq(ids));
          return;
        }
      },
    },
  };
</script>
<style lang="less" scoped>
  .optional-list-wrapper {
    margin: -10px;
    padding: 10px;
    position: relative;
    z-index: 1000000;
  }
  .popover-title {
    color: rgba(0, 0, 0, 0.4);
    font-size: 14px;
    padding: 0 4px 4px 0;
    margin-bottom: 3px;
    max-width: 150px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
</style>

它的效果如图:

相关推荐
头茬韭菜几秒前
第 3 篇:「Pydantic 即 Schema」—— 工具生态三层解剖
前端·chrome·ai·openmanus
恋猫de小郭21 分钟前
Flutter 状态管理基准测评,一个很有趣的观点
android·前端·flutter
雪芽蓝域zzs27 分钟前
前端编辑组件wangEditor
前端
风骏时光牛马1 小时前
稳定性治理及疑难问题深度剖析与实战复盘
前端
JeffongTan9 小时前
在LWC中镶嵌VF Page获取用户IP
前端·javascript·salesforce
陆枫Larry11 小时前
Astro 是什么
前端
是立不是利13 小时前
写给设计师的 CSS 博客美学指南
前端·css
默_笙14 小时前
🏝 Docker 就是"房地产开发":从施工图纸到小区物业的容器化指南
前端·javascript
计算机魔术师14 小时前
Rohan Paul 谈用时间跨度衡量智能体能力,并引用 OpenAI 自动化研究实习生里程碑
前端