Vue - 自定义ElementUI树形下拉select选择组件教程(替代Cascader级联选择)
作者:hangge | 2024-02-01 09:54
当页面下拉框内容是层级结构时,我们通常使用 Element-UI 提供的 Cascader 级联选择器逐级查看并选择。该组件支持单选和多选,下面是其多选状态的效果:

(2)运行结果如下:

(2)运行结果:
(2)运行结果:

但有时设计会要求下拉框的展示形式要采用树形结构样式,那么我们可以通过结合 Select 选择器和 Tree 树形控件来创建一个自定义组件,下面我将通过样例进行演示。
一、自定义单选树形下拉 select 组件
1,组件代码
我们自定义一个支持单选的 TreeSelect.vue 自定义组件,代码如下:<template>
<div>
<!-- 下拉选择框 -->
<el-select
:title="optionData.name"
ref="select"
:value="value"
:placeholder="placeholder"
clearable
:disabled="disabled"
:filterable="filterable"
:filter-method="filterMethod"
style="width: 100%"
@clear="clear"
@visible-change="visibleChange"
>
<!-- 下拉选项 -->
<el-option
ref="option"
class="tree-select__option"
:value="optionData.id"
:label="optionData.name"
>
<!-- 树形结构 -->
<el-tree
style=""
ref="tree"
class="tree-select__tree tree-select__tree--radio"
:node-key="nodeKey"
:data="data"
:props="props"
:default-expanded-keys="[value]"
:highlight-current="true"
:filter-node-method="filterNode"
@node-click="handleNodeClick"
></el-tree>
</el-option>
</el-select>
</div>
</template>
<script>
export default {
name: "TreeSelect",
props: {
// 提示文本
placeholder: {
type: String,
},
// v-model绑定的值
value: {
type: [String, Number],
default: "",
},
// 树形的数据
data: {
type: Array,
default: function () {
return [];
},
},
// 每个树节点用来作为唯一标识的属性
nodeKey: {
type: [String, Number],
default: "id",
},
// 是否可搜索
filterable: {
type: Boolean,
default: true,
},
// 是否禁用
disabled: {
type: Boolean,
default: false,
},
// tree的props配置
props: {
type: Object,
default: function () {
return {
label: "label",
children: "children",
};
},
},
},
data() {
return {
// 选中的节点数据
optionData: {
id: "",
name: "",
},
// 标记是否进行了过滤
filterFlag: false,
};
},
watch: {
// 监听value的变化
value: {
handler(val) {
if (!this.isEmpty(this.data)) {
this.init(val);
}
},
immediate: true,
},
// 监听data的变化
data: function (val) {
if (!this.isEmpty(val)) {
this.init(this.value);
}
},
},
created() {},
mounted() {
// 添加监听事件,用于处理文档body上的点击事件
document.body.addEventListener("click", this.handleBodyClick);
},
beforeDestroy() {
// 销毁监听事件
document.body.removeEventListener("click", this.handleBodyClick);
},
methods: {
// 判断对象是否为空
isEmpty(val) {
for (let key in val) {
return false;
}
return true;
},
// 处理节点点击事件
handleNodeClick(data) {
const selectedValue = data[this.nodeKey];
// 检查选中值是否真正变化
if (selectedValue !== this.value) {
this.optionData.name = data[this.props.label];
this.$emit("input", selectedValue);
this.$emit("change", selectedValue);
}
this.$refs.select.visible = false;
},
// 初始化选中值
init(val) {
val = val === "" ? null : val;
this.$nextTick(() => {
this.$refs.tree.setCurrentKey(val);
if (val === null) {
return;
}
const node = this.$refs.tree.getNode(val);
this.optionData.id = val;
this.optionData.name = node.label;
});
this.flag = true;
},
// 下拉框显示状态变化的回调函数
visibleChange(e) {
if (e) {
const tree = this.$refs.tree;
this.filterFlag && tree.filter("");
this.filterFlag = false;
let selectDom = tree.$el.querySelector(".is-current");
setTimeout(() => {
this.$refs.select.scrollToOption({ $el: selectDom });
}, 0);
}
},
// 清空选中值的回调函数
clear() {
if (this.value) {
this.$emit("input", "");
this.$emit("change", "");
}
},
// 下拉框搜索的回调函数
filterMethod(val) {
this.filterFlag = true;
this.$refs.tree.filter(val);
},
// 树节点过滤的方法
filterNode(value, data) {
if (!value) return true;
const label = this.props.label || "name";
return data[label].indexOf(value) !== -1;
},
// 处理文档body上的点击事件
handleBodyClick(event) {
// 检查点击是否发生在下拉框外部
if (this.$refs.select && !this.$refs.select.$el.contains(event.target)) {
// 关闭下拉框
this.$refs.select.visible = false;
}
},
},
};
</script>
<style lang="scss">
/* 下拉选项样式 */
.tree-select__option {
&.el-select-dropdown__item {
height: auto;
line-height: 1;
padding: 0;
background-color: #fff;
}
}
/* 树形结构样式 */
.tree-select__tree {
color: #333333;
padding: 4px 20px;
font-weight: 400;
&.tree-select__tree--radio {
/* 选中节点样式 */
.el-tree-node.is-current > .el-tree-node__content {
font-weight: 700;
}
}
}
</style>
2,使用样例
(1)我们引入这个自定义组件并使用:<template>
<div id="container">
<div>选中项的值: {{ selectedValue }}</div>
<tree-select
v-model="selectedValue"
:data="treeData"
:nodeKey="'id'"
:props="{ label: 'name', children: 'children' }"
placeholder="请选择"
@change="handleSelectionChange"
></tree-select>
</div>
</template>
<script>
import TreeSelect from "./components/TreeSelect.vue";
export default {
components: {
TreeSelect,
},
data() {
return {
selectedValue: 312, // 初始化选中项的值
treeData: [
{
id: 1,
name: "江苏省",
children: [
{
id: 11,
name: "南京市"
},
{
id: 12,
name: "苏州市",
},
{
id: 13,
name: "无锡市",
},
],
},
{
id: 2,
name: "浙江省",
children: [
{
id: 21,
name: "杭州市",
},
{
id: 22,
name: "宁波市",
},
{
id: 23,
name: "温州市",
},
],
},
{
id: 3,
name: "安徽省",
children: [
{
id: 31,
name: "合肥市",
children: [
{
id: 311,
name: "瑶海区",
},
{
id: 312,
name: "庐阳区",
},
{
id: 313,
name: "蜀山区",
},
],
},
{
id: 32,
name: "芜湖市",
},
{
id: 33,
name: "蚌埠市",
},
],
},
],
};
},
methods: {
// 当选中节点变化时触发
handleSelectionChange(newValue) {
console.log("选中项的值:", newValue);
// 在这里可以执行其他逻辑,例如向后端发送请求
},
},
};
</script>
(2)运行结果如下:

二、自定义多选树形下拉select组件
1,组件代码
我们自定义一个支持多选的 TreeMultipleSelect.vue 自定义组件,代码如下:
<template>
<div>
<!-- 树形选择器 -->
<el-select
:title="optionData.name"
ref="select"
:value="optionData.id"
:placeholder="placeholder"
clearable
:disabled="disabled"
:filterable="filterable"
:filter-method="filterMethod"
style="width: 100%"
@clear="clear"
@visible-change="visibleChange"
>
<!-- 树形选择器选项 -->
<el-option
ref="option"
class="tree-select__option"
:value="optionData.id"
:label="optionData.name"
>
<!-- 树形结构 -->
<el-tree
style=""
ref="tree"
class="tree-select__tree tree-select__tree--checked"
:node-key="nodeKey"
:data="data"
:props="props"
:default-expanded-keys="value"
:default-checked-keys="value"
:show-checkbox="true"
:expand-on-click-node="true"
:filter-node-method="filterNode"
@check-change="handleCheckChange"
></el-tree>
</el-option>
</el-select>
</div>
</template>
<script>
export default {
name: "TreeSelect",
props: {
// 提示文本
placeholder: {
type: String,
},
// 绑定值
value: {
type: Array,
default: [],
},
// 是否多选
multiple: {
type: Boolean,
default: true,
},
// 树形数据
data: {
type: Array,
default: function () {
return [];
},
},
// 节点唯一标识属性
nodeKey: {
type: [String, Number],
default: "id",
},
// 是否可过滤
filterable: {
type: Boolean,
default: true,
},
// 是否禁用
disabled: {
type: Boolean,
default: false,
},
// 树形props配置
props: {
type: Object,
default: function () {
return {
label: "label",
children: "children",
};
},
},
},
data() {
return {
// 选项数据
optionData: {
id: "",
name: "",
},
// 过滤标志
filterFlag: false,
};
},
watch: {
// 监听值变化
value: {
handler(val) {
if (!this.isEmpty(this.data)) {
this.init(val);
}
},
immediate: true,
},
// 监听数据变化
data: function (val) {
if (!this.isEmpty(val)) {
this.init(this.value);
}
},
},
created() {},
mounted() {
// 监听文档点击事件
document.body.addEventListener("click", this.handleBodyClick);
},
beforeDestroy() {
// 移除文档点击事件监听
document.body.removeEventListener("click", this.handleBodyClick);
},
methods: {
// 判断对象是否为空
isEmpty(val) {
for (let key in val) {
return false;
}
return true;
},
// 处理选项勾选改变事件
handleCheckChange() {
let arr = [];
const nodes = this.$refs.tree.getCheckedNodes();
nodes.map((item) => arr.push(item[this.nodeKey]));
const value = nodes.map((item) => item[this.nodeKey]).join(",");
this.$emit("input", arr);
},
// 初始化选项
init(val) {
const arr = val;
this.$nextTick(() => {
this.$refs.tree.setCheckedKeys(arr);
const nodes = this.$refs.tree.getCheckedNodes();
this.optionData.id = val.join(',');
this.optionData.name = nodes
.map((item) => item[this.props.label])
.join(",");
});
this.flag = true;
},
// 下拉框显示状态改变事件
visibleChange(e) {
if (e) {
const tree = this.$refs.tree;
this.filterFlag && tree.filter("");
this.filterFlag = false;
let selectDom = tree.$el.querySelector(".el-tree-node.is-checked");
setTimeout(() => {
this.$refs.select.scrollToOption({ $el: selectDom });
}, 0);
}
},
// 清空选项事件
clear() {
if (this.value) {
this.$emit("input", []);
this.$emit("change", []);
}
},
// 过滤方法
filterMethod(val) {
this.filterFlag = true;
this.$refs.tree.filter(val);
},
// 过滤节点方法
filterNode(value, data) {
if (!value) return true;
const label = this.props.label || "name";
return data[label].indexOf(value) !== -1;
},
// 处理文档body上的点击事件
handleBodyClick(event) {
// 检查点击是否发生在下拉框外部
if (this.$refs.select && !this.$refs.select.$el.contains(event.target)) {
// 关闭下拉框
this.$refs.select.visible = false;
}
},
},
};
</script>
<style lang="scss">
/* 下拉选项样式 */
.tree-select__option {
&.el-select-dropdown__item {
height: auto;
line-height: 1;
padding: 0;
background-color: #fff;
}
}
/* 树形样式 */
.tree-select__tree {
color: #333333;
padding: 4px 20px;
font-weight: 400;
}
</style>
2,使用样例
(1)我们引入这个自定义组件并使用:
(2)运行结果如下:
<template>
<div id="container">
<div>选中项的值: {{ selectedValue }}</div>
<tree-multiple-select
v-model="selectedValue"
:data="treeData"
:nodeKey="'id'"
:props="{ label: 'name', children: 'children' }"
placeholder="请选择"
@change="handleSelectionChange"
></tree-multiple-select>
</div>
</template>
<script>
import TreeMultipleSelect from "./components/TreeMultipleSelect.vue";
export default {
components: {
TreeMultipleSelect,
},
data() {
return {
selectedValue: [311, 313], // 初始化选中项的值
treeData: [
{
id: 1,
name: "江苏省",
children: [
{
id: 11,
name: "南京市"
},
{
id: 12,
name: "苏州市",
},
{
id: 13,
name: "无锡市",
},
],
},
{
id: 2,
name: "浙江省",
children: [
{
id: 21,
name: "杭州市",
},
{
id: 22,
name: "宁波市",
},
{
id: 23,
name: "温州市",
},
],
},
{
id: 3,
name: "安徽省",
children: [
{
id: 31,
name: "合肥市",
children: [
{
id: 311,
name: "瑶海区",
},
{
id: 312,
name: "庐阳区",
},
{
id: 313,
name: "蜀山区",
},
],
},
{
id: 32,
name: "芜湖市",
},
{
id: 33,
name: "蚌埠市",
},
],
},
],
};
},
methods: {
// 当选中节点变化时触发
handleSelectionChange(newValue) {
console.log("选中项的值:", newValue);
// 在这里可以执行其他逻辑,例如向后端发送请求
},
},
};
</script>
(2)运行结果如下:

附:Cascader 级联选择器使用样例
1,单选样例
(1)样例代码:
<template>
<div id="container">
<div>选中项的值: {{ selectedValue }}</div>
<el-cascader
v-model="selectedValue"
:options="cascaderData"
:props="{ value: 'id', label: 'name', children: 'children' }"
:show-all-levels="true"
placeholder="请选择"
@change="handleSelectionChange"
></el-cascader>
</div>
</template>
<script>
export default {
data() {
return {
selectedValue: [ 3, 31, 313 ], // 初始化选中项的值
cascaderData: [
{
id: 1,
name: "江苏省",
children: [
{ id: 11, name: "南京市" },
{ id: 12, name: "苏州市" },
{ id: 13, name: "无锡市" },
],
},
{
id: 2,
name: "浙江省",
children: [
{ id: 21, name: "杭州市" },
{ id: 22, name: "宁波市" },
{ id: 23, name: "温州市" },
],
},
{
id: 3,
name: "安徽省",
children: [
{
id: 31,
name: "合肥市",
children: [
{ id: 311, name: "瑶海区" },
{ id: 312, name: "庐阳区" },
{ id: 313, name: "蜀山区" },
],
},
{ id: 32, name: "芜湖市" },
{ id: 33, name: "蚌埠市" },
],
},
],
};
},
methods: {
// 当选中节点变化时触发
handleSelectionChange(newValue) {
console.log("选中项的值:", newValue);
// 在这里可以执行其他逻辑,例如向后端发送请求
},
},
};
</script>
(2)运行结果:

2,多选样例
(1)样例代码:<template>
<div id="container">
<div>选中项的值: {{ selectedValue }}</div>
<el-cascader
v-model="selectedValue"
:options="cascaderData"
:props="{ value: 'id', label: 'name', children: 'children', multiple: true }"
:show-all-levels="true"
placeholder="请选择"
@change="handleSelectionChange"
></el-cascader>
</div>
</template>
<script>
export default {
data() {
return {
selectedValue: [ [ 3, 31, 311 ], [ 3, 31, 313 ] ], // 初始化选中项的值
cascaderData: [
{
id: 1,
name: "江苏省",
children: [
{ id: 11, name: "南京市" },
{ id: 12, name: "苏州市" },
{ id: 13, name: "无锡市" },
],
},
{
id: 2,
name: "浙江省",
children: [
{ id: 21, name: "杭州市" },
{ id: 22, name: "宁波市" },
{ id: 23, name: "温州市" },
],
},
{
id: 3,
name: "安徽省",
children: [
{
id: 31,
name: "合肥市",
children: [
{ id: 311, name: "瑶海区" },
{ id: 312, name: "庐阳区" },
{ id: 313, name: "蜀山区" },
],
},
{ id: 32, name: "芜湖市" },
{ id: 33, name: "蚌埠市" },
],
},
],
};
},
methods: {
// 当选中节点变化时触发
handleSelectionChange(newValue) {
console.log("选中项的值:", newValue);
// 在这里可以执行其他逻辑,例如向后端发送请求
},
},
};
</script>
(2)运行结果:
全部评论(0)