hookehuyr

自定义节点锚点

.sql {
.table-container {
box-sizing: border-box;
padding: 10px;
}
.table-node {
width: 100%;
height: 100%;
overflow: hidden;
background: #fff;
border-radius: 4px;
box-shadow: 0 1px 3px rgb(0 0 0 / 30%);
}
.table-node::before {
display: block;
width: 100%;
height: 8px;
background: #d79b00;
content: '';
}
.table-node.table-color-1::before {
background: #9673a6;
}
.table-node.table-color-2::before {
background: #dae8fc;
}
.table-node.table-color-3::before {
background: #82b366;
}
.table-node.table-color-4::before {
background: #f8cecc;
}
.table-name {
height: 28px;
font-size: 14px;
line-height: 28px;
text-align: center;
background: #f5f5f5;
}
.table-felid {
display: flex;
justify-content: space-between;
height: 24px;
padding: 0 10px;
font-size: 12px;
line-height: 24px;
}
.felid-type {
color: #9f9c9f;
}
/* 自定义锚点样式 */
.custom-anchor {
cursor: crosshair;
fill: #d9d9d9;
stroke: #999;
stroke-width: 1;
rx: 3;
ry: 3;
}
.custom-anchor:hover {
fill: #ff7f0e;
stroke: #ff7f0e;
}
.lf-node-not-allow .custom-anchor:hover {
cursor: not-allowed;
fill: #d9d9d9;
stroke: #999;
}
.incoming-anchor {
stroke: #d79b00;
}
.outgoing-anchor {
stroke: #82b366;
}
}
<!--
* @Date: 2025-03-10 16:52:35
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-12 23:36:42
* @FilePath: /logic-flow2/src/views/adv-node/anchor/index.vue
* @Description: 自定义锚点
-->
<template>
<div class="helloworld-app sql container">
<div ref="container" class="app-content flow-container"></div>
<button @click="handleAddField">Users添加字段</button>
</div>
</template>
<script setup>
import LogicFlow from "@logicflow/core";
import sqlNode from "./sqlNode";
import sqlEdge from "./sqlEdge";
import data from "./sqlData";
import "./index.less";
const container = ref(null);
let lf = null;
// 添加字段的处理函数
const handleAddField = () => {
const nodeModel = lf.getNodeModelById("node_id_1");
if (typeof nodeModel?.addField === "function") {
nodeModel.addField({
key: Math.random().toString(36).substring(2, 7),
type: ["integer", "long", "string", "boolean"][Math.floor(Math.random() * 4)],
});
}
};
onMounted(() => {
lf = new LogicFlow({
container: container.value,
grid: true,
});
lf.register(sqlNode);
lf.register(sqlEdge);
lf.setDefaultEdgeType("sql-edge");
lf.setTheme({
bezier: {
stroke: "#afafaf",
strokeWidth: 1,
},
});
lf.render(data);
lf.translateCenter();
// 1.1.28新增,可以自定义锚点显示时机了
lf.on("anchor:dragstart", ({ data, nodeModel }) => { // 当开始拖拽某个节点的锚点时
console.log("dragstart", data);
if (nodeModel.type === "sql-node") { // 如果拖拽的是 sql-node 类型的节点
lf.graphModel.nodes.forEach((node) => {
// 找到其他的 sql-node 节点(排除当前正在拖拽的节点)
console.warn(node);
if (node.type === "sql-node" && nodeModel.id !== node.id) {
node.isShowAnchor = true; // 显示这些节点的锚点
node.setProperties({
isConnection: true, // 设置连接状态属性
});
}
});
}
});
lf.on("anchor:dragend", ({ data, nodeModel }) => {
console.log("dragend", data);
if (nodeModel.type === "sql-node") {
lf.graphModel.nodes.forEach((node) => {
if (node.type === "sql-node" && nodeModel.id !== node.id) {
node.isShowAnchor = false;
lf.deleteProperty(node.id, "isConnection");
}
});
}
});
});
</script>
<style scoped>
.container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
.flow-container {
flex: 1;
width: 100%;
height: 100%;
}
</style>
/*
* @Date: 2025-03-12 21:23:48
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-12 23:15:14
* @FilePath: /logic-flow2/src/views/adv-node/anchor/sqlData.js
* @Description: 文件描述
*/
const data = {
nodes: [
{
id: 'node_id_1',
type: 'sql-node',
x: 100,
y: 100,
properties: {
tableName: 'Users',
fields: [
{
key: 'id',
type: 'string',
},
{
key: 'name',
type: 'string',
},
{
key: 'age',
type: 'integer',
},
],
},
},
{
id: 'node_id_2',
type: 'sql-node',
x: 400,
y: 200,
properties: {
tableName: 'Settings',
fields: [
{
key: 'id',
type: 'string',
},
{
key: 'key',
type: 'integer',
},
{
key: 'value',
type: 'string',
},
],
},
},
{
id: 'node_id_3',
type: 'sql-node',
x: 400,
y: 400,
properties: {
tableName: 'Settings',
fields: [
{
key: 'id',
type: 'string',
},
{
key: 'key',
type: 'integer',
},
{
key: 'value',
type: 'string',
},
],
},
},
],
edges: [],
};
export default data;
/*
* @Date: 2025-03-12 21:20:42
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-12 23:53:01
* @FilePath: /logic-flow2/src/views/adv-node/anchor/sqlEdge.js
* @Description: 文件描述
*/
import { BezierEdge, BezierEdgeModel } from '@logicflow/core';
class CustomEdge2 extends BezierEdge {} // 继承贝塞尔曲线边的视图类
class CustomEdgeModel2 extends BezierEdgeModel { // 继承贝塞尔曲线边的模型类
getEdgeStyle() {
const style = super.getEdgeStyle();
// svg属性
style.strokeWidth = 1;
style.stroke = '#ababac';
return style;
}
/**
* 重写此方法,使保存数据是能带上锚点数据。
*/
getData() {
const data = super.getData();
// 保存源节点和目标节点的锚点ID,用于后续恢复连线
data.sourceAnchorId = this.sourceAnchorId;
data.targetAnchorId = this.targetAnchorId;
return data;
}
/**
* 给边自定义方案,使其支持基于锚点的位置更新边的路径
*/
updatePathByAnchor() {
// 获取源节点和其锚点
const sourceNodeModel = this.graphModel.getNodeModelById(this.sourceNodeId);
const sourceAnchor = sourceNodeModel
?.getDefaultAnchor()
.find((anchor) => anchor.id === this.sourceAnchorId);
// 获取目标节点和其锚点
const targetNodeModel = this.graphModel.getNodeModelById(this.targetNodeId);
const targetAnchor = targetNodeModel
?.getDefaultAnchor()
.find((anchor) => anchor.id === this.targetAnchorId);
// 更新连线起点
if (sourceAnchor) {
const startPoint = {
x: sourceAnchor?.x,
y: sourceAnchor?.y,
};
this.updateStartPoint(startPoint);
}
// 更新连线终点
if (targetAnchor) {
const endPoint = {
x: targetAnchor?.x,
y: targetAnchor?.y,
};
this.updateEndPoint(endPoint);
}
// 这里需要将原有的pointsList设置为空,才能触发bezier的自动计算control点。
this.pointsList = [];
this.initPoints();
}
}
export default {
type: 'sql-edge',
view: CustomEdge2,
model: CustomEdgeModel2,
};
import { HtmlNode, HtmlNodeModel, h } from '@logicflow/core'
class SqlNode extends HtmlNode {
/**
* 1.1.7版本后支持在view中重写锚点形状。
* 重写锚点新增
*/
// 自定义锚点形状
getAnchorShape(anchorData) {
const { x, y, type } = anchorData
// 将锚点渲染为矩形,左右锚点使用不同的样式类
return h('rect', {
x: x - 5,
y: y - 5,
width: 10,
height: 10,
className: `custom-anchor ${
type === 'left' ? 'incoming-anchor' : 'outgoing-anchor'
}`,
})
}
setHtml(rootEl) { // 渲染节点的 HTML 内容
rootEl.innerHTML = ''
const {
properties: { fields, tableName },
} = this.props.model
rootEl.setAttribute('class', 'table-container')
const container = document.createElement('div')
container.className = `table-node table-color-${Math.ceil(
Math.random() * 4,
)}`
const tableNameElement = document.createElement('div')
tableNameElement.innerText = tableName
tableNameElement.className = 'table-name'
container.appendChild(tableNameElement)
const fragment = document.createDocumentFragment()
for (let i = 0; i < fields.length; i++) {
const item = fields[i]
const itemElement = document.createElement('div')
itemElement.className = 'table-felid'
const itemKey = document.createElement('span')
itemKey.innerText = item.key
const itemType = document.createElement('span')
itemType.innerText = item.type
itemType.className = 'felid-type'
itemElement.appendChild(itemKey)
itemElement.appendChild(itemType)
fragment.appendChild(itemElement)
}
container.appendChild(fragment)
rootEl.appendChild(container)
}
}
class SqlNodeModel extends HtmlNodeModel {
/**
* 给model自定义添加字段方法
*/
addField(item) {
this.properties.fields.unshift(item) // 在字段列表开头添加新字段
this.setAttributes() // 更新节点尺寸
// 为了保持节点顶部位置不变,在节点变化后,对节点进行一个位移,位移距离为添加高度的一半。
this.move(0, 24 / 2) // 调整节点位置保持顶部对齐
// 更新节点连接边的path
this.incoming.edges.forEach((edge) => {
// 调用自定义的更新方案
edge.updatePathByAnchor()
})
this.outgoing.edges.forEach((edge) => {
// 调用自定义的更新方案
edge.updatePathByAnchor()
})
}
getOutlineStyle() {
const style = super.getOutlineStyle()
style.stroke = 'none' // 移除选中外框
style.hover.stroke = 'none' // 设置节点轮廓为透明
return style
}
// 如果不用修改锚地形状,可以重写颜色相关样式
getAnchorStyle(anchorInfo) {
const style = super.getAnchorStyle()
if (anchorInfo.type === 'left') { // 左侧锚点红色(输入)
style.fill = 'red'
style.hover.fill = 'transparent'
style.hover.stroke = 'transparent'
style.className = 'lf-hide-default'
} else { // 右侧锚点绿色(输出)
style.fill = 'green'
}
return style
}
setAttributes() { // 设置节点属性
this.width = 200; // 设置节点宽度为 200
const {
properties: { fields },
} = this
this.height = 60 + fields.length * 24; // 根据字段数量计算高度
// 添加连线规则:
// 1. 只能从右侧锚点连出
// 2. 只能连接到左侧锚点
const circleOnlyAsTarget = {
message: '只允许从右边的锚点连出',
validate: (sourceNode, targetNode, sourceAnchor) => {
return sourceAnchor.type === 'right'
},
}
this.sourceRules.push(circleOnlyAsTarget)
this.targetRules.push({
message: '只允许连接左边的锚点',
validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => {
return targetAnchor.type === 'left'
},
})
}
getDefaultAnchor() { // 获取默认锚点
const {
id,
x,
y,
width,
height,
isHovered,
isSelected,
properties: { fields, isConnection },
} = this
const anchors = []
fields.forEach((felid, index) => {
// 如果是连出,就不显示左边的锚点
if (isConnection || !(isHovered || isSelected)) {
anchors.push({
x: x - width / 2 + 10,
y: y - height / 2 + 60 + index * 24,
id: `${id}_${felid.key}_left`,
edgeAddable: false, // 设置左侧锚点不能作为连线起点, 自定义锚点支持设置edgeAddable属性,用于控制是否可以在此锚点手动创建连线。
type: 'left',
})
}
if (!isConnection) {
anchors.push({
x: x + width / 2 - 10,
y: y - height / 2 + 60 + index * 24,
id: `${id}_${felid.key}_right`,
type: 'right',
})
}
})
return anchors
}
}
export default {
type: 'sql-node',
model: SqlNodeModel,
view: SqlNode,
}