前言
此类选择器根据vue+elementUI实现,使用vue3的可以根据此案例稍作改动即可实现,主要功能有弹出选择、搜索过滤、搜索结果高亮等,此选择器只支持单选,如需多选可在此基础进行改造。
效果图
代码实现
使用时,props-value必须要传,并且要保证其唯一性!!!
HTML
<!--
* @description: 通用树形弹窗选择框
* @fileName: treeDialogSelect/index.vue
* @author: tan
* @date: 2024-09-10 16:31:49
* @Attributes: data 展示数据 array
value 展示在输入框的值
props 配置选项,具体配置可以参照element ui库中el-tree的配置 object
expand-first-level 当根节点只有一个是,是否展开,默认为是
search-highlight 是否开启搜索高亮,默认开启,仅在slot-tree不传入时生效
onFilter 自定义过滤规则,参数为:
value 搜索框的值
data 原始数据
callback 回调函数 接受一个参数类型boolean 为是否展示该节点
!!!!!必传!!!!!! props-value 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 !!!!!!
!-->
<template>
<div class="treeDialogSelectCom">
<slot :value="value" v-if="$slots.default"></slot>
<el-input
v-else
v-bind="$attrs"
:value="valueFilter(value)"
suffix-icon="el-icon-arrow-down"
:placeholder="$attrs['placeholder'] || '请选择'"
:clearable="$attrs['clearable'] || true"
class="treeDialogSelectInput"
@focus="open"
@clear="selectClear()"
></el-input>
<el-dialog
:visible.sync="visible"
:width="$attrs['width'] || '50vw'"
:append-to-body="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
:title="$attrs['dialog-title'] || '请选择'"
>
<div class="treeDialogBody">
<el-input placeholder="输入关键字进行过滤" v-model="filterText" clearable>
<slot slot="prepend" name="prepend"></slot>
</el-input>
<div class="treeDialogBodyTree">
<el-tree
v-bind="$attrs"
:data="data"
:props="props"
@node-click="handleNodeClick"
:check-strictly="$attrs['check-strictly']"
:icon-class="$attrs['icon-class']"
:lazy="$attrs['lazy']"
:load="$attrs['load']"
:node-key="props.value"
:filter-node-method="filterNode"
:default-expanded-keys="defaultExpandedKeys"
ref="myTree"
>
<template slot-scope="{ node, data }">
<slot :node="node" :data="data" name="tree">
<span class="slotSpan">
<i v-show="!node.disabled && filterText" class="el-icon-warning"></i>
<span>
<span v-html="searchHighlightFilter(node, data)"></span>
<b v-if="$attrs['show-count'] != undefined && data[props.children]">({{ data[props.children].length }})</b>
</span>
</span>
</slot>
</template>
</el-tree>
</div>
</div>
<div class="footer">
<el-button type="primary" plain @click="selectClear()">清 空</el-button>
</div>
</el-dialog>
</div>
</template>
JS
export default {
props: {
value: {
type: undefined,
default: null,
},
data: {
type: Array,
default: () => new Array(),
},
props: {
type: Object,
default: () => {
return {
label: 'label',
value: 'value',
children: 'children',
};
},
},
},
data() {
return {
defaultExpandedKeys: [],
visible: false,
filterText: '',
};
},
created() {
this.propsInit();
},
mounted() {
setTimeout(this.initData, 10);
},
beforeUpdate() {
this.propsInit();
this.initData();
},
methods: {
open() {
this.visible = true;
},
initData() {
let newItem = this.recurrenceQuery(this.data, this.props.value, this.value);
if (newItem?.length) {
if (this.props.value && newItem[0][this.props.value]) {
this.defaultExpandedKeys = [newItem[0][this.props.value]];
}
this.$nextTick(() => {
if (this.$refs.myTree?.setCurrentNode) this.$refs.myTree.setCurrentNode(newItem[0]);
});
} else {
if (this.data.length == 1 && this.$attrs['expand-first-level'] !== false) {
this.defaultExpandedKeys = [this.data[0][this.props.value]];
}
}
this.$forceUpdate();
},
// 单选事件
handleNodeClick(data, e) {
if (this.props.disabled && e.disabled) {
return false;
} else {
if (data[this.props.children] && data[this.props.children]?.length) {
return false;
}
}
this.$emit('input', data[this.props.value]);
this.visible = false;
this.$emit('change', data, e);
},
// 递归查找通用方法
recurrenceQuery(list, key, value) {
if (!list || !key || !value) return [];
let queryData = [];
list.map(item => {
if (item[this.props.children] && item[this.props.children].length) {
queryData.push(...this.recurrenceQuery(item[this.props.children], key, value));
}
if (item[key] == value) {
queryData.push(item);
}
return item;
});
return queryData;
},
selectClear(flag) {
if (!flag) {
this.$emit('input', '');
this.$emit('change', null, null);
}
this.$refs.myTree.setCurrentKey(null);
this.remoteMethod('');
},
getCheckedNodes() {
if (this.value !== null && this.value !== undefined && this.value !== '') {
return this.$refs.myTree.getCheckedNodes();
}
return [];
},
getCurrentNode() {
if (this.value !== null && this.value !== undefined && this.value !== '') {
return this.$refs.myTree.getCurrentNode();
}
return null;
},
valueFilter(val) {
let res = '';
[res] = this.recurrenceQuery(this.data, this.props.value, val);
return res?.[this.props.label] || '';
},
propsInit() {
this.props.label = this.props.label || 'label';
this.props.value = this.props.value || 'value';
this.props.children = this.props.children || 'children';
},
remoteMethod(query) {
this.$refs.myTree.filter(query);
},
filterNode(value, data) {
if (!value) return true;
let result = true;
if (this.$listeners.onFilter) {
this.$emit('onFilter', value, data, res => {
result = res;
});
} else {
result = data[this.props.label].indexOf(value) !== -1;
}
return result;
},
searchHighlightFilter(node, data) {
let { label } = this.props;
if (this.$attrs['search-highlight'] === false) return data[label];
if (!this.filterText) return data[label];
const regex = new RegExp(this.filterText, 'gi');
let text = data[label].replace(regex, match => {
return `<strong class="highlight">${match}</strong>`;
});
return text;
},
},
watch: {
value: {
deep: true,
handler(val) {
if (!val) {
this.selectClear(true);
}
},
},
filterText(val) {
this.$refs.myTree.filter(val);
},
},
};
CSS
.selecTree {
max-height: 50vh;
overflow: auto;
padding: 5px;
::v-deep .el-tree-node__content {
font-size: 14px;
}
}
::v-deep.slotSpan {
font-size: 14px;
> i {
color: #67c23a;
margin-right: 5px;
}
b {
font-weight: normal;
font-size: 12px;
color: #999;
}
.highlight {
color: #67c23a;
}
}
.treeDialogBody {
max-height: 60vh;
display: flex;
flex-direction: column;
::v-deep .el-input__validateIcon {
display: none;
}
::v-deep .treeDialogBodyTree {
flex: 1;
overflow: auto;
padding: 12px 8px;
margin: 12px 0;
background-color: #f5f7fa;
border-radius: 5px;
.el-tree {
background: transparent;
.el-tree-node__content:hover {
background-color: #eaeef4;
}
}
}
}
.footer {
text-align: right;
}
使用案例
<template>
<treeDialogSelect
v-model="treeDialogSelectVal"
:data="treeDialogSelectData"
show-count
></treeDialogSelect>
</template>
<script>
import treeDialogSelect from '@/components/treeDialogSelect';
export default {
components: { treeDialogSelect },
data() {
return {
treeDialogSelectValue: '',
treeDialogSelectData: [
{
id: 1,
label: '一级 1',
children: [
{
id: 4,
label: '二级 1-1',
children: [
{
id: 9,
label: '三级 1-1-1',
},
{
id: 10,
label: '三级 1-1-2',
},
],
},
],
},
{
id: 2,
label: '一级 2',
children: [
{
id: 5,
label: '二级 2-1',
},
{
id: 6,
label: '二级 2-2',
},
],
},
{
id: 3,
label: '一级 3',
children: [
{
id: 7,
label: '二级 3-1',
},
{
id: 8,
label: '二级 3-2',
},
],
},
],
};
},
};
</script>
使用文档
以下只列举主要属性与方法,更多具体的属性配置请移步element官网进行查看。
属性
属性名 | 类型 | 默认值 | 是否必传 | 说明 |
---|---|---|---|---|
value / v-model | string / number | - | 是 | 绑定值 |
props | object | 与element保持一致 | 否 | 配置选项,具体配置可以参照element ui库中el-tree的配置 |
expand-first-level | boolean | true | 否 | 当根节点只有一个时,是否展开 |
search-highlight | boolean | true | 否 | 是否开启搜索高亮,仅在slot-tree未传入时生效 |
show-count | boolean | false | 否 | 若节点中存在children,则在父节点展示所属children的数量,注意但设置插槽时 show-count将失效 |
事件
事件名称 | 说明 | 回调参数 |
---|---|---|
change | 当选择项发生改变时触发 | 共两个参数,依次为:当前节点的数据,当前节点的 Node 对象 |
onFilter | 当过滤框输入的值改变时触发 | 共三个参数,依次为:搜索框的值,当前节点的数据,回调函数callback, 接受一个参数类型boolean,表示是否展示该节点 |
插槽
name | 说明 |
---|---|
- | 页面展示的输入框slot,如果传入默认插槽,则会不显示默认el-input,参数为 { value } |
prepend | 弹窗中过滤文本框的顶部插槽 |
tree | 自定义树节点的内容,参数为 { node, data } |