# 部署流程图弹窗开发指南 ## 1. 概述 本指南用于指导前端开发部署流程图可视化弹窗,展示部署工作流的执行状态、节点详情和统计信息。 --- ## 2. API 接口 ### 2.1 接口信息 ```typescript GET /api/v1/deploy-records/{deployRecordId}/flow-graph ``` ### 2.2 请求参数 | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | deployRecordId | Long | 是 | 部署记录ID | ### 2.3 响应数据结构 ```typescript interface DeployRecordFlowGraphDTO { // ============ 部署记录基本信息 ============ deployRecordId: number; // 部署记录ID businessKey: string; // 业务标识(UUID) deployStatus: DeployStatus; // 部署状态 deployBy: string; // 部署人 deployRemark: string; // 部署备注 deployStartTime: string; // 部署开始时间(ISO格式) deployEndTime: string; // 部署结束时间(ISO格式) deployDuration: number; // 部署总时长(秒) // ============ 业务上下文信息 ============ applicationName: string; // 应用名称 applicationCode: string; // 应用编码 environmentName: string; // 环境名称 teamName: string; // 团队名称 // ============ 工作流实例信息 ============ workflowInstanceId: number; // 工作流实例ID processInstanceId: string; // 流程实例ID(Flowable) workflowStatus: WorkflowStatus; // 工作流实例状态 workflowStartTime: string; // 工作流开始时间 workflowEndTime: string; // 工作流结束时间 workflowDuration: number; // 工作流总时长(秒) // ============ 执行统计信息 ============ totalNodeCount: number; // 总节点数 executedNodeCount: number; // 已执行节点数 successNodeCount: number; // 成功节点数 failedNodeCount: number; // 失败节点数 runningNodeCount: number; // 运行中节点数 // ============ 流程图数据 ============ graph: WorkflowDefinitionGraph; // 流程图结构 nodeInstances: NodeInstanceDTO[]; // 节点执行状态列表 } // 节点实例DTO interface NodeInstanceDTO { id: number | null; // 节点实例ID(未执行节点为null) nodeId: string; // 节点ID nodeName: string; // 节点名称 nodeType: string; // 节点类型 status: NodeStatus; // 节点状态 startTime: string | null; // 开始时间 endTime: string | null; // 结束时间 duration: number | null; // 执行时长(秒) errorMessage: string | null; // 错误信息(失败时) processInstanceId: string; // 流程实例ID createTime: string | null; // 创建时间 updateTime: string | null; // 更新时间 } // 流程图结构 interface WorkflowDefinitionGraph { nodes: GraphNode[]; // 节点列表 edges: GraphEdge[]; // 边列表 } interface GraphNode { id: string; // 节点ID nodeCode: string; // 节点编码 nodeType: string; // 节点类型 nodeName: string; // 节点名称 position: { x: number; y: number }; // 节点位置 configs: Record; // 节点配置 inputMapping: Record; // 输入映射 outputs: OutputField[]; // 输出字段定义 } interface GraphEdge { id: string; // 边ID from: string; // 源节点ID to: string; // 目标节点ID name: string; // 边名称 config: { type: string; // 边类型 condition: { type: string; // 条件类型 priority: number; // 优先级 expression?: string; // 条件表达式 }; }; } // 枚举类型 enum DeployStatus { CREATED = 'CREATED', // 已创建 PENDING_APPROVAL = 'PENDING_APPROVAL', // 待审批 RUNNING = 'RUNNING', // 运行中 SUCCESS = 'SUCCESS', // 成功 FAILED = 'FAILED', // 失败 REJECTED = 'REJECTED', // 审批拒绝 CANCELLED = 'CANCELLED', // 已取消 TERMINATED = 'TERMINATED', // 已终止 PARTIAL_SUCCESS = 'PARTIAL_SUCCESS' // 部分成功 } enum NodeStatus { NOT_STARTED = 'NOT_STARTED', // 未开始 RUNNING = 'RUNNING', // 运行中 SUSPENDED = 'SUSPENDED', // 已暂停 COMPLETED = 'COMPLETED', // 已完成 TERMINATED = 'TERMINATED', // 已终止 FAILED = 'FAILED' // 执行失败 } ``` --- ## 3. UI 设计建议 ### 3.1 整体布局 建议使用全屏或大尺寸弹窗(1200px+),分为三个主要区域: ``` ┌─────────────────────────────────────────────────────────────┐ │ 📋 头部信息区(部署基本信息 + 统计卡片) │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 🎨 流程图可视化区(Canvas绘制) │ │ - 节点按状态着色 │ │ - 支持缩放、拖拽 │ │ - 点击节点查看详情 │ │ │ ├─────────────────────────────────────────────────────────────┤ │ 📊 底部详情区(选中节点的详细信息) │ └─────────────────────────────────────────────────────────────┘ ``` ### 3.2 头部信息区 显示关键信息和统计数据: ```tsx // 第一行:基本信息 ┌───────────────────────────────────────────────────────────┐ │ 🚀 部署流程详情 [关闭 ✕] │ ├───────────────────────────────────────────────────────────┤ │ 应用:应用名称 (应用编码) | 环境:生产环境 | 团队:运维组 │ │ 部署人:张三 | 开始时间:2025-11-05 14:00:00 │ │ 状态:🟢 运行中 | 总时长:5分30秒 │ └───────────────────────────────────────────────────────────┘ // 第二行:统计卡片 ┌──────────┬──────────┬──────────┬──────────┬──────────┐ │ 总节点 │ 已执行 │ 成功 │ 失败 │ 运行中 │ │ 8 │ 5 │ 4 │ 0 │ 1 │ │ 100% │ 62% │ 50% │ 0% │ 12% │ └──────────┴──────────┴──────────┴──────────┴──────────┘ ``` ### 3.3 流程图可视化区 #### 节点状态着色方案 ```typescript const nodeColorMap = { NOT_STARTED: '#d9d9d9', // 未开始:灰色 RUNNING: '#1890ff', // 运行中:蓝色(闪烁动画) COMPLETED: '#52c41a', // 已完成:绿色 FAILED: '#ff4d4f', // 失败:红色 SUSPENDED: '#faad14', // 暂停:橙色 TERMINATED: '#8c8c8c' // 终止:深灰 }; const nodeIconMap = { START_EVENT: '▶️', END_EVENT: '⏹️', USER_TASK: '👤', SERVICE_TASK: '⚙️', APPROVAL: '✓', SCRIPT_TASK: '📝', JENKINS_BUILD: '🔨', NOTIFICATION: '🔔' }; ``` #### 节点显示内容 ``` ┌─────────────────┐ │ 🔨 Jenkins构建 │ ← 节点图标 + 名称 ├─────────────────┤ │ ✓ 已完成 │ ← 状态(带图标) │ ⏱ 2分30秒 │ ← 执行时长 └─────────────────┘ ``` 失败节点特殊标记: ``` ┌─────────────────┐ │ ⚙️ 部署服务 │ ├─────────────────┤ │ ✗ 执行失败 │ ← 红色状态 │ ⚠️ 点击查看错误 │ ← 错误提示 └─────────────────┘ ``` ### 3.4 底部详情区 选中节点后显示完整信息: ``` ┌─────────────────────────────────────────────────────────┐ │ 📌 节点详情 │ ├─────────────────────────────────────────────────────────┤ │ 节点名称:Jenkins构建 │ │ 节点类型:SERVICE_TASK (服务任务) │ │ 执行状态:✓ 已完成 │ │ │ │ ⏱️ 时间信息 │ │ 开始时间:2025-11-05 14:02:30 │ │ 结束时间:2025-11-05 14:05:00 │ │ 执行时长:2分30秒 │ │ │ │ 📊 执行结果(如果有) │ │ 构建编号:#123 │ │ 构建状态:SUCCESS │ │ 构建产物:app-1.0.0.jar │ │ │ │ ⚠️ 错误信息(失败时显示) │ │ 连接超时:无法连接到Jenkins服务器 (timeout: 30s) │ └─────────────────────────────────────────────────────────┘ ``` --- ## 4. 组件划分建议 ### 4.1 组件层次结构 ``` DeployFlowGraphModal (弹窗容器) ├── FlowGraphHeader (头部信息) │ ├── DeployBasicInfo (基本信息) │ └── ExecutionStatistics (统计卡片) ├── FlowGraphCanvas (流程图画布) │ ├── GraphNode (节点组件) │ └── GraphEdge (边组件) └── NodeDetailPanel (节点详情面板) ``` ### 4.2 核心组件代码示例 #### 4.2.1 弹窗容器组件 ```tsx // DeployFlowGraphModal.tsx import React, { useState, useEffect } from 'react'; import { Modal, Spin, message } from 'antd'; import { getDeployFlowGraph } from '@/services/deploy'; import type { DeployRecordFlowGraphDTO, NodeInstanceDTO } from '@/types/deploy'; interface Props { visible: boolean; deployRecordId: number; onClose: () => void; } export const DeployFlowGraphModal: React.FC = ({ visible, deployRecordId, onClose }) => { const [loading, setLoading] = useState(false); const [data, setData] = useState(null); const [selectedNode, setSelectedNode] = useState(null); useEffect(() => { if (visible && deployRecordId) { loadFlowGraph(); } }, [visible, deployRecordId]); const loadFlowGraph = async () => { setLoading(true); try { const response = await getDeployFlowGraph(deployRecordId); setData(response.data); } catch (error) { message.error('加载流程图失败'); } finally { setLoading(false); } }; return ( {data && (
{/* 头部信息 */} {/* 流程图画布 */} {/* 节点详情面板 */} {selectedNode && ( setSelectedNode(null)} /> )}
)}
); }; ``` #### 4.2.2 头部信息组件 ```tsx // FlowGraphHeader.tsx import React from 'react'; import { Tag, Statistic, Card, Row, Col, Descriptions } from 'antd'; import type { DeployRecordFlowGraphDTO } from '@/types/deploy'; import { formatDuration, getStatusTag } from '@/utils/deploy'; interface Props { data: DeployRecordFlowGraphDTO; } export const FlowGraphHeader: React.FC = ({ data }) => { return (
{/* 基本信息 */} {data.applicationName} ({data.applicationCode}) {data.environmentName} {data.teamName} {data.deployBy} {data.deployStartTime} {getStatusTag(data.deployStatus)} {formatDuration(data.deployDuration)} {data.deployRemark && ( {data.deployRemark} )} {/* 统计卡片 */}
{((data.executedNodeCount / data.totalNodeCount) * 100).toFixed(0)}%
0 ? '#ff4d4f' : undefined }} /> 0 ? '#1890ff' : undefined }} />
); }; ``` #### 4.2.3 流程图画布组件(使用 AntV X6) ```tsx // FlowGraphCanvas.tsx import React, { useEffect, useRef } from 'react'; import { Graph } from '@antv/x6'; import type { WorkflowDefinitionGraph, NodeInstanceDTO } from '@/types/deploy'; import { getNodeColor, getNodeIcon } from '@/utils/workflow'; interface Props { graph: WorkflowDefinitionGraph; nodeInstances: NodeInstanceDTO[]; selectedNode: NodeInstanceDTO | null; onNodeClick: (node: NodeInstanceDTO) => void; } export const FlowGraphCanvas: React.FC = ({ graph, nodeInstances, selectedNode, onNodeClick }) => { const containerRef = useRef(null); const graphRef = useRef(null); useEffect(() => { if (!containerRef.current) return; // 创建画布 const graphInstance = new Graph({ container: containerRef.current, width: 1100, height: 500, grid: true, panning: true, mousewheel: { enabled: true, modifiers: ['ctrl', 'meta'], }, }); graphRef.current = graphInstance; // 渲染节点 renderNodes(graphInstance); // 渲染边 renderEdges(graphInstance); // 监听节点点击 graphInstance.on('node:click', ({ node }) => { const nodeId = node.id; const nodeInstance = nodeInstances.find(n => n.nodeId === nodeId); if (nodeInstance) { onNodeClick(nodeInstance); } }); return () => { graphInstance.dispose(); }; }, [graph, nodeInstances]); const renderNodes = (graphInstance: Graph) => { graph.nodes.forEach(node => { const nodeInstance = nodeInstances.find(n => n.nodeId === node.id); const status = nodeInstance?.status || 'NOT_STARTED'; const color = getNodeColor(status); const icon = getNodeIcon(node.nodeType); graphInstance.addNode({ id: node.id, x: node.position.x, y: node.position.y, width: 120, height: 80, label: `${icon} ${node.nodeName}`, attrs: { body: { fill: color, stroke: status === selectedNode?.nodeId ? '#1890ff' : '#d9d9d9', strokeWidth: status === selectedNode?.nodeId ? 3 : 1, rx: 6, ry: 6, }, label: { fill: '#ffffff', fontSize: 12, }, }, data: { status, duration: nodeInstance?.duration, hasError: !!nodeInstance?.errorMessage, }, }); // 添加状态标签 if (nodeInstance) { graphInstance.addNode({ id: `${node.id}-status`, x: node.position.x + 5, y: node.position.y + 60, width: 110, height: 18, label: getStatusLabel(status, nodeInstance.duration), attrs: { body: { fill: 'rgba(255, 255, 255, 0.9)', stroke: 'none', rx: 3, ry: 3, }, label: { fill: '#000000', fontSize: 10, }, }, }); } }); }; const renderEdges = (graphInstance: Graph) => { graph.edges.forEach(edge => { graphInstance.addEdge({ id: edge.id, source: edge.from, target: edge.to, label: edge.name, attrs: { line: { stroke: '#8c8c8c', strokeWidth: 2, targetMarker: { name: 'classic', size: 8, }, }, label: { fill: '#595959', fontSize: 11, }, }, }); }); }; return (
); }; ``` #### 4.2.4 节点详情面板 ```tsx // NodeDetailPanel.tsx import React from 'react'; import { Card, Descriptions, Tag, Alert } from 'antd'; import type { NodeInstanceDTO } from '@/types/deploy'; import { formatDuration, getStatusTag } from '@/utils/deploy'; interface Props { node: NodeInstanceDTO; onClose: () => void; } export const NodeDetailPanel: React.FC = ({ node, onClose }) => { return ( 关闭} style={{ marginTop: 16 }} > {node.nodeName} {node.nodeType} {getStatusTag(node.status)} {node.startTime && ( <> {node.startTime} {node.endTime || '执行中...'} {node.duration ? formatDuration(node.duration) : '计算中...'} )} {node.status === 'NOT_STARTED' && ( 该节点尚未执行 )} {/* 错误信息 */} {node.errorMessage && ( )} {/* 执行结果(outputs)*/} {node.outputs && Object.keys(node.outputs).length > 0 && (

📊 执行结果

{Object.entries(node.outputs).map(([key, value]) => ( {typeof value === 'object' ? JSON.stringify(value) : String(value)} ))}
)}
); }; ``` --- ## 5. 工具函数 ```typescript // utils/deploy.ts /** * 格式化时长 */ export function formatDuration(seconds: number | null): string { if (seconds === null || seconds === undefined) { return '-'; } const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; if (hours > 0) { return `${hours}小时${minutes}分${secs}秒`; } else if (minutes > 0) { return `${minutes}分${secs}秒`; } else { return `${secs}秒`; } } /** * 获取状态标签 */ export function getStatusTag(status: string): React.ReactNode { const statusConfig = { NOT_STARTED: { color: 'default', text: '未开始' }, RUNNING: { color: 'processing', text: '运行中' }, COMPLETED: { color: 'success', text: '已完成' }, FAILED: { color: 'error', text: '执行失败' }, SUSPENDED: { color: 'warning', text: '已暂停' }, TERMINATED: { color: 'default', text: '已终止' }, CREATED: { color: 'default', text: '已创建' }, PENDING_APPROVAL: { color: 'warning', text: '待审批' }, SUCCESS: { color: 'success', text: '成功' }, REJECTED: { color: 'error', text: '审批拒绝' }, CANCELLED: { color: 'default', text: '已取消' }, PARTIAL_SUCCESS: { color: 'warning', text: '部分成功' }, }; const config = statusConfig[status] || { color: 'default', text: status }; return {config.text}; } /** * 获取节点颜色 */ export function getNodeColor(status: string): string { const colorMap = { NOT_STARTED: '#d9d9d9', RUNNING: '#1890ff', COMPLETED: '#52c41a', FAILED: '#ff4d4f', SUSPENDED: '#faad14', TERMINATED: '#8c8c8c', }; return colorMap[status] || '#d9d9d9'; } /** * 获取节点图标 */ export function getNodeIcon(nodeType: string): string { const iconMap = { START_EVENT: '▶️', END_EVENT: '⏹️', USER_TASK: '👤', SERVICE_TASK: '⚙️', APPROVAL: '✓', SCRIPT_TASK: '📝', JENKINS_BUILD: '🔨', NOTIFICATION: '🔔', }; return iconMap[nodeType] || '⚙️'; } /** * 获取状态文本 */ function getStatusLabel(status: string, duration: number | null): string { const statusText = { NOT_STARTED: '未开始', RUNNING: '运行中', COMPLETED: '已完成', FAILED: '执行失败', SUSPENDED: '已暂停', TERMINATED: '已终止', }; const text = statusText[status] || status; const timeText = duration ? ` | ${formatDuration(duration)}` : ''; return `${text}${timeText}`; } ``` --- ## 6. 高级功能建议 ### 6.1 实时刷新 对于运行中的部署,建议实现定时刷新: ```typescript useEffect(() => { if (data?.runningNodeCount > 0) { const timer = setInterval(() => { loadFlowGraph(); // 每5秒刷新一次 }, 5000); return () => clearInterval(timer); } }, [data?.runningNodeCount]); ``` ### 6.2 节点动画 运行中的节点添加闪烁动画: ```css @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } } .node-running { animation: pulse 2s infinite; } ``` ### 6.3 进度条展示 在头部添加整体进度条: ```tsx 0 ? 'exception' : 'active'} format={(percent) => `${data.executedNodeCount} / ${data.totalNodeCount}`} /> ``` ### 6.4 节点搜索/过滤 添加节点名称搜索功能: ```tsx } onChange={(e) => handleSearchNode(e.target.value)} /> ``` ### 6.5 导出功能 支持导出流程图为图片或PDF: ```typescript import html2canvas from 'html2canvas'; const exportFlowGraph = async () => { const canvas = await html2canvas(containerRef.current); const link = document.createElement('a'); link.download = `deploy-flow-${deployRecordId}.png`; link.href = canvas.toDataURL(); link.click(); }; ``` --- ## 7. 注意事项 ### 7.1 性能优化 1. **虚拟滚动**:节点数量超过50个时使用虚拟滚动 2. **懒加载**:详情面板内容按需加载 3. **防抖节流**:缩放、拖拽等操作使用防抖 ### 7.2 异常处理 1. **空数据处理**:显示"暂无流程图数据"提示 2. **加载失败**:提供重试按钮 3. **超时处理**:超过30秒显示超时提示 ### 7.3 用户体验 1. **加载状态**:使用骨架屏或加载动画 2. **操作提示**:提供操作引导(缩放、拖拽说明) 3. **快捷键**:支持ESC关闭弹窗,Ctrl+滚轮缩放 ### 7.4 移动端适配 如需移动端支持,建议: - 使用响应式布局 - 触摸手势支持 - 简化节点显示信息 --- ## 8. 测试用例 ### 8.1 基本功能测试 - [ ] 弹窗正常打开和关闭 - [ ] 数据正确加载和显示 - [ ] 节点正确渲染(位置、颜色、图标) - [ ] 边正确连接 - [ ] 点击节点显示详情 ### 8.2 状态测试 - [ ] 未开始节点显示为灰色 - [ ] 运行中节点显示蓝色并闪烁 - [ ] 成功节点显示绿色 - [ ] 失败节点显示红色并显示错误信息 ### 8.3 交互测试 - [ ] 画布拖拽功能正常 - [ ] 画布缩放功能正常 - [ ] 节点选中高亮正常 - [ ] 统计数据准确 --- ## 9. 参考资源 ### 9.1 推荐库 - **流程图渲染**:[AntV X6](https://x6.antv.vision/) - **UI组件**:[Ant Design](https://ant.design/) - **图表统计**:[AntV G2Plot](https://g2plot.antv.vision/) ### 9.2 设计参考 - Jenkins 构建详情页 - GitLab CI/CD Pipeline 可视化 - GitHub Actions 工作流可视化 --- ## 10. 常见问题 **Q1: 节点太多时如何优化显示?** A: 使用缩略图导航 + 局部放大;超过100个节点时考虑分页或折叠显示。 **Q2: 如何处理长时间运行的部署?** A: 实现自动刷新(5-10秒间隔)+ WebSocket推送更新。 **Q3: 失败节点如何快速定位?** A: 在头部添加"快速定位失败节点"按钮,点击自动滚动并高亮。 **Q4: 如何显示审批节点的审批人信息?** A: 在节点 outputs 中包含 `approver`、`approvalTime` 等信息,详情面板中展示。 --- ## 11. 迭代计划 ### V1.0(基础版) - ✅ 流程图基本渲染 - ✅ 节点状态显示 - ✅ 统计信息展示 - ✅ 节点详情查看 ### V2.0(增强版) - 📋 实时刷新 - 📋 节点搜索/过滤 - 📋 导出功能 - 📋 操作历史记录 ### V3.0(高级版) - 📋 时间轴视图 - 📋 性能分析(节点耗时对比) - 📋 历史部署对比 - 📋 智能建议(性能优化建议) --- ## 联系方式 如有问题,请联系后端开发团队或查阅API文档。