• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
尚善若水
博客园    首页    新随笔    联系   管理    订阅  订阅

自定义组件解决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>

 

posted @ 2026-04-27 17:37  尚尚是高尚的尚  阅读(56)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3