diff --git a/packages/dag/src/EditorView.vue b/packages/dag/src/EditorView.vue index 67dd7ec67..4c41bf8ec 100644 --- a/packages/dag/src/EditorView.vue +++ b/packages/dag/src/EditorView.vue @@ -54,6 +54,7 @@ const { onDeleteNode, onDeleteNodes, onAddNode, + canAddNode, onMoveNodePosition, onUpdateNodesPosition, onClickNode, @@ -496,6 +497,7 @@ provide('dataflow', dataflow) provide('dataflowName', dataflowName) provide('dataflowDesc', dataflowDesc) provide('onNameInputChange', onNameInputChange) +provide('canAddNode', canAddNode) provide('formScope', formScope) provide('isSaving', isSaving) provide('previewData', previewData) diff --git a/packages/dag/src/MonitorView.vue b/packages/dag/src/MonitorView.vue index 6b4533324..d87c72a9c 100644 --- a/packages/dag/src/MonitorView.vue +++ b/packages/dag/src/MonitorView.vue @@ -69,6 +69,7 @@ const { onDeleteNode, onDeleteNodes, onAddNode, + canAddNode, onMoveNodePosition, onUpdateNodesPosition, onClickNode, @@ -641,6 +642,7 @@ provide('buttonShowMap', buttonShowMap) provide('dataflow', dataflow) provide('dataflowName', dataflowName) provide('dataflowDesc', dataflowDesc) +provide('canAddNode', canAddNode) provide('onNameInputChange', onNameInputChange) provide('formScope', formScope) provide('isSaving', isSaving) diff --git a/packages/dag/src/RecordView.vue b/packages/dag/src/RecordView.vue index 506b14d35..137f1ab26 100644 --- a/packages/dag/src/RecordView.vue +++ b/packages/dag/src/RecordView.vue @@ -31,6 +31,7 @@ const { onDeleteNode, onDeleteNodes, onAddNode, + canAddNode, onMoveNodePosition, onUpdateNodesPosition, onClickNode, @@ -65,6 +66,7 @@ provide('dataflow', dataflow) provide('dataflowName', dataflowName) provide('dataflowDesc', dataflowDesc) provide('onNameInputChange', onNameInputChange) +provide('canAddNode', canAddNode) provide('formScope', formScope) provide('isSaving', isSaving) diff --git a/packages/dag/src/components/NodesPanel.vue b/packages/dag/src/components/NodesPanel.vue index 19f5169e2..ede57d37d 100644 --- a/packages/dag/src/components/NodesPanel.vue +++ b/packages/dag/src/components/NodesPanel.vue @@ -26,9 +26,16 @@ const pageSize = 20 const dataflow = inject('dataflow') const isSyncTask = inject('isSyncTask') const onAddNode = inject<(node: any) => void>('onAddNode') +const canAddNode = inject<(node: any) => boolean>('canAddNode') const onCreateConnection = inject<(connection: any) => void>('onCreateConnection') +const addNode = (node: any) => { + if (canAddNode && !canAddNode(node)) return false + onAddNode?.(node) + return true +} + const { dragNode, dragStarting, @@ -37,7 +44,7 @@ const { onDragMove, onDragStop, onDrop, -} = useDnD({ emit, onAddNode }) +} = useDnD({ emit, onAddNode: addNode }) const showConnectionSearch = ref(false) const connectionSearchRef = ref { node.attrs.position = [maxX + maxNodeWidth + X_OFFSET, maxY] } - onAddNode?.(node) + addNode(node) } } @@ -396,13 +403,17 @@ const handleDblClickAddNode = (node: any) => { if (dataflowStore.stateIsReadonly) return historyStore.startRecordingUndo() + if (canAddNode && !canAddNode(node)) { + historyStore.stopRecordingUndo() + return + } const allNodes = dataflowStore.dag.nodes if (!allNodes.length) { // 画布为空,放到视口中心 node.attrs.position = getViewportCenterPosition() - onAddNode?.(node) + addNode(node) } else { // 找到一个没有输出的节点作为上游 const source = allNodes.find((n) => !n.$outputs || n.$outputs.length === 0) @@ -427,12 +438,12 @@ const handleDblClickAddNode = (node: any) => { node.attrs.position = findNonOverlappingPosition( position as [number, number], ) - onAddNode?.(node) + addNode(node) onCreateConnection?.({ source: source.id, target: node.id }) } else { // 不能连线或没有可用上游:放到最右侧节点右边 node.attrs.position = findNonOverlappingPosition(getRightmostPosition()) - onAddNode?.(node) + addNode(node) } } diff --git a/packages/dag/src/components/elements/NodesPopover.vue b/packages/dag/src/components/elements/NodesPopover.vue index 45f10926f..5a497e29c 100644 --- a/packages/dag/src/components/elements/NodesPopover.vue +++ b/packages/dag/src/components/elements/NodesPopover.vue @@ -35,6 +35,7 @@ const historyStore = useHistoryStore() // Inject tracking functions for history support const dataflow = inject>('dataflow')! const onAddNode = inject<(node: any) => void>('onAddNode') +const canAddNode = inject<(node: any) => boolean>('canAddNode') const onCreateConnection = inject<(connection: any) => void>('onCreateConnection') const onDeleteConnection = @@ -300,6 +301,10 @@ const handleAddNode = (node: any) => { // 开始批量记录 - 所有操作作为一个 BulkCommand historyStore.startRecordingUndo() + if (canAddNode && !canAddNode(node)) { + historyStore.stopRecordingUndo() + return + } if (nextNodeId && prevNodeId) { // 在两个节点之间添加 @@ -315,7 +320,6 @@ const handleAddNode = (node: any) => { historyStore.stopRecordingUndo() return } - const afterNodes = dataflowStore.getAfterNodesInSameBranch(nextNodeId) const nextNode = findNode(nextNodeId)! const prevCanvasNode = findNode(prevNodeId)! @@ -353,9 +357,7 @@ const handleAddNode = (node: any) => { target: nextNodeId, }) - // 添加新节点 onAddNode?.(node) - // 添加新连线 onCreateConnection?.({ source: prevNodeId, diff --git a/packages/dag/src/composables/useCanvasOperation.ts b/packages/dag/src/composables/useCanvasOperation.ts index 61a71f009..2ebc5de60 100644 --- a/packages/dag/src/composables/useCanvasOperation.ts +++ b/packages/dag/src/composables/useCanvasOperation.ts @@ -548,7 +548,32 @@ export function useCanvasOperation() { historyStore.stopRecordingUndo() } + const isEnabledJoinNode = (node: any) => + node?.type === 'join_processor' && !node.disabled && !node.attrs?.disabled + + const getEnabledJoinNodeCount = () => + dataflowStore.dag.nodes.filter(isEnabledJoinNode).length + + const showJoinNodeLimitMessage = () => { + ElMessage.error(t('packages_dag_join_node_limit')) + } + + const canAddJoinNodes = (count = 1) => { + if (!count) return true + return getEnabledJoinNodeCount() + count <= 1 + } + + const canAddNode = (node: any) => { + if (isEnabledJoinNode(node) && !canAddJoinNodes()) { + showJoinNodeLimitMessage() + return false + } + return true + } + const onAddNode = (node: any, { trackHistory = true } = {}) => { + if (!canAddNode(node)) return false + if ( (node.type === 'table' || node.type === 'database') && node.attrs?.pdkHash @@ -587,6 +612,8 @@ export function useCanvasOperation() { if (trackHistory) { historyStore.pushCommandToUndo(new AddNodeCommand(node, Date.now())) } + + return true } // ========== Copy / Paste ========== @@ -687,6 +714,11 @@ export function useCanvasOperation() { const edges: any[] = cloneDeep(data.edges || []) if (!nodes.length) return [] + if (!canAddJoinNodes(nodes.filter(isEnabledJoinNode).length)) { + showJoinNodeLimitMessage() + return [] + } + // 计算原始节点组的包围盒左上角 let origMinX = Infinity let origMinY = Infinity @@ -907,6 +939,13 @@ export function useCanvasOperation() { await Promise.all(nodes.map((node) => validateNode(node))) } + // --- Join 节点数量校验 --- + const validateJoin = () => { + return getEnabledJoinNodeCount() > 1 + ? t('packages_dag_join_node_limit') + : '' + } + // --- DAG 结构校验 --- const validateDag = () => { let someErrorMsg = '' @@ -1456,6 +1495,7 @@ export function useCanvasOperation() { await validateAllNodes(nodes) return await eachValidate( + validateJoin, validateSetting, validateDag, validateAgent, @@ -2140,6 +2180,7 @@ export function useCanvasOperation() { onDeleteNode, onDeleteNodes, onAddNode, + canAddNode, onCopyNodes, onPasteNodes, validate, diff --git a/packages/dag/src/locale/lang/en.js b/packages/dag/src/locale/lang/en.js index 8037b402d..b8ffce043 100644 --- a/packages/dag/src/locale/lang/en.js +++ b/packages/dag/src/locale/lang/en.js @@ -88,6 +88,8 @@ export default { packages_dag_editor_cell_validate_empty_name: 'Name is required.', packages_dag_editor_cell_validate_none_data_node: 'At least 2 data node in graph', + packages_dag_join_node_limit: + 'This task supports only one Join node. For multi-level Joins, write the first Join result to an intermediate table, then create a second task to read the intermediate table and perform the next Join.', packages_dag_dag_save_fail: 'Failed to save, please check the node.', packages_dag_message_operation_error: 'Operation Failed', packages_dag_message_resetOk: 'Reset Success', diff --git a/packages/dag/src/locale/lang/zh-CN.js b/packages/dag/src/locale/lang/zh-CN.js index 7baa9efa8..37c38d8ba 100644 --- a/packages/dag/src/locale/lang/zh-CN.js +++ b/packages/dag/src/locale/lang/zh-CN.js @@ -77,6 +77,8 @@ export default { packages_dag_task_list_button_monitor: '运行监控', packages_dag_editor_cell_validate_empty_name: '名称必填.', packages_dag_editor_cell_validate_none_data_node: '至少有两个数据节点', + packages_dag_join_node_limit: + '当前任务仅支持一个 Join 节点;如需多层 Join,请将第一层 Join 结果写入中间表,再创建第二个任务读取中间表并执行下一层 Join。', packages_dag_dag_save_fail: '保存出错,请检查节点', packages_dag_message_operation_error: '操作失败', packages_dag_message_resetOk: '重置成功', diff --git a/packages/dag/src/locale/lang/zh-TW.js b/packages/dag/src/locale/lang/zh-TW.js index fe33f1261..9759ea041 100644 --- a/packages/dag/src/locale/lang/zh-TW.js +++ b/packages/dag/src/locale/lang/zh-TW.js @@ -77,6 +77,8 @@ export default { packages_dag_task_list_button_monitor: '運行監控', packages_dag_editor_cell_validate_empty_name: '名稱必填.', packages_dag_editor_cell_validate_none_data_node: '至少有兩個數據節點', + packages_dag_join_node_limit: + '當前任務僅支持一個 Join 節點;如需多層 Join,請將第一層 Join 結果寫入中間表,再建立第二個任務讀取中間表並執行下一層 Join。', packages_dag_dag_save_fail: '保存出錯,請檢查節點', packages_dag_message_operation_error: '操作失敗', packages_dag_message_resetOk: '重置成功',