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;
|