# 部署流程图弹窗开发指南

## 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文档。

