介绍

codding 仓库

Vue3 实现Vue组件库

一、组件初始化操作

1、创建树组件

src/packages/tree/tree.jsx

  1. import { getCurrentInstance, provide, reactive,watch } from 'vue';
  2. import TreeNode from './tree-node';
  3. import { flattenTree } from './util'
  4. export default {
  5. name: 'ZfTree', // zf-tree
  6. components: {
  7. [TreeNode.name]: TreeNode
  8. },
  9. props: {
  10. data: {
  11. type: Array,
  12. default: () => []
  13. },
  14. load: {
  15. type: Function
  16. },
  17. draggable: {
  18. type: Boolean,
  19. default: false
  20. }
  21. },
  22. setup(props, context) {
  23. let data = props.data; // 获取的是当前用户传递来的数据
  24. let flatMap = flattenTree(data);
  25. const instance = getCurrentInstance();
  26. watch(data,()=>{
  27. flatMap = flattenTree(data);
  28. })
  29. const state = reactive({
  30. dropPosition: '',// 拖拽的位置 0 表示放到里面去 作为儿子 1 作为哥哥 -1 弟弟
  31. dragNode: null, // 拖动的这个元素的数据
  32. draggingNode: null, // 拖拽的实例
  33. showIndicator: false
  34. })
  35. function renderNode(data) {
  36. if (data && data.length == 0) {
  37. return <div>暂无数据</div>
  38. }
  39. return data.map(item => <zf-tree-node data={item}></zf-tree-node>)
  40. }
  41. const methods = {
  42. getCheckNodes() {
  43. // 获取选中节点
  44. return Object.values(flatMap).filter(item => item.node.checked)
  45. },
  46. updateTreeDown(node, checked) {
  47. if (node.children) { // 有孩子在循环
  48. node.children.forEach(child => {
  49. child.checked = checked;
  50. methods.updateTreeDown(child, checked);
  51. })
  52. }
  53. },
  54. updateTreeUp(node, checked) {
  55. let parent = flatMap[node.key].parent; // 获取当前这个节点的父亲
  56. if (!parent) return;
  57. // 获取父节点 // {0:xxx,1:xxx,2:xxx}
  58. if (checked) {
  59. parent.checked = parent.children.every(node => node.checked)
  60. } else { // 自己没有选中父亲就没有选中
  61. parent.checked = false;
  62. }
  63. methods.updateTreeUp(parent, checked)
  64. },
  65. dragStart(e, nodeInstance, data) {
  66. state.draggingNode = nodeInstance;
  67. state.dragNode = data;
  68. },
  69. dragOver(e, nodeInstance, data) {
  70. if (state.dragNode.key == data.key) {
  71. return; // 不能在自己身上操作
  72. }
  73. let overElm = nodeInstance.ctx.$el; // 经过的人
  74. if (state.draggingNode.ctx.$el.contains(overElm)) {// 当前拖动的人
  75. return
  76. }
  77. // 获取当前节点中label的位置
  78. let targetPosition = overElm.firstElementChild.getBoundingClientRect();
  79. let treePosition = instance.ctx.$el.getBoundingClientRect();
  80. let distance = e.clientY - targetPosition.top;
  81. if (distance < targetPosition.height * 0.2) { // 当前的距离小于整个label的20% 偏上
  82. state.dropPosition = 1;
  83. } else if (distance > targetPosition.height * 0.8) {
  84. state.dropPosition = -1;
  85. } else {
  86. state.dropPosition = 0;
  87. }
  88. let iconPosition = overElm.querySelector('.zf-icon').getBoundingClientRect();
  89. let indicatorTop = -9999;
  90. if (state.dropPosition == 1) {
  91. indicatorTop = iconPosition.top - treePosition.top; // 当前这个线距离树的顶部位置
  92. } else if (state.dropPosition == -1) {
  93. indicatorTop = iconPosition.bottom - treePosition.top;
  94. }
  95. const indicator = instance.ctx.$refs.indicator;
  96. indicator.style.top = indicatorTop + 'px';
  97. indicator.style.left = iconPosition.right - treePosition.left + 'px';
  98. state.showIndicator = (state.dropPosition == 1) || (state.dropPosition == -1)
  99. },
  100. dragEnd(e, nodeInstance, data) {
  101. state.dropPosition = '';// 拖拽的位置 0 表示放到里面去 作为儿子 1 作为哥哥 -1 弟弟
  102. state.dragNode = null;// 拖动的这个元素的数据
  103. state.draggingNode = null; // 拖拽的实例
  104. state.showIndicato = false;
  105. }
  106. }
  107. provide('TREE_PROVIDER', {
  108. treeMethods: methods,
  109. slot: context.slots.default,
  110. load: props.load,
  111. draggable: props.draggable
  112. })
  113. instance.ctx.getCheckNodes = methods.getCheckNodes;
  114. return () => { // render函数
  115. return <div class="zf-tree">
  116. {renderNode(data)}
  117. <div class="zf-tree-indicator" ref="indicator" vShow={state.showIndicator}></div>
  118. </div>
  119. }
  120. }
  121. }

2、注册树组件

src/packages/tree/index.js

  1. import Tree from './tree.jsx';
  2. import '../../style/tree.scss'
  3. Tree.install = (app) => { // 注册全局组件
  4. app.component(Tree.name,Tree)
  5. }
  6. export default Tree

src/examples/tree.vue

  1. <template>
  2. <rk-tree :data="treeDate" ref="tree" v-slot="{name,id}" :load="loadFn">
  3. {{name}} - {{id}}
  4. </rk-tree>
  5. <rk-button @click="getCheckNodes">点击获取选中的内容</rk-button>
  6. </template>
  7. <script>
  8. import { reactive,toRefs, ref } from "vue";
  9. export default{
  10. setup(){
  11. const tree = ref(null);
  12. const state = reactive({
  13. treedata: [{
  14. id:1,
  15. name:'菜单1',
  16. children:[...]
  17. }]
  18. })
  19. function getCheckNodes(){
  20. console.log(tree.value.getCheckNodes()); // 获取当前用户传递来的数据
  21. }
  22. // onMounted(){..},
  23. function loadFn(data,cb){
  24. if(data.id===1){
  25. setTimeout(()=>{
  26. cb([id:'1-1',name:'菜单1-1'])
  27. },1000)
  28. }else if(data.id=='1-1'){
  29. setTimeout(()=>{
  30. cb([id:'1-1-1',name:'菜单1-1-1'])
  31. },1000)
  32. }
  33. },
  34. return {
  35. ...toRefs(state),
  36. tree,
  37. getCheckNodes,
  38. loadFn
  39. }
  40. }
  41. }
  42. </script>

packages/tree/tree-node.jsx

  1. import { computed, withModifiers, inject, ref, getCurrentInstance } from 'vue'
  2. export default {
  3. name: 'ZfTreeNode',
  4. props: {
  5. data: {
  6. type: Object
  7. }
  8. },
  9. setup(props, context) {
  10. let data = props.data;
  11. let { treeMethods, slot, load, draggable } = inject('TREE_PROVIDER')
  12. const isLoaded = ref(false);
  13. // 是否显示箭头
  14. const showArrow = computed(() => {
  15. return (data.children && data.children.length > 0) || (load && !isLoaded.value)
  16. });
  17. // 计算节点的样式
  18. const classes = computed(() => [
  19. 'zf-tree-node',
  20. !showArrow.value && 'zf-tree-no-expand',
  21. draggable && 'zf-tree-draggable'
  22. ]);
  23. // ---------------------- 方法
  24. const methods = {
  25. handleExpand() {
  26. if (data.children && data.children.length == 0) { // 点击展开时 先看下有没有孩子
  27. if (load) { // 没孩子有loading
  28. data.loading = true;
  29. load(data, (children) => {
  30. data.children = children;
  31. data.loading = false;
  32. isLoaded.value = true;
  33. })
  34. }
  35. } else {
  36. isLoaded.value = true;
  37. }
  38. data.expand = !data.expand
  39. },
  40. handleCheck(e) {
  41. data.checked = !data.checked;
  42. treeMethods.updateTreeDown(data, data.checked);
  43. treeMethods.updateTreeUp(data, data.checked);
  44. }
  45. }
  46. const instance = getCurrentInstance();
  47. const dragEvent = {
  48. ...(draggable?{
  49. onDragstart(e){
  50. e.stopPropagation(); // 组件的实例中 $el
  51. treeMethods.dragStart(e,instance,data)
  52. },
  53. onDragover(e){
  54. e.stopPropagation();
  55. treeMethods.dragOver(e,instance,data)
  56. },
  57. onDragend(e){
  58. e.stopPropagation();
  59. treeMethods.dragEnd(e,instance,data)
  60. }
  61. }:{})
  62. }
  63. return () => (
  64. <div class={classes.value} {...dragEvent}>
  65. <div class="zf-tree-label" onClick={methods.handleExpand}>
  66. <zf-icon icon="right"></zf-icon>
  67. {data.loading ? <zf-icon icon="loading"></zf-icon> : null}
  68. <input type="checkbox" checked={data.checked} onClick={withModifiers(methods.handleCheck, ['stop'])} />
  69. {slot ? slot(data) : <span>{data.name}</span>}
  70. </div>
  71. <div class="zf-tree-list" vShow={data.expand}>
  72. {data.children && data.children.map(child => <zf-tree-node data={child}></zf-tree-node>)}
  73. </div>
  74. </div>
  75. )
  76. }
  77. }

src/packages/tree/util.js

  1. export const flattenTree = (data) => {
  2. let key = 0;
  3. function flat(data, parent) { // 数组拍平
  4. return data.reduce((obj, currentNode) => { // [{},{}]
  5. currentNode.key = key; // 给每个节点添加一个标识
  6. obj[key] = {
  7. parent,
  8. node: currentNode
  9. }
  10. key++;
  11. if (currentNode.children) {
  12. obj = { ...obj, ...flat(currentNode.children, currentNode) }
  13. }
  14. return obj
  15. }, {})
  16. }
  17. return flat(data)
  18. }
  19. // {0: node , 1: node , 2:node}

image.png
image.png
image.png

image.png

二、通过数据渲染树组件

1、组件的递归渲染

2、组件的分割

  1. export default {
  2. }

3、美化树组件央视

三、组件展开收缩功能

1、显示展开图标

2、增加树的折叠功能

四、增加选择功能

五、设置级联选中状态

六、异步加载

七、定制化节点插槽实现

八、拖拽