deploy-ease-platform/backend/docs/frontend/部署流程图弹窗开发指南.md
2025-11-11 14:06:52 +08:00

990 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 部署流程图弹窗开发指南
## 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; // 流程实例IDFlowable
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
状态:🟢 运行中 | 总时长:530
└───────────────────────────────────────────────────────────┘
// 第二行:统计卡片
┌──────────┬──────────┬──────────┬──────────┬──────────┐
总节点 已执行 成功 失败 运行中
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文档。