1. 需求
elementUI的form表单如果实现一个下拉树的需求,也不想用级联的功能,就得自己手动搓代码了。
2. 代码
组件代码
<template>
<div ref="treeSelect" class="tree-select">
<div class="tree-select-dom" @click="handleIconClick">
<el-input
v-model="inputValue"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
readonly
@click.native="handleInputClick"
@clear="clearSelection"
>
<i
slot="suffix"
:class="[
'el-input__icon', showPopover ? 'el-icon-arrow-up' : 'el-icon-arrow-down'
]"
/>
</el-input>
</div>
<el-popover
ref="popover"
v-model="showPopover"
v-click-outside="handleClickOutside"
placement="bottom-start"
trigger="manual"
:width="popoverWidth"
popper-class="tree-select-popover"
>
<div class="popover-content">
<div class="tree-filter-input">
<el-input
v-if="filterable"
v-model="filterText"
placeholder="输入关键字过滤"
prefix-icon="el-icon-search"
size="small"
/>
</div>
<el-tree
ref="tree"
class="tree-select-tree"
:data="data"
:props="defaultProps"
:highlight-current="true"
:filter-node-method="filterNode"
:node-key="nodeKey"
:default-expand-all="defaultExpandAll"
:expand-on-click-node="false"
@node-click="handleNodeClick"
>
<div slot-scope="{ node }" class="custom-tree-node">
<span :class="['tree-node-label', { 'is-leaf': node.isLeaf }]">
<i v-if="!node.isLeaf" class="el-icon-folder" />
<i v-else class="el-icon-document" />
{{ node.label }}
</span>
</div>
</el-tree>
</div>
</el-popover>
</div>
</template>
export default {
name: 'TreeSelect',
directives: {
// 自定义指令:点击外部区域关闭下拉框
'click-outside': {
bind: function(el, binding, vnode) {
el.clickOutsideEvent = function(event) {
if (!(el === event.target || el.contains(event.target))) {
vnode.context[binding.expression](event);
}
};
document.body.addEventListener('click', el.clickOutsideEvent);
},
unbind: function(el) {
document.body.removeEventListener('click', el.clickOutsideEvent);
}
}
},
props: {
// 树形数据
data: {
type: Array,
default: () => []
},
// 树节点配置
props: {
type: Object,
default: () => ({})
},
// 选中的节点值
value: {
type: [String, Number],
default: null
},
// 占位文本
placeholder: {
type: String,
default: '请选择'
},
// 是否可清空
clearable: {
type: Boolean,
default: true
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否可搜索
filterable: {
type: Boolean,
default: false
},
// 是否默认展开所有节点
defaultExpandAll: {
type: Boolean,
default: false
},
// 节点唯一标识字段
nodeKey: {
type: String,
default: 'id'
},
// 弹出框宽度
popoverWidth: {
type: [String, Number],
default: 300
}
},
data() {
return {
showPopover: false,
filterText: '',
currentValue: null,
currentLabel: '',
defaultProps: {
children: 'children',
label: 'label'
}
};
},
computed: {
inputValue() {
return this.currentLabel || '';
}
},
watch: {
value: {
immediate: true,
handler(newVal) {
this.currentValue = newVal;
if (newVal) {
this.setCurrentLabel();
} else {
this.currentLabel = '';
}
}
},
props: {
immediate: true,
handler(newProps) {
this.defaultProps = Object.assign({}, this.defaultProps, newProps);
}
},
filterText(val) {
this.$refs.tree.filter(val);
}
},
methods: {
// 处理输入框点击
handleInputClick() {
if (this.disabled) return;
this.togglePopover();
},
// 处理图标点击
handleIconClick(e) {
e.stopPropagation();
if (this.disabled) return;
this.togglePopover();
},
// 切换下拉框显示状态
togglePopover() {
this.showPopover = !this.showPopover;
if (this.showPopover) {
this.$nextTick(() => {
if (this.currentValue) {
this.$refs.tree.setCurrentKey(this.currentValue);
}
});
}
},
// 节点点击事件
handleNodeClick(data, node) {
if (node.isLeaf) {
this.currentValue = data[this.nodeKey];
this.currentLabel = node.label;
this.$emit('input', this.currentValue);
this.$emit('change', this.currentValue, node);
this.showPopover = false;
} else {
// 非叶子节点点击展开/折叠
node.expanded = !node.expanded;
}
},
// 设置当前显示的标签
setCurrentLabel() {
if (!this.data.length || !this.currentValue) return;
const findNode = (data, value) => {
for (const node of data) {
if (node[this.nodeKey] === value) {
return node;
}
if (node[this.defaultProps.children] && node[this.defaultProps.children].length) {
const result = findNode(node[this.defaultProps.children], value);
if (result) return result;
}
}
return null;
};
const node = findNode(this.data, this.currentValue);
if (node) {
this.currentLabel = node[this.defaultProps.label];
}
},
// 清空选择
clearSelection() {
this.currentValue = null;
this.currentLabel = '';
this.$emit('input', null);
this.$emit('change', null);
this.showPopover = false;
},
// 过滤节点
filterNode(value, data, node) {
if (!value) return true;
return node.label && node.label.toLowerCase().indexOf(value.toLowerCase()) !== -1;
},
// 处理点击外部区域
handleClickOutside() {
this.showPopover = false;
},
// 获取当前选中的节点
getCurrentNode() {
return this.$refs.tree.getCurrentNode();
}
}
};
<style lang="less" scoped>
.tree-select {
position: relative;
display: inline-block;
width: 100%;
}
.tree-select-dom{
position: relative;
&::after{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: '';
cursor: pointer;
}
}
.tree-select .el-input {
cursor: pointer;
}
.tree-select .el-input__icon {
transition: transform 0.3s;
cursor: pointer;
}
.popover-content {
padding: 10px;
max-height: 300px;
overflow-y: auto;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.tree-filter-input {
margin-bottom: 10px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
font-size: 14px;
padding: 5px 0;
}
.tree-node-label {
display: flex;
align-items: center;
transition: all 0.2s;
}
.tree-node-label:hover {
color: #3498db;
}
.tree-node-label i {
margin-right: 8px;
}
.tree-node-label.is-leaf i {
color: #2ecc71;
}
</style>
<style lang="less">
.tree-select-popover{
background-color: #4e5470;
padding: 0;
.popover-content{
background-color: #4e5470;
display: flex;
flex-direction: column;
.tree-filter-input{
flex-shrink: 0;
}
}
.tree-select-tree{
flex: 1;
overflow-x: hidden;
overflow-y: auto;
}
.el-input__inner{
padding-left: 30px;
}
}
</style>
3. 使用
<template>
<TreeSingleSelect
v-model="selectedValue1"
:data="treeData"
:prop="{
children: 'subitems',
label: 'name'
}"
placeholder="请选择部门"
node-key="id"
filterable
:default-expand-all="true"
class="demo-component"
/>
</template>
import TreeSingleSelect from '@/components/treeSelect/TreeSingleSelect.vue';
export default {
components: {
TreeSingleSelect
},
data() {
return {
selectedValue1: '',
treeData: [
{
id: 1,
label: '集团总部',
children: [
{
id: 101,
label: '总裁办公室',
children: [
{ id: 1011, label: '秘书处' },
{ id: 1012, label: '行政科' }
]
},
{
id: 102,
label: '人力资源部',
children: [
{ id: 1021, label: '招聘组' },
{ id: 1022, label: '培训组' },
{ id: 1023, label: '薪酬组' }
]
}
]
}
]
}
}
}
4. 效果
求关注 |
---|
![]() |