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