背景
最近项目中有一个部门选择需求,一开始是用element-plus的级联下拉写的,但是由于层级过深,会出现级联下拉超出屏幕的情况,所以改用树形下拉,但是element没有相关组件,现记录下vue3+js自定义实现可以根据关键字筛选的树形下拉选择框。
实现效果
代码
<template>
<el-popover popper-class="tree-select-popper el-select-dropdown" :width="width" trigger="click">
<template #reference>
<div
ref="selectRef"
class="tree-select el-select"
:class="[elFormltemSize ? 'el-select--' + elFormltemSize : '']"
>
<overlay-scrollbars class="tree-select-input">
<div :class="{ 'is-placeholder': !treeSelectText }" class="input-content">
{{ treeSelectText || placeholder }}
</div>
</overlay-scrollbars>
</div>
</template>
<div class="tree-select_wrapper">
<div class="tree-search_input">
<el-input v-model="treeFilterText" class="" placeholder="筛选" />
</div>
<overlay-scrollbars class="tree-scroll">
<div class="select-tree-box">
<el-tree
ref="selectTree"
:node-key="nodeKey"
:current-node-key="modelValue"
:data="data"
:props="treeProps"
:expand-on-click-node="false"
class="select-tree"
:filter-node-method="filterTreeNode"
@node-click="handleClickTreeNode"
/>
</div>
</overlay-scrollbars>
</div>
</el-popover>
</template>
<script setup>
import { computed, inject, nextTick, ref, watch } from 'vue';
import { useElementSize } from '@vueuse/core';
const props = defineProps({
modelValue: {
type: String,
required: true,
},
size: String,
data: {
type: Array,
default: () => [],
},
props: {
type: Object,
default: () => ({}),
},
placeholder: String,
searchPlaceholder: String,
nodeKey: {
type: String,
default: 'id',
},
});
const emits = defineEmits(['update:modelValue']);
const treeProps = computed(() => ({
label: 'label',
id: 'id',
children: 'children',
...props.props,
}));
const elFormltem = inject('elFormltem');
const elFormltemSize = computed(() => elFormltem?.size); //大小
//选中的label
const treeSelectText = computed(() => {
let text = '';
const getTreeSelectData = (arr, id, textArr) => {
for (const item of arr) {
if (item[treeProps.value.id] === id) {
text = [...textArr, item[treeProps.value.label]].join('/');
return;
}
if (!item[treeProps.value.children] || !item[treeProps.value.children]?.length) continue;
getTreeSelectData(item[treeProps.value.children], id, [
...textArr,
item[treeProps.value.label],
]);
}
};
getTreeSelectData(props.data, props.modelValue, []);
return text || props.modelValue;
});
//选中某个节点
const handleClickTreeNode = (node) => {
emits('update:modelValue', node[treeProps.value.id]);
};
const selectTree = ref(null);
const treeFilterText = ref(''); //过滤树文字
//节点过滤
const filterTreeNode = (value, data) => {
if (!value) return true;
return data.label.includes(value);
};
watch(treeFilterText, (val) => selectTree.value?.filter(val));
const selectRef = ref(null);
const { width } = useElementSize(selectRef);
const expandParents = (node) => {
if (!node) return;
node.expanded = true;
expandParents(node?.parent);
};
//选中节点初始化
watch(
() => props.modelValue,
(value) => {
if (!value) return;
nextTick(() => {
selectTree.value?.setCurrentKey(value);
const node = selectTree.value?.getNode(value);
expandParents(node?.parent);
});
},
{ immediate: true, flush: 'post' },
);
</script>
<style scoped lang="scss">
.tree-select {
width: 100%;
.tree-select-input {
width: 100%;
color: #c0c4cc;
border-radius: 4px;
border: 1px solid #8a99b0;
.os-content {
.input-content {
line-height: 32px;
height: 32px !important;
overflow: visible;
white-space: nowrap;
padding: 0 11px !important;
display: inline-block;
color: #606266;
font-size: 14px;
&.is-placeholder {
color: #7f8da5;
}
}
}
}
}
.tree-select_wrapper {
max-height: 271px;
display: flex;
flex-direction: column;
.tree-search_input {
flex: 0 0 auto;
padding: 10px;
}
.tree-scroll {
max-height: 271px;
flex: 1;
margin-top: 10px;
.select-tree-box {
padding: 0 10px 10px;
}
:v-deep(.select-tree) {
.el-tree-node_children {
overflow: visible !important;
}
.el-tree-node {
&.is-current > .el-tree-node_content {
color: $common;
}
}
}
}
}
</style>
<style>
.tree-select-popper {
padding: O;
}
</style>