使用vue自定义如何实现Tree组件和拖拽功能
作者:风如也
这篇文章主要介绍了使用vue自定义如何实现Tree组件和拖拽功能,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
vue自定义实现Tree组件和拖拽功能
实现功能:树结构、右键菜单、拖拽
效果图
vue2 + js版
/components/drag-tree/utils/utils.js
let _treeId = 0; /** * 初始化树 * @param {Array} tree 树的原始结构 * @param {Object} props 树的字段值 * @param {Boolean} defaultExpandAll 是否展开节点 */ function initTree(tree, props, defaultExpandAll: boolean) { let right = localStorage.getItem("right"); right = JSON.parse(right); return initTreed(tree, 1, props, defaultExpandAll, [], right); } /** * 初始化树 * @param {Array} tree 树的原始结构 * @param {Number} layer 层级 * @param {Object} props 树的字段值 * @param {Boolean} defaultExpandAll 是否展开节点 * @param {Array} props 新树 * @param {Array} right 判断节点展不展开 */ function initTreed(tree, layer, props, defaultExpandAll, newTree, right) { for (let i = 0; i < tree.length; i++) { let obj {}; for (const item in tree[i]) { if (item === props.label) { obj.label = tree[i][item]; } else if (item === props.id) { obj.id = tree[i][item]; } else if (item === props.children && tree[i][props.children].length) { obj.children = []; } else { obj[item] = tree[i][item]; if (item === "children") { delete obj.children } } } if (right) { right.indexOf(obj.id) !== -1 ? (obj.defaultExpandAll = true) : (obj.defaultExpandAll = false); } else { obj.defaultExpandAll = defaultExpandAll; } obj._treeId = _treeId++; obj.layer = layer; obj.data = JSON.parse(JSON.stringify(tree[i])); newTree.push(obj); if ("children" in obj) { initTreed( tree[i][props.children], layer + 1, props, defaultExpandAll, newTree[i].children, right ); } obj = {}; } return newTree; } /** * * @param {Array} tree 树 * @param {Number} layer 层级 * @returns */ function draggableTree(tree: IAnyType[], layer) { for (let i = 0; i < tree.length; i++) { tree[i].layer = layer; if ("children" in tree[i]) { draggableTree(tree[i].children, layer + 1); } } return tree; } /** * 寻找 */ function findNearestComponent(element, componentName) { let target = element; while (target && target.tagName !== "BODY") { if (target.__vue__ && target.__vue__.$options.name === componentName) { return target.__vue__; } target = target.parentNode; } return null; } export { initTree, draggableTree, findNearestComponent };
/components/drag-tree/node.vue
<template> <div class="drag-tree item" :draggable="tree.draggable" @dragstart.stop="dragstart" @dragover.stop="dragover" @drop.stop="drop" @contextmenu="($event) => this.handleContextMenu($event)" ref="item" :id="data._treeId" > <!-- 每一行 --> <div style="height: 1px" :style="{ background: dropType == 'before' ? `#${draggableColor}` : '' }" ></div> <div @click="itemClick($event, data)" :class="['text', active === data.id ? 'is-current' : '']" :style="{ height: height, lineHeight: height, fontSize: fontSize, position: 'relative', margin: '0 auto', }" > <span :style="{ display: 'inline-block', width: (data.layer - 1) * 18 + 'px', }" ></span> <img :class="[data.defaultExpandAll ? 'iconBottom' : 'iconRight']" v-show="data.children && data.children.length !== 0" :src="iconImg" :style="{ width: fontSize, height: fontSize, display: 'inline-block', verticalAlign: 'middle', marginRight: '3px', }" alt="" /> <span v-show="!data.children || data.children.length == 0" :style="{ width: fontSize, height: fontSize, display: 'inline-block', verticalAlign: 'middle', marginRight: '3px', }" ></span> <img v-if="data.TreeImg" :src="dataImg" :style="{ width: fontSize, height: fontSize + 5, display: 'inline-block', verticalAlign: 'middle', marginRight: '3px', }" /> <span :style="{ background: dropType == 'inner' ? `#${draggableColor}` : '', height: fontSize + 5, color: dropType == 'inner' ? '#fff' : '#7d90b2', overflow: 'hidden', }" >{{ data.label }}{{ data.isCurrent }}</span > <node-content :node="data"></node-content> </div> <div style="height: 1px" :style="{ background: dropType == 'after' ? `#${draggableColor}` : '' }" ></div> <div v-if="data.children && data.children.length != 0" :class="[data.defaultExpandAll ? 'sonShow' : 'sonVanish', 'son']" > <my-node v-for="item in data.children" :key="item._treeId" :render-content="renderContent" :data="item" :active-id.sync="active" ></my-node> </div> </div> </template> <script> import { findNearestComponent } from "./utils/utils.ts"; export default { name: "MyNode", props: { data: { // 接收的数据 type: Object, }, activeId: { type: [Number, String] }, renderContent: Function, }, components: { NodeContent: { props: { node: { required: true } }, render(h) { const parent = this.$parent; const tree = parent.tree; const node = this.node; const { data, store } = node; return ( parent.renderContent ? parent.renderContent.call(parent._renderProxy, h, { _self: tree.$vnode.context, node, data, store }) : tree.$scopedSlots.default ? tree.$scopedSlots.default({ node, data }) : '' ); } } }, inject: ["draggableColor", "height", "fontSize", "icon"], data() { return { curNode: null, tree: "", // 最上一级 dropType: "none", iconImg: "", dataImg: "", }; }, computed: { active: { set (val) { this.$emit("update:activeId", val); }, get () { return this.activeId; } } }, created() { let parent = this.$parent; if (parent.isTree) { this.tree = parent; } else { this.tree = parent.tree; } // console.log(this.$parent) // console.log(this.tree) // console.log(parent) // console.log(parent.isTree) // console.log(parent.tree) // 有没有自定义icon if (this.icon.length != 0) { let s = this.icon.slice(0, 2); let url = this.icon.slice(2); if (s == "@/") { this.iconImg = require(`@/${url}`); } else { this.iconImg = this.icon; } } else { this.iconImg = require("@/assets/images/business/tree/right.png"); } if (this.data.TreeImg) { let s = this.data.TreeImg.slice(0, 2); let url = this.data.TreeImg.slice(2); if (s == "@/") { this.dataImg = require(`@/${url}`); } else { this.dataImg = this.data.TreeImg; } } }, mounted() { document.body.addEventListener('click', this.closeMenu); }, destroyed() { document.body.removeEventListener('click', this.closeMenu); }, methods: { closeMenu() { this.tree.$emit('close-menu'); }, handleContextMenu(event) { if (this.tree._events['node-contextmenu'] && this.tree._events['node-contextmenu'].length > 0) { event.stopPropagation(); event.preventDefault(); } this.tree.$emit('node-contextmenu', event, this.data, this); }, // 选择要滑动的元素 dragstart(ev) { if (!this.tree.draggable) return; this.tree.$emit("node-start", this.data, this, ev); }, // 滑动中 dragover(ev) { if (!this.tree.draggable) return; ev.preventDefault(); this.tree.$emit("node-over", this.data, this, ev); }, // 滑动结束 drop(ev) { if (!this.tree.draggable) return; this.tree.$emit("node-drop", this.data, this, ev); }, // 行点击事件 itemClick(ev, data) { let dropNode = findNearestComponent(ev.target, "MyNode"); // 现在的节点 this.active = data.id; this.data.defaultExpandAll = !this.data.defaultExpandAll; // 改变树的伸缩状态 this.tree.$emit("tree-click", this.data, dropNode); let right = localStorage.getItem("right"); if (this.data.defaultExpandAll === true) { if (right) { right = JSON.parse(right); right.push(this.data.id); } else { right = []; right.push(this.data.id); } } else { if (right) { right = JSON.parse(right); right.indexOf(this.data.id) !== -1 ? right.splice(right.indexOf(this.data.id), 1) : ""; } } localStorage.setItem("right", JSON.stringify(right)); }, }, }; </script> <style lang="less"> .drag-tree { .text { color: #7d90b2; font-size: 14px; height: 32px; line-height: 32px; cursor: pointer; &.is-current { background: #f5f7fa; } } .text:hover { background: #f5f7fa; } .iconBottom { transition: 0.3s; transform: rotate(90deg); } .iconRight { transition: 0.3s; transform: rotate(0deg); } .son { max-height: 0px; overflow: hidden; transition: 0.3s max-height; } .sonVanish { max-height: 0px; } .sonShow { max-height: 1000px; } &-popover { width: 100px; height: auto; position: fixed; background: #fff; border: 1px solid #ddd; box-shadow: 0 1px 6px rgba(54, 54, 54, 0.2); z-index: 9999; border-radius: 4px; &-item { color: #515a6e; line-height: 35px; text-align: center; cursor: pointer; transition: background .2s ease-in-out; &:hover, &:active { background: #f3f3f3; } } } } </style>
/components/drag-tree/index.vue
<template> <div style="width: 100%; height: 100%"> <Node :render-content="renderContent" v-for="item in root" :key="item._treeId" :data="item" :active-id.sync="activeId" :isTree="true" ></Node> </div> </template> <script> import Node from "./node.vue"; import { initTree, findNearestComponent } from "./utils/utils.ts"; export default { name: "TreeDrag", components: { Node, }, provide() { return { draggableColor: this.draggableColor, height: this.height, fontSize: this.fontSize, icon: this.icon, }; }, props: { data: { type: Array, }, renderContent: Function, draggable: { // 是否开启拖拽 type: Boolean, default: false, }, defaultExpandAll: { // 是否默认展开所有节点 type: Boolean, default: false, }, draggableColor: { // 拖拽时的颜色 type: String, default: "409EFF", }, height: { // 每行高度 type: String, default: "40px", }, fontSize: { type: String, default: "14px", }, icon: { type: String, default: "", }, props: { type: Object, default() { return { label: "label", children: "children", }; }, }, }, watch: { data(nerVal) { this.root = initTree(nerVal, this.props, this.defaultExpandAll); // 新树 if (this.root?.length && !this.activeId) { this.activeId = this.root[0].id; } }, deep: true }, data() { return { activeId: 0, startData: {}, // 拖拽时被拖拽的节点 lg1: null, // 拖拽经过的最后一个节点 lg2: null, // 拖拽经过的最后第二个节点 root: null, // data的数据 dragState: { showDropIndicator: false, draggingNode: null, // 拖动的节点 dropNode: null, allowDrop: true, }, odata: "", }; }, created() { this.odata = this.data; this.isTree = true; // 这是最高级 this.root = initTree(this.data, this.props, this.defaultExpandAll); // 新树 // 选择移动的元素 事件 this.$on("node-start", (data, that, ev) => { this.startData = data; this.dragState.draggingNode = that; this.$emit("tree-start", that.data.data, that.data, ev); }); // 移动事件 this.$on("node-over", (data, that, ev) => { console.log(2222) console.log(ev.target) if (that.$refs.item.id != this.lg1) { this.lg2 = this.lg1; this.lg1 = that.$refs.item.id; } let dropNode = findNearestComponent(ev.target, "MyNode"); // 现在的节点 const oldDropNode = this.dragState.dropNode; // 上一个节点 if (oldDropNode && oldDropNode !== dropNode) { // 判断节点改没改变 oldDropNode.dropType = "none"; } const draggingNode = this.dragState.draggingNode; // 移动的节点 console.log(draggingNode) console.log(dropNode) console.log(this.dragState) if (!draggingNode || !dropNode) return; console.log(33333) let dropPrev = true; // 上 let dropInner = true; // 中 let dropNext = true; // 下 ev.dataTransfer.dropEffect = dropInner ? "move" : "none"; this.dragState.dropNode = dropNode; const targetPosition = dropNode.$el.getBoundingClientRect(); const prevPercent = dropPrev ? dropInner ? 0.25 : dropNext ? 0.45 : 1 : -1; const nextPercent = dropNext ? dropInner ? 0.75 : dropPrev ? 0.55 : 0 : 1; var dropType = ""; const distance = ev.clientY - targetPosition.top; if (distance < targetPosition.height * prevPercent) { // 在上面 dropType = "before"; } else if (distance > targetPosition.height * nextPercent) { // 在下面 dropType = "after"; } else if (dropInner) { dropType = "inner"; } else { dropType = "none"; } if (this.digui(draggingNode.data, dropNode.data._treeId)) { dropType = "none"; } dropNode.dropType = dropType; console.log(1111111) console.log(dropType) this.$emit("tree-over", that.data.data, that.data, ev, dropType); }); // 移动结束 事件 this.$on("node-drop", (data, that, ev) => { console.log(data, that, ev) console.log(this.startData) let sd = JSON.stringify(this.startData.data); let ad = JSON.stringify(this.data); let ss = ad.split(sd); let newData; ss = ss.join(""); console.log(that.dropType) if (that.dropType == "none") { return; } console.log(that.dropType) if (this.lg2 != null && this.lg1 != this.startData._treeId) { // 删除startData ss = this.deleteStr(ss); let od = JSON.stringify(data.data); let a = ss.indexOf(od); console.log(newData) if (that.dropType == "after") { newData = JSON.parse( ss.substring(0, a + od.length) + "," + sd + ss.substring(a + od.length) ); } else if (that.dropType == "before") { if (a == -1) { let s = this.deleteStr(od.split(sd).join("")); newData = JSON.parse( ss.substring(0, ss.indexOf(s)) + sd + "," + ss.substring(ss.indexOf(s)) ); } else { newData = JSON.parse( ss.substring(0, a) + sd + "," + ss.substring(a) ); } } else if (that.dropType == "inner") { ss = JSON.parse(ss); this.oldData(ss, data.data, JSON.parse(sd)); newData = ss; } console.log(newData) this.root = initTree(newData, this.props, this.defaultExpandAll); // 新树 this.$parent.data = newData; this.lg1 = null; this.lg2 = null; } this.$emit( "tree-drop", this.data.data, this.data, ev, this.startData.id, data.id, that.dropType, this.root ); that.dropType = "none"; }); }, methods: { /** * 修改data,添加输入 * @param {Array} ss 需要被加入的数据 * @param {Object} data 落点 * @param {Object} sd 需要加入的数据 */ oldData(ss, data, sd) { for (let i = 0; i < ss.length; i++) { if (JSON.stringify(ss[i]) == JSON.stringify(data)) { if ("children" in ss[i]) { ss[i].children.push(sd); } else { ss[i].children = []; ss[i].children.push(sd); } break; } else if ("children" in ss[i]) { this.oldData(ss[i].children, data, sd); } } }, // 判断拖拽时贴近的是不是自己的子元素 digui(data, id) { if (data.children && data.children.length != 0) { for (let i = 0; i < data.children.length; i++) { if (data.children[i]._treeId == id) { return true; } let s = this.digui(data.children[i], id); if (s == true) { return true; } } } }, deleteStr(ss) { if (ss.indexOf(",,") !== -1) { ss = ss.split(",,"); if (ss.length !== 1) { ss = ss.join(","); } } else if (ss.indexOf("[,") !== -1) { ss = ss.split("[,"); if (ss.length !== 1) { ss = ss.join("["); } } else if (ss.indexOf(",]") !== -1) { ss = ss.split(",]"); if (ss.length !== 1) { ss = ss.join("]"); } } return ss; }, }, }; </script> <style scoped> .drag { font-size: 14px; text-align: right; padding-right: 5px; cursor: pointer; } </style>
使用:Test.vue
<template> <div style="width: 100%; height: 100%;"> <tree-drag ref="dragTree" @node-contextmenu="handleContextMenu" @tree-click="treeClick" @tree-drop="treeDrop" @close-menu="closeMenu" :data="data" :props="defaultProps" :draggable="true" > </tree-drag> <div class="drag-tree-popover" :style="style" v-if="isShowPopover"> <div class="drag-tree-popover-item" v-for="(item, index) in popoverList" :key="index" @click="menuClick(item)" > <i class="iconfont" :class="'icon-' + item.type"></i> {{ item.name }} </div> </div> </div> </template> <script> export default { name: "Test", data() { return { parent_id: "0", data: [], defaultProps: { children: "children", label: "name", }, popoverLeft: 0, // 距离左边的距离 popoverTop: 0, // 距离顶部的距离 isShowPopover: false, // 是否展示右键内容 popoverList: [ { name: "新增", type: "xinzeng" }, { name: "编辑", type: "bianji" }, { name: "删除", type: "shanchu" }, ], treeNode: null, activeId: 0, }; }, created() { this.getTreeData(); }, computed: { // 计算出距离 style() { return { left: this.popoverLeft + "px", top: this.popoverTop + "px", }; }, }, methods: { // 显示自定义菜单 handleContextMenu(event, node, that) { this.popoverLeft = event.clientX + 10; this.popoverTop = event.clientY; this.isShowPopover = true; }, // 关闭菜单 closeMenu() { this.isShowPopover = false; }, treeClick(data) { this.activeId = data.id; }, treeDrop(node, data, ev, startId, targetId, dropType, root) { console.log(startId, targetId, dropType, root); }, // 菜单某一项被点击 menuClick(item) { // 操作 this.closeMenu(); }, // 判断activeId是否存在 findIdIsExit(data, id) { if (data && data.length) { for (let i = 0; i < data.length; i++) { if (data[i].id == id) { return true; } if (data[i].children && data[i].children.length) { let s = this.findIdIsExit(data[i].children, id); if (s === true) { return true; } } } } }, async getTreeData() { let res = await this.$service.invoke({}); this.data = res?.result ? res.result : []; this.activeId = this.data[0].id; this.$refs.dragTree.activeId = this.activeId; }, }, }; </script>
vue2 + ts 版
只有两个组件的ts部分文件不一样,其他一样
/components/drag-tree/node.vue
<template> <div class="drag-tree item" :draggable="tree.draggable" @dragstart.stop="dragstart" @dragover.stop="dragover" @drop.stop="drop" @contextmenu="($event) => this.handleContextMenu($event)" ref="item" :id="data._treeId" > <!-- 每一行 --> <div style="height: 1px" :style="{ background: dropType == 'before' ? `#${draggableColor}` : '' }" ></div> <div @click="itemClick($event, data)" :class="['text', active === data.id ? 'is-current' : '']" :style="{ height: height, lineHeight: height, fontSize: fontSize, position: 'relative', margin: '0 auto', }" > <span :style="{ display: 'inline-block', width: (data.layer - 1) * 18 + 'px', }" ></span> <img :class="[data.defaultExpandAll ? 'iconBottom' : 'iconRight']" v-show="data.children && data.children.length !== 0" :src="iconImg" :style="{ width: fontSize, height: fontSize, display: 'inline-block', verticalAlign: 'middle', marginRight: '3px', }" alt="" /> <span v-show="!data.children || data.children.length == 0" :style="{ width: fontSize, height: fontSize, display: 'inline-block', verticalAlign: 'middle', marginRight: '3px', }" ></span> <img v-if="data.TreeImg" :src="dataImg" :style="{ width: fontSize, height: fontSize + 5, display: 'inline-block', verticalAlign: 'middle', marginRight: '3px', }" /> <span :style="{ background: dropType == 'inner' ? `#${draggableColor}` : '', height: fontSize + 5, color: dropType == 'inner' ? '#fff' : '#7d90b2', overflow: 'hidden', }" >{{ data.label }}{{ data.isCurrent }}</span > <node-content :node="data"></node-content> </div> <div style="height: 1px" :style="{ background: dropType == 'after' ? `#${draggableColor}` : '' }" ></div> <div v-if="data.children && data.children.length != 0" :class="[data.defaultExpandAll ? 'sonShow' : 'sonVanish', 'son']" > <my-node v-for="item in data.children" :key="item._treeId" :render-content="renderContent" :data="item" :active-id.sync="active" ></my-node> </div> </div> </template> <script lang="ts"> import node from "./node"; export default node; </script> <style lang="less"> .drag-tree { .text { color: #7d90b2; font-size: 14px; height: 32px; line-height: 32px; cursor: pointer; &.is-current { background: #f5f7fa; } } .text:hover { background: #f5f7fa; } .iconBottom { transition: 0.3s; transform: rotate(90deg); } .iconRight { transition: 0.3s; transform: rotate(0deg); } .son { max-height: 0px; overflow: hidden; transition: 0.3s max-height; } .sonVanish { max-height: 0px; } .sonShow { max-height: 1000px; } &-popover { width: 100px; height: auto; position: fixed; background: #fff; border: 1px solid #ddd; box-shadow: 0 1px 6px rgba(54, 54, 54, 0.2); z-index: 9999; border-radius: 4px; &-item { color: #515a6e; line-height: 35px; text-align: center; cursor: pointer; transition: background .2s ease-in-out; &:hover, &:active { background: #f3f3f3; } } } } </style>
/components/drag-tree/node.ts
import { Vue, Component, Prop, PropSync, Inject } from "vue-property-decorator"; import { findNearestComponent } from "./utils/utils"; @Component({ name: "MyNode", components: { NodeContent: { props: { node: { required: true } }, render(h) { const parent = this.$parent; const tree = parent.tree; const node = this.node; const { data, store } = node; return ( parent.renderContent ? parent.renderContent.call(parent._renderProxy, h, { _self: tree.$vnode.context, node, data, store }) : tree.$scopedSlots.default ? tree.$scopedSlots.default({ node, data }) : '' ); } } } }) export default class node extends Vue { @Prop() data: IAnyType @PropSync("activeId", { type: [Number, String] }) active!: string | number @Prop(Function) renderContent @Inject("draggableColor") readonly draggableColor!: string @Inject("height") readonly height!: string @Inject("fontSize") readonly fontSize!: string @Inject("icon") readonly icon!: string curNode = null tree: IAnyType // 最上一级 dropType = "none" iconImg = "" dataImg = "" created(): void { const parent: any = this.$parent; if (parent.isTree) { this.tree = parent; } else { this.tree = parent.tree; } // 有没有自定义icon if (this.icon.length != 0) { const s = this.icon.slice(0, 2); const url = this.icon.slice(2); if (s == "@/") { this.iconImg = require(`@/${url}`); } else { this.iconImg = this.icon; } } else { this.iconImg = require("@/assets/images/business/tree/right.png"); } if (this.data.TreeImg) { const s = this.data.TreeImg.slice(0, 2); const url = this.data.TreeImg.slice(2); if (s == "@/") { this.dataImg = require(`@/${url}`); } else { this.dataImg = this.data.TreeImg; } } } mounted(): void { document.body.addEventListener("click", this.closeMenu); } destroyed(): void { document.body.removeEventListener("click", this.closeMenu); } closeMenu(): void { this.tree.$emit("close-menu"); } handleContextMenu(event: DragEvent): void { if (this.tree._events["node-contextmenu"] && this.tree._events["node-contextmenu"].length > 0) { event.stopPropagation(); event.preventDefault(); } this.tree.$emit("node-contextmenu", event, this.data, this); } // 选择要滑动的元素 dragstart(ev: DragEvent): void { if (!this.tree.draggable) return; this.tree.$emit("node-start", this.data, this, ev); } // 滑动中 dragover(ev: DragEvent): void { if (!this.tree.draggable) return; ev.preventDefault(); this.tree.$emit("node-over", this.data, this, ev); } // 滑动结束 drop(ev: DragEvent): void { if (!this.tree.draggable) return; this.tree.$emit("node-drop", this.data, this, ev); } // 行点击事件 itemClick(ev: DragEvent, data: IAnyType): void { const dropNode = findNearestComponent(ev.target, "MyNode"); // 现在的节点 this.active = data.id; this.data.defaultExpandAll = !this.data.defaultExpandAll; // 改变树的伸缩状态 this.tree.$emit("tree-click", this.data, dropNode); const right: string = localStorage.getItem("right"); let rightArr: IAnyType[]; if (right) { rightArr = JSON.parse(right); } if (this.data.defaultExpandAll === true) { if (right) { rightArr.push(this.data.id); } else { rightArr = []; rightArr.push(this.data.id); } } else { if (right) { rightArr.indexOf(this.data.id) !== -1 ? rightArr.splice(rightArr.indexOf(this.data.id), 1) : ""; } } localStorage.setItem("right", JSON.stringify(rightArr)); } }
/components/drag-tree/index.vue
<template> <div style="width: 100%; height: 100%"> <Node :render-content="renderContent" v-for="item in root" :key="item._treeId" :data="item" :active-id.sync="activeId" :isTree="true" ></Node> </div> </template> <script lang="ts"> import index from "./index"; export default index; </script> <style scoped> .drag { font-size: 14px; text-align: right; padding-right: 5px; cursor: pointer; } </style>
/components/drag-tree/index.ts
import { Vue, Component, Provide, Prop, Watch } from "vue-property-decorator"; import Node from "./node.vue"; import { initTree, findNearestComponent } from "./utils/utils"; @Component({ name: "TreeDrag", components: { Node } }) export default class index extends Vue { @Prop({ default: [] }) data?: any[] @Prop(Function) renderContent @Prop({ default: true }) isTree?: boolean // 是否开启拖拽 @Prop({ default: false }) draggable?: boolean // 是否默认展开所有节点 @Prop({ default: false }) defaultExpandAll?: boolean // 拖拽时的颜色 @Prop({ default: "409EFF" }) dragColor: string // 每行高度 @Prop({ default: "40px" }) lineHeight: string @Prop({ default: "14px" }) lineFontSize: string @Prop({ default: "" }) iconName: string @Prop({ default: () => { return { label: "label", children: "children", } } }) props: IAnyType @Provide("draggableColor") draggableColor = "409EFF" @Provide("height") height = "40px" @Provide("fontSize") fontSize = "14px" @Provide("icon") icon = "" activeId = 0 startData = { data: [], _treeId: "", id: "" } // 拖拽时被拖拽的节点 lg1 = null // 拖拽经过的最后一个节点 lg2 = null // 拖拽经过的最后第二个节点 root = null // data的数据 dragState = { showDropIndicator: false, draggingNode: null, // 拖动的节点 dropNode: null, allowDrop: true, } odata = [] @Watch("data", { deep: true }) onData(nerVal) { this.root = initTree(nerVal, this.props, this.defaultExpandAll); // 新树 if (this.root?.length && !this.activeId) { this.activeId = this.root[0].id; } } @Watch("dragColor", { immediate: true }) onDragColor(nerVal) { this.draggableColor = nerVal; } @Watch("lineHeight", { immediate: true }) onHeight(nerVal) { this.height = nerVal; } @Watch("lineFontSize", { immediate: true }) onFontSize(nerVal) { this.fontSize = nerVal; } @Watch("iconName", { immediate: true }) onIconName(nerVal) { this.icon = nerVal; } created(): void { this.odata = this.data; this.root = initTree(this.data, this.props, this.defaultExpandAll); // 新树 // 选择移动的元素 事件 this.$on("node-start", (data, that, ev) => { this.startData = data; this.dragState.draggingNode = that; this.$emit("tree-start", that.data.data, that.data, ev); }); // 移动事件 this.$on("node-over", (data, that, ev) => { if (that.$refs.item.id != this.lg1) { this.lg2 = this.lg1; this.lg1 = that.$refs.item.id; } const dropNode = findNearestComponent(ev.target, "MyNode"); // 现在的节点 const oldDropNode = this.dragState.dropNode; // 上一个节点 if (oldDropNode && oldDropNode !== dropNode) { // 判断节点改没改变 oldDropNode.dropType = "none"; } const draggingNode = this.dragState.draggingNode; // 移动的节点 if (!draggingNode || !dropNode) return; const dropPrev = true; // 上 const dropInner = true; // 中 const dropNext = true; // 下 ev.dataTransfer.dropEffect = dropInner ? "move" : "none"; this.dragState.dropNode = dropNode; const targetPosition = dropNode.$el.getBoundingClientRect(); const prevPercent = dropPrev ? dropInner ? 0.25 : dropNext ? 0.45 : 1 : -1; const nextPercent = dropNext ? dropInner ? 0.75 : dropPrev ? 0.55 : 0 : 1; let dropType = ""; const distance = ev.clientY - targetPosition.top; if (distance < targetPosition.height * prevPercent) { // 在上面 dropType = "before"; } else if (distance > targetPosition.height * nextPercent) { // 在下面 dropType = "after"; } else if (dropInner) { dropType = "inner"; } else { dropType = "none"; } if (this.digui(draggingNode.data, dropNode.data._treeId)) { dropType = "none"; } dropNode.dropType = dropType; this.$emit("tree-over", that.data.data, that.data, ev, dropType); }); // 移动结束 事件 this.$on("node-drop", (data, that, ev) => { const sd = JSON.stringify(this.startData.data); const ad = JSON.stringify(this.data); let ss: string | string[] = ad.split(sd); let newData; ss = ss.join(""); if (that.dropType == "none") { return; } if (this.lg2 != null && this.lg1 != this.startData._treeId) { // 删除startData ss = this.deleteStr(ss); const od = JSON.stringify(data.data); const a = ss.indexOf(od); if (that.dropType == "after") { newData = JSON.parse( ss.substring(0, a + od.length) + "," + sd + ss.substring(a + od.length) ); } else if (that.dropType == "before") { if (a == -1) { const s = this.deleteStr(od.split(sd).join("")); newData = JSON.parse( ss.substring(0, ss.indexOf(s)) + sd + "," + ss.substring(ss.indexOf(s)) ); } else { newData = JSON.parse( ss.substring(0, a) + sd + "," + ss.substring(a) ); } } else if (that.dropType == "inner") { ss = JSON.parse(ss); this.oldData(ss, data.data, JSON.parse(sd)); newData = ss; } this.root = initTree(newData, this.props, this.defaultExpandAll); // 新树 const parent: any = this.$parent; parent.data = newData; this.lg1 = null; this.lg2 = null; } this.$emit( "tree-drop", this.data, ev, this.startData.id, data.id, that.dropType, this.root ); that.dropType = "none"; }); } /** * 修改data,添加输入 * @param {Array} ss 需要被加入的数据 * @param {Object} data 落点 * @param {Object} sd 需要加入的数据 */ oldData(ss, data, sd): void { for (let i = 0; i < ss.length; i++) { if (JSON.stringify(ss[i]) == JSON.stringify(data)) { if ("children" in ss[i]) { ss[i].children.push(sd); } else { ss[i].children = []; ss[i].children.push(sd); } break; } else if ("children" in ss[i]) { this.oldData(ss[i].children, data, sd); } } } // 判断拖拽时贴近的是不是自己的子元素 digui(data, id): boolean { if (data.children && data.children.length != 0) { for (let i = 0; i < data.children.length; i++) { if (data.children[i]._treeId == id) { return true; } const s = this.digui(data.children[i], id); if (s == true) { return true; } } } } deleteStr(ss): string { if (ss.indexOf(",,") !== -1) { ss = ss.split(",,"); if (ss.length !== 1) { ss = ss.join(","); } } else if (ss.indexOf("[,") !== -1) { ss = ss.split("[,"); if (ss.length !== 1) { ss = ss.join("["); } } else if (ss.indexOf(",]") !== -1) { ss = ss.split(",]"); if (ss.length !== 1) { ss = ss.join("]"); } } return ss; } }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。