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