chen.lin
2026-02-07 9f724c61dfa4dc4c0eea66253ea0780b023622ae
测试部分
20个文件已添加
6个文件已修改
6387 ■■■■■ 已修改文件
rsf-admin/src/i18n/zh.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/ResourceContent.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/rcsTest/RcsTestCustomMode.jsx 1629 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/rcsTest/RcsTestList.jsx 1074 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/rcsTest/components/ApiSelector.jsx 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/rcsTest/components/ComponentConfigPanel.jsx 1114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/rcsTest/components/ComponentMenuFactory.js 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/rcsTest/components/JmeterComponents.js 500 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/rcsTest/components/SelectModal.jsx 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/rcsTest/components/SummaryReport.jsx 213 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/rcsTest/index.jsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/rcsTest/utils/variableExtractor.js 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasContainerController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/RcsTestController.java 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/RcsTestParams.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/RcsTestConfig.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/RcsTestPlan.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/RcsTestConfigMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/RcsTestPlanMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/PakinSchedules.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/RcsTestPlanService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/RcsTestService.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestPlanServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestServiceImpl.java 525 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WaitPakinServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js
@@ -229,6 +229,7 @@
        inStatisticItem: '日入库明细查询',
        outStatisticItem: '日出库明细查询',
        statisticCount: '日出入库汇总统计',
        rcsTest: 'RCS自动测试',
    },
    table: {
        field: {
rsf-admin/src/page/ResourceContent.js
@@ -64,6 +64,7 @@
import outStatisticItem from './statistics/outStockItem';
import inStatisticItem from './statistics/inStockItem';
import statisticCount from './statistics/stockStatisticNum';
import rcsTest from './rcsTest';
const ResourceContent = (node) => {
  switch (node.component) {
@@ -187,6 +188,8 @@
      return inStatisticItem;
    case "statisticCount":
      return statisticCount;
    case "rcsTest":
      return rcsTest;
    default:
      return {
        list: ListGuesser,
rsf-admin/src/page/rcsTest/RcsTestCustomMode.jsx
New file
@@ -0,0 +1,1629 @@
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;
rsf-admin/src/page/rcsTest/RcsTestList.jsx
New file
@@ -0,0 +1,1074 @@
import React, { useState, useEffect } from "react";
import {
    useNotify,
    useRefresh,
} 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,
    Tabs,
    Tab,
    Autocomplete,
    FormHelperText,
} from '@mui/material';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import SaveIcon from '@mui/icons-material/Save';
import AddIcon from '@mui/icons-material/Add';
import request from '@/utils/request';
import RcsTestCustomMode from './RcsTestCustomMode';
import SelectModal from './components/SelectModal';
// TabPanel组件
function TabPanel(props) {
    const { children, value, index, ...other } = props;
    return (
        <div
            role="tabpanel"
            hidden={value !== index}
            id={`rcs-test-tabpanel-${index}`}
            aria-labelledby={`rcs-test-tab-${index}`}
            {...other}
        >
            {value === index && <Box>{children}</Box>}
        </div>
    );
}
// 获取入库接口类型的中文显示名称
const getInboundApiTypeLabel = (apiType) => {
    const typeMap = {
        'create_in_task': '创建入库任务',
        'location_allocate': '申请库位分配',
    };
    return typeMap[apiType] || apiType;
};
const RcsTestList = () => {
    const notify = useNotify();
    const refresh = useRefresh();
    // Tab切换状态
    const [currentTab, setCurrentTab] = useState(0); // 0: 入库模式, 1: 出库模式, 2: 自定义模式
    // 入库模式参数状态
    const [inboundMatnrCodes, setInboundMatnrCodes] = useState([]);
    const [inboundSelectedMatnrs, setInboundSelectedMatnrs] = useState([]);
    const [inboundStation, setInboundStation] = useState('');
    const [inboundLocNos, setInboundLocNos] = useState([]);
    const [inboundApiType, setInboundApiType] = useState('create_in_task');
    const [inboundRandomMaterialCount, setInboundRandomMaterialCount] = useState(1);
    const [inboundConfigId, setInboundConfigId] = useState(null);
    // 出库模式参数状态
    const [outboundStation, setOutboundStation] = useState('');
    const [outboundLocNos, setOutboundLocNos] = useState([]);
    const [checkStock, setCheckStock] = useState(true);
    const [outboundConfigId, setOutboundConfigId] = useState(null);
    const [outboundMatnrCodes, setOutboundMatnrCodes] = useState([]);
    const [outboundSelectedMatnrs, setOutboundSelectedMatnrs] = useState([]);
    const [showOutboundMatnrModal, setShowOutboundMatnrModal] = useState(false);
    // 下拉选项数据
    const [stationList, setStationList] = useState([]);
    const [loadingStations, setLoadingStations] = useState(false);
    // 弹窗状态
    const [showInboundMatnrModal, setShowInboundMatnrModal] = useState(false);
    const [showInboundLocModal, setShowInboundLocModal] = useState(false);
    const [showOutboundLocModal, setShowOutboundLocModal] = useState(false);
    // 测试结果状态
    const [testResult, setTestResult] = useState(null);
    const [isLoading, setIsLoading] = useState(false);
    const [testSteps, setTestSteps] = useState([]);
    // 配置管理状态
    const [configs, setConfigs] = useState([]);
    const [showConfigDialog, setShowConfigDialog] = useState(false);
    const [currentConfig, setCurrentConfig] = useState(null);
    // 加载配置列表和数据选项
    useEffect(() => {
        loadConfigs();
        loadStationList();
    }, []);
    // 加载站点列表
    const loadStationList = async () => {
        setLoadingStations(true);
        try {
            const { data: { code, data } } = await request.post('/basStation/list', {});
            if (code === 200 && data) {
                setStationList(data.map(item => ({
                    id: item.stationName || item.id,
                    label: `${item.stationName || item.id}`,
                    value: item.stationName || String(item.id),
                })));
            }
        } catch (error) {
            console.error('加载站点列表失败:', error);
        } finally {
            setLoadingStations(false);
        }
    };
    const loadConfigs = async () => {
        try {
            const { data: { code, data } } = await request.post('/rcs/test/config/list', {});
            if (code === 200 && data) {
                setConfigs(data);
            }
        } catch (error) {
            console.error('加载配置失败:', error);
        }
    };
    // 判断是否为入库配置
    const isInboundConfig = (config) => {
        // 入库配置:autoOutbound === 0 或者有 inboundStation 且有物料
        if (config.autoOutbound === 0) {
            return true;
        }
        if (config.inboundStation) {
            try {
                const matnrCodes = config.matnrCodes ? JSON.parse(config.matnrCodes) : [];
                if (matnrCodes && matnrCodes.length > 0) {
                    return true;
                }
            } catch (e) {
                // 解析失败,如果有物料字符串也算入库配置
                if (config.matnrCodes && config.matnrCodes.length > 0) {
                    return true;
                }
            }
        }
        return false;
    };
    // 判断是否为出库配置
    const isOutboundConfig = (config) => {
        // 出库配置:autoOutbound === 1 或者有 outboundStation 且没有物料(或物料为空)
        if (config.autoOutbound === 1) {
            return true;
        }
        if (config.outboundStation) {
            try {
                const matnrCodes = config.matnrCodes ? JSON.parse(config.matnrCodes) : [];
                if (!config.matnrCodes || matnrCodes.length === 0) {
                    return true;
                }
            } catch (e) {
                // 解析失败,如果没有物料字符串也算出库配置
                if (!config.matnrCodes || config.matnrCodes.length === 0) {
                    return true;
                }
            }
        }
        return false;
    };
    // 入库物料选择确认
    const handleInboundMatnrConfirm = (selectedItems) => {
        setInboundSelectedMatnrs(selectedItems);
        setInboundMatnrCodes(selectedItems.map(item => item.code || item.id));
    };
    // 入库库位号选择确认
    const handleInboundLocConfirm = (selectedItems) => {
        setInboundLocNos(selectedItems.map(item => item.code || item.id));
    };
    // 出库库位号选择确认
    const handleOutboundLocConfirm = (selectedItems) => {
        setOutboundLocNos(selectedItems.map(item => item.code || item.id));
    };
    // 出库物料选择确认
    const handleOutboundMatnrConfirm = (selectedItems) => {
        setOutboundSelectedMatnrs(selectedItems);
        setOutboundMatnrCodes(selectedItems.map(item => item.code || item.id));
        // 如果选择了物料组,强制启用库存检查
        if (selectedItems.length > 0) {
            setCheckStock(true);
        }
    };
    // 物料数据转换函数
    const transformMatnrData = (item) => ({
        id: item.code,
        code: item.code,
        name: item.name || '',
        label: `${item.code}${item.name ? ' - ' + item.name : ''}`,
    });
    // 库位数据转换函数
    const transformLocData = (item) => ({
        id: item.code,
        code: item.code,
        label: item.code,
        useStatus: item.useStatus || '',
    });
    // 执行入库测试
    const handleExecuteInboundTest = async () => {
        if (inboundMatnrCodes.length === 0 && !inboundConfigId) {
            notify('请至少添加一个物料编号或选择一个配置', { type: 'warning' });
            return;
        }
        setIsLoading(true);
        setTestResult(null);
        setTestSteps([]);
        try {
            const params = {
                matnrCodes: inboundMatnrCodes,
                inboundStation: inboundStation || undefined,
                inboundLocNos: inboundLocNos.length > 0 ? inboundLocNos : undefined,
                inboundApiType: inboundApiType,
                randomMaterialCount: inboundRandomMaterialCount,
                autoOutbound: false, // 入库模式不自动出库
                configId: inboundConfigId || undefined,
            };
            const { data: { code, data, msg } } = await request.post('/rcs/test/execute', params);
            if (code === 200) {
                setTestResult(data);
                if (data.steps) {
                    setTestSteps(data.steps);
                }
                notify('入库测试执行成功!', { type: 'success' });
                refresh();
            } else {
                notify(msg || '测试执行失败', { type: 'error' });
                if (data && data.steps) {
                    setTestSteps(data.steps);
                }
            }
        } catch (error) {
            setTestResult({ success: false, error: error.message || '执行异常' });
            notify('执行异常:' + (error.message || '未知错误'), { type: 'error' });
        } finally {
            setIsLoading(false);
        }
    };
    // 执行出库测试
    const handleExecuteOutboundTest = async () => {
        setIsLoading(true);
        setTestResult(null);
        setTestSteps([]);
        try {
            const params = {
                matnrCodes: outboundMatnrCodes.length > 0 ? outboundMatnrCodes : undefined,
                outboundStation: outboundStation || undefined,
                outboundLocNos: outboundLocNos.length > 0 ? outboundLocNos : undefined,
                checkStock: checkStock,
                autoOutbound: true, // 出库模式自动出库
                configId: outboundConfigId || undefined,
            };
            const { data: { code, data, msg } } = await request.post('/rcs/test/execute', params);
            if (code === 200) {
                setTestResult(data);
                if (data.steps) {
                    setTestSteps(data.steps);
                }
                notify('出库测试执行成功!', { type: 'success' });
                refresh();
            } else {
                notify(msg || '测试执行失败', { type: 'error' });
                if (data && data.steps) {
                    setTestSteps(data.steps);
                }
            }
        } catch (error) {
            notify('测试执行异常:' + (error.message || '未知错误'), { type: 'error' });
        } finally {
            setIsLoading(false);
        }
    };
    // 保存入库配置
    const handleSaveInboundConfig = async () => {
        if (!currentConfig?.configName) {
            notify('请输入配置名称', { type: 'warning' });
            return;
        }
        try {
            const configData = {
                ...currentConfig,
                matnrCodes: JSON.stringify(inboundMatnrCodes),
                inboundStation: inboundStation || null,
                inboundLocNos: inboundLocNos.length > 0 ? JSON.stringify(inboundLocNos) : null,
                inboundApiType: inboundApiType,
                randomMaterialCount: inboundRandomMaterialCount,
                autoOutbound: 0, // 入库配置不自动出库
                status: 1,
            };
            const { data: { code, msg } } = await request.post('/rcs/test/config/save', configData);
            if (code === 200) {
                notify('入库配置保存成功!', { type: 'success' });
                setShowConfigDialog(false);
                setCurrentConfig(null);
                loadConfigs();
            } else {
                notify(msg || '配置保存失败', { type: 'error' });
            }
        } catch (error) {
            notify('配置保存异常:' + (error.message || '未知错误'), { type: 'error' });
        }
    };
    // 保存出库配置
    const handleSaveOutboundConfig = async () => {
        if (!currentConfig?.configName) {
            notify('请输入配置名称', { type: 'warning' });
            return;
        }
        try {
            const configData = {
                ...currentConfig,
                matnrCodes: outboundMatnrCodes.length > 0 ? JSON.stringify(outboundMatnrCodes) : null,
                outboundStation: outboundStation || null,
                outboundLocNos: outboundLocNos.length > 0 ? JSON.stringify(outboundLocNos) : null,
                checkStock: checkStock ? 1 : 0,
                autoOutbound: 1, // 出库配置自动出库
                status: 1,
            };
            const { data: { code, msg } } = await request.post('/rcs/test/config/save', configData);
            if (code === 200) {
                notify('出库配置保存成功!', { type: 'success' });
                setShowConfigDialog(false);
                setCurrentConfig(null);
                loadConfigs();
            } else {
                notify(msg || '配置保存失败', { type: 'error' });
            }
        } catch (error) {
            notify('配置保存异常:' + (error.message || '未知错误'), { type: 'error' });
        }
    };
    // 保存配置(通用,根据testType决定)
    const handleSaveConfig = async () => {
        if (currentConfig?.testType === 'inbound') {
            await handleSaveInboundConfig();
        } else if (currentConfig?.testType === 'outbound') {
            await handleSaveOutboundConfig();
        } else {
            // 兼容旧逻辑
            if (!currentConfig?.configName) {
                notify('请输入配置名称', { type: 'warning' });
                return;
            }
            try {
                const configData = {
                    ...currentConfig,
                    matnrCodes: JSON.stringify(inboundMatnrCodes),
                    inboundStation: inboundStation || null,
                    outboundStation: outboundStation || null,
                    inboundLocNos: inboundLocNos.length > 0 ? JSON.stringify(inboundLocNos) : null,
                    outboundLocNos: outboundLocNos.length > 0 ? JSON.stringify(outboundLocNos) : null,
                    checkStock: checkStock ? 1 : 0,
                    inboundApiType: inboundApiType,
                    randomMaterialCount: inboundRandomMaterialCount,
                    autoOutbound: 0,
                    status: 1,
                };
                const { data: { code, msg } } = await request.post('/rcs/test/config/save', configData);
                if (code === 200) {
                    notify('配置保存成功!', { type: 'success' });
                    setShowConfigDialog(false);
                    setCurrentConfig(null);
                    loadConfigs();
                } else {
                    notify(msg || '配置保存失败', { type: 'error' });
                }
            } catch (error) {
                notify('配置保存异常:' + (error.message || '未知错误'), { type: 'error' });
            }
        }
    };
    // 删除配置
    const handleDeleteConfig = async (id) => {
        if (!window.confirm('确定要删除此配置吗?')) {
            return;
        }
        try {
            const { data: { code, msg } } = await request.post(`/rcs/test/config/delete/${id}`);
            if (code === 200) {
                notify('配置删除成功!', { type: 'success' });
                loadConfigs();
            } else {
                notify(msg || '配置删除失败', { type: 'error' });
            }
        } catch (error) {
            notify('配置删除异常:' + (error.message || '未知错误'), { type: 'error' });
        }
    };
    return (
        <Box sx={{ p: 3 }}>
            <Typography variant="h4" gutterBottom>
                RCS全流程自动测试
            </Typography>
            {/* Tab切换 */}
            <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
                <Tabs
                    value={currentTab}
                    onChange={(e, newValue) => setCurrentTab(newValue)}
                    aria-label="测试模式切换"
                >
                    <Tab label="入库模式" id="rcs-test-tab-0" aria-controls="rcs-test-tabpanel-0" />
                    <Tab label="出库模式" id="rcs-test-tab-1" aria-controls="rcs-test-tabpanel-1" />
                    <Tab label="自定义模式" id="rcs-test-tab-2" aria-controls="rcs-test-tabpanel-2" />
                </Tabs>
            </Box>
            {/* 入库模式 */}
            <TabPanel value={currentTab} index={0}>
                <Card sx={{ mt: 2, mb: 2 }}>
                <CardContent>
                    <Typography variant="h6" gutterBottom>
                        测试参数配置
                    </Typography>
                    {/* 配置选择 - 只显示入库配置 */}
                    {configs.filter(isInboundConfig).length > 0 && (
                        <Box sx={{ mb: 3 }}>
                            <FormLabel>快速选择入库配置</FormLabel>
                            <Stack direction="row" spacing={1} sx={{ mt: 1, flexWrap: 'wrap' }}>
                                {configs.filter(isInboundConfig).map((config) => (
                                    <Chip
                                        key={config.id}
                                        label={config.configName}
                                        onClick={() => handleLoadInboundConfig(config)}
                                        onDelete={() => handleDeleteConfig(config.id)}
                                        color={inboundConfigId === config.id ? 'primary' : 'default'}
                                        sx={{ mb: 1 }}
                                    />
                                ))}
                            </Stack>
                        </Box>
                    )}
                    <Divider sx={{ my: 2 }} />
                    {/* 物料编号组 */}
                    <Box sx={{ mb: 3 }}>
                        <FormLabel required>物料编号组</FormLabel>
                        <Stack direction="row" spacing={1} sx={{ mt: 1 }}>
                            <MuiButton
                                variant="outlined"
                                startIcon={<AddIcon />}
                                onClick={() => setShowInboundMatnrModal(true)}
                            >
                                选择物料
                            </MuiButton>
                            {inboundSelectedMatnrs.length > 0 && (
                                <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, flex: 1 }}>
                                    {inboundSelectedMatnrs.map((item) => (
                                        <Chip
                                            key={item.id || item.code}
                                            label={item.code || item.id}
                                            onDelete={() => {
                                                const newMatnrs = inboundSelectedMatnrs.filter(
                                                    m => (m.id || m.code) !== (item.id || item.code)
                                                );
                                                setInboundSelectedMatnrs(newMatnrs);
                                                setInboundMatnrCodes(newMatnrs.map(m => m.code || m.id));
                                            }}
                                            size="small"
                                        />
                                    ))}
                                </Box>
                            )}
                        </Stack>
                        <FormHelperText>点击按钮从物料表中选择物料编号,支持多选</FormHelperText>
                    </Box>
                    {/* 入库站点 */}
                    <Box sx={{ mb: 3 }}>
                        <Autocomplete
                            options={stationList}
                            getOptionLabel={(option) => option.label || option.value || option.id}
                            value={stationList.find(s => s.value === inboundStation) || null}
                            onChange={(e, newValue) => {
                                setInboundStation(newValue ? newValue.value : '');
                            }}
                            loading={loadingStations}
                            renderInput={(params) => (
                                <TextField
                                    {...params}
                                    label="入库站点"
                                    size="small"
                                    placeholder="选择入库站点"
                                />
                            )}
                        />
                    </Box>
                    {/* 入库库位号配置 */}
                    <Box sx={{ mb: 3 }}>
                        <FormLabel>入库库位号(可选,多选)</FormLabel>
                        <Stack direction="row" spacing={1} sx={{ mt: 1 }}>
                            <MuiButton
                                variant="outlined"
                                startIcon={<AddIcon />}
                                onClick={() => setShowInboundLocModal(true)}
                            >
                                选择入库库位
                            </MuiButton>
                            {inboundLocNos.length > 0 && (
                                <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, flex: 1 }}>
                                    {inboundLocNos.map((locNo) => (
                                        <Chip
                                            key={locNo}
                                            label={locNo}
                                            onDelete={() => {
                                                setInboundLocNos(inboundLocNos.filter(l => l !== locNo));
                                            }}
                                            size="small"
                                            color="primary"
                                        />
                                    ))}
                                </Box>
                            )}
                        </Stack>
                        <FormHelperText>选择多个库位时,将随机选择一个库位入库</FormHelperText>
                    </Box>
                    {/* 入库接口类型 */}
                    <Box sx={{ mb: 3 }}>
                        <FormControl component="fieldset">
                            <FormLabel>入库接口类型</FormLabel>
                            <RadioGroup
                                row
                                value={inboundApiType}
                                onChange={(e) => setInboundApiType(e.target.value)}
                            >
                                <FormControlLabel
                                    value="create_in_task"
                                    control={<Radio />}
                                    label="创建入库任务"
                                />
                                <FormControlLabel
                                    value="location_allocate"
                                    control={<Radio />}
                                    label="申请库位分配"
                                />
                            </RadioGroup>
                        </FormControl>
                    </Box>
                    {/* 随机物料数量 */}
                    <Box sx={{ mb: 3 }}>
                        <TextField
                            label="随机物料数量"
                            type="number"
                            size="small"
                            value={inboundRandomMaterialCount}
                            onChange={(e) => setInboundRandomMaterialCount(parseInt(e.target.value) || 1)}
                            inputProps={{ min: 1 }}
                            sx={{ width: 200 }}
                        />
                        <FormHelperText>从选中的物料中随机选择的数量</FormHelperText>
                    </Box>
                    {/* 操作按钮 */}
                    <Stack direction="row" spacing={2}>
                        <MuiButton
                            onClick={handleExecuteInboundTest}
                            disabled={isLoading}
                            variant="contained"
                            startIcon={isLoading ? <CircularProgress size={20} /> : <PlayArrowIcon />}
                        >
                            执行入库测试
                        </MuiButton>
                        <MuiButton
                            onClick={() => {
                                setCurrentConfig({
                                    configName: '',
                                    testType: 'inbound' // 标记为入库配置
                                });
                                setShowConfigDialog(true);
                            }}
                            variant="outlined"
                            startIcon={<SaveIcon />}
                        >
                            保存入库配置
                        </MuiButton>
                    </Stack>
                </CardContent>
            </Card>
            {/* 入库测试结果 */}
            {currentTab === 0 && testResult && (
                <Card sx={{ mt: 2 }}>
                    <CardContent>
                        <Typography variant="h6" gutterBottom>
                            测试结果
                        </Typography>
                        <Alert severity="success" sx={{ mb: 2 }}>
                            入库测试执行成功!
                        </Alert>
                        {/* 关键信息 */}
                        {testResult.barcode && (
                            <Box sx={{ mb: 2 }}>
                                <Typography variant="subtitle2">关键信息:</Typography>
                                <Stack direction="row" spacing={2} sx={{ mt: 1 }}>
                                    {testResult.barcode && (
                                        <Chip label={`托盘码: ${testResult.barcode}`} />
                                    )}
                                    {testResult.locNo && (
                                        <Chip label={`库位号: ${testResult.locNo}`} />
                                    )}
                                    {testResult.waitPakinCode && (
                                        <Chip label={`组托编码: ${testResult.waitPakinCode}`} />
                                    )}
                                    {testResult.inboundTaskCode && (
                                        <Chip label={`入库任务: ${testResult.inboundTaskCode}`} />
                                    )}
                                </Stack>
                            </Box>
                        )}
                        {/* 测试步骤 */}
                        {testSteps.length > 0 && (
                            <Box>
                                <Typography variant="subtitle2" gutterBottom>
                                    执行步骤:
                                </Typography>
                                <Box
                                    component="ul"
                                    sx={{
                                        pl: 3,
                                        maxHeight: 300,
                                        overflow: 'auto',
                                        bgcolor: 'grey.50',
                                        p: 2,
                                        borderRadius: 1,
                                    }}
                                >
                                    {testSteps.map((step, index) => (
                                        <li key={index} style={{ marginBottom: 8 }}>
                                            <Typography variant="body2">{step}</Typography>
                                        </li>
                                    ))}
                                </Box>
                            </Box>
                        )}
                    </CardContent>
                </Card>
            )}
            </TabPanel>
            {/* 出库模式 */}
            <TabPanel value={currentTab} index={1}>
                <Card sx={{ mt: 2, mb: 2 }}>
                <CardContent>
                    <Typography variant="h6" gutterBottom>
                        出库测试参数配置
                    </Typography>
                    {/* 配置选择 - 只显示出库配置 */}
                    {configs.filter(isOutboundConfig).length > 0 && (
                        <Box sx={{ mb: 3 }}>
                            <FormLabel>快速选择出库配置</FormLabel>
                            <Stack direction="row" spacing={1} sx={{ mt: 1, flexWrap: 'wrap' }}>
                                {configs.filter(isOutboundConfig).map((config) => (
                                    <Chip
                                        key={config.id}
                                        label={config.configName}
                                        onClick={() => handleLoadOutboundConfig(config)}
                                        onDelete={() => handleDeleteConfig(config.id)}
                                        color={outboundConfigId === config.id ? 'primary' : 'default'}
                                        sx={{ mb: 1 }}
                                    />
                                ))}
                            </Stack>
                        </Box>
                    )}
                    <Divider sx={{ my: 2 }} />
                    {/* 出库站点 */}
                    <Box sx={{ mb: 3 }}>
                        <Autocomplete
                            options={stationList}
                            getOptionLabel={(option) => option.label || option.value || option.id}
                            value={stationList.find(s => s.value === outboundStation) || null}
                            onChange={(e, newValue) => {
                                setOutboundStation(newValue ? newValue.value : '');
                            }}
                            loading={loadingStations}
                            renderInput={(params) => (
                                <TextField
                                    {...params}
                                    label="出库站点"
                                    size="small"
                                    placeholder="选择出库站点"
                                />
                            )}
                        />
                    </Box>
                    {/* 物料组选择 */}
                    <Box sx={{ mb: 3 }}>
                        <FormLabel>物料组(可选)</FormLabel>
                        <Stack direction="row" spacing={1} sx={{ mt: 1 }}>
                            <MuiButton
                                variant="outlined"
                                startIcon={<AddIcon />}
                                onClick={() => setShowOutboundMatnrModal(true)}
                            >
                                选择物料组
                            </MuiButton>
                            {outboundMatnrCodes.length > 0 && (
                                <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, flex: 1 }}>
                                    {outboundSelectedMatnrs.map((matnr) => (
                                        <Chip
                                            key={matnr.code || matnr.id}
                                            label={matnr.label || matnr.code || matnr.id}
                                            onDelete={() => {
                                                const newMatnrs = outboundSelectedMatnrs.filter(m => (m.code || m.id) !== (matnr.code || matnr.id));
                                                setOutboundSelectedMatnrs(newMatnrs);
                                                setOutboundMatnrCodes(newMatnrs.map(m => m.code || m.id));
                                                // 如果清空了物料组,可以取消库存检查
                                                if (newMatnrs.length === 0) {
                                                    // 不自动取消,让用户自己决定
                                                }
                                            }}
                                            size="small"
                                            color="primary"
                                        />
                                    ))}
                                </Box>
                            )}
                        </Stack>
                        <FormHelperText>
                            {outboundMatnrCodes.length > 0
                                ? '已选择物料组,将强制检查库存,只从包含这些物料的库位中选择'
                                : '选择物料组后,将强制检查库存。不选择物料组且不检查库存时,将使用固定模拟物料(000267)'}
                        </FormHelperText>
                    </Box>
                    {/* 出库库位号配置 */}
                    <Box sx={{ mb: 3 }}>
                        <FormLabel>出库库位号(可选,多选)</FormLabel>
                        <Stack direction="row" spacing={1} sx={{ mt: 1 }}>
                            <MuiButton
                                variant="outlined"
                                startIcon={<AddIcon />}
                                onClick={() => setShowOutboundLocModal(true)}
                            >
                                选择出库库位
                            </MuiButton>
                            {outboundLocNos.length > 0 && (
                                <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, flex: 1 }}>
                                    {outboundLocNos.map((locNo) => (
                                        <Chip
                                            key={locNo}
                                            label={locNo}
                                            onDelete={() => {
                                                setOutboundLocNos(outboundLocNos.filter(l => l !== locNo));
                                            }}
                                            size="small"
                                            color="secondary"
                                        />
                                    ))}
                                </Box>
                            )}
                        </Stack>
                        <FormHelperText>选择多个库位时,将随机从多个库位出货,支持多库位组合或单库位出库</FormHelperText>
                    </Box>
                    {/* 检查库存 */}
                    <Box sx={{ mb: 3 }}>
                        <FormControlLabel
                            control={
                                <Checkbox
                                    checked={checkStock}
                                    onChange={(e) => {
                                        // 如果选择了物料组,不允许取消库存检查
                                        if (outboundMatnrCodes.length > 0 && !e.target.checked) {
                                            notify('选择了物料组时必须检查库存', { type: 'warning' });
                                            return;
                                        }
                                        setCheckStock(e.target.checked);
                                    }}
                                    disabled={outboundMatnrCodes.length > 0} // 选择了物料组时禁用
                                />
                            }
                            label={
                                outboundMatnrCodes.length > 0
                                    ? "检查库存(已选择物料组,必须检查库存)"
                                    : "检查库存(启用后只从有库存的库位中选择,不启用则使用固定模拟物料000267)"
                            }
                        />
                    </Box>
                    {/* 操作按钮 */}
                    <Stack direction="row" spacing={2}>
                        <MuiButton
                            onClick={handleExecuteOutboundTest}
                            disabled={isLoading}
                            variant="contained"
                            color="secondary"
                            startIcon={isLoading ? <CircularProgress size={20} /> : <PlayArrowIcon />}
                        >
                            执行出库测试
                        </MuiButton>
                        <MuiButton
                            onClick={() => {
                                setCurrentConfig({
                                    configName: '',
                                    testType: 'outbound' // 标记为出库配置
                                });
                                setShowConfigDialog(true);
                            }}
                            variant="outlined"
                            startIcon={<SaveIcon />}
                        >
                            保存出库配置
                        </MuiButton>
                    </Stack>
                </CardContent>
            </Card>
            {/* 出库测试结果 */}
            {currentTab === 1 && testResult && (
                <Card sx={{ mt: 2 }}>
                    <CardContent>
                        <Typography variant="h6" gutterBottom>
                            测试结果
                        </Typography>
                        <Alert severity="success" sx={{ mb: 2 }}>
                            出库测试执行成功!
                        </Alert>
                        {/* 关键信息 */}
                        {testResult.barcode && (
                            <Box sx={{ mb: 2 }}>
                                <Typography variant="subtitle2">关键信息:</Typography>
                                <Stack direction="row" spacing={2} sx={{ mt: 1 }}>
                                    {testResult.barcode && (
                                        <Chip label={`托盘码: ${testResult.barcode}`} />
                                    )}
                                    {testResult.locNo && (
                                        <Chip label={`库位号: ${testResult.locNo}`} />
                                    )}
                                    {testResult.waitPakinCode && (
                                        <Chip label={`组托编码: ${testResult.waitPakinCode}`} />
                                    )}
                                    {testResult.inboundTaskCode && (
                                        <Chip label={`入库任务: ${testResult.inboundTaskCode}`} />
                                    )}
                                    {testResult.outboundTaskCode && (
                                        <Chip label={`出库任务: ${testResult.outboundTaskCode}`} />
                                    )}
                                    {testResult.outboundTaskCodes && (
                                        <Chip label={`出库任务数: ${testResult.outboundTaskCodes.length}`} />
                                    )}
                                </Stack>
                            </Box>
                        )}
                        {/* 测试步骤 */}
                        {testSteps.length > 0 && (
                            <Box>
                                <Typography variant="subtitle2" gutterBottom>
                                    执行步骤:
                                </Typography>
                                <Box
                                    component="ul"
                                    sx={{
                                        pl: 3,
                                        maxHeight: 300,
                                        overflow: 'auto',
                                        bgcolor: 'grey.50',
                                        p: 2,
                                        borderRadius: 1,
                                    }}
                                >
                                    {testSteps.map((step, index) => (
                                        <li key={index} style={{ marginBottom: 8 }}>
                                            <Typography variant="body2">{step}</Typography>
                                        </li>
                                    ))}
                                </Box>
                            </Box>
                        )}
                    </CardContent>
                </Card>
            )}
            </TabPanel>
            {/* 自定义模式 */}
            <TabPanel value={currentTab} index={2}>
                <RcsTestCustomMode />
            </TabPanel>
            {/* 配置保存对话框 */}
            <Dialog
                open={showConfigDialog}
                onClose={() => {
                    setShowConfigDialog(false);
                    setCurrentConfig(null);
                }}
                maxWidth="sm"
                fullWidth
            >
                <DialogTitle>保存测试配置</DialogTitle>
                <DialogContent>
                    <TextField
                        label="配置名称"
                        fullWidth
                        size="small"
                        value={currentConfig?.configName || ''}
                        onChange={(e) =>
                            setCurrentConfig({ ...currentConfig, configName: e.target.value })
                        }
                        sx={{ mt: 2 }}
                        required
                    />
                </DialogContent>
                <DialogActions>
                    <MuiButton
                        onClick={() => {
                            setShowConfigDialog(false);
                            setCurrentConfig(null);
                        }}
                    >
                        取消
                    </MuiButton>
                    <MuiButton
                        onClick={handleSaveConfig}
                        variant="contained"
                        startIcon={<SaveIcon />}
                    >
                        保存
                    </MuiButton>
                </DialogActions>
            </Dialog>
            {/* 入库物料选择弹窗 */}
            <SelectModal
                open={showInboundMatnrModal}
                onClose={() => setShowInboundMatnrModal(false)}
                onConfirm={handleInboundMatnrConfirm}
                title="选择物料编号(入库)"
                apiUrl="/matnr/page"
                apiMethod="post"
                transformData={transformMatnrData}
                columns={[
                    { field: 'code', headerName: '物料编码', width: 200 },
                    { field: 'name', headerName: '物料名称', width: 250, flex: 1 },
                ]}
                searchField="code"
                multiple={true}
                selectedItems={inboundSelectedMatnrs}
                idField="id"
            />
            {/* 入库库位号选择弹窗 */}
            <SelectModal
                open={showInboundLocModal}
                onClose={() => setShowInboundLocModal(false)}
                onConfirm={handleInboundLocConfirm}
                title="选择入库库位号(可多选)"
                apiUrl="/loc/page"
                apiMethod="post"
                transformData={transformLocData}
                columns={[
                    { field: 'code', headerName: '库位号', width: 200 },
                    { field: 'useStatus', headerName: '使用状态', width: 120 },
                ]}
                searchField="code"
                multiple={true}
                selectedItems={inboundLocNos.map(locNo => ({ id: locNo, code: locNo }))}
                idField="id"
            />
            {/* 出库库位号选择弹窗 */}
            <SelectModal
                open={showOutboundLocModal}
                onClose={() => setShowOutboundLocModal(false)}
                onConfirm={handleOutboundLocConfirm}
                title="选择出库库位号(可多选)"
                apiUrl="/loc/page"
                apiMethod="post"
                transformData={transformLocData}
                columns={[
                    { field: 'code', headerName: '库位号', width: 200 },
                    { field: 'useStatus', headerName: '使用状态', width: 120 },
                ]}
                searchField="code"
                multiple={true}
                selectedItems={outboundLocNos.map(locNo => ({ id: locNo, code: locNo }))}
                idField="id"
            />
            {/* 出库物料选择对话框 */}
            <SelectModal
                open={showOutboundMatnrModal}
                onClose={() => setShowOutboundMatnrModal(false)}
                onConfirm={handleOutboundMatnrConfirm}
                title="选择物料组(可多选)"
                apiUrl="/matnr/page"
                apiMethod="post"
                transformData={transformMatnrData}
                columns={[
                    { field: 'code', headerName: '物料编号', width: 200 },
                    { field: 'name', headerName: '物料名称', width: 300 },
                ]}
                searchField="code"
                multiple={true}
                selectedItems={outboundSelectedMatnrs}
                idField="id"
            />
        </Box>
    );
};
export default RcsTestList;
rsf-admin/src/page/rcsTest/components/ApiSelector.jsx
New file
@@ -0,0 +1,186 @@
import React, { useState, useEffect } from 'react';
import {
    Dialog,
    DialogTitle,
    DialogContent,
    DialogActions,
    TextField,
    Button as MuiButton,
    List,
    ListItem,
    ListItemText,
    ListItemButton,
    Chip,
    Box,
    Typography,
    Divider,
    Stack,
} from '@mui/material';
import { Search as SearchIcon } from '@mui/icons-material';
import request from '@/utils/request';
/**
 * 接口选择器组件
 * 用于从系统加载可用接口列表并选择
 */
const ApiSelector = ({ open, onClose, onSelect, currentUrl = '' }) => {
    const [apiList, setApiList] = useState([]);
    const [filteredApiList, setFilteredApiList] = useState([]);
    const [searchText, setSearchText] = useState('');
    const [loading, setLoading] = useState(false);
    // 预定义的常用接口列表
    const predefinedApis = [
        { method: 'POST', path: '/rcs/test/execute', description: 'RCS测试执行', category: 'RCS测试' },
        { method: 'POST', path: '/wcs/create/in/task', description: '创建入库任务', category: 'WCS' },
        { method: 'POST', path: '/rsf-open-api/rcs/api/open/location/allocate', description: '申请库位分配', category: 'RCS开放接口' },
        { method: 'POST', path: '/matnr/page', description: '物料分页查询', category: '基础数据' },
        { method: 'POST', path: '/basStation/list', description: '站点列表', category: '基础数据' },
        { method: 'POST', path: '/loc/page', description: '库位分页查询', category: '基础数据' },
        { method: 'POST', path: '/deviceSite/selectStaList/list', description: '设备站点列表', category: '基础数据' },
        { method: 'POST', path: '/task/list', description: '任务列表', category: '任务管理' },
        { method: 'POST', path: '/waitPakin/page', description: '组托单分页查询', category: '组托管理' },
    ];
    useEffect(() => {
        if (open) {
            loadApiList();
        }
    }, [open]);
    useEffect(() => {
        filterApis();
    }, [searchText, apiList]);
    const loadApiList = async () => {
        setLoading(true);
        try {
            // 这里可以从系统加载接口列表
            // 暂时使用预定义列表
            setApiList(predefinedApis);
        } catch (error) {
            console.error('加载接口列表失败:', error);
            setApiList(predefinedApis);
        } finally {
            setLoading(false);
        }
    };
    const filterApis = () => {
        if (!searchText.trim()) {
            setFilteredApiList(apiList);
            return;
        }
        const filtered = apiList.filter(api => {
            const searchLower = searchText.toLowerCase();
            return (
                api.path.toLowerCase().includes(searchLower) ||
                api.description.toLowerCase().includes(searchLower) ||
                api.category.toLowerCase().includes(searchLower) ||
                api.method.toLowerCase().includes(searchLower)
            );
        });
        setFilteredApiList(filtered);
    };
    const handleSelect = (api) => {
        // 构建完整URL(需要根据实际环境配置)
        const baseUrl = window.location.origin;
        const fullUrl = api.path.startsWith('http') ? api.path : `${baseUrl}${api.path}`;
        onSelect({
            method: api.method,
            url: fullUrl,
            path: api.path,
            description: api.description,
        });
        onClose();
    };
    const handleManualInput = () => {
        onSelect({
            method: 'POST',
            url: currentUrl || '',
            path: '',
            description: '手动输入',
        });
        onClose();
    };
    return (
        <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
            <DialogTitle>选择接口</DialogTitle>
            <DialogContent>
                <Stack spacing={2}>
                    <TextField
                        fullWidth
                        size="small"
                        placeholder="搜索接口路径、描述或分类..."
                        value={searchText}
                        onChange={(e) => setSearchText(e.target.value)}
                        InputProps={{
                            startAdornment: <SearchIcon sx={{ mr: 1, color: 'text.secondary' }} />,
                        }}
                    />
                    <Box>
                        <Typography variant="subtitle2" gutterBottom>
                            常用接口 ({filteredApiList.length})
                        </Typography>
                        <List sx={{ maxHeight: 400, overflow: 'auto' }}>
                            {filteredApiList.map((api, index) => (
                                <ListItem key={index} disablePadding>
                                    <ListItemButton onClick={() => handleSelect(api)}>
                                        <ListItemText
                                            primary={
                                                <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
                                                    <Chip
                                                        label={api.method}
                                                        size="small"
                                                        color={api.method === 'GET' ? 'primary' : 'secondary'}
                                                    />
                                                    <Typography variant="body2" sx={{ fontFamily: 'monospace' }}>
                                                        {api.path}
                                                    </Typography>
                                                </Box>
                                            }
                                            secondary={api.description}
                                        />
                                        <Chip label={api.category} size="small" variant="outlined" />
                                    </ListItemButton>
                                </ListItem>
                            ))}
                        </List>
                    </Box>
                    <Divider />
                    <Box>
                        <Typography variant="subtitle2" gutterBottom>
                            手动输入
                        </Typography>
                        <TextField
                            fullWidth
                            size="small"
                            placeholder="输入完整的接口URL,如:http://example.com/api/test"
                            value={currentUrl}
                            onChange={(e) => {
                                // 这里可以更新currentUrl,但需要父组件支持
                            }}
                            helperText="可以直接输入完整的接口URL"
                        />
                    </Box>
                </Stack>
            </DialogContent>
            <DialogActions>
                <MuiButton onClick={onClose}>取消</MuiButton>
                <MuiButton onClick={handleManualInput} variant="contained">
                    使用手动输入
                </MuiButton>
            </DialogActions>
        </Dialog>
    );
};
export default ApiSelector;
rsf-admin/src/page/rcsTest/components/ComponentConfigPanel.jsx
New file
@@ -0,0 +1,1114 @@
import React, { useState } from 'react';
import {
    Box,
    Tabs,
    Tab,
    TextField,
    Stack,
    FormControl,
    InputLabel,
    Select,
    MenuItem,
    Checkbox,
    FormControlLabel,
    Typography,
    Divider,
    Button as MuiButton,
    Table,
    TableBody,
    TableCell,
    TableContainer,
    TableHead,
    TableRow,
    Paper,
    IconButton,
} from '@mui/material';
import { Add as AddIcon, Delete as DeleteIcon, Search as SearchIcon } from '@mui/icons-material';
import { COMPONENT_CONFIGS } from './JmeterComponents';
import ApiSelector from './ApiSelector';
function TabPanel(props) {
    const { children, value, index, ...other } = props;
    return (
        <div
            role="tabpanel"
            hidden={value !== index}
            id={`config-tabpanel-${index}`}
            aria-labelledby={`config-tab-${index}`}
            {...other}
        >
            {value === index && <Box sx={{ p: 2 }}>{children}</Box>}
        </div>
    );
}
const ComponentConfigPanel = ({ node, onConfigChange, onNodeNameChange, onNodeEnabledChange }) => {
    const [configTab, setConfigTab] = useState(0);
    const [apiSelectorOpen, setApiSelectorOpen] = useState(false);
    // 使用node的最新配置,确保数据同步
    const config = node?.config || {};
    const componentConfig = COMPONENT_CONFIGS[node?.type] || {};
    if (!node) {
        return (
            <Box sx={{ p: 3, textAlign: 'center', color: 'text.secondary' }}>
                请选择一个测试步骤进行配置
            </Box>
        );
    }
    const handleConfigChange = (key, value) => {
        // 使用最新的node.config,确保数据同步
        const currentConfig = node?.config || {};
        const newConfig = { ...currentConfig, [key]: value };
        onConfigChange(node.id, newConfig);
    };
    const handleArrayItemAdd = (key, newItem = {}) => {
        const currentArray = config[key] || [];
        handleConfigChange(key, [...currentArray, newItem]);
    };
    const handleArrayItemChange = (key, index, field, value) => {
        const currentArray = [...(config[key] || [])];
        currentArray[index] = { ...currentArray[index], [field]: value };
        handleConfigChange(key, currentArray);
    };
    const handleArrayItemDelete = (key, index) => {
        const currentArray = [...(config[key] || [])];
        currentArray.splice(index, 1);
        handleConfigChange(key, currentArray);
    };
    // 根据组件类型渲染不同的配置面板
    const renderConfigByType = () => {
        switch (node.type) {
            case 'http_request':
                return renderHttpRequestConfig();
            case 'thread_group':
                return renderThreadGroupConfig();
            case 'response_assertion':
                return renderResponseAssertionConfig();
            case 'json_assertion':
                return renderJsonAssertionConfig();
            case 'regular_expression_extractor':
                return renderRegexExtractorConfig();
            case 'json_extractor':
                return renderJsonExtractorConfig();
            case 'loop_controller':
                return renderLoopControllerConfig();
            case 'if_controller':
                return renderIfControllerConfig();
            case 'constant_timer':
                return renderConstantTimerConfig();
            case 'user_defined_variables':
                return renderUserDefinedVariablesConfig();
            case 'csv_data_set_config':
                return renderCsvDataSetConfig();
            case 'palletize_task':
                return renderPalletizeTaskConfig();
            case 'inbound_task':
                return renderInboundTaskConfig();
            case 'outbound_task':
                return renderOutboundTaskConfig();
            default:
                return (
                    <Typography variant="body2" color="text.secondary">
                        该组件暂无详细配置项
                    </Typography>
                );
        }
    };
    // HTTP请求配置
    const renderHttpRequestConfig = () => (
        <Stack spacing={2}>
            <FormControl fullWidth>
                <InputLabel>请求方法</InputLabel>
                <Select
                    value={config.method || 'GET'}
                    onChange={(e) => handleConfigChange('method', e.target.value)}
                >
                    <MenuItem value="GET">GET</MenuItem>
                    <MenuItem value="POST">POST</MenuItem>
                    <MenuItem value="PUT">PUT</MenuItem>
                    <MenuItem value="DELETE">DELETE</MenuItem>
                    <MenuItem value="PATCH">PATCH</MenuItem>
                    <MenuItem value="HEAD">HEAD</MenuItem>
                    <MenuItem value="OPTIONS">OPTIONS</MenuItem>
                </Select>
            </FormControl>
            <TextField
                label="协议"
                value={config.protocol || 'http'}
                onChange={(e) => handleConfigChange('protocol', e.target.value)}
                fullWidth
            />
            <TextField
                label="服务器名称或IP"
                value={config.serverName || ''}
                onChange={(e) => handleConfigChange('serverName', e.target.value)}
                fullWidth
            />
            <TextField
                label="端口号"
                value={config.portNumber || ''}
                onChange={(e) => handleConfigChange('portNumber', e.target.value)}
                fullWidth
            />
            <TextField
                label="路径"
                value={config.path || ''}
                onChange={(e) => handleConfigChange('path', e.target.value)}
                fullWidth
            />
            <Box>
                <TextField
                    label="完整URL(可选,如果填写则覆盖上面的协议/服务器/端口/路径)"
                    value={config.url || ''}
                    onChange={(e) => handleConfigChange('url', e.target.value)}
                    fullWidth
                    placeholder="http://example.com/api/test"
                    helperText="如果填写了完整URL,将优先使用此URL"
                    InputProps={{
                        endAdornment: (
                            <MuiButton
                                size="small"
                                startIcon={<SearchIcon />}
                                onClick={() => setApiSelectorOpen(true)}
                                sx={{ mr: -1 }}
                            >
                                选择接口
                            </MuiButton>
                        ),
                    }}
                />
            </Box>
            <TextField
                label="内容编码"
                value={config.contentEncoding || 'UTF-8'}
                onChange={(e) => handleConfigChange('contentEncoding', e.target.value)}
                fullWidth
            />
            <TextField
                label="请求间隔(毫秒)"
                type="number"
                value={config.requestInterval || 0}
                onChange={(e) => handleConfigChange('requestInterval', parseInt(e.target.value) || 0)}
                fullWidth
                inputProps={{ min: 0 }}
                helperText="执行此步骤后等待的时间,0表示不等待"
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.followRedirects !== false}
                        onChange={(e) => handleConfigChange('followRedirects', e.target.checked)}
                    />
                }
                label="跟随重定向"
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.useKeepAlive !== false}
                        onChange={(e) => handleConfigChange('useKeepAlive', e.target.checked)}
                    />
                }
                label="使用KeepAlive"
            />
            <Divider />
            <Typography variant="subtitle2">请求控制</Typography>
            <TextField
                label="请求间隔(毫秒)"
                type="number"
                value={config.requestInterval || 0}
                onChange={(e) => handleConfigChange('requestInterval', parseInt(e.target.value) || 0)}
                fullWidth
                inputProps={{ min: 0 }}
                helperText="执行此步骤前等待的时间,0表示无等待"
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.printRequest !== false}
                        onChange={(e) => handleConfigChange('printRequest', e.target.checked)}
                    />
                }
                label="打印请求内容"
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.printResponse !== false}
                        onChange={(e) => handleConfigChange('printResponse', e.target.checked)}
                    />
                }
                label="打印响应内容"
            />
            <Divider />
            <Typography variant="subtitle2">请求头</Typography>
            <Box sx={{ mb: 1 }}>
                <Stack direction="row" spacing={1} sx={{ mb: 1, flexWrap: 'wrap' }}>
                    <MuiButton
                        size="small"
                        variant="outlined"
                        onClick={() => {
                            const commonHeaders = [
                                { name: 'Content-Type', value: 'application/json' },
                                { name: 'Accept', value: 'application/json' },
                            ];
                            const existingHeaders = config.headers || [];
                            const newHeaders = [...existingHeaders];
                            commonHeaders.forEach(header => {
                                if (!newHeaders.find(h => h.name === header.name)) {
                                    newHeaders.push(header);
                                }
                            });
                            handleConfigChange('headers', newHeaders);
                        }}
                    >
                        添加常用请求头
                    </MuiButton>
                    <MuiButton
                        size="small"
                        variant="outlined"
                        onClick={() => {
                            const authHeaders = [
                                { name: 'Authorization', value: 'Bearer ${token}' },
                            ];
                            const existingHeaders = config.headers || [];
                            const newHeaders = [...existingHeaders];
                            authHeaders.forEach(header => {
                                if (!newHeaders.find(h => h.name === header.name)) {
                                    newHeaders.push(header);
                                }
                            });
                            handleConfigChange('headers', newHeaders);
                        }}
                    >
                        添加认证头
                    </MuiButton>
                </Stack>
            </Box>
            <TableContainer component={Paper} variant="outlined">
                <Table size="small">
                    <TableHead>
                        <TableRow>
                            <TableCell width="40%">名称</TableCell>
                            <TableCell width="50%">值</TableCell>
                            <TableCell width="10%">操作</TableCell>
                        </TableRow>
                    </TableHead>
                    <TableBody>
                        {(config.headers || []).length === 0 ? (
                            <TableRow>
                                <TableCell colSpan={3} align="center" sx={{ color: 'text.secondary', py: 3 }}>
                                    暂无请求头,点击"添加请求头"或使用快捷按钮添加
                                </TableCell>
                            </TableRow>
                        ) : (
                            (config.headers || []).map((header, index) => (
                                <TableRow key={index}>
                                    <TableCell>
                                        <TextField
                                            size="small"
                                            fullWidth
                                            placeholder="如: Content-Type"
                                            value={header.name || ''}
                                            onChange={(e) =>
                                                handleArrayItemChange('headers', index, 'name', e.target.value)
                                            }
                                        />
                                    </TableCell>
                                    <TableCell>
                                        <TextField
                                            size="small"
                                            fullWidth
                                            placeholder="如: application/json"
                                            value={header.value || ''}
                                            onChange={(e) =>
                                                handleArrayItemChange('headers', index, 'value', e.target.value)
                                            }
                                        />
                                    </TableCell>
                                    <TableCell>
                                        <IconButton
                                            size="small"
                                            onClick={() => handleArrayItemDelete('headers', index)}
                                        >
                                            <DeleteIcon />
                                        </IconButton>
                                    </TableCell>
                                </TableRow>
                            ))
                        )}
                    </TableBody>
                </Table>
            </TableContainer>
            <MuiButton
                startIcon={<AddIcon />}
                onClick={() => handleArrayItemAdd('headers', { name: '', value: '' })}
                size="small"
                variant="outlined"
                sx={{ mt: 1 }}
            >
                添加请求头
            </MuiButton>
            <Divider sx={{ my: 2 }} />
            <Typography variant="subtitle2">请求参数</Typography>
            <TableContainer component={Paper} variant="outlined">
                <Table size="small">
                    <TableHead>
                        <TableRow>
                            <TableCell>名称</TableCell>
                            <TableCell>值</TableCell>
                            <TableCell>操作</TableCell>
                        </TableRow>
                    </TableHead>
                    <TableBody>
                        {(config.parameters || []).map((param, index) => (
                            <TableRow key={index}>
                                <TableCell>
                                    <TextField
                                        size="small"
                                        value={param.name || ''}
                                        onChange={(e) =>
                                            handleArrayItemChange('parameters', index, 'name', e.target.value)
                                        }
                                    />
                                </TableCell>
                                <TableCell>
                                    <TextField
                                        size="small"
                                        fullWidth
                                        value={param.value || ''}
                                        onChange={(e) =>
                                            handleArrayItemChange('parameters', index, 'value', e.target.value)
                                        }
                                    />
                                </TableCell>
                                <TableCell>
                                    <IconButton
                                        size="small"
                                        onClick={() => handleArrayItemDelete('parameters', index)}
                                    >
                                        <DeleteIcon />
                                    </IconButton>
                                </TableCell>
                            </TableRow>
                        ))}
                    </TableBody>
                </Table>
            </TableContainer>
            <MuiButton
                startIcon={<AddIcon />}
                onClick={() => handleArrayItemAdd('parameters', { name: '', value: '' })}
                size="small"
            >
                添加参数
            </MuiButton>
            <Divider sx={{ my: 2 }} />
            <Typography variant="subtitle2">请求体</Typography>
            <TextField
                label="请求体内容"
                multiline
                rows={6}
                value={config.body || ''}
                onChange={(e) => handleConfigChange('body', e.target.value)}
                fullWidth
                placeholder='{"key": "value"}'
                helperText="POST/PUT/PATCH请求时使用,支持JSON格式"
            />
        </Stack>
    );
    // 线程组配置
    const renderThreadGroupConfig = () => (
        <Stack spacing={2}>
            <TextField
                label="线程数(用户数)"
                type="number"
                value={config.numThreads || 1}
                onChange={(e) => handleConfigChange('numThreads', parseInt(e.target.value) || 1)}
                fullWidth
                inputProps={{ min: 1 }}
                helperText="模拟并发用户数"
            />
            <TextField
                label="Ramp-up时间(秒)"
                type="number"
                value={config.rampUp || 1}
                onChange={(e) => handleConfigChange('rampUp', parseInt(e.target.value) || 1)}
                fullWidth
                inputProps={{ min: 0 }}
                helperText="所有线程启动完成所需的时间"
            />
            <TextField
                label="循环次数"
                type="number"
                value={config.loops || 1}
                onChange={(e) => handleConfigChange('loops', parseInt(e.target.value) || 1)}
                fullWidth
                inputProps={{ min: 1 }}
                helperText="每个线程执行的循环次数"
            />
            <Divider />
            <Typography variant="subtitle2">请求频率控制</Typography>
            <TextField
                label="请求间隔(毫秒)"
                type="number"
                value={config.requestInterval || 0}
                onChange={(e) => handleConfigChange('requestInterval', parseInt(e.target.value) || 0)}
                fullWidth
                inputProps={{ min: 0 }}
                helperText="每个请求之间的间隔时间,0表示无间隔"
            />
            <TextField
                label="请求频率(每秒请求数)"
                type="number"
                value={config.requestFrequency || ''}
                onChange={(e) => handleConfigChange('requestFrequency', e.target.value ? parseFloat(e.target.value) : null)}
                fullWidth
                inputProps={{ min: 0, step: 0.1 }}
                helperText="限制每秒请求数量,留空表示不限制"
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.sameUserOnNextIteration !== false}
                        onChange={(e) =>
                            handleConfigChange('sameUserOnNextIteration', e.target.checked)
                        }
                    />
                }
                label="Same user on next iteration"
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.scheduler || false}
                        onChange={(e) => handleConfigChange('scheduler', e.target.checked)}
                    />
                }
                label="调度器"
            />
            {config.scheduler && (
                <>
                    <TextField
                        label="持续时间(秒)"
                        type="number"
                        value={config.duration || 60}
                        onChange={(e) => handleConfigChange('duration', parseInt(e.target.value) || 60)}
                        fullWidth
                    />
                    <TextField
                        label="启动延迟(秒)"
                        type="number"
                        value={config.delay || 0}
                        onChange={(e) => handleConfigChange('delay', parseInt(e.target.value) || 0)}
                        fullWidth
                    />
                </>
            )}
        </Stack>
    );
    // 响应断言配置
    const renderResponseAssertionConfig = () => (
        <Stack spacing={2}>
            <FormControl fullWidth>
                <InputLabel>测试字段</InputLabel>
                <Select
                    value={config.testField || 'response_code'}
                    onChange={(e) => handleConfigChange('testField', e.target.value)}
                >
                    <MenuItem value="response_code">响应代码</MenuItem>
                    <MenuItem value="response_message">响应消息</MenuItem>
                    <MenuItem value="response_data">响应数据</MenuItem>
                    <MenuItem value="response_headers">响应头</MenuItem>
                    <MenuItem value="request_url">请求URL</MenuItem>
                    <MenuItem value="request_data">请求数据</MenuItem>
                </Select>
            </FormControl>
            <FormControl fullWidth>
                <InputLabel>测试类型</InputLabel>
                <Select
                    value={config.testType || 'equals'}
                    onChange={(e) => handleConfigChange('testType', e.target.value)}
                >
                    <MenuItem value="equals">等于</MenuItem>
                    <MenuItem value="not_equals">不等于</MenuItem>
                    <MenuItem value="contains">包含</MenuItem>
                    <MenuItem value="not_contains">不包含</MenuItem>
                    <MenuItem value="matches">匹配正则表达式</MenuItem>
                    <MenuItem value="not_matches">不匹配正则表达式</MenuItem>
                </Select>
            </FormControl>
            <TextField
                label="测试字符串"
                value={config.testString || ''}
                onChange={(e) => handleConfigChange('testString', e.target.value)}
                fullWidth
                multiline
                rows={3}
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.not || false}
                        onChange={(e) => handleConfigChange('not', e.target.checked)}
                    />
                }
                label="NOT(取反)"
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.or || false}
                        onChange={(e) => handleConfigChange('or', e.target.checked)}
                    />
                }
                label="OR(或)"
            />
        </Stack>
    );
    // JSON断言配置
    const renderJsonAssertionConfig = () => (
        <Stack spacing={2}>
            <TextField
                label="JSON路径表达式"
                value={config.jsonPath || ''}
                onChange={(e) => handleConfigChange('jsonPath', e.target.value)}
                fullWidth
                placeholder="$.data.code"
            />
            <TextField
                label="期望值"
                value={config.expectedValue || ''}
                onChange={(e) => handleConfigChange('expectedValue', e.target.value)}
                fullWidth
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.validateJsonPath !== false}
                        onChange={(e) => handleConfigChange('validateJsonPath', e.target.checked)}
                    />
                }
                label="验证JSON路径"
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.expectNull || false}
                        onChange={(e) => handleConfigChange('expectNull', e.target.checked)}
                    />
                }
                label="期望值为null"
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.invert || false}
                        onChange={(e) => handleConfigChange('invert', e.target.checked)}
                    />
                }
                label="反转断言(取反)"
            />
        </Stack>
    );
    // 正则表达式提取器配置
    const renderRegexExtractorConfig = () => (
        <Stack spacing={2}>
            <TextField
                label="变量名"
                value={config.variableName || ''}
                onChange={(e) => handleConfigChange('variableName', e.target.value)}
                fullWidth
            />
            <TextField
                label="正则表达式"
                value={config.regex || ''}
                onChange={(e) => handleConfigChange('regex', e.target.value)}
                fullWidth
                placeholder='code":(\d+)'
            />
            <TextField
                label="模板"
                value={config.template || '$1$'}
                onChange={(e) => handleConfigChange('template', e.target.value)}
                fullWidth
                helperText="$1$表示第一个匹配组,$2$表示第二个匹配组"
            />
            <TextField
                label="匹配编号"
                type="number"
                value={config.matchNumber || 1}
                onChange={(e) => handleConfigChange('matchNumber', parseInt(e.target.value) || 1)}
                fullWidth
                helperText="0表示随机,负数表示全部"
            />
            <TextField
                label="默认值"
                value={config.defaultValue || ''}
                onChange={(e) => handleConfigChange('defaultValue', e.target.value)}
                fullWidth
            />
            <FormControl fullWidth>
                <InputLabel>使用字段</InputLabel>
                <Select
                    value={config.useField || 'body'}
                    onChange={(e) => handleConfigChange('useField', e.target.value)}
                >
                    <MenuItem value="body">响应体</MenuItem>
                    <MenuItem value="headers">响应头</MenuItem>
                    <MenuItem value="url">URL</MenuItem>
                    <MenuItem value="code">响应代码</MenuItem>
                </Select>
            </FormControl>
        </Stack>
    );
    // JSON提取器配置
    const renderJsonExtractorConfig = () => (
        <Stack spacing={2}>
            <TextField
                label="变量名"
                value={config.variableName || ''}
                onChange={(e) => handleConfigChange('variableName', e.target.value)}
                fullWidth
            />
            <TextField
                label="JSON路径表达式"
                value={config.jsonPath || ''}
                onChange={(e) => handleConfigChange('jsonPath', e.target.value)}
                fullWidth
                placeholder="$.data.locNo"
            />
            <TextField
                label="匹配编号"
                type="number"
                value={config.matchNumber || 1}
                onChange={(e) => handleConfigChange('matchNumber', parseInt(e.target.value) || 1)}
                fullWidth
            />
            <TextField
                label="默认值"
                value={config.defaultValue || ''}
                onChange={(e) => handleConfigChange('defaultValue', e.target.value)}
                fullWidth
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.computeConcatenation || false}
                        onChange={(e) => handleConfigChange('computeConcatenation', e.target.checked)}
                    />
                }
                label="计算连接(多个匹配时)"
            />
        </Stack>
    );
    // 循环控制器配置
    const renderLoopControllerConfig = () => (
        <Stack spacing={2}>
            <TextField
                label="循环次数"
                type="number"
                value={config.loops || 1}
                onChange={(e) => handleConfigChange('loops', parseInt(e.target.value) || 1)}
                fullWidth
                inputProps={{ min: 1 }}
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.continueForever || false}
                        onChange={(e) => handleConfigChange('continueForever', e.target.checked)}
                    />
                }
                label="永远循环"
            />
        </Stack>
    );
    // If控制器配置
    const renderIfControllerConfig = () => (
        <Stack spacing={2}>
            <TextField
                label="条件"
                value={config.condition || ''}
                onChange={(e) => handleConfigChange('condition', e.target.value)}
                fullWidth
                multiline
                rows={3}
                placeholder='${variable} == "value"'
                helperText="支持变量和表达式,如:${code} == 200"
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.evaluateAll || false}
                        onChange={(e) => handleConfigChange('evaluateAll', e.target.checked)}
                    />
                }
                label="对所有子元素求值"
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.useExpression !== false}
                        onChange={(e) => handleConfigChange('useExpression', e.target.checked)}
                    />
                }
                label="使用表达式"
            />
        </Stack>
    );
    // 固定定时器配置
    const renderConstantTimerConfig = () => (
        <Stack spacing={2}>
            <TextField
                label="线程延迟(毫秒)"
                type="number"
                value={config.delay || 1000}
                onChange={(e) => handleConfigChange('delay', parseInt(e.target.value) || 1000)}
                fullWidth
                inputProps={{ min: 0 }}
            />
        </Stack>
    );
    // 用户定义变量配置
    const renderUserDefinedVariablesConfig = () => (
        <Stack spacing={2}>
            <TableContainer component={Paper} variant="outlined">
                <Table size="small">
                    <TableHead>
                        <TableRow>
                            <TableCell>变量名</TableCell>
                            <TableCell>值</TableCell>
                            <TableCell>操作</TableCell>
                        </TableRow>
                    </TableHead>
                    <TableBody>
                        {(config.variables || []).map((variable, index) => (
                            <TableRow key={index}>
                                <TableCell>
                                    <TextField
                                        size="small"
                                        value={variable.name || ''}
                                        onChange={(e) =>
                                            handleArrayItemChange('variables', index, 'name', e.target.value)
                                        }
                                    />
                                </TableCell>
                                <TableCell>
                                    <TextField
                                        size="small"
                                        fullWidth
                                        value={variable.value || ''}
                                        onChange={(e) =>
                                            handleArrayItemChange('variables', index, 'value', e.target.value)
                                        }
                                    />
                                </TableCell>
                                <TableCell>
                                    <IconButton
                                        size="small"
                                        onClick={() => handleArrayItemDelete('variables', index)}
                                    >
                                        <DeleteIcon />
                                    </IconButton>
                                </TableCell>
                            </TableRow>
                        ))}
                    </TableBody>
                </Table>
            </TableContainer>
            <MuiButton
                startIcon={<AddIcon />}
                onClick={() => handleArrayItemAdd('variables', { name: '', value: '' })}
                size="small"
            >
                添加变量
            </MuiButton>
        </Stack>
    );
    // CSV数据集配置
    const renderCsvDataSetConfig = () => (
        <Stack spacing={2}>
            <TextField
                label="文件名"
                value={config.filename || ''}
                onChange={(e) => handleConfigChange('filename', e.target.value)}
                fullWidth
            />
            <TextField
                label="文件编码"
                value={config.fileEncoding || 'UTF-8'}
                onChange={(e) => handleConfigChange('fileEncoding', e.target.value)}
                fullWidth
            />
            <TextField
                label="变量名(逗号分隔)"
                value={config.variableNames || ''}
                onChange={(e) => handleConfigChange('variableNames', e.target.value)}
                fullWidth
                placeholder="var1,var2,var3"
            />
            <TextField
                label="分隔符"
                value={config.delimiter || ','}
                onChange={(e) => handleConfigChange('delimiter', e.target.value)}
                fullWidth
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.recycle !== false}
                        onChange={(e) => handleConfigChange('recycle', e.target.checked)}
                    />
                }
                label="Recycle on EOF(文件结束后重新开始)"
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.stopThread || false}
                        onChange={(e) => handleConfigChange('stopThread', e.target.checked)}
                    />
                }
                label="Stop thread on EOF(文件结束后停止线程)"
            />
        </Stack>
    );
    // 组托任务配置
    const renderPalletizeTaskConfig = () => (
        <Stack spacing={2}>
            <TextField
                label="托盘码(可选,不填则自动生成,支持变量)"
                value={config.barcode || ''}
                onChange={(e) => handleConfigChange('barcode', e.target.value)}
                fullWidth
                placeholder="留空则自动生成,可使用变量如 ${barcode}"
                helperText="支持变量替换,如 ${barcode}"
            />
            <TextField
                label="物料编号(逗号分隔,可选)"
                value={config.matnrCodes ? (Array.isArray(config.matnrCodes) ? config.matnrCodes.join(',') : config.matnrCodes) : ''}
                onChange={(e) => {
                    const codes = e.target.value.split(',').map(c => c.trim()).filter(c => c);
                    handleConfigChange('matnrCodes', codes);
                }}
                fullWidth
                placeholder="MAT001,MAT002,MAT003"
                helperText="多个物料用逗号分隔,留空则从系统随机选择"
            />
            <TextField
                label="随机物料数量"
                type="number"
                value={config.randomMaterialCount || 1}
                onChange={(e) => handleConfigChange('randomMaterialCount', parseInt(e.target.value) || 1)}
                fullWidth
                inputProps={{ min: 1 }}
                helperText="从物料列表中随机选择的数量"
            />
            <TextField
                label="收货数量(默认值)"
                type="number"
                value={config.receiptQty || 10}
                onChange={(e) => handleConfigChange('receiptQty', parseFloat(e.target.value) || 10)}
                fullWidth
                inputProps={{ min: 0, step: 0.1 }}
                helperText="每个物料的收货数量,实际数量会在10-100之间随机"
            />
            <Divider />
            <TextField
                label="请求间隔(毫秒)"
                type="number"
                value={config.requestInterval || 0}
                onChange={(e) => handleConfigChange('requestInterval', parseInt(e.target.value) || 0)}
                fullWidth
                inputProps={{ min: 0 }}
                helperText="执行此步骤后等待的时间,0表示不等待"
            />
        </Stack>
    );
    // 入库任务配置
    const renderInboundTaskConfig = () => (
        <Stack spacing={2}>
            <FormControl fullWidth>
                <InputLabel>接口类型</InputLabel>
                <Select
                    value={config.apiType || 'create_in_task'}
                    onChange={(e) => handleConfigChange('apiType', e.target.value)}
                >
                    <MenuItem value="create_in_task">创建入库任务</MenuItem>
                    <MenuItem value="location_allocate">申请库位分配</MenuItem>
                </Select>
            </FormControl>
            <TextField
                label="托盘码(支持变量,如:${barcode})"
                value={config.barcode || ''}
                onChange={(e) => handleConfigChange('barcode', e.target.value)}
                fullWidth
                placeholder="留空则从组托步骤获取"
                helperText="可以使用变量,如 ${barcode} 或 ${waitPakinCode}"
            />
            <TextField
                label="入库站点"
                value={config.staNo || ''}
                onChange={(e) => handleConfigChange('staNo', e.target.value)}
                fullWidth
                placeholder="如:1007"
            />
            <TextField
                label="入库类型"
                type="number"
                value={config.type || 1}
                onChange={(e) => handleConfigChange('type', parseInt(e.target.value) || 1)}
                fullWidth
            />
            <TextField
                label="入库库位号(可选,多个用逗号分隔)"
                value={config.inboundLocNos ? (Array.isArray(config.inboundLocNos) ? config.inboundLocNos.join(',') : config.inboundLocNos) : ''}
                onChange={(e) => {
                    const locs = e.target.value.split(',').map(l => l.trim()).filter(l => l);
                    handleConfigChange('inboundLocNos', locs);
                }}
                fullWidth
                placeholder="A100100101,A100100102"
                helperText="多个库位用逗号分隔,将随机选择一个"
            />
            <Divider />
            <TextField
                label="请求间隔(毫秒)"
                type="number"
                value={config.requestInterval || 0}
                onChange={(e) => handleConfigChange('requestInterval', parseInt(e.target.value) || 0)}
                fullWidth
                inputProps={{ min: 0 }}
                helperText="执行此步骤后等待的时间,0表示不等待"
            />
        </Stack>
    );
    // 出库任务配置
    const renderOutboundTaskConfig = () => (
        <Stack spacing={2}>
            <TextField
                label="出库站点"
                value={config.staNo || ''}
                onChange={(e) => handleConfigChange('staNo', e.target.value)}
                fullWidth
                placeholder="如:1008"
            />
            <TextField
                label="出库库位号(可选,多个用逗号分隔)"
                value={config.outboundLocNos ? (Array.isArray(config.outboundLocNos) ? config.outboundLocNos.join(',') : config.outboundLocNos) : ''}
                onChange={(e) => {
                    const locs = e.target.value.split(',').map(l => l.trim()).filter(l => l);
                    handleConfigChange('outboundLocNos', locs);
                }}
                fullWidth
                placeholder="A100100201,A100100202"
                helperText="多个库位用逗号分隔,将随机选择单个或多个组合出库"
            />
            <FormControlLabel
                control={
                    <Checkbox
                        checked={config.checkStock !== false}
                        onChange={(e) => handleConfigChange('checkStock', e.target.checked)}
                    />
                }
                label="检查库存(启用后只从有库存的库位中选择)"
            />
            <Divider />
            <TextField
                label="请求间隔(毫秒)"
                type="number"
                value={config.requestInterval || 0}
                onChange={(e) => handleConfigChange('requestInterval', parseInt(e.target.value) || 0)}
                fullWidth
                inputProps={{ min: 0 }}
                helperText="执行此步骤后等待的时间,0表示不等待"
            />
        </Stack>
    );
    return (
        <Box>
            <Tabs value={configTab} onChange={(e, v) => setConfigTab(v)}>
                <Tab label="基本信息" />
                <Tab label="参数配置" />
                <Tab label="高级设置" />
            </Tabs>
            <TabPanel value={configTab} index={0}>
                <Stack spacing={2} sx={{ mt: 2 }}>
                    <TextField
                        label="步骤名称"
                        fullWidth
                        value={node?.name || ''}
                        onChange={(e) => {
                            if (onNodeNameChange) {
                                onNodeNameChange(node.id, e.target.value);
                            }
                        }}
                    />
                    <FormControlLabel
                        control={
                            <Checkbox
                                checked={node?.enabled !== false}
                                onChange={(e) => {
                                    if (onNodeEnabledChange) {
                                        onNodeEnabledChange(node.id, e.target.checked);
                                    }
                                }}
                            />
                        }
                        label="启用此步骤"
                    />
                </Stack>
            </TabPanel>
            <TabPanel value={configTab} index={1}>
                {renderConfigByType()}
            </TabPanel>
            <TabPanel value={configTab} index={2}>
                <Typography variant="body2" color="text.secondary">
                    高级设置功能开发中...
                </Typography>
            </TabPanel>
            {/* 接口选择器 */}
            {node?.type === 'http_request' && (
                <ApiSelector
                    open={apiSelectorOpen}
                    onClose={() => setApiSelectorOpen(false)}
                    onSelect={(api) => {
                        handleConfigChange('method', api.method);
                        handleConfigChange('url', api.url);
                        if (api.path) {
                            handleConfigChange('path', api.path);
                        }
                    }}
                    currentUrl={config.url || ''}
                />
            )}
        </Box>
    );
};
export default ComponentConfigPanel;
rsf-admin/src/page/rcsTest/components/ComponentMenuFactory.js
New file
@@ -0,0 +1,201 @@
/**
 * 组件菜单工厂
 * 提供分类的组件菜单,参考测试计划工具的设计理念
 */
import {
    THREAD_GROUP,
    SETUP_THREAD_GROUP,
    TEARDOWN_THREAD_GROUP,
    HTTP_REQUEST,
    PALLETIZE_TASK,
    INBOUND_TASK,
    OUTBOUND_TASK,
    VIEW_RESULTS_TREE,
    SUMMARY_REPORT,
    AGGREGATE_REPORT,
    RESPONSE_ASSERTION,
    JSON_ASSERTION,
    HTTP_REQUEST_DEFAULT_CONFIG,
    HTTP_COOKIE_MANAGER,
    HTTP_HEADER_MANAGER,
    USER_DEFINED_VARIABLES,
    CSV_DATA_SET_CONFIG,
    REGULAR_EXPRESSION_EXTRACTOR,
    JSON_EXTRACTOR,
    CONSTANT_TIMER,
    LOOP_CONTROLLER,
    IF_CONTROLLER,
    SIMPLE_CONTROLLER,
    WHILE_CONTROLLER,
    SWITCH_CONTROLLER,
    FOR_EACH_CONTROLLER,
} from './JmeterComponents';
import { COMPONENT_CONFIGS } from './JmeterComponents';
/**
 * 菜单分类
 */
export const MENU_CATEGORIES = {
    THREADS: 'threads',           // 线程组
    SAMPLERS: 'samplers',         // 采样器
    LOGIC_CONTROLLERS: 'logic_controllers', // 逻辑控制器
    CONFIG_ELEMENTS: 'config_elements', // 配置元件
    PRE_PROCESSORS: 'pre_processors', // 前置处理器
    POST_PROCESSORS: 'post_processors', // 后置处理器
    ASSERTIONS: 'assertions',      // 断言
    LISTENERS: 'listeners',        // 监听器
    TIMERS: 'timers',              // 定时器
};
/**
 * 组件菜单映射
 * 按照测试计划工具的分类方式组织组件
 */
export const COMPONENT_MENU_MAP = {
    [MENU_CATEGORIES.THREADS]: [
        { type: THREAD_GROUP, label: '线程组', icon: '👥' },
        { type: SETUP_THREAD_GROUP, label: 'setUp线程组', icon: '⬆️' },
        { type: TEARDOWN_THREAD_GROUP, label: 'tearDown线程组', icon: '⬇️' },
    ],
    [MENU_CATEGORIES.SAMPLERS]: [
        { type: HTTP_REQUEST, label: 'HTTP请求', icon: '🌐' },
        { type: PALLETIZE_TASK, label: '组托任务', icon: '📦' },
        { type: INBOUND_TASK, label: '入库任务', icon: '📥' },
        { type: OUTBOUND_TASK, label: '出库任务', icon: '📤' },
    ],
    [MENU_CATEGORIES.LOGIC_CONTROLLERS]: [
        { type: SIMPLE_CONTROLLER, label: '简单控制器', icon: '📁' },
        { type: LOOP_CONTROLLER, label: '循环控制器', icon: '🔄' },
        { type: IF_CONTROLLER, label: '如果(If)控制器', icon: '❓' },
        { type: WHILE_CONTROLLER, label: 'While控制器', icon: '🔁' },
        { type: SWITCH_CONTROLLER, label: 'Switch控制器', icon: '🔀' },
        { type: FOR_EACH_CONTROLLER, label: 'ForEach控制器', icon: '🔂' },
    ],
    [MENU_CATEGORIES.CONFIG_ELEMENTS]: [
        { type: HTTP_REQUEST_DEFAULT_CONFIG, label: 'HTTP请求默认值', icon: '⚙️' },
        { type: HTTP_COOKIE_MANAGER, label: 'HTTP Cookie管理器', icon: '🍪' },
        { type: HTTP_HEADER_MANAGER, label: 'HTTP头管理器', icon: '📋' },
        { type: USER_DEFINED_VARIABLES, label: '用户定义的变量', icon: '📝' },
        { type: CSV_DATA_SET_CONFIG, label: 'CSV数据集配置', icon: '📊' },
    ],
    [MENU_CATEGORIES.PRE_PROCESSORS]: [
        // 前置处理器组件
    ],
    [MENU_CATEGORIES.POST_PROCESSORS]: [
        { type: REGULAR_EXPRESSION_EXTRACTOR, label: '正则表达式提取器', icon: '🔍' },
        { type: JSON_EXTRACTOR, label: 'JSON提取器', icon: '📄' },
    ],
    [MENU_CATEGORIES.ASSERTIONS]: [
        { type: RESPONSE_ASSERTION, label: '响应断言', icon: '✓' },
        { type: JSON_ASSERTION, label: 'JSON断言', icon: '📋' },
        { type: 'duration_assertion', label: '持续时间断言', icon: '⏱️' },
        { type: 'size_assertion', label: '大小断言', icon: '📏' },
    ],
    [MENU_CATEGORIES.LISTENERS]: [
        { type: VIEW_RESULTS_TREE, label: '查看结果树', icon: '🌳' },
        { type: SUMMARY_REPORT, label: '汇总报告', icon: '📊' },
        { type: AGGREGATE_REPORT, label: '聚合报告', icon: '📈' },
    ],
    [MENU_CATEGORIES.TIMERS]: [
        { type: CONSTANT_TIMER, label: '固定定时器', icon: '⏱️' },
    ],
};
/**
 * 获取分类的中文名称
 */
export const getCategoryLabel = (category) => {
    const labels = {
        [MENU_CATEGORIES.THREADS]: '线程组',
        [MENU_CATEGORIES.SAMPLERS]: '采样器',
        [MENU_CATEGORIES.LOGIC_CONTROLLERS]: '逻辑控制器',
        [MENU_CATEGORIES.CONFIG_ELEMENTS]: '配置元件',
        [MENU_CATEGORIES.PRE_PROCESSORS]: '前置处理器',
        [MENU_CATEGORIES.POST_PROCESSORS]: '后置处理器',
        [MENU_CATEGORIES.ASSERTIONS]: '断言',
        [MENU_CATEGORIES.LISTENERS]: '监听器',
        [MENU_CATEGORIES.TIMERS]: '定时器',
    };
    return labels[category] || category;
};
/**
 * 根据节点类型判断可以添加哪些类型的子组件
 */
export const getAllowedChildTypes = (nodeType) => {
    // 测试计划可以添加所有类型
    if (nodeType === 'test_plan') {
        return Object.values(MENU_CATEGORIES);
    }
    // 线程组可以添加采样器、逻辑控制器、配置元件、前置处理器、后置处理器、断言、监听器、定时器
    if (nodeType === THREAD_GROUP || nodeType === SETUP_THREAD_GROUP || nodeType === TEARDOWN_THREAD_GROUP) {
        return [
            MENU_CATEGORIES.SAMPLERS,
            MENU_CATEGORIES.LOGIC_CONTROLLERS,
            MENU_CATEGORIES.CONFIG_ELEMENTS,
            MENU_CATEGORIES.PRE_PROCESSORS,
            MENU_CATEGORIES.POST_PROCESSORS,
            MENU_CATEGORIES.ASSERTIONS,
            MENU_CATEGORIES.LISTENERS,
            MENU_CATEGORIES.TIMERS,
        ];
    }
    // 逻辑控制器可以添加采样器、逻辑控制器、配置元件、前置处理器、后置处理器、断言、监听器、定时器
    if ([SIMPLE_CONTROLLER, LOOP_CONTROLLER, IF_CONTROLLER].includes(nodeType)) {
        return [
            MENU_CATEGORIES.SAMPLERS,
            MENU_CATEGORIES.LOGIC_CONTROLLERS,
            MENU_CATEGORIES.CONFIG_ELEMENTS,
            MENU_CATEGORIES.PRE_PROCESSORS,
            MENU_CATEGORIES.POST_PROCESSORS,
            MENU_CATEGORIES.ASSERTIONS,
            MENU_CATEGORIES.LISTENERS,
            MENU_CATEGORIES.TIMERS,
        ];
    }
    // 采样器可以添加前置处理器、后置处理器、断言、监听器、定时器
    if ([HTTP_REQUEST, PALLETIZE_TASK, INBOUND_TASK, OUTBOUND_TASK].includes(nodeType)) {
        return [
            MENU_CATEGORIES.PRE_PROCESSORS,
            MENU_CATEGORIES.POST_PROCESSORS,
            MENU_CATEGORIES.ASSERTIONS,
            MENU_CATEGORIES.LISTENERS,
            MENU_CATEGORIES.TIMERS,
        ];
    }
    // 默认:可以添加所有类型
    return Object.values(MENU_CATEGORIES);
};
/**
 * 获取组件配置信息
 */
export const getComponentInfo = (componentType) => {
    return COMPONENT_CONFIGS[componentType] || null;
};
/**
 * 检查组件类型是否存在于菜单中
 */
export const isComponentInMenu = (componentType) => {
    for (const components of Object.values(COMPONENT_MENU_MAP)) {
        if (components.some(comp => comp.type === componentType)) {
            return true;
        }
    }
    return false;
};
rsf-admin/src/page/rcsTest/components/JmeterComponents.js
New file
@@ -0,0 +1,500 @@
/**
 * RCS测试组件类型定义
 * 完整实现测试计划的所有核心功能
 */
// ==================== 测试计划 ====================
export const TEST_PLAN = 'test_plan';
// ==================== 线程组 ====================
export const THREAD_GROUP = 'thread_group';
export const SETUP_THREAD_GROUP = 'setup_thread_group';
export const TEARDOWN_THREAD_GROUP = 'teardown_thread_group';
// ==================== 采样器 (Sampler) ====================
export const HTTP_REQUEST = 'http_request';
export const HTTP_REQUEST_DEFAULT = 'http_request_default';
export const JDBC_REQUEST = 'jdbc_request';
export const FTP_REQUEST = 'ftp_request';
export const SOAP_REQUEST = 'soap_request';
export const TCP_REQUEST = 'tcp_request';
export const LDAP_REQUEST = 'ldap_request';
export const SMTP_REQUEST = 'smtp_request';
export const PALLETIZE_TASK = 'palletize_task'; // RCS特定 - 组托
export const INBOUND_TASK = 'inbound_task'; // RCS特定 - 入库
export const OUTBOUND_TASK = 'outbound_task'; // RCS特定 - 出库
// ==================== 监听器 (Listener) ====================
export const VIEW_RESULTS_TREE = 'view_results_tree';
export const SUMMARY_REPORT = 'summary_report';
export const AGGREGATE_REPORT = 'aggregate_report';
export const GRAPH_RESULTS = 'graph_results';
export const RESPONSE_TIME_GRAPH = 'response_time_graph';
export const RESULT_STATUS_ACTION_HANDLER = 'result_status_action_handler';
export const SAVE_RESPONSES_TO_A_FILE = 'save_responses_to_a_file';
export const SIMPLE_DATA_WRITER = 'simple_data_writer';
export const BACKEND_LISTENER = 'backend_listener';
// ==================== 断言 (Assertion) ====================
export const RESPONSE_ASSERTION = 'response_assertion';
export const JSON_ASSERTION = 'json_assertion';
export const XPATH_ASSERTION = 'xpath_assertion';
export const BEANSHELL_ASSERTION = 'beanshell_assertion';
export const DURATION_ASSERTION = 'duration_assertion';
export const SIZE_ASSERTION = 'size_assertion';
export const HTML_ASSERTION = 'html_assertion';
export const XML_ASSERTION = 'xml_assertion';
// ==================== 配置元件 (Config Element) ====================
export const HTTP_REQUEST_DEFAULT_CONFIG = 'http_request_default_config';
export const HTTP_COOKIE_MANAGER = 'http_cookie_manager';
export const HTTP_HEADER_MANAGER = 'http_header_manager';
export const HTTP_CACHE_MANAGER = 'http_cache_manager';
export const USER_DEFINED_VARIABLES = 'user_defined_variables';
export const CSV_DATA_SET_CONFIG = 'csv_data_set_config';
export const COUNTER_CONFIG = 'counter_config';
export const RANDOM_VARIABLE = 'random_variable';
export const JDBC_CONNECTION_CONFIGURATION = 'jdbc_connection_configuration';
// ==================== 前置处理器 (Pre Processor) ====================
export const USER_PARAMETERS = 'user_parameters';
export const BEANSHELL_PRE_PROCESSOR = 'beanshell_pre_processor';
export const JSR223_PRE_PROCESSOR = 'jsr223_pre_processor';
export const HTTP_URL_REWRITING_MODIFIER = 'http_url_rewriting_modifier';
export const HTML_LINK_PARSER = 'html_link_parser';
export const REGEX_USER_PARAMETERS = 'regex_user_parameters';
// ==================== 后置处理器 (Post Processor) ====================
export const REGULAR_EXPRESSION_EXTRACTOR = 'regular_expression_extractor';
export const JSON_EXTRACTOR = 'json_extractor';
export const XPATH_EXTRACTOR = 'xpath_extractor';
export const CSS_SELECTOR_EXTRACTOR = 'css_selector_extractor';
export const BEANSHELL_POST_PROCESSOR = 'beanshell_post_processor';
export const JSR223_POST_PROCESSOR = 'jsr223_post_processor';
export const RESULT_STATUS_ACTION_HANDLER_POST = 'result_status_action_handler_post';
// ==================== 定时器 (Timer) ====================
export const CONSTANT_TIMER = 'constant_timer';
export const GAUSSIAN_RANDOM_TIMER = 'gaussian_random_timer';
export const UNIFORM_RANDOM_TIMER = 'uniform_random_timer';
export const SYNCHRONIZING_TIMER = 'synchronizing_timer';
export const CONSTANT_THROUGHPUT_TIMER = 'constant_throughput_timer';
export const BEAST_THREADS_TIMER = 'beast_threads_timer';
export const POISSON_RANDOM_TIMER = 'poisson_random_timer';
// ==================== 逻辑控制器 (Logic Controller) ====================
export const SIMPLE_CONTROLLER = 'simple_controller';
export const LOOP_CONTROLLER = 'loop_controller';
export const ONCE_ONLY_CONTROLLER = 'once_only_controller';
export const IF_CONTROLLER = 'if_controller';
export const WHILE_CONTROLLER = 'while_controller';
export const SWITCH_CONTROLLER = 'switch_controller';
export const FOR_EACH_CONTROLLER = 'for_each_controller';
export const RANDOM_CONTROLLER = 'random_controller';
export const RANDOM_ORDER_CONTROLLER = 'random_order_controller';
export const TRANSACTION_CONTROLLER = 'transaction_controller';
export const MODULE_CONTROLLER = 'module_controller';
export const INCLUDE_CONTROLLER = 'include_controller';
export const EXCLUDE_CONTROLLER = 'exclude_controller';
export const RECORDING_CONTROLLER = 'recording_controller';
// ==================== 组件配置 ====================
export const COMPONENT_CONFIGS = {
    // 线程组
    [THREAD_GROUP]: {
        label: '线程组',
        icon: '👥',
        category: 'thread_group',
        defaultConfig: {
            numThreads: 1,
            rampUp: 1,
            loops: 1,
            sameUserOnNextIteration: true,
            scheduler: false,
            duration: 60,
            delay: 0,
        },
    },
    [SETUP_THREAD_GROUP]: {
        label: 'setUp线程组',
        icon: '⬆️',
        category: 'thread_group',
        defaultConfig: {
            numThreads: 1,
            rampUp: 1,
            loops: 1,
        },
    },
    [TEARDOWN_THREAD_GROUP]: {
        label: 'tearDown线程组',
        icon: '⬇️',
        category: 'thread_group',
        defaultConfig: {
            numThreads: 1,
            rampUp: 1,
            loops: 1,
        },
    },
    // HTTP请求
    [HTTP_REQUEST]: {
        label: 'HTTP请求',
        icon: '🌐',
        category: 'sampler',
        defaultConfig: {
            method: 'GET',
            protocol: 'http',
            serverName: '',
            portNumber: '',
            path: '',
            contentEncoding: 'UTF-8',
            followRedirects: true,
            autoRedirects: false,
            useKeepAlive: true,
            doMultipartPost: false,
            browserCompatibleMultipart: false,
            parameters: [],
            body: '',
            files: [],
            headers: [],
        },
    },
    [HTTP_REQUEST_DEFAULT]: {
        label: 'HTTP请求默认值',
        icon: '🌐',
        category: 'config',
        defaultConfig: {
            protocol: 'http',
            serverName: '',
            portNumber: '',
            path: '',
        },
    },
    // 监听器
    [VIEW_RESULTS_TREE]: {
        label: '查看结果树',
        icon: '🌳',
        category: 'listener',
        defaultConfig: {
            filename: '',
            logErrors: true,
            logSuccess: true,
            showOnlyLogErrors: false,
        },
    },
    [SUMMARY_REPORT]: {
        label: '汇总报告',
        icon: '📊',
        category: 'listener',
        defaultConfig: {
            filename: '',
            includeResponseTime: true,
            includeLatency: true,
            includeConnectTime: true,
        },
    },
    [AGGREGATE_REPORT]: {
        label: '聚合报告',
        icon: '📈',
        category: 'listener',
        defaultConfig: {
            filename: '',
            includeResponseTime: true,
            includeLatency: true,
            includeConnectTime: true,
        },
    },
    [GRAPH_RESULTS]: {
        label: '图形结果',
        icon: '📉',
        category: 'listener',
        defaultConfig: {
            filename: '',
            graphType: 'line',
        },
    },
    // 断言
    [RESPONSE_ASSERTION]: {
        label: '响应断言',
        icon: '✓',
        category: 'assertion',
        defaultConfig: {
            testField: 'response_code',
            testType: 'equals',
            testString: '200',
            not: false,
            or: false,
        },
    },
    [JSON_ASSERTION]: {
        label: 'JSON断言',
        icon: '📋',
        category: 'assertion',
        defaultConfig: {
            jsonPath: '',
            expectedValue: '',
            validateJsonPath: true,
            expectNull: false,
            invert: false,
        },
    },
    [DURATION_ASSERTION]: {
        label: '持续时间断言',
        icon: '⏱️',
        category: 'assertion',
        defaultConfig: {
            duration: 2000,
        },
    },
    // 配置元件
    [USER_DEFINED_VARIABLES]: {
        label: '用户定义的变量',
        icon: '📝',
        category: 'config',
        defaultConfig: {
            variables: [],
        },
    },
    [CSV_DATA_SET_CONFIG]: {
        label: 'CSV数据集配置',
        icon: '📄',
        category: 'config',
        defaultConfig: {
            filename: '',
            fileEncoding: 'UTF-8',
            variableNames: '',
            delimiter: ',',
            allowQuotedData: false,
            recycle: true,
            stopThread: false,
            sharingMode: 'All threads',
        },
    },
    [COUNTER_CONFIG]: {
        label: '计数器',
        icon: '🔢',
        category: 'config',
        defaultConfig: {
            start: 1,
            end: 100,
            increment: 1,
            format: '',
            perUser: false,
        },
    },
    [RANDOM_VARIABLE]: {
        label: '随机变量',
        icon: '🎲',
        category: 'config',
        defaultConfig: {
            variableName: '',
            outputFormat: '',
            minimumValue: 0,
            maximumValue: 100,
            randomSeed: '',
        },
    },
    // 前置处理器
    [USER_PARAMETERS]: {
        label: '用户参数',
        icon: '👤',
        category: 'pre_processor',
        defaultConfig: {
            parameters: [],
            updateOncePerIteration: false,
        },
    },
    // 后置处理器
    [REGULAR_EXPRESSION_EXTRACTOR]: {
        label: '正则表达式提取器',
        icon: '🔍',
        category: 'post_processor',
        defaultConfig: {
            variableName: '',
            regex: '',
            template: '$1$',
            matchNumber: 1,
            defaultValue: '',
            useField: 'body',
        },
    },
    [JSON_EXTRACTOR]: {
        label: 'JSON提取器',
        icon: '📋',
        category: 'post_processor',
        defaultConfig: {
            variableName: '',
            jsonPath: '',
            matchNumber: 1,
            defaultValue: '',
            computeConcatenation: false,
        },
    },
    // 定时器
    [CONSTANT_TIMER]: {
        label: '固定定时器',
        icon: '⏱️',
        category: 'timer',
        defaultConfig: {
            delay: 1000,
        },
    },
    [GAUSSIAN_RANDOM_TIMER]: {
        label: '高斯随机定时器',
        icon: '📊',
        category: 'timer',
        defaultConfig: {
            delay: 0,
            range: 100,
        },
    },
    [UNIFORM_RANDOM_TIMER]: {
        label: '统一随机定时器',
        icon: '🎲',
        category: 'timer',
        defaultConfig: {
            delay: 0,
            range: 100,
        },
    },
    [SYNCHRONIZING_TIMER]: {
        label: '同步定时器',
        icon: '🔗',
        category: 'timer',
        defaultConfig: {
            numThreads: 0,
            timeout: 0,
        },
    },
    [CONSTANT_THROUGHPUT_TIMER]: {
        label: '常数吞吐量定时器',
        icon: '📈',
        category: 'timer',
        defaultConfig: {
            throughput: 60,
            calculateThroughputBasedOn: 'this thread only',
        },
    },
    // 逻辑控制器
    [SIMPLE_CONTROLLER]: {
        label: '简单控制器',
        icon: '📦',
        category: 'logic_controller',
        defaultConfig: {},
    },
    [LOOP_CONTROLLER]: {
        label: '循环控制器',
        icon: '🔄',
        category: 'logic_controller',
        defaultConfig: {
            loops: 1,
            continueForever: false,
        },
    },
    [IF_CONTROLLER]: {
        label: '如果(If)控制器',
        icon: '❓',
        category: 'logic_controller',
        defaultConfig: {
            condition: '',
            evaluateAll: false,
            useExpression: true,
        },
    },
    [WHILE_CONTROLLER]: {
        label: 'While控制器',
        icon: '🔁',
        category: 'logic_controller',
        defaultConfig: {
            condition: '',
        },
    },
    [FOR_EACH_CONTROLLER]: {
        label: 'ForEach控制器',
        icon: '🔂',
        category: 'logic_controller',
        defaultConfig: {
            inputVariable: '',
            outputVariable: '',
            startIndex: '',
            endIndex: '',
            addSeparator: false,
        },
    },
    [RANDOM_CONTROLLER]: {
        label: '随机控制器',
        icon: '🎲',
        category: 'logic_controller',
        defaultConfig: {},
    },
    [RANDOM_ORDER_CONTROLLER]: {
        label: '随机顺序控制器',
        icon: '🔀',
        category: 'logic_controller',
        defaultConfig: {},
    },
    [TRANSACTION_CONTROLLER]: {
        label: '事务控制器',
        icon: '📦',
        category: 'logic_controller',
        defaultConfig: {
            generateParentSample: false,
            includeTimers: true,
        },
    },
    // RCS特定
    [PALLETIZE_TASK]: {
        label: '组托任务',
        icon: '📦',
        category: 'sampler',
        defaultConfig: {
            matnrCodes: [],
            barcode: '',
            randomMaterialCount: 1,
            receiptQty: 10,
        },
    },
    [INBOUND_TASK]: {
        label: '入库任务',
        icon: '📥',
        category: 'sampler',
        defaultConfig: {
            apiType: 'create_in_task',
            barcode: '',
            staNo: '',
            type: 1,
            inboundLocNos: [],
        },
    },
    [OUTBOUND_TASK]: {
        label: '出库任务',
        icon: '📤',
        category: 'sampler',
        defaultConfig: {
            staNo: '',
            checkStock: true,
            outboundLocNos: [],
        },
    },
};
// 组件分类
export const COMPONENT_CATEGORIES = {
    thread_group: { label: '线程组', icon: '👥' },
    sampler: { label: '采样器', icon: '📡' },
    listener: { label: '监听器', icon: '👂' },
    assertion: { label: '断言', icon: '✓' },
    config: { label: '配置元件', icon: '⚙️' },
    pre_processor: { label: '前置处理器', icon: '⬆️' },
    post_processor: { label: '后置处理器', icon: '⬇️' },
    timer: { label: '定时器', icon: '⏱️' },
    logic_controller: { label: '逻辑控制器', icon: '🎛️' },
};
rsf-admin/src/page/rcsTest/components/SelectModal.jsx
New file
@@ -0,0 +1,301 @@
import React, { useState, useEffect, useRef } from 'react';
import {
    Dialog,
    DialogTitle,
    DialogContent,
    DialogActions,
    TextField,
    Button as MuiButton,
    Box,
    Stack,
    Chip,
    CircularProgress,
    InputAdornment,
} from '@mui/material';
import { DataGrid } from '@mui/x-data-grid';
import SearchIcon from '@mui/icons-material/Search';
import DialogCloseButton from '../../components/DialogCloseButton';
import request from '@/utils/request';
/**
 * 通用选择弹窗组件
 * @param {Object} props
 * @param {boolean} props.open - 是否打开
 * @param {Function} props.onClose - 关闭回调
 * @param {Function} props.onConfirm - 确认回调,参数为选中的项数组
 * @param {string} props.title - 弹窗标题
 * @param {string} props.apiUrl - API地址
 * @param {Function} props.apiMethod - API方法,默认'post'
 * @param {Function} props.transformData - 数据转换函数,将API返回的数据转换为表格格式
 * @param {Array} props.columns - 表格列定义
 * @param {string} props.searchField - 搜索字段名
 * @param {boolean} props.multiple - 是否多选,默认true
 * @param {Array} props.selectedItems - 已选中的项(用于回显)
 * @param {string} props.idField - ID字段名,默认'id'
 */
const SelectModal = ({
    open,
    onClose,
    onConfirm,
    title,
    apiUrl,
    apiMethod = 'post',
    transformData,
    columns,
    searchField = 'code',
    multiple = true,
    selectedItems = [],
    idField = 'id',
}) => {
    const [data, setData] = useState([]);
    const [loading, setLoading] = useState(false);
    const [searchKeyword, setSearchKeyword] = useState('');
    const [paginationModel, setPaginationModel] = useState({
        page: 0,
        pageSize: 20,
    });
    const [rowCount, setRowCount] = useState(0);
    const [selectedRows, setSelectedRows] = useState([]);
    const searchTimeoutRef = useRef(null);
    // 初始化选中项
    useEffect(() => {
        if (open && selectedItems.length > 0) {
            const ids = selectedItems.map(item => item[idField] || item);
            setSelectedRows(ids);
        } else if (open) {
            setSelectedRows([]);
        }
    }, [open, selectedItems, idField]);
    // 加载数据
    const loadData = async (page = 0, pageSize = 20, keyword = '') => {
        setLoading(true);
        try {
            const params = {
                current: page + 1,
                pageSize: pageSize,
            };
            // 添加搜索条件
            if (keyword) {
                // 支持多个搜索字段(物料支持code和name)
                if (searchField === 'code' && apiUrl.includes('/matnr/')) {
                    // 物料搜索:支持编码和名称
                    params.code = keyword;
                    params.name = keyword;
                } else {
                    params[searchField] = keyword;
                }
            }
            const response = await request[apiMethod](apiUrl, params);
            if (response?.data?.code === 200) {
                const result = response.data.data;
                let items = [];
                let total = 0;
                // 处理分页数据
                if (result.records) {
                    items = result.records;
                    total = result.total || 0;
                } else if (Array.isArray(result)) {
                    items = result;
                    total = result.length;
                } else {
                    items = [];
                    total = 0;
                }
                // 转换数据
                const transformedData = transformData ? items.map(transformData) : items;
                setData(transformedData);
                setRowCount(total);
            } else {
                console.error('加载数据失败:', response?.data?.msg);
                setData([]);
                setRowCount(0);
            }
        } catch (error) {
            console.error('加载数据失败:', error);
            setData([]);
            setRowCount(0);
        } finally {
            setLoading(false);
        }
    };
    // 搜索防抖
    useEffect(() => {
        if (searchTimeoutRef.current) {
            clearTimeout(searchTimeoutRef.current);
        }
        searchTimeoutRef.current = setTimeout(() => {
            if (open) {
                loadData(0, paginationModel.pageSize, searchKeyword);
                setPaginationModel(prev => ({ ...prev, page: 0 }));
            }
        }, 500);
        return () => {
            if (searchTimeoutRef.current) {
                clearTimeout(searchTimeoutRef.current);
            }
        };
    }, [searchKeyword, open]);
    // 打开弹窗时加载数据
    useEffect(() => {
        if (open) {
            loadData(paginationModel.page, paginationModel.pageSize, searchKeyword);
        } else {
            // 关闭时重置
            setSearchKeyword('');
            setSelectedRows([]);
            setPaginationModel({ page: 0, pageSize: 20 });
        }
    }, [open]);
    // 分页变化
    const handlePaginationModelChange = (newModel) => {
        setPaginationModel(newModel);
        loadData(newModel.page, newModel.pageSize, searchKeyword);
    };
    // 确认选择
    const handleConfirm = () => {
        const selectedData = data.filter(item => selectedRows.includes(item[idField]));
        onConfirm(selectedData);
        onClose();
    };
    // 表格列定义(带复选框)
    const gridColumns = [
        ...columns,
    ];
    return (
        <Dialog
            open={open}
            onClose={onClose}
            maxWidth="md"
            fullWidth
            PaperProps={{
                sx: { height: '80vh' }
            }}
        >
            <DialogTitle>
                {title}
                <DialogCloseButton onClose={onClose} />
            </DialogTitle>
            <DialogContent>
                <Stack spacing={2} sx={{ mt: 1 }}>
                    {/* 搜索框 */}
                    <TextField
                        fullWidth
                        size="small"
                        placeholder={
                            apiUrl.includes('/matnr/')
                                ? '搜索物料编码或名称...'
                                : apiUrl.includes('/loc/')
                                ? '搜索库位号...'
                                : `搜索${searchField === 'code' ? '编码' : '名称'}...`
                        }
                        value={searchKeyword}
                        onChange={(e) => setSearchKeyword(e.target.value)}
                        InputProps={{
                            startAdornment: (
                                <InputAdornment position="start">
                                    <SearchIcon />
                                </InputAdornment>
                            ),
                        }}
                    />
                    {/* 已选中的项 */}
                    {selectedRows.length > 0 && (
                        <Box>
                            <Box sx={{ mb: 1, fontSize: '0.875rem', color: 'text.secondary' }}>
                                {multiple ? `已选择 ${selectedRows.length} 项:` : '已选择:'}
                            </Box>
                            <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
                                {data
                                    .filter(item => selectedRows.includes(item[idField]))
                                    .map((item) => (
                                        <Chip
                                            key={item[idField]}
                                            label={item.label || item.code || item.name || item[idField]}
                                            size="small"
                                            color={multiple ? 'default' : 'primary'}
                                            onDelete={multiple ? () => {
                                                setSelectedRows(prev =>
                                                    prev.filter(id => id !== item[idField])
                                                );
                                            } : () => {
                                                setSelectedRows([]);
                                            }}
                                        />
                                    ))}
                            </Box>
                        </Box>
                    )}
                    {/* 数据表格 */}
                    <Box sx={{ height: 400, width: '100%' }}>
                        <DataGrid
                            rows={data}
                            columns={gridColumns}
                            loading={loading}
                            checkboxSelection={multiple}
                            disableRowSelectionOnClick={!multiple}
                            rowSelectionModel={selectedRows}
                            onRowSelectionModelChange={(newSelection) => {
                                if (multiple) {
                                    setSelectedRows(newSelection);
                                } else {
                                    // 单选模式:只保留最后一个选中的项
                                    setSelectedRows(newSelection.length > 0 ? [newSelection[newSelection.length - 1]] : []);
                                }
                            }}
                            onRowClick={(params) => {
                                if (!multiple) {
                                    // 单选模式:点击行直接选择
                                    const rowId = params.row[idField];
                                    setSelectedRows([rowId]);
                                }
                            }}
                            paginationMode="server"
                            paginationModel={paginationModel}
                            onPaginationModelChange={handlePaginationModelChange}
                            pageSizeOptions={[10, 20, 50, 100]}
                            rowCount={rowCount}
                            getRowId={(row) => row[idField]}
                            sx={{
                                '& .MuiDataGrid-cell': {
                                    fontSize: '0.875rem',
                                },
                                '& .MuiDataGrid-row': {
                                    cursor: multiple ? 'default' : 'pointer',
                                },
                            }}
                        />
                    </Box>
                </Stack>
            </DialogContent>
            <DialogActions>
                <MuiButton onClick={onClose}>取消</MuiButton>
                <MuiButton
                    onClick={handleConfirm}
                    variant="contained"
                    disabled={selectedRows.length === 0}
                >
                    确认选择 {multiple ? `(${selectedRows.length})` : ''}
                </MuiButton>
            </DialogActions>
        </Dialog>
    );
};
export default SelectModal;
rsf-admin/src/page/rcsTest/components/SummaryReport.jsx
New file
@@ -0,0 +1,213 @@
import React, { useMemo } from 'react';
import {
    Box,
    Typography,
    Paper,
    Table,
    TableBody,
    TableCell,
    TableContainer,
    TableHead,
    TableRow,
    Chip,
    Stack,
} from '@mui/material';
/**
 * 汇总报告组件
 * 参考JMeter的SummaryReport,显示统计信息
 */
const SummaryReport = ({ results = [] }) => {
    const statistics = useMemo(() => {
        if (results.length === 0) {
            return {
                total: 0,
                success: 0,
                failed: 0,
                successRate: 0,
                avgResponseTime: 0,
                minResponseTime: 0,
                maxResponseTime: 0,
                totalTime: 0,
            };
        }
        const successfulResults = results.filter(r => r.success);
        const failedResults = results.filter(r => !r.success);
        const responseTimes = results
            .map(r => {
                if (r.startTime && r.endTime) {
                    return new Date(r.endTime) - new Date(r.startTime);
                }
                return 0;
            })
            .filter(t => t > 0);
        const totalTime = results.length > 0
            ? new Date(results[results.length - 1].endTime || Date.now()) - new Date(results[0].startTime)
            : 0;
        const avgResponseTime = responseTimes.length > 0
            ? responseTimes.reduce((sum, t) => sum + t, 0) / responseTimes.length
            : 0;
        const minResponseTime = responseTimes.length > 0
            ? Math.min(...responseTimes)
            : 0;
        const maxResponseTime = responseTimes.length > 0
            ? Math.max(...responseTimes)
            : 0;
        return {
            total: results.length,
            success: successfulResults.length,
            failed: failedResults.length,
            successRate: results.length > 0 ? (successfulResults.length / results.length * 100) : 0,
            avgResponseTime: Math.round(avgResponseTime),
            minResponseTime: Math.round(minResponseTime),
            maxResponseTime: Math.round(maxResponseTime),
            totalTime: Math.round(totalTime),
            throughput: totalTime > 0 ? (results.length / (totalTime / 1000)).toFixed(2) : 0, // 每秒请求数
        };
    }, [results]);
    const formatTime = (ms) => {
        if (ms === 0) return 'N/A';
        return `${ms}ms`;
    };
    return (
        <Box>
            <Typography variant="h6" gutterBottom>
                汇总报告
            </Typography>
            <Paper variant="outlined" sx={{ p: 2, mb: 2 }}>
                <Stack direction="row" spacing={2} flexWrap="wrap">
                    <Box>
                        <Typography variant="caption" color="text.secondary">样本数</Typography>
                        <Typography variant="h6">{statistics.total}</Typography>
                    </Box>
                    <Box>
                        <Typography variant="caption" color="text.secondary">成功</Typography>
                        <Typography variant="h6" color="success.main">{statistics.success}</Typography>
                    </Box>
                    <Box>
                        <Typography variant="caption" color="text.secondary">失败</Typography>
                        <Typography variant="h6" color="error.main">{statistics.failed}</Typography>
                    </Box>
                    <Box>
                        <Typography variant="caption" color="text.secondary">成功率</Typography>
                        <Typography variant="h6">
                            {statistics.successRate.toFixed(2)}%
                        </Typography>
                    </Box>
                    <Box>
                        <Typography variant="caption" color="text.secondary">平均响应时间</Typography>
                        <Typography variant="h6">{formatTime(statistics.avgResponseTime)}</Typography>
                    </Box>
                    <Box>
                        <Typography variant="caption" color="text.secondary">最小响应时间</Typography>
                        <Typography variant="h6">{formatTime(statistics.minResponseTime)}</Typography>
                    </Box>
                    <Box>
                        <Typography variant="caption" color="text.secondary">最大响应时间</Typography>
                        <Typography variant="h6">{formatTime(statistics.maxResponseTime)}</Typography>
                    </Box>
                    <Box>
                        <Typography variant="caption" color="text.secondary">总耗时</Typography>
                        <Typography variant="h6">{formatTime(statistics.totalTime)}</Typography>
                    </Box>
                    <Box>
                        <Typography variant="caption" color="text.secondary">吞吐量</Typography>
                        <Typography variant="h6">{statistics.throughput} req/s</Typography>
                    </Box>
                </Stack>
            </Paper>
            <TableContainer component={Paper} variant="outlined">
                <Table size="small">
                    <TableHead>
                        <TableRow>
                            <TableCell>步骤名称</TableCell>
                            <TableCell align="right">样本数</TableCell>
                            <TableCell align="right">成功</TableCell>
                            <TableCell align="right">失败</TableCell>
                            <TableCell align="right">成功率</TableCell>
                            <TableCell align="right">平均响应时间</TableCell>
                            <TableCell align="right">最小响应时间</TableCell>
                            <TableCell align="right">最大响应时间</TableCell>
                        </TableRow>
                    </TableHead>
                    <TableBody>
                        {(() => {
                            // 按步骤类型分组统计
                            const groupedResults = {};
                            results.forEach(result => {
                                const key = result.nodeName || result.nodeType || 'unknown';
                                if (!groupedResults[key]) {
                                    groupedResults[key] = [];
                                }
                                groupedResults[key].push(result);
                            });
                            return Object.keys(groupedResults).map(key => {
                                const groupResults = groupedResults[key];
                                const successCount = groupResults.filter(r => r.success).length;
                                const responseTimes = groupResults
                                    .map(r => {
                                        if (r.startTime && r.endTime) {
                                            return new Date(r.endTime) - new Date(r.startTime);
                                        }
                                        return 0;
                                    })
                                    .filter(t => t > 0);
                                const avgTime = responseTimes.length > 0
                                    ? Math.round(responseTimes.reduce((sum, t) => sum + t, 0) / responseTimes.length)
                                    : 0;
                                const minTime = responseTimes.length > 0 ? Math.min(...responseTimes) : 0;
                                const maxTime = responseTimes.length > 0 ? Math.max(...responseTimes) : 0;
                                return (
                                    <TableRow key={key}>
                                        <TableCell>{key}</TableCell>
                                        <TableCell align="right">{groupResults.length}</TableCell>
                                        <TableCell align="right">
                                            <Chip
                                                label={successCount}
                                                size="small"
                                                color="success"
                                                variant="outlined"
                                            />
                                        </TableCell>
                                        <TableCell align="right">
                                            <Chip
                                                label={groupResults.length - successCount}
                                                size="small"
                                                color="error"
                                                variant="outlined"
                                            />
                                        </TableCell>
                                        <TableCell align="right">
                                            {groupResults.length > 0
                                                ? ((successCount / groupResults.length) * 100).toFixed(2) + '%'
                                                : '0%'}
                                        </TableCell>
                                        <TableCell align="right">{formatTime(avgTime)}</TableCell>
                                        <TableCell align="right">{formatTime(minTime)}</TableCell>
                                        <TableCell align="right">{formatTime(maxTime)}</TableCell>
                                    </TableRow>
                                );
                            });
                        })()}
                    </TableBody>
                </Table>
            </TableContainer>
        </Box>
    );
};
export default SummaryReport;
rsf-admin/src/page/rcsTest/index.jsx
New file
@@ -0,0 +1,9 @@
import React from "react";
import RcsTestList from "./RcsTestList";
export default {
    list: RcsTestList,
    recordRepresentation: (record) => {
        return `${record.id}`
    }
};
rsf-admin/src/page/rcsTest/utils/variableExtractor.js
New file
@@ -0,0 +1,149 @@
/**
 * 变量提取器
 * 参考JMeter的变量提取实现,从响应中提取变量
 */
/**
 * 正则表达式提取器
 */
export const extractByRegex = (source, regex, template = '$1$', matchNumber = 1) => {
    if (!source || !regex) {
        return null;
    }
    try {
        const regexObj = new RegExp(regex, 'g');
        const matches = [];
        let match;
        while ((match = regexObj.exec(source)) !== null) {
            matches.push(match);
        }
        if (matches.length === 0) {
            return null;
        }
        // matchNumber: 0=随机, -1=所有, 1-N=第N个
        let selectedMatch;
        if (matchNumber === 0) {
            // 随机选择一个
            selectedMatch = matches[Math.floor(Math.random() * matches.length)];
        } else if (matchNumber === -1) {
            // 返回所有匹配(用逗号分隔)
            return matches.map(m => applyTemplate(m, template)).join(',');
        } else {
            // 选择第N个(从1开始)
            selectedMatch = matches[matchNumber - 1] || matches[0];
        }
        if (!selectedMatch) {
            return null;
        }
        return applyTemplate(selectedMatch, template);
    } catch (error) {
        console.error('正则表达式提取失败:', error);
        return null;
    }
};
/**
 * 应用模板
 */
const applyTemplate = (match, template) => {
    if (!template || template === '$1$') {
        return match[1] || match[0];
    }
    let result = template;
    // 替换 $1$, $2$, ... 为匹配组
    for (let i = 0; i < match.length; i++) {
        result = result.replace(new RegExp(`\\$${i}\\$`, 'g'), match[i] || '');
    }
    return result;
};
/**
 * JSON提取器
 */
export const extractByJsonPath = (data, jsonPath, defaultValue = '') => {
    if (!jsonPath || !data) {
        return defaultValue;
    }
    try {
        // 简化版JSONPath实现
        if (jsonPath.startsWith('$.')) {
            const path = jsonPath.substring(2);
            const keys = path.split('.');
            let value = data;
            for (const key of keys) {
                if (value === null || value === undefined) {
                    return defaultValue;
                }
                // 支持数组索引 [0]
                if (key.includes('[') && key.includes(']')) {
                    const arrayKey = key.substring(0, key.indexOf('['));
                    const index = parseInt(key.substring(key.indexOf('[') + 1, key.indexOf(']')));
                    if (arrayKey) {
                        value = value[arrayKey];
                    }
                    if (Array.isArray(value)) {
                        value = value[index];
                    } else {
                        return defaultValue;
                    }
                } else {
                    value = value[key];
                }
            }
            return value !== null && value !== undefined ? String(value) : defaultValue;
        }
        if (jsonPath === '$') {
            return typeof data === 'string' ? data : JSON.stringify(data);
        }
        return defaultValue;
    } catch (error) {
        console.error('JSONPath提取失败:', error);
        return defaultValue;
    }
};
/**
 * 从响应中提取变量
 */
export const extractVariables = (extractorConfig, response, request) => {
    const { extractType, variableName, extractPath, template, matchNumber, defaultValue } = extractorConfig;
    if (!variableName) {
        return {};
    }
    let extractedValue = null;
    switch (extractType) {
        case 'regex':
            const source = typeof response?.data === 'string'
                ? response.data
                : JSON.stringify(response?.data || {});
            extractedValue = extractByRegex(source, extractPath, template || '$1$', matchNumber || 1);
            break;
        case 'json_path':
            extractedValue = extractByJsonPath(response?.data || response, extractPath, defaultValue || '');
            break;
        default:
            extractedValue = null;
    }
    if (extractedValue === null || extractedValue === undefined) {
        return {};
    }
    return {
        [variableName]: String(extractedValue),
    };
};
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasContainerController.java
@@ -82,8 +82,10 @@
        basContainer.setCreateTime(new Date());
        basContainer.setUpdateBy(getLoginUserId());
        basContainer.setUpdateTime(new Date());
        BasContainer container = basContainerService.getOne(new LambdaQueryWrapper<BasContainer>().eq(BasContainer::getContainerType, basContainer.getContainerType()));
        if (null != container) {
        long count = basContainerService.count(new LambdaQueryWrapper<BasContainer>()
                .eq(BasContainer::getContainerType, basContainer.getContainerType())
                .eq(BasContainer::getDeleted, 0));
        if (count > 0) {
            return R.error("该类型已被初始化");
        }
        if (null !=basContainer.getAreaIds()){
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/RcsTestController.java
New file
@@ -0,0 +1,199 @@
package com.vincent.rsf.server.manager.controller;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.vincent.rsf.server.manager.controller.params.RcsTestParams;
import com.vincent.rsf.server.manager.entity.RcsTestConfig;
import com.vincent.rsf.server.manager.entity.RcsTestPlan;
import com.vincent.rsf.server.manager.service.RcsTestService;
import com.vincent.rsf.server.manager.service.RcsTestPlanService;
import com.vincent.rsf.server.system.controller.BaseController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.Objects;
@RestController
@RequestMapping("/rcs/test")
@Api(tags = "RCS自动测试")
public class RcsTestController extends BaseController {
    @Autowired
    private RcsTestService rcsTestService;
    @Autowired
    private RcsTestPlanService rcsTestPlanService;
    /**
     * 执行RCS全流程自动测试
     */
    @ApiOperation("执行RCS全流程自动测试")
    @OperationLog("执行RCS全流程自动测试")
    @PostMapping("/execute")
    public R executeTest(@RequestBody RcsTestParams params) {
        if (Objects.isNull(params)) {
            return R.error("参数不能为空!!");
        }
        // 出库测试不需要物料编号,只有入库测试或全流程测试才需要物料编号
        // 如果 autoOutbound 为 true 且没有物料编号,说明是纯出库测试,允许通过
        if ((params.getMatnrCodes() == null || params.getMatnrCodes().isEmpty())
                && (params.getAutoOutbound() == null || !params.getAutoOutbound())) {
            return R.error("物料编号组不能为空!!");
        }
        Long userId = getLoginUserId();
        if (userId == null) {
            userId = 1L; // 默认用户ID
        }
        return rcsTestService.executeRcsTest(params, userId);
    }
    /**
     * 保存测试配置
     */
    @ApiOperation("保存测试配置")
    @OperationLog("保存测试配置")
    @PostMapping("/config/save")
    public R saveConfig(@RequestBody RcsTestConfig config) {
        if (Objects.isNull(config)) {
            return R.error("参数不能为空!!");
        }
        Long userId = getLoginUserId();
        if (userId == null) {
            userId = 1L; // 默认用户ID
        }
        if (config.getId() == null) {
            config.setCreateBy(userId);
        } else {
            config.setUpdateBy(userId);
        }
        return rcsTestService.saveOrUpdate(config) ? R.ok() : R.error("保存失败!");
    }
    /**
     * 查询测试配置列表
     */
    @ApiOperation("查询测试配置列表")
    @PostMapping("/config/list")
    public R listConfig(@RequestBody Map<String, Object> params) {
        return R.ok().add(rcsTestService.list());
    }
    /**
     * 删除测试配置
     */
    @ApiOperation("删除测试配置")
    @OperationLog("删除测试配置")
    @PostMapping("/config/delete/{id}")
    public R deleteConfig(@PathVariable Long id) {
        return rcsTestService.removeById(id) ? R.ok() : R.error("删除失败!");
    }
    /**
     * 保存测试计划(JMeter风格)
     */
    @ApiOperation("保存测试计划")
    @OperationLog("保存测试计划")
    @PostMapping("/plan/save")
    public R savePlan(@RequestBody RcsTestPlan plan) {
        if (Objects.isNull(plan)) {
            return R.error("参数不能为空!!");
        }
        if (Objects.isNull(plan.getPlanName()) || plan.getPlanName().trim().isEmpty()) {
            return R.error("测试计划名称不能为空!!");
        }
        if (Objects.isNull(plan.getPlanData()) || plan.getPlanData().trim().isEmpty()) {
            return R.error("测试计划数据不能为空!!");
        }
        Long userId = getLoginUserId();
        if (userId == null) {
            userId = 1L; // 默认用户ID
        }
        Long tenantId = getTenantId();
        if (plan.getId() == null) {
            plan.setCreateBy(userId);
            plan.setTenantId(tenantId);
        } else {
            plan.setUpdateBy(userId);
        }
        boolean result = rcsTestPlanService.saveOrUpdate(plan);
        if (result) {
            return R.ok("保存成功!").add(plan);
        } else {
            return R.error("保存失败!");
        }
    }
    /**
     * 查询测试计划列表
     */
    @ApiOperation("查询测试计划列表")
    @PostMapping("/plan/list")
    public R listPlan(@RequestBody Map<String, Object> params) {
        return R.ok().add(rcsTestPlanService.list());
    }
    /**
     * 根据ID查询测试计划详情
     */
    @ApiOperation("查询测试计划详情")
    @GetMapping("/plan/{id}")
    public R getPlan(@PathVariable Long id) {
        RcsTestPlan plan = rcsTestPlanService.getById(id);
        if (plan == null) {
            return R.error("测试计划不存在!");
        }
        return R.ok().add(plan);
    }
    /**
     * 删除测试计划
     */
    @ApiOperation("删除测试计划")
    @OperationLog("删除测试计划")
    @PostMapping("/plan/delete/{id}")
    public R deletePlan(@PathVariable Long id) {
        return rcsTestPlanService.removeById(id) ? R.ok("删除成功!") : R.error("删除失败!");
    }
    /**
     * 复制测试计划
     */
    @ApiOperation("复制测试计划")
    @OperationLog("复制测试计划")
    @PostMapping("/plan/copy/{id}")
    public R copyPlan(@PathVariable Long id) {
        RcsTestPlan sourcePlan = rcsTestPlanService.getById(id);
        if (sourcePlan == null) {
            return R.error("源测试计划不存在!");
        }
        RcsTestPlan newPlan = new RcsTestPlan();
        newPlan.setPlanName(sourcePlan.getPlanName() + "_副本");
        newPlan.setPlanDescription(sourcePlan.getPlanDescription());
        newPlan.setPlanData(sourcePlan.getPlanData());
        newPlan.setVersion(sourcePlan.getVersion());
        newPlan.setStatus(1);
        Long userId = getLoginUserId();
        if (userId == null) {
            userId = 1L;
        }
        Long tenantId = getTenantId();
        newPlan.setCreateBy(userId);
        newPlan.setTenantId(tenantId);
        boolean result = rcsTestPlanService.save(newPlan);
        if (result) {
            return R.ok("复制成功!").add(newPlan);
        } else {
            return R.error("复制失败!");
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/RcsTestParams.java
New file
@@ -0,0 +1,51 @@
package com.vincent.rsf.server.manager.controller.params;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.List;
@Data
@Accessors(chain = true)
@ApiModel(value = "RcsTestParams", description = "RCS自动测试参数")
public class RcsTestParams implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "物料编号组", required = true)
    private List<String> matnrCodes;
    @ApiModelProperty("入库站点(不填则使用配置中的站点)")
    private String inboundStation;
    @ApiModelProperty("出库站点(不填则使用配置中的站点)")
    private String outboundStation;
    @ApiModelProperty("入库库位号数组(可选,多选时随机选择一个)")
    private List<String> inboundLocNos;
    @ApiModelProperty("出库库位号数组(可选,多选时随机选择单个或多个组合出库)")
    private List<String> outboundLocNos;
    @ApiModelProperty("库位号(可选,已废弃,使用inboundLocNos)")
    @Deprecated
    private String locNo;
    @ApiModelProperty("是否检查库存(true:检查 false:不检查,默认true)")
    private Boolean checkStock = true;
    @ApiModelProperty("入库接口类型(create_in_task/location_allocate,默认create_in_task)")
    private String inboundApiType = "create_in_task";
    @ApiModelProperty("随机物料数量(默认1)")
    private Integer randomMaterialCount = 1;
    @ApiModelProperty("是否自动出库(true:是 false:否,默认true)")
    private Boolean autoOutbound = true;
    @ApiModelProperty("配置ID(如果提供,将使用配置中的参数)")
    private Long configId;
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/RcsTestConfig.java
New file
@@ -0,0 +1,86 @@
package com.vincent.rsf.server.manager.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("rcs_test_config")
@ApiModel(value = "RcsTestConfig", description = "RCS自动测试配置")
public class RcsTestConfig implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty("主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("配置名称")
    private String configName;
    @ApiModelProperty("物料编号组(JSON数组格式)")
    private String matnrCodes;
    @ApiModelProperty("入库站点")
    private String inboundStation;
    @ApiModelProperty("出库站点")
    private String outboundStation;
    @ApiModelProperty("入库库位号数组(JSON数组格式)")
    private String inboundLocNos;
    @ApiModelProperty("出库库位号数组(JSON数组格式)")
    private String outboundLocNos;
    @ApiModelProperty("库位号(已废弃,使用inboundLocNos)")
    @Deprecated
    private String locNo;
    @ApiModelProperty("是否检查库存(1:是 0:否)")
    private Integer checkStock;
    @ApiModelProperty("入库接口类型(create_in_task/location_allocate)")
    private String inboundApiType;
    @ApiModelProperty("随机物料数量")
    private Integer randomMaterialCount;
    @ApiModelProperty("是否自动出库(1:是 0:否)")
    private Integer autoOutbound;
    @ApiModelProperty("状态(1:启用 0:禁用)")
    private Integer status;
    @ApiModelProperty("租户ID")
    private Integer tenantId;
    @ApiModelProperty("创建人")
    private Long createBy;
    @ApiModelProperty("创建时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    @ApiModelProperty("更新人")
    private Long updateBy;
    @ApiModelProperty("更新时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
    @ApiModelProperty("备注")
    private String memo;
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/RcsTestPlan.java
New file
@@ -0,0 +1,64 @@
package com.vincent.rsf.server.manager.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("rcs_test_plan")
@ApiModel(value = "RcsTestPlan", description = "RCS测试计划(JMeter风格)")
public class RcsTestPlan implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty("主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("测试计划名称")
    private String planName;
    @ApiModelProperty("测试计划描述")
    private String planDescription;
    @ApiModelProperty("测试计划数据(JSON格式,包含完整的树形结构)")
    private String planData;
    @ApiModelProperty("版本号")
    private String version;
    @ApiModelProperty("状态(1:启用 0:禁用)")
    private Integer status;
    @ApiModelProperty("租户ID")
    private Long tenantId;
    @ApiModelProperty("创建人")
    private Long createBy;
    @ApiModelProperty("创建时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    @ApiModelProperty("更新人")
    private Long updateBy;
    @ApiModelProperty("更新时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
    @ApiModelProperty("备注")
    private String memo;
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/RcsTestConfigMapper.java
New file
@@ -0,0 +1,9 @@
package com.vincent.rsf.server.manager.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.manager.entity.RcsTestConfig;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RcsTestConfigMapper extends BaseMapper<RcsTestConfig> {
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/RcsTestPlanMapper.java
New file
@@ -0,0 +1,9 @@
package com.vincent.rsf.server.manager.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.manager.entity.RcsTestPlan;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RcsTestPlanMapper extends BaseMapper<RcsTestPlan> {
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/PakinSchedules.java
@@ -65,7 +65,13 @@
            if (pakinItems.isEmpty()) {
                throw new CoolException("组拖明细为空!!");
            }
            List<String> pkinItems = pakinItems.stream().map(WaitPakinItem::getAsnCode).collect(Collectors.toList());
            // 过滤掉asnCode为null或空字符串的情况(无ASN单号的组托明细不需要处理单据状态)
            List<String> pkinItems = pakinItems.stream()
                    .map(WaitPakinItem::getAsnCode)
                    .filter(Objects::nonNull)
                    .filter(code -> !code.trim().isEmpty())
                    .distinct()
                    .collect(Collectors.toList());
            pkinItems.forEach(item -> {
                List<WkOrderItem> wkOrders = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderCode, item));
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/RcsTestPlanService.java
New file
@@ -0,0 +1,7 @@
package com.vincent.rsf.server.manager.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.manager.entity.RcsTestPlan;
public interface RcsTestPlanService extends IService<RcsTestPlan> {
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/RcsTestService.java
New file
@@ -0,0 +1,17 @@
package com.vincent.rsf.server.manager.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.manager.controller.params.RcsTestParams;
import com.vincent.rsf.server.manager.entity.RcsTestConfig;
public interface RcsTestService extends IService<RcsTestConfig> {
    /**
     * 执行RCS全流程自动测试
     * @param params 测试参数
     * @param userId 用户ID
     * @return 测试结果
     */
    R executeRcsTest(RcsTestParams params, Long userId);
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestPlanServiceImpl.java
New file
@@ -0,0 +1,11 @@
package com.vincent.rsf.server.manager.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.manager.entity.RcsTestPlan;
import com.vincent.rsf.server.manager.mapper.RcsTestPlanMapper;
import com.vincent.rsf.server.manager.service.RcsTestPlanService;
import org.springframework.stereotype.Service;
@Service("rcsTestPlanService")
public class RcsTestPlanServiceImpl extends ServiceImpl<RcsTestPlanMapper, RcsTestPlan> implements RcsTestPlanService {
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestServiceImpl.java
New file
@@ -0,0 +1,525 @@
package com.vincent.rsf.server.manager.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.api.controller.erp.params.TaskInParam;
import com.vincent.rsf.server.api.entity.dto.InTaskMsgDto;
import com.vincent.rsf.server.api.service.WcsService;
import com.vincent.rsf.server.manager.controller.params.PakinItem;
import com.vincent.rsf.server.manager.controller.params.RcsTestParams;
import com.vincent.rsf.server.manager.controller.params.WaitPakinParam;
import com.vincent.rsf.server.manager.controller.params.LocToTaskParams;
import com.vincent.rsf.server.manager.entity.*;
import com.vincent.rsf.server.manager.enums.*;
import com.vincent.rsf.server.manager.mapper.RcsTestConfigMapper;
import com.vincent.rsf.server.manager.service.*;
import com.vincent.rsf.server.common.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.Random;
@Slf4j
@Service
public class RcsTestServiceImpl extends ServiceImpl<RcsTestConfigMapper, RcsTestConfig> implements RcsTestService {
    @Autowired
    private MatnrService matnrService;
    @Autowired
    private WaitPakinService waitPakinService;
    @Autowired
    private WcsService wcsService;
    @Autowired
    private TaskService taskService;
    @Autowired
    private LocService locService;
    @Autowired
    private LocItemService locItemService;
    @Autowired
    private DeviceSiteService deviceSiteService;
    @Autowired
    private BasStationService basStationService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R executeRcsTest(RcsTestParams params, Long userId) {
        log.info("========== 开始执行RCS全流程自动测试 ==========");
        Map<String, Object> result = new HashMap<>();
        List<String> steps = new ArrayList<>();
        try {
            // 1. 加载配置(如果提供了configId)
            RcsTestConfig config = null;
            if (params.getConfigId() != null) {
                config = this.getById(params.getConfigId());
                if (config == null) {
                    throw new CoolException("配置不存在,配置ID:" + params.getConfigId());
                }
                // 使用配置中的参数覆盖请求参数
                if (StringUtils.isBlank(params.getInboundStation()) && StringUtils.isNotBlank(config.getInboundStation())) {
                    params.setInboundStation(config.getInboundStation());
                }
                if (StringUtils.isBlank(params.getOutboundStation()) && StringUtils.isNotBlank(config.getOutboundStation())) {
                    params.setOutboundStation(config.getOutboundStation());
                }
                // 加载入库库位号数组
                if ((params.getInboundLocNos() == null || params.getInboundLocNos().isEmpty())
                        && StringUtils.isNotBlank(config.getInboundLocNos())) {
                    try {
                        params.setInboundLocNos(JSON.parseArray(config.getInboundLocNos(), String.class));
                    } catch (Exception e) {
                        log.warn("解析入库库位号数组失败: {}", e.getMessage());
                    }
                }
                // 兼容旧数据:如果inboundLocNos为空但locNo有值,使用locNo
                if ((params.getInboundLocNos() == null || params.getInboundLocNos().isEmpty())
                        && StringUtils.isNotBlank(config.getLocNo())) {
                    params.setInboundLocNos(Collections.singletonList(config.getLocNo()));
                }
                // 加载出库库位号数组
                if ((params.getOutboundLocNos() == null || params.getOutboundLocNos().isEmpty())
                        && StringUtils.isNotBlank(config.getOutboundLocNos())) {
                    try {
                        params.setOutboundLocNos(JSON.parseArray(config.getOutboundLocNos(), String.class));
                    } catch (Exception e) {
                        log.warn("解析出库库位号数组失败: {}", e.getMessage());
                    }
                }
                if (params.getCheckStock() == null && config.getCheckStock() != null) {
                    params.setCheckStock(config.getCheckStock() == 1);
                }
                if (StringUtils.isBlank(params.getInboundApiType()) && StringUtils.isNotBlank(config.getInboundApiType())) {
                    params.setInboundApiType(config.getInboundApiType());
                }
                if (params.getRandomMaterialCount() == null && config.getRandomMaterialCount() != null) {
                    params.setRandomMaterialCount(config.getRandomMaterialCount());
                }
                if (params.getAutoOutbound() == null && config.getAutoOutbound() != null) {
                    params.setAutoOutbound(config.getAutoOutbound() == 1);
                }
                if (params.getMatnrCodes() == null || params.getMatnrCodes().isEmpty()) {
                    if (StringUtils.isNotBlank(config.getMatnrCodes())) {
                        params.setMatnrCodes(JSON.parseArray(config.getMatnrCodes(), String.class));
                    }
                }
            }
            // 2. 验证参数
            // 出库测试不需要物料编号,只有入库测试或全流程测试才需要物料编号
            // 如果 autoOutbound 为 true 且没有物料编号,说明是纯出库测试,跳过物料验证
            boolean isPureOutboundTest = Boolean.TRUE.equals(params.getAutoOutbound())
                    && (params.getMatnrCodes() == null || params.getMatnrCodes().isEmpty());
            if (!isPureOutboundTest && (params.getMatnrCodes() == null || params.getMatnrCodes().isEmpty())) {
                throw new CoolException("物料编号组不能为空!");
            }
            // 3. 处理入库流程(仅当非纯出库测试时)
            String barcode = null;
            String locNo = null;
            String taskCode = null;
            String requestedLocNo = null;
            WaitPakin waitPakin = null;
            if (!isPureOutboundTest) {
                // 3.1 随机选择物料
                List<String> selectedMatnrCodes = selectRandomMaterials(params.getMatnrCodes(), params.getRandomMaterialCount());
                steps.add("随机选择物料:" + selectedMatnrCodes);
                log.info("随机选择的物料:{}", selectedMatnrCodes);
                // 3.2 查询物料信息
                List<Matnr> matnrs = matnrService.list(new LambdaQueryWrapper<Matnr>()
                        .in(Matnr::getCode, selectedMatnrCodes));
                if (matnrs.isEmpty()) {
                    throw new CoolException("未找到物料信息!");
                }
                steps.add("查询到物料数量:" + matnrs.size());
                // 3.3 生成随机托盘码
                barcode = generateBarcode();
                steps.add("生成托盘码:" + barcode);
                log.info("生成的托盘码:{}", barcode);
                // 3.4 自动组托
                WaitPakinParam waitPakinParam = new WaitPakinParam();
                waitPakinParam.setBarcode(barcode);
                List<PakinItem> pakinItems = new ArrayList<>();
                for (Matnr matnr : matnrs) {
                    PakinItem item = new PakinItem();
                    item.setMatnrId(matnr.getId());
                    item.setReceiptQty(10.0 + Math.random() * 90); // 随机数量10-100
                    pakinItems.add(item);
                }
                waitPakinParam.setItems(pakinItems);
                waitPakin = waitPakinService.mergeItems(waitPakinParam, userId);
                if (waitPakin == null) {
                    throw new CoolException("组托失败,返回结果为空!");
                }
                steps.add("组托成功,组托编码:" + waitPakin.getCode());
                log.info("组托成功,组托编码:{},托盘码:{}", waitPakin.getCode(), barcode);
                // 3.5 自动入库
                String inboundStation = params.getInboundStation();
                if (StringUtils.isBlank(inboundStation)) {
                    // 获取默认入库站点
                    List<DeviceSite> deviceSites = deviceSiteService.list(new LambdaQueryWrapper<DeviceSite>()
                            .eq(DeviceSite::getType, TaskType.TASK_TYPE_IN.type)
                            .last("limit 1"));
                    if (deviceSites.isEmpty()) {
                        throw new CoolException("未找到入库站点!");
                    }
                    inboundStation = deviceSites.get(0).getSite();
                }
                // 处理入库库位号:如果指定了多个,随机选择一个
                List<String> inboundLocNos = params.getInboundLocNos();
                if (inboundLocNos != null && !inboundLocNos.isEmpty()) {
                    // 验证库位是否存在
                    List<String> validLocNos = new ArrayList<>();
                    for (String locCode : inboundLocNos) {
                        Loc loc = locService.getOne(new LambdaQueryWrapper<Loc>()
                                .eq(Loc::getCode, locCode)
                                .eq(Loc::getDeleted, 0));
                        if (loc != null) {
                            validLocNos.add(locCode);
                        } else {
                            steps.add("警告:指定的入库库位号不存在:" + locCode);
                        }
                    }
                    if (!validLocNos.isEmpty()) {
                        // 随机选择一个库位
                        Collections.shuffle(validLocNos);
                        requestedLocNo = validLocNos.get(0);
                        if (validLocNos.size() > 1) {
                            steps.add("从" + validLocNos.size() + "个入库库位中随机选择:" + requestedLocNo);
                        } else {
                            steps.add("使用指定的入库库位号:" + requestedLocNo);
                        }
                    }
                }
                // 兼容旧参数:如果inboundLocNos为空但locNo有值
                if (requestedLocNo == null && StringUtils.isNotBlank(params.getLocNo())) {
                    requestedLocNo = params.getLocNo();
                }
                if ("location_allocate".equals(params.getInboundApiType())) {
                    // 使用 location_allocate 接口(内部调用createInTask)
                    R allocateResult = wcsService.allocateLocation(barcode, inboundStation, 1);
                    if (allocateResult != null) {
                        Object dataObj = allocateResult.get("data");
                        if (dataObj != null) {
                            if (dataObj instanceof JSONObject) {
                                JSONObject data = (JSONObject) dataObj;
                                locNo = data.getString("locNo");
                            } else if (dataObj instanceof Map) {
                                @SuppressWarnings("unchecked")
                                Map<String, Object> data = (Map<String, Object>) dataObj;
                                locNo = (String) data.get("locNo");
                            }
                        }
                    }
                    if (StringUtils.isNotBlank(requestedLocNo) && !requestedLocNo.equals(locNo)) {
                        steps.add("使用location_allocate接口申请入库,系统分配库位号:" + locNo + "(用户指定:" + requestedLocNo + ")");
                    } else {
                        steps.add("使用location_allocate接口申请入库,库位号:" + (locNo != null ? locNo : "未分配"));
                    }
                } else {
                    // 使用 create_in_task 接口
                    TaskInParam taskInParam = new TaskInParam();
                    taskInParam.setBarcode(barcode);
                    taskInParam.setSourceStaNo(inboundStation);
                    taskInParam.setIoType(TaskType.TASK_TYPE_IN.type);
                    taskInParam.setLocType1(1);
                    taskInParam.setUser(userId);
                    InTaskMsgDto inTaskMsgDto = wcsService.createInTask(taskInParam);
                    locNo = inTaskMsgDto.getLocNo();
                    taskCode = inTaskMsgDto.getWorkNo();
                    if (StringUtils.isNotBlank(requestedLocNo) && locNo != null && !requestedLocNo.equals(locNo)) {
                        steps.add("使用create_in_task接口创建入库任务,系统分配库位号:" + locNo + "(用户指定:" + requestedLocNo + "),任务编码:" + taskCode);
                    } else {
                        steps.add("使用create_in_task接口创建入库任务,库位号:" + (locNo != null ? locNo : "未分配") + ",任务编码:" + taskCode);
                    }
                }
            } else {
                steps.add("纯出库测试,跳过物料选择、组托和入库流程");
            }
            result.put("barcode", barcode);
            result.put("locNo", locNo);
            result.put("requestedLocNo", requestedLocNo); // 记录用户指定的库位号
            if (waitPakin != null) {
                result.put("waitPakinCode", waitPakin.getCode());
            }
            if (taskCode != null) {
                result.put("inboundTaskCode", taskCode);
            }
            // 8. 自动出库(如果配置了)
            if (params.getAutoOutbound() != null && params.getAutoOutbound()) {
                String outboundStation = params.getOutboundStation();
                if (StringUtils.isBlank(outboundStation)) {
                    // 获取默认出库站点
                    List<DeviceSite> deviceSites = deviceSiteService.list(new LambdaQueryWrapper<DeviceSite>()
                            .eq(DeviceSite::getType, TaskType.TASK_TYPE_OUT.type)
                            .last("limit 1"));
                    if (!deviceSites.isEmpty()) {
                        outboundStation = deviceSites.get(0).getSite();
                    }
                }
                if (StringUtils.isNotBlank(outboundStation)) {
                    List<String> outboundLocNos = params.getOutboundLocNos();
                    List<Loc> selectedOutboundLocs = new ArrayList<>();
                    if (outboundLocNos != null && !outboundLocNos.isEmpty()) {
                        // 使用用户指定的出库库位号
                        for (String locCode : outboundLocNos) {
                            Loc loc = locService.getOne(new LambdaQueryWrapper<Loc>()
                                    .eq(Loc::getCode, locCode)
                                    .eq(Loc::getDeleted, 0));
                            if (loc != null) {
                                // 如果检查库存,验证是否有库存
                                if (params.getCheckStock() != null && params.getCheckStock()) {
                                    List<LocItem> locItems = locItemService.list(new LambdaQueryWrapper<LocItem>()
                                            .eq(LocItem::getLocId, loc.getId()));
                                    if (!locItems.isEmpty()) {
                                        selectedOutboundLocs.add(loc);
                                    }
                                } else {
                                    // 不检查库存,直接添加
                                    selectedOutboundLocs.add(loc);
                                }
                            }
                        }
                        if (selectedOutboundLocs.isEmpty()) {
                            steps.add("指定的出库库位号均无库存或不存在,跳过自动出库");
                            log.warn("指定的出库库位号均无库存或不存在,跳过自动出库");
                        }
                    } else {
                        // 没有指定出库库位号,查找有库存的库位
                        if (params.getCheckStock() != null && params.getCheckStock()) {
                            // 检查库存
                            List<Loc> locsWithStock = locService.list(new LambdaQueryWrapper<Loc>()
                                    .eq(Loc::getUseStatus, LocStsType.LOC_STS_TYPE_F.type)
                                    .last("limit 10"));
                            for (Loc loc : locsWithStock) {
                                List<LocItem> locItems = locItemService.list(new LambdaQueryWrapper<LocItem>()
                                        .eq(LocItem::getLocId, loc.getId()));
                                if (!locItems.isEmpty()) {
                                    selectedOutboundLocs.add(loc);
                                }
                            }
                            if (selectedOutboundLocs.isEmpty()) {
                                steps.add("未找到有库存的库位,跳过自动出库");
                                log.warn("未找到有库存的库位,跳过自动出库");
                            }
                        } else {
                            // 不检查库存,随机选择一个库位(后续会使用固定模拟物料000267)
                            List<Loc> locs = locService.list(new LambdaQueryWrapper<Loc>()
                                    .eq(Loc::getUseStatus, LocStsType.LOC_STS_TYPE_F.type)
                                    .last("limit 10"));
                            if (!locs.isEmpty()) {
                                Collections.shuffle(locs);
                                selectedOutboundLocs.add(locs.get(0));
                                steps.add("不检查库存,随机选择库位:" + locs.get(0).getCode() + ",将使用固定模拟物料000267");
                            }
                        }
                    }
                    // 处理出库:随机选择单个或多个库位组合
                    if (!selectedOutboundLocs.isEmpty()) {
                        Random random = new Random();
                        // 随机决定:单库位出库 或 多库位组合出库(最多3个库位)
                        boolean useMultipleLocs = random.nextBoolean() && selectedOutboundLocs.size() > 1;
                        int locCount = useMultipleLocs
                                ? Math.min(random.nextInt(selectedOutboundLocs.size()) + 1, 3)
                                : 1;
                        // 随机选择库位
                        Collections.shuffle(selectedOutboundLocs);
                        List<Loc> finalOutboundLocs = selectedOutboundLocs.subList(0, Math.min(locCount, selectedOutboundLocs.size()));
                        if (finalOutboundLocs.size() == 1) {
                            steps.add("随机选择单库位出库:" + finalOutboundLocs.get(0).getCode());
                        } else {
                            steps.add("随机选择" + finalOutboundLocs.size() + "个库位组合出库:" +
                                    finalOutboundLocs.stream().map(Loc::getCode).collect(java.util.stream.Collectors.joining(", ")));
                        }
                        // 为每个库位生成出库任务
                        List<String> outboundTaskCodes = new ArrayList<>();
                        List<String> matnrCodes = params.getMatnrCodes();
                        boolean hasMatnrFilter = matnrCodes != null && !matnrCodes.isEmpty();
                        boolean checkStockFlag = params.getCheckStock() != null && params.getCheckStock();
                        for (Loc outboundLoc : finalOutboundLocs) {
                            try {
                                LocToTaskParams taskParams = new LocToTaskParams();
                                List<LocItem> locItems;
                                if (hasMatnrFilter && checkStockFlag) {
                                    // 如果指定了物料组且检查库存,只查找包含这些物料的库存
                                    List<String> matnrIds = new ArrayList<>();
                                    for (String matnrCode : matnrCodes) {
                                        Matnr matnr = matnrService.getOne(new LambdaQueryWrapper<Matnr>()
                                                .eq(Matnr::getCode, matnrCode)
                                                .eq(Matnr::getDeleted, 0)
                                                .last("limit 1"));
                                        if (matnr != null) {
                                            matnrIds.add(String.valueOf(matnr.getId()));
                                        }
                                    }
                                    if (!matnrIds.isEmpty()) {
                                        // 将String列表转换为Long列表
                                        List<Long> matnrIdLongs = new ArrayList<>();
                                        for (String matnrIdStr : matnrIds) {
                                            try {
                                                matnrIdLongs.add(Long.parseLong(matnrIdStr));
                                            } catch (NumberFormatException e) {
                                                log.warn("物料ID格式错误: {}", matnrIdStr);
                                            }
                                        }
                                        if (!matnrIdLongs.isEmpty()) {
                                            locItems = locItemService.list(new LambdaQueryWrapper<LocItem>()
                                                    .eq(LocItem::getLocId, outboundLoc.getId())
                                                    .in(LocItem::getMatnrId, matnrIdLongs)
                                                    .last("limit 1"));
                                        } else {
                                            locItems = new ArrayList<>();
                                        }
                                    } else {
                                        locItems = new ArrayList<>();
                                    }
                                } else if (!checkStockFlag) {
                                    // 不检查库存,使用固定模拟物料000267
                                    Matnr fixedMatnr = matnrService.getOne(new LambdaQueryWrapper<Matnr>()
                                            .eq(Matnr::getCode, "000267")
                                            .eq(Matnr::getDeleted, 0)
                                            .last("limit 1"));
                                    if (fixedMatnr != null) {
                                        // 创建一个模拟的LocItem用于生成任务
                                        locItems = new ArrayList<>();
                                        LocItem mockLocItem = new LocItem();
                                        mockLocItem.setLocId(outboundLoc.getId());
                                        mockLocItem.setMatnrId(fixedMatnr.getId());
                                        mockLocItem.setQty(1.0); // 默认数量
                                        locItems.add(mockLocItem);
                                        steps.add("不检查库存,使用固定模拟物料:000267");
                                    } else {
                                        // 如果找不到000267,尝试从库位中获取任意一个库存项
                                        locItems = locItemService.list(new LambdaQueryWrapper<LocItem>()
                                                .eq(LocItem::getLocId, outboundLoc.getId())
                                                .last("limit 1"));
                                        if (locItems.isEmpty()) {
                                            steps.add("警告:库位 " + outboundLoc.getCode() + " 无库存,且未找到固定模拟物料000267");
                                        }
                                    }
                                } else {
                                    // 检查库存但没有指定物料组,查找任意库存
                                    locItems = locItemService.list(new LambdaQueryWrapper<LocItem>()
                                            .eq(LocItem::getLocId, outboundLoc.getId())
                                            .last("limit 1"));
                                }
                                if (!locItems.isEmpty()) {
                                    taskParams.setItems(locItems);
                                    taskParams.setSiteNo(outboundStation);
                                    taskParams.setType(Constants.TASK_TYPE_OUT_STOCK);
                                    taskParams.setTarLoc(outboundLoc.getCode());
                                    locItemService.generateTask(TaskResouceType.TASK_RESOUCE_ORDER_TYPE.val, taskParams, userId);
                                    steps.add("生成出库任务成功,源库位:" + outboundLoc.getCode() + ",目标站点:" + outboundStation);
                                    log.info("生成出库任务成功,源库位:{},目标站点:{}", outboundLoc.getCode(), outboundStation);
                                    // 查询刚生成的任务并下发到RCS
                                    List<Task> outTasks = taskService.list(new LambdaQueryWrapper<Task>()
                                            .eq(Task::getOrgLoc, outboundLoc.getCode())
                                            .eq(Task::getTargSite, outboundStation)
                                            .eq(Task::getTaskStatus, TaskStsType.GENERATE_OUT.id)
                                            .orderByDesc(Task::getCreateTime)
                                            .last("limit 1"));
                                    if (!outTasks.isEmpty()) {
                                        taskService.pubTaskToWcs(outTasks);
                                        outboundTaskCodes.add(outTasks.get(0).getTaskCode());
                                        steps.add("出库任务已下发到RCS,任务编码:" + outTasks.get(0).getTaskCode());
                                    }
                                } else {
                                    steps.add("库位 " + outboundLoc.getCode() + " 无符合条件的库存,跳过生成出库任务");
                                }
                            } catch (Exception e) {
                                log.error("生成出库任务失败,库位:{}", outboundLoc.getCode(), e);
                                steps.add("生成出库任务失败,库位:" + outboundLoc.getCode() + ",错误:" + e.getMessage());
                            }
                        }
                        if (!outboundTaskCodes.isEmpty()) {
                            result.put("outboundTaskCodes", outboundTaskCodes);
                            result.put("outboundTaskCode", outboundTaskCodes.get(0)); // 兼容旧字段
                        }
                    }
                }
            }
            result.put("steps", steps);
            result.put("success", true);
            log.info("========== RCS全流程自动测试完成 ==========");
            return R.ok(result);
        } catch (Exception e) {
            log.error("========== RCS全流程自动测试失败 ==========", e);
            steps.add("测试失败:" + e.getMessage());
            result.put("steps", steps);
            result.put("success", false);
            result.put("error", e.getMessage());
            return R.error("测试失败:" + e.getMessage()).add(result);
        }
    }
    /**
     * 随机选择物料
     */
    private List<String> selectRandomMaterials(List<String> matnrCodes, Integer count) {
        if (matnrCodes == null || matnrCodes.isEmpty()) {
            return new ArrayList<>();
        }
        List<String> shuffled = new ArrayList<>(matnrCodes);
        Collections.shuffle(shuffled);
        int selectCount = Math.min(count != null ? count : 1, shuffled.size());
        return shuffled.subList(0, selectCount);
    }
    /**
     * 生成随机托盘码
     */
    private String generateBarcode() {
        return "TEST_" + System.currentTimeMillis() + "_" + (int)(Math.random() * 10000);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
@@ -1889,8 +1889,10 @@
            List<TaskItem> items = orderMap.get(key);
            //保存入出库明细
            saveStockItems(items, task, pakinItem.getId(), pakinItem.getAsnCode(), pakinItem.getWkType(), pakinItem.getType(), loginUserId);
            //移出收货区库存, 修改组托状态
            //移出收货区库存, 修改组托状态(只有当source不为null时才需要移除收货区库存)
            if (Objects.nonNull(pakinItem.getSource())) {
            removeReceiptStock(pakinItem, loginUserId);
            }
        });
        Set<Long> pkinItemIds = taskItems.stream().map(TaskItem::getSource).collect(Collectors.toSet());
@@ -1926,6 +1928,10 @@
     */
    @Transactional(rollbackFor = Exception.class)
    public synchronized void removeReceiptStock(WaitPakinItem pakinItem, Long loginUserId) {
        // 如果source为null,说明组托明细不在收货区,无需移除收货区库存
        if (Objects.isNull(pakinItem.getSource())) {
            return;
        }
        WarehouseAreasItem itemServiceOne = warehouseAreasItemService.getOne(new LambdaQueryWrapper<WarehouseAreasItem>()
                .eq(WarehouseAreasItem::getId, pakinItem.getSource()));
        if (Objects.isNull(itemServiceOne)) {
@@ -1973,7 +1979,8 @@
                    .eq(LocItem::getMatnrId, taskItem.getMatnrId())
                    .eq(LocItem::getLocId, loc.getId())
                    .eq(StringUtils.isNotBlank(taskItem.getBatch()), LocItem::getBatch, taskItem.getBatch())
                    .eq(StringUtils.isNotBlank(taskItem.getFieldsIndex()), LocItem::getFieldsIndex, taskItem.getFieldsIndex()));
                    .eq(StringUtils.isNotBlank(taskItem.getFieldsIndex()), LocItem::getFieldsIndex, taskItem.getFieldsIndex())
            );
            if (Objects.isNull(locItem)) {
                BeanUtils.copyProperties(taskItem, item);
                item.setLocCode(loc.getCode())
@@ -1986,7 +1993,7 @@
                    throw new CoolException("库位明细更新失败!!");
                }
            } else {
                logger.error("当前票号:"  + locItem.getFieldsIndex()  + " 已在库内,请检查后再操作!!");
//                logger.error("当前票号:"  + locItem.getFieldsIndex()  + " 已在库内,请检查后再操作!!");
//                throw new CoolException("当前票号已在库内,请检查后再操作!!");
//                locItem.setAnfme(Math.round((locItem.getAnfme() + taskItem.getAnfme()) * 1000000) / 1000000.0)
//                        .setUpdateTime(new Date());
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WaitPakinServiceImpl.java
@@ -230,7 +230,7 @@
        if (!this.updateById(waitPakin1)) {
            throw new CoolException("组托数量修改失败!!");
        }
        return pakin;
        return waitPakin1;
    }
    /**