import React, { useState, useEffect, useRef } from "react";
|
import {
|
useNotify,
|
} from 'react-admin';
|
import {
|
Box,
|
Card,
|
CardContent,
|
Typography,
|
TextField,
|
Stack,
|
Chip,
|
Alert,
|
CircularProgress,
|
Divider,
|
FormControl,
|
FormLabel,
|
RadioGroup,
|
FormControlLabel,
|
Radio,
|
Checkbox,
|
Dialog,
|
DialogTitle,
|
DialogContent,
|
DialogActions,
|
Button as MuiButton,
|
Paper,
|
Accordion,
|
AccordionSummary,
|
AccordionDetails,
|
IconButton,
|
Menu,
|
MenuItem,
|
Select,
|
InputLabel,
|
Tabs,
|
Tab,
|
} from '@mui/material';
|
// 使用简单的列表结构代替TreeView,更易实现
|
import {
|
Add as AddIcon,
|
Delete as DeleteIcon,
|
Edit as EditIcon,
|
PlayArrow as PlayArrowIcon,
|
ExpandMore as ExpandMoreIcon,
|
ChevronRight as ChevronRightIcon,
|
MoreVert as MoreVertIcon,
|
ContentCopy as ContentCopyIcon,
|
ArrowUpward as ArrowUpwardIcon,
|
ArrowDownward as ArrowDownwardIcon,
|
Save as SaveIcon,
|
FolderOpen as FolderOpenIcon,
|
FileCopy as FileCopyIcon,
|
Download as DownloadIcon,
|
} from '@mui/icons-material';
|
import request from '@/utils/request';
|
import {
|
COMPONENT_MENU_MAP,
|
MENU_CATEGORIES,
|
getCategoryLabel,
|
getAllowedChildTypes
|
} from './components/ComponentMenuFactory';
|
import { COMPONENT_CONFIGS } from './components/JmeterComponents';
|
import ComponentConfigPanel from './components/ComponentConfigPanel';
|
|
// 测试步骤类型
|
const STEP_TYPES = {
|
HTTP_REQUEST: 'http_request',
|
PALLETIZE_TASK: 'palletize_task', // 组托任务
|
INBOUND_TASK: 'inbound_task',
|
OUTBOUND_TASK: 'outbound_task',
|
VARIABLE_EXTRACT: 'variable_extract',
|
ASSERTION: 'assertion',
|
LOOP: 'loop',
|
CONDITION: 'condition',
|
DELAY: 'delay',
|
};
|
|
// 步骤类型配置
|
const STEP_TYPE_CONFIG = {
|
[STEP_TYPES.HTTP_REQUEST]: {
|
label: 'HTTP请求',
|
icon: '🌐',
|
defaultConfig: {
|
method: 'POST',
|
url: '',
|
headers: {},
|
body: {},
|
},
|
},
|
[STEP_TYPES.PALLETIZE_TASK]: {
|
label: '组托任务',
|
icon: '📦',
|
defaultConfig: {
|
matnrCodes: [],
|
barcode: '',
|
randomMaterialCount: 1,
|
receiptQty: 10,
|
requestInterval: 0, // 请求间隔
|
},
|
},
|
[STEP_TYPES.INBOUND_TASK]: {
|
label: '入库任务',
|
icon: '📥',
|
defaultConfig: {
|
apiType: 'create_in_task',
|
barcode: '',
|
staNo: '',
|
type: 1,
|
inboundLocNos: [],
|
requestInterval: 0, // 请求间隔
|
},
|
},
|
[STEP_TYPES.OUTBOUND_TASK]: {
|
label: '出库任务',
|
icon: '📤',
|
defaultConfig: {
|
staNo: '',
|
checkStock: true,
|
outboundLocNos: [],
|
requestInterval: 0, // 请求间隔
|
},
|
},
|
[STEP_TYPES.VARIABLE_EXTRACT]: {
|
label: '变量提取',
|
icon: '🔍',
|
defaultConfig: {
|
variableName: '',
|
extractType: 'json_path',
|
extractPath: '',
|
},
|
},
|
[STEP_TYPES.ASSERTION]: {
|
label: '断言',
|
icon: '✓',
|
defaultConfig: {
|
assertType: 'equals',
|
expectedValue: '',
|
actualValue: '',
|
},
|
},
|
[STEP_TYPES.LOOP]: {
|
label: '循环控制器',
|
icon: '🔄',
|
defaultConfig: {
|
loopCount: 1,
|
loopType: 'count',
|
},
|
},
|
[STEP_TYPES.CONDITION]: {
|
label: '条件判断',
|
icon: '❓',
|
defaultConfig: {
|
condition: '',
|
operator: 'equals',
|
},
|
},
|
[STEP_TYPES.DELAY]: {
|
label: '延时',
|
icon: '⏱️',
|
defaultConfig: {
|
delayMs: 1000,
|
},
|
},
|
};
|
|
// 自定义TabPanel组件
|
function TabPanel(props) {
|
const { children, value, index, ...other } = props;
|
return (
|
<div
|
role="tabpanel"
|
hidden={value !== index}
|
id={`custom-tabpanel-${index}`}
|
aria-labelledby={`custom-tab-${index}`}
|
{...other}
|
>
|
{value === index && <Box sx={{ p: 2 }}>{children}</Box>}
|
</div>
|
);
|
}
|
|
const RcsTestCustomMode = () => {
|
const notify = useNotify();
|
|
// 测试计划树结构
|
const [testPlan, setTestPlan] = useState({
|
id: 'root',
|
name: '测试计划',
|
type: 'test_plan',
|
children: [],
|
});
|
|
// 当前选中的节点
|
const [selectedNode, setSelectedNode] = useState(null);
|
|
// 右侧配置面板的Tab
|
const [configTab, setConfigTab] = useState(0);
|
|
// 执行结果
|
const [executionResults, setExecutionResults] = useState([]);
|
const [isExecuting, setIsExecuting] = useState(false);
|
|
// 右键菜单
|
const [contextMenu, setContextMenu] = useState(null);
|
const [contextMenuNode, setContextMenuNode] = useState(null);
|
|
// 添加步骤对话框
|
const [addStepDialog, setAddStepDialog] = useState(false);
|
const [selectedStepType, setSelectedStepType] = useState('');
|
|
// 变量管理
|
const [variables, setVariables] = useState({});
|
|
// 展开/折叠状态
|
const [expandedNodes, setExpandedNodes] = useState(new Set(['root']));
|
|
// 测试计划管理
|
const [savedPlans, setSavedPlans] = useState([]);
|
const [savePlanDialog, setSavePlanDialog] = useState(false);
|
const [loadPlanDialog, setLoadPlanDialog] = useState(false);
|
const [planName, setPlanName] = useState('');
|
const [planDescription, setPlanDescription] = useState('');
|
const [currentPlanId, setCurrentPlanId] = useState(null);
|
|
const toggleNodeExpanded = (nodeId) => {
|
const newExpanded = new Set(expandedNodes);
|
if (newExpanded.has(nodeId)) {
|
newExpanded.delete(nodeId);
|
} else {
|
newExpanded.add(nodeId);
|
}
|
setExpandedNodes(newExpanded);
|
};
|
|
// 渲染树节点(使用简单的列表结构)
|
const renderTreeNode = (node, level = 0) => {
|
const config = STEP_TYPE_CONFIG[node.type] || { label: node.name, icon: '📄' };
|
const isSelected = selectedNode?.id === node.id;
|
const hasChildren = node.children && node.children.length > 0;
|
const expanded = expandedNodes.has(node.id);
|
|
return (
|
<Box key={node.id}>
|
<Box
|
sx={{
|
display: 'flex',
|
alignItems: 'center',
|
gap: 1,
|
py: 0.5,
|
px: 1,
|
pl: level * 2 + 1,
|
borderRadius: 1,
|
bgcolor: isSelected ? 'primary.light' : 'transparent',
|
'&:hover': {
|
bgcolor: 'action.hover',
|
},
|
cursor: 'pointer',
|
}}
|
onClick={(e) => {
|
e.stopPropagation();
|
setSelectedNode(node);
|
}}
|
onContextMenu={(e) => {
|
e.preventDefault();
|
e.stopPropagation();
|
setContextMenuNode(node);
|
setContextMenu({
|
mouseX: e.clientX,
|
mouseY: e.clientY,
|
});
|
}}
|
>
|
{hasChildren && (
|
<IconButton
|
size="small"
|
onClick={(e) => {
|
e.stopPropagation();
|
toggleNodeExpanded(node.id);
|
}}
|
>
|
{expanded ? <ExpandMoreIcon /> : <ChevronRightIcon />}
|
</IconButton>
|
)}
|
{!hasChildren && <Box sx={{ width: 32 }} />}
|
<Typography variant="body2">{config.icon}</Typography>
|
<Typography variant="body2" sx={{ flexGrow: 1 }}>
|
{node.name}
|
</Typography>
|
{node.enabled !== false && (
|
<Chip size="small" label="启用" color="success" />
|
)}
|
{node.enabled === false && (
|
<Chip size="small" label="禁用" color="default" />
|
)}
|
</Box>
|
{hasChildren && expanded && (
|
<Box>
|
{node.children.map((child) => renderTreeNode(child, level + 1))}
|
</Box>
|
)}
|
</Box>
|
);
|
};
|
|
// 添加子节点
|
const handleAddChild = (parentId, stepType) => {
|
// 优先使用COMPONENT_CONFIGS,如果没有则使用STEP_TYPE_CONFIG
|
const config = COMPONENT_CONFIGS[stepType] || STEP_TYPE_CONFIG[stepType];
|
if (!config) {
|
notify(`未知的组件类型:${stepType}`, { type: 'error' });
|
return;
|
}
|
|
const newNode = {
|
id: `step_${Date.now()}_${Math.random()}`,
|
name: `${config.label || stepType}_${Date.now()}`,
|
type: stepType,
|
enabled: true,
|
config: { ...(config.defaultConfig || {}) },
|
children: [],
|
};
|
|
const addNode = (node) => {
|
if (node.id === parentId) {
|
return {
|
...node,
|
children: [...(node.children || []), newNode],
|
};
|
}
|
return {
|
...node,
|
children: (node.children || []).map(addNode),
|
};
|
};
|
|
setTestPlan(addNode(testPlan));
|
setSelectedNode(newNode);
|
setAddStepDialog(false);
|
notify(`已添加${config.label}`, { type: 'success' });
|
};
|
|
// 删除节点
|
const handleDeleteNode = (nodeId) => {
|
const deleteNode = (node) => {
|
if (node.id === nodeId) {
|
return null;
|
}
|
return {
|
...node,
|
children: (node.children || [])
|
.map(deleteNode)
|
.filter(Boolean),
|
};
|
};
|
|
const newPlan = deleteNode(testPlan);
|
if (newPlan) {
|
setTestPlan(newPlan);
|
if (selectedNode?.id === nodeId) {
|
setSelectedNode(null);
|
}
|
notify('已删除节点', { type: 'success' });
|
}
|
};
|
|
// 更新节点配置
|
const handleUpdateNodeConfig = (nodeId, config) => {
|
const updateNode = (node) => {
|
if (node.id === nodeId) {
|
return {
|
...node,
|
config: { ...node.config, ...config },
|
};
|
}
|
return {
|
...node,
|
children: (node.children || []).map(updateNode),
|
};
|
};
|
|
const newPlan = updateNode(testPlan);
|
setTestPlan(newPlan);
|
|
// 同步更新 selectedNode
|
if (selectedNode?.id === nodeId) {
|
const findNode = (node) => {
|
if (node.id === nodeId) {
|
return node;
|
}
|
for (const child of node.children || []) {
|
const found = findNode(child);
|
if (found) return found;
|
}
|
return null;
|
};
|
const updatedNode = findNode(newPlan);
|
if (updatedNode) {
|
setSelectedNode(updatedNode);
|
}
|
}
|
};
|
|
// 更新节点名称
|
const handleUpdateNodeName = (nodeId, name) => {
|
const updateNode = (node) => {
|
if (node.id === nodeId) {
|
return { ...node, name };
|
}
|
return {
|
...node,
|
children: (node.children || []).map(updateNode),
|
};
|
};
|
|
const newPlan = updateNode(testPlan);
|
setTestPlan(newPlan);
|
|
// 同步更新 selectedNode
|
if (selectedNode?.id === nodeId) {
|
const findNode = (node) => {
|
if (node.id === nodeId) {
|
return node;
|
}
|
for (const child of node.children || []) {
|
const found = findNode(child);
|
if (found) return found;
|
}
|
return null;
|
};
|
const updatedNode = findNode(newPlan);
|
if (updatedNode) {
|
setSelectedNode(updatedNode);
|
}
|
}
|
};
|
|
// 切换节点启用状态
|
const handleToggleNodeEnabled = (nodeId) => {
|
const toggleNode = (node) => {
|
if (node.id === nodeId) {
|
return {
|
...node,
|
enabled: node.enabled === false ? true : false,
|
};
|
}
|
return {
|
...node,
|
children: (node.children || []).map(toggleNode),
|
};
|
};
|
|
setTestPlan(toggleNode(testPlan));
|
};
|
|
// 执行日志
|
const [executionLogs, setExecutionLogs] = useState([]);
|
|
// 添加日志
|
const addLog = (level, message, data = null) => {
|
const logEntry = {
|
timestamp: new Date().toISOString(),
|
level, // 'info', 'request', 'response', 'error'
|
message,
|
data,
|
};
|
setExecutionLogs((prev) => [...prev, logEntry]);
|
};
|
|
// 下载日志
|
const handleDownloadLogs = () => {
|
const logText = executionLogs.map(log => {
|
const time = new Date(log.timestamp).toLocaleString('zh-CN');
|
let line = `[${time}] [${log.level.toUpperCase()}] ${log.message}`;
|
if (log.data) {
|
line += '\n' + JSON.stringify(log.data, null, 2);
|
}
|
return line;
|
}).join('\n\n');
|
|
const blob = new Blob([logText], { type: 'text/plain;charset=utf-8' });
|
const url = URL.createObjectURL(blob);
|
const link = document.createElement('a');
|
link.href = url;
|
link.download = `rcs_test_logs_${new Date().toISOString().replace(/[:.]/g, '-')}.txt`;
|
document.body.appendChild(link);
|
link.click();
|
document.body.removeChild(link);
|
URL.revokeObjectURL(url);
|
};
|
|
// 执行测试计划
|
const handleExecuteTestPlan = async () => {
|
setIsExecuting(true);
|
setExecutionResults([]);
|
setExecutionLogs([]);
|
|
// 获取线程组配置
|
const threadGroupNode = testPlan.children?.find(child => child.type === 'thread_group');
|
const threadConfig = threadGroupNode?.config || {};
|
const numThreads = threadConfig.numThreads || 1;
|
const rampUp = threadConfig.rampUp || 1;
|
const loops = threadConfig.loops || 1;
|
const requestInterval = threadConfig.requestInterval || 0; // 请求间隔(毫秒)
|
const requestFrequency = threadConfig.requestFrequency || null; // 请求频率(每秒请求数)
|
|
addLog('info', `开始执行测试计划,线程数:${numThreads},循环次数:${loops},Ramp-up:${rampUp}秒`);
|
|
try {
|
// 递归执行节点
|
const executeNode = async (node, parentVariables = {}, stepIndex = 0) => {
|
if (node.enabled === false) {
|
return { success: true, skipped: true, message: '节点已禁用' };
|
}
|
|
const currentVariables = { ...parentVariables, ...variables };
|
let result = {
|
nodeId: node.id,
|
nodeName: node.name,
|
nodeType: node.type,
|
success: false,
|
startTime: new Date().toISOString(),
|
endTime: null,
|
response: null,
|
error: null,
|
extractedVariables: {},
|
};
|
|
try {
|
// 应用步骤级别的请求间隔
|
const stepInterval = node.config?.requestInterval || 0;
|
if (stepInterval > 0 && stepIndex > 0) {
|
addLog('info', `等待 ${stepInterval}ms 后执行下一步`);
|
await new Promise(resolve => setTimeout(resolve, stepInterval));
|
}
|
|
switch (node.type) {
|
case STEP_TYPES.HTTP_REQUEST:
|
// 执行HTTP请求
|
const httpConfig = node.config || {};
|
// 替换变量
|
let url = httpConfig.url || '';
|
let body = httpConfig.body || {};
|
const headers = httpConfig.headers || {};
|
|
// 替换URL中的变量
|
Object.keys(currentVariables).forEach(key => {
|
url = url.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), currentVariables[key]);
|
});
|
|
// 替换请求体中的变量
|
if (typeof body === 'string') {
|
Object.keys(currentVariables).forEach(key => {
|
body = body.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), currentVariables[key]);
|
});
|
} else if (typeof body === 'object') {
|
body = JSON.parse(JSON.stringify(body).replace(/\$\{(\w+)\}/g, (match, key) => {
|
return currentVariables[key] || match;
|
}));
|
}
|
|
// 替换请求头中的变量
|
const processedHeaders = {};
|
Object.keys(headers).forEach(key => {
|
let value = headers[key];
|
if (typeof value === 'string') {
|
Object.keys(currentVariables).forEach(varKey => {
|
value = value.replace(new RegExp(`\\$\\{${varKey}\\}`, 'g'), currentVariables[varKey]);
|
});
|
}
|
processedHeaders[key] = value;
|
});
|
|
const requestData = { url, method: httpConfig.method || 'POST', headers: processedHeaders, body };
|
addLog('request', `执行HTTP请求: ${httpConfig.method || 'POST'} ${url}`, requestData);
|
|
const response = await request({
|
method: httpConfig.method || 'POST',
|
url: url,
|
data: body,
|
headers: processedHeaders,
|
});
|
|
addLog('response', `HTTP响应: ${url}`, response.data);
|
result.request = requestData;
|
result.response = response.data;
|
result.success = response.data?.code === 200;
|
break;
|
|
case STEP_TYPES.PALLETIZE_TASK:
|
// 执行组托任务
|
const palletizeConfig = node.config || {};
|
let palletizeMatnrCodes = palletizeConfig.matnrCodes || [];
|
let palletizeBarcode = palletizeConfig.barcode || '';
|
|
// 替换变量
|
if (palletizeBarcode) {
|
Object.keys(currentVariables).forEach(key => {
|
palletizeBarcode = palletizeBarcode.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), currentVariables[key]);
|
});
|
}
|
|
// 如果没有指定物料,从系统随机选择
|
if (palletizeMatnrCodes.length === 0) {
|
addLog('info', '未指定物料,从系统随机选择');
|
// 从系统获取物料列表
|
const matnrListRes = await request.post('/matnr/page', { current: 1, size: 100 });
|
if (matnrListRes.data?.code === 200 && matnrListRes.data?.data?.records) {
|
const matnrs = matnrListRes.data.data.records;
|
const randomCount = palletizeConfig.randomMaterialCount || 1;
|
const shuffled = [...matnrs].sort(() => Math.random() - 0.5);
|
palletizeMatnrCodes = shuffled.slice(0, Math.min(randomCount, matnrs.length)).map(m => m.code);
|
addLog('info', `随机选择物料: ${palletizeMatnrCodes.join(', ')}`);
|
}
|
}
|
|
const palletizeRequestData = {
|
matnrCodes: palletizeMatnrCodes,
|
barcode: palletizeBarcode,
|
randomMaterialCount: palletizeConfig.randomMaterialCount || 1
|
};
|
addLog('request', '执行组托任务', palletizeRequestData);
|
|
// 调用组托接口(通过RCS测试接口)
|
const palletizeResponse = await request.post('/rcs/test/execute', {
|
matnrCodes: palletizeMatnrCodes,
|
randomMaterialCount: palletizeConfig.randomMaterialCount || 1,
|
autoOutbound: false, // 组托不自动出库
|
});
|
|
addLog('response', '组托任务响应', palletizeResponse.data);
|
result.request = palletizeRequestData;
|
result.response = palletizeResponse.data;
|
result.success = palletizeResponse.data?.code === 200;
|
if (palletizeResponse.data?.data) {
|
result.extractedVariables = {
|
barcode: palletizeResponse.data.data.barcode,
|
waitPakinCode: palletizeResponse.data.data.waitPakinCode,
|
};
|
addLog('info', `提取变量: barcode=${result.extractedVariables.barcode}, waitPakinCode=${result.extractedVariables.waitPakinCode}`);
|
// 更新变量
|
Object.assign(currentVariables, result.extractedVariables);
|
}
|
break;
|
|
case STEP_TYPES.INBOUND_TASK:
|
// 执行入库任务
|
const inboundConfig = node.config || {};
|
let inboundBarcode = inboundConfig.barcode || '';
|
let inboundStation = inboundConfig.staNo || '';
|
let inboundLocNos = inboundConfig.inboundLocNos || [];
|
|
// 替换变量
|
if (inboundBarcode) {
|
Object.keys(currentVariables).forEach(key => {
|
inboundBarcode = inboundBarcode.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), currentVariables[key]);
|
});
|
}
|
|
// 如果没有指定托盘码,尝试从变量中获取
|
if (!inboundBarcode && currentVariables.barcode) {
|
inboundBarcode = currentVariables.barcode;
|
addLog('info', `从变量获取托盘码: ${inboundBarcode}`);
|
}
|
|
const inboundRequestData = {
|
barcode: inboundBarcode,
|
staNo: inboundStation,
|
apiType: inboundConfig.apiType || 'create_in_task',
|
inboundLocNos: Array.isArray(inboundLocNos) ? inboundLocNos : []
|
};
|
addLog('request', '执行入库任务', inboundRequestData);
|
|
const inboundResponse = await request.post('/rcs/test/execute', {
|
matnrCodes: [],
|
inboundStation: inboundStation,
|
inboundLocNos: Array.isArray(inboundLocNos) ? inboundLocNos : [],
|
inboundApiType: inboundConfig.apiType || 'create_in_task',
|
autoOutbound: false,
|
});
|
|
addLog('response', '入库任务响应', inboundResponse.data);
|
result.request = inboundRequestData;
|
result.response = inboundResponse.data;
|
result.success = inboundResponse.data?.code === 200;
|
if (inboundResponse.data?.data) {
|
result.extractedVariables = {
|
barcode: inboundResponse.data.data.barcode,
|
locNo: inboundResponse.data.data.locNo,
|
inboundTaskCode: inboundResponse.data.data.inboundTaskCode,
|
};
|
addLog('info', `提取变量: barcode=${result.extractedVariables.barcode}, locNo=${result.extractedVariables.locNo}`);
|
Object.assign(currentVariables, result.extractedVariables);
|
}
|
break;
|
|
case STEP_TYPES.OUTBOUND_TASK:
|
// 执行出库任务
|
const outboundConfig = node.config || {};
|
let outboundStation = outboundConfig.staNo || '';
|
let outboundLocNos = outboundConfig.outboundLocNos || [];
|
|
const outboundRequestData = {
|
staNo: outboundStation,
|
checkStock: outboundConfig.checkStock !== false,
|
outboundLocNos: Array.isArray(outboundLocNos) ? outboundLocNos : []
|
};
|
addLog('request', '执行出库任务', outboundRequestData);
|
|
const outboundResponse = await request.post('/rcs/test/execute', {
|
matnrCodes: [],
|
outboundStation: outboundStation,
|
outboundLocNos: Array.isArray(outboundLocNos) ? outboundLocNos : [],
|
checkStock: outboundConfig.checkStock !== false,
|
autoOutbound: true,
|
});
|
|
addLog('response', '出库任务响应', outboundResponse.data);
|
result.request = outboundRequestData;
|
result.response = outboundResponse.data;
|
result.success = outboundResponse.data?.code === 200;
|
if (outboundResponse.data?.data) {
|
result.extractedVariables = {
|
outboundTaskCode: outboundResponse.data.data.outboundTaskCode,
|
outboundTaskCodes: outboundResponse.data.data.outboundTaskCodes,
|
};
|
addLog('info', `提取变量: outboundTaskCode=${result.extractedVariables.outboundTaskCode}`);
|
Object.assign(currentVariables, result.extractedVariables);
|
}
|
break;
|
|
case STEP_TYPES.DELAY:
|
case 'constant_timer':
|
// 延时/固定定时器
|
const delayMs = node.config?.delayMs || node.config?.delay || 1000;
|
addLog('info', `等待 ${delayMs}ms`);
|
await new Promise((resolve) =>
|
setTimeout(resolve, delayMs)
|
);
|
result.success = true;
|
break;
|
|
case STEP_TYPES.LOOP:
|
// 循环执行子节点
|
const loopCount = node.config?.loopCount || 1;
|
for (let i = 0; i < loopCount; i++) {
|
for (const child of node.children || []) {
|
await executeNode(child, currentVariables);
|
}
|
}
|
result.success = true;
|
break;
|
|
default:
|
// 执行子节点
|
for (const child of node.children || []) {
|
await executeNode(child, currentVariables);
|
}
|
result.success = true;
|
break;
|
}
|
} catch (error) {
|
result.error = error.message || '执行失败';
|
result.success = false;
|
addLog('error', `执行失败: ${node.name}`, { error: error.message, stack: error.stack });
|
}
|
|
result.endTime = new Date().toISOString();
|
const duration = new Date(result.endTime) - new Date(result.startTime);
|
addLog('info', `步骤完成: ${node.name},耗时: ${duration}ms,结果: ${result.success ? '成功' : '失败'}`);
|
setExecutionResults((prev) => [...prev, result]);
|
|
return result;
|
};
|
|
// 执行根节点的所有子节点(支持多线程模拟)
|
const executeChildren = async (children, vars, threadIndex = 0) => {
|
for (let i = 0; i < children.length; i++) {
|
const child = children[i];
|
await executeNode(child, vars, i);
|
|
// 应用请求间隔
|
if (requestInterval > 0 && i < children.length - 1) {
|
await new Promise(resolve => setTimeout(resolve, requestInterval));
|
}
|
}
|
};
|
|
// 多线程执行(模拟)
|
if (numThreads > 1) {
|
addLog('info', `使用 ${numThreads} 个线程执行`);
|
const threadPromises = [];
|
for (let t = 0; t < numThreads; t++) {
|
const delay = (rampUp * 1000 / numThreads) * t; // Ramp-up延迟
|
threadPromises.push(
|
new Promise(resolve => {
|
setTimeout(async () => {
|
addLog('info', `线程 ${t + 1} 开始执行`);
|
for (let loop = 0; loop < loops; loop++) {
|
if (loop > 0) {
|
addLog('info', `线程 ${t + 1} 第 ${loop + 1} 次循环`);
|
}
|
await executeChildren(testPlan.children || [], variables, t);
|
}
|
addLog('info', `线程 ${t + 1} 执行完成`);
|
resolve();
|
}, delay);
|
})
|
);
|
}
|
await Promise.all(threadPromises);
|
} else {
|
// 单线程执行
|
for (let loop = 0; loop < loops; loop++) {
|
if (loop > 0) {
|
addLog('info', `第 ${loop + 1} 次循环`);
|
}
|
await executeChildren(testPlan.children || [], variables, 0);
|
}
|
}
|
|
notify('测试计划执行完成', { type: 'success' });
|
} catch (error) {
|
notify('测试执行异常:' + error.message, { type: 'error' });
|
} finally {
|
setIsExecuting(false);
|
}
|
};
|
|
// 加载测试计划列表
|
const loadTestPlans = async () => {
|
try {
|
const { data: { code, data } } = await request.post('/rcs/test/plan/list', {});
|
if (code === 200 && data) {
|
setSavedPlans(data);
|
}
|
} catch (error) {
|
console.error('加载测试计划失败:', error);
|
}
|
};
|
|
// 保存测试计划
|
const handleSavePlan = async () => {
|
if (!planName.trim()) {
|
notify('请输入测试计划名称', { type: 'warning' });
|
return;
|
}
|
|
try {
|
const planData = JSON.stringify(testPlan);
|
const plan = {
|
id: currentPlanId,
|
planName: planName.trim(),
|
planDescription: planDescription.trim() || null,
|
planData: planData,
|
version: '1.0',
|
status: 1,
|
};
|
|
const { data: { code, msg, data } } = await request.post('/rcs/test/plan/save', plan);
|
|
if (code === 200) {
|
notify('测试计划保存成功!', { type: 'success' });
|
setSavePlanDialog(false);
|
setPlanName('');
|
setPlanDescription('');
|
setCurrentPlanId(null);
|
loadTestPlans();
|
} else {
|
notify(msg || '保存失败', { type: 'error' });
|
}
|
} catch (error) {
|
notify('保存异常:' + (error.message || '未知错误'), { type: 'error' });
|
}
|
};
|
|
// 加载测试计划
|
const handleLoadPlan = async (planId) => {
|
try {
|
const { data: { code, data, msg } } = await request.get(`/rcs/test/plan/${planId}`);
|
|
if (code === 200 && data) {
|
try {
|
const loadedPlan = JSON.parse(data.planData);
|
setTestPlan(loadedPlan);
|
setCurrentPlanId(data.id);
|
notify('测试计划加载成功!', { type: 'success' });
|
setLoadPlanDialog(false);
|
} catch (e) {
|
notify('解析测试计划数据失败', { type: 'error' });
|
}
|
} else {
|
notify(msg || '加载失败', { type: 'error' });
|
}
|
} catch (error) {
|
notify('加载异常:' + (error.message || '未知错误'), { type: 'error' });
|
}
|
};
|
|
// 删除测试计划
|
const handleDeletePlan = async (planId) => {
|
if (!window.confirm('确定要删除此测试计划吗?')) {
|
return;
|
}
|
|
try {
|
const { data: { code, msg } } = await request.post(`/rcs/test/plan/delete/${planId}`);
|
|
if (code === 200) {
|
notify('删除成功!', { type: 'success' });
|
loadTestPlans();
|
} else {
|
notify(msg || '删除失败', { type: 'error' });
|
}
|
} catch (error) {
|
notify('删除异常:' + (error.message || '未知错误'), { type: 'error' });
|
}
|
};
|
|
// 复制测试计划
|
const handleCopyPlan = async (planId) => {
|
try {
|
const { data: { code, data, msg } } = await request.post(`/rcs/test/plan/copy/${planId}`);
|
|
if (code === 200) {
|
notify('复制成功!', { type: 'success' });
|
loadTestPlans();
|
} else {
|
notify(msg || '复制失败', { type: 'error' });
|
}
|
} catch (error) {
|
notify('复制异常:' + (error.message || '未知错误'), { type: 'error' });
|
}
|
};
|
|
// 打开保存对话框
|
const handleOpenSaveDialog = () => {
|
setPlanName('');
|
setPlanDescription('');
|
setCurrentPlanId(null);
|
setSavePlanDialog(true);
|
};
|
|
// 初始化加载测试计划列表
|
useEffect(() => {
|
loadTestPlans();
|
}, []);
|
|
// 渲染节点配置面板
|
const renderNodeConfig = () => {
|
if (!selectedNode) {
|
return (
|
<Box sx={{ p: 3, textAlign: 'center', color: 'text.secondary' }}>
|
请选择一个测试步骤进行配置
|
</Box>
|
);
|
}
|
|
// 从testPlan中获取最新的节点配置,确保数据同步
|
const findNodeInPlan = (node) => {
|
if (node.id === selectedNode.id) {
|
return node;
|
}
|
for (const child of node.children || []) {
|
const found = findNodeInPlan(child);
|
if (found) return found;
|
}
|
return null;
|
};
|
const latestNode = findNodeInPlan(testPlan) || selectedNode;
|
const config = latestNode.config || {};
|
const stepConfig = STEP_TYPE_CONFIG[latestNode.type];
|
|
return (
|
<Box>
|
<Tabs value={configTab} onChange={(e, v) => setConfigTab(v)}>
|
<Tab label="基本信息" />
|
<Tab label="参数配置" />
|
<Tab label="断言" />
|
</Tabs>
|
|
<TabPanel value={configTab} index={0}>
|
<TextField
|
label="步骤名称"
|
fullWidth
|
value={latestNode.name || ''}
|
onChange={(e) => handleUpdateNodeName(latestNode.id, e.target.value)}
|
sx={{ mb: 2 }}
|
/>
|
<FormControlLabel
|
control={
|
<Checkbox
|
checked={latestNode.enabled !== false}
|
onChange={() => handleToggleNodeEnabled(latestNode.id)}
|
/>
|
}
|
label="启用此步骤"
|
/>
|
</TabPanel>
|
|
<TabPanel value={configTab} index={1}>
|
{latestNode.type === STEP_TYPES.INBOUND_TASK && (
|
<Stack spacing={2}>
|
<FormControl fullWidth>
|
<InputLabel>接口类型</InputLabel>
|
<Select
|
value={config.apiType || 'create_in_task'}
|
onChange={(e) =>
|
handleUpdateNodeConfig(latestNode.id, {
|
apiType: e.target.value,
|
})
|
}
|
>
|
<MenuItem value="create_in_task">创建入库任务</MenuItem>
|
<MenuItem value="location_allocate">申请库位分配</MenuItem>
|
</Select>
|
</FormControl>
|
<TextField
|
label="托盘码"
|
fullWidth
|
value={config.barcode || ''}
|
onChange={(e) =>
|
handleUpdateNodeConfig(latestNode.id, {
|
barcode: e.target.value,
|
})
|
}
|
/>
|
<TextField
|
label="入库站点"
|
fullWidth
|
value={config.staNo || ''}
|
onChange={(e) =>
|
handleUpdateNodeConfig(latestNode.id, {
|
staNo: e.target.value,
|
})
|
}
|
/>
|
<TextField
|
label="入库类型"
|
type="number"
|
fullWidth
|
value={config.type || 1}
|
onChange={(e) =>
|
handleUpdateNodeConfig(latestNode.id, {
|
type: parseInt(e.target.value) || 1,
|
})
|
}
|
/>
|
</Stack>
|
)}
|
|
{latestNode.type === STEP_TYPES.OUTBOUND_TASK && (
|
<Stack spacing={2}>
|
<TextField
|
label="出库站点"
|
fullWidth
|
value={config.staNo || ''}
|
onChange={(e) =>
|
handleUpdateNodeConfig(latestNode.id, {
|
staNo: e.target.value,
|
})
|
}
|
/>
|
<FormControlLabel
|
control={
|
<Checkbox
|
checked={config.checkStock !== false}
|
onChange={(e) =>
|
handleUpdateNodeConfig(latestNode.id, {
|
checkStock: e.target.checked,
|
})
|
}
|
/>
|
}
|
label="检查库存"
|
/>
|
</Stack>
|
)}
|
|
{latestNode.type === STEP_TYPES.HTTP_REQUEST && (
|
<Stack spacing={2}>
|
<FormControl fullWidth>
|
<InputLabel>请求方法</InputLabel>
|
<Select
|
value={config.method || 'POST'}
|
onChange={(e) =>
|
handleUpdateNodeConfig(latestNode.id, {
|
method: e.target.value,
|
})
|
}
|
>
|
<MenuItem value="GET">GET</MenuItem>
|
<MenuItem value="POST">POST</MenuItem>
|
<MenuItem value="PUT">PUT</MenuItem>
|
<MenuItem value="DELETE">DELETE</MenuItem>
|
</Select>
|
</FormControl>
|
<TextField
|
label="请求URL"
|
fullWidth
|
value={config.url || ''}
|
onChange={(e) =>
|
handleUpdateNodeConfig(latestNode.id, {
|
url: e.target.value,
|
})
|
}
|
/>
|
<TextField
|
label="请求体 (JSON)"
|
fullWidth
|
multiline
|
rows={4}
|
value={JSON.stringify(config.body || {}, null, 2)}
|
onChange={(e) => {
|
try {
|
const body = JSON.parse(e.target.value);
|
handleUpdateNodeConfig(latestNode.id, { body });
|
} catch (e) {
|
// 忽略JSON解析错误
|
}
|
}}
|
/>
|
</Stack>
|
)}
|
|
{latestNode.type === STEP_TYPES.DELAY && (
|
<TextField
|
label="延时(毫秒)"
|
type="number"
|
fullWidth
|
value={config.delayMs || 1000}
|
onChange={(e) =>
|
handleUpdateNodeConfig(latestNode.id, {
|
delayMs: parseInt(e.target.value) || 1000,
|
})
|
}
|
/>
|
)}
|
|
{latestNode.type === STEP_TYPES.LOOP && (
|
<TextField
|
label="循环次数"
|
type="number"
|
fullWidth
|
value={config.loopCount || 1}
|
onChange={(e) =>
|
handleUpdateNodeConfig(latestNode.id, {
|
loopCount: parseInt(e.target.value) || 1,
|
})
|
}
|
/>
|
)}
|
</TabPanel>
|
|
<TabPanel value={configTab} index={2}>
|
<Typography variant="body2" color="text.secondary">
|
断言配置功能开发中...
|
</Typography>
|
</TabPanel>
|
</Box>
|
);
|
};
|
|
return (
|
<Box sx={{ display: 'flex', height: 'calc(100vh - 200px)', gap: 2 }}>
|
{/* 左侧:测试计划树 */}
|
<Paper sx={{ width: 350, p: 2, overflow: 'auto' }}>
|
<Stack direction="row" spacing={1} sx={{ mb: 2 }} flexWrap="wrap">
|
<MuiButton
|
size="small"
|
variant="contained"
|
startIcon={<AddIcon />}
|
onClick={() => {
|
setContextMenuNode(testPlan);
|
setAddStepDialog(true);
|
}}
|
>
|
添加步骤
|
</MuiButton>
|
<MuiButton
|
size="small"
|
variant="contained"
|
color="success"
|
startIcon={<PlayArrowIcon />}
|
onClick={handleExecuteTestPlan}
|
disabled={isExecuting}
|
>
|
{isExecuting ? '执行中...' : '执行'}
|
</MuiButton>
|
<MuiButton
|
size="small"
|
variant="outlined"
|
startIcon={<SaveIcon />}
|
onClick={handleOpenSaveDialog}
|
>
|
保存
|
</MuiButton>
|
<MuiButton
|
size="small"
|
variant="outlined"
|
startIcon={<FolderOpenIcon />}
|
onClick={() => setLoadPlanDialog(true)}
|
>
|
加载
|
</MuiButton>
|
</Stack>
|
|
<Box sx={{ maxHeight: 'calc(100vh - 300px)', overflow: 'auto' }}>
|
{renderTreeNode(testPlan)}
|
</Box>
|
</Paper>
|
|
{/* 中间:配置面板 */}
|
<Paper sx={{ flex: 1, overflow: 'auto' }}>
|
<CardContent>
|
<Typography variant="h6" gutterBottom>
|
步骤配置
|
</Typography>
|
<ComponentConfigPanel
|
node={selectedNode}
|
onConfigChange={handleUpdateNodeConfig}
|
onNodeNameChange={handleUpdateNodeName}
|
onNodeEnabledChange={handleToggleNodeEnabled}
|
/>
|
</CardContent>
|
</Paper>
|
|
{/* 右侧:执行结果和日志 */}
|
<Paper sx={{ width: 400, overflow: 'auto' }}>
|
<CardContent>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
<Typography variant="h6">
|
执行结果
|
</Typography>
|
{executionLogs.length > 0 && (
|
<MuiButton
|
size="small"
|
variant="outlined"
|
startIcon={<SaveIcon />}
|
onClick={handleDownloadLogs}
|
>
|
下载日志
|
</MuiButton>
|
)}
|
</Box>
|
|
{/* 日志显示 */}
|
{executionLogs.length > 0 && (
|
<Box sx={{ mb: 3 }}>
|
<Typography variant="subtitle2" gutterBottom>
|
执行日志 ({executionLogs.length} 条)
|
</Typography>
|
<Paper
|
variant="outlined"
|
sx={{
|
p: 2,
|
maxHeight: 300,
|
overflow: 'auto',
|
bgcolor: 'grey.50',
|
fontFamily: 'monospace',
|
fontSize: '0.75rem'
|
}}
|
>
|
{executionLogs.map((log, index) => (
|
<Box
|
key={index}
|
sx={{
|
mb: 1,
|
color: log.level === 'error' ? 'error.main' :
|
log.level === 'request' ? 'info.main' :
|
log.level === 'response' ? 'success.main' : 'text.primary'
|
}}
|
>
|
<Typography variant="caption" component="div">
|
[{new Date(log.timestamp).toLocaleTimeString()}] [{log.level.toUpperCase()}] {log.message}
|
</Typography>
|
{log.data && (
|
<Typography
|
variant="caption"
|
component="pre"
|
sx={{
|
mt: 0.5,
|
ml: 2,
|
whiteSpace: 'pre-wrap',
|
wordBreak: 'break-all'
|
}}
|
>
|
{JSON.stringify(log.data, null, 2)}
|
</Typography>
|
)}
|
</Box>
|
))}
|
</Paper>
|
</Box>
|
)}
|
|
{/* 执行结果 */}
|
{executionResults.length === 0 ? (
|
<Typography variant="body2" color="text.secondary">
|
暂无执行结果
|
</Typography>
|
) : (
|
<Stack spacing={1}>
|
{executionResults.map((result, index) => (
|
<Accordion key={index}>
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
<Stack direction="row" spacing={1} alignItems="center">
|
{result.success ? (
|
<Chip size="small" label="成功" color="success" />
|
) : (
|
<Chip size="small" label="失败" color="error" />
|
)}
|
<Typography variant="body2">{result.nodeName}</Typography>
|
</Stack>
|
</AccordionSummary>
|
<AccordionDetails>
|
<Stack spacing={1}>
|
<Typography variant="caption">
|
类型: {result.nodeType}
|
</Typography>
|
<Typography variant="caption">
|
开始时间: {new Date(result.startTime).toLocaleString('zh-CN')}
|
</Typography>
|
{result.endTime && (
|
<Typography variant="caption">
|
结束时间: {new Date(result.endTime).toLocaleString('zh-CN')}
|
</Typography>
|
)}
|
{result.request && (
|
<Box>
|
<Typography variant="caption" fontWeight="bold">请求:</Typography>
|
<TextField
|
size="small"
|
multiline
|
rows={3}
|
fullWidth
|
value={JSON.stringify(result.request, null, 2)}
|
InputProps={{ readOnly: true }}
|
sx={{ mt: 0.5 }}
|
/>
|
</Box>
|
)}
|
{result.error && (
|
<Alert severity="error">{result.error}</Alert>
|
)}
|
{result.response && (
|
<Box>
|
<Typography variant="caption" fontWeight="bold">响应:</Typography>
|
<TextField
|
size="small"
|
multiline
|
rows={4}
|
fullWidth
|
value={JSON.stringify(result.response, null, 2)}
|
InputProps={{ readOnly: true }}
|
sx={{ mt: 0.5 }}
|
/>
|
</Box>
|
)}
|
</Stack>
|
</AccordionDetails>
|
</Accordion>
|
))}
|
</Stack>
|
)}
|
</CardContent>
|
</Paper>
|
|
{/* 添加步骤对话框 - 使用分类菜单 */}
|
<Dialog
|
open={addStepDialog}
|
onClose={() => setAddStepDialog(false)}
|
maxWidth="md"
|
fullWidth
|
>
|
<DialogTitle>添加测试组件</DialogTitle>
|
<DialogContent>
|
<Stack spacing={2} sx={{ mt: 1 }}>
|
{(() => {
|
const nodeType = contextMenuNode?.type || 'test_plan';
|
const allowedCategories = getAllowedChildTypes(nodeType);
|
|
return Object.entries(COMPONENT_MENU_MAP)
|
.filter(([category]) => allowedCategories.includes(category))
|
.map(([category, components]) => (
|
<Box key={category}>
|
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
|
{getCategoryLabel(category)}
|
</Typography>
|
<Stack spacing={1}>
|
{components.map((comp) => {
|
const config = COMPONENT_CONFIGS[comp.type] || STEP_TYPE_CONFIG[comp.type];
|
if (!config) return null;
|
|
return (
|
<MuiButton
|
key={comp.type}
|
variant="outlined"
|
fullWidth
|
onClick={() => {
|
const parentId = contextMenuNode?.id || 'root';
|
handleAddChild(parentId, comp.type);
|
setAddStepDialog(false);
|
}}
|
startIcon={<Typography>{comp.icon || config.icon}</Typography>}
|
sx={{ justifyContent: 'flex-start' }}
|
>
|
{comp.label || config.label}
|
</MuiButton>
|
);
|
})}
|
</Stack>
|
<Divider sx={{ my: 2 }} />
|
</Box>
|
));
|
})()}
|
</Stack>
|
</DialogContent>
|
<DialogActions>
|
<MuiButton onClick={() => setAddStepDialog(false)}>取消</MuiButton>
|
</DialogActions>
|
</Dialog>
|
|
{/* 右键菜单 - 使用分类子菜单 */}
|
<Menu
|
open={contextMenu !== null}
|
onClose={() => setContextMenu(null)}
|
anchorReference="anchorPosition"
|
anchorPosition={
|
contextMenu !== null
|
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
|
: undefined
|
}
|
>
|
<MenuItem
|
onClick={() => {
|
setAddStepDialog(true);
|
setContextMenu(null);
|
}}
|
>
|
<AddIcon sx={{ mr: 1 }} /> 添加
|
</MenuItem>
|
|
{/* 分类子菜单 */}
|
{(() => {
|
const nodeType = contextMenuNode?.type || 'test_plan';
|
const allowedCategories = getAllowedChildTypes(nodeType);
|
|
return Object.entries(COMPONENT_MENU_MAP)
|
.filter(([category]) => allowedCategories.includes(category))
|
.map(([category, components]) => {
|
if (components.length === 0) return null;
|
|
return (
|
<MenuItem
|
key={category}
|
onClick={(e) => {
|
e.stopPropagation();
|
// 打开添加对话框,显示该分类的组件
|
setAddStepDialog(true);
|
setContextMenu(null);
|
}}
|
>
|
{getCategoryLabel(category)}
|
</MenuItem>
|
);
|
});
|
})()}
|
|
{contextMenuNode && contextMenuNode.id !== 'root' && (
|
<>
|
<Divider />
|
<MenuItem
|
onClick={() => {
|
handleDeleteNode(contextMenuNode.id);
|
setContextMenu(null);
|
}}
|
>
|
<DeleteIcon sx={{ mr: 1 }} /> 删除
|
</MenuItem>
|
<MenuItem
|
onClick={() => {
|
handleToggleNodeEnabled(contextMenuNode.id);
|
setContextMenu(null);
|
}}
|
>
|
{contextMenuNode.enabled === false ? (
|
<>
|
<EditIcon sx={{ mr: 1 }} /> 启用
|
</>
|
) : (
|
<>
|
<EditIcon sx={{ mr: 1 }} /> 禁用
|
</>
|
)}
|
</MenuItem>
|
</>
|
)}
|
</Menu>
|
|
{/* 保存测试计划对话框 */}
|
<Dialog
|
open={savePlanDialog}
|
onClose={() => {
|
setSavePlanDialog(false);
|
setPlanName('');
|
setPlanDescription('');
|
setCurrentPlanId(null);
|
}}
|
maxWidth="sm"
|
fullWidth
|
>
|
<DialogTitle>{currentPlanId ? '更新测试计划' : '保存测试计划'}</DialogTitle>
|
<DialogContent>
|
<Stack spacing={2} sx={{ mt: 1 }}>
|
<TextField
|
label="测试计划名称"
|
fullWidth
|
required
|
value={planName}
|
onChange={(e) => setPlanName(e.target.value)}
|
placeholder="请输入测试计划名称"
|
/>
|
<TextField
|
label="测试计划描述"
|
fullWidth
|
multiline
|
rows={3}
|
value={planDescription}
|
onChange={(e) => setPlanDescription(e.target.value)}
|
placeholder="请输入测试计划描述(可选)"
|
/>
|
</Stack>
|
</DialogContent>
|
<DialogActions>
|
<MuiButton
|
onClick={() => {
|
setSavePlanDialog(false);
|
setPlanName('');
|
setPlanDescription('');
|
setCurrentPlanId(null);
|
}}
|
>
|
取消
|
</MuiButton>
|
<MuiButton
|
onClick={handleSavePlan}
|
variant="contained"
|
startIcon={<SaveIcon />}
|
>
|
保存
|
</MuiButton>
|
</DialogActions>
|
</Dialog>
|
|
{/* 加载测试计划对话框 */}
|
<Dialog
|
open={loadPlanDialog}
|
onClose={() => setLoadPlanDialog(false)}
|
maxWidth="md"
|
fullWidth
|
>
|
<DialogTitle>加载测试计划</DialogTitle>
|
<DialogContent>
|
{savedPlans.length === 0 ? (
|
<Box sx={{ p: 3, textAlign: 'center', color: 'text.secondary' }}>
|
暂无保存的测试计划
|
</Box>
|
) : (
|
<Stack spacing={1} sx={{ mt: 1 }}>
|
{savedPlans.map((plan) => (
|
<Card key={plan.id} variant="outlined">
|
<CardContent>
|
<Stack direction="row" spacing={2} alignItems="center">
|
<Box sx={{ flexGrow: 1 }}>
|
<Typography variant="subtitle1">
|
{plan.planName}
|
</Typography>
|
{plan.planDescription && (
|
<Typography variant="body2" color="text.secondary">
|
{plan.planDescription}
|
</Typography>
|
)}
|
<Typography variant="caption" color="text.secondary">
|
创建时间: {new Date(plan.createTime).toLocaleString()}
|
</Typography>
|
</Box>
|
<Stack direction="row" spacing={1}>
|
<MuiButton
|
size="small"
|
variant="contained"
|
onClick={() => handleLoadPlan(plan.id)}
|
>
|
加载
|
</MuiButton>
|
<MuiButton
|
size="small"
|
variant="outlined"
|
startIcon={<FileCopyIcon />}
|
onClick={() => handleCopyPlan(plan.id)}
|
>
|
复制
|
</MuiButton>
|
<MuiButton
|
size="small"
|
variant="outlined"
|
color="error"
|
startIcon={<DeleteIcon />}
|
onClick={() => handleDeletePlan(plan.id)}
|
>
|
删除
|
</MuiButton>
|
</Stack>
|
</Stack>
|
</CardContent>
|
</Card>
|
))}
|
</Stack>
|
)}
|
</DialogContent>
|
<DialogActions>
|
<MuiButton onClick={() => setLoadPlanDialog(false)}>关闭</MuiButton>
|
</DialogActions>
|
</Dialog>
|
</Box>
|
);
|
};
|
|
export default RcsTestCustomMode;
|