From f91bc849119dd228b260bb589445414775e9a40a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E5=AE=81?= <1772105645@qq.com> Date: Mon, 25 May 2026 14:25:43 +0800 Subject: [PATCH] feat: add X6 designer configs, FormFieldNode, and useGraph composable --- .../components/nodes/FormFieldNode.vue | 55 ++++++++ .../forms/designer/composables/useGraph.ts | 121 ++++++++++++++++++ .../forms/designer/config/edge-config.ts | 42 ++++++ .../forms/designer/config/graph-config.ts | 36 ++++++ .../forms/designer/config/port-config.ts | 24 ++++ 5 files changed, 278 insertions(+) create mode 100644 apps/web-antd/src/views/workflow/forms/designer/components/nodes/FormFieldNode.vue create mode 100644 apps/web-antd/src/views/workflow/forms/designer/composables/useGraph.ts create mode 100644 apps/web-antd/src/views/workflow/forms/designer/config/edge-config.ts create mode 100644 apps/web-antd/src/views/workflow/forms/designer/config/graph-config.ts create mode 100644 apps/web-antd/src/views/workflow/forms/designer/config/port-config.ts diff --git a/apps/web-antd/src/views/workflow/forms/designer/components/nodes/FormFieldNode.vue b/apps/web-antd/src/views/workflow/forms/designer/components/nodes/FormFieldNode.vue new file mode 100644 index 0000000..a89a17e --- /dev/null +++ b/apps/web-antd/src/views/workflow/forms/designer/components/nodes/FormFieldNode.vue @@ -0,0 +1,55 @@ + + + diff --git a/apps/web-antd/src/views/workflow/forms/designer/composables/useGraph.ts b/apps/web-antd/src/views/workflow/forms/designer/composables/useGraph.ts new file mode 100644 index 0000000..f576444 --- /dev/null +++ b/apps/web-antd/src/views/workflow/forms/designer/composables/useGraph.ts @@ -0,0 +1,121 @@ +import type { Graph, Node } from '@antv/x6'; +import type { Ref } from 'vue'; + +import { onBeforeUnmount, ref, shallowRef } from 'vue'; +import { Graph as X6Graph } from '@antv/x6'; +import { register, getTeleport } from '@antv/x6-vue-shape'; +import { Selection } from '@antv/x6-plugin-selection'; +import { Snapline } from '@antv/x6-plugin-snapline'; + +import FormFieldNode from '../components/nodes/FormFieldNode.vue'; +import { graphOptions, NODE_WIDTH, NODE_HEIGHT } from '../config/graph-config'; +import { portConfig } from '../config/port-config'; + +let registered = false; +function ensureVueShapeRegistered() { + if (registered) return; + register({ + shape: 'form-field-node', + width: NODE_WIDTH, + height: NODE_HEIGHT, + component: FormFieldNode, + ports: portConfig, + }); + registered = true; +} + +export function useGraph(containerRef: Ref) { + const graph = shallowRef(null); + const selectedNodeId = ref(null); + const selectedEdgeId = ref(null); + + function initGraph() { + if (!containerRef.value) return; + ensureVueShapeRegistered(); + + const g = new X6Graph({ + ...graphOptions, + container: containerRef.value, + autoResize: true, + }); + + g.use( + new Selection({ + enabled: true, + rubberband: true, + showNodeSelectionBox: true, + }), + ); + g.use(new Snapline({ enabled: true })); + + g.on('node:click', ({ node }: { node: Node }) => { + selectedNodeId.value = node.id; + selectedEdgeId.value = null; + }); + g.on('edge:click', ({ edge }: { edge: any }) => { + selectedEdgeId.value = edge.id; + selectedNodeId.value = null; + }); + g.on('blank:click', () => { + selectedNodeId.value = null; + selectedEdgeId.value = null; + }); + + graph.value = g; + } + + function addFieldNode(params: { + fieldKey: string; + title: string; + componentType: string; + required?: boolean; + x: number; + y: number; + schemaNode: Record; + }): Node { + const g = graph.value; + if (!g) throw new Error('Graph not initialized'); + return g.addNode({ + shape: 'form-field-node', + x: params.x, + y: params.y, + width: NODE_WIDTH, + height: NODE_HEIGHT, + id: params.fieldKey, + data: { + fieldKey: params.fieldKey, + title: params.title, + componentType: params.componentType, + required: params.required ?? false, + schemaNode: params.schemaNode, + }, + ports: portConfig, + }); + } + + function removeNode(nodeId: string) { + graph.value?.getCellById(nodeId)?.remove(); + if (selectedNodeId.value === nodeId) selectedNodeId.value = null; + } + + function clearCanvas() { + graph.value?.clearCells(); + selectedNodeId.value = null; + selectedEdgeId.value = null; + } + + onBeforeUnmount(() => { + graph.value?.dispose(); + }); + + return { + graph, + selectedNodeId, + selectedEdgeId, + initGraph, + addFieldNode, + removeNode, + clearCanvas, + TeleportContainer: getTeleport(), + }; +} diff --git a/apps/web-antd/src/views/workflow/forms/designer/config/edge-config.ts b/apps/web-antd/src/views/workflow/forms/designer/config/edge-config.ts new file mode 100644 index 0000000..718e859 --- /dev/null +++ b/apps/web-antd/src/views/workflow/forms/designer/config/edge-config.ts @@ -0,0 +1,42 @@ +import { Graph } from '@antv/x6'; + +export type LinkageType = 'condition' | 'data'; + +export const edgeStyles: Record< + LinkageType, + { stroke: string; strokeDasharray: string; label: string } +> = { + condition: { stroke: '#1677ff', strokeDasharray: '5,5', label: '条件' }, + data: { stroke: '#52c41a', strokeDasharray: '', label: '数据' }, +}; + +export function createLinkageEdge(type: LinkageType) { + const style = edgeStyles[type]; + return new Graph.ShapeEdge({ + attrs: { + line: { + stroke: style.stroke, + strokeWidth: 2, + strokeDasharray: style.strokeDasharray, + targetMarker: { name: 'block', width: 12, height: 8 }, + }, + }, + labels: [ + { + position: 0.5, + attrs: { + label: { text: style.label, fill: style.stroke, fontSize: 12 }, + rect: { + fill: '#fff', + stroke: style.stroke, + strokeWidth: 1, + rx: 4, + ry: 4, + }, + }, + }, + ], + router: { name: 'manhattan' }, + connector: { name: 'rounded', args: { radius: 8 } }, + }); +} diff --git a/apps/web-antd/src/views/workflow/forms/designer/config/graph-config.ts b/apps/web-antd/src/views/workflow/forms/designer/config/graph-config.ts new file mode 100644 index 0000000..f060fa1 --- /dev/null +++ b/apps/web-antd/src/views/workflow/forms/designer/config/graph-config.ts @@ -0,0 +1,36 @@ +import type { GraphOptions } from '@antv/x6'; + +export const graphOptions: Partial = { + grid: { + visible: true, + type: 'doubleMesh', + args: [ + { color: '#eee', thickness: 1 }, + { color: '#ddd', thickness: 1, factor: 4 }, + ], + }, + background: { color: '#f8f9fa' }, + snapline: { enabled: true }, + scroller: { enabled: true, pannable: true }, + mousewheel: { enabled: true, modifiers: ['ctrl', 'meta'] }, + selecting: { enabled: true }, + connecting: { + router: 'manhattan', + connector: { name: 'rounded', args: { radius: 8 } }, + anchor: 'center', + connectionPoint: 'anchor', + allowBlank: false, + allowLoop: false, + allowMulti: true, + snap: { radius: 30 }, + }, + highlighting: { + magnetAdsorbed: { + name: 'stroke', + args: { attrs: { fill: '#fff', stroke: '#1677ff' } }, + }, + }, +}; + +export const NODE_WIDTH = 200; +export const NODE_HEIGHT = 60; diff --git a/apps/web-antd/src/views/workflow/forms/designer/config/port-config.ts b/apps/web-antd/src/views/workflow/forms/designer/config/port-config.ts new file mode 100644 index 0000000..1eccb16 --- /dev/null +++ b/apps/web-antd/src/views/workflow/forms/designer/config/port-config.ts @@ -0,0 +1,24 @@ +const portAttrs = { + circle: { + r: 5, + magnet: true, + stroke: '#1677ff', + fill: '#fff', + strokeWidth: 1, + }, +}; + +export const portConfig = { + groups: { + top: { position: 'top', attrs: portAttrs }, + bottom: { position: 'bottom', attrs: portAttrs }, + left: { position: 'left', attrs: portAttrs }, + right: { position: 'right', attrs: portAttrs }, + }, + items: [ + { group: 'top', id: 'top' }, + { group: 'bottom', id: 'bottom' }, + { group: 'left', id: 'left' }, + { group: 'right', id: 'right' }, + ], +};