试试写个树组件吧

实现树组件

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)

相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
浪浪山小白兔7 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
limit for me8 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者8 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架
qq_392794488 小时前
前端缓存策略:强缓存与协商缓存深度剖析
前端·缓存