介绍
codding 仓库
Vue3 实现Vue组件库
一、组件初始化操作
1、创建树组件
src/packages/tree/tree.jsx
import { getCurrentInstance, provide, reactive,watch } from 'vue';import TreeNode from './tree-node';import { flattenTree } from './util'export default { name: 'ZfTree', // zf-tree components: { [TreeNode.name]: TreeNode }, props: { data: { type: Array, default: () => [] }, load: { type: Function }, draggable: { type: Boolean, default: false } }, setup(props, context) { let data = props.data; // 获取的是当前用户传递来的数据 let flatMap = flattenTree(data); const instance = getCurrentInstance(); watch(data,()=>{ flatMap = flattenTree(data); }) const state = reactive({ dropPosition: '',// 拖拽的位置 0 表示放到里面去 作为儿子 1 作为哥哥 -1 弟弟 dragNode: null, // 拖动的这个元素的数据 draggingNode: null, // 拖拽的实例 showIndicator: false }) function renderNode(data) { if (data && data.length == 0) { return <div>暂无数据</div> } return data.map(item => <zf-tree-node data={item}></zf-tree-node>) } const methods = { getCheckNodes() { // 获取选中节点 return Object.values(flatMap).filter(item => item.node.checked) }, updateTreeDown(node, checked) { if (node.children) { // 有孩子在循环 node.children.forEach(child => { child.checked = checked; methods.updateTreeDown(child, checked); }) } }, updateTreeUp(node, checked) { let parent = flatMap[node.key].parent; // 获取当前这个节点的父亲 if (!parent) return; // 获取父节点 // {0:xxx,1:xxx,2:xxx} if (checked) { parent.checked = parent.children.every(node => node.checked) } else { // 自己没有选中父亲就没有选中 parent.checked = false; } methods.updateTreeUp(parent, checked) }, dragStart(e, nodeInstance, data) { state.draggingNode = nodeInstance; state.dragNode = data; }, dragOver(e, nodeInstance, data) { if (state.dragNode.key == data.key) { return; // 不能在自己身上操作 } let overElm = nodeInstance.ctx.$el; // 经过的人 if (state.draggingNode.ctx.$el.contains(overElm)) {// 当前拖动的人 return } // 获取当前节点中label的位置 let targetPosition = overElm.firstElementChild.getBoundingClientRect(); let treePosition = instance.ctx.$el.getBoundingClientRect(); let distance = e.clientY - targetPosition.top; if (distance < targetPosition.height * 0.2) { // 当前的距离小于整个label的20% 偏上 state.dropPosition = 1; } else if (distance > targetPosition.height * 0.8) { state.dropPosition = -1; } else { state.dropPosition = 0; } let iconPosition = overElm.querySelector('.zf-icon').getBoundingClientRect(); let indicatorTop = -9999; if (state.dropPosition == 1) { indicatorTop = iconPosition.top - treePosition.top; // 当前这个线距离树的顶部位置 } else if (state.dropPosition == -1) { indicatorTop = iconPosition.bottom - treePosition.top; } const indicator = instance.ctx.$refs.indicator; indicator.style.top = indicatorTop + 'px'; indicator.style.left = iconPosition.right - treePosition.left + 'px'; state.showIndicator = (state.dropPosition == 1) || (state.dropPosition == -1) }, dragEnd(e, nodeInstance, data) { state.dropPosition = '';// 拖拽的位置 0 表示放到里面去 作为儿子 1 作为哥哥 -1 弟弟 state.dragNode = null;// 拖动的这个元素的数据 state.draggingNode = null; // 拖拽的实例 state.showIndicato = false; } } provide('TREE_PROVIDER', { treeMethods: methods, slot: context.slots.default, load: props.load, draggable: props.draggable }) instance.ctx.getCheckNodes = methods.getCheckNodes; return () => { // render函数 return <div class="zf-tree"> {renderNode(data)} <div class="zf-tree-indicator" ref="indicator" vShow={state.showIndicator}></div> </div> } }}
2、注册树组件
src/packages/tree/index.js
import Tree from './tree.jsx';import '../../style/tree.scss'Tree.install = (app) => { // 注册全局组件 app.component(Tree.name,Tree)}export default Tree
src/examples/tree.vue
<template> <rk-tree :data="treeDate" ref="tree" v-slot="{name,id}" :load="loadFn"> {{name}} - {{id}} </rk-tree> <rk-button @click="getCheckNodes">点击获取选中的内容</rk-button></template><script> import { reactive,toRefs, ref } from "vue"; export default{ setup(){ const tree = ref(null); const state = reactive({ treedata: [{ id:1, name:'菜单1', children:[...] }] }) function getCheckNodes(){ console.log(tree.value.getCheckNodes()); // 获取当前用户传递来的数据 } // onMounted(){..}, function loadFn(data,cb){ if(data.id===1){ setTimeout(()=>{ cb([id:'1-1',name:'菜单1-1']) },1000) }else if(data.id=='1-1'){ setTimeout(()=>{ cb([id:'1-1-1',name:'菜单1-1-1']) },1000) } }, return { ...toRefs(state), tree, getCheckNodes, loadFn } } }</script>
packages/tree/tree-node.jsx
import { computed, withModifiers, inject, ref, getCurrentInstance } from 'vue'export default { name: 'ZfTreeNode', props: { data: { type: Object } }, setup(props, context) { let data = props.data; let { treeMethods, slot, load, draggable } = inject('TREE_PROVIDER') const isLoaded = ref(false); // 是否显示箭头 const showArrow = computed(() => { return (data.children && data.children.length > 0) || (load && !isLoaded.value) }); // 计算节点的样式 const classes = computed(() => [ 'zf-tree-node', !showArrow.value && 'zf-tree-no-expand', draggable && 'zf-tree-draggable' ]); // ---------------------- 方法 const methods = { handleExpand() { if (data.children && data.children.length == 0) { // 点击展开时 先看下有没有孩子 if (load) { // 没孩子有loading data.loading = true; load(data, (children) => { data.children = children; data.loading = false; isLoaded.value = true; }) } } else { isLoaded.value = true; } data.expand = !data.expand }, handleCheck(e) { data.checked = !data.checked; treeMethods.updateTreeDown(data, data.checked); treeMethods.updateTreeUp(data, data.checked); } } const instance = getCurrentInstance(); const dragEvent = { ...(draggable?{ onDragstart(e){ e.stopPropagation(); // 组件的实例中 $el treeMethods.dragStart(e,instance,data) }, onDragover(e){ e.stopPropagation(); treeMethods.dragOver(e,instance,data) }, onDragend(e){ e.stopPropagation(); treeMethods.dragEnd(e,instance,data) } }:{}) } return () => ( <div class={classes.value} {...dragEvent}> <div class="zf-tree-label" onClick={methods.handleExpand}> <zf-icon icon="right"></zf-icon> {data.loading ? <zf-icon icon="loading"></zf-icon> : null} <input type="checkbox" checked={data.checked} onClick={withModifiers(methods.handleCheck, ['stop'])} /> {slot ? slot(data) : <span>{data.name}</span>} </div> <div class="zf-tree-list" vShow={data.expand}> {data.children && data.children.map(child => <zf-tree-node data={child}></zf-tree-node>)} </div> </div> ) }}
src/packages/tree/util.js
export const flattenTree = (data) => { let key = 0; function flat(data, parent) { // 数组拍平 return data.reduce((obj, currentNode) => { // [{},{}] currentNode.key = key; // 给每个节点添加一个标识 obj[key] = { parent, node: currentNode } key++; if (currentNode.children) { obj = { ...obj, ...flat(currentNode.children, currentNode) } } return obj }, {}) } return flat(data)}// {0: node , 1: node , 2:node}




二、通过数据渲染树组件
1、组件的递归渲染
2、组件的分割
export default {}
3、美化树组件央视
三、组件展开收缩功能
1、显示展开图标
2、增加树的折叠功能
四、增加选择功能
五、设置级联选中状态
六、异步加载
七、定制化节点插槽实现
八、拖拽