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