前言
hello,大家好!我是DongXH丶
今天想和大家分享一个vue-tree-selet组件封装,开发过程中我们可能会遇到一个全新的业务需求,也可能会遇到一个原来已有的功能去迭代新的功能。
这次的tree-select组件就是一个对原来的业务需求的改版迭代,在el-drawer
嵌套el-tree
的基础上进行结合el-select
的下拉选择。
在一个原有的功能基础上去开发迭代是值得我们去思考的。
而不仅仅只是command+c和command+v了!
这篇文章总结了一些组件封装时候的遇到的问题以及解决方案,还有我自己的一个组件封装的思想。
写文总结不易,若有更好的设计和思想,期待一起碰撞,大家共同进步。
需求分析
大概需求如下图:
当我拿到需求拟稿的时候,我先看了
element-ui
的el-tree
单独的组件是无法满足需求,所以需要对组件进行二次的开发,结合el-select
和el-dialog
进行需求实现。
其中进行了几点处理:
- 如果父级下所有子元素都被勾选,那么默认只回传父级一个id给到后台,如果不这么处理的话,勾选所有子集在我们这个
tree
中的话会有1w+节点,全部回传对接口性能是极大的不友好。 - 如果父级下所有子节点有未勾选状态,那么将所有勾选子集进行回传。
- 选中的节点,点击时进行子集数据的加载,减少组件初次渲染的数据。
接下来,我根据需求思考要实现的功能如下:
- 支持数据一次性和懒加载。
- 支持用户通过展开类目点击选择。(ps:点击第一个面板时将它子节点的第一级面板全部展开)
- 支持初始化状态,将默认展开第一个父级节点或者回显值值得展开选中。
- 支持是否严格遵循父子不互相关联。
- 支持单选和多选。
- 支持select选择框宽度自定义,dialog的宽高自定义。
组件具体用法
当前页面引入组件后页面具体设置如下:
<select-tree
v-model="绑定的model名"
:width="`100%`"
:height="`auto`"
:maxHeight="`400px`"
:nodeKey="'department_id'"
size=""
multiple
clearable
:defaultProps="{
children: 'children',
label: 'short_name'
}"
:defaultExpandedKeys="默认展开的节点数组"
:checkedKeys="默认选中节点数组"
@change="changeTreeItem"
:getGroupSequence="getGroupSequence"
/>
具体实现
第一步是确定组件需要的字段,那些是需要从父级传入。
props: {
// 获取tree-data的接口
getGroupSequence: {
type: Function,
default() {}
},
// props自定义配置
defaultProps: {
type: Object,
default() {
return {};
}
},
// 配置是否可多选
multiple: {
type: Boolean,
default() {
return false;
}
},
clear: {
type: Boolean,
default() {
return false;
}
},
// el-select配置是否可清空选择
clearable: {
type: Boolean,
default() {
return false;
}
},
// 配置多选时是否将选中值按文字的形式展示
collapseTags: {
type: Boolean,
default() {
return false;
}
},
// 设置el-tree的nodeKey值
nodeKey: {
type: String,
default() {
return "department_id";
}
},
// 显示复选框情况下,是否严格遵循父子不互相关联
checkStrictly: {
type: Boolean,
default() {
return false;
}
},
// 默认选中的节点key数组
checkedKeys: {
type: Array,
default() {
return [];
}
},
// el-select的大小样式
size: {
type: String,
default() {
return "medium";
}
},
// el-select的大小样式
width: {
type: String,
default() {
return `250px`;
}
},
// el-dialog的默认高度
height: {
type: String,
default() {
return `300px`;
}
},
// 默认展开的tree节点集合
defaultExpandedKeys: {
type: Array,
default() {
return ["D1000001"];
}
},
// el-dialog的默认高度,如果高度设置100%时配置最大的高度,不设置会有滚动太长超出页面的情况
maxHeight: {
type: String,
default() {
return `400px`;
}
},
// 节点数据是否懒加载
lazy:{
type: Boolean,
default() {
return true;
}
}
},
如果默认为新增,没有传入的defaultExpandedKeys
,默认组件渲染时渲染一级列表:
getTreeData(cb) {
this.getGroupSequence({ department_id: "一级组件id" }).then(res => {
this.treeData = res.data;
cb();
});
},
在mounted
中执行渲染数据函数:
mounted() {
this.getTreeData(() => {
this.initCheckedData();
});
},
需要有一个callback函数来进行el-dialog
的默认高度,在回调函数执行:
initCheckedData() {
if (this.multiple) {
// 多选
this.defaultExpand = this.defaultExpandedKeys;
this.selectedData = this.checkedKeys;
this.options = this.checkedKeys;
this.checkedKeys.map(item => {
if (item) {
this.defaultDeptAll.push(item.department_id);
}
});
} else {
// 单选
if (this.selectedData.length > 0) {
this.checkSelectedNode(this.selectedData);
}
}
this.$nextTick(() => {
this.popoverWidth = this.$refs.select.$el.clientWidth - 24;
});
},
依据上述的传入字段就可以来进行接下来的交互操作了,根据lazy
的具体值设置:load="loadNode",
loadNode(node, resolve) {
if (node.level === 0) {
return;
}
this.getGroupSequence({ department_id: node.data.department_id }).then(
res => {
if (res.data && res.data.length > 0) {
resolve(res.data);
this.$refs.tree.setCheckedKeys(this.defaultDeptAll);
} else {
resolve([]);
}
}
);
}
设置点击节点时根据当前父节点id去获取子集,进行渲染。然后就是根据传入的单选多选的值进行选择的判断处理:
// 单选,节点被点击时的回调,返回被点击的节点数据
handleNodeClick(data, node) {
if (!this.multiple) {
this.setSelectOption(node);
this.isShowSelect = !this.isShowSelect;
this.$emit("change", this.selectedData);
}
},
// 多选,节点勾选状态发生变化时的回调
handleCheckChange(data, checked, node) {
console.log("handleCheckChange", data, checked, node);
if (checked) {
let array = this.$refs.tree.getNode(data.department_id);
if (array.childNodes.length == 0) {
this.checkedData.push(data);
this.defaultDeptAll.push(data.department_id);
}
} else {
if (this.checkedData.length > 0) {
this.checkedData.forEach((item, index) => {
if (item.department_id == data.department_id) {
this.defaultDeptAll.splice(index, 1);
this.checkedData.splice(index, 1);
}
});
}
}
if (this.firstLoad || this.checkedKeys.length == 0) {
this.setCheckedKey();
} else {
this.firstLoad = true;
}
this.$emit("change", this.defaultDeptAll, this.defaultExpand);
},
然后是选中渲染后如果操作el-select
中的删除时要对el-tree
中的数据进行更新:
// 多选,删除任一select选项的回调
removeSelectedNodes(val) {
this.$refs.tree.setChecked(val, false);
var node = this.$refs.tree.getNode(val);
if (!this.checkStrictly && node.childNodes.length > 0) {
this.treeToList(node).map(item => {
if (item.childNodes.length <= 0) {
this.$refs.tree.setChecked(item, false);
}
});
}
this.$emit("change", this.selectedData);
}
还有一些其他公用功能的函数就不一一列举,具体的代码请自行跳转查看;
组件源码地址
复盘
完成业务需求并不难,可能有很多种的方式可以达到目的。我觉得更多的是在开发过程中的思考以及如何设计是写代码一开始很重要的一部分。整体的组件设计会影响很多,一个好的设计思想会让你的代码提高更良好的复用性,扩展性,以及组件的性能。
开发过程中遇到了很多的问题,有些问题卡壳很长时间,比如有默认值反显时会重新渲染一次,子组件暴露给父级组件的内容重新赋值导致组件循环执行等一些列问题。遇到问题、解决问题、反思问题、对自己的技术提升有很大的帮助~
目前组件虽然已经上线使用,但是现在还有以下几个问题,等后期还需要继续优化:
- 我们的需求是如果子集全部勾选返回父级id,但是如果当前父级下只有一个子集,返回的结果是当前子集还是父级节点?
el-tree
目前版本没有搜索功能。- 其他的一些不合理的地方进行优化迭代。
总结
在开发的过程当中,我还是觉得封装组件设计组件是有必要的,开发思想和开发能力同样的重要,思路清晰,事半功倍。
这篇文章是最近项目上线抽空码的,如果大家觉得有帮助的话,欢迎点赞,star~~
如果有更好的想法或者实现的方式的话,欢迎评论区留言,生命不息,讨论不止~