赞
踩
闲来无事不从容,睡觉东窗日已红。
万物静观皆自得,四时佳兴与认同。
最近学习vue3组件的时候实现了一个简单的树组件。话不多说,直接上代码。
这个数组件实现了展开、选中和选择三个基本效果。如果有兴趣的话,可以自己参考代码实现其他更加牛叉的效果,例如,自定义树结构,单多选,搜索、排序等等。
1.使用树组件的文件
<!-- * @Date: 2022-10-27 15:46:26 * @LastEditors: zhangsk * @LastEditTime: 2022-11-11 14:09:16 * @FilePath: \basic-demo\src\pages\index.vue * @Label: Do not edit --> <template> <div class="container"> <h1>hello,World!</h1> <!-- 树组件 --> <TreeVue :data="treeData"></TreeVue> </div> </template> <script lang="ts" setup> import TreeVue from "@/components/Tree/index.vue"; import { reactive, toRefs, ref, onBeforeMount, onMounted } from "vue"; // 树数据 const treeData = reactive([ { name: "1-1", key: "1-1", lable: "1-1", children: [ { name: "1-1-1", key: "1-1-1", lable: "1-1-1", children: [], }, { name: "1-1-2", key: "1-1-2", lable: "1-1-2", children: [], }, { name: "1-1-3", key: "1-1-3", lable: "1-1-3", children: [], }, ], }, { name: "1-2", key: "1-2", lable: "1-2", children: [ { name: "1-2-1", key: "1-2-1", lable: "1-2-1", children: [ { name: "1-2-1-1", key: "1-2-1-1", lable: "1-2-1-1", children: null, }, { name: "1-2-1-2", key: "1-2-1-2", lable: "1-2-1-2", children: [], }, ], }, { name: "1-2-2", key: "1-2-2", lable: "1-2-2", children: [], }, { name: "1-2-3", key: "1-2-3", lable: "1-2-3", children: [], }, ], }, ]) as any; </script> <style lang="scss" scoped></style>
<!-- * @Date: 2022-11-01 09:50:50 * @LastEditors: zhangsk * @LastEditTime: 2022-11-11 14:19:13 * @FilePath: \basic-demo\src\components\Tree\index.vue * @Label: 树结构列表 --> <template> <div class="tree"> <div>展开项:{{ treeObj.insideExpandArr }} - 长度{{ len1 }}</div> <div>选中项:{{ treeObj.selectArr }} - 长度{{ len2 }}</div> <div>选择项:{{ treeObj.chooseArr }} - 长度{{ len3 }}</div> <TreeItemVue :data-source="data" v-model:inside-expand-arr="treeObj.insideExpandArr" v-model:selected-arr="treeObj.selectArr" v-model:choose-arr="treeObj.chooseArr" > </TreeItemVue> </div> </template> <script lang="ts" setup> import { reactive, toRefs, onBeforeMount, onMounted, watch, computed, ref, } from "vue"; import TreeItemVue from "./Tree-item.vue"; interface tree { name: string; label: string; key: string | number; children: Array<tree>; } const props = defineProps({ data: { type: Array<tree>, default: () => [], }, // 展开项,默认不展开 expandArr: { type: Array<string | number>, default: [], }, // 选中项 selectArr: { type: Array<string>, default: ["1-1"], }, chooseArr: { type: Array<string>, default: ["1-1"], }, }); // 组件内部定义展开项 const treeObj = reactive({ insideExpandArr: [...props.expandArr], selectArr: [...props.selectArr.slice(0, 1)], // 默认选中第一个 chooseArr: [...props.chooseArr], }); const len1 = computed(() => { return treeObj.insideExpandArr.length; }); const len2 = computed(() => { return treeObj.selectArr.length; }); const len3 = computed(() => { return treeObj.chooseArr.length; }); const { chooseArr } = toRefs(treeObj); watch(treeObj, (value) => { console.log(value, "改变了"); }); // </script> <style lang="scss" scoped></style>
<!-- * @Date: 2022-11-01 09:49:47 * @LastEditors: zhangsk * @LastEditTime: 2022-11-11 14:08:53 * @FilePath: \basic-demo\src\components\Tree\Tree-item.vue * @Label: 树结构列表 --> <template> <div class="tree__item"> <div v-for="item of dataSource"> <div class="tree__item__content"> <div class="arrow__wrapper" v-if="item.children && item.children.length > 0" @click="switchArrow(item.key)" > <div class="arrow"></div> </div> <div> <input type="checkbox" :value="item.name" :checked="isChecked(item.name, item.children)" @change="(e) => chooseChange(e, item)" /> </div> <div :class="{ active: props.selectedArr.includes(item.name) }" @click="selected(item.name)" > {{ item.name }} </div> </div> <div v-show="props.insideExpandArr.includes(item.key)"> <TreeItemVue v-if="item.children && item.children.length > 0" class="tree__item__child" :data-source="item.children" v-model:inside-expand-arr="childTreeData.childInsideExpandArr" v-model:selected-arr="childTreeData.isSelectedArr" v-model:choose-arr="childTreeData.isCheckedArr" > </TreeItemVue> </div> </div> </div> </template> <script lang="ts" setup> import { reactive, toRefs, onBeforeMount, onMounted, ref, computed, watch, } from "vue"; import TreeItemVue from "./Tree-item.vue"; interface tree { name: string; label: string; key: string | number; children: Array<tree>; } const props = defineProps({ dataSource: { type: Array<tree>, default: () => [], }, // 展开项数组 insideExpandArr: { type: Array<string | number>, default: [], }, // 选中数组 selectedArr: { type: Array<string>, default: () => [], }, // 选择数组 chooseArr: { type: Array<string>, default: () => [], }, }); const $emit = defineEmits([ "update:insideExpandArr", "update:selectedArr", "update:chooseArr", ]); // 组件展开项 interface treeData { childInsideExpandArr: Array<string | number>; isCheckedArr: Array<string>; isSelectedArr: Array<string>; } const childTreeData = reactive<treeData>({ childInsideExpandArr: [...props.insideExpandArr], isCheckedArr: [...props.chooseArr], isSelectedArr: [...props.selectedArr], }); watch( () => childTreeData.childInsideExpandArr, (value) => { //将最新的子value推送到父组件展开项中 // 由于子展开项数组赋了赋组件初始值,故包含父组件所有值,有变化后直接更新即可 $emit("update:insideExpandArr", value); } ); watch( () => childTreeData.isSelectedArr, (value) => { // 同上 $emit("update:selectedArr", value); } ); watch( () => childTreeData.isCheckedArr, (value) => { // 同上 $emit("update:chooseArr", value); } ); // 箭头切换,显隐子组件函数 const switchArrow = (key: string | number) => { let deleteKey: string | number = ""; if (childTreeData.childInsideExpandArr.includes(key)) { childTreeData.childInsideExpandArr.splice( childTreeData.childInsideExpandArr.findIndex((v) => v === key), 1 ); deleteKey = key; } else { childTreeData.childInsideExpandArr.push(key); } // 合并去重并触发事件 const arr = concatDuplicateRemoval( props.insideExpandArr, childTreeData.childInsideExpandArr ); if (deleteKey !== "" && arr.includes(deleteKey)) { arr.splice( arr.findIndex((v) => v === deleteKey), 1 ); } $emit("update:insideExpandArr", arr); }; /* 选中 */ const selected = (name: string) => { let deleteKey = ""; if (childTreeData.isSelectedArr.includes(name)) { // 删除 childTreeData.isSelectedArr.splice(0, 1); deleteKey = name; // let arr = props.selectedArr.filter((item) => item !== name); } else { // 替换第一个 childTreeData.isSelectedArr.splice(0, 1, name); // 默认选中一个 } if (deleteKey !== "") { $emit("update:selectedArr", []); return; } $emit("update:selectedArr", [name]); }; // 复选框 const chooseChange = (e: any, obj: tree) => { if (e) { let value = e.target.value; let delArr: Array<string> = []; let temp: Array<string> = []; // 递归删除或添加 const recursive = (children: Array<tree>, flag = false) => { if (!children) return false; if (children.length <= 0) return false; children.forEach((item) => { if (flag) { // 删除 delArr.push(item.name); } else { // 添加 temp.push(item.name); } recursive(item.children, flag); }); }; // 先判断这个item是否被选中 let flag = false; // 默认添加 if (props.chooseArr.includes(value)) { delArr.push(value); flag = true; } else { temp.push(value); } // 如果有子组件,选中或者取消子组件的选择状态 if (obj.children && obj.children.length > 0) { recursive(obj.children, flag); } // 更新响应子组件选择状态数据 temp.forEach((item) => { childTreeData.isCheckedArr.push(item); }); delArr.forEach((item) => { childTreeData.isCheckedArr.splice( childTreeData.isCheckedArr.findIndex((v) => v === item), 1 ); }); // 更新父组件的选择数据 const resArr = concatDuplicateRemoval(props.chooseArr, temp); if (delArr.length > 0) { delArr.forEach((item) => { resArr.splice( resArr.findIndex((v) => v === item), 1 ); }); } $emit("update:chooseArr", resArr); } }; // 是否选择 const isChecked = (name: string, children: Array<tree>): boolean => { let flag = false; if (props.chooseArr.includes(name)) { flag = true; // 有bug,这里应该判断有没有子组件,有的话请及时更新状态childTreeData.isCheckedArr数据,逻辑请参考chooseChange方法 // 我比较懒就不写了,嘿嘿 } else { flag = false; } return flag; }; // 合并去重函数 const concatDuplicateRemoval = <T extends {}>( arr1: Array<T>, arr2: Array<T> ): Array<T> => { let arr: Array<T> = [...arr1, ...arr2]; let temp: Array<T> = []; arr.forEach((item) => { if (temp.includes(item)) return; temp.push(item); }); return temp; }; </script> <style lang="scss" scoped> .tree__item { display: inline-block; line-height: 30px; margin-left: 16px; &__content { width: 80px; display: flex; align-items: center; margin-top: 2px; padding: 0 10px; cursor: pointer; .arrow__wrapper { height: 20px; height: 20px; margin-right: 8px; flex-shrink: 0; } .arrow { position: relative; top: 6px; left: 6px; width: 0; height: 0px; border-top: 4px solid #000; border-left: 4px solid #000; border-right: 4px solid transparent; border-bottom: 4px solid transparent; transform: rotate(-135deg); } .active { color: #fff; background-color: skyblue; } } &__child { } } </style>
上面实现逻辑利用了递归、props和emit来改变和更新父组件和子组件的数据,以及状态,这有一个非常不好的弊端!!!
没错,这个弊端就是递归+props完美的踩到了Vue官网的Prop逐级透传问题。这不但使代码逻辑复杂化,还贼容易出错。
所以,我们需要使用 provide 和 inject对代码进行改进。provide用于提供可以被后代组件注入的值,inject用于声明要通过从上层提供方匹配并注入进当前组件的属性。两者相辅相成,轻松的解决了props逐级透传问题。
上面主要实现了展开项、选中项和选择项;我们以选择项改进为例,选择项数据没有问题,但是显示状态有问题,代码中已经标注了bug处,我们利用provide和inject对选择项进行改进。
1。新创建了一个chooseArr来代替treeObj里的chooseArr。
2。新建一个改变chooseArr数据的函数
3。使用provide向子组件提供chooseArr和changeChooseArr
//provide(提供依赖属性) function changeChooseArr(arr: Array<string>, flag: boolean = false) { if (flag) { // 添加 chooseArr.value.splice(0); arr.forEach((item) => { chooseArr.value.push(item); }); } else { // 删除 arr.forEach((item) => { if (chooseArr.value.includes(item)) { chooseArr.value.splice( chooseArr.value.findIndex((v) => v === item), 1 ); } }); } } const chooseArr = ref([...props.chooseArr]); provide("chooseArr", { chooseArr, changeChooseArr, });
1.注入chooseArr和changeChooseArr 使用
2.根据操作使用它们
3.这里有一个我没写,就是根据树结构里的children里子组件状态改变当前item选中状态,意思是子组件全部选中了的话,自动选中当前item。
// 复选框 // inject(注入依赖) const { chooseArr, changeChooseArr } = inject("chooseArr") as any; const chooseChange = (e: any, obj: tree) => { if (e) { let value = e.target.value; let delArr: Array<string> = []; let temp: Array<string> = []; // 递归删除或添加 const recursive = (children: Array<tree>, flag = false) => { if (!children) return false; if (children.length <= 0) return false; children.forEach((item) => { if (flag) { // 删除 delArr.push(item.name); } else { // 添加 temp.push(item.name); } recursive(item.children, flag); }); }; // 先判断这个item是否被选中 let flag = false; // 默认添加 if (chooseArr.value.includes(value)) { delArr.push(value); flag = true; } else { temp.push(value); } // 如果有子组件,选中或者取消子组件的选择状态 if (obj.children && obj.children.length > 0) { recursive(obj.children, flag); } // 更新chooseArr if (flag) { // 删除 changeChooseArr(delArr, !flag); } else { // 添加 // 合并去重 const resArr = concatDuplicateRemoval(chooseArr.value, temp); changeChooseArr(resArr, !flag); } } }; // 是否选择 const isChecked = (name: string): boolean => { let flag = false; if (chooseArr.value.includes(name)) { flag = true; } else { flag = false; } return flag; };
另外两个也可以做类似改进。
本文主要利用递归组件和provide、inject,实现了一个简单的树组件。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。