参考网址
vue.draggable vue3官网:
https://www.itxst.com/vue-draggable-next/tutorial.html
antvX6 实现图表功能
https://x6.antv.vision/zh/examples/gallery
Vue 实现可视化拖拽页面编辑器
https://www.jb51.net/article/205203.htm
https://demo-page.raindays.cn/
返回可拖拽区域的x、y坐标与宽高
https://blog.csdn.net/weixin_38318244/article/details/126603904
实现效果
image.png
image.png
代码参考
<template>
<div class='content'>
<a-row>
<a-col :span='4'>
<div class='lists'>
<draggable
:list='list'
animation='300'
@start='onStart'
@end='onEnd'
item-key='id'
:sort='false'>
<template #item='{ element }'>
<div class='item move' :ele='JSON.stringify(element)'>
<label class='move'>{{ element.name }}</label>
</div>
</template>
</draggable>
</div>
</a-col>
<a-col :span='20'>
<div class='lists view-content'>
<div id='container'
@drop='drog'
@dragover='dragOver'
></div>
</div>
</a-col>
</a-row>
</div>
</template>
<script lang='ts'>
import { defineComponent, onMounted, reactive, toRefs, ref } from 'vue';
import { Graph } from '@antv/x6';
import draggable from 'vuedraggable';
export default defineComponent({
name: 'Demo',
components: {
draggable,
},
setup() {
//拖拽
const state = reactive({
//需要拖拽的数据
list: [
{ name: 'www.itxst.com', id: '0' },
{ name: 'www.baidu.com', id: '1' },
{ name: 'www.google.com', id: '2' },
],
data: {
// // 节点
nodes: [
// {
// id: 'node1', // String,可选,节点的唯一标识
// x: 40, // Number,必选,节点位置的 x 值
// y: 40, // Number,必选,节点位置的 y 值
// width: 80, // Number,可选,节点大小的 width 值
// height: 40, // Number,可选,节点大小的 height 值
// label: 'hello', // String,节点标签
// },
],
// // 边
edges: [
// {
// source: 'node1', // String,必须,起始节点 id
// target: 'node2', // String,必须,目标节点 id
// },
],
},
active: '',
x: 0,
y: 0,
});
const graph = ref<any>({});
const init = () => {
graph.value = new Graph({
container: document.getElementById('container'),
grid: true,
embedding: {
enabled: true,
findParent({ node }) {
const bbox = node.getBBox();
return this.getNodes().filter((node) => {
const data = node.getData<any>();
if (data && data.parent) {
const targetBBox = node.getBBox();
return bbox.isIntersectWithRect(targetBBox);
}
return false;
});
},
},
translating: {
restrict: -20, // 将移动范围限制在画布距离画布边缘 20px 处
},
highlighting: {
embedding: {
name: 'stroke',
args: {
padding: -1,
attrs: {
stroke: '#73d13d',
},
},
},
},
});
graph.value.addNode({
x: 200,
y: 80,
width: 240,
height: 160,
zIndex: 1,
label: 'Parent',
attrs: {
body: {
fill: '#fffbe6',
stroke: '#ffe7ba',
},
label: {
fontSize: 12,
},
},
data: {
parent: true,
},
});
graph.value.on('node:change:parent', ({ node }) => {
console.log(node);
// node.attr({
// label: {
// text: 'Child\n(embed)',
// },
// });
});
//删除
// graph.value.on('node:mouseenter', ({ node }) => {
// if (node === target) {
// node.addTools({
// name: 'button-remove',
// args: {
// x: 0,
// y: 0,
// offset: { x: 10, y: 10 },
// },
// });
// }
// });
//
// graph.value.on('node:mouseleave', ({ node }) => {
// if (node === target) {
// node.removeTools();
// }
// });
};
//拖拽开始的事件
const onStart = (e) => {
state.active = e.item.attributes.ele.value;
console.log('开始拖拽');
};
//拖拽结束的事件
const onEnd = () => {
let siderWidth = document.querySelector('.ant-layout-sider-children').clientWidth;
let listWidth = document.querySelector('.lists').clientWidth;
let headerHeight = document.querySelector('.ant-layout-header').clientHeight;
let tabHeight = document.querySelector('.ant-tabs-top').clientHeight;
let list = JSON.parse(JSON.stringify(state.list));
let data = JSON.parse(JSON.stringify(state.data));
let active = JSON.parse(state.active);
let width = 150;
let height = 40;
//减去元素宽高
let nowX = state.x - siderWidth - listWidth - width - 25;
let nowY = state.y - headerHeight - tabHeight - height - 25;
state.list = list.filter(item => item.id !== active.id);
let baseNode = {
id: active.id, // String,可选,节点的唯一标识
x: nowX, // Number,必选,节点位置的 x 值
y: nowY, // Number,必选,节点位置的 y 值
width, // Number,可选,节点大小的 width 值
height, // Number,可选,节点大小的 height 值
label: active.name, // String,节点标签
};
data.nodes.push(baseNode);
state.data = data;
graph.value.addNode(baseNode);
if (data.nodes.length > 1) {
for (let i = 0; i < data.nodes.length; i++) {
if (i + 1 < data.nodes.length) {
let baseEdge = {
source: data.nodes[i].id,
target: data.nodes[i + 1].id,
connector: { name: 'rounded' },
attrs: {
line: {
sourceMarker: {
tagName: 'path',
d: 'M 20 -10 0 0 20 10 Z',
},
targetMarker: {
tagName: 'path',
d: 'M 20 -10 0 0 20 10 Z',
strokeWidth: 2,
fill: '#73d13d',
stroke: '#237804',
},
},
},
};
graph.value.addEdge(baseEdge);
data.edges.push(baseEdge);
}
}
}
};
const dragOver = (e) => {
// let className = e.target.className.animVal;
state.x = e.clientX;
state.y = e.clientY;
e.preventDefault();
e.stopPropagation();
};
const drog = (e) => {
e.preventDefault();
e.stopPropagation();
};
onMounted(() => {
init();
});
return {
onStart,
dragOver,
drog,
onEnd,
...toRefs(state),
};
},
});
</script>
<style scoped>
.lists {
padding: 24px;
border-right: 1px solid #ddd;
}
.lists > div {
min-height: 500px;
}
.item {
border: solid 1px #eee;
padding: 6px 10px;
text-align: left;
}
.item:hover {
cursor: move;
}
.item + .item {
margin-top: 10px;
}
.ghost {
border: solid 1px #399bff;
}
.chosenClass {
background-color: #f1f1f1;
}
.itxst {
background-color: #f1f1f1;
display: flex;
padding: 20px;
min-height: 500px;
}
.graph .item:not(:first-child) {
height: 0;
border: none;
}
</style>