vue3: vuedraggable 的使用方法(正常数据的基本使用与树结构数据递归使用)
目录
效果展示
第一章 准备
第二章 源代码
2.1 基本使用
2.2 树结构数据递归使用
效果展示
- 正常数据拖拽
- 树结构数据拖拽 ——说明:自定义拖拽树,添加数据展开收起,拖拽时非同层级不可拖拽,使用灰色区分,跨层拖拽不生效,并使用递归组件实现。
第一章 准备
官方文档:vuedraggable - npm
安装:
yarn add vuedraggable@4.1.0
yarn add vuedraggable-es@4.1.0
第二章 源代码
(为了方便大家,小编直接上源码,里面附带讲解,大家复制即可使用)
2.1 基本使用
- 封装组件:
js——
<script setup name="NormalDraggable">
import draggable from "vuedraggable";
import { reactive } from "vue";const props = defineProps({originData: {type: Array,default: () => {return [];},},
});const emit = defineEmits(["handleChange"]);const state = reactive({list: props.originData.map((item) => {return {...item,};}),
});//拖拽开始的事件
const onStart = () => {console.log("开始拖拽");
};//拖拽结束的事件
const onEnd = () => {// 拖拽结束后重新处理数据,参数:原数据,变化的数据emit("handleChange", state.list);
};
</script>
html——
<template><draggable:list="state.list"itemKey="id"animation="0"@start="onStart"@end="onEnd"><template #item="{ element }"><div><div class="draggable_item"><div class="draggable_item-left"><span>{{ element.fieldName }}</span></div><div class="draggable_item-right"><div class="example"><span>{{ element.exampleValue }}</span></div></div></div></div></template></draggable>
</template>
css——
<style lang="less" scoped>
.draggable_item {width: 100%;padding: 0px 12px;min-height: 40px;background: #ffffff;display: flex;justify-content: space-between;align-items: center;border: 1px solid #e0e6eb;box-shadow: 0px 2px 0px 0px rgba(0, 0, 0, 0.02);border-radius: 2px;margin-bottom: 4px;.draggable_item-left {display: flex;align-items: center;max-width: 260px;.icon {margin-right: 10px;width: 14px;}}.draggable_item-right {display: flex;align-items: center;.example {color: rgba(0, 0, 0, 0.25);margin-right: 10px;max-width: 184px;}}
}
</style>
- 使用:
<script setup name="NormalDraggable">
import NormalDraggable from "./NormalDraggable/index.vue";const originDataList = ref([{exampleValue: "1",fieldName: "1层1号",id: "41cf48fb5c7546758b8ea679c7fc40b3",parentId: "",},{exampleValue: "123",fieldName: "1层2号",id: "57e6b0b1f0f14dff86deeb9ca2937772",parentId: "",},{exampleValue: '{a:"123"}',fieldName: "1层3号",id: "57e6b0b1f0f14dff86deeb9ca2937773",parentId: "",},{exampleValue: "1",fieldName: "1层4号",id: "b83e93dd599d40e6b58b24c0f3cf1cc8",parentId: "",},{exampleValue: "1",fieldName: "1层5号",id: "b83e93dd599d40e6b58b24c0f3cf1cd8",parentId: "",},{exampleValue: "1",fieldName: "1层6号",id: "b83e93dd599d40e6b58b24c0f3cf1cf8",parentId: "",},
]);const handleChangeList = (data) => {console.log("data", data);
};
</script><NormalDraggable:originData="originDataList"@handleChange="handleChangeList"
/>
2.2 树结构数据递归使用
- 封装拖拽组件:
js——
<script>
// 由于是递归组件,小编的demo中配置不是特别完全,所以需要将该组件名字向外暴露,但是需要区分,setup语法糖
export default {name: "TreeDraggable",
};
</script>
<script setup name="TreeDraggable">
import {CaretRightOutlined,CaretDownOutlined,DragOutlined,
} from "@ant-design/icons-vue"; // 小编使用的时antd小图标需要额外引入import draggable from "vuedraggable"; // 引入拖拽组件const props = defineProps({// 整个数据对象originData: {type: Array,default: () => {return [];},},// 数据对象中的子节点childrenData: {type: Array,default: () => {return [];},},
});const emit = defineEmits(["handleChange","startHandleChange","initHandleChange",
]);//拖拽开始的事件
const onStart = (event) => {const { element } = event.item.__draggable_context;// 拖拽开始时处理数据(向父组件发送消息)emit("startHandleChange", element.level);
};// 递归组件中介函数
const startHandleChange = (level) => {emit("startHandleChange", level);
};//拖拽结束的事件
const onEnd = () => {// 拖拽结束后重新处理数据,参数:原数据,变化的数据(向父组件发送消息)emit("handleChange", props.originData, props.childrenData);
};// 递归组件中介函数
const handleChange = (data1, data2) => {emit("handleChange", data1, data2);
};// 侧边小图标点击事件
const showTreeInfo = (ele) => {if (ele.children) {ele.open = !ele.open;}
};
</script>
html——
<template><draggable:list="childrenData" // 传入的拖拽数据itemKey="id" // 需要传入id防止会跨层拖拽animation="0"@start="onStart" // 拖拽开始事件@end="onEnd" // 拖拽结束事件>// 下面是拖拽组件的基本使用结构<template #item="{ element }"> // 单条拖拽元素数据,即draggable组件的插槽<div> //自定义每一条拖拽数据的展示结构<divclass="draggable_item":style="{backgroundColor: element.isdraggable // 通过isdraggable区分是否能拖拽的颜色? '#ffffff': 'rgba(0, 0, 0, 0.04)',color: element.isdraggable? 'rgba(0, 0, 0, 0.88)': 'rgba(0, 0, 0, 0.25)',}"><divclass="draggable_item-left":style="{ marginLeft: `${(element.level - 1) * 16}px` }" // 构造一下树结构><div class="icon" @click="showTreeInfo(element)"> // 通过点击小图标控制数据的展开与隐藏<div v-if="element.children.length"> // 如果没子级则不展示小图标<CaretRightOutlined v-if="!element.open" /> // open控制小图标<CaretDownOutlined v-else /> </div></div><div class="name"><span>{{ element.fieldName }}</span></div></div><divclass="draggable_item-right":style="{color: element.isdraggable? 'rgba(0, 0, 0, 0.45)': 'rgba(0, 0, 0, 0.25)',}"><div class="example"><span>{{ element.exampleValue }}</span></div><div><DragOutlined /></div></div></div><TreeDraggable // 递归组件传入对应的方法以及参数:originData="originData":childrenData="element.children"@handleChange="handleChange"@startHandleChange="startHandleChange"v-if="element.children && element.open"/></div></template></draggable>
</template>
css(与2.1基本使用的样式一致)
- 使用
<script name="App" setup>
import TreeDraggable from "./TreeDraggable/index.vue"; // 引入主键
import { onMounted, ref } from "vue";
let originData = ref([ // 数据格式{children: [],exampleValue: "1",fieldName: "1层1号",id: "41cf48fb5c7546758b8ea679c7fc40b3",parentId: "",},{children: [{children: [{children: [],exampleValue: "示例值",fieldName: "2号2层1号",id: "7647910dc6d94a51ac5311392ba290d3",parentId: "7647910dc6d94a51ac5311382ba990d3",},{children: [],exampleValue: "示例值",fieldName: "2号2层2号",id: "7647910dc6d94a51ac5311382ba290d4",parentId: "7647910dc6d94a51ac5311382ba990d3",},],exampleValue: "示例值",fieldName: "2号1层1",id: "7647910dc6d94a51ac5311382ba990d3",parentId: "57e6b0b1f0f14dff86deeb9ca2937772",},],exampleValue: "123",fieldName: "1层2号",id: "57e6b0b1f0f14dff86deeb9ca2937772",parentId: "",},{children: [{children: [{children: [],exampleValue: "示例值",fieldName: "3号2层1号",id: "7647910dc6d94a51ac5311382ba294d3",parentId: "7647910dc6d94a51ac5311382ba291d3",},{children: [],exampleValue: "示例值",fieldName: "3号2层2号",id: "7647910dc6d94a51ac5311382ba290d4",parentId: "7647910dc6d94a51ac5311382ba291d3",},],exampleValue: "示例值",fieldName: "3号1层1",id: "7647910dc6d94a51ac5311382ba291d3",parentId: "57e6b0b1f0f14dff86deeb9ca2937773",},{children: [],exampleValue: "示例值",fieldName: "3号1层2",id: "7647910dc6d94a51ac5311382ba290d3",parentId: "57e6b0b1f0f14dff86deeb9ca2937773",},],exampleValue: '{a:"123"}',fieldName: "1层3号",id: "57e6b0b1f0f14dff86deeb9ca2937773",parentId: "",},{children: [],exampleValue: "1",fieldName: "1层4号",id: "b83e93dd599d40e6b58b24c0f3cf1cc8",parentId: "",},{children: [],exampleValue: "1",fieldName: "1层5号",id: "b83e93dd599d40e6b58b24c0f3cf1cd8",parentId: "",},{children: [],exampleValue: "1",fieldName: "1层6号",id: "b83e93dd599d40e6b58b24c0f3cf1cf8",parentId: "",},
]);//下面的方法基本上都是使用递归实现的
// 初始化数据,添加level(层级)、serial(序号)、isdraggable(是否可拖拽)、open(控制数据是否展开):要进行如下初始化的原因是正常开发时后端是不会这么给我们前端返回的,我们需要处理数据
const initTreeData = (nodes, level) => {for (let i = 0; i < nodes.length; i++) { // 遍历数据const node = nodes[i];node.serial = i + 1; // 额外添加以下字段node.level = level;node.isdraggable = true;node.open = false;// 如果节点有子节点,递归遍历子节点,并将层数加1if (node.children) {initTreeData(node.children, level + 1);}}return nodes;
};onMounted(() => {originData.value = initTreeData(originData.value, 1);
});// 部分数据重置(添加重置的方法原因是拖拽后已经展开的数据(open:true)不再收起设置为(open:false))
const restTreeData = (nodes, level) => {for (let i = 0; i < nodes.length; i++) {const node = nodes[i];node.serial = i + 1;node.level = level;node.isdraggable = true;// 如果节点有子节点,递归遍历子节点,并将层数加1if (node.children) {restTreeData(node.children, level + 1);}}return nodes;
};// 开始拖动时处理节点是否可拖拽,需要样式区分
const dealAddData = (nodes, level) => {for (let i = 0; i < nodes.length; i++) {const node = nodes[i];if (node.level === level) {node.isdraggable = true;} else {node.isdraggable = false;}// 如果节点有子节点,递归遍历子节点,并将层数加1if (node.children) {dealAddData(node.children, level);}}return nodes;
};// 拖动后处理变化的数据(拖拽后需要对拖拽的chilren节点进行初始化,这里有一点优化,由于只能同层级拖拽,所以该节点下的子节点数据是没有变化的,只需要处理当前层级的即可)
const dealChangeData = (nodes, childrenNode, parentId) => {if (parentId === "") {nodes = childrenNode;} else {for (let i = 0; i < nodes.length; i++) {const node = nodes[i];if (node.parentId === parentId) {node.children = childrenNode;}// 如果节点有子节点,递归遍历子节点,并将层数加1if (node.children) {dealAddData(node.children, childrenNode, parentId);}}}return nodes;
};// 结束拖动时处理数据
const handleChange = (data1, data2) => {const parentId = data2[0].parentId;const data = dealChangeData(data1, data2, parentId); // 处理数据originData.value = restTreeData(data, 1); // 初始化数据
};// 开始拖动时修改数据:添加字段控制样式
const startHandleChange = (level) => {originData.value = dealAddData(originData.value, level);
};
</script><template><div class="row"><div style="min-height: 500px"><TreeDraggable // 使用组件:originData="originData":childrenData="originData"@handleChange="handleChange"@startHandleChange="startHandleChange"/></div></div>
</template><style scoped></style>
如果数据格式中所有children都是[],也可以与基本使用的样式一样:
let originData = ref([{children: [],exampleValue: "1",fieldName: "1层1号",id: "41cf48fb5c7546758b8ea679c7fc40b3",parentId: "",},{children: [],exampleValue: "123",fieldName: "1层2号",id: "57e6b0b1f0f14dff86deeb9ca2937772",parentId: "",},{children: [],exampleValue: '{a:"123"}',fieldName: "1层3号",id: "57e6b0b1f0f14dff86deeb9ca2937773",parentId: "",},{children: [],exampleValue: "1",fieldName: "1层4号",id: "b83e93dd599d40e6b58b24c0f3cf1cc8",parentId: "",},{children: [],exampleValue: "1",fieldName: "1层5号",id: "b83e93dd599d40e6b58b24c0f3cf1cd8",parentId: "",},{children: [],exampleValue: "1",fieldName: "1层6号",id: "b83e93dd599d40e6b58b24c0f3cf1cf8",parentId: "",},
]);