antv-x6实现拖拽图形连线,并一键布局

依赖下载

npm install --save @antv/layout @antv/x6 @antv/x6-plugin-clipboard @antv/x6-plugin-history @antv/x6-plugin-keyboard @antv/x6-plugin-selection @antv/x6-plugin-snapline @antv/x6-plugin-transform

封装js

import { Transform } from "@antv/x6-plugin-transform";
import { Selection } from "@antv/x6-plugin-selection";
import { Snapline } from "@antv/x6-plugin-snapline";
import { Keyboard } from "@antv/x6-plugin-keyboard";
import { Clipboard } from "@antv/x6-plugin-clipboard";
import { History } from "@antv/x6-plugin-history";

/* 给画布绑定插件 */
export const bindPlugin = (graph) => {
	graph
		.use(
			new Transform({
				resizing: true, //节点大小
				rotating: true, //节点旋转
			})
		)
		.use(
			new Selection({
				rubberband: true, //框选节点功能
				showNodeSelectionBox: true, //显示节点的选择框
			})
		)
		.use(
			new Snapline({
				resizing: true, //改变节点大小时触发对齐线
			})
		)
		.use(
			new Clipboard({
				useLocalStorage: false, //禁用复制后保存到本地
			})
		)
		.use(
			new Keyboard({
				enabled: true,
			})
		)
		.use(new History());
	return graph;
};

// 画布基本设置
export const configSetting = (Shape) => {
	return {
		grid: true, //网格
		autoResize: true, //自动缩放
		translating: { restrict: true }, //限制节点的移动范围
		/* 鼠标滚轮 */
		mousewheel: {
			enabled: true, //是否开启滚轮缩放交互
			zoomAtMousePosition: true, //将鼠标位置作为中心缩放
			modifiers: ["alt", "ctrl"], //修饰键
			minScale: 0.1, //最小缩放比例
			maxScale: 5, //最大缩放比例
		},
		/* 连线交互 */
		connecting: {
			snap: { radius: 20 }, //自动吸附
			/* 路径点 */
			router: {
				name: "metro", //智能地铁线路由,自动避开路径上的其他节点,无法使用manhattan
				args: {
					padding: 10, //锚点距离转角的最小距离
				},
			},
			/* 连线方式 */
			connector: {
				name: "jumpover", //跳线连接器
				args: {
					size: 10,
				},
			},
			anchor: "center", //锚点
			connectionPoint: "anchor", //连接点和锚点相同
			allowBlank: false, //连接到画布空白位置
			/* 连线的样式 */
			createEdge() {
				return new Shape.Edge({
					attrs: {
						line: {
							stroke: "#A2B1C3",
							strokeWidth: 2,
							targetMarker: {
								name: "block",
								width: 12,
								height: 8,
							},
						},
					},
					zIndex: 0,
				});
			},
			/* 移动边的时候判断连接是否有效 */
			validateConnection({ targetMagnet }) {
				return !!targetMagnet;
			},
		},
		// 添加小工具暂时没用
		// onToolItemCreated({ tool }) {
		// 	const handle = tool;
		// 	const options = handle.options;
		// 	if (options && options.index % 2 === 1) {
		// 		tool.setAttrs({ fill: "red" });
		// 	}
		// },
		/* 交互时的高亮 */
		highlighting: {
			/* 连接桩吸附连线时在连接桩外围围渲染一个包围框 */
			magnetAdsorbed: {
				name: "stroke",
				args: {
					attrs: {
						fill: "#fff",
						stroke: "#31d0c6",
						strokeWidth: 4,
					},
				},
			},
		},
	};
};

/**
 * 根据节点的类型返回不同的形状
 * @param {String} type 节点的类型
 * @description 通过传入的type===匹配 data里面设置的type获取到相应的节点设置内容
 * 							 编辑的时候也可以通过节点里面的data.type 获取到到底是什么节点进行响应设设置
 * 					defaultOval 0椭圆形
 * 					defaultSquare 1方形
 * 					defaultYSquare 2圆角矩形
 * 					defaultRhombus 3菱形
 *					defaultRhomboid 4平行四边形
 *					defaultCircle 5圆形
 *					otherImage 6图片
 */
export const configNodeShape = (type) => {
	const nodeShapeList = [
		{
			label: "椭圆形",
			data: {
				type: "defaultOval", //为了方便编辑的时候找到相对应的类型进行不同的编辑处理
			},
			shape: "rect",
			width: 100,
			height: 50,
			attrs: {
				body: {
					rx: 20,
					ry: 26,
					fill: "#fff",
					stroke: "#333",
				},
				label: {
					text: "椭圆形",
					fontSize: 16,
					fill: "#333",
				},
			},
		},
		{
			label: "方形",
			data: {
				type: "defaultSquare",
			},
			shape: "rect",
			width: 100,
			height: 50,
			attrs: {
				label: {
					text: "方形",
					fontSize: 16,
					fill: "#333",
				},
				body: {
					fill: "#fff",
					stroke: "#333",
				},
			},
		},
		{
			label: "圆角矩形",
			data: {
				type: "defaultYSquare",
			},
			shape: "rect",
			width: 100,
			height: 50,
			attrs: {
				body: {
					rx: 6,
					ry: 6,
					fill: "#fff",
					stroke: "#333",
				},
				label: {
					text: "圆角矩形",
					fontSize: 16,
					fill: "#333",
				},
			},
		},
		{
			label: "菱形",
			data: {
				type: "defaultRhombus",
			},
			shape: "polygon",
			width: 120,
			height: 50,
			attrs: {
				body: {
					refPoints: "0,10 10,0 20,10 10,20",
					fill: "#fff",
					stroke: "#333",
				},
				label: {
					text: "菱形",
					fontSize: 16,
					fill: "#333",
				},
			},
		},
		{
			label: "平行四边形",
			data: {
				type: "defaultRhomboid",
			},
			shape: "polygon",
			width: 120,
			height: 50,
			attrs: {
				body: {
					refPoints: "10,0 40,0 30,20 0,20",
					fill: "#fff",
					stroke: "#333",
				},
				label: {
					text: "平行四边形",
					fontSize: 16,
					fill: "#333",
				},
			},
		},
		{
			label: "圆形",
			data: {
				type: "defaultCircle",
			},
			shape: "circle",
			width: 80,
			height: 80,
			attrs: {
				label: {
					text: "圆形",
					fontSize: 16,
					fill: "#333",
				},
				body: {
					fill: "#fff",
					stroke: "#333",
				},
			},
		},
		{
			label: "图片",
			data: {
				type: "otherImage",
			},
			shape: "rect",
			width: 80,
			height: 80,
			markup: [
				{
					tagName: "rect",
					selector: "body",
				},
				{
					tagName: "image",
				},
				{
					tagName: "text",
					selector: "label",
				},
			],
			attrs: {
				body: {
					stroke: "#5F95FF",
					fill: "#5F95FF",
				},
				image: {
					width: 80,
					height: 80,
					refX: 0,
					refY: 0,
					xlinkHref: "https://gw.alipayobjects.com/zos/bmw-prod/2010ac9f-40e7-49d4-8c4a-4fcf2f83033b.svg",
				},
				label: {
					fontSize: 14,
					fill: "#fff",
					text: "图片",
				},
			},
		},
	];
	if (type) {
		const obj = nodeShapeList.find((item) => {
			return item.data.type === type;
		});
		return obj || nodeShapeList;
	}
	return nodeShapeList;
};

/* 连接柱的设置,默认隐藏,鼠标经过显示 */
export const configNodePorts = () => {
	return {
		groups: {
			top: {
				position: "top",
				attrs: {
					circle: {
						r: 4,
						magnet: true,
						stroke: "#5F95FF",
						strokeWidth: 1,
						fill: "#fff",
						style: {
							visibility: "hidden",
						},
					},
				},
			},
			right: {
				position: "right",
				attrs: {
					circle: {
						r: 4,
						magnet: true,
						stroke: "#5F95FF",
						strokeWidth: 1,
						fill: "#fff",
						style: {
							visibility: "hidden",
						},
					},
				},
			},
			bottom: {
				position: "bottom",
				attrs: {
					circle: {
						r: 4,
						magnet: true,
						stroke: "#5F95FF",
						strokeWidth: 1,
						fill: "#fff",
						style: {
							visibility: "hidden",
						},
					},
				},
			},
			left: {
				position: "left",
				attrs: {
					circle: {
						r: 4,
						magnet: true,
						stroke: "#5F95FF",
						strokeWidth: 1,
						fill: "#fff",
						style: {
							visibility: "hidden",
						},
					},
				},
			},
		},
		items: [
			{
				group: "top",
			},
			{
				group: "right",
			},
			{
				group: "bottom",
			},
			{
				group: "left",
			},
		],
	};
};

/**
 * 元素文字设置
 * @param {String} labelText 文字
 * @param {String} fontSize 文字大小
 * @param {String} fontFill 文字背景
 * @param {String} fill 元素填充
 * @param {String} stroke 元素边框
 */
export const configCell = (cellForm) => {
	return {
		label: {
			text: cellForm.labelText,
			fontSize: cellForm.fontSize,
			fill: cellForm.fontFill,
		},
		body: {
			fill: cellForm.fill,
			stroke: cellForm.stroke,
		},
	};
};
/**
 * 连接线上标签设置
 * @param {String} labelText 连接线文字
 * @param {String} fontColor 字体颜色
 * @param {String} fill 填充色
 * @param {String} stroke 线条颜色
 */
export const configEdgeLabel = (lineForm) => {
	if (!lineForm.label) return { attrs: { labelText: { text: "" }, labelBody: { fill: "", stroke: "" } } };
	return {
		markup: [
			{
				tagName: "rect",
				selector: "labelBody",
			},
			{
				tagName: "text",
				selector: "labelText",
			},
		],
		attrs: {
			labelText: {
				text: lineForm.label || "",
				fill: lineForm.fontColor || "#333",
				textAnchor: "middle",
				textVerticalAnchor: "middle",
			},
			labelBody: {
				ref: "labelText",
				refX: -8,
				refY: -5,
				refWidth: "100%",
				refHeight: "100%",
				refWidth2: 16,
				refHeight2: 10,
				stroke: lineForm.stroke || "#555",
				fill: lineForm.fill || "#fff",
				strokeWidth: 2,
				rx: 5,
				ry: 5,
			},
		},
	};
};

/**
 * 修改图片节点
 * @param {Object} imgForm 图片节点的配置数据
 */
export const configImageNode = (imgForm) => {
	return {
		label: {
			text: imgForm.labelText,
			fill: imgForm.labelFill,
		},
		body: {
			fill: imgForm.fill,
		},
		image: {
			"xlink:href": imgForm.xlinkHref,
			xlinkHref: imgForm.xlinkHref,
			width: imgForm.width,
			height: imgForm.height,
		},
	};
};

// 键盘事件
export const graphBindKey = (graph) => {
	// 复制
	graph.bindKey(["ctrl+c"], () => {
		const cells = graph.getSelectedCells();
		if (cells.length) {
			graph.copy(cells);
		}
		return false;
	});
	// 剪切
	graph.bindKey(["ctrl+x"], () => {
		const cells = graph.getSelectedCells();
		if (cells.length) {
			graph.cut(cells);
		}
		return false;
	});
	// 黏贴
	graph.bindKey(["ctrl+v"], () => {
		if (!graph.isClipboardEmpty()) {
			const cells = graph.paste({ offset: 32 });
			graph.cleanSelection();
			graph.select(cells);
		}
		return false;
	});
	// 上一步
	graph.bindKey(["ctrl+z"], () => {
		if (graph.canUndo()) {
			graph.undo();
		}
		return false;
	});
	// 下一步
	graph.bindKey(["ctrl+y"], () => {
		if (graph.canRedo()) {
			graph.redo();
		}
		return false;
	});
	// 全选
	graph.bindKey(["ctrl+a"], () => {
		const nodes = graph.getNodes();
		if (nodes) {
			graph.select(nodes);
		}
	});
	// 缩放
	graph.bindKey(["alt+1"], () => {
		const zoom = graph.zoom();
		if (zoom < 1.5) {
			graph.zoom(0.1);
		}
	});
	graph.bindKey(["alt+2"], () => {
		const zoom = graph.zoom();
		if (zoom > 0.5) {
			graph.zoom(-0.1);
		}
	});

	return graph;
};

// 链接桩的显示与隐藏,主要是照顾菱形
export const changePortsShow = (val) => {
	const container = document.getElementById("antvCanvasContent");
	const ports = container.querySelectorAll(".x6-port-body");
	for (let i = 0; i < ports.length; i = i + 1) {
		ports[i].style.visibility = val ? "visible" : "hidden";
	}
};

实际代码

<script setup>
import { ElMessage, ElMessageBox } from "element-plus";
import { Graph, Shape } from "@antv/x6";
import { bindPlugin, configSetting, configNodeShape, configNodePorts, configCell, configEdgeLabel, configImageNode, graphBindKey, changePortsShow } from "@/utils/antvSetting";
import { shallowReactive, ref, reactive, defineProps, onMounted } from "vue";
import { ElNotification } from "element-plus";
import { GridLayout } from "@antv/layout";

let graphNode = null, //图形
	isChange = ref(false), //是否有图形在右侧
	isPortsShow = ref(false), //链接桩常显
	isDrag = ref(false), //链接桩常显
	menuItem = reactive({}), //左侧选择的图形
	selectCell = {}, //选中的节点
	drawerInfo = reactive({
		isShow: false,
		title: "",
		type: 1,
	}), //右侧编辑抽屉
	form = reactive({}), //每个节点的内容
	labelForm = reactive({
		fontColor: "#333",
		fill: "#FFF",
		stroke: "#555",
	}); //连线标签的样式

defineProps({
	height: {
		type: String,
		default: "100vh", //'720px'
	},
	value: {
		type: String,
		default: "",
	},
});

// 对象置空
const deleteObj = (obj) => {
	Object.keys(obj).map((key) => {
		delete obj[key];
	});
};
// 初始化画布
const initGraph = () => {
	let graph = new Graph({
		container: document.getElementById("antvCanvasContent"),
		...configSetting(Shape),
		panning: {
			enabled: false,
			modifiers: ["shift"],
			eventTypes: "leftMouseDown",
		},
	});
	bindPlugin(graph); //画布绑定插件
	graphBindKey(graph); //画布绑定插件

	// 鼠标移入node节点显示连接柱
	graph.on("node:mouseenter", () => {
		changePortsShow(true);
	});
	graph.on("node:mouseleave", () => {
		if (isPortsShow.value) return;
		changePortsShow(false);
	});
	// 点击编辑
	graph.on("cell:click", ({ cell }) => {
		editForm(cell);
	});
	// 绑定删除事件
	graph.bindKey(["delete", "backspace"], () => {
		handlerDel();
	});
	// 画布有变化
	graph.on("cell:changed", () => {
		isChangeValue();
	});
	graphNode = graph;
};

// 切换是否可拖拽
const isOpenDrag = () => {
	graphNode.togglePanning(isDrag.value);
};
// 从左侧拖拽图形
const menuDrag = (type) => {
	deleteObj(menuItem);
	// 根据type获取到不同节点的预设参数
	Object.assign(menuItem, configNodeShape(type));
};
// 鼠标释放
const drop = (event) => {
	// 节点预设 ,添加位置信息和链接桩信息组合成完整的节点
	const nodeItem = {
		...menuItem,
		x: event.offsetX - menuItem.width / 2,
		y: event.offsetY - menuItem.height / 2,
		ports: configNodePorts(),
	};
	// 创建节点
	graphNode.addNode(nodeItem);
	isChangeValue();
};
// 画布是否有变动
const isChangeValue = () => {
	if (!isChange.value && graphNode.getCells().length) {
		isChange.value = true;
	}
	if (!graphNode.getCells().length) {
		isChange.value = false;
	}
};
// 点击编辑根据不同的内容获取编辑数据
const editForm = (cell) => {
	if (Object.keys(selectCell).length) selectCell.removeTools(); // 删除修改线的工具
	selectCell = cell;
	// 编辑node节点
	if (cell.isNode() && cell.data.type && cell.data.type.includes("default")) {
		drawerInfo.title = "编辑节点";
		drawerInfo.type = 1;
		const body = cell.attrs.body || cell.attrs.rect || cell.attrs.polygon || cell.attrs.circle;
		let data = {
			labelText: cell.attrs.label.text || "",
			fontSize: cell.attrs.label.fontSize || 14,
			fontFill: cell.attrs.label.fill || "",
			fill: body.fill || "",
			stroke: body.stroke || "",
		};
		deleteObj(form);
		Object.assign(form, data);
		drawerInfo.isShow = true;
	}
	// 编辑图片节点
	if (cell.isNode() && cell.data.type && cell.data.type === "otherImage") {
		drawerInfo.title = "编辑图片节点";
		drawerInfo.type = 2;
		const attrs = cell.attrs || { body: { fill: "" }, label: { text: "", fill: "" }, image: { xlinkHref: "", height: 80, width: 80 } };
		let data = {
			fill: attrs.body.fill,
			labelText: attrs.label.text,
			labelFill: attrs.label.fill,
			height: (attrs.image && attrs.image.height) || 80,
			width: (attrs.image && attrs.image.width) || 80,
			xlinkHref: (attrs.image && (attrs.image.xlinkHref || attrs.image["xlink:href"])) || "https://gw.alipayobjects.com/zos/bmw-prod/2010ac9f-40e7-49d4-8c4a-4fcf2f83033b.svg",
		};
		deleteObj(form);
		Object.assign(form, data);
		drawerInfo.isShow = true;
	}
	// 编辑连线
	if (!cell.isNode() && cell.shape === "edge") {
		drawerInfo.title = "编辑连线";
		drawerInfo.type = 3;
		let data = {
			label: cell.labels && cell.labels[0] ? cell.labels[0].attrs.labelText.text : "",
			stroke: cell.attrs.line.stroke || "",
			connector: "normal",
			strokeWidth: cell.attrs.line.strokeWidth || "",
			isArrows: cell.attrs.line.sourceMarker ? true : false,
			isAnit: cell.attrs.line.strokeDasharray ? true : false,
			isTools: false,
		};
		deleteObj(form);
		Object.assign(form, data);
		// 看连线上是否有label
		const edgeCellLabel = (cell.labels && cell.labels[0] && cell.labels[0].attrs) || false;
		if (form.label && edgeCellLabel) {
			labelForm = shallowReactive({
				fontColor: edgeCellLabel.labelText.fill || "#333",
				fill: edgeCellLabel.labelBody.fill || "#fff",
				stroke: edgeCellLabel.labelBody.stroke || "#555",
			});
		} else {
			labelForm = shallowReactive({ fontColor: "#333", fill: "#FFF", stroke: "#555" });
		}
		drawerInfo.isShow = true;
	}
};
// 关闭右侧
const closeEditForm = () => {
	drawerInfo.isShow = false;
	if (Object.keys(selectCell).length) selectCell.removeTools();
};
// 修改一般节点
const changeNode = () => {
	selectCell.setAttrs(configCell(form));
};
// 修改图片节点
const changeImageNode = () => {
	selectCell.setAttrs(configImageNode(form));
};
// 修改边label属性
const changeEdgeLabel = () => {
	selectCell.setLabels([
		configEdgeLabel({
			label: form.label,
			fontColor: labelForm.fontColor,
			fill: labelForm.fill,
			stroke: labelForm.stroke,
		}),
	]);
	if (!form.label) labelForm = shallowReactive({ fontColor: "#333", fill: "#FFF", stroke: "#555" });
};
// 修改边的颜色
const changeEdgeStroke = (val) => {
	selectCell.setAttrs({ line: { stroke: val } });
};
// 边的样式
const changeEdgeConnector = (val) => {
	switch (val) {
		case "normal":
			selectCell.setConnector(val);
			break;
		case "smooth":
			selectCell.setConnector(val);
			break;
		case "rounded":
			selectCell.setConnector(val, { radius: 20 });
			break;
		case "jumpover":
			selectCell.setConnector(val, { radius: 20 });
			break;
	}
};
// 边的宽度
const changeEdgeStrokeWidth = (val) => {
	// 判断是否为双向箭头
	if (form.isArrows) {
		selectCell.attr({
			line: {
				strokeWidth: val,
				sourceMarker: {
					width: 12 * (val / 2) || 12,
					height: 8 * (val / 2) || 8,
				},
				targetMarker: {
					width: 12 * (val / 2) || 12,
					height: 8 * (val / 2) || 8,
				},
			},
		});
	} else {
		selectCell.attr({
			line: {
				strokeWidth: val,
				targetMarker: {
					width: 12 * (val / 2) || 12,
					height: 8 * (val / 2) || 8,
				},
			},
		});
	}
};
// 双向箭头
const changeEdgeArrows = (val) => {
	if (val) {
		selectCell.attr({
			line: {
				sourceMarker: {
					name: "block",
					width: 12 * (form.strokeWidth / 2) || 12,
					height: 8 * (form.strokeWidth / 2) || 8,
				},
				targetMarker: {
					name: "block",
					width: 12 * (form.strokeWidth / 2) || 12,
					height: 8 * (form.strokeWidth / 2) || 8,
				},
			},
		});
	} else {
		selectCell.attr({
			line: {
				sourceMarker: "",
				targetMarker: {
					name: "block",
					size: 10 * (form.strokeWidth / 2) || 10,
				},
			},
		});
	}
};
// 边修改为流动虚线
const changeEdgeAnit = (val) => {
	if (val) {
		selectCell.attr({ line: { strokeDasharray: 5, style: { animation: "ant-line 30s infinite linear" } } });
	} else {
		selectCell.attr({ line: { strokeDasharray: 0, style: { animation: "" } } });
	}
};
// 给线添加调节工具
const changeEdgeTools = (val) => {
	if (val) selectCell.addTools(["vertices", "segments"]);
	else selectCell.removeTools();
};
// 删除节点
const handlerDel = () => {
	ElMessageBox.confirm(`此操作将永久删除此${drawerInfo.type == 1 ? "节点" : "连线"}, 是否继续?`, "提示", {
		confirmButtonText: "确定",
		cancelButtonText: "取消",
		type: "warning",
	})
		.then(() => {
			const cells = graphNode.getSelectedCells();
			if (cells.length) {
				graphNode.removeCells(cells);
				deleteObj(form);
				drawerInfo.isShow = false;
				isChangeValue();
				ElMessage({ type: "success", message: "删除成功" });
			}
		})
		.catch(() => {});
};
// 布局
const layout = () => {
	const gridLayout = new GridLayout({
		type: "grid",
		width: 738,
		sortBy: "label",
		cols: 8,
	});
	const { cells } = graphNode.toJSON();
	let data = {
		nodes: [],
		edges: [],
	};
	for (const i of cells) {
		if (i.shape == "edge") {
			data.edges.push(i);
		} else {
			data.nodes.push(i);
		}
	}
	const model = gridLayout.layout(data);
	graphNode.fromJSON(model);
};
// 导出
const handlerSend = () => {
	const { cells } = graphNode.toJSON();

	const tempGroupJson = cells.map((item) => {
		if (item.ports && item.ports.groups) delete item.ports.groups;
		if (item.tools) delete item.tools;
		return item;
	});
	if (selectCell) {
		selectCell.removeTools();
		selectCell = "";
	}
	console.log(tempGroupJson);
	// this.$emit("finish", JSON.stringify(tempGroupJson));
	// console.log(JSON.stringify(tempGroupJson));
};
// 保存数据
const handlerData = () => {
	let data = selectCell.getData();
	let form = {
		time: new Date(),
		info: "业务数据",
	};
	selectCell.setData({ ...data, ...form });
	console.log(selectCell.getData());
};

const openTip = () => {
	ElNotification({
		title: "快捷键提示",
		dangerouslyUseHTMLString: true,
		message: `ctrl+c&nbsp;复制&nbsp;&nbsp;ctrl+x&nbsp;剪切&nbsp;&nbsp;ctrl+v&nbsp;黏贴&nbsp;&nbsp;ctrl+z&nbsp;后退&nbsp;&nbsp;ctrl+y&nbsp;前进&nbsp;&nbsp;ctrl+a&nbsp;全选`,
		duration: 0,
		type: "success",
		position: "bottom-left",
	});
};
onMounted(() => {
	initGraph();
});
</script>
<template>
	<div class="antv-content">
		<div class="antv-menu">
			<p class="menu-title">
				基础图形列表
				<svg @click="openTip" width="18px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-ea893728="">
					<path
						fill="currentColor"
						d="M512 64a448 448 0 1 1 0 896.064A448 448 0 0 1 512 64zm67.2 275.072c33.28 0 60.288-23.104 60.288-57.344s-27.072-57.344-60.288-57.344c-33.28 0-60.16 23.104-60.16 57.344s26.88 57.344 60.16 57.344zM590.912 699.2c0-6.848 2.368-24.64 1.024-34.752l-52.608 60.544c-10.88 11.456-24.512 19.392-30.912 17.28a12.992 12.992 0 0 1-8.256-14.72l87.68-276.992c7.168-35.136-12.544-67.2-54.336-71.296-44.096 0-108.992 44.736-148.48 101.504 0 6.784-1.28 23.68.064 33.792l52.544-60.608c10.88-11.328 23.552-19.328 29.952-17.152a12.8 12.8 0 0 1 7.808 16.128L388.48 728.576c-10.048 32.256 8.96 63.872 55.04 71.04 67.84 0 107.904-43.648 147.456-100.416z"
					></path>
				</svg>
			</p>
			<ul class="menu-list">
				<li draggable="true" @drag="menuDrag('defaultOval')"><i class="icon-oval"></i> <span>椭圆形</span></li>
				<li draggable="true" @drag="menuDrag('defaultSquare')"><i class="icon-square"></i><span>矩形</span></li>
				<li draggable="true" @drag="menuDrag('defaultYSquare')"><i class="icon-ysquare"></i><span>圆角矩形</span></li>
				<li draggable="true" @drag="menuDrag('defaultRhombus')"><i class="icon-rhombus"></i><span>菱形</span></li>
				<li draggable="true" @drag="menuDrag('defaultRhomboid')"><i class="icon-rhomboid"></i><span>平行四边形</span></li>
				<li draggable="true" @drag="menuDrag('defaultCircle')"><i class="icon-circle"></i><span>圆形</span></li>
				<li draggable="true" @drag="menuDrag('otherImage')">
					<svg width="18px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
						<path fill="currentColor" d="M160 160v704h704V160H160zm-32-64h768a32 32 0 0 1 32 32v768a32 32 0 0 1-32 32H128a32 32 0 0 1-32-32V128a32 32 0 0 1 32-32z"></path>
						<path
							fill="currentColor"
							d="M384 288q64 0 64 64t-64 64q-64 0-64-64t64-64zM185.408 876.992l-50.816-38.912L350.72 556.032a96 96 0 0 1 134.592-17.856l1.856 1.472 122.88 99.136a32 32 0 0 0 44.992-4.864l216-269.888 49.92 39.936-215.808 269.824-.256.32a96 96 0 0 1-135.04 14.464l-122.88-99.072-.64-.512a32 32 0 0 0-44.8 5.952L185.408 876.992z"
						></path>
					</svg>
					<span>图片</span>
				</li>
			</ul>
			<div class="save-btn" v-if="isChange">
				<el-button type="success" @click="handlerSend">保存当前方案</el-button>
				<el-button type="success" @click="layout">布局</el-button>
			</div>
		</div>
		<div class="antv-canvas">
			<div class="canvas-content" :style="{ height: height }" id="antvCanvasContent" @drop="drop($event)" @dragover.prevent></div>
			<div class="canvas-tips">
				<div class="canvas-tips-item">
					<el-switch v-model="isPortsShow" @change="changePortsShow"></el-switch>
					<span>链接桩常显</span>
				</div>
				<div class="canvas-tips-item">
					<el-switch v-model="isDrag" @change="isOpenDrag"></el-switch>
					<span>开启拖拽(按住shift在空白区域拖拽)</span>
				</div>
			</div>
		</div>
		<div class="antv-edit el-drawer rtl" v-if="drawerInfo.isShow">
			<header class="el-drawer__header">
				<span class="el-drawer__title">{{ drawerInfo.title }}</span>
				<button class="el-drawer__close-btn" type="button" @click="closeEditForm">
					<i class="el-icon el-drawer__close">
						<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
							<path
								fill="currentColor"
								d="M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z"
							></path>
						</svg>
					</i>
				</button>
			</header>
			<el-divider />
			<div class="el-drawer__body">
				<div v-if="drawerInfo.type == 1">
					<el-form ref="nodeForm" :model="form" label-width="80px">
						<el-form-item label="节点文本">
							<el-input v-model="form.labelText" size="small" @input="changeNode('labelText', form.labelText)"></el-input>
						</el-form-item>
						<el-form-item label="字体大小">
							<el-input v-model="form.fontSize" size="small" @input="changeNode('fontSize', form.fontSize)"></el-input>
						</el-form-item>
						<el-form-item label="字体颜色">
							<el-color-picker v-model="form.fontFill" @change="changeNode('fontFill', form.fontFill)"></el-color-picker>
						</el-form-item>
						<el-form-item label="节点背景">
							<el-color-picker v-model="form.fill" @change="changeNode('fill', form.fill)"></el-color-picker>
						</el-form-item>
						<el-form-item label="边框颜色">
							<el-color-picker v-model="form.stroke" @change="changeNode('stroke', form.stroke)"></el-color-picker>
						</el-form-item>
						<div class="see-box">
							<h5>预览</h5>
							<div class="see-item" :style="{ background: form.fill, color: form.fontFill, 'border-color': form.stroke, 'font-size': form.fontSize + 'px' }">{{ form.labelText }}</div>
						</div>
					</el-form>
				</div>
				<div v-if="drawerInfo.type == 2">
					<el-form ref="imageForm" :model="form" label-width="80px">
						<el-form-item label="节点文本">
							<el-input v-model="form.labelText" size="small" @input="changeImageNode('labelText', form.labelText)"></el-input>
						</el-form-item>
						<el-form-item label="字体颜色">
							<el-color-picker v-model="form.labelFill" @change="changeImageNode('labelFill', form.labelFill)"></el-color-picker>
						</el-form-item>
						<el-form-item label="节点背景">
							<el-color-picker v-model="form.fill" @change="changeImageNode('fill', form.fill)"></el-color-picker>
						</el-form-item>
						<el-form-item label="图片地址">
							<el-input type="textarea" autosize v-model="form.xlinkHref" size="small" placeholder="图片地址" @input="changeImageNode('xlinkHref', form.xlinkHref)"></el-input>
							<el-image :preview-src-list="[form.xlinkHref]" :src="form.xlinkHref" style="width: 80px; height: 80px; background: #f2f2f2" fit="fill"></el-image>
						</el-form-item>
						<el-form-item label="图片尺寸">
							<div>
								<span style="font-size: 14px; padding-right: 5px; color: #888">宽</span>
								<el-input-number v-model="form.width" :min="0" label="宽" size="small" @change="changeImageNode('width', form.width)"></el-input-number>
							</div>

							<div>
								<span style="font-size: 14px; padding-right: 5px; color: #888">高</span>
								<el-input-number v-model="form.height" :min="0" label="高" size="small" @change="changeImageNode('height', form.height)"></el-input-number>
							</div>
						</el-form-item>
					</el-form>
				</div>
				<div v-if="drawerInfo.type == 3">
					<el-form ref="edgeForm" :model="form" label-width="80px">
						<el-form-item label="标签内容">
							<el-input v-model="form.label" size="small" placeholder="标签文字,空则没有" @input="changeEdgeLabel()"></el-input>
							<div v-if="form.label" class="label-style">
								<p>字体颜色:<el-color-picker v-model="labelForm.fontColor" size="small" @change="changeEdgeLabel()"></el-color-picker></p>
								<p>背景颜色:<el-color-picker v-model="labelForm.fill" size="small" @change="changeEdgeLabel()"></el-color-picker></p>
								<p>描边颜色:<el-color-picker v-model="labelForm.stroke" size="small" @change="changeEdgeLabel()"></el-color-picker></p>
							</div>
						</el-form-item>
						<el-form-item label="线条颜色">
							<el-color-picker v-model="form.stroke" size="small" @change="changeEdgeStroke"></el-color-picker>
						</el-form-item>
						<el-form-item label="线条样式">
							<el-select v-model="form.connector" size="small" placeholder="请选择" @change="changeEdgeConnector">
								<el-option label="直角" value="normal"></el-option>
								<el-option label="圆角" value="rounded"></el-option>
								<el-option label="平滑" value="smooth"></el-option>
								<el-option label="跳线(两线交叉)" value="jumpover"></el-option>
							</el-select>
						</el-form-item>
						<el-form-item label="线条宽度">
							<el-input-number v-model="form.strokeWidth" size="small" @change="changeEdgeStrokeWidth" :min="2" :step="2" :max="6" label="线条宽度"></el-input-number>
						</el-form-item>
						<el-form-item label="双向箭头">
							<el-switch v-model="form.isArrows" @change="changeEdgeArrows"></el-switch>
						</el-form-item>
						<el-form-item label="流动线条">
							<el-switch v-model="form.isAnit" @change="changeEdgeAnit"></el-switch>
						</el-form-item>
						<el-form-item label="调整线条">
							<el-switch v-model="form.isTools" @change="changeEdgeTools"></el-switch>
						</el-form-item>
					</el-form>
				</div>
				<div class="edit-btn">
					<el-row :gutter="20">
						<el-col :span="12">
							<el-button type="primary" @click="handlerData" style="width: 100%">保存数据</el-button>
						</el-col>
						<el-col :span="12">
							<el-button type="danger" @click="handlerDel" style="width: 100%">删除此{{ drawerInfo.type == 1 ? "节点" : "连线" }}</el-button>
						</el-col>
					</el-row>
				</div>
			</div>
		</div>
	</div>
</template>

<style>
@keyframes ant-line {
	from {
		stroke-dashoffset: 100%;
	}
	to {
		stroke-dashoffset: 0%;
	}
}
* {
	margin: 0;
	padding: 0;
}
html,
body,
#app {
	width: 100%;
	height: 100%;
}
.modal-position {
	position: unset !important;
}
</style>
<style lang="less" scoped>
.antv-content {
	height: 100%;
	background: #fff;
	display: flex;
	overflow: hidden;
	position: relative;
	.antv-menu {
		width: 200px;
		border-right: 1px solid #d5d5d5;
		padding: 12px;
		flex-shrink: 0;
		.menu-title {
			display: flex;
			svg {
				cursor: pointer;
			}
		}
		li {
			padding: 10px;
			border-radius: 8px;
			border: 1px solid #555;
			background: #fff;
			margin: 8px 12px;
			font-size: 12px;
			display: flex;
			align-items: center;
			cursor: pointer;
			transition: all 0.5s ease;
			&:hover {
				box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
			}
			i,
			svg {
				font-size: 18px;
				margin-right: 10px;
			}
		}
	}
	.antv-canvas {
		flex: 1;
		position: relative;
		.canvas-content {
			position: relative;
			height: 100vh;
			min-height: 100vh;
		}
		.canvas-tips {
			padding: 10px;
			display: flex;
			align-items: center;
			position: absolute;
			top: 0;
			left: 0;
			.canvas-tips-item {
				margin-right: 10px;
				span {
					padding-left: 10px;
					font-size: 12px;
				}
			}
		}
	}

	.antv-edit {
		width: 300px;
		.el-drawer__header {
			margin-bottom: 0;
		}
		.el-drawer__body {
			padding-top: 0;
		}
		.see-box {
			padding: 20px;
			background: #f2f2f2;
			h5 {
				padding-bottom: 10px;
			}
			.see-item {
				padding: 10px 30px;
				border: 2px solid #333;
				text-align: center;
			}
		}
		.edit-btn {
			margin-top: 14px;
		}
	}
}

i.icon-oval {
	display: inline-block;
	width: 16px;
	height: 10px;
	border-radius: 10px;
	border: 2px solid #555;
}
i.icon-square {
	display: inline-block;
	width: 16px;
	height: 10px;
	border: 2px solid #555;
}
i.icon-ysquare {
	display: inline-block;
	width: 16px;
	height: 10px;
	border-radius: 4px;
	border: 2px solid #555;
}
i.icon-rhombus {
	display: inline-block;
	width: 10px;
	height: 10px;
	border: 2px solid #555;
	transform: rotate(45deg);
}
i.icon-rhomboid {
	display: inline-block;
	width: 10px;
	height: 10px;
	border: 2px solid #555;
	transform: skew(-30deg);
}
i.icon-circle {
	display: inline-block;
	width: 16px;
	height: 16px;
	border-radius: 16px;
	border: 2px solid #555;
}
</style>

posted @ 2025-03-21 10:46  火炬冬天  阅读(692)  评论(0)    收藏  举报