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
this.handleContextMenu($event)" ref="item" :id="data._treeId" >{{ data.label }}{{ data.isCurrent }}
/components/drag-tree/index.vue
使用:Test.vue
{{ item.name }}
vue2 + ts 版
只有两个组件的ts部分文件不一样,其他一样
/components/drag-tree/node.vue
this.handleContextMenu($event)" ref="item" :id="data._treeId" >{{ data.label }}{{ data.isCurrent }}
/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
/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; } }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。