Vue - 自定义ElementUI树形下拉select选择组件教程(替代Cascader级联选择)
当页面下拉框内容是层级结构时,我们通常使用 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)运行结果: