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 ( +
+
+ 🧪 {label.value} +
+
+
+ 类型: {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 +}