990 lines
29 KiB
Markdown
990 lines
29 KiB
Markdown
# 部署流程图弹窗开发指南
|
||
|
||
## 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<string, any>; // 节点配置
|
||
inputMapping: Record<string, any>; // 输入映射
|
||
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<Props> = ({
|
||
visible,
|
||
deployRecordId,
|
||
onClose
|
||
}) => {
|
||
const [loading, setLoading] = useState(false);
|
||
const [data, setData] = useState<DeployRecordFlowGraphDTO | null>(null);
|
||
const [selectedNode, setSelectedNode] = useState<NodeInstanceDTO | null>(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 (
|
||
<Modal
|
||
title="部署流程详情"
|
||
open={visible}
|
||
onCancel={onClose}
|
||
width={1200}
|
||
footer={null}
|
||
destroyOnClose
|
||
>
|
||
<Spin spinning={loading}>
|
||
{data && (
|
||
<div className="deploy-flow-graph">
|
||
{/* 头部信息 */}
|
||
<FlowGraphHeader data={data} />
|
||
|
||
{/* 流程图画布 */}
|
||
<FlowGraphCanvas
|
||
graph={data.graph}
|
||
nodeInstances={data.nodeInstances}
|
||
selectedNode={selectedNode}
|
||
onNodeClick={setSelectedNode}
|
||
/>
|
||
|
||
{/* 节点详情面板 */}
|
||
{selectedNode && (
|
||
<NodeDetailPanel
|
||
node={selectedNode}
|
||
onClose={() => setSelectedNode(null)}
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
</Spin>
|
||
</Modal>
|
||
);
|
||
};
|
||
```
|
||
|
||
#### 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<Props> = ({ data }) => {
|
||
return (
|
||
<div className="flow-graph-header">
|
||
{/* 基本信息 */}
|
||
<Descriptions column={3} size="small" bordered style={{ marginBottom: 16 }}>
|
||
<Descriptions.Item label="应用">
|
||
{data.applicationName} ({data.applicationCode})
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="环境">
|
||
{data.environmentName}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="团队">
|
||
{data.teamName}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="部署人">
|
||
{data.deployBy}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="开始时间">
|
||
{data.deployStartTime}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="状态">
|
||
{getStatusTag(data.deployStatus)}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="总时长" span={3}>
|
||
{formatDuration(data.deployDuration)}
|
||
</Descriptions.Item>
|
||
{data.deployRemark && (
|
||
<Descriptions.Item label="备注" span={3}>
|
||
{data.deployRemark}
|
||
</Descriptions.Item>
|
||
)}
|
||
</Descriptions>
|
||
|
||
{/* 统计卡片 */}
|
||
<Row gutter={16}>
|
||
<Col span={4}>
|
||
<Card>
|
||
<Statistic
|
||
title="总节点"
|
||
value={data.totalNodeCount}
|
||
suffix="个"
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
<Col span={5}>
|
||
<Card>
|
||
<Statistic
|
||
title="已执行"
|
||
value={data.executedNodeCount}
|
||
suffix={`/ ${data.totalNodeCount}`}
|
||
valueStyle={{ color: '#1890ff' }}
|
||
/>
|
||
<div style={{ marginTop: 8, fontSize: 12, color: '#8c8c8c' }}>
|
||
{((data.executedNodeCount / data.totalNodeCount) * 100).toFixed(0)}%
|
||
</div>
|
||
</Card>
|
||
</Col>
|
||
<Col span={5}>
|
||
<Card>
|
||
<Statistic
|
||
title="成功"
|
||
value={data.successNodeCount}
|
||
valueStyle={{ color: '#52c41a' }}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
<Col span={5}>
|
||
<Card>
|
||
<Statistic
|
||
title="失败"
|
||
value={data.failedNodeCount}
|
||
valueStyle={{ color: data.failedNodeCount > 0 ? '#ff4d4f' : undefined }}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
<Col span={5}>
|
||
<Card>
|
||
<Statistic
|
||
title="运行中"
|
||
value={data.runningNodeCount}
|
||
valueStyle={{ color: data.runningNodeCount > 0 ? '#1890ff' : undefined }}
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
#### 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<Props> = ({
|
||
graph,
|
||
nodeInstances,
|
||
selectedNode,
|
||
onNodeClick
|
||
}) => {
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const graphRef = useRef<Graph | null>(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 (
|
||
<div
|
||
ref={containerRef}
|
||
className="flow-graph-canvas"
|
||
style={{ border: '1px solid #d9d9d9', marginTop: 16 }}
|
||
/>
|
||
);
|
||
};
|
||
```
|
||
|
||
#### 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<Props> = ({ node, onClose }) => {
|
||
return (
|
||
<Card
|
||
title="📌 节点详情"
|
||
extra={<a onClick={onClose}>关闭</a>}
|
||
style={{ marginTop: 16 }}
|
||
>
|
||
<Descriptions column={2} bordered size="small">
|
||
<Descriptions.Item label="节点名称" span={2}>
|
||
{node.nodeName}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="节点类型">
|
||
{node.nodeType}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="执行状态">
|
||
{getStatusTag(node.status)}
|
||
</Descriptions.Item>
|
||
|
||
{node.startTime && (
|
||
<>
|
||
<Descriptions.Item label="开始时间">
|
||
{node.startTime}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="结束时间">
|
||
{node.endTime || '执行中...'}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="执行时长" span={2}>
|
||
{node.duration ? formatDuration(node.duration) : '计算中...'}
|
||
</Descriptions.Item>
|
||
</>
|
||
)}
|
||
|
||
{node.status === 'NOT_STARTED' && (
|
||
<Descriptions.Item label="说明" span={2}>
|
||
<Tag color="default">该节点尚未执行</Tag>
|
||
</Descriptions.Item>
|
||
)}
|
||
</Descriptions>
|
||
|
||
{/* 错误信息 */}
|
||
{node.errorMessage && (
|
||
<Alert
|
||
message="执行错误"
|
||
description={node.errorMessage}
|
||
type="error"
|
||
showIcon
|
||
style={{ marginTop: 16 }}
|
||
/>
|
||
)}
|
||
|
||
{/* 执行结果(outputs)*/}
|
||
{node.outputs && Object.keys(node.outputs).length > 0 && (
|
||
<div style={{ marginTop: 16 }}>
|
||
<h4>📊 执行结果</h4>
|
||
<Descriptions column={1} bordered size="small">
|
||
{Object.entries(node.outputs).map(([key, value]) => (
|
||
<Descriptions.Item key={key} label={key}>
|
||
{typeof value === 'object' ? JSON.stringify(value) : String(value)}
|
||
</Descriptions.Item>
|
||
))}
|
||
</Descriptions>
|
||
</div>
|
||
)}
|
||
</Card>
|
||
);
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 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 <Tag color={config.color}>{config.text}</Tag>;
|
||
}
|
||
|
||
/**
|
||
* 获取节点颜色
|
||
*/
|
||
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
|
||
<Progress
|
||
percent={(data.executedNodeCount / data.totalNodeCount) * 100}
|
||
status={data.failedNodeCount > 0 ? 'exception' : 'active'}
|
||
format={(percent) => `${data.executedNodeCount} / ${data.totalNodeCount}`}
|
||
/>
|
||
```
|
||
|
||
### 6.4 节点搜索/过滤
|
||
|
||
添加节点名称搜索功能:
|
||
|
||
```tsx
|
||
<Input
|
||
placeholder="搜索节点"
|
||
prefix={<SearchOutlined />}
|
||
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文档。
|
||
|