hookehuyr

自定义节点锚点

1 +.sql {
2 + .table-container {
3 + box-sizing: border-box;
4 + padding: 10px;
5 + }
6 +
7 + .table-node {
8 + width: 100%;
9 + height: 100%;
10 + overflow: hidden;
11 + background: #fff;
12 + border-radius: 4px;
13 + box-shadow: 0 1px 3px rgb(0 0 0 / 30%);
14 + }
15 +
16 + .table-node::before {
17 + display: block;
18 + width: 100%;
19 + height: 8px;
20 + background: #d79b00;
21 + content: '';
22 + }
23 +
24 + .table-node.table-color-1::before {
25 + background: #9673a6;
26 + }
27 +
28 + .table-node.table-color-2::before {
29 + background: #dae8fc;
30 + }
31 +
32 + .table-node.table-color-3::before {
33 + background: #82b366;
34 + }
35 +
36 + .table-node.table-color-4::before {
37 + background: #f8cecc;
38 + }
39 +
40 + .table-name {
41 + height: 28px;
42 + font-size: 14px;
43 + line-height: 28px;
44 + text-align: center;
45 + background: #f5f5f5;
46 + }
47 +
48 + .table-felid {
49 + display: flex;
50 + justify-content: space-between;
51 + height: 24px;
52 + padding: 0 10px;
53 + font-size: 12px;
54 + line-height: 24px;
55 + }
56 +
57 + .felid-type {
58 + color: #9f9c9f;
59 + }
60 +
61 + /* 自定义锚点样式 */
62 +
63 + .custom-anchor {
64 + cursor: crosshair;
65 + fill: #d9d9d9;
66 + stroke: #999;
67 + stroke-width: 1;
68 + rx: 3;
69 + ry: 3;
70 + }
71 +
72 + .custom-anchor:hover {
73 + fill: #ff7f0e;
74 + stroke: #ff7f0e;
75 + }
76 +
77 + .lf-node-not-allow .custom-anchor:hover {
78 + cursor: not-allowed;
79 + fill: #d9d9d9;
80 + stroke: #999;
81 + }
82 +
83 + .incoming-anchor {
84 + stroke: #d79b00;
85 + }
86 +
87 + .outgoing-anchor {
88 + stroke: #82b366;
89 + }
90 +}
1 +<!--
2 + * @Date: 2025-03-10 16:52:35
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-12 23:36:42
5 + * @FilePath: /logic-flow2/src/views/adv-node/anchor/index.vue
6 + * @Description: 自定义锚点
7 +-->
8 +<template>
9 + <div class="helloworld-app sql container">
10 + <div ref="container" class="app-content flow-container"></div>
11 + <button @click="handleAddField">Users添加字段</button>
12 + </div>
13 +</template>
14 +
15 +<script setup>
16 +import LogicFlow from "@logicflow/core";
17 +import sqlNode from "./sqlNode";
18 +import sqlEdge from "./sqlEdge";
19 +
20 +import data from "./sqlData";
21 +import "./index.less";
22 +
23 +const container = ref(null);
24 +let lf = null;
25 +
26 +// 添加字段的处理函数
27 +const handleAddField = () => {
28 + const nodeModel = lf.getNodeModelById("node_id_1");
29 + if (typeof nodeModel?.addField === "function") {
30 + nodeModel.addField({
31 + key: Math.random().toString(36).substring(2, 7),
32 + type: ["integer", "long", "string", "boolean"][Math.floor(Math.random() * 4)],
33 + });
34 + }
35 +};
36 +
37 +onMounted(() => {
38 + lf = new LogicFlow({
39 + container: container.value,
40 + grid: true,
41 + });
42 +
43 + lf.register(sqlNode);
44 + lf.register(sqlEdge);
45 +
46 + lf.setDefaultEdgeType("sql-edge");
47 + lf.setTheme({
48 + bezier: {
49 + stroke: "#afafaf",
50 + strokeWidth: 1,
51 + },
52 + });
53 +
54 + lf.render(data);
55 + lf.translateCenter();
56 +
57 + // 1.1.28新增,可以自定义锚点显示时机了
58 + lf.on("anchor:dragstart", ({ data, nodeModel }) => { // 当开始拖拽某个节点的锚点时
59 + console.log("dragstart", data);
60 + if (nodeModel.type === "sql-node") { // 如果拖拽的是 sql-node 类型的节点
61 + lf.graphModel.nodes.forEach((node) => {
62 + // 找到其他的 sql-node 节点(排除当前正在拖拽的节点)
63 + console.warn(node);
64 +
65 + if (node.type === "sql-node" && nodeModel.id !== node.id) {
66 + node.isShowAnchor = true; // 显示这些节点的锚点
67 + node.setProperties({
68 + isConnection: true, // 设置连接状态属性
69 + });
70 + }
71 + });
72 + }
73 + });
74 + lf.on("anchor:dragend", ({ data, nodeModel }) => {
75 + console.log("dragend", data);
76 + if (nodeModel.type === "sql-node") {
77 + lf.graphModel.nodes.forEach((node) => {
78 + if (node.type === "sql-node" && nodeModel.id !== node.id) {
79 + node.isShowAnchor = false;
80 + lf.deleteProperty(node.id, "isConnection");
81 + }
82 + });
83 + }
84 + });
85 +});
86 +</script>
87 +
88 +<style scoped>
89 +.container {
90 + width: 100vw;
91 + height: 100vh;
92 + display: flex;
93 + flex-direction: column;
94 +}
95 +
96 +.flow-container {
97 + flex: 1;
98 + width: 100%;
99 + height: 100%;
100 +}
101 +</style>
1 +/*
2 + * @Date: 2025-03-12 21:23:48
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-12 23:15:14
5 + * @FilePath: /logic-flow2/src/views/adv-node/anchor/sqlData.js
6 + * @Description: 文件描述
7 + */
8 +const data = {
9 + nodes: [
10 + {
11 + id: 'node_id_1',
12 + type: 'sql-node',
13 + x: 100,
14 + y: 100,
15 + properties: {
16 + tableName: 'Users',
17 + fields: [
18 + {
19 + key: 'id',
20 + type: 'string',
21 + },
22 + {
23 + key: 'name',
24 + type: 'string',
25 + },
26 + {
27 + key: 'age',
28 + type: 'integer',
29 + },
30 + ],
31 + },
32 + },
33 + {
34 + id: 'node_id_2',
35 + type: 'sql-node',
36 + x: 400,
37 + y: 200,
38 + properties: {
39 + tableName: 'Settings',
40 + fields: [
41 + {
42 + key: 'id',
43 + type: 'string',
44 + },
45 + {
46 + key: 'key',
47 + type: 'integer',
48 + },
49 + {
50 + key: 'value',
51 + type: 'string',
52 + },
53 + ],
54 + },
55 + },
56 + {
57 + id: 'node_id_3',
58 + type: 'sql-node',
59 + x: 400,
60 + y: 400,
61 + properties: {
62 + tableName: 'Settings',
63 + fields: [
64 + {
65 + key: 'id',
66 + type: 'string',
67 + },
68 + {
69 + key: 'key',
70 + type: 'integer',
71 + },
72 + {
73 + key: 'value',
74 + type: 'string',
75 + },
76 + ],
77 + },
78 + },
79 + ],
80 + edges: [],
81 +};
82 +export default data;
1 +/*
2 + * @Date: 2025-03-12 21:20:42
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-12 23:53:01
5 + * @FilePath: /logic-flow2/src/views/adv-node/anchor/sqlEdge.js
6 + * @Description: 文件描述
7 + */
8 +import { BezierEdge, BezierEdgeModel } from '@logicflow/core';
9 +
10 +class CustomEdge2 extends BezierEdge {} // 继承贝塞尔曲线边的视图类
11 +
12 +class CustomEdgeModel2 extends BezierEdgeModel { // 继承贝塞尔曲线边的模型类
13 + getEdgeStyle() {
14 + const style = super.getEdgeStyle();
15 + // svg属性
16 + style.strokeWidth = 1;
17 + style.stroke = '#ababac';
18 + return style;
19 + }
20 +
21 + /**
22 + * 重写此方法,使保存数据是能带上锚点数据。
23 + */
24 + getData() {
25 + const data = super.getData();
26 + // 保存源节点和目标节点的锚点ID,用于后续恢复连线
27 + data.sourceAnchorId = this.sourceAnchorId;
28 + data.targetAnchorId = this.targetAnchorId;
29 + return data;
30 + }
31 +
32 + /**
33 + * 给边自定义方案,使其支持基于锚点的位置更新边的路径
34 + */
35 + updatePathByAnchor() {
36 + // 获取源节点和其锚点
37 + const sourceNodeModel = this.graphModel.getNodeModelById(this.sourceNodeId);
38 + const sourceAnchor = sourceNodeModel
39 + ?.getDefaultAnchor()
40 + .find((anchor) => anchor.id === this.sourceAnchorId);
41 +
42 + // 获取目标节点和其锚点
43 + const targetNodeModel = this.graphModel.getNodeModelById(this.targetNodeId);
44 + const targetAnchor = targetNodeModel
45 + ?.getDefaultAnchor()
46 + .find((anchor) => anchor.id === this.targetAnchorId);
47 +
48 + // 更新连线起点
49 + if (sourceAnchor) {
50 + const startPoint = {
51 + x: sourceAnchor?.x,
52 + y: sourceAnchor?.y,
53 + };
54 + this.updateStartPoint(startPoint);
55 + }
56 +
57 + // 更新连线终点
58 + if (targetAnchor) {
59 + const endPoint = {
60 + x: targetAnchor?.x,
61 + y: targetAnchor?.y,
62 + };
63 + this.updateEndPoint(endPoint);
64 + }
65 +
66 + // 这里需要将原有的pointsList设置为空,才能触发bezier的自动计算control点。
67 + this.pointsList = [];
68 + this.initPoints();
69 + }
70 +}
71 +
72 +export default {
73 + type: 'sql-edge',
74 + view: CustomEdge2,
75 + model: CustomEdgeModel2,
76 +};
1 +import { HtmlNode, HtmlNodeModel, h } from '@logicflow/core'
2 +
3 +class SqlNode extends HtmlNode {
4 + /**
5 + * 1.1.7版本后支持在view中重写锚点形状。
6 + * 重写锚点新增
7 + */
8 + // 自定义锚点形状
9 + getAnchorShape(anchorData) {
10 + const { x, y, type } = anchorData
11 + // 将锚点渲染为矩形,左右锚点使用不同的样式类
12 + return h('rect', {
13 + x: x - 5,
14 + y: y - 5,
15 + width: 10,
16 + height: 10,
17 + className: `custom-anchor ${
18 + type === 'left' ? 'incoming-anchor' : 'outgoing-anchor'
19 + }`,
20 + })
21 + }
22 +
23 + setHtml(rootEl) { // 渲染节点的 HTML 内容
24 + rootEl.innerHTML = ''
25 + const {
26 + properties: { fields, tableName },
27 + } = this.props.model
28 + rootEl.setAttribute('class', 'table-container')
29 + const container = document.createElement('div')
30 + container.className = `table-node table-color-${Math.ceil(
31 + Math.random() * 4,
32 + )}`
33 + const tableNameElement = document.createElement('div')
34 + tableNameElement.innerText = tableName
35 + tableNameElement.className = 'table-name'
36 + container.appendChild(tableNameElement)
37 + const fragment = document.createDocumentFragment()
38 + for (let i = 0; i < fields.length; i++) {
39 + const item = fields[i]
40 + const itemElement = document.createElement('div')
41 + itemElement.className = 'table-felid'
42 + const itemKey = document.createElement('span')
43 + itemKey.innerText = item.key
44 + const itemType = document.createElement('span')
45 + itemType.innerText = item.type
46 + itemType.className = 'felid-type'
47 + itemElement.appendChild(itemKey)
48 + itemElement.appendChild(itemType)
49 + fragment.appendChild(itemElement)
50 + }
51 + container.appendChild(fragment)
52 + rootEl.appendChild(container)
53 + }
54 +}
55 +
56 +class SqlNodeModel extends HtmlNodeModel {
57 + /**
58 + * 给model自定义添加字段方法
59 + */
60 + addField(item) {
61 + this.properties.fields.unshift(item) // 在字段列表开头添加新字段
62 + this.setAttributes() // 更新节点尺寸
63 + // 为了保持节点顶部位置不变,在节点变化后,对节点进行一个位移,位移距离为添加高度的一半。
64 + this.move(0, 24 / 2) // 调整节点位置保持顶部对齐
65 + // 更新节点连接边的path
66 + this.incoming.edges.forEach((edge) => {
67 + // 调用自定义的更新方案
68 + edge.updatePathByAnchor()
69 + })
70 + this.outgoing.edges.forEach((edge) => {
71 + // 调用自定义的更新方案
72 + edge.updatePathByAnchor()
73 + })
74 + }
75 +
76 + getOutlineStyle() {
77 + const style = super.getOutlineStyle()
78 + style.stroke = 'none' // 移除选中外框
79 + style.hover.stroke = 'none' // 设置节点轮廓为透明
80 + return style
81 + }
82 +
83 + // 如果不用修改锚地形状,可以重写颜色相关样式
84 + getAnchorStyle(anchorInfo) {
85 + const style = super.getAnchorStyle()
86 + if (anchorInfo.type === 'left') { // 左侧锚点红色(输入)
87 + style.fill = 'red'
88 + style.hover.fill = 'transparent'
89 + style.hover.stroke = 'transparent'
90 + style.className = 'lf-hide-default'
91 + } else { // 右侧锚点绿色(输出)
92 + style.fill = 'green'
93 + }
94 + return style
95 + }
96 +
97 + setAttributes() { // 设置节点属性
98 + this.width = 200; // 设置节点宽度为 200
99 + const {
100 + properties: { fields },
101 + } = this
102 + this.height = 60 + fields.length * 24; // 根据字段数量计算高度
103 + // 添加连线规则:
104 + // 1. 只能从右侧锚点连出
105 + // 2. 只能连接到左侧锚点
106 + const circleOnlyAsTarget = {
107 + message: '只允许从右边的锚点连出',
108 + validate: (sourceNode, targetNode, sourceAnchor) => {
109 + return sourceAnchor.type === 'right'
110 + },
111 + }
112 + this.sourceRules.push(circleOnlyAsTarget)
113 + this.targetRules.push({
114 + message: '只允许连接左边的锚点',
115 + validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => {
116 + return targetAnchor.type === 'left'
117 + },
118 + })
119 + }
120 +
121 + getDefaultAnchor() { // 获取默认锚点
122 + const {
123 + id,
124 + x,
125 + y,
126 + width,
127 + height,
128 + isHovered,
129 + isSelected,
130 + properties: { fields, isConnection },
131 + } = this
132 + const anchors = []
133 + fields.forEach((felid, index) => {
134 + // 如果是连出,就不显示左边的锚点
135 + if (isConnection || !(isHovered || isSelected)) {
136 + anchors.push({
137 + x: x - width / 2 + 10,
138 + y: y - height / 2 + 60 + index * 24,
139 + id: `${id}_${felid.key}_left`,
140 + edgeAddable: false, // 设置左侧锚点不能作为连线起点, 自定义锚点支持设置edgeAddable属性,用于控制是否可以在此锚点手动创建连线。
141 + type: 'left',
142 + })
143 + }
144 + if (!isConnection) {
145 + anchors.push({
146 + x: x + width / 2 - 10,
147 + y: y - height / 2 + 60 + index * 24,
148 + id: `${id}_${felid.key}_right`,
149 + type: 'right',
150 + })
151 + }
152 + })
153 + return anchors
154 + }
155 +}
156 +
157 +export default {
158 + type: 'sql-node',
159 + model: SqlNodeModel,
160 + view: SqlNode,
161 +}