From 0c8d8f187c0816f6cf82b7b5c4a9aacb5a69281e Mon Sep 17 00:00:00 2001 From: dengqichen Date: Fri, 14 Nov 2025 17:18:15 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B6=88=E6=81=AF=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DeployFlowGraphModal.tsx | 28 +-- .../Workflow/Design/components/CustomEdge.tsx | 194 ++++++++++-------- .../Design/nodes/JenkinsBuildNode.tsx | 52 ++++- 3 files changed, 166 insertions(+), 108 deletions(-) diff --git a/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx b/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx index ad1239da..9d432c73 100644 --- a/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx +++ b/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx @@ -71,14 +71,14 @@ const getLayoutedElements = ( const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); - const nodeWidth = 200; - const nodeHeight = 150; + const nodeWidth = 180; + const nodeHeight = 120; const isHorizontal = direction === 'LR'; dagreGraph.setGraph({ rankdir: direction, - nodesep: isHorizontal ? 80 : 100, // 同一层节点间距:横向80,纵向100 - ranksep: isHorizontal ? 150 : 120, // 层级间距:横向150,纵向120 + nodesep: isHorizontal ? 90 : 120, // 同一层节点间距:横向80,纵向100 + ranksep: isHorizontal ? 170 : 160, // 层级间距:横向150,纵向120 marginx: 40, marginy: 40, align: 'UL', // 对齐方式 @@ -138,7 +138,7 @@ const CustomFlowNode: React.FC = ({ data }) => { const nodeContent = (
= ({ data }) => { {/* 节点状态 - 使用徽章样式 */}
= ({ data }) => { )} {/* 弹性空间,让底部提示始终在底部 */} -
+
{/* 错误提示 - 增强样式 */} {hasFailed && errorMessage && ( -
+
- 查看错误 + 查看错误
)} {/* 查看日志提示 - 增强样式 */} {canViewLog && !hasFailed && ( -
+
- 点击查看日志 + 查看日志
)}
@@ -675,7 +675,7 @@ export const DeployFlowGraphModal: React.FC = ({ id: edge.id || `edge-${source}-${target}-${index}`, source, target, - type: 'smoothstep', // 使用平滑曲线类型 + type: layoutDirection === 'TB' ? 'step' : 'smoothstep', // TB用直角折线,LR用平滑曲线 animated, style: { stroke: strokeColor, @@ -690,7 +690,7 @@ export const DeployFlowGraphModal: React.FC = ({ }, }; }); - }, [flowData, nodeInstanceMap, visibleNodeIds]); + }, [flowData, nodeInstanceMap, visibleNodeIds, layoutDirection]); // 应用自动布局 const { nodes: layoutedNodes, edges: layoutedEdges } = useMemo(() => { @@ -786,7 +786,7 @@ export const DeployFlowGraphModal: React.FC = ({ onNodeClick={onNodeClick} fitView className="bg-muted/10" - fitViewOptions={{ padding: 0.15, maxZoom: 1.2, minZoom: 0.3 }} + fitViewOptions={{ padding: 0.22, maxZoom: 1.2, minZoom: 0.3 }} nodesDraggable={false} nodesConnectable={false} elementsSelectable={true} diff --git a/frontend/src/pages/Workflow/Design/components/CustomEdge.tsx b/frontend/src/pages/Workflow/Design/components/CustomEdge.tsx index 778e6330..cfd003c4 100644 --- a/frontend/src/pages/Workflow/Design/components/CustomEdge.tsx +++ b/frontend/src/pages/Workflow/Design/components/CustomEdge.tsx @@ -1,11 +1,11 @@ import React, { useState, useMemo, useCallback } from 'react'; -import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, useReactFlow } from '@xyflow/react'; +import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath, useReactFlow } from '@xyflow/react'; import type { FlowNode, ControlPoint } from '../types'; import { convertToDisplayName } from '@/utils/workflow/variableConversion'; /** * 自定义边组件 - * 支持hover高亮、样式优化和拖动改变路径 + * 使用简单贝塞尔曲线 + 单控制点,支持 hover 高亮和拖动调整路径 */ const CustomEdge: React.FC = ({ id, @@ -25,9 +25,6 @@ const CustomEdge: React.FC = ({ const [isDragging, setIsDragging] = useState(false); const { getNodes, setEdges, screenToFlowPosition } = useReactFlow(); - // 从 data 中获取自定义的控制点 - const customControlPoint = data?.controlPoint as ControlPoint | undefined; - // 将 label 中的 UUID 转换为节点名 const displayLabel = useMemo(() => { if (!label || typeof label !== 'string') { @@ -38,77 +35,98 @@ const CustomEdge: React.FC = ({ return convertToDisplayName(label, allNodes); }, [label, getNodes]); - // 计算三个拐点(vertices):优先用数据中的 vertices;否则从旧的 controlPoint 推导;最后用默认1/4、1/2、3/4 - const vertices: ControlPoint[] = useMemo(() => { - const dataVertices = (data as any)?.vertices as ControlPoint[] | undefined; - if (Array.isArray(dataVertices) && dataVertices.length === 3) return dataVertices; - const cp = customControlPoint; - if (cp) { - return [ - { x: (sourceX + cp.x) / 2, y: (sourceY + cp.y) / 2 }, - { x: cp.x, y: cp.y }, - { x: (cp.x + targetX) / 2, y: (cp.y + targetY) / 2 }, - ]; - } - return [ - { x: sourceX + (targetX - sourceX) * 0.25, y: sourceY + (targetY - sourceY) * 0.25 }, - { x: (sourceX + targetX) / 2, y: (sourceY + targetY) / 2 }, - { x: sourceX + (targetX - sourceX) * 0.75, y: sourceY + (targetY - sourceY) * 0.75 }, - ]; - }, [data, customControlPoint, sourceX, sourceY, targetX, targetY]); + // 计算单个控制点:优先使用保存的 controlPoint,否则使用智能默认位置 + const controlPoint: ControlPoint = useMemo(() => { + const savedCP = (data as any)?.controlPoint as ControlPoint | undefined; + if (savedCP) return savedCP; - // 若数据中缺少 vertices,则在首次渲染时写回,确保后续保存能持久化 + // 智能默认位置:根据源和目标位置关系计算 + const dx = targetX - sourceX; + const dy = targetY - sourceY; + const distance = Math.sqrt(dx * dx + dy * dy); + + // 控制点在中点位置,垂直偏移距离的 20%,方向取决于节点位置关系 + const midX = (sourceX + targetX) / 2; + const midY = (sourceY + targetY) / 2; + + // 计算垂直方向的偏移(顺时针 90 度) + const offsetRatio = 0.2; + const offsetX = -dy / distance * distance * offsetRatio; + const offsetY = dx / distance * distance * offsetRatio; + + return { + x: midX + offsetX, + y: midY + offsetY, + }; + }, [data, sourceX, sourceY, targetX, targetY]); + + // 若数据中缺少 controlPoint,则在首次渲染时写回 React.useEffect(() => { - const hasDataVertices = Array.isArray((data as any)?.vertices) && (data as any).vertices.length === 3; - if (!hasDataVertices) { - setEdges((eds) => eds.map(ed => ed.id === id ? { ...ed, data: { ...ed.data, vertices } } : ed)); + if (!(data as any)?.controlPoint) { + setEdges((eds) => eds.map(ed => + ed.id === id ? { ...ed, data: { ...ed.data, controlPoint } } : ed + )); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // 生成路径:使用 Catmull-Rom -> Cubic Bezier 插值,曲线严格经过拐点 + // 使用 Catmull-Rom 样条曲线,确保曲线严格经过控制点且平滑 const [edgePath, labelX, labelY] = useMemo(() => { - // 拼接点序列:起点、3个拐点、终点 - const pts = [ - { x: sourceX, y: sourceY }, - vertices[0], - vertices[1], - vertices[2], - { x: targetX, y: targetY }, - ]; - // Catmull-Rom 转三次贝塞尔:C1 = P0 + (P1 - P_{-1})/6, C2 = P1 - (P2 - P0)/6 - const segs: string[] = []; - for (let i = 0; i < pts.length - 1; i++) { - const p_1 = i === 0 ? pts[i] : pts[i - 1]; - const p0 = pts[i]; - const p1 = pts[i + 1]; - const p2 = i + 2 < pts.length ? pts[i + 2] : p1; - const c1x = p0.x + (p1.x - p_1.x) / 6; - const c1y = p0.y + (p1.y - p_1.y) / 6; - const c2x = p1.x - (p2.x - p0.x) / 6; - const c2y = p1.y - (p2.y - p0.y) / 6; - segs.push(`C ${c1x},${c1y} ${c2x},${c2y} ${p1.x},${p1.y}`); - } - const path = `M ${pts[0].x},${pts[0].y} ${segs.join(' ')}`; - // 标签显示在第二个拐点 - return [path, vertices[1].x, vertices[1].y] as [string, number, number]; - }, [vertices, sourceX, sourceY, targetX, targetY]); + // Catmull-Rom 样条曲线会经过所有关键点 + // 转换为三次贝塞尔曲线:需要计算切线 + + // 点序列:起点 -> 控制点 -> 终点 + const p0 = { x: sourceX, y: sourceY }; + const p1 = controlPoint; + const p2 = { x: targetX, y: targetY }; + + // 为了使用 Catmull-Rom,需要虚拟的前后点(用于计算切线) + // 虚拟前点:起点的镜像延伸 + const p_1 = { + x: p0.x - (p1.x - p0.x) * 0.3, + y: p0.y - (p1.y - p0.y) * 0.3, + }; + + // 虚拟后点:终点的镜像延伸 + const p3 = { + x: p2.x + (p2.x - p1.x) * 0.3, + y: p2.y + (p2.y - p1.y) * 0.3, + }; + + // Catmull-Rom 转贝塞尔:第一段(p0 -> p1) + // 切线: t0 = (p1 - p_1) / 2, t1 = (p2 - p0) / 2 + const c1x = p0.x + (p1.x - p_1.x) / 6; + const c1y = p0.y + (p1.y - p_1.y) / 6; + const c2x = p1.x - (p2.x - p0.x) / 6; + const c2y = p1.y - (p2.y - p0.y) / 6; + + // Catmull-Rom 转贝塞尔:第二段(p1 -> p2) + // 切线: t0 = (p2 - p0) / 2, t1 = (p3 - p1) / 2 + const c3x = p1.x + (p2.x - p0.x) / 6; + const c3y = p1.y + (p2.y - p0.y) / 6; + const c4x = p2.x - (p3.x - p1.x) / 6; + const c4y = p2.y - (p3.y - p1.y) / 6; + + // 拼接两段三次贝塞尔曲线 + const path = `M ${p0.x},${p0.y} C ${c1x},${c1y} ${c2x},${c2y} ${p1.x},${p1.y} C ${c3x},${c3y} ${c4x},${c4y} ${p2.x},${p2.y}`; + + // 标签显示在控制点位置 + return [path, controlPoint.x, controlPoint.y] as [string, number, number]; + }, [controlPoint, sourceX, sourceY, targetX, targetY]); - // 拖动处理(支持第 idx 个拐点) - const handleVertexMouseDown = useCallback((idx: number, e: React.MouseEvent) => { + // 拖动控制点处理 + const handleControlPointMouseDown = useCallback((e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); setIsDragging(true); const handleMove = (moveEvent: MouseEvent) => { const flowPosition = screenToFlowPosition({ x: moveEvent.clientX, y: moveEvent.clientY }); - setEdges((eds) => eds.map(ed => { - if (ed.id !== id) return ed; - const old = (ed as any).data?.vertices as ControlPoint[] | undefined; - const base = Array.isArray(old) && old.length === 3 ? [...old] : vertices.slice(); - base[idx] = { x: flowPosition.x, y: flowPosition.y }; - return { ...ed, data: { ...ed.data, vertices: base } } as any; - })); + setEdges((eds) => eds.map(ed => + ed.id === id + ? { ...ed, data: { ...ed.data, controlPoint: { x: flowPosition.x, y: flowPosition.y } } } + : ed + )); }; const handleUp = () => { @@ -119,7 +137,8 @@ const CustomEdge: React.FC = ({ window.addEventListener('mousemove', handleMove); window.addEventListener('mouseup', handleUp); - }, [id, setEdges, screenToFlowPosition, vertices]); + }, [id, setEdges, screenToFlowPosition]); + // 根据状态确定样式 const edgeStyle = { @@ -129,7 +148,6 @@ const CustomEdge: React.FC = ({ transition: 'all 0.2s ease', }; - // ✅ 修复:确保 markerEnd 是对象后再 spread const markerEndStyle = (() => { if (!markerEnd || typeof markerEnd !== 'object') return markerEnd; const markerObj = markerEnd as Record; @@ -153,36 +171,36 @@ const CustomEdge: React.FC = ({ fill="none" stroke="transparent" strokeWidth={20} - style={{ cursor: isDragging ? 'grabbing' : 'grab' }} + style={{ cursor: isDragging ? 'grabbing' : 'pointer' }} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} /> - {/* 控制点(三个拐点,直接在SVG中渲染,可拖动) */} + {/* 单个可拖动控制点 */} {(selected || isHovered) && ( - {vertices.map((pt, idx) => ( - - handleVertexMouseDown(idx, e)} - /> - handleVertexMouseDown(idx, e)} - /> - - ))} + + )} diff --git a/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx b/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx index 2382fb88..34204890 100644 --- a/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx +++ b/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx @@ -1,5 +1,5 @@ import {ConfigurableNodeDefinition, NodeType, NodeCategory, defineNodeOutputs} from './types'; -import { DataSourceType } from '@/domain/dataSource'; +import {DataSourceType} from '@/domain/dataSource'; /** * Jenkins构建节点定义(纯配置) @@ -69,10 +69,18 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { required: ["serverId", "jobName"] }, outputs: defineNodeOutputs( + { + name: "buildStatus", + title: "构建状态", + type: "string", + enum: ["SUCCESS", "FAILURE", "ABORTED", "NOT_FOUND"], + description: "节点执行的最终状态", + required: true + }, { name: "buildNumber", title: "构建编号", - type: "number", + type: "number", description: "Jenkins构建的唯一编号", example: 123, required: true @@ -80,7 +88,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { { name: "buildUrl", title: "构建URL", - type: "string", + type: "string", description: "Jenkins构建页面的访问地址", example: "http://jenkins.example.com/job/app/123/", required: true @@ -88,7 +96,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { { name: "artifactUrl", title: "构建产物地址", - type: "string", + type: "string", description: "构建生成的jar/war包下载地址", example: "http://jenkins.example.com/job/app/123/artifact/target/app-1.0.0.jar", required: false @@ -96,7 +104,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { { name: "gitCommitId", title: "Git提交ID", - type: "string", + type: "string", description: "本次构建使用的Git提交哈希值", example: "a3f5e8d2c4b1a5e9f2d3e7b8c9d1a2f3e4b5c6d7", required: true @@ -104,10 +112,42 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { { name: "buildDuration", title: "构建时长", - type: "number", + type: "number", description: "构建执行的时长(秒)", example: 120, required: true + }, + { + name: "buildDurationMillis", + title: "构建时长(毫秒)", + type: "number", + description: "构建执行的时长(毫秒)", + example: 158000, + required: false + }, + { + name: "buildDurationFormatted", + title: "构建时长(格式)", + type: "string", + description: "构建执行的时长(格式:HH:mm:ss)", + example: "00:02:39", + required: false + }, + { + name: "buildEndTimeMillis", + title: "部署结束时间(ms)", + type: "number", + description: "部署结束时间(毫秒)", + example: 1700000000000, + required: false + }, + { + name: "buildEndTime", + title: "部署结束时间", + type: "string", + description: "部署结束时间(格式化)", + example: "2025-11-14 15:30:00", + required: false } ) };