antdv下拉框树的封装(可懒加载,可级联下级,可单独勾选,可禁用,可搜索)

## 1. index.vue

js 复制代码
<template>
  <div style="display: flex; flex-direction: column;">
    <a-tree-select
      class="tree-select"
      :dropdownClassName="dropdownClassName"
      :getPopupContainer="
        (triggerNode) => {
          return triggerNode.parentNode
        }
        "
      multiple
      :filterTreeNode="filterTreeNode"
      treeNodeFilterProp="title"
      :show-search="showSearch"
      :labelInValue="labelInValue"
      :tree-checkable="treeCheckable"
      v-model="model"
      :tree-default-expand-all="treeDefaultExpandAll"
      :show-checked-strategy="showCheckedStrategy"
      :treeCheckStrictly="treeCheckStrictly"
      style="width: 100%"
      :dropdown-style="{ maxHeight: maxHeight, overflow: 'auto' }"
      :tree-data="processedTreeData"
      :load-data="loadData"
      :replaceFields="formatReplaceFields"
      :placeholder="placeholder"
      @search="(e) => this.$emit('search', e)"
      @select="(e) => this.$emit('select', e)"
      @blur="(e) => this.$emit('blur', e)"
      @change="handleChange"
      @treeExpand="handleExpand"
    >
     <template slot="title" slot-scope="scope">
      <span style="display: flex;justify-content: space-between;">
          {{ scope[replaceFields.title] }}
          <!-- 动态控制 disabled 属性(已展开过 + 有子节点 → 可勾选) -->
          <a-checkbox
            :disabled="!isCheckboxEnable(scope)"
            :checked="scope.isCheckedAllChildren"
            @change="handleCheckAllChildren($event, scope)">
            选中下级
          </a-checkbox>
      </span>
     </template>
      <treeselectIcon v-if="showTreeSelectIcon" slot="suffixIcon"></treeselectIcon>
    </a-tree-select>
    <span v-if="split" style="margin-left: 8px; height: 30px; border-left: 1px solid #bebebe"></span>
  </div>
</template>

<script>
import treeselectIcon from '@/components/treeSelect/treeselectIcon'
export default {
  components: {
    treeselectIcon
  },
  props: {
    labelInValue: {
      type: Boolean,
      default: true
    },
    showSearch: {
      type: Boolean,
      default: true
    },
    split: {
      type: Boolean,
      default: false
    },
    value: [Object, Array, String],
    replaceFields: {
      type: Object,
      default: () => ({
        key: 'key',
        title: 'title',
        label: 'label',
        value: 'value',
        children: 'children'
      })
    },
    treeCheckable: {
      type: Boolean,
      default: false
    },
    showCheckedStrategy: {
      type: String,
      default: 'SHOW_CHILD'
    },
    dropdownClassName: {
      type: String,
      default: 'tree-select-dropdown'
    },
    maxHeight: {
      type: String,
      default: '300px'
    },
    treeDefaultExpandAll: {
      type: Boolean,
      default: false
    },
    showTreeSelectIcon: {
      type: Boolean,
      default: true
    },
    treeCheckStrictly: {
      type: Boolean,
      default: false
    },
    placeholder: {
      type: String,
      default: '请选择查询项'
    },
    loadData: Function,
    treeData: Array
  },
  data () {
    return {
      model: {},
      processedTreeData: [],
      expandedNodeKeys: new Set() // 存储「已展开过」的节点 key(核心新增)
    }
  },
  watch: {
    value: {
      deep: true,
      immediate: true,
      handler: function (val, oldVal) {
        if (Array.isArray(val)) {
          this.model = val
        } else if (val && typeof val === 'object') {
          if (val && val.value) {
            this.model = val
          }
        } else {
          this.model = val
        }
      }
    },
    // 初始化 treeData 时,给所有节点添加 isCheckedAllChildren 字段(默认 false)
    treeData: {
      deep: true,
      immediate: true,
      handler (val) {
        if (val && val.length > 0) {
          this.processedTreeData = val
          this.initCheckedAllChildren()
          // 初始化后检查一次子节点选中状态
          this.syncCheckAllChildrenStatus()
          // 若默认展开所有节点,初始化展开历史
          if (this.treeDefaultExpandAll) {
            this.initExpandedNodeKeys()
          }
        }
      }
    },
    // 监听选中状态变化,同步「选中下级」复选框状态
    model: {
      deep: true,
      handler () {
        this.syncCheckAllChildrenStatus()
      }
    }
  },
  computed: {
    formatReplaceFields () { // 目的:title插槽生效
      // 组件默认字段:key(唯一标识)、label(文本)、children(子节点)
      // 父组件传入的 replaceFields 可能用 title 表示文本,需映射为 label
      return {
        key: this.replaceFields.key || 'key',
        // 优先使用父组件传入的 label 或 title 作为文本字段(兼容父组件配置)
        label: this.replaceFields.label || this.replaceFields.title || 'label',
        value: this.replaceFields.value || 'dmcod',
        children: this.replaceFields.children || 'children'
      }
    }
  },
  methods: {
    /**
   * 自定义节点筛选逻辑
   * @param {string} searchText - 搜索框输入的文本
   * @param {object} treeNode - 待筛选的节点对象(antd内部封装的节点)
   * @returns {boolean} - true显示节点,false隐藏节点
   */
    filterTreeNode (searchText, treeNode) {
      // 无搜索文本时,显示所有节点
      if (!searchText) return true

      // 1. 获取节点原始数据(treeNode.data 是你传入的节点数据)
      const nodeData = treeNode.data.props
      if (!nodeData) return false

      // 2. 适配你的 replaceFields,获取节点文本字段(兼容 title/label)
      const textField = this.replaceFields.title || this.replaceFields.label || 'title'
      const nodeText = (nodeData[textField] || '').toLowerCase()
      const searchLower = searchText.toLowerCase()

      // 3. 基础匹配:节点文本包含搜索词
      if (nodeText.includes(searchLower)) return true

      // 4. 高级匹配:若节点有子节点(未加载/已加载),递归检查子节点是否匹配
      // (目的:父节点可因子节点匹配而显示)
      const childrenKey = this.replaceFields.children || 'children'
      // 懒加载场景:nodeData.isLeaf 为 false 表示有未加载的子节点
      if (!nodeData.isLeaf && (!nodeData[childrenKey] || nodeData[childrenKey].length === 0)) {
      // 懒加载未加载的节点,暂不递归(可根据业务调整)
        return false
      }

      // 递归检查子节点
      const checkChildren = (children) => {
        if (!children || children.length === 0) return false
        return children.some(child => {
          const childText = (child[textField] || '').toLowerCase()
          // 子节点文本匹配,或子节点的子节点匹配
          return childText.includes(searchLower) || checkChildren(child[childrenKey])
        })
      }

      // 若子节点匹配,父节点也显示
      return checkChildren(nodeData[childrenKey])
    },

    /**
     * 初始化所有节点的 isCheckedAllChildren 字段(默认 false)
     */
    initCheckedAllChildren () {
      const { children: childrenKey } = this.replaceFields
      const recursiveInit = (nodes) => {
        nodes.forEach(node => {
          // 给节点添加 isCheckedAllChildren 字段(避免 undefined)
          if (node.isCheckedAllChildren === undefined) {
            node.isCheckedAllChildren = false
          }
          // 递归处理子节点
          if (node[childrenKey] && node[childrenKey].length > 0) {
            recursiveInit(node[childrenKey])
          }
        })
      }
      recursiveInit(this.processedTreeData)
      // 手动触发数组更新检测
      this.processedTreeData = [...this.processedTreeData]
    },
    /**
     * 同步「选中下级」复选框状态:
     * - 子节点全部选中 → 勾选复选框
     * - 子节点全部未选中 → 取消勾选复选框
     * - 部分选中 → 保持原有状态
     */
    syncCheckAllChildrenStatus () {
      const { children: childrenKey, value: valueKey } = this.replaceFields
      // 获取当前选中的所有 value(兼容 labelInValue 模式)
      const checkedValues = this.model.map(item => item.value || item).filter(Boolean)
      const checkedSet = new Set(checkedValues)
      const recursiveCheck = (nodes) => {
        if (!nodes || nodes.length === 0) return
        nodes.forEach(node => {
          // 仅处理有子节点的父节点
          if (node[childrenKey] && node[childrenKey].length > 0) {
            // 递归获取当前节点的所有子节点 value(含多级)
            const allChildValues = this.getAllChildValues(node)
            if (allChildValues.length === 0) return

            // 判断子节点是否全部选中
            const isAllChildChecked = allChildValues.every(val => checkedSet.has(val))
            // 判断子节点是否全部未选中
            const isAllChildUnchecked = allChildValues.every(val => !checkedSet.has(val))

            // 更新 isCheckedAllChildren 状态
            if (isAllChildChecked) {
              node.isCheckedAllChildren = true
            } else if (isAllChildUnchecked) {
              node.isCheckedAllChildren = false
            }
            // 部分选中时,保持原有状态(不修改)

            // 递归处理子节点的子节点
            recursiveCheck(node[childrenKey])
          }
        })
      }

      recursiveCheck(this.processedTreeData)
      // 手动触发重新渲染
      this.processedTreeData = [...this.processedTreeData]
    },
    /**
     * 初始化「已展开过」的节点 key(针对 treeDefaultExpandAll: true 场景)
     */
    initExpandedNodeKeys () {
      const { key: keyKey, children: childrenKey } = this.replaceFields
      const recursiveInit = (nodes) => {
        if (!nodes || nodes.length === 0) return
        nodes.forEach(node => {
          this.expandedNodeKeys.add(node[keyKey]) // 加入展开历史
          if (node[childrenKey] && node[childrenKey].length > 0) {
            recursiveInit(node[childrenKey])
          }
        })
      }
      recursiveInit(this.processedTreeData)
    },
    handleChange (value) {
      this.$emit('change', value)
    },
    /**
     * 监听节点展开/折叠事件,记录展开历史(折叠不删除)
     */
    handleExpand (expandedKeys) {
      // expandedKeys 是当前所有展开的节点 key 数组
      expandedKeys.forEach(key => this.expandedNodeKeys.add(key))
      // 强制更新,确保复选框状态同步
      this.$nextTick(() => {
        this.processedTreeData = [...this.processedTreeData]
      })
    },
    /**
     * 判断「选中下级」复选框是否可勾选:
     * 条件1:节点已展开过(expandedNodeKeys 中存在该节点 key)
     * 条件2:节点有子节点(含直接子节点,或已加载的子节点)
     */
    isCheckboxEnable (node) {
      const { key: keyKey, children: childrenKey } = this.replaceFields
      const nodeKey = node[keyKey] // 当前节点的 key

      // 条件1:节点已展开过(即使当前折叠,只要展开过就满足)
      const hasExpandedBefore = this.expandedNodeKeys.has(nodeKey)

      // 条件2:节点有子节点(区分懒加载和非懒加载)
      let hasChildren = false
      if (this.loadData) {
        // 懒加载场景:!node.isLeaf 表示有子节点(未加载或已加载)
        hasChildren = !node.isLeaf
      } else {
        // 非懒加载场景:直接判断 children 数组是否存在且长度>0
        hasChildren = !!node[childrenKey] && node[childrenKey].length > 0
      }

      // 两个条件同时满足,复选框可勾选;否则禁用
      return hasExpandedBefore && hasChildren
    },
    /**
     * 「选中下级」复选框点击事件
     */
    handleCheckAllChildren (e, node) {
      e.stopPropagation() // 阻止事件冒泡(避免触发节点选中/展开)
      const isChecked = e.target.checked
      const { children: childrenKey } = this.replaceFields

      // 再次校验:防止通过控制台修改属性绕过禁用状态
      if (!this.isCheckboxEnable(node)) return

      // 懒加载场景:若子节点未加载,先加载再处理
      if (this.loadData && !node[childrenKey] && !node.isLeaf) {
        this.loadData(node).then(() => {
          this.processedTreeData = JSON.parse(JSON.stringify(this.processedTreeData)) // 深度更新
          this.handleCheckAllChildrenLogic(isChecked, node)
        })
      }

      // 非懒加载场景:直接处理
      this.handleCheckAllChildrenLogic(isChecked, node)
    },
    /**
     * 「选中下级」核心逻辑(抽取为独立方法,便于复用)
     */
    handleCheckAllChildrenLogic (isChecked, node) {
      const { children: childrenKey } = this.replaceFields

      // 若节点无子节点,直接返回
      if (!node[childrenKey] || node[childrenKey].length === 0) {
        node.isCheckedAllChildren = false
        this.processedTreeData = [...this.processedTreeData]
        return
      }
      // 获取当前节点的所有子节点 value
      const allChildValues = this.getAllChildValues(node)
      let newModel = [...this.model]
      const existingValues = newModel.map(item => item.value || item)

      if (isChecked) {
        // 勾选:添加所有子节点(去重)
        const newChildNodes = allChildValues
          .filter(val => !existingValues.includes(val))
          .map(val => this.labelInValue
            ? { value: val, label: this.getNodeLabelByValue(val) }
            : val
          )
        newModel = [...newModel, ...newChildNodes]
      } else {
        // 取消勾选:移除所有子节点
        const childValueSet = new Set(allChildValues)
        newModel = newModel.filter(item => !childValueSet.has(item.value || item))
      }

      // 更新选中状态和复选框状态
      this.model = newModel
      node.isCheckedAllChildren = isChecked
      this.handleChange(newModel)
      this.processedTreeData = [...this.processedTreeData]
    },

    /**
     * 递归获取节点的所有子节点value(包括多级子节点)
     * @param {Object} node - 父节点数据
     * @returns {Array} 所有子节点的value数组
     */
    getAllChildValues (node) {
      const { children: childrenKey, value: valueKey } = this.replaceFields
      let childValues = []

      // 如果当前节点有子节点,递归遍历
      if (node[childrenKey] && node[childrenKey].length > 0) {
        node[childrenKey].forEach(child => {
          if (!child.childrenNum) {
            childValues.push(child[valueKey]) // 添加当前子节点value
            childValues = [...childValues, ...this.getAllChildValues(child)] // 递归添加子节点的子节点
          }
        })
      }

      return childValues
    },
    /**
     * 根据value获取节点的label(用于labelInValue模式下的显示)
     * @param {String/Number} value - 节点value
     * @returns {String} 节点label
     */
    getNodeLabelByValue (value) {
      const { value: valueKey, title: titleKey, children: childrenKey } = this.replaceFields
      const findLabel = (nodes) => {
        for (const node of nodes) {
          if (node[valueKey] === value) {
            return node[titleKey]
          }
          if (node[childrenKey] && node[childrenKey].length > 0) {
            const label = findLabel(node[childrenKey])
            if (label) return label
          }
        }
        return ''
      }
      return findLabel(this.treeData)
    }
  }
}
</script>

2. treeselectIcon.vue

相关推荐
麻辣兔变形记3 小时前
永续合约杠杆逻辑全解析:前端、后端和保证金的关系
前端·后端·区块链·智能合约
你真的可爱呀3 小时前
对接deepseek(全面版)【前端写全局图标和对话框】
前端·deepseek
222you3 小时前
SpringBoot+Vue项目创建
前端·javascript·vue.js
冉冰学姐3 小时前
SSM社区疫情防控管理系统rgb2a(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·管理系统·信息化管理·ssm 框架·社区疫情防控
天问一3 小时前
前端通过用户权限来显示对应权限的页面
前端·html
222you3 小时前
vue目录文件夹的作用
前端·javascript·vue.js
mpHH3 小时前
postgresql源码阅读 search_path
数据库·postgresql
月屯3 小时前
pandoc安装与使用(html、makdown转docx、pdf)
前端·pdf·html·pandoc·转docx、pdf
我爱学习_zwj3 小时前
Node.js模块化入门指南
前端·node.js