vue自定义实现Tree组件和拖拽功能
实现功能:树结构、右键菜单、拖拽
效果图
vue2 + js版
/components/drag-tree/utils/utils.js
let _treeId = 0;
function initTree(tree, props, defaultExpandAll: boolean) {
let right = localStorage.getItem("right");
right = JSON.parse(right);
return initTreed(tree, 1, props, defaultExpandAll, [], 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;
}
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: {
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";
});
}
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;
}
}
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。