## 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>