试试写个树组件吧

实现树组件

tree

拆分为整个树,结点内容,生成子节点

xml 复制代码
<template>
  <div class="h-tree">
    <tree-node>
        ...
    </tree-node>
  </div>
</template>

props.data作为参数传给组件

对数据进行加工,获取到其它的信息(如是否展开结点等)和解耦数据

css 复制代码
const list = [{       label: "Level one 1",    children: [      {               label: "Level two 1-1",        children: [          {            parentData:"Level two 1-1",            label: "Level three 1-1-1",          },        ],
      },
    ],
  },
  {
 
    label: "Level one 2",
    children: [
      {
        
        label: "Level two 2-1",
        children: [
          { parentData:"Level two 2-1",
            label: "Level three 2-1-1",
          },
        ],
      },
      {
        label: "Level two 2-2",
      },
    ],
  },
  {
    label: "Level one 3",
    children: [
      {
        label: "Level two 3-1",
        children: [
          {
            label: "Level three 3-1-1",
          },
        ],
      },
      {
        label: "Level two 3-2",
        children: [
          {
            
            label: "Level three 3-2-1",
          },
        ],
      },
    ],
  },
];
ini 复制代码
​
let i  = ref<number>(0);
  const initFn = (data: any, level: number = 0) => {
  copyData.value = data.map((item: any, index: number) => {
    item.uid = i.value ++;//计算唯一标识
    item.children = item[props.children] || []; //把儿子节点传输
    item.label = item[props.label];
    item.id = item[props.nodeKey];
    item.isOpen = false;
    item.isChecked = false;
    item.level = level;
    if (item.children && item.children.length) {
      initFn(item.children, level + 1);//计算层级,递归加一
    }
    return {
      uid:item.uid,//唯一标识
    //  id: item.id,
      label: item.label,//内容
      children: item.children,
      isOpen: item.isOpen, //展开
      isChecked: item.isChecked, //父节点选择框的的一个判断
      disabled: item.disabled,//默认禁止
      level: item.level,//层级
    };
  });
};

把我们的新数据copydata传入其中,然后来递归生成结点

ruby 复制代码
<template>
  <div class="h-tree">
    <tree-node
      v-for="(item, index) in copyData"
      :key="index"
      :uid="item.uid"
      :level="item.level"
      :items="item"
      :label="label"
      :children="children"
      :show-checkbox="showCheckbox" 
      :index="0"
      :node-key="nodeKey"//唯一标识
      :default-expanded-keys="defaultExpandedKeys"
      :default-checked-keys="checkedKeys"
      :default-expand-all="defaultExpandAll"
      :render-content="renderContent"
      :parent-data="copyData">
    </tree-node>
  </div>
</template>
​
  1. onMounted 钩子在组件挂载时执行,调用 initFn 方法进行初始化。
  2. initFn 方法用来初始化树形数据结构。该方法接收两个参数,一个是数据源,一个是当前节点所在的层级。该方法会遍历数据源,给每个节点加上一些属性,然后返回一个新的树形结构。
  3. checkboxChange 方法用来更新选中状态。该方法会调用 updateChecked 方法更新每个节点的选中状态。
  4. toggleChange 方法在节点展开或收起时触发。它会通过 emits 向外发射一个 toggle-change 事件。
  5. checkedChange 方法在选中状态改变时触发。它会调用 getCheckedNodes 方法获取当前选中的节点,然后通过 emits 向外发射一个 checked-change 事件。
  6. updateChecked 方法用来更新每个节点的选中状态。如果某个节点的所有子节点都被选中,那么该节点也会被选中。如果某个节点的某个子节点被选中,那么该节点的选中状态就是半选中状态。
  7. collapseOtherlevelNodes 方法用来在展开某个节点时,收起其他同级节点。该方法会遍历树形结构,找到与当前节点同级的节点,并将其收起。
  8. collapseOtherNodes 方法用来在展开某个节点时,收起其他节点。该方法会遍历树形结构,并将除当前节点以外的所有节点收起。
  9. setCheckedKeys 方法用来设置选中的节点。它接收一个键值数组作为参数,并将其赋值给 checkedKeys 变量。
  10. getCheckedKeys 方法用来获取选中节点的键值数组。它会调用 getCheckedNodes 方法获取

生成节点

ini 复制代码
<template>
 <div >
  <ul class="tree-node" >
  
    <div :class="['tree-node__content']"
     @click.stop="handleToggle(items)"
     :draggable="true"
     @dragstart="onDragStart"   
     @dragover="onDragOver"
     @drop="onDrop"
     @dragend="onDragEnd"
     >
      <span >
        <i class="icon iconfont icon-xiala"></i>
      </span>
      <el-checkbox
      v-if="showCheckbox"
      v-model="items!.isChecked" //复选框检查
        :indeterminate="items.indeterminate"
        :disabled="items.disabled"
      @change="handleCheckChange"
      
      ></el-checkbox>
      <nodeContent :data="items" :render-content="renderContent" :parent-data="parentData" />
    </div>
​
      <div class="tree-ul-box" v-if="isShow" v-show="items.isOpen" > 
     
        <tree-node
          v-for="(i, j) in items.children"
          :key="j"
          :items="i"
          :uid="uid"
          :label="label"
          :children="children"
          :show-checkbox="showCheckbox"
          :index="index && index + 1"
          :node-key="nodeKey"
          :render-content="renderContent"
          :parent-data="items.children">
        </tree-node>
      </div>
  </ul>
</div>

作为递归函数,我们需要传入接收刚刚父组件的参数,同样是用props接收

javascript 复制代码
const props = defineProps({
  items: {
    type: Object, 
    default: () => {},
  },
  label: String,
  children: String,
  showCheckbox: Boolean,
  index: Number,
  nodeKey: String,
  // 默认展开项
  defaultExpandedKeys: Array,
  // 默认选中项
  defaultCheckedKeys: Array,
  // 默认展开所有
  defaultExpandAll: Boolean,
  renderContent: Function,
  parentData: Array,
});
​
/*parentData 是在 HTree 组件中提供的一个 provide/inject 机制的值,用于获取整棵树的节点数据。在这里的子节点中,通过 :parent-data="items.children" 的方式将父节点的 children 属性向下传递,使子节点在使用 parentData 时可以获取到整个树的节点数据。这样可以方便在节点组件中实现一些功能,比如展开/收起其他节点时,需要获取到整棵树的节点数据来进行操作。*/
​

如果改结点存在子节点才能继续递归, 具体实现就是用v-if来判断即可。

ini 复制代码
const isShow = computed(() => {  return props.items.children && props.items.children.length; }); 

点击事件,判断展开

ini 复制代码
const handleToggle = (item: any) => {
  item.isOpen = !item.isOpen;
  // 展开/收起子节点时触发
};

手风琴模式

scss 复制代码
const handleToggle = (item: any) => {
  item.isOpen = !item.isOpen;
  // 展开/收起子节点时触发
  if(item.isOpen) //当前节点展开,就关闭同层级节点
  {
    collapseOtherlevelNodes(item);  
  }
  toggleChange(item);
};
​
ini 复制代码
const collapseOtherlevelNodes = (item: any)=>{
  collapseOtherlevelNodesFromParent (item,props.parentData?props.parentData:[]) 
}
​

生成节点向根节点接收collapse-other-level-nodes方法

typescript 复制代码
const collapseOtherlevelNodesFromParent = inject<(currentNode: any, parentData: any[]) => void>("collapse-other-level-nodes", () => {});
​
​

根节点

typescript 复制代码
const collapseOtherlevelNodes=(currentNode: any, parentData: any[])=>{  
  const collapselevelNodes = (nodes: any[]) => { 
    nodes.forEach((node: any) => {
      // console.log("node.level",node.level);
      // console.log("currennt",currentNode);
     if (node.level === currentNode.level && node.uid !== currentNode.uid) {  //同层级但是不同id的节点会关闭
        node.isOpen = false;
      }
      if (node.children && node.children.length) { //检查节点还有没有儿子节点
        collapselevelNodes(node.children); //有就递归
      }
 
    });
  };
  collapselevelNodes(parentData); //递归调用
​
}
​
​
​

复选框实现

ini 复制代码
 <div >
  <ul class="tree-node" >
  
    <div :class="['tree-node__content']"
     @click.stop="handleToggle(items)"
     :draggable="true"
     @dragstart="onDragStart" 
     @dragover="onDragOver"
     @drop="onDrop"
     @dragend="onDragEnd"
     >
      <span >
        <i class="icon iconfont icon-xiala"></i>
      </span>
      <el-checkbox
      v-if="showCheckbox"
      v-model="items!.isChecked"
        :indeterminate="items.indeterminate"
        :disabled="items.disabled"
      @change="handleCheckChange"
      
      ></el-checkbox>
      <nodeContent :data="items" :render-content="renderContent" :parent-data="parentData" />
    </div>
​
      <div class="tree-ul-box" v-if="isShow" v-show="items.isOpen" >
     
        <tree-node
          v-for="(i, j) in items.children"
          :key="j"
          :items="i"
          :uid="uid"
          :level="level"
          :label="label"
          :children="children"
          :show-checkbox="showCheckbox"
          :index="index && index + 1"
          :node-key="nodeKey"
          :render-content="renderContent"
          :parent-data="items.children">
        </tree-node>
      </div>
  </ul>
</div>

checkbox点击事件

1.该节点的状态变化,其子节点需要全部改变

ini 复制代码
// 选中一个节点时,递归地遍历下面所属的所有子节点
const updateChildChecked = (item: any, val: any) => {
  item.isChecked = val;
  if (item.children && item.children.length) {
    item.children.forEach((el: any) => {
      updateChildChecked(el, val);
    });
  }
};

2.同时父级的状态需要根据子节点是否全选,等发生变化

ini 复制代码
​
// 子有一个选中,父为半选
// 子全选中,父为全选
// 子一个都没选中,父不选
const updateChecked = (data: any) => {
  data.forEach((item: any) => {
    let checked;//选中
    let indeterminate;//半选
    let checkedNodes;
    if (item.children && item.children.length) {
      updateChecked(item.children);
      const children = item.children;
      // 过滤出选中的
      checkedNodes = children.filter((child: any) => child.isChecked);
      if (checkedNodes.length === 0) {
        checked = false; 
        indeterminate = false; 
      } else if (checkedNodes.length === children.length) {
        checked = true;
        indeterminate = false;
      } else {
        checked = false;
        indeterminate = true;
      }
      item.isChecked = checked;
      item.indeterminate = indeterminate;
    }
  });
};

拖拽实现(生成节点)

typescript 复制代码
const onDragStart = (event:any) => {
  event.dataTransfer.setData('item', JSON.stringify(props.items)); //当前拖拽节点的信息设置传递用item的名称
  const droppedItem = JSON.parse(event.dataTransfer.getData('item'));//获取信息
  console.log("当前拖拽节点的parentData",props.parentData);
​
  const updatedData = removeNode(droppedItem, props.parentData);
  const proxy = new Proxy (updatedData,{});//因为props.item.parentData是proxy类型,所以赋值要转换类型
  event.target.classList.add("dragging");//设置css
  if (updatedData) {
    nextTick(() => {
    props.items.parentData = proxy;//更新了就赋值
  });
  }
};
​
ini 复制代码
const onDragOver = (event:any) => {
  event.preventDefault();
};
​
const onDrop = (event:any) => {
  event.preventDefault();
  const droppedItem = JSON.parse(event.dataTransfer.getData('item'));
​
 
 
  if (!props.items.children) {
    props.items.children = [];
  }
 
  console.log(props.items.children);
  props.items.children = [...props.items.children, droppedItem];//添加
  props.items.isOpen = true; // 展开目标节点
​
collapseOtherNodes(props.items); // 收缩其他节点
​
​
};
​
const onDragEnd = (event:any) => {
  event.dataTransfer.clearData('item');
  event.target.classList.remove("dragging");
};
​

未解决

当我再一次更新视图时结果正确

结点内容

结点内容其实非常简单,我们只需要接收结点的数据,用虚拟结点的方式返回即可。

xml 复制代码
<script lang="ts">
import { h, toRefs, reactive } from "vue";
export default {
  props: {
    data: {
      type: Object,
      required: true,
    },
    renderContent: Function,
    parentData: Array,
  },
  setup(props) {
    const { data, renderContent, parentData } = toRefs(props);
    const nodeData = reactive({
      data: data.value,
      parentData: parentData.value,
    });
    return () => [renderContent.value ? renderContent.value(h, nodeData) : h("span", data.value.label)];
  },
};
</script>
​

结果:

参考

Vue实现一个Tree组件 - 掘金 (juejin.cn)

HTML5拖拽API实现vue树形拖拽组件 - 掘金 (juejin.cn)

ChaiMayor/hview-ui: Hview UI - A Vue.js 3 UI library 👻 (github.com)

相关推荐
正小安1 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch3 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光3 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   3 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   3 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web3 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常3 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇4 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr4 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho5 小时前
【TypeScript】知识点梳理(三)
前端·typescript