From 9f724c61dfa4dc4c0eea66253ea0780b023622ae Mon Sep 17 00:00:00 2001
From: chen.lin <1442464845@qq.com>
Date: 星期六, 07 二月 2026 09:13:22 +0800
Subject: [PATCH] 测试部分
---
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/RcsTestConfigMapper.java | 9
rsf-admin/src/page/rcsTest/components/ComponentMenuFactory.js | 201 +
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/RcsTestPlanService.java | 7
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WaitPakinServiceImpl.java | 2
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/RcsTestPlanMapper.java | 9
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/PakinSchedules.java | 8
rsf-admin/src/page/rcsTest/components/JmeterComponents.js | 500 ++++
rsf-admin/src/page/rcsTest/index.jsx | 9
rsf-admin/src/page/rcsTest/components/ComponentConfigPanel.jsx | 1114 ++++++++++
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java | 15
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/RcsTestConfig.java | 86
rsf-admin/src/i18n/zh.js | 1
rsf-admin/src/page/ResourceContent.js | 3
rsf-admin/src/page/rcsTest/components/SummaryReport.jsx | 213 +
rsf-admin/src/page/rcsTest/components/ApiSelector.jsx | 186 +
rsf-admin/src/page/rcsTest/components/SelectModal.jsx | 301 ++
rsf-admin/src/page/rcsTest/utils/variableExtractor.js | 149 +
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/RcsTestService.java | 17
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/RcsTestController.java | 199 +
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/RcsTestParams.java | 51
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestPlanServiceImpl.java | 11
rsf-admin/src/page/rcsTest/RcsTestCustomMode.jsx | 1629 +++++++++++++++
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestServiceImpl.java | 525 ++++
rsf-admin/src/page/rcsTest/RcsTestList.jsx | 1074 ++++++++++
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasContainerController.java | 6
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/RcsTestPlan.java | 64
26 files changed, 6,381 insertions(+), 8 deletions(-)
diff --git a/rsf-admin/src/i18n/zh.js b/rsf-admin/src/i18n/zh.js
index 4512550..735cbac 100644
--- a/rsf-admin/src/i18n/zh.js
+++ b/rsf-admin/src/i18n/zh.js
@@ -229,6 +229,7 @@
inStatisticItem: '鏃ュ叆搴撴槑缁嗘煡璇�',
outStatisticItem: '鏃ュ嚭搴撴槑缁嗘煡璇�',
statisticCount: '鏃ュ嚭鍏ュ簱姹囨�荤粺璁�',
+ rcsTest: 'RCS鑷姩娴嬭瘯',
},
table: {
field: {
diff --git a/rsf-admin/src/page/ResourceContent.js b/rsf-admin/src/page/ResourceContent.js
index bc61dd8..b5be33d 100644
--- a/rsf-admin/src/page/ResourceContent.js
+++ b/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,
diff --git a/rsf-admin/src/page/rcsTest/RcsTestCustomMode.jsx b/rsf-admin/src/page/rcsTest/RcsTestCustomMode.jsx
new file mode 100644
index 0000000..877f8bb
--- /dev/null
+++ b/rsf-admin/src/page/rcsTest/RcsTestCustomMode.jsx
@@ -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';
+// 浣跨敤绠�鍗曠殑鍒楄〃缁撴瀯浠f浛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,
+ },
+ },
+};
+
+// 鑷畾涔塗abPanel缁勪欢
+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);
+
+ // 鍙充晶閰嶇疆闈㈡澘鐨凾ab
+ 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}锛孯amp-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('瑙f瀽娴嬭瘯璁″垝鏁版嵁澶辫触', { 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>
+ );
+ }
+
+ // 浠巘estPlan涓幏鍙栨渶鏂扮殑鑺傜偣閰嶇疆锛岀‘淇濇暟鎹悓姝�
+ 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瑙f瀽閿欒
+ }
+ }}
+ />
+ </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;
diff --git a/rsf-admin/src/page/rcsTest/RcsTestList.jsx b/rsf-admin/src/page/rcsTest/RcsTestList.jsx
new file mode 100644
index 0000000..0608637
--- /dev/null
+++ b/rsf-admin/src/page/rcsTest/RcsTestList.jsx
@@ -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) => {
+ // 鍏ュ簱閰嶇疆锛歛utoOutbound === 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) {
+ // 瑙f瀽澶辫触锛屽鏋滄湁鐗╂枡瀛楃涓蹭篃绠楀叆搴撻厤缃�
+ if (config.matnrCodes && config.matnrCodes.length > 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ // 鍒ゆ柇鏄惁涓哄嚭搴撻厤缃�
+ const isOutboundConfig = (config) => {
+ // 鍑哄簱閰嶇疆锛歛utoOutbound === 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) {
+ // 瑙f瀽澶辫触锛屽鏋滄病鏈夌墿鏂欏瓧绗︿覆涔熺畻鍑哄簱閰嶇疆
+ 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' });
+ }
+ };
+
+ // 淇濆瓨閰嶇疆锛堥�氱敤锛屾牴鎹畉estType鍐冲畾锛�
+ 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;
diff --git a/rsf-admin/src/page/rcsTest/components/ApiSelector.jsx b/rsf-admin/src/page/rcsTest/components/ApiSelector.jsx
new file mode 100644
index 0000000..121cabb
--- /dev/null
+++ b/rsf-admin/src/page/rcsTest/components/ApiSelector.jsx
@@ -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';
+
+/**
+ * 鎺ュ彛閫夋嫨鍣ㄧ粍浠�
+ * 鐢ㄤ簬浠庣郴缁熷姞杞藉彲鐢ㄦ帴鍙e垪琛ㄥ苟閫夋嫨
+ */
+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 {
+ // 杩欓噷鍙互浠庣郴缁熷姞杞芥帴鍙e垪琛�
+ // 鏆傛椂浣跨敤棰勫畾涔夊垪琛�
+ 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="杈撳叆瀹屾暣鐨勬帴鍙RL锛屽锛歨ttp://example.com/api/test"
+ value={currentUrl}
+ onChange={(e) => {
+ // 杩欓噷鍙互鏇存柊currentUrl锛屼絾闇�瑕佺埗缁勪欢鏀寔
+ }}
+ helperText="鍙互鐩存帴杈撳叆瀹屾暣鐨勬帴鍙RL"
+ />
+ </Box>
+ </Stack>
+ </DialogContent>
+ <DialogActions>
+ <MuiButton onClick={onClose}>鍙栨秷</MuiButton>
+ <MuiButton onClick={handleManualInput} variant="contained">
+ 浣跨敤鎵嬪姩杈撳叆
+ </MuiButton>
+ </DialogActions>
+ </Dialog>
+ );
+};
+
+export default ApiSelector;
diff --git a/rsf-admin/src/page/rcsTest/components/ComponentConfigPanel.jsx b/rsf-admin/src/page/rcsTest/components/ComponentConfigPanel.jsx
new file mode 100644
index 0000000..8693f01
--- /dev/null
+++ b/rsf-admin/src/page/rcsTest/components/ComponentConfigPanel.jsx
@@ -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="濡傛灉濉啓浜嗗畬鏁碪RL锛屽皢浼樺厛浣跨敤姝RL"
+ 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">鍝嶅簲浠g爜</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">鍖归厤姝e垯琛ㄨ揪寮�</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>
+ );
+
+ // 姝e垯琛ㄨ揪寮忔彁鍙栧櫒閰嶇疆
+ const renderRegexExtractorConfig = () => (
+ <Stack spacing={2}>
+ <TextField
+ label="鍙橀噺鍚�"
+ value={config.variableName || ''}
+ onChange={(e) => handleConfigChange('variableName', e.target.value)}
+ fullWidth
+ />
+ <TextField
+ label="姝e垯琛ㄨ揪寮�"
+ 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">鍝嶅簲浠g爜</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;
diff --git a/rsf-admin/src/page/rcsTest/components/ComponentMenuFactory.js b/rsf-admin/src/page/rcsTest/components/ComponentMenuFactory.js
new file mode 100644
index 0000000..c50c0a9
--- /dev/null
+++ b/rsf-admin/src/page/rcsTest/components/ComponentMenuFactory.js
@@ -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: '濡傛灉锛圛f锛夋帶鍒跺櫒', 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: '姝e垯琛ㄨ揪寮忔彁鍙栧櫒', 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;
+};
diff --git a/rsf-admin/src/page/rcsTest/components/JmeterComponents.js b/rsf-admin/src/page/rcsTest/components/JmeterComponents.js
new file mode 100644
index 0000000..b71dfb5
--- /dev/null
+++ b/rsf-admin/src/page/rcsTest/components/JmeterComponents.js
@@ -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: '姝e垯琛ㄨ揪寮忔彁鍙栧櫒',
+ 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: '濡傛灉锛圛f锛夋帶鍒跺櫒',
+ 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: '馃帥锔�' },
+};
diff --git a/rsf-admin/src/page/rcsTest/components/SelectModal.jsx b/rsf-admin/src/page/rcsTest/components/SelectModal.jsx
new file mode 100644
index 0000000..df9dfa9
--- /dev/null
+++ b/rsf-admin/src/page/rcsTest/components/SelectModal.jsx
@@ -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) {
+ // 鏀寔澶氫釜鎼滅储瀛楁锛堢墿鏂欐敮鎸乧ode鍜宯ame锛�
+ 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;
diff --git a/rsf-admin/src/page/rcsTest/components/SummaryReport.jsx b/rsf-admin/src/page/rcsTest/components/SummaryReport.jsx
new file mode 100644
index 0000000..3a229b9
--- /dev/null
+++ b/rsf-admin/src/page/rcsTest/components/SummaryReport.jsx
@@ -0,0 +1,213 @@
+import React, { useMemo } from 'react';
+import {
+ Box,
+ Typography,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Chip,
+ Stack,
+} from '@mui/material';
+
+/**
+ * 姹囨�绘姤鍛婄粍浠�
+ * 鍙傝�僇Meter鐨凷ummaryReport锛屾樉绀虹粺璁′俊鎭�
+ */
+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;
diff --git a/rsf-admin/src/page/rcsTest/index.jsx b/rsf-admin/src/page/rcsTest/index.jsx
new file mode 100644
index 0000000..b115525
--- /dev/null
+++ b/rsf-admin/src/page/rcsTest/index.jsx
@@ -0,0 +1,9 @@
+import React from "react";
+import RcsTestList from "./RcsTestList";
+
+export default {
+ list: RcsTestList,
+ recordRepresentation: (record) => {
+ return `${record.id}`
+ }
+};
diff --git a/rsf-admin/src/page/rcsTest/utils/variableExtractor.js b/rsf-admin/src/page/rcsTest/utils/variableExtractor.js
new file mode 100644
index 0000000..5896f5e
--- /dev/null
+++ b/rsf-admin/src/page/rcsTest/utils/variableExtractor.js
@@ -0,0 +1,149 @@
+/**
+ * 鍙橀噺鎻愬彇鍣�
+ * 鍙傝�僇Meter鐨勫彉閲忔彁鍙栧疄鐜帮紝浠庡搷搴斾腑鎻愬彇鍙橀噺
+ */
+
+/**
+ * 姝e垯琛ㄨ揪寮忔彁鍙栧櫒
+ */
+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=绗琋涓�
+ 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 {
+ // 閫夋嫨绗琋涓紙浠�1寮�濮嬶級
+ selectedMatch = matches[matchNumber - 1] || matches[0];
+ }
+
+ if (!selectedMatch) {
+ return null;
+ }
+
+ return applyTemplate(selectedMatch, template);
+ } catch (error) {
+ console.error('姝e垯琛ㄨ揪寮忔彁鍙栧け璐�:', 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),
+ };
+};
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasContainerController.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasContainerController.java
index 264d337..8f8d42a 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasContainerController.java
+++ b/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()){
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/RcsTestController.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/RcsTestController.java
new file mode 100644
index 0000000..a3cc540
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/RcsTestController.java
@@ -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("鍒犻櫎澶辫触锛�");
+ }
+
+ /**
+ * 淇濆瓨娴嬭瘯璁″垝锛圝Meter椋庢牸锛�
+ */
+ @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("澶嶅埗澶辫触锛�");
+ }
+ }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/RcsTestParams.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/RcsTestParams.java
new file mode 100644
index 0000000..1d225b5
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/RcsTestParams.java
@@ -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("鍏ュ簱鎺ュ彛绫诲瀷锛坈reate_in_task/location_allocate锛岄粯璁reate_in_task锛�")
+ private String inboundApiType = "create_in_task";
+
+ @ApiModelProperty("闅忔満鐗╂枡鏁伴噺锛堥粯璁�1锛�")
+ private Integer randomMaterialCount = 1;
+
+ @ApiModelProperty("鏄惁鑷姩鍑哄簱锛坱rue:鏄� false:鍚︼紝榛樿true锛�")
+ private Boolean autoOutbound = true;
+
+ @ApiModelProperty("閰嶇疆ID锛堝鏋滄彁渚涳紝灏嗕娇鐢ㄩ厤缃腑鐨勫弬鏁帮級")
+ private Long configId;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/RcsTestConfig.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/RcsTestConfig.java
new file mode 100644
index 0000000..0293ddb
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/RcsTestConfig.java
@@ -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("鍏ュ簱鎺ュ彛绫诲瀷锛坈reate_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;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/RcsTestPlan.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/RcsTestPlan.java
new file mode 100644
index 0000000..31c14c4
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/RcsTestPlan.java
@@ -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娴嬭瘯璁″垝锛圝Meter椋庢牸锛�")
+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("娴嬭瘯璁″垝鏁版嵁锛圝SON鏍煎紡锛屽寘鍚畬鏁寸殑鏍戝舰缁撴瀯锛�")
+ 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;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/RcsTestConfigMapper.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/RcsTestConfigMapper.java
new file mode 100644
index 0000000..edcc856
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/RcsTestConfigMapper.java
@@ -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> {
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/RcsTestPlanMapper.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/RcsTestPlanMapper.java
new file mode 100644
index 0000000..a32c931
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/RcsTestPlanMapper.java
@@ -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> {
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/PakinSchedules.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/PakinSchedules.java
index 1c8fe5c..6395da0 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/PakinSchedules.java
+++ b/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());
+ // 杩囨护鎺塧snCode涓簄ull鎴栫┖瀛楃涓茬殑鎯呭喌锛堟棤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));
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/RcsTestPlanService.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/RcsTestPlanService.java
new file mode 100644
index 0000000..f4890d8
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/RcsTestPlanService.java
@@ -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> {
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/RcsTestService.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/RcsTestService.java
new file mode 100644
index 0000000..4e540f3
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/RcsTestService.java
@@ -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);
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestPlanServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestPlanServiceImpl.java
new file mode 100644
index 0000000..d4134ae
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestPlanServiceImpl.java
@@ -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 {
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestServiceImpl.java
new file mode 100644
index 0000000..7fa708e
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestServiceImpl.java
@@ -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("========== 寮�濮嬫墽琛孯CS鍏ㄦ祦绋嬭嚜鍔ㄦ祴璇� ==========");
+ 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("瑙f瀽鍏ュ簱搴撲綅鍙锋暟缁勫け璐�: {}", e.getMessage());
+ }
+ }
+ // 鍏煎鏃ф暟鎹細濡傛灉inboundLocNos涓虹┖浣唋ocNo鏈夊�硷紝浣跨敤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("瑙f瀽鍑哄簱搴撲綅鍙锋暟缁勫け璐�: {}", 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涓虹┖浣唋ocNo鏈夊��
+ if (requestedLocNo == null && StringUtils.isNotBlank(params.getLocNo())) {
+ requestedLocNo = params.getLocNo();
+ }
+
+ if ("location_allocate".equals(params.getInboundApiType())) {
+ // 浣跨敤 location_allocate 鎺ュ彛锛堝唴閮ㄨ皟鐢╟reateInTask锛�
+ 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()) {
+ // 灏哠tring鍒楄〃杞崲涓篖ong鍒楄〃
+ 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);
+ }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
index 5599cb5..62f7b06 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
+++ b/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);
- //绉诲嚭鏀惰揣鍖哄簱瀛橈紝 淇敼缁勬墭鐘舵��
- removeReceiptStock(pakinItem, loginUserId);
+ //绉诲嚭鏀惰揣鍖哄簱瀛橈紝 淇敼缁勬墭鐘舵�侊紙鍙湁褰搒ource涓嶄负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涓簄ull锛岃鏄庣粍鎵樻槑缁嗕笉鍦ㄦ敹璐у尯锛屾棤闇�绉婚櫎鏀惰揣鍖哄簱瀛�
+ 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());
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WaitPakinServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WaitPakinServiceImpl.java
index a4af675..45f82f8 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WaitPakinServiceImpl.java
+++ b/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;
}
/**
--
Gitblit v1.9.1