需求
目标:两组数据,分为两个列表展示,把对应的数据进行关联
关联方式:从左边列表拖拽出一条线关联到右边列表,一进一出,一一对应,无对应关系可不进行对应
选型
看了一些库,很多看起来都不支持类似功能(没有相关demo演示)
最终选定两个库:jsPlumb 和 antv G6
G6: 左右列表在G6内部生成,支持类型比较局限,控制显示内容的字段label好像只支持string,不支持自定义dom,灵活性不够,不能自定义关联的操作按钮
jsPlumb:可以自定义dom,通过id绑定的方式,与dom进行关联,相对灵活,操作按钮也可以自定义添加
使用
安装依赖:yarn add jsplumb
- 自定义配置 + 含义:
// jsPlumb默认配置
jsPlumbSetting: {
Container: "helen", // 在指定范围内划线,不能超出此范围,以id确定范围
// 设置锚点类型:静态锚点,动态锚点,周边锚点,连续锚点【 https://docs.jsplumbtoolkit.com/toolkit/6.x/lib/anchors 】
// Anchors: [
// "Continuous", // "Top","TopCenter","TopRight", "TopLeft", "Right", "RightMiddle", "Bottom", "BottomCenter", "BottomRight", "BottomLeft", "Left", "LeftMiddle","AutoDefault","Perimeter","Continuous" ],
// 设置连线的样式:StateMachine、Flowchart,Bezier、Straight【 https://docs.jsplumbtoolkit.com/toolkit/6.x/lib/connectors 】
Connector: ["Straight", { gap: 5 }], // gap: 与端点间的距离; stub: 线出发多远开始弯折
// 鼠标是否拖动删除线
ConnectionsDetachable: true,
// 删除线的时候节点不删除
DeleteEndpointsOnDetach: true,
// 连线的两端端点类型:矩形 Rectangle;圆形Dot; eight: 矩形的高 ,idth: 矩形的宽
Endpoint: ["Dot", { radius: 5, cssClass: "yellow" }], //radius hoverClass:不能是局部样式 cssClass:不能是局部样式
// 线端点的样式: 不支持多个配置参数EndpointStyles
// EndpointStyle: { fill: "skyblue", radius: 3 },
// 连线样式
PaintStyle: {
stroke: "#000000",
strokeWidth: 1,
outlineStroke: "red", // 设定线外边的颜色
outlineWidth: 0, // 设定线外边的宽,单位px
},
// 设置所有箭头的样式, Overlays绘制连线箭头
ConnectionOverlays: [
[
"Arrow",
{
// 设置参数可以参考中文文档
width: 8, // 箭头尾部的宽度
length: 8, // 从箭头的尾部到头部的距离
location: 1, // 位置,建议使用0~1之间
// direction: 1, // 方向,默认值为1(表示向前),可选-1(表示向后)
// foldback: 0.623, // 折回,也就是尾翼的角度,默认0.623,当为1时,为正三角
// paintStyle: { stroke: "#999", fill: "#999" }, // 箭头样式
},
],
],
// 绘制图的模式 svg、canvas
RenderMode: "svg",
// ReattachConnections : true, //是否重新连接使用鼠标分离的线
// DragOptions: { cursor: "pointer", zIndex: 2000 },
DrapOptions: { cursor: "crosshair", zIndex: 2000 },
// 鼠标滑过线的样式
HoverPaintStyle: { stroke: "yellow", strokeWidth: 3, cursor: "pointer" },
},
- 初始化
onMounted(async () => {
// dom加载完成后才可以初始化jsplumb实例
await nextTick()
// 创建jsPlumb实例
jsPlumbInstanceRef.value = jsPlumb.getInstance();
});
- 创建端点:
创建端点的方式有三种:
3.1 makeSource/makeTarget:源端点和目标端点分别绑定,将绑定的dom元素当作端点,无连线时,无端点显示;有连线时才显示端点
const elem = document.getElementById(item.id);
if (item.info.group === "list1") {
jsPlumbInstanceRef.value.makeSource(elem, {
anchor: "Continuous", // 左 上 右 下
allowLoopback: false, // 允许回连
maxConnections: 1, //最大连接数(-1表示不限制)
});
} else {
jsPlumbInstanceRef.value.makeTarget(elem, {
anchor: "Continuous",
allowLoopback: false,
maxConnections: 1,
});
}
3.2 connect:创建连线时,会同时创建前后两个端点和一条连线,连线断开时,两边端点也会消失,无法再次连接
(文档说配置deleteEndpointsOnDetach:true可以在删除连线时保留端点,但是实际操作时配置未生效)
data.lineList.forEach((item) => {
jsPlumbInstanceRef.value.connect({
source: item.sourceId,
target: item.targetId,
deleteEndpointsOnDetach:true, // 不生效,删除线后,端点依然消失了
});
});
3.3 addEndpoint【采用这种】:创建端点,可以绑定uuid,connect连接时可以通过uuid进行端点连线,防止connect多次创建两边的端点,导致遮挡,拖拽不生效问题
data.nodeList.forEach((item, index) => {
jsPlumbInstanceRef.value.addEndpoint(
item.id,
{
anchor: [item.info.group === "list1" ? "Right" : "Left"],
maxConnections: 1,
uuid: item.id,
},
{
isSource: item.info.group === "list1", // 是否可以作为源
isTarget: item.info.group === "list2", // 是否可以作为目标
maxConnections: 1,
}
);
});
// 通过uuid绑定端点连线
data.lineList.forEach((item) => {
jsPlumbInstanceRef.value.connect({uuids: [item.sourceId, item.targetId]});
});
- 给节点添加拖拽::不是给锚点添加拖拽,锚点本身可以拖拽
// 通过draggable给节点添加拖拽:不是给锚点添加拖拽,锚点本身可以拖拽
jsPlumbInstanceRef.value.draggable(item.id);
- 对操作进行批处理:处理期间不触发渲染
jsPlumbInstanceRef.value.batch(() => {
// 进行操作
})
- 等待实例渲染完成后进行操作
jsPlumbInstanceRef.value.ready(() => {
// 进行操作
})
- 进行事件绑定
//连线成功时触发
jsPlumbInstanceRef.value.bind("connection", connectLine);
//连线断开时 触发
jsPlumbInstanceRef.value.bind("connectionDetached", connectLineDetached);
//连接取消connectionAborted
jsPlumbInstanceRef.value.bind(
"connectionAborted",
(conn, originalEvent) => {
console.log("连接取消", conn, originalEvent);
message.info("目标节点上已有关联属性,不支持该连接");
}
);
//在连线上点击右键触发
jsPlumbInstanceRef.value.bind("contextmenu", (conn, originalEvent) => {
console.log("点击右键", conn, originalEvent);
});
//点击连线
jsPlumbInstanceRef.value.bind("click", (conn, originalEvent) => {
console.log("click事件", conn, originalEvent);
});
//超过端点数量触发
jsPlumbInstanceRef.value.bind("onMaxConnections", (conn, originalEvent) => {
console.log("超过端点数量触发", conn, originalEvent);
});
- 实例方法
jsPlumbInstanceRef.value.repaintEverything(); // 重绘
jsPlumbInstanceRef.value.deleteEveryConnection(); // 断掉所有连线
总结:
// jsPlumb实例ready之后初始化一些设置
jsPlumbInstanceRef.value.ready(() => {
// 导入准备好的jsPlumb配置,通过importDefaults对默认配置进行覆盖
jsPlumbInstanceRef.value.importDefaults(data.jsPlumbSetting);
// 通过batch进行批处理操作:处理期间不触发渲染
jsPlumbInstanceRef.value.batch(() => {
// 对节点进行配置:创建端点,进行连线
data.nodeList.forEach((item, index) => {
jsPlumbInstanceRef.value.addEndpoint(
item.id,
{
anchor: [item.info.group === "list1" ? "Right" : "Left"],
maxConnections: 1,
uuid: item.id,
},
{
isSource: item.info.group === "list1", // 是否可以作为源
isTarget: item.info.group === "list2", // 是否可以作为目标
maxConnections: 1,
}
);
});
data.lineList.forEach((item) => {
jsPlumbInstanceRef.value.connect(item);
});
});
});
jsPlumbInstanceRef.value.repaintEverything(); // 重绘
//连线时触发 存储连接信息
jsPlumbInstanceRef.value.bind("connection", connectLine);
//连线断开时 触发
jsPlumbInstanceRef.value.bind("connectionDetached", connectLineDetached);
//连接取消connectionAborted
jsPlumbInstanceRef.value.bind(
"connectionAborted",
(conn, originalEvent) => {
console.log("连接取消", conn, originalEvent);
message.info("目标节点上已有关联属性,不支持该连接");
}
);
//在连线上点击右键触发
jsPlumbInstanceRef.value.bind("contextmenu", (conn, originalEvent) => {
console.log("点击右键", conn, originalEvent);
});
//点击连线
jsPlumbInstanceRef.value.bind("click", (conn, originalEvent) => {
console.log("click事件", conn, originalEvent);
});
//超过端点数量触发
jsPlumbInstanceRef.value.bind("onMaxConnections", (conn, originalEvent) => {
console.log("超过端点数量触发", conn, originalEvent);
});
问题汇总
Q1. 端点无法跟随窗口大小改变进行自适应
A1. 需要监听窗口大小变化,进行重绘,注意监听需要销毁
window.addEventListener('resize', () => {
jsPlumbInstanceRef.value.repaintEverything(); // 重绘
})
Q2. 页面上下滚动时,端点不跟随滚动
A2. 端点是通过绝对定位的方式,跟随上一级相对定位的盒子进行定位,需要给包裹的容器盒子添加属性 position: relative 进行相对定位
Q3. 通过addEndpoint 创建端点之后,再使用connect进行连线,导致端点重复生成
A3. connect进行连线会同时生成两个端点和一条线,如果之前已有端点,可以通过端点的uuid创建连线,防止重复生成端点。(本文例子便是这种方式)
Q4. 排序后,对应连线不跟随端点位置变化,进行更新
A4. 连线未跟随端点绑定,需要手动进行重绘
Q5. 原连线可以进行拖拽,通过更新ConnectionsDetachable:false设置为不可拖拽,但原有端点仍可进行拖拽
A5. 配置发生变化后,已生成端点不会更新状态,需要先全部断开,再重新划线 。
Q6. 删除源数据后,端点及连线没有跟随变化,仍停留在原有位置
A6. 重新设置实例,端点及连线