目前效果
使用组件
<z-tree
:data="data"
key-field="keyName"
label-field="labelName"
children-field="childrenName"
show-checkbox
:default-checked-keys="['1', '2']"
></z-tree>
<script lang="ts" setup>
// 先生成数据
function createData(level = 1, parentLabel = ''): TreeOption[] {
if (level > 4) return [];
const arr = Array<number>(10).fill(0);
return arr.map((item, index) => {
// 当前的label 等于 父级的 label + index
const label = parentLabel
? `${parentLabel}-${index + 1}`
: String(index + 1);
return {
keyName: label,
labelName: label,
childrenName: createData(level + 1, label),
};
});
}
const data = ref(createData());
</script>
功能展示
1、展开图标添加旋转功能
// // 切换展开和收起状态
function expand(node: TreeNode) {
expandedKeysSet.value.add(node.key);
// 展开的时候调用异步加载的逻辑
triggerLoading(node);
}
我们来看一下现在的效果
2、实现树的异步加载
因为是异步加载的,用一个变量来维护正在加载的节点的keys。
// 4. 实现调用用户传递的异步加载函数
// 用来装正在加载节点的 key
const loadingKeysRef = ref(new Set<Key>());
// 如果传递了 onload 异步加载函数
function triggerLoading(node: TreeNode) {
// 判断当前点击的 node 节点是否可以调用异步加载函数
if (!node.children.length && !node.isLeaf) {
const loadingKeys = loadingKeysRef.value;
// 没有加载的时候,再调用加载函数
if (!loadingKeys.has(node.key)) {
loadingKeys.add(node.key);
const onLoad = props.onLoad;
if (onLoad) {
onLoad(node.rawNode).then((children) => {
// 把用户传递进来的孩子保存在 原始数据的位置
node.rawNode.children = children;
// 重新构造树新的节点
node.children = createTree(children, node);
loadingKeys.delete(node.key);
});
}
}
}
}
3、实现树节点的选择
实现tree节点的单选和多选功能
4、实现节点的禁用效果
5、实现自定义节点
关键代码
tree.vue
// 把tree组件的slot提供出去,方便后代组件使用
provide(treeInjectKey, {
slots: useSlots(),
});
tree-node-content.tsx
import { defineComponent, inject, PropType } from 'vue';
import { TreeNode, treeInjectKey } from './tree';
export default defineComponent({
name: 'z-tree-node-content',
props: {
node: {
type: Object as PropType<TreeNode>,
required: true,
},
},
setup(props) {
// 拿到tree组件的 slot 内容将数据回传给 外部的 v-slot
const treeContext = inject(treeInjectKey);
return () => {
return treeContext?.slots.default
? treeContext.slots.default({ node: props.node })
: props.node.label;
};
},
});
6、实现树的虚拟滚动
实现虚拟滚动首先就需要把tree型结构扁平化,通过监听滚动条滚动的位置,计算出当前要扁平化之后数组的区间,来动态更新树结构。
// 采用深度优先遍历
const flattenTree = computed(() => {
const expandedKeys = expandedKeysSet.value;
const result: TreeNode[] = [];
const stack: TreeNode[] = [];
for (let i = tree.value.length - 1; i >= 0; i--) {
stack.push(tree.value[i]);
}
while (stack.length) {
const topNode: TreeNode | undefined = stack.pop();
if (!topNode) continue;
result.push(topNode);
// 判断当前的 key在不在 选中的里面
if (expandedKeys.has(topNode.key)) {
// 如果在里面就把儿子也加入到 栈 里面
// 从后面往前面 压入栈中,取出来的时候就是反的。
for (let i = topNode.children.length - 1; i >= 0; i--) {
stack.push(topNode.children[i]);
}
}
}
return result;
});
上面实现了初步的虚拟滚动效果,根据滚动的scrollTop动态计算出当前要显示的数据,从而实现了虚拟滚动,下面,在此基础上进行优化,让上边和下边多显示几条,这样就不会出现,如果快速滚动下面有白屏的现象。
如下图所示:
7、实现checkbox组件
8、实现tree组件的级联选择
1. 实现自顶向下选中
// 自顶向下更新
function toggle(node: TreeNode, checked: boolean) {
if (!node) return;
// 如果是选中就添加,否则删除
const checkedKeys = checkedKeysRef.value;
if (checked) {
checkedKeys.add(node.key);
indeterminateRef.value.delete(node.key);
} else {
checkedKeys.delete(node.key);
}
// 检测是否有儿子,如果有则递归更新
const children = node.children;
if (children && children.length) {
children.forEach((childNode) => {
toggle(childNode, checked);
});
}
}
2. 实现自底向上选中
// 自下向上更新 目的是为了控制父节点的 选中 和 半选 (重要)
function updateCheckKeys(node: TreeNode) {
// 判断有没有父节点
if (node.parentKey) {
const parentNode = fineNode(node.parentKey);
if (parentNode) {
let allChecked = true;
let hasChecked = false;
const nodes = parentNode.children;
for (const node of nodes) {
// 如果选中的容器里面有当前节点,则说明子节点有选中的
if (checkedKeysRef.value.has(node.key)) {
hasChecked = true;
}
// 如果 半选的容器中有当前节点,则说明当前子节点半选, 其父节点就不能是选中的
else if (indeterminateRef.value.has(node.key)) {
allChecked = false;
hasChecked = true;
} else {
// 半选和选中的容器里面都没有,则父节点什么也没选中
allChecked = false;
}
}
// 如果子节点全部选中了,则就把父节点也添加到选中的容器中去,并从半选的容器中移除掉
if (allChecked) {
checkedKeysRef.value.add(parentNode.key);
indeterminateRef.value.delete(parentNode.key);
} else if (hasChecked) {
// 如果子节点有选中的,则说明父节点需要半选,把父节点加入到半选容器中,并从选中容器中删除
checkedKeysRef.value.delete(parentNode.key);
indeterminateRef.value.add(parentNode.key);
}
updateCheckKeys(parentNode);
}
}
}
最终代码
本来是想着把使用pnpm搭建monorepo的过程,以及实现bem规范的的过程都记录下来形成文字版的呢,后面有时间的话在写把。先放到git仓库里面了。如果感兴趣可以查看仓库。
js??运算符
developer.mozilla.org/zh-CN/docs/…