最近公司有个新需求,希望能有一个下拉选择树的功能,大概的功能和样式如下所示:
然后我的第一反应就是上elementui上找现成的组件,但是挺遗憾的就是element并没有提供这样的组件,所以只能自己动手造一个了。
1. 组件需求 (1) 支持单选和多选功能 (2) 叶子节点控制是否能选择 (3) 数据回显到选择框支持多选和单选显示 (4) 支持树节点搜索功能 (5) 基本样式应与elementUi样式保持一致
2.布局和样式代码编写 由于这个地方需要用到弹出下拉框,所以我就借助了el-popover来实现这个功能,代码如下:
<template> <div class="ka-tree-select" :style="{width:width+'px'}"> <el-popover placement="bottom" :width="width" trigger="click"> <div class="ka-select-box" slot="reference"> <div class="tag-box"> <div v-show="selecteds.length>0"> 显示的内容 </div> <p class="ka-placeholder-box" v-show="selecteds.length===0">请输入内容</p> </div> </div> </el-popover> </div> </template> <script> export default { name: "treeSelect", props: { width: { type: [String, Number], default: 200 } }, data() { return { selecteds: [] // 选择到的数据 }; } }; </script> <style lang="scss" scoped> .ka-tree-select{ position: relative; display: inline-block; width: 100%; vertical-align: middle; outline: none; .ka-select-box { display: flex; border: 1px solid #dcdfe6; padding: 0 5px 0 8px; width: 100%; min-height: 36px; // height: 36px; line-height: 34px; box-sizing: border-box; border-radius: 4px; cursor: pointer; outline: none; &:focus { border-color: #409eff; } > .tag-box { display: inline-block; width: calc(100% - 20px); text-align: left; } > .icon-box { float: right; display: flex; width: 20px; justify-content: center; align-items: Center; color: #c0c4cc; // transform: rotateZ(45deg); .el-icon-arrow-down { transition: all 0.2s; } .down { transform: rotateZ(180deg); } } .ka-placeholder-box { color: #c0c4cc; margin: 0; } } } </style>样式和基本结构也已经构建好了,值得注意一点就是HTML结构中,.tag-box这个类名容器中会存放所选择到的数据,是用来存放单选和多选的页签。
3.树结构制作 这里的树结构我主要使用到了el-tree这个组件,为了让它有个好看的滚动条,这里我也选择了el-scrollBar滚动条组件,值得一提的就是使用el-scrollbar组件时候,必须给父容器添加一个高度,然后el-scrollBar的高度为100%,要不然滚动条为出不来。代码如下:
<template> <div class="ka-tree-select" :style="{width:width+'px'}"> <el-popover placement="bottom" :width="width" trigger="click"> <el-input v-if="filterable" v-model="filterText" :size="size" placeholder="请输入关键词"></el-input> <el-scrollbar class="ka-treeselect-popover"> <el-tree :data="data" :props="selfProps" :node-key="nodeKey" highlight-current :default-checked-keys="checked_keys" :default-expanded-keys="expandedKeys" :show-checkbox="checkbox" check-strictly ref="tree-select" :filter-node-method="filterNode" ></el-tree> <!-- @check="handleCheckChange" --> <!-- @node-click="handleNodeClick" --> </el-scrollbar> <div class="ka-select-box" slot="reference"> <div class="tag-box"> <div v-show="selecteds.length>0"> 显示的内容 </div> <p class="ka-placeholder-box" v-show="selecteds.length===0">请输入内容</p> </div> </div> </el-popover> </div> </template> <script> export default { name: "treeSelect", props: { width: { // 宽度 type: [String, Number], default: 200 }, size: { // 尺寸 type: String, default: "mini" }, data: { // 树结构的数据 type: Array, default: () => [] }, nodeKey: { // 树结构的唯一标识 type: String, default: "id" }, // 是否使用搜索 filterable: { type: Boolean, default: true }, // 显示的字段 props: { type: Object, default: () => ({ label: "label", children: "children", }) }, // 是否可多选 checkbox: { type: Boolean, default: false }, }, data() { return { selecteds: [], // 选择到的数据 checked_keys: [], // 默认选中的数据 expandedKeys: [], // 默认展开的数据 filterText: "" // 筛选的数据 }; }, computed: { selfProps () { return { label: "label", children: "children", disabled: data => data.disabled, ...this.props }; }, }, methods: { // 树节点筛选 filterNode (value, data) { if (!value) { return true; } return data[this.selfProps.label].indexOf(value) !== -1; } }, watch: { filterText (val) { this.$refs["tree-select"].filter(val); } } }; </script> <style lang="scss" scoped> .ka-tree-select{ position: relative; display: inline-block; width: 100%; vertical-align: middle; outline: none; .ka-select-box { display: flex; border: 1px solid #dcdfe6; padding: 0 5px 0 8px; width: 100%; min-height: 36px; // height: 36px; line-height: 34px; box-sizing: border-box; border-radius: 4px; cursor: pointer; outline: none; &:focus { border-color: #409eff; } > .tag-box { display: inline-block; width: calc(100% - 20px); text-align: left; } > .icon-box { float: right; display: flex; width: 20px; justify-content: center; align-items: Center; color: #c0c4cc; // transform: rotateZ(45deg); .el-icon-arrow-down { transition: all 0.2s; } .down { transform: rotateZ(180deg); } } .ka-placeholder-box { color: #c0c4cc; margin: 0; } } } .ka-treeselect-popover { height: 360px; /deep/ .el-scrollbar__wrap { overflow-x: hidden; } } </style>4.处理显示的值 接下来的话就轮到控制显示输入框的值了,我们要考虑一点,作为公共组件是如何提供给业务组件使用的问题,我假设在业务组件A中,使用了这个值,并且期望它的使用方式如下:
<template> <div style="padding:10px"> <el-row> <el-col :span="12"> <treeSelect :data="treeData" v-model="select" checkbox :width="500" ref="treeSelect" ></treeSelect> </el-col> </el-row> </div> </template> <script> import treeSelect from "@/components/treeSelect/test"; export default { components: { treeSelect }, data () { return { treeData: [{ 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" }, { id: 18, label: "二级 3-2" }, { id: 82, label: "二级 3-2" }, { id: 84, label: "二级 3-2" }, { id: 842, label: "二级 3-2" }, { id: 847, label: "二级 3-2" }] }, { id: 11, label: "最外面" } ], select: [9, 5] }; } }; </script> <style> </style>select这个变量代表我选中了这两个节点,还可以默认展开,并且是在输入框中显示所对应的label值,那么就回到treeSelect组件中编写对应的逻辑。在created中定义处理默认值的方法handDefaultValue
// 处理默认值的方法 handDefaultValue(value) { if (!Array.isArray(value) || value.length === 0) { return; } this.expandedKeys = []; if (!this.checkbox) { // 单选的情况 this.$nextTick(() => { this.$refs["tree-select"].setCurrentNode({ id: value[0] }); let currentNode = this.$refs["tree-select"].getCurrentNode(); this.expandedKeys.push(value[0]); this.selecteds = [currentNode]; this.$emit("change", this.selecteds); }); } else { // 多选的情况 this.$nextTick(() => { this.$refs["tree-select"].setCheckedKeys(value); let currentAllNode = this.$refs["tree-select"].getCheckedNodes(); value.forEach(v => { this.expandedKeys.push(v); }); this.selecteds = currentAllNode; this.$emit("change", this.selecteds); }); } }这个时候我们的selecteds数组中已经初始化了选中当前树节点的数据,这个时候我们只需要把树节点对应的label渲染出来即可,这里我使用到el-tag这个组件,我把文字的显示分成了两种情况一种是平铺展示,一种折叠显示,代码如下:
<template v-if="!collapseTags"> <el-tag closable :size="size" v-for="item in selecteds" :title="item[selfProps.label]" :key="item[nodeKey]" class="ka-select-tag" @close="tabClose(item[nodeKey])" >{{ item[selfProps.label] }}</el-tag> </template> <template v-else> <el-tag closable :size="size" class="ka-select-tag" :title="collapseTagsItem[selfProps.label]" @close="tabClose(collapseTagsItem[nodeKey])" >{{ collapseTagsItem[selfProps.label] }}</el-tag> <el-tag v-if="this.selecteds.length>1" :size="size" class="ka-select-tag" >+{{ this.selecteds.length-1}}</el-tag> </template>此时完整的代码:
<template> <div class="ka-tree-select" :style="{width:width+'px'}"> <el-popover placement="bottom" :width="width" trigger="click"> <el-input v-if="filterable" v-model="filterText" :size="size" placeholder="请输入关键词"></el-input> <el-scrollbar class="ka-treeselect-popover"> <el-tree :data="data" :props="selfProps" :node-key="nodeKey" highlight-current :default-checked-keys="checked_keys" :default-expanded-keys="expandedKeys" :show-checkbox="checkbox" check-strictly ref="tree-select" :filter-node-method="filterNode" ></el-tree> <!-- @check="handleCheckChange" --> <!-- @node-click="handleNodeClick" --> </el-scrollbar> <div class="ka-select-box" slot="reference"> <div class="tag-box"> <div v-show="selecteds.length>0"> <template v-if="!collapseTags"> <el-tag closable :size="size" v-for="item in selecteds" :title="item[selfProps.label]" :key="item[nodeKey]" class="ka-select-tag" @close="tabClose(item[nodeKey])" >{{ item[selfProps.label] }}</el-tag> </template> <template v-else> <el-tag closable :size="size" class="ka-select-tag" :title="collapseTagsItem[selfProps.label]" @close="tabClose(collapseTagsItem[nodeKey])" >{{ collapseTagsItem[selfProps.label] }}</el-tag> <el-tag v-if="this.selecteds.length>1" :size="size" class="ka-select-tag" >+{{ this.selecteds.length-1}}</el-tag> </template> </div> <p class="ka-placeholder-box" v-show="selecteds.length===0">请输入内容</p> </div> </div> </el-popover> </div> </template> <script> export default { name: "treeSelect", props: { width: { // 宽度 type: [String, Number], default: 200 }, size: { // 尺寸 type: String, default: "mini" }, data: { // 树结构的数据 type: Array, default: () => [] }, nodeKey: { // 树结构的唯一标识 type: String, default: "id" }, // 是否使用搜索 filterable: { type: Boolean, default: true }, // 显示的字段 props: { type: Object, default: () => ({ label: "label", children: "children", }) }, // 是否可多选 checkbox: { type: Boolean, default: false }, // 选中数据 value: [Array], // 多选时是否将选中值按文字的形式展示 collapseTags: { type: Boolean, default: false }, }, created () { this.handDefaultValue(this.value); }, data() { return { selecteds: [], // 选择到的数据 checked_keys: [], // 默认选中的数据 expandedKeys: [], // 默认展开的数据 filterText: "" // 筛选的数据 }; }, computed: { selfProps () { return { label: "label", children: "children", disabled: data => data.disabled, ...this.props }; }, }, methods: { // 树节点筛选 filterNode (value, data) { if (!value) { return true; } return data[this.selfProps.label].indexOf(value) !== -1; }, // 处理默认值的方法 handDefaultValue(value) { if (!Array.isArray(value) || value.length === 0) { return; } this.expandedKeys = []; if (!this.checkbox) { // 单选的情况 this.$nextTick(() => { this.$refs["tree-select"].setCurrentNode({ id: value[0] }); let currentNode = this.$refs["tree-select"].getCurrentNode(); this.expandedKeys.push(value[0]); this.selecteds = [currentNode]; this.$emit("change", this.selecteds); }); } else { // 多选的情况 this.$nextTick(() => { this.$refs["tree-select"].setCheckedKeys(value); let currentAllNode = this.$refs["tree-select"].getCheckedNodes(); value.forEach(v => { this.expandedKeys.push(v); }); this.selecteds = currentAllNode; this.$emit("change", this.selecteds); }); } } }, watch: { filterText (val) { this.$refs["tree-select"].filter(val); } } }; </script> <style lang="scss" scoped> .ka-tree-select{ position: relative; display: inline-block; width: 100%; vertical-align: middle; outline: none; .ka-select-box { display: flex; border: 1px solid #dcdfe6; padding: 0 5px 0 8px; width: 100%; min-height: 36px; // height: 36px; line-height: 34px; box-sizing: border-box; border-radius: 4px; cursor: pointer; outline: none; &:focus { border-color: #409eff; } > .tag-box { display: inline-block; width: calc(100% - 20px); text-align: left; } > .icon-box { float: right; display: flex; width: 20px; justify-content: center; align-items: Center; color: #c0c4cc; // transform: rotateZ(45deg); .el-icon-arrow-down { transition: all 0.2s; } .down { transform: rotateZ(180deg); } } .ka-placeholder-box { color: #c0c4cc; margin: 0; } .ka-select-tag { max-width: 100%; text-overflow: ellipsis; overflow: hidden; word-wrap: break-word; word-break: break-all; vertical-align: middle; } .ka-select-tag + .ka-select-tag { margin-left: 4px; } } } .ka-treeselect-popover { height: 360px; /deep/ .el-scrollbar__wrap { overflow-x: hidden; } } </style>这个时候只需关注点击树节点所触发的函数,点击树节点el-checkBox和关闭el-tag时的函数即可。
// 点击checkbox变化 handleCheckChange (val) { let nodes = this.$refs["tree-select"].getCheckedNodes(false); this.selecteds = nodes; this.$emit("change", this.selecteds); }, // 点击列表树 handleNodeClick (item, node) { if (this.checkbox) { return; } this.selecteds = [item]; // this.options_show = false; this.$emit("change", this.selecteds); }, // tag标签关闭 tabClose (id) { if (this.disabled) { return; } if (!this.checkbox) { // 单选 this.selecteds = []; this.$refs["tree-select"].setCurrentKey(null); } else { // 多选 this.$refs["tree-select"].setChecked(id, false, true); this.selecteds = this.$refs["tree-select"].getCheckedNodes(); } this.$emit("change", this.selecteds); },那么最后还有最后一个小的功能点,就是实现只能选择叶子节点的功能,那么我们就定义一个变量isLeaf来进行标识。在el-tree中只需要给节点添加上disabled字段即可标识当前节点是否可选了,那么我们只需要遍历这个树节点,循坏遍历,判断当前节点的children字段是否是大于0,大于0,disable则为true。
// 转换treedata changeTreeData (data) { if (!data) { return; } let stack = []; data.forEach(v => { stack.push(v); }); while (stack.length) { const result = stack.shift(); if (result.children && result.children.length > 0) { result.disabled = true; stack = stack.concat(result.children); } else { result.disabled = false; } } return data; }, created(){ this.isLeaf && this.changeTreeData(this.data); }这个时候已经完成了下拉选择树的功能,功能要点如下: (1) 支持单选和多选功能 (2) 叶子节点控制是否能选择 (3) 数据回显到选择框支持多选和单选显示 (4) 支持树节点搜索功能 (5) 基本样式应与elementUi样式保持一致
源码地址为:https://github.com/whenTheMorningDark/vue-kai-admin/blob/master/src/components/treeSelect/index.vue