自定义组件解决el-tree-select 在数据量过大时候页面卡顿
代码由DeepSeek生成,初步测试,没有什么问题。
1 <template> 2 <el-select 3 ref="selectRef" 4 v-model="selectValue" 5 :placeholder="placeholder" 6 :disabled="disabled" 7 :clearable="clearable" 8 :multiple="multiple" 9 :filterable="filterable" 10 :filter-method="handleFilterMethod" 11 :value-key="valueKey" 12 popper-class="virtual-tree-select-popper" 13 collapse-tags 14 collapse-tags-tooltip 15 @visible-change="onVisibleChange" 16 @remove-tag="onRemoveTag" 17 @clear="onClear" 18 > 19 <!-- 隐藏的选项,用于多选回显 label --> 20 <template v-show="false"> 21 <el-option 22 v-for="item in cacheOptions" 23 :key="item[valueKey]" 24 :label="item[labelKey]" 25 :value="item[valueKey]" 26 /> 27 </template> 28 29 <div class="virtual-tree-container"> 30 <el-tree-v2 31 ref="treeRef" 32 :data="treeData" 33 :props="treeProps" 34 :height="treeHeight" 35 :highlight-current="!multiple" 36 :show-checkbox="multiple" 37 :check-strictly="checkStrictly" 38 :expand-on-click-node="true" 39 :filter-method="filterMethod" 40 :default-expanded-keys="defaultExpandedKeys" 41 @node-click="onNodeClick" 42 @check="onCheck" 43 @current-change="onCurrentChange" 44 /> 45 </div> 46 </el-select> 47 </template> 48 49 <script setup> 50 import { ref, computed, watch, nextTick } from 'vue' 51 52 const props = defineProps({ 53 modelValue: { 54 type: [String, Number, Array], 55 default: null 56 }, 57 data: { 58 type: Array, 59 default: () => [] 60 }, 61 fieldNames: { 62 type: Object, 63 default: () => ({ 64 value: 'knowledgeId', 65 label: 'knowledgeName', 66 children: 'children', 67 disabled: 'disabled' 68 }) 69 }, 70 placeholder: { 71 type: String, 72 default: '请选择' 73 }, 74 disabled: Boolean, 75 clearable: { 76 type: Boolean, 77 default: true 78 }, 79 multiple: { 80 type: Boolean, 81 default: false 82 }, 83 filterable: { 84 type: Boolean, 85 default: true 86 }, 87 checkStrictly: { 88 type: Boolean, 89 default: false 90 }, 91 treeHeight: { 92 type: [String, Number], 93 default: 300 94 } 95 }) 96 97 const emit = defineEmits(['update:modelValue', 'change']) 98 99 const selectRef = ref(null) 100 const treeRef = ref(null) 101 const isVisible = ref(false) 102 const defaultExpandedKeys = ref([]) 103 const treeData = ref([]) 104 const filterText = ref('') 105 106 // 字段名配置 107 const valueKey = computed(() => props.fieldNames.value || 'knowledgeId') 108 const labelKey = computed(() => props.fieldNames.label || 'knowledgeName') 109 const childrenKey = computed(() => props.fieldNames.children || 'children') 110 const disabledKey = computed(() => props.fieldNames.disabled || 'disabled') 111 112 // 树组件的 props 配置 113 const treeProps = computed(() => ({ 114 children: childrenKey.value, 115 label: labelKey.value, 116 disabled: disabledKey.value, 117 value: valueKey.value 118 })) 119 120 // el-select 绑定的值 121 const selectValue = computed({ 122 get() { 123 return props.modelValue 124 }, 125 set(val) {} 126 }) 127 128 // 缓存选项(扁平化所有节点,用于多选回显) 129 const cacheOptions = computed(() => { 130 if (!props.multiple) return [] 131 132 const options = [] 133 const flattenTree = (nodes) => { 134 if (!nodes || !Array.isArray(nodes)) return 135 nodes.forEach(node => { 136 options.push({ 137 [valueKey.value]: node[valueKey.value], 138 [labelKey.value]: node[labelKey.value], 139 [disabledKey.value]: node[disabledKey.value] || false 140 }) 141 if (node[childrenKey.value] && node[childrenKey.value].length > 0) { 142 flattenTree(node[childrenKey.value]) 143 } 144 }) 145 } 146 flattenTree(treeData.value) 147 return options 148 }) 149 150 // el-tree-v2 的筛选方法 151 const filterMethod = (keyword, node) => { 152 if (!keyword) return true 153 const label = node[labelKey.value] || '' 154 return label.toLowerCase().includes(keyword.toLowerCase()) 155 } 156 157 // el-select 的筛选方法 158 const handleFilterMethod = (keyword) => { 159 filterText.value = keyword 160 161 if (!treeRef.value) return 162 163 // 调用 el-tree-v2 的 filter 方法 164 treeRef.value.filter(keyword) 165 166 // 搜索时自动展开匹配节点的所有父节点 167 if (keyword) { 168 const expandKeys = getExpandKeysByFilter(treeData.value, keyword) 169 defaultExpandedKeys.value = expandKeys 170 nextTick(() => { 171 treeRef.value?.setExpandedKeys(expandKeys) 172 }) 173 } else { 174 defaultExpandedKeys.value = [] 175 nextTick(() => { 176 treeRef.value?.setExpandedKeys([]) 177 }) 178 } 179 } 180 181 // 根据筛选关键字获取需要展开的节点 keys 182 const getExpandKeysByFilter = (nodes, keyword) => { 183 if (!keyword) return [] 184 185 const keys = [] 186 const lowerKeyword = keyword.toLowerCase() 187 188 const traverse = (nodeList, parentKeys = []) => { 189 nodeList.forEach(node => { 190 const label = (node[labelKey.value] || '').toLowerCase() 191 const hasMatch = label.includes(lowerKeyword) 192 193 if (hasMatch) { 194 // 当前节点匹配,添加所有父节点和当前节点 195 keys.push(...parentKeys, node[valueKey.value]) 196 } 197 198 if (node[childrenKey.value] && node[childrenKey.value].length > 0) { 199 traverse(node[childrenKey.value], hasMatch ? [] : [...parentKeys, node[valueKey.value]]) 200 } 201 }) 202 } 203 204 traverse(nodes) 205 return [...new Set(keys)] 206 } 207 208 // 初始化回显状态 209 const initCheckState = () => { 210 if (!treeRef.value) return 211 212 nextTick(() => { 213 if (props.multiple) { 214 // 多选:设置选中的 keys 215 if (Array.isArray(props.modelValue) && props.modelValue.length > 0) { 216 treeRef.value?.setCheckedKeys(props.modelValue) 217 } 218 } else { 219 // 单选:设置当前选中节点 220 if (props.modelValue) { 221 treeRef.value?.setCurrentKey(props.modelValue) 222 } 223 } 224 }) 225 } 226 227 // 下拉显示/隐藏 228 const onVisibleChange = (visible) => { 229 isVisible.value = visible 230 if (visible) { 231 filterText.value = '' 232 defaultExpandedKeys.value = [] 233 nextTick(() => { 234 if (treeRef.value) { 235 treeRef.value.filter('') 236 treeRef.value.setExpandedKeys([]) 237 // 打开时初始化回显 238 initCheckState() 239 } 240 }) 241 } 242 } 243 244 // 单选:节点点击 245 const onNodeClick = (data, node) => { 246 if (props.multiple) return 247 248 const value = data[valueKey.value] 249 250 emit('update:modelValue', value) 251 emit('change', value) 252 253 nextTick(() => { 254 // 关闭下拉框 255 isVisible.value = false 256 selectRef.value?.blur() 257 }) 258 } 259 260 // 单选:当前节点变化(备用) 261 const onCurrentChange = (data) => { 262 if (props.multiple || !data) return 263 264 const value = data[valueKey.value] 265 266 emit('update:modelValue', value) 267 emit('change', value) 268 269 nextTick(() => { 270 isVisible.value = false 271 selectRef.value?.blur() 272 }) 273 } 274 275 // 多选:复选框变化 276 const onCheck = (checkedData, checkedInfo) => { 277 if (!props.multiple) return 278 279 const checkedKeys = checkedInfo.checkedNodes.map(node => node[valueKey.value]) 280 const halfCheckedKeys = checkedInfo.halfCheckedNodes.map(node => node[valueKey.value]) 281 282 emit('update:modelValue', checkedKeys) 283 emit('change', { 284 checkedKeys, 285 checkedNodes: checkedInfo.checkedNodes, 286 halfCheckedKeys, 287 halfCheckedNodes: checkedInfo.halfCheckedNodes 288 }) 289 } 290 291 // 删除多选标签 292 const onRemoveTag = (tagValue) => { 293 if (!props.multiple || !treeRef.value) return 294 295 nextTick(() => { 296 // 同步取消树节点的选中状态 297 treeRef.value?.setChecked(tagValue, false, true) 298 299 // 获取更新后的选中值 300 const currentCheckedKeys = treeRef.value?.getCheckedKeys() || [] 301 emit('update:modelValue', currentCheckedKeys) 302 emit('change', currentCheckedKeys) 303 }) 304 } 305 306 // 清空选择 307 const onClear = () => { 308 if (props.multiple) { 309 emit('update:modelValue', []) 310 emit('change', []) 311 nextTick(() => { 312 treeRef.value?.setCheckedKeys([]) 313 }) 314 } else { 315 emit('update:modelValue', undefined) 316 emit('change', undefined) 317 nextTick(() => { 318 treeRef.value?.setCurrentKey(undefined) 319 }) 320 } 321 } 322 323 // 监听外部数据变化 324 watch(() => props.data, (newData) => { 325 treeData.value = newData || [] 326 327 // 数据变化后,如果下拉是打开的,重新初始化回显 328 nextTick(() => { 329 if (isVisible.value) { 330 initCheckState() 331 } 332 }) 333 }, { deep: true, immediate: true }) 334 335 // 监听 modelValue 外部变化 336 watch(() => props.modelValue, () => { 337 if (isVisible.value && treeRef.value) { 338 initCheckState() 339 } 340 }, { deep: true }) 341 342 // 暴露方法 343 const getCheckedNodes = () => treeRef.value?.getCheckedNodes() || [] 344 const getCheckedKeys = () => treeRef.value?.getCheckedKeys() || [] 345 const setCheckedKeys = (keys) => { 346 nextTick(() => { 347 if (treeRef.value) { 348 treeRef.value.setCheckedKeys(keys || []) 349 } 350 }) 351 } 352 const filter = (keyword) => { 353 filterText.value = keyword 354 handleFilterMethod(keyword) 355 } 356 357 defineExpose({ 358 getCheckedNodes, 359 getCheckedKeys, 360 setCheckedKeys, 361 filter, 362 treeRef, 363 selectRef 364 }) 365 366 // 初始化 367 treeData.value = props.data || [] 368 </script> 369 370 <style scoped> 371 .virtual-tree-container { 372 max-height: 400px; 373 overflow-y: auto; 374 } 375 376 :deep(.virtual-tree-select-popper) { 377 /* 确保下拉面板的样式正确 */ 378 } 379 380 :deep(.el-tree-v2) { 381 overflow-x: hidden; 382 } 383 384 /* 确保树组件在下拉面板中正确显示 */ 385 :deep(.el-select-dropdown) { 386 padding: 0; 387 } 388 389 :deep(.el-select-dropdown__list) { 390 padding: 0; 391 margin: 0; 392 } 393 394 :deep(.el-select-dropdown__item) { 395 padding: 0; 396 height: auto; 397 } 398 399 /* 隐藏 el-select 默认的空数据提示 */ 400 :deep(.el-select-dropdown__empty) { 401 display: none; 402 } 403 404 /* 树组件节点样式优化 */ 405 :deep(.el-tree-v2 .el-tree-node__content) { 406 padding-left: 8px; 407 height: 32px; 408 line-height: 32px; 409 } 410 411 :deep(.el-tree-v2 .el-tree-node__content:hover) { 412 background-color: var(--el-fill-color-light); 413 } 414 415 /* 复选框间距调整 */ 416 :deep(.el-tree-v2 .el-checkbox) { 417 margin-right: 8px; 418 } 419 </style>
使用示例:
<template>
<div class="demo">
<!-- 单选 -->
<virtual-tree-select
v-model="selectedId"
:data="treeData"
placeholder="请选择知识节点"
/>
<p>单选结果:{{ selectedId }}</p>
<!-- 多选 -->
<virtual-tree-select
v-model="selectedIds"
:data="treeData"
multiple
placeholder="请选择多个知识节点"
collapse-tags
collapse-tags-tooltip
/>
<p>多选结果:{{ selectedIds }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import VirtualTreeSelect from '@/components/VirtualTreeSelect.vue'
const selectedId = ref(null)
const selectedIds = ref([])
const treeData = ref([
{
knowledgeId: '1',
knowledgeName: '根节点1',
children: [
{
knowledgeId: '1-1',
knowledgeName: '子节点1-1',
children: [
{
knowledgeId: '1-1-1',
knowledgeName: '孙节点1-1-1',
children: []
}
]
},
{
knowledgeId: '1-2',
knowledgeName: '子节点1-2',
children: []
}
]
},
{
knowledgeId: '2',
knowledgeName: '根节点2',
children: [
{
knowledgeId: '2-1',
knowledgeName: '子节点2-1',
children: []
}
]
}
])
</script>
浙公网安备 33010602011771号