diff --git a/packages/ui-vue/components/flow-canvas/src/assets/config-default.json b/packages/ui-vue/components/flow-canvas/src/assets/config-default.json
new file mode 100644
index 0000000000000000000000000000000000000000..bbe6e0bdaee43f9637b2aee224c89a1e59371ba9
--- /dev/null
+++ b/packages/ui-vue/components/flow-canvas/src/assets/config-default.json
@@ -0,0 +1,4 @@
+{
+ "nodeSchemasUrl": "./assets/node-schema.json",
+ "toolboxUrl": "./assets/toolbox.json"
+}
\ No newline at end of file
diff --git a/packages/ui-vue/components/flow-canvas/src/assets/node-schema.json b/packages/ui-vue/components/flow-canvas/src/assets/node-schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..8ab9709dcb5530cb255f94c64bd8404247d54243
--- /dev/null
+++ b/packages/ui-vue/components/flow-canvas/src/assets/node-schema.json
@@ -0,0 +1,9 @@
+{
+ "flow-connection": "./schema/flow-connection.schema.json",
+ "flow-node": "./schema/flow-node.schema.json",
+ "flow-port": "./schema/flow-port.schema.json",
+ "if-node": "./schema/if-node.schema.json",
+ "start-node": "./schema/start-node.schema.json",
+ "selector-node": "./schema/selector-node.schema.json",
+ "test-node": "./schema/test-node.schema.json"
+}
\ No newline at end of file
diff --git a/packages/ui-vue/components/flow-canvas/src/assets/toolbox.json b/packages/ui-vue/components/flow-canvas/src/assets/toolbox.json
new file mode 100644
index 0000000000000000000000000000000000000000..c12dbebfff136cc0dd3076fa05668863bda9b956
--- /dev/null
+++ b/packages/ui-vue/components/flow-canvas/src/assets/toolbox.json
@@ -0,0 +1,120 @@
+[
+ {
+ "title": "逻辑判断",
+ "leftOptions": [
+ {
+ "id": "Selector Node",
+ "text": "选择器",
+ "description": "多选项选择节点",
+ "category": "logic"
+ },
+ {
+ "id": "action-set",
+ "text": "动作集合",
+ "description": "动作组合节点",
+ "category": "logic"
+ },
+ {
+ "id": "field-access",
+ "text": "访问字段",
+ "description": "字段访问节点",
+ "category": "logic"
+ }
+ ],
+ "rightOptions": [
+ {
+ "id": "function-call",
+ "text": "执行函数",
+ "description": "函数执行节点",
+ "category": "logic"
+ },
+ {
+ "id": "math-calc",
+ "text": "数学计算",
+ "description": "数学运算节点",
+ "category": "logic"
+ },
+ {
+ "id": "If Node",
+ "text": "条件表达式",
+ "description": "条件判断节点",
+ "category": "logic"
+ }
+ ]
+ },
+ {
+ "title": "大模型",
+ "leftOptions": [
+ {
+ "id": "knowledge-node",
+ "text": "知识库",
+ "description": "知识库查询节点",
+ "category": "ai"
+ },
+ {
+ "id": "intent-node",
+ "text": "意图识别",
+ "description": "用户意图识别",
+ "category": "ai"
+ }
+ ],
+ "rightOptions": [
+ {
+ "id": "agent-node",
+ "text": "智能体",
+ "description": "AI智能体节点",
+ "category": "ai"
+ },
+ {
+ "id": "memory-node",
+ "text": "长期记忆",
+ "description": "长期记忆存储",
+ "category": "ai"
+ }
+ ]
+ },
+ {
+ "title": "图像处理",
+ "leftOptions": [
+ {
+ "id": "ocr-node",
+ "text": "提取文字",
+ "description": "OCR文字识别",
+ "category": "image"
+ }
+ ],
+ "rightOptions": [
+ {
+ "id": "cutout-node",
+ "text": "抠图",
+ "description": "图像背景移除",
+ "category": "image"
+ }
+ ]
+ },
+ {
+ "title": "测试节点",
+ "leftOptions": [
+ {
+ "id": "Test Node",
+ "text": "动态测试节点",
+ "description": "验证动态加载功能",
+ "category": "test"
+ }
+ ],
+ "rightOptions": [
+ {
+ "id": "Start Node",
+ "text": "开始节点",
+ "description": "流程开始节点",
+ "category": "test"
+ },
+ {
+ "id": "End Node",
+ "text": "结束节点",
+ "description": "流程结束节点",
+ "category": "test"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/packages/ui-vue/components/flow-canvas/src/components/context-menu.component.tsx b/packages/ui-vue/components/flow-canvas/src/components/context-menu.component.tsx
index a9492f9e069264b9a020e3e105bf20a399a468ab..da688ae1a7c5eb0b0f91cd0a142112468395c68b 100644
--- a/packages/ui-vue/components/flow-canvas/src/components/context-menu.component.tsx
+++ b/packages/ui-vue/components/flow-canvas/src/components/context-menu.component.tsx
@@ -37,7 +37,9 @@ export default defineComponent({
const adjustedPosition = ref({ x: 0, y: 0 });
const adjustPosition = () => {
- if (!menuRef.value || !props.visible) return;
+ if (!menuRef.value || !props.visible) {
+ return;
+ }
nextTick(() => {
const menu = menuRef.value!;
@@ -101,7 +103,9 @@ export default defineComponent({
})();
return () => {
- if (!props.visible) return null;
+ if (!props.visible) {
+ return null;
+ }
return (
>,
+ default: () => ({})
+ },
+ x: { type: Number, default: 0 },
+ y: { type: Number, default: 0 }
+};
+
+export type DynamicNodeProps = ExtractPropTypes
;
+
+/**
+ * 动态节点组件 - 用于渲染通过JSON配置的节点
+ * 不需要预先定义组件,完全基于schema渲染
+ */
+export default defineComponent({
+ name: 'FDynamicNode',
+ props: dynamicNodeProps,
+ emits: [],
+ setup(props: DynamicNodeProps) {
+ const schema = ref(props.modelValue);
+
+ // 从schema中提取显示信息
+ const label = computed(() => schema.value?.label || props.type || 'Dynamic Node');
+ const config = computed(() => schema.value?.config || {});
+ const nodeColor = computed(() => config.value?.color || '#6b7280');
+ const testValue = computed(() => config.value?.testValue || '');
+
+ // 动态样式
+ const dynamicStyle = computed(() => ({
+ borderColor: nodeColor.value,
+ borderWidth: '2px'
+ }));
+
+ // 根据节点类型渲染不同的内容
+ const renderNodeContent = () => {
+ // 如果是test-node,显示特殊内容
+ if (props.type === 'test-node' || props.type === 'Test Node') {
+ return (
+
+
+
+
+ 类型: {props.type}
+
+
+ {testValue.value}
+
+
+
+ );
+ }
+
+ // 默认渲染
+ return (
+
+
+ {label.value}
+
+
+ {props.type}
+
+
+ );
+ };
+
+ return () => {
+ return renderNodeContent();
+ };
+ }
+});
diff --git a/packages/ui-vue/components/flow-canvas/src/components/end-node.component.tsx b/packages/ui-vue/components/flow-canvas/src/components/end-node.component.tsx
index c1a7944e3fd5c39dc624e10ec3ad6288f0346dfb..53f479159d7fbc48aa7b15b36ab75d116774018d 100644
--- a/packages/ui-vue/components/flow-canvas/src/components/end-node.component.tsx
+++ b/packages/ui-vue/components/flow-canvas/src/components/end-node.component.tsx
@@ -21,4 +21,4 @@ export default defineComponent({
);
};
}
-});
\ No newline at end of file
+});
diff --git a/packages/ui-vue/components/flow-canvas/src/components/flow-node-item.component.tsx b/packages/ui-vue/components/flow-canvas/src/components/flow-node-item.component.tsx
index 196a3d6d5e42728d777f12d67c8928a1312a059e..bffb86e974631c74ce732e98de02b814e81132e2 100644
--- a/packages/ui-vue/components/flow-canvas/src/components/flow-node-item.component.tsx
+++ b/packages/ui-vue/components/flow-canvas/src/components/flow-node-item.component.tsx
@@ -8,7 +8,7 @@ import { usePortEventDispatcher } from "../hooks/use-port-event-dispatcher";
export default defineComponent({
name: 'FFlowNodeItem',
props: flowNodeItemProps,
- emits: ['connection-start', 'connection-end', 'node-drag', 'node-select', 'contextmenu'],
+ emits: ['connection-start', 'connection-end', 'node-drag', 'node-select', 'contextmenu', 'click'],
setup(props: FlowNodeItemProps, context) {
const id = ref(props.node?.id || props.id);
const schema = ref(props.modelValue || props.node);
@@ -216,9 +216,7 @@ export default defineComponent({
onMousedown={handleNodeMouseDown}
onClick={(event) => {
event.stopPropagation();
- if (props.onClick) {
- props.onClick(id.value);
- }
+ context.emit('click', id.value);
}}
onContextmenu={(event) => {
event.stopPropagation();
@@ -233,4 +231,4 @@ export default defineComponent({
);
};
}
-});
\ No newline at end of file
+});
diff --git a/packages/ui-vue/components/flow-canvas/src/components/flow-node-item.props.ts b/packages/ui-vue/components/flow-canvas/src/components/flow-node-item.props.ts
index b24e6f3a85b557e8f83e3d78e04613987f5682ab..e573f34182fbbccd65b8366727aea3452a170b6f 100644
--- a/packages/ui-vue/components/flow-canvas/src/components/flow-node-item.props.ts
+++ b/packages/ui-vue/components/flow-canvas/src/components/flow-node-item.props.ts
@@ -27,11 +27,7 @@ export const flowNodeItemProps = {
icon: { type: String, default: '' },
- class: { type: String, default: '' },
-
- onClick: { type: Function, default: null },
-
- onNodeDrag: { type: Function, default: null }
+ class: { type: String, default: '' }
} as Record;
export type FlowNodeItemProps = ExtractPropTypes;
diff --git a/packages/ui-vue/components/flow-canvas/src/components/if-node.component.tsx b/packages/ui-vue/components/flow-canvas/src/components/if-node.component.tsx
index 20be40a9b9b01def9da9944171f2e34bd155d46b..88ff78f5816e7b4d0263e3490fe1973bf4f746bd 100644
--- a/packages/ui-vue/components/flow-canvas/src/components/if-node.component.tsx
+++ b/packages/ui-vue/components/flow-canvas/src/components/if-node.component.tsx
@@ -105,4 +105,4 @@ export default defineComponent({
);
};
}
-});
\ No newline at end of file
+});
diff --git a/packages/ui-vue/components/flow-canvas/src/components/node-configs.ts b/packages/ui-vue/components/flow-canvas/src/components/node-configs.ts
deleted file mode 100644
index 4b0717f75e5a3cc97199946bef46c0e2b11cc2cc..0000000000000000000000000000000000000000
--- a/packages/ui-vue/components/flow-canvas/src/components/node-configs.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { Component } from 'vue';
-import { NodeConfig, NodeConfigFactory, NodeType, nodeConfigFactories } from './node-map';
-
-export interface NodeTemplate {
- type: string;
- displayName: string;
- description: string;
- icon: string;
- category: 'logic' | 'ai' | 'image' | 'flow';
- configFactory: NodeConfigFactory;
-}
-
-export const nodeTemplates: NodeTemplate[] = [
- {
- type: NodeType.START,
- displayName: '开始节点',
- description: '流程开始节点',
- icon: '▶️',
- category: 'flow',
- configFactory: nodeConfigFactories[NodeType.START]
- },
- {
- type: NodeType.END,
- displayName: '结束节点',
- description: '流程结束节点',
- icon: '⏹️',
- category: 'flow',
- configFactory: nodeConfigFactories[NodeType.END]
- },
- {
- type: NodeType.IF,
- displayName: '条件判断',
- description: '条件分支节点',
- icon: '❓',
- category: 'logic',
- configFactory: nodeConfigFactories[NodeType.IF]
- },
- {
- type: NodeType.SELECTOR,
- displayName: '选择器',
- description: '多选项选择节点',
- icon: '📋',
- category: 'logic',
- configFactory: nodeConfigFactories[NodeType.SELECTOR]
- }
-];
-
-export function getNodeTemplatesByCategory(category: string): NodeTemplate[] {
- return nodeTemplates.filter(template => template.category === category);
-}
-
-export function getNodeTemplateByType(nodeType: string): NodeTemplate | undefined {
- return nodeTemplates.find(template => template.type === nodeType);
-}
-
-export function getNodeCategories(): string[] {
- return [...new Set(nodeTemplates.map(template => template.category))];
-}
-
-export function getCategoryDisplayName(category: string): string {
- const categoryNames: Record = {
- 'flow': '流程控制',
- 'logic': '逻辑判断',
- 'ai': 'AI 功能',
- 'image': '图像处理'
- };
- return categoryNames[category] || category;
-}
\ No newline at end of file
diff --git a/packages/ui-vue/components/flow-canvas/src/components/node-map.ts b/packages/ui-vue/components/flow-canvas/src/components/node-map.ts
index c9ed6dd63b4eb7a360b82523c17e7097508ecf1c..bd1e5eb51abb6928007c3388618ad2e3bd06d54d 100644
--- a/packages/ui-vue/components/flow-canvas/src/components/node-map.ts
+++ b/packages/ui-vue/components/flow-canvas/src/components/node-map.ts
@@ -1,180 +1,86 @@
+
import { Component } from 'vue';
-import StartNodeComponent from './start-node.component';
-import EndNodeComponent from './end-node.component';
-import IfNodeComponent from './if-node.component';
-import SelectorNodeComponent from './selector-node.component';
-import EmptyNodeComponent from './empty-node.component';
+import StartNodeComponent from '../components/start-node.component';
+import EndNodeComponent from '../components/end-node.component';
+import IfNodeComponent from '../components/if-node.component';
+import SelectorNodeComponent from '../components/selector-node.component';
+import EmptyNodeComponent from '../components/empty-node.component';
+import DynamicNodeComponent from '../components/dynamic-node.component';
export interface NodeComponentMap {
[key: string]: Component;
}
-export interface NodeConfig {
- id: string;
- type: string;
- label: string;
- icon?: string;
- input?: Array<{
- id: string;
- type: string;
- direction: string;
- position: string;
- autoConnect: boolean;
- }>;
- output?: Array<{
- id: string;
- type: string;
- direction: string;
- position: string;
- autoConnect: boolean;
- }>;
- conditions?: Array<{
- id: string;
- label: string;
- expression?: string;
- output?: Array;
- }>;
- options?: Array;
-}
-
-export type NodeConfigFactory = (nodeId: string) => NodeConfig;
-
export enum NodeType {
- START = 'start-node',
- END = 'end-node',
- IF = 'if-node',
- SELECTOR = 'selector-node',
- EMPTY = 'empty-node'
+ START = 'Start Node',
+ END = 'End Node',
+ IF = 'If Node',
+ SELECTOR = 'Selector Node',
+ EMPTY = 'Empty Node',
+ TEST = 'Test Node',
+ DYNAMIC = 'Dynamic Node'
}
export const nodeMap: NodeComponentMap = {
- [NodeType.START]: StartNodeComponent,
- [NodeType.END]: EndNodeComponent,
- [NodeType.IF]: IfNodeComponent,
- [NodeType.SELECTOR]: SelectorNodeComponent,
- [NodeType.EMPTY]: EmptyNodeComponent
-};
-
-export const nodeConfigFactories: Record = {
- [NodeType.START]: (nodeId: string): NodeConfig => ({
- id: nodeId,
- type: 'start-node',
- label: '开始',
- output: [{
- id: `${nodeId}-output`,
- type: 'port',
- direction: 'output',
- position: 'right',
- autoConnect: true
- }]
- }),
-
- [NodeType.END]: (nodeId: string): NodeConfig => ({
- id: nodeId,
- type: 'end-node',
- label: '结束',
- input: [{
- id: `${nodeId}-input`,
- type: 'port',
- direction: 'input',
- position: 'left',
- autoConnect: true
- }]
- }),
-
- [NodeType.SELECTOR]: (nodeId: string): NodeConfig => ({
- id: nodeId,
- type: 'selector-node',
- label: '选择节点',
- input: [{
- id: `${nodeId}-input`,
- type: 'port',
- direction: 'input',
- position: 'left',
- autoConnect: true
- }]
- }),
-
- [NodeType.IF]: (nodeId: string): NodeConfig => ({
- id: nodeId,
- type: 'if-node',
- label: '选择器',
- input: [{
- id: `${nodeId}-input`,
- type: 'port',
- direction: 'input',
- position: 'left',
- autoConnect: true
- }],
- conditions: [
- {
- id: `${nodeId}-condition-1`,
- label: '如果',
- expression: '',
- output: [{
- id: `${nodeId}-condition-1-output`,
- type: 'port',
- direction: 'output',
- position: 'right',
- parentId: `${nodeId}-condition-1`,
- autoConnect: true
- }]
- },
- {
- id: `${nodeId}-condition-2`,
- label: '否则如果',
- expression: '',
- output: [{
- id: `${nodeId}-condition-2-output`,
- type: 'port',
- direction: 'output',
- position: 'right',
- parentId: `${nodeId}-condition-2`,
- autoConnect: true
- }]
- }
- ]
- }),
-
- [NodeType.EMPTY]: (nodeId: string): NodeConfig => ({
- id: nodeId,
- type: 'empty-node',
- label: 'Empty Node',
- input: [{
- id: `${nodeId}-input`,
- type: 'port',
- direction: 'input',
- position: 'left',
- autoConnect: true
- }]
- })
+ 'Start Node': StartNodeComponent,
+ 'End Node': EndNodeComponent,
+ 'If Node': IfNodeComponent,
+ 'Selector Node': SelectorNodeComponent,
+ 'Empty Node': EmptyNodeComponent,
+ 'Test Node': DynamicNodeComponent, // Test Node使用动态组件渲染
+ 'Dynamic Node': DynamicNodeComponent
};
export function getNodeComponentByType(type: string): Component {
- const normalizedType = type.toLowerCase();
+ if (nodeMap[type]) {
+ return nodeMap[type];
+ }
+ const normalizedType = type.toLowerCase();
const typeMapping: Record = {
- 'start': NodeType.START,
- 'end': NodeType.END,
- 'if': NodeType.IF,
- 'condition': NodeType.IF,
- 'selector': NodeType.SELECTOR,
- 'select': NodeType.SELECTOR
+ 'start': 'Start Node',
+ 'end': 'End Node',
+ 'if': 'If Node',
+ 'condition': 'If Node',
+ 'selector': 'Selector Node',
+ 'select': 'Selector Node',
+ 'start-node': 'Start Node',
+ 'end-node': 'End Node',
+ 'if-node': 'If Node',
+ 'selector-node': 'Selector Node',
+ 'empty-node': 'Empty Node',
+ 'test-node': 'Test Node',
+ 'test': 'Test Node'
};
- const mappedType = typeMapping[normalizedType] || normalizedType;
+ const mappedType = typeMapping[normalizedType];
+ if (mappedType && nodeMap[mappedType]) {
+ return nodeMap[mappedType];
+ }
- return nodeMap[mappedType] || nodeMap[NodeType.EMPTY];
+ // 对于未知类型,使用动态节点组件
+ return DynamicNodeComponent;
}
-export function createNodeConfig(nodeType: string, nodeId: string): NodeConfig {
- const configFactory = nodeConfigFactories[nodeType];
-
- if (!configFactory) {
- return nodeConfigFactories[NodeType.EMPTY](nodeId);
- }
+export function getNodeDisplayName(type: string): string {
+ const displayNames: Record = {
+ 'Start Node': '开始',
+ 'End Node': '结束',
+ 'If Node': '选择器',
+ 'Selector Node': '选择节点',
+ 'Empty Node': '未知',
+ 'Test Node': '测试节点',
+ 'Dynamic Node': '动态节点',
+ // Legacy mappings
+ 'start-node': '开始',
+ 'end-node': '结束',
+ 'if-node': '选择器',
+ 'selector-node': '选择节点',
+ 'empty-node': '未知',
+ 'test-node': '测试节点'
+ };
- return configFactory(nodeId);
+ return displayNames[type] || type;
}
export function getAllSupportedNodeTypes(): Array<{
@@ -182,7 +88,7 @@ export function getAllSupportedNodeTypes(): Array<{
displayName: string;
hasConfig: boolean;
}> {
- return Object.keys(nodeConfigFactories).map(type => ({
+ return Object.keys(nodeMap).map(type => ({
type,
displayName: getNodeDisplayName(type),
hasConfig: true
@@ -190,32 +96,17 @@ export function getAllSupportedNodeTypes(): Array<{
}
export function isValidNodeType(type: string): boolean {
- return Object.values(NodeType).includes(type as NodeType);
+ return true;
}
export function getSupportedNodeTypes(): string[] {
- return Object.values(NodeType);
-}
-
-export function getNodeDisplayName(type: string): string {
- const displayNames: Record = {
- [NodeType.START]: '开始',
- [NodeType.END]: '结束',
- [NodeType.IF]: '选择器',
- [NodeType.SELECTOR]: '选择节点',
- [NodeType.EMPTY]: '未知'
- };
-
- return displayNames[type] || '未知';
+ return Object.keys(nodeMap);
}
export function registerNodeType(
nodeType: string,
component: Component,
- configFactory: NodeConfigFactory,
displayName?: string
): void {
nodeMap[nodeType] = component;
-
- nodeConfigFactories[nodeType] = configFactory;
}
diff --git a/packages/ui-vue/components/flow-canvas/src/components/node-selector-panel.component.tsx b/packages/ui-vue/components/flow-canvas/src/components/node-selector-panel.component.tsx
index 5b2b16dc48819fdf730464f743db7e67d75028ca..deff95ca0fd06151b6a2c2be9bb94c263277d2f9 100644
--- a/packages/ui-vue/components/flow-canvas/src/components/node-selector-panel.component.tsx
+++ b/packages/ui-vue/components/flow-canvas/src/components/node-selector-panel.component.tsx
@@ -1,6 +1,5 @@
import { defineComponent } from "vue";
-import { getAllSupportedNodeTypes, getNodeDisplayName, NodeType } from "./node-map";
-import { getNodeTemplatesByCategory, getCategoryDisplayName } from "./node-configs";
+import { nodeTypeOptions, NodeTypeCategory } from "../schema/configs/node-type-options";
export default defineComponent({
name: 'NodeSelectorPanel',
@@ -19,29 +18,23 @@ export default defineComponent({
props.onClose();
};
- function renderCategoryRow(category: string) {
- const templates = getNodeTemplatesByCategory(category);
- const leftOptions = templates.slice(0, Math.ceil(templates.length / 2));
- const rightOptions = templates.slice(Math.ceil(templates.length / 2));
-
+ function renderCategoryRow(category: NodeTypeCategory) {
return (
-
-
{getCategoryDisplayName(category)}
+
+
{category.title}
- {leftOptions.map((template) =>
-
handleNodeSelect(template.type)}>
-
{template.icon}
-
{template.displayName}
+ {category.leftOptions.map((option) =>
+
handleNodeSelect(option.id)}>
+ {option.text}
+
)}
- {rightOptions.map((template) =>
-
handleNodeSelect(template.type)}>
-
{template.icon}
-
{template.displayName}
+ {category.rightOptions.map((option) =>
+
handleNodeSelect(option.id)}>
+ {option.text}
+
)}
@@ -52,9 +45,9 @@ export default defineComponent({
}
return () => {
- if (!props.visible) return null;
-
- const categories = ['flow', 'logic', 'ai', 'image'];
+ if (!props.visible) {
+ return null;
+ }
return (
- {categories.map((category) => renderCategoryRow(category))}
+ {/* 🎯 使用统一的 node-type-options 配置 */}
+ {nodeTypeOptions.map((category) => renderCategoryRow(category))}
);
};
}
-});
\ No newline at end of file
+});
diff --git a/packages/ui-vue/components/flow-canvas/src/components/node-selector.component.tsx b/packages/ui-vue/components/flow-canvas/src/components/node-selector.component.tsx
index f24e66200eac59ee7d1fee624dd84ae51b5c576c..0c41a0c2e735b2a4e695778eec7cb5a9daa7a6c0 100644
--- a/packages/ui-vue/components/flow-canvas/src/components/node-selector.component.tsx
+++ b/packages/ui-vue/components/flow-canvas/src/components/node-selector.component.tsx
@@ -30,12 +30,13 @@ export default defineComponent({
name: 'FNodeSelector',
props: nodeSelectorProps,
setup(props: NodeSelectorProps) {
+ // 使用schema title作为nodeType,与新的工厂模式保持一致
const nodeTypes = [
- { type: 'start-node', label: '开始节点', icon: '▶️' },
- { type: 'end-node', label: '结束节点', icon: '⏹️' },
- { type: 'if-node', label: '条件节点', icon: '❓' },
- { type: 'selector-node', label: '选择器节点', icon: '📋' },
- { type: 'empty-node', label: '空节点', icon: '⚪' }
+ { type: 'Start Node', label: '开始节点', icon: '▶️' },
+ { type: 'End Node', label: '结束节点', icon: '⏹️' },
+ { type: 'If Node', label: '条件节点', icon: '❓' },
+ { type: 'Selector Node', label: '选择器节点', icon: '📋' },
+ { type: 'Empty Node', label: '空节点', icon: '⚪' }
];
const handleNodeSelect = (nodeType: string) => {
@@ -160,4 +161,4 @@ export default defineComponent({
);
};
}
-});
\ No newline at end of file
+});
diff --git a/packages/ui-vue/components/flow-canvas/src/components/selector-node.component.tsx b/packages/ui-vue/components/flow-canvas/src/components/selector-node.component.tsx
index 59c84b176cc9c510377be901a022801299c316fa..3168b9a41401b10d71cd8ceb94d7903cadcc0d12 100644
--- a/packages/ui-vue/components/flow-canvas/src/components/selector-node.component.tsx
+++ b/packages/ui-vue/components/flow-canvas/src/components/selector-node.component.tsx
@@ -107,4 +107,4 @@ export default defineComponent({
);
};
}
-});
\ No newline at end of file
+});
diff --git a/packages/ui-vue/components/flow-canvas/src/components/start-node.component.tsx b/packages/ui-vue/components/flow-canvas/src/components/start-node.component.tsx
index fc45ba53d3962340c28fe7528ce3a7de43b1c1dd..fb80cbc55150ce7815012e81942d1e9274159f99 100644
--- a/packages/ui-vue/components/flow-canvas/src/components/start-node.component.tsx
+++ b/packages/ui-vue/components/flow-canvas/src/components/start-node.component.tsx
@@ -35,4 +35,4 @@ export default defineComponent({
);
};
}
-});
\ No newline at end of file
+});
diff --git a/packages/ui-vue/components/flow-canvas/src/composition/types.ts b/packages/ui-vue/components/flow-canvas/src/composition/types.ts
index 9bda336c14eb27f1c6726633b9bb9dad6289fd64..6a5249ade6d98ef9e32837e7142c075db5f4c3f8 100644
--- a/packages/ui-vue/components/flow-canvas/src/composition/types.ts
+++ b/packages/ui-vue/components/flow-canvas/src/composition/types.ts
@@ -118,3 +118,18 @@ export interface UseConnections {
connectionId: string;
}>;
}
+
+
+export interface ConfigOptions {
+ /** 节点元模型集合 */
+ nodeSchemasUrl: string;
+ /** 流程节点工具箱 */
+ toolboxUrl: string;
+}
+
+export interface UseConfig {
+
+ options: ConfigOptions;
+
+ initialize: () => Promise
+}
diff --git a/packages/ui-vue/components/flow-canvas/src/composition/use-bezier-curve.ts b/packages/ui-vue/components/flow-canvas/src/composition/use-bezier-curve.ts
index 8d1cbd0245dd2b3067bfa71b0062fad6dc78d0ea..19b138bf678651f9bb59de980a0340f16ee08bad 100644
--- a/packages/ui-vue/components/flow-canvas/src/composition/use-bezier-curve.ts
+++ b/packages/ui-vue/components/flow-canvas/src/composition/use-bezier-curve.ts
@@ -1,4 +1,5 @@
/* eslint-disable complexity */
+/* eslint-disable no-use-before-define */
import { CurveOrientation, CurvePoint, CurvePointOffset, UseBezierCurve } from "./types";
@@ -136,7 +137,9 @@ export function useBezierCurve(): UseBezierCurve & {
let insertNodeHandler: ((connectionId: string, position: { x: number; y: number }) => void) | null = null;
const createInsertButton = () => {
- if (insertButton) return insertButton;
+ if (insertButton) {
+ return insertButton;
+ }
insertButton = document.createElement('div');
insertButton.className = 'connection-insert-button';
@@ -178,7 +181,9 @@ export function useBezierCurve(): UseBezierCurve & {
// 显示插入按钮
const showInsertButton = (event: MouseEvent) => {
- if (isSelected) return; // 选中状态下不显示插入按钮
+ if (isSelected) {
+ return; // 选中状态下不显示插入按钮
+ }
const button = createInsertButton();
const rect = curveElement.getBoundingClientRect();
diff --git a/packages/ui-vue/components/flow-canvas/src/composition/use-config.ts b/packages/ui-vue/components/flow-canvas/src/composition/use-config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..66f78f2a3eb42eb9ea8c7956e722174038718830
--- /dev/null
+++ b/packages/ui-vue/components/flow-canvas/src/composition/use-config.ts
@@ -0,0 +1,29 @@
+import axios from "axios";
+import { ConfigOptions, UseConfig } from "./types";
+
+export function useConfig(): UseConfig {
+
+ const defaultConfigFileUrl = './assets/config-default.json';
+ // 全局配置对象接口
+ const options: ConfigOptions = {
+ /** 节点元模型集合 */
+ nodeSchemasUrl: '',
+ /** 流程节点工具箱 */
+ toolboxUrl: ''
+ };
+
+ function initialize() {
+ return new Promise((resolve, reject) => {
+ axios.get(defaultConfigFileUrl).then((response) => {
+ const config = response.data;
+ if (config) {
+ options.nodeSchemasUrl = config['nodeSchemasUrl'] || options.nodeSchemasUrl;
+ options.toolboxUrl = config['toolboxUrl'] || options.toolboxUrl;
+ }
+ resolve(options);
+ });
+ });
+ }
+
+ return { options, initialize };
+}
diff --git a/packages/ui-vue/components/flow-canvas/src/composition/use-connection-manager.ts b/packages/ui-vue/components/flow-canvas/src/composition/use-connection-manager.ts
index 86a0ca73b08805c8a8f2a2e26dfeddb91f043e9d..ecf9452b3ce3170c4a4ff8776d371bd4a4789d47 100644
--- a/packages/ui-vue/components/flow-canvas/src/composition/use-connection-manager.ts
+++ b/packages/ui-vue/components/flow-canvas/src/composition/use-connection-manager.ts
@@ -160,4 +160,4 @@ export function useConnectionManager(options: ConnectionManagerOptions): UseConn
insertNodeInConnection,
getConnectionsByNodeId
};
-}
\ No newline at end of file
+}
diff --git a/packages/ui-vue/components/flow-canvas/src/composition/use-drawing-bezier.ts b/packages/ui-vue/components/flow-canvas/src/composition/use-drawing-bezier.ts
index d00ea65e1cb6abc93e368d9b48269b0523cf4265..e4da036a9e27cd829f4d9c17e3ae07968bdf15a9 100644
--- a/packages/ui-vue/components/flow-canvas/src/composition/use-drawing-bezier.ts
+++ b/packages/ui-vue/components/flow-canvas/src/composition/use-drawing-bezier.ts
@@ -30,45 +30,17 @@ export function useDrawingBezier(
const showNodeSelector = ref(false);
const nodeSelectorPosition = ref({ x: 0, y: 0 });
- function extractNodeIdFromPortId(portId: string): string {
- const selectorOptionMatch = portId.match(/^(.+?)-option-\d+-output$/);
- if (selectorOptionMatch) {
- return selectorOptionMatch[1];
- }
-
- const conditionMatch = portId.match(/^(.+?)-condition-\d+-output$/);
- if (conditionMatch) {
- return conditionMatch[1];
- }
-
- const simpleMatch = portId.match(/^(.+?)-(input|output)$/);
- if (simpleMatch) {
- return simpleMatch[1];
- }
-
- return portId;
- }
-
function convertToNumber(pxStr: string) {
return Number(pxStr.replace('px', ''));
}
- interface DragStartInfo {
- sourceNodeId: string;
- sourcePortId: string;
- startPosition: { x: number; y: number };
- startTime: number;
- }
-
function isAncherPoint(element: HTMLElement) {
const classNames = element && element.className && element.className.split ? element.className.split(' ') : [];
return classNames.includes('f-flow-ancher');
}
function getAncherPointPosition(ancherElement: HTMLElement | null) {
- if (!ancherElement) {
- return 'center';
- }
+ if (!ancherElement) { return 'center'; }
const classNames = ancherElement.className.split(' ');
@@ -150,14 +122,9 @@ export function useDrawingBezier(
ancherPosition.value = getAncherPointPosition(ancherElement);
}
- function resetDragState() {
- }
-
function updateDrawingLine(mouseMovingArgs: MouseEvent) {
const svgContainer = document.getElementById('svg-container');
- if (!svgContainer) {
- return;
- }
+ if (!svgContainer) { return; }
const canvasRect = svgContainer.getBoundingClientRect();
const mouseX = mouseMovingArgs.clientX - canvasRect.left;
@@ -197,15 +164,28 @@ export function useDrawingBezier(
document.removeEventListener('mouseup', handleMouseUp);
}
- function showNodeSelectorPanel(mouseEvent: MouseEvent) {
- if (!startAncherElement.value) {
+ function handleMouseUp(event: MouseEvent) {
+ const targetElement = event.target as HTMLElement;
+
+ if (targetElement &&
+ targetElement.classList.contains('f-flow-ancher') &&
+ targetElement.classList.contains('input-port') &&
+ startAncherElement.value) {
+
+ finishToDraw(event);
return;
}
- const svgContainer = document.getElementById('svg-container');
- if (!svgContainer) {
- return;
+ if (startAncherElement.value) {
+ showNodeSelectorPanel(event);
}
+ }
+
+ function showNodeSelectorPanel(mouseEvent: MouseEvent) {
+ if (!startAncherElement.value) { return; }
+
+ const svgContainer = document.getElementById('svg-container');
+ if (!svgContainer) { return; }
const canvasRect = svgContainer.getBoundingClientRect();
@@ -229,23 +209,6 @@ export function useDrawingBezier(
releaseDrawingEvents();
}
- function handleMouseUp(event: MouseEvent) {
- const targetElement = event.target as HTMLElement;
-
- if (targetElement &&
- targetElement.classList.contains('f-flow-ancher') &&
- targetElement.classList.contains('input-port') &&
- startAncherElement.value) {
-
- finishToDraw(event);
- return;
- }
-
- if (startAncherElement.value) {
- showNodeSelectorPanel(event);
- }
- }
-
function cancelNodeSelection() {
showNodeSelector.value = false;
@@ -267,6 +230,19 @@ export function useDrawingBezier(
releaseDrawingEvents();
}
+ function extractNodeIdFromPortId(portId: string): string {
+ const selectorOptionMatch = portId.match(/^(.+?)-option-\d+-output$/);
+ if (selectorOptionMatch) { return selectorOptionMatch[1]; }
+
+ const conditionMatch = portId.match(/^(.+?)-condition-\d+-output$/);
+ if (conditionMatch) { return conditionMatch[1]; }
+
+ const simpleMatch = portId.match(/^(.+?)-(input|output)$/);
+ if (simpleMatch) { return simpleMatch[1]; }
+
+ return portId;
+ }
+
function finishToDraw(event: MouseEvent) {
startAncherElement.value = undefined;
releaseDrawingEvents();
@@ -307,4 +283,4 @@ export function useDrawingBezier(
cancelNodeSelection,
completeNodeSelection
};
-}
\ No newline at end of file
+}
diff --git a/packages/ui-vue/components/flow-canvas/src/composition/use-drawing.ts b/packages/ui-vue/components/flow-canvas/src/composition/use-drawing.ts
index 6ed51941a5e7db6f11704bf1c5f7f606322e80e4..0768f2b566b68be9e47000e7f31432b36f81a210 100644
--- a/packages/ui-vue/components/flow-canvas/src/composition/use-drawing.ts
+++ b/packages/ui-vue/components/flow-canvas/src/composition/use-drawing.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-use-before-define */
import { ref } from "vue";
import { UseCurve } from "./types";
diff --git a/packages/ui-vue/components/flow-canvas/src/composition/use-node-resolver.ts b/packages/ui-vue/components/flow-canvas/src/composition/use-node-resolver.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b4bf7f64ef9435e3d3e1914af6175785233293fd
--- /dev/null
+++ b/packages/ui-vue/components/flow-canvas/src/composition/use-node-resolver.ts
@@ -0,0 +1,97 @@
+import { cloneDeep } from "lodash-es";
+import { UseNodeSchema } from "./use-node-schema";
+
+export interface NodeFactory {
+ createNodeModelByType: (nodeType: string, config?: Record) => any;
+ createPort: (portType: string, config?: Record) => any;
+ createConnection: (connectionType: string, config?: Record) => any;
+}
+
+export function useNodeResolver(useNodeSchemaComposition: UseNodeSchema): NodeFactory {
+ const { getNodeSchema, getNodeSchemaResolver } = useNodeSchemaComposition;
+
+ /**
+ * 递归解析变量(如 ${nodeId})
+ */
+ function resolveDefaultValue(originalDefaultValue: any, nodeId: string): any {
+ if (Array.isArray(originalDefaultValue)) {
+ return originalDefaultValue.map(item => resolveDefaultValue(item, nodeId));
+ } else if (typeof originalDefaultValue === 'object' && originalDefaultValue !== null) {
+ const resolvedValue = cloneDeep(originalDefaultValue);
+ for (const [key, value] of Object.entries(resolvedValue)) {
+ if (typeof value === 'string' && value.includes('${nodeId}')) {
+ resolvedValue[key] = value.replace(/\$\{nodeId\}/g, nodeId);
+ } else if (typeof value === 'object') {
+ resolvedValue[key] = resolveDefaultValue(value, nodeId);
+ }
+ }
+ return resolvedValue;
+ }
+ return originalDefaultValue;
+ }
+
+ /**
+ * 基于JSON Schema构造节点模型
+ */
+ function createNodeModelFromSchema(defaultSchema: Record): Record {
+ const { properties, title, required: requiredProperty } = defaultSchema;
+ const timestamp = Date.now();
+ const nodeType = title ? title.replace(/\s+/g, '').charAt(0).toLowerCase() + title.replace(/\s+/g, '').slice(1) : '';
+ const nodeId = `${nodeType}-${timestamp}`;
+
+ if (requiredProperty && Array.isArray(requiredProperty)) {
+ const newNodeModel = requiredProperty.reduce((nodeModel: Record, propKey: string) => {
+ const property = properties[propKey];
+ if (property.type === 'object' && property.properties) {
+ // 递归处理嵌套对象
+ nodeModel[propKey] = createNodeModelFromSchema(property);
+ } else {
+ // 解析默认值中的变量
+ nodeModel[propKey] = resolveDefaultValue(cloneDeep(property.default), nodeId);
+ }
+ return nodeModel;
+ }, {});
+ newNodeModel.id = nodeId;
+ return newNodeModel;
+ }
+ return { type: title, id: `${nodeType}-${timestamp}` };
+ }
+
+ /**
+ * 工厂方法入口 - 根据节点类型创建节点
+ */
+ function createNodeModelByType(nodeType: string, resolveContext: Record = {}): Record | null {
+ const defaultSchema = getNodeSchema(nodeType);
+ if (defaultSchema) {
+ const newNodeModel = createNodeModelFromSchema(defaultSchema);
+ const schemaResolver = getNodeSchemaResolver(nodeType);
+ const resolvedNodeModel = schemaResolver ? schemaResolver(newNodeModel, resolveContext) : newNodeModel;
+ return resolvedNodeModel;
+ }
+ return null;
+ }
+
+ function createPort(portType: string, config: Record = {}) {
+ // 端口创建逻辑
+ return {
+ id: config.id || `port-${Date.now()}`,
+ type: portType,
+ ...config
+ };
+ }
+
+ function createConnection(connectionType: string, config: Record = {}) {
+ // 连接创建逻辑
+ return {
+ id: config.id || `connection-${Date.now()}`,
+ type: connectionType,
+ ...config
+ };
+ }
+
+ return {
+ createNodeModelByType,
+ createPort,
+ createConnection
+ };
+}
diff --git a/packages/ui-vue/components/flow-canvas/src/composition/use-node-schema.ts b/packages/ui-vue/components/flow-canvas/src/composition/use-node-schema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d7450d74e2f2403c2af2ea8bb2093f128956d4cb
--- /dev/null
+++ b/packages/ui-vue/components/flow-canvas/src/composition/use-node-schema.ts
@@ -0,0 +1,66 @@
+import axios from "axios";
+import { UseConfig } from "./types";
+export type NodeSchemaResolverFunction = (
+ schema: Record,
+ resolveContext: Record
+) => Record;
+export const nodeSchemaMap = {} as Record;
+export const nodeSchemaResolverMap = {} as Record;
+
+export interface UseNodeSchema {
+ getNodeSchema: (nodeType: string) => Record;
+ getNodeSchemaResolver: (nodeType: string) => NodeSchemaResolverFunction | undefined;
+}
+
+export function registerNodeSchema(schema: Record, schemaResolver?: NodeSchemaResolverFunction) {
+ nodeSchemaMap[schema.title] = schema;
+ nodeSchemaResolverMap[schema.title] = schemaResolver;
+}
+
+export function useNodeSchema(config: UseConfig) {
+
+
+ function loadSchema(schemaUrl: string) {
+ return new Promise((resolve, reject) => {
+ axios.get(schemaUrl).then((response) => {
+ resolve(response.data);
+ });
+ });
+ }
+
+ function loadNodeSchema() {
+ return new Promise((resolve, reject) => {
+ axios.get(config.options.nodeSchemasUrl).then((response) => {
+ if (response && response.data) {
+ const schemaSourceMap = response.data;
+ const schemaPromises = Object.keys(schemaSourceMap).map((key) => {
+ const schemaUrl = schemaSourceMap[key];
+ return loadSchema(schemaUrl);
+ });
+ Promise.all(schemaPromises).then((schemas) => {
+ if (schemas && Array.isArray(schemas) && schemas.length) {
+ schemas.forEach((schema) => {
+ registerNodeSchema(schema);
+ });
+ }
+ resolve(schemas);
+ });
+ }
+ });
+ });
+ }
+
+ function getNodeSchema(nodeType: string) {
+ return nodeSchemaMap[nodeType];
+ }
+
+ function getNodeSchemaResolver(nodeType: string) {
+ return nodeSchemaResolverMap[nodeType];
+ }
+
+ return {
+ getNodeSchema,
+ getNodeSchemaResolver,
+ loadNodeSchema
+ };
+}
diff --git a/packages/ui-vue/components/flow-canvas/src/composition/use-toolbox.ts b/packages/ui-vue/components/flow-canvas/src/composition/use-toolbox.ts
new file mode 100644
index 0000000000000000000000000000000000000000..474e43fa064ba243c75cef6a8e025c5ed757710f
--- /dev/null
+++ b/packages/ui-vue/components/flow-canvas/src/composition/use-toolbox.ts
@@ -0,0 +1,25 @@
+import axios from "axios";
+import { UseConfig } from "./types";
+import { ref } from "vue";
+import { NodeTypeCategory } from "../schema/configs/node-type-options";
+
+export function useToolbox(config:UseConfig) {
+
+ const nodeTypes = ref([]);
+
+ function loadToolbox() {
+ return new Promise((resolve, reject) => {
+ axios.get(config.options.toolboxUrl).then((response) => {
+ if (response && response.data && Array.isArray(response.data)) {
+ nodeTypes.value = response.data;
+ }
+ resolve(nodeTypes.value);
+ });
+ });
+ }
+
+ return {
+ loadToolbox,
+ nodeTypes
+ };
+}
diff --git a/packages/ui-vue/components/flow-canvas/src/flow-canvas.component.tsx b/packages/ui-vue/components/flow-canvas/src/flow-canvas.component.tsx
index 7848efe5aee9b7e126781f8633ba3d3a9fd6e404..4e61338f17f35ec25f2b55b230eaa56b2013882f 100644
--- a/packages/ui-vue/components/flow-canvas/src/flow-canvas.component.tsx
+++ b/packages/ui-vue/components/flow-canvas/src/flow-canvas.component.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable no-use-before-define */
import { computed, defineComponent, nextTick, onMounted, provide, ref, watch, onBeforeUnmount } from "vue";
import { FlowCanvasProps, flowCanvasProps } from "./flow-canvas.props";
@@ -5,13 +6,16 @@ import './flow-canvas.css';
import { useBezierCurve } from "./composition/use-bezier-curve";
import { useDrawingBezier } from "./composition/use-drawing-bezier";
import { useConnections } from "./composition/use-connections";
-import { useConnectionManager } from "./composition/use-connection-manager";
-import { createNodeConfig } from "./components/node-map";
-import { getElementPosition } from "./utils/element-utils";
+import { useConnectionManager } from "./hooks/use-connection-manager";
+
+import { useNodeSchema } from "./composition/use-node-schema";
+import { useNodeResolver } from "./composition/use-node-resolver";
import FFlowNodeItem from './components/flow-node-item.component';
import NodeSelectorPanel from './components/node-selector-panel.component';
import ContextMenu, { ContextMenuOption } from './components/context-menu.component';
+import { useConfig } from "./composition/use-config";
+import { ConfigOptions } from "./composition/types";
export default defineComponent({
name: 'FFlowCanvas',
@@ -22,9 +26,55 @@ export default defineComponent({
const selectedNodeId = ref(null);
const selectedConnectionId = ref(null);
+ // 添加加载状态标志
+ const isSchemaLoaded = ref(false);
+ const isLoading = ref(true);
+
const useBezierCurveComposition = useBezierCurve();
const useDrawingBezierComposition = useDrawingBezier(useBezierCurveComposition);
const useConnectionsComposition = useConnections([]);
+ const config = useConfig();
+ const useNodeSchemaComposition = useNodeSchema(config);
+ const { createNodeModelByType } = useNodeResolver(useNodeSchemaComposition);
+
+ // 修复的初始化流程 - 确保Schema在渲染前加载完成
+ const initializeCanvas = async () => {
+ try {
+ isLoading.value = true;
+
+ // 1. 先初始化配置
+ await config.initialize();
+
+ // 2. 加载所有节点的schema - 这是关键步骤
+ await useNodeSchemaComposition.loadNodeSchema();
+
+ // 3. 标记schema加载完成 - 只有这之后才能渲染节点
+ isSchemaLoaded.value = true;
+
+ // 4. 等待DOM更新
+ await nextTick();
+
+ // 5. 恢复已有的连接
+ if (connectionManager && schema.value?.contents) {
+ // 延迟恢复连接,确保节点已渲染
+ setTimeout(async () => {
+ try {
+ await connectionManager.restoreConnections();
+ } catch (error) {
+ console.warn('Failed to restore connections:', error);
+ }
+ }, 100);
+ }
+
+ isLoading.value = false;
+
+ } catch (error) {
+ console.error('Canvas initialization failed:', error);
+ // 即使失败也允许使用默认节点
+ isSchemaLoaded.value = true;
+ isLoading.value = false;
+ }
+ };
const connectionManager = useConnectionManager({
schema,
@@ -96,15 +146,23 @@ export default defineComponent({
};
};
+ // 创建节点时检查Schema是否已加载
const createNode = (nodeType: string, position: { x: number; y: number }): string | null => {
if (!schema.value) {
return null;
}
- const timestamp = Date.now();
- const nodeId = `${nodeType}-${timestamp}`;
+ // 确保schema已加载
+ if (!isSchemaLoaded.value) {
+ return null;
+ }
- const newNode = createNodeConfig(nodeType, nodeId);
+ const newNode = createNodeModelByType(nodeType);
+
+ if (!newNode || !newNode.id) {
+ console.error('Failed to create node of type:', nodeType);
+ return null;
+ }
schema.value.contents.push(newNode);
@@ -113,13 +171,12 @@ export default defineComponent({
}
schema.value.diagram.nodes.push({
- id: nodeId,
+ id: newNode.id,
position: position
});
context.emit('update:modelValue', schema.value);
-
- return nodeId;
+ return newNode.id;
};
const deleteSelectedNode = (): void => {
@@ -131,11 +188,13 @@ export default defineComponent({
const relatedConnections = useConnectionsComposition.getConnectionsByNodeId(nodeId);
relatedConnections.forEach(conn => {
- const connectionElement = document.getElementById(conn.connectionId);
- if (connectionElement) {
- connectionElement.remove();
+ if (conn.connectionId && conn.startPortId && conn.endPortId) {
+ const connectionElement = document.getElementById(conn.connectionId);
+ if (connectionElement && connectionElement.parentNode) {
+ connectionElement.remove();
+ }
+ removeConnectionFromSchema(conn.startPortId, conn.endPortId);
}
- removeConnectionFromSchema(conn.startPortId, conn.endPortId);
});
removeNodeFromSchema(nodeId);
@@ -149,9 +208,14 @@ export default defineComponent({
}
const [startPortId, endPortId] = selectedConnectionId.value.split('_');
- const connectionElement = document.getElementById(selectedConnectionId.value);
- if (connectionElement) {
- connectionElement.remove();
+ if (selectedConnectionId.value && startPortId && endPortId) {
+ const connectionElement = document.getElementById(selectedConnectionId.value);
+ if (connectionElement && connectionElement.parentNode) {
+ connectionElement.remove();
+ }
+ removeConnectionFromSchema(startPortId, endPortId);
+ useConnectionsComposition.removeConnection(startPortId, endPortId);
+ selectedConnectionId.value = null;
}
removeConnectionFromSchema(startPortId, endPortId);
@@ -187,12 +251,17 @@ export default defineComponent({
const newNodeId = createNode(nodeType, newNodePosition);
if (newNodeId) {
- await connectionManager.createConnection(
- connectionInfo.startPortId,
- `${newNodeId}-input`,
- connectionInfo.startNodeId,
- newNodeId
- );
+ // 等待节点渲染完成
+ await nextTick();
+
+ if (connectionInfo.startPortId && connectionInfo.startNodeId) {
+ const success = await connectionManager.createConnection(
+ connectionInfo.startPortId,
+ `${newNodeId}-input`,
+ connectionInfo.startNodeId,
+ newNodeId
+ );
+ }
}
useDrawingBezierComposition.completeNodeSelection(nodeType);
@@ -202,12 +271,12 @@ export default defineComponent({
useDrawingBezierComposition.completeNodeSelection('');
};
- const handleInsertNodeSelect = (nodeType: string): void => {
+ const handleInsertNodeSelect = async (nodeType: string): Promise => {
if (!insertNodeState.value.connectionId) {
return;
}
- const connectionId = insertNodeState.value.connectionId;
+ const { connectionId } = insertNodeState.value;
const [startPortId, endPortId] = connectionId.split('_');
const connectionIndex = findConnectionIndex(startPortId, endPortId);
@@ -224,9 +293,11 @@ export default defineComponent({
const newNodeId = createNode(nodeType, nodePosition);
if (newNodeId) {
schema.value.contents.splice(connectionIndex, 1);
- const connectionElement = document.getElementById(connectionId);
- if (connectionElement) {
- connectionElement.remove();
+ if (connectionId) {
+ const connectionElement = document.getElementById(connectionId);
+ if (connectionElement && connectionElement.parentNode) {
+ connectionElement.remove();
+ }
}
const newNodeInputId = `${newNodeId}-input`;
@@ -253,8 +324,11 @@ export default defineComponent({
schema.value.contents.push(connection1, connection2);
context.emit('update:modelValue', schema.value);
- nextTick(() => {
- setTimeout(() => {
+ // 等待DOM更新
+ await nextTick();
+
+ setTimeout(() => {
+ if (startPortId && newNodeInputId) {
const startElement = document.getElementById(startPortId);
const newNodeInputElement = document.getElementById(newNodeInputId);
if (startElement && newNodeInputElement) {
@@ -262,7 +336,9 @@ export default defineComponent({
const inputPos = getElementPosition(newNodeInputElement);
useBezierCurveComposition.connect(startPortId, newNodeInputId, startPos, inputPos);
}
+ }
+ if (newNodeOutputId && endPortId) {
const newNodeOutputElement = document.getElementById(newNodeOutputId);
const endElement = document.getElementById(endPortId);
if (newNodeOutputElement && endElement) {
@@ -270,8 +346,8 @@ export default defineComponent({
const endPos = getElementPosition(endElement);
useBezierCurveComposition.connect(newNodeOutputId, endPortId, outputPos, endPos);
}
- }, 100);
- });
+ }
+ }, 100);
}
handleInsertNodeClose();
@@ -384,6 +460,39 @@ export default defineComponent({
closeContextMenu();
};
+ const getElementPosition = (element: HTMLElement): string => {
+ const classNames = element.className.split(' ');
+ const circleClass = classNames.find(name => name.startsWith('circle-'));
+ if (circleClass) {
+ switch (circleClass) {
+ case 'circle-left':
+ return 'west';
+ case 'circle-right':
+ return 'east';
+ case 'circle-top':
+ return 'north';
+ case 'circle-bottom':
+ return 'south';
+ default:
+ break;
+ }
+ }
+ const portClass = classNames.find(name => name.startsWith('port-'));
+ if (portClass) {
+ switch (portClass) {
+ case 'port-left':
+ return 'west';
+ case 'port-top':
+ return 'north';
+ case 'port-bottom':
+ return 'south';
+ default:
+ break;
+ }
+ }
+ return 'center';
+ };
+
const handleKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Delete' || event.key === 'Backspace') {
if (selectedNodeId.value) {
@@ -402,9 +511,12 @@ export default defineComponent({
provide('use-connections-composition', useConnectionsComposition);
provide('use-drawing-bezier-composition', useDrawingBezierComposition);
- onMounted(() => {
+ onMounted(async () => {
schema.value = props.modelValue;
- connectionManager.restoreConnections();
+
+ // 初始化画布(包括加载schema)- 这是关键调用
+ await initializeCanvas();
+
document.addEventListener('keydown', handleKeyDown);
});
@@ -418,6 +530,34 @@ export default defineComponent({
return () => {
const { showNodeSelector, nodeSelectorPosition } = useDrawingBezierComposition;
+
+ // 加载中显示提示 - 阻止渲染直到Schema加载完成
+ if (isLoading.value) {
+ return (
+
+ );
+ }
+
+ // 只有在Schema加载完成后才渲染节点
+ if (!isSchemaLoaded.value) {
+ return (
+
+ );
+ }
+
+ // 正常渲染 - 此时Schema已确保加载完成
return (
{
+ const classNames = element.className.split(' ');
+ const circleClass = classNames.find(name => name.startsWith('circle-'));
+ if (circleClass) {
+ switch (circleClass) {
+ case 'circle-left':
+ return 'west';
+ case 'circle-right':
+ return 'east';
+ case 'circle-top':
+ return 'north';
+ case 'circle-bottom':
+ return 'south';
+ default:
+ break;
+ }
+ }
+ const portClass = classNames.find(name => name.startsWith('port-'));
+ if (portClass) {
+ switch (portClass) {
+ case 'port-left':
+ return 'west';
+ case 'port-right':
+ return 'east';
+ case 'port-top':
+ return 'north';
+ case 'port-bottom':
+ return 'south';
+ default:
+ break;
+ }
+ }
+ return 'center';
+};
+
+export interface ConnectionManagerOptions {
+ schema: Ref
;
+ useBezierCurveComposition: UseBezierCurve;
+ useConnectionsComposition: UseConnections;
+ onSchemaUpdate: (newSchema: any) => void;
+}
+
+export interface UseConnectionManager {
+ restoreConnections: () => Promise;
+ createConnection: (startPortId: string, endPortId: string, startNodeId: string, endNodeId: string) => Promise;
+ deleteConnection: (startPortId: string, endPortId: string) => boolean;
+ insertNodeInConnection: (connectionId: string, newNodeId: string, newNodeType: string) => Promise;
+ getConnectionsByNodeId: (nodeId: string) => Array<{ startPortId: string; endPortId: string; connectionId: string }>;
+}
+
+export function useConnectionManager(options: ConnectionManagerOptions): UseConnectionManager {
+ const { schema, useBezierCurveComposition, useConnectionsComposition, onSchemaUpdate } = options;
+
+ const extractNodeIdFromPortId = (portId: string): string => {
+ const selectorOptionMatch = portId.match(/^(.+?)-option-\d+-output$/);
+ if (selectorOptionMatch) {
+ return selectorOptionMatch[1];
+ }
+ const conditionMatch = portId.match(/^(.+?)-condition-\d+-output$/);
+ if (conditionMatch) {
+ return conditionMatch[1];
+ }
+ const simpleMatch = portId.match(/^(.+?)-(input|output)$/);
+ if (simpleMatch) {
+ return simpleMatch[1];
+ }
+ return portId;
+ };
+
+ const waitForElement = async (elementId: string, maxAttempts = 10): Promise => {
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
+ const element = document.getElementById(elementId);
+ if (element) {
+ return element;
+ }
+ await new Promise(resolve => setTimeout(resolve, 50));
+ }
+ throw new Error(`Element not found: ${elementId}`);
+ };
+
+ const saveConnectionToSchema = (startPortId: string, endPortId: string): void => {
+ const connection = {
+ id: `connection-${Date.now()}`,
+ type: 'flow-connection',
+ source: startPortId,
+ target: endPortId,
+ lineType: 'bezier',
+ lineStyle: {
+ strokeWidth: 2,
+ strokeColor: '#4d53e8'
+ }
+ };
+ schema.value.contents.push(connection);
+ onSchemaUpdate(schema.value);
+ };
+
+ const restoreConnections = async (): Promise => {
+ if (!schema.value?.contents) {
+ return;
+ }
+ const connections = schema.value.contents.filter(
+ (item: any) => item.type === 'flow-connection'
+ );
+
+ const connectionPromises = connections.map(async (conn: any) => {
+ const startElement = await waitForElement(conn.source);
+ const endElement = await waitForElement(conn.target);
+ const startPos = getElementPosition(startElement);
+ const endPos = getElementPosition(endElement);
+ useBezierCurveComposition.connect(conn.source, conn.target, startPos, endPos);
+ useConnectionsComposition.addConnection(
+ extractNodeIdFromPortId(conn.source),
+ conn.source,
+ extractNodeIdFromPortId(conn.target),
+ conn.target
+ );
+ });
+
+ await Promise.all(connectionPromises);
+ };
+
+ const createConnection = async (startPortId: string, endPortId: string, startNodeId: string, endNodeId: string): Promise => {
+ const startElement = await waitForElement(startPortId);
+ const endElement = await waitForElement(endPortId);
+ const startPos = getElementPosition(startElement);
+ const endPos = getElementPosition(endElement);
+ useBezierCurveComposition.connect(startPortId, endPortId, startPos, endPos);
+ useConnectionsComposition.addConnection(startNodeId, startPortId, endNodeId, endPortId);
+ saveConnectionToSchema(startPortId, endPortId);
+ return true;
+ };
+
+ const deleteConnection = (startPortId: string, endPortId: string): boolean => {
+ const connectionId = `${startPortId}_${endPortId}`;
+ const connectionElement = document.getElementById(connectionId);
+ if (connectionElement) {
+ connectionElement.remove();
+ }
+ const connectionIndex = schema.value.contents.findIndex(
+ (item: any) => item.type === 'flow-connection' && item.source === startPortId && item.target === endPortId
+ );
+ if (connectionIndex >= 0) {
+ schema.value.contents.splice(connectionIndex, 1);
+ }
+ useConnectionsComposition.removeConnection(startPortId, endPortId);
+ onSchemaUpdate(schema.value);
+ return true;
+ };
+
+ const insertNodeInConnection = async (connectionId: string, newNodeId: string, newNodeType: string): Promise => {
+ const [startPortId, endPortId] = connectionId.split('_');
+ const connectionIndex = schema.value.contents.findIndex(
+ (item: any) => item.type === 'flow-connection' && item.source === startPortId && item.target === endPortId
+ );
+ if (connectionIndex < 0) {
+ return false;
+ }
+ const originalConnection = schema.value.contents[connectionIndex];
+ if (!deleteConnection(startPortId, endPortId)) {
+ return false;
+ }
+ const newNodeInputId = `${newNodeId}-input`;
+ const newNodeOutputId = `${newNodeId}-output`;
+ const success1 = await createConnection(
+ startPortId,
+ newNodeInputId,
+ extractNodeIdFromPortId(startPortId),
+ newNodeId
+ );
+ const success2 = await createConnection(
+ newNodeOutputId,
+ endPortId,
+ newNodeId,
+ extractNodeIdFromPortId(endPortId)
+ );
+ if (success1 && success2) {
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ const getConnectionsByNodeId = (nodeId: string) => {
+ return useConnectionsComposition.getConnectionsByNodeId(nodeId);
+ };
+
+ return {
+ restoreConnections,
+ createConnection,
+ deleteConnection,
+ insertNodeInConnection,
+ getConnectionsByNodeId
+ };
+}
diff --git a/packages/ui-vue/components/flow-canvas/src/hooks/use-port-event-dispatcher.ts b/packages/ui-vue/components/flow-canvas/src/hooks/use-port-event-dispatcher.ts
index d1ca0a49fea3875a8668b37fa50ae6610ceaf656..9cbe4a4b70e96581276c1dec84e78ad078989b8d 100644
--- a/packages/ui-vue/components/flow-canvas/src/hooks/use-port-event-dispatcher.ts
+++ b/packages/ui-vue/components/flow-canvas/src/hooks/use-port-event-dispatcher.ts
@@ -41,6 +41,11 @@ export function usePortEventDispatcher() {
return portId;
}
+ function resetDragState() {
+ isDragging.value = false;
+ dragStartInfo.value = null;
+ }
+
const handleOutputPortMouseDown = (port: Port, event: MouseEvent) => {
event.stopPropagation();
@@ -55,11 +60,6 @@ export function usePortEventDispatcher() {
drawFrom(port.id, event);
};
- function resetDragState() {
- isDragging.value = false;
- dragStartInfo.value = null;
- }
-
const handleInputPortMouseUp = (port: Port, event: MouseEvent) => {
event.stopPropagation();
diff --git a/packages/ui-vue/components/flow-canvas/src/schema/configs/node-type-options.ts b/packages/ui-vue/components/flow-canvas/src/schema/configs/node-type-options.ts
index 20ad99f108161c968b00ad00751af05b75eb236e..ef4e2447c69bcdf698696dbe0f58ca7be8021bf4 100644
--- a/packages/ui-vue/components/flow-canvas/src/schema/configs/node-type-options.ts
+++ b/packages/ui-vue/components/flow-canvas/src/schema/configs/node-type-options.ts
@@ -111,7 +111,9 @@ export function getNodeOptionById(id: string): NodeTypeOption | undefined {
for (const category of nodeTypeOptions) {
const found = [...category.leftOptions, ...category.rightOptions]
.find(option => option.id === id);
- if (found) return found;
+ if (found) {
+ return found;
+ }
}
return undefined;
}
@@ -138,4 +140,4 @@ export function getAllNodeTypeIds(): string[] {
export function isValidNodeType(nodeTypeId: string): boolean {
return getAllNodeTypeIds().includes(nodeTypeId);
-}
\ No newline at end of file
+}
diff --git a/packages/ui-vue/components/flow-canvas/src/schema/test-node.schema.json b/packages/ui-vue/components/flow-canvas/src/schema/test-node.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..4550742b0f2bae980c95d494c8f9a38fd524f928
--- /dev/null
+++ b/packages/ui-vue/components/flow-canvas/src/schema/test-node.schema.json
@@ -0,0 +1,87 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://farris-design.gitee.io/test-node.schema.json",
+ "title": "Test Node",
+ "description": "A dynamically loaded test node to verify async loading",
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "flow-node.schema.json"
+ }
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique identifier",
+ "default": "test-node-${nodeId}"
+ },
+ "type": {
+ "type": "string",
+ "description": "Node type",
+ "default": "test-node"
+ },
+ "label": {
+ "type": "string",
+ "description": "Node display label",
+ "default": "测试节点"
+ },
+ "input": {
+ "type": "array",
+ "description": "Input ports",
+ "items": {
+ "$ref": "flow-port.schema.json"
+ },
+ "default": [
+ {
+ "id": "${nodeId}-input",
+ "type": "port",
+ "direction": "input",
+ "position": "left",
+ "autoConnect": true
+ }
+ ]
+ },
+ "output": {
+ "type": "array",
+ "description": "Output ports",
+ "items": {
+ "$ref": "flow-port.schema.json"
+ },
+ "default": [
+ {
+ "id": "${nodeId}-output",
+ "type": "port",
+ "direction": "output",
+ "position": "right",
+ "autoConnect": true
+ }
+ ]
+ },
+ "config": {
+ "type": "object",
+ "description": "Custom configuration for test node",
+ "properties": {
+ "testValue": {
+ "type": "string",
+ "default": "这是动态加载的测试节点"
+ },
+ "color": {
+ "type": "string",
+ "default": "#9333ea"
+ }
+ },
+ "default": {
+ "testValue": "这是动态加载的测试节点",
+ "color": "#9333ea"
+ }
+ }
+ },
+ "required": [
+ "id",
+ "type",
+ "label",
+ "input",
+ "output",
+ "config"
+ ]
+}
\ No newline at end of file
diff --git a/packages/ui-vue/components/flow-canvas/src/utils/element-utils.ts b/packages/ui-vue/components/flow-canvas/src/utils/element-utils.ts
index e7908002588ff392a1a75cf6f4f254279ce3077b..0e130bdeec1c5c9a4b94ea457198871189df1abf 100644
--- a/packages/ui-vue/components/flow-canvas/src/utils/element-utils.ts
+++ b/packages/ui-vue/components/flow-canvas/src/utils/element-utils.ts
@@ -31,4 +31,4 @@ export const getElementPosition = (element: HTMLElement): string => {
}
}
return 'center';
-};
\ No newline at end of file
+};
diff --git a/packages/ui-vue/components/flow-canvas/src/utils/render-ports.util.ts b/packages/ui-vue/components/flow-canvas/src/utils/render-ports.util.ts
index c525146b639c06af4c0bd4d0a890bdc1bd5f446b..6f20c37d09a032826aad9bad755e4adc05cc1731 100644
--- a/packages/ui-vue/components/flow-canvas/src/utils/render-ports.util.ts
+++ b/packages/ui-vue/components/flow-canvas/src/utils/render-ports.util.ts
@@ -15,6 +15,14 @@ export interface PortRenderOptions {
}
const PORT_OFFSET_CONFIG = {
+ 'Selector Node': {
+ output: 18,
+ input: 4
+ },
+ 'If Node': {
+ output: 18,
+ input: 4
+ },
'selector-node': {
output: 18,
input: 4
@@ -142,4 +150,4 @@ export function renderSchemaPorts(
});
return ports;
-}
\ No newline at end of file
+}