29 KiB
29 KiB
部署流程图弹窗开发指南
1. 概述
本指南用于指导前端开发部署流程图可视化弹窗,展示部署工作流的执行状态、节点详情和统计信息。
2. API 接口
2.1 接口信息
GET /api/v1/deploy-records/{deployRecordId}/flow-graph
2.2 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| deployRecordId | Long | 是 | 部署记录ID |
2.3 响应数据结构
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 头部信息区
显示关键信息和统计数据:
// 第一行:基本信息
┌───────────────────────────────────────────────────────────┐
│ 🚀 部署流程详情 [关闭 ✕] │
├───────────────────────────────────────────────────────────┤
│ 应用:应用名称 (应用编码) | 环境:生产环境 | 团队:运维组 │
│ 部署人:张三 | 开始时间:2025-11-05 14:00:00 │
│ 状态:🟢 运行中 | 总时长:5分30秒 │
└───────────────────────────────────────────────────────────┘
// 第二行:统计卡片
┌──────────┬──────────┬──────────┬──────────┬──────────┐
│ 总节点 │ 已执行 │ 成功 │ 失败 │ 运行中 │
│ 8 │ 5 │ 4 │ 0 │ 1 │
│ 100% │ 62% │ 50% │ 0% │ 12% │
└──────────┴──────────┴──────────┴──────────┴──────────┘
3.3 流程图可视化区
节点状态着色方案
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 弹窗容器组件
// 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 头部信息组件
// 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)
// 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 节点详情面板
// 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. 工具函数
// 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 实时刷新
对于运行中的部署,建议实现定时刷新:
useEffect(() => {
if (data?.runningNodeCount > 0) {
const timer = setInterval(() => {
loadFlowGraph(); // 每5秒刷新一次
}, 5000);
return () => clearInterval(timer);
}
}, [data?.runningNodeCount]);
6.2 节点动画
运行中的节点添加闪烁动画:
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.6; }
100% { opacity: 1; }
}
.node-running {
animation: pulse 2s infinite;
}
6.3 进度条展示
在头部添加整体进度条:
<Progress
percent={(data.executedNodeCount / data.totalNodeCount) * 100}
status={data.failedNodeCount > 0 ? 'exception' : 'active'}
format={(percent) => `${data.executedNodeCount} / ${data.totalNodeCount}`}
/>
6.4 节点搜索/过滤
添加节点名称搜索功能:
<Input
placeholder="搜索节点"
prefix={<SearchOutlined />}
onChange={(e) => handleSearchNode(e.target.value)}
/>
6.5 导出功能
支持导出流程图为图片或PDF:
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 性能优化
- 虚拟滚动:节点数量超过50个时使用虚拟滚动
- 懒加载:详情面板内容按需加载
- 防抖节流:缩放、拖拽等操作使用防抖
7.2 异常处理
- 空数据处理:显示"暂无流程图数据"提示
- 加载失败:提供重试按钮
- 超时处理:超过30秒显示超时提示
7.3 用户体验
- 加载状态:使用骨架屏或加载动画
- 操作提示:提供操作引导(缩放、拖拽说明)
- 快捷键:支持ESC关闭弹窗,Ctrl+滚轮缩放
7.4 移动端适配
如需移动端支持,建议:
- 使用响应式布局
- 触摸手势支持
- 简化节点显示信息
8. 测试用例
8.1 基本功能测试
- 弹窗正常打开和关闭
- 数据正确加载和显示
- 节点正确渲染(位置、颜色、图标)
- 边正确连接
- 点击节点显示详情
8.2 状态测试
- 未开始节点显示为灰色
- 运行中节点显示蓝色并闪烁
- 成功节点显示绿色
- 失败节点显示红色并显示错误信息
8.3 交互测试
- 画布拖拽功能正常
- 画布缩放功能正常
- 节点选中高亮正常
- 统计数据准确
9. 参考资源
9.1 推荐库
- 流程图渲染:AntV X6
- UI组件:Ant Design
- 图表统计:AntV G2Plot
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文档。