feat: add X6 designer configs, FormFieldNode, and useGraph composable
This commit is contained in:
parent
069172bb00
commit
f91bc84911
@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface FieldNodeProps {
|
||||||
|
fieldKey: string;
|
||||||
|
title: string;
|
||||||
|
componentType: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<FieldNodeProps>();
|
||||||
|
|
||||||
|
const componentLabel = computed(() => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
Input: '输入框',
|
||||||
|
'Input.TextArea': '文本域',
|
||||||
|
'Input.Password': '密码框',
|
||||||
|
'Input.Number': '数字',
|
||||||
|
Select: '下拉选择',
|
||||||
|
'Radio.Group': '单选',
|
||||||
|
'Checkbox.Group': '多选',
|
||||||
|
Switch: '开关',
|
||||||
|
DatePicker: '日期',
|
||||||
|
TimePicker: '时间',
|
||||||
|
Upload: '上传',
|
||||||
|
FormGrid: '栅格',
|
||||||
|
Card: '卡片',
|
||||||
|
};
|
||||||
|
return map[props.componentType] ?? props.componentType;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 rounded-lg border bg-white px-3 py-2 shadow-sm"
|
||||||
|
style="min-width: 180px"
|
||||||
|
>
|
||||||
|
<div class="flex flex-1 flex-col">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span v-if="required" class="text-red-500">*</span>
|
||||||
|
<span class="text-sm font-medium">{{ title || '未命名' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1 text-xs text-gray-400">
|
||||||
|
<span>{{ fieldKey }}</span>
|
||||||
|
<Tag :bordered="false" class="text-xs" color="blue">
|
||||||
|
{{
|
||||||
|
componentLabel
|
||||||
|
}}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -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<HTMLElement | null>) {
|
||||||
|
const graph = shallowRef<Graph | null>(null);
|
||||||
|
const selectedNodeId = ref<string | null>(null);
|
||||||
|
const selectedEdgeId = ref<string | null>(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<string, any>;
|
||||||
|
}): 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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -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 } },
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import type { GraphOptions } from '@antv/x6';
|
||||||
|
|
||||||
|
export const graphOptions: Partial<GraphOptions> = {
|
||||||
|
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;
|
||||||
@ -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' },
|
||||||
|
],
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user