基于现有的MES Test Project(mes-web + mes-plcSend),扩展支持多设备联合测试功能,实现"上大车设备 → 大理片设备 → 玻璃存储设备"的完整生产流程自动化测试。
mes-plcSend/src/main/java/com/mes/
├── device/ # 设备管理层
│ ├── entity/
│ │ ├── DeviceConfig.java # 设备配置实体
│ │ ├── DeviceGroup.java # 设备组实体
│ │ └── DeviceStatus.java # 设备状态实体
│ ├── service/
│ │ ├── DeviceService.java # 设备管理服务
│ │ ├── DeviceGroupService.java # 设备组服务
│ │ └── DeviceCoordinationService.java # 设备协调服务
│ └── controller/
│ ├── DeviceController.java # 设备管理API
│ └── DeviceGroupController.java # 设备组管理API
├── interaction/ # 交互逻辑模块
│ ├── base/
│ │ ├── BaseInteraction.java # 基础交互抽象
│ │ ├── InteractionContext.java # 交互上下文
│ │ └── InteractionResult.java # 交互结果
│ ├── 上大车/
│ │ ├── 上大车Interaction.java # 上大车交互逻辑
│ │ └── 上大车Config.java # 上大车配置
│ ├── 大理片/
│ │ ├── 大理片Interaction.java # 大理片交互逻辑
│ │ └── 大理片Config.java # 大理片配置
│ └── 玻璃存储/
│ ├── 玻璃存储Interaction.java # 玻璃存储交互逻辑
│ └── 玻璃存储Config.java # 玻璃存储配置
└── task/ # 任务管理层
├── entity/
│ ├── MultiDeviceTask.java # 多设备任务实体
│ └── TaskStep.java # 任务步骤实体
├── service/
│ ├── MultiDeviceTaskService.java # 多设备任务服务
│ └── TaskExecutionEngine.java # 任务执行引擎
└── controller/
└── MultiDeviceTaskController.java # 多设备任务API
-- 设备配置表
CREATE TABLE device_config (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
device_id VARCHAR(50) UNIQUE NOT NULL COMMENT '设备ID',
device_name VARCHAR(100) NOT NULL COMMENT '设备名称',
device_type VARCHAR(50) NOT NULL COMMENT '设备类型(上大车/大理片/玻璃存储)',
plc_ip VARCHAR(15) NOT NULL COMMENT 'PLC IP地址',
plc_type VARCHAR(20) NOT NULL COMMENT 'PLC类型',
module_name VARCHAR(50) NOT NULL COMMENT '模块名称',
is_primary BOOLEAN DEFAULT FALSE COMMENT '是否主控设备',
enabled BOOLEAN DEFAULT TRUE COMMENT '是否启用',
config_json TEXT COMMENT '设备特定配置(JSON)',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 设备组配置表
CREATE TABLE device_group (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
group_id VARCHAR(50) UNIQUE NOT NULL COMMENT '设备组ID',
group_name VARCHAR(100) NOT NULL COMMENT '设备组名称',
project_id VARCHAR(50) NOT NULL COMMENT '关联项目ID',
execution_mode ENUM('SERIAL', 'PARALLEL') DEFAULT 'SERIAL' COMMENT '执行模式',
execution_config JSON COMMENT '执行配置',
dependencies JSON COMMENT '设备依赖关系',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 设备组与设备关系表
CREATE TABLE device_group_mapping (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
group_id VARCHAR(50) NOT NULL,
device_id VARCHAR(50) NOT NULL,
execution_order INT NOT NULL COMMENT '执行顺序',
FOREIGN KEY (group_id) REFERENCES device_group(group_id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES device_config(device_id) ON DELETE CASCADE,
UNIQUE KEY uk_group_device (group_id, device_id)
);
-- 多设备任务表
CREATE TABLE multi_device_task (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
task_id VARCHAR(50) UNIQUE NOT NULL COMMENT '任务ID',
group_id VARCHAR(50) NOT NULL COMMENT '设备组ID',
project_id VARCHAR(50) NOT NULL COMMENT '项目ID',
status ENUM('PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'CANCELLED') DEFAULT 'PENDING',
current_step INT DEFAULT 0 COMMENT '当前步骤',
total_steps INT DEFAULT 0 COMMENT '总步骤数',
start_time DATETIME COMMENT '开始时间',
end_time DATETIME COMMENT '结束时间',
error_message TEXT COMMENT '错误信息',
result_data JSON COMMENT '结果数据',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 任务步骤详情表
CREATE TABLE task_step_detail (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
task_id VARCHAR(50) NOT NULL COMMENT '任务ID',
step_order INT NOT NULL COMMENT '步骤顺序',
device_id VARCHAR(50) NOT NULL COMMENT '设备ID',
step_name VARCHAR(100) NOT NULL COMMENT '步骤名称',
status ENUM('PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'SKIPPED') DEFAULT 'PENDING',
start_time DATETIME COMMENT '步骤开始时间',
end_time DATETIME COMMENT '步骤结束时间',
duration_ms BIGINT COMMENT '执行耗时(毫秒)',
input_data JSON COMMENT '输入数据',
output_data JSON COMMENT '输出数据',
error_message TEXT COMMENT '错误信息',
retry_count INT DEFAULT 0 COMMENT '重试次数',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (task_id) REFERENCES multi_device_task(task_id) ON DELETE CASCADE
);
mes-web/src/views/plcTest/
├── components/
│ ├── DeviceManagement/ # 设备管理组件
│ │ ├── DeviceList.vue # 设备列表
│ │ ├── DeviceConfig.vue # 设备配置
│ │ └── DeviceStatus.vue # 设备状态
│ ├── DeviceGroup/ # 设备组组件
│ │ ├── GroupList.vue # 设备组列表
│ │ ├── GroupConfig.vue # 设备组配置
│ │ └── GroupTopology.vue # 设备组拓扑图
│ ├── MultiDeviceTest/ # 多设备测试组件
│ │ ├── TestOrchestration.vue # 测试编排
│ │ ├── ExecutionMonitor.vue # 执行监控
│ │ └── ResultAnalysis.vue # 结果分析
│ └── InteractionLogic/ # 交互逻辑组件
│ ├── 上大车Config.vue # 上大车配置
│ ├── 大理片Config.vue # 大理片配置
│ └── 玻璃存储Config.vue # 玻璃存储配置
@Data
@Entity
@TableName("device_config")
public class DeviceConfig {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("device_id")
private String deviceId;
@TableField("device_name")
private String deviceName;
@TableField("device_type")
private String deviceType; // "上大车" / "大理片" / "玻璃存储"
@TableField("plc_ip")
private String plcIp;
@TableField("plc_type")
private String plcType;
@TableField("module_name")
private String moduleName;
@TableField("is_primary")
private Boolean isPrimary;
@TableField("config_json")
private String configJson; // 设备特定配置
// 配置解析方法
public <T> T getConfig(Class<T> clazz) {
if (StringUtils.isBlank(configJson)) {
return null;
}
try {
return JsonUtils.fromJson(configJson, clazz);
} catch (Exception e) {
log.error("解析设备配置失败: {}", deviceId, e);
return null;
}
}
}
@Service
public class DeviceService {
@Resource
private DeviceConfigMapper deviceConfigMapper;
/**
* 注册设备
*/
@Transactional
public void registerDevice(DeviceConfig device) {
// 验证PLC连接
validatePlcConnection(device.getPlcIp(), device.getPlcType());
// 保存设备配置
deviceConfigMapper.insert(device);
// 更新地址映射
updatePlcAddressMapping(device);
log.info("设备注册成功: {}", device.getDeviceId());
}
/**
* 获取设备的PLC地址映射
*/
public Map<String, Integer> getDeviceAddressMapping(String deviceId) {
DeviceConfig device = getDeviceById(deviceId);
if (device == null) {
throw new RuntimeException("设备不存在: " + deviceId);
}
// 根据设备类型获取对应的地址映射配置
String mappingKey = device.getDeviceType() + "_" + device.getModuleName();
return plcAddressService.getMappingByKey(mappingKey);
}
}
@Data
@TableName("device_group")
public class DeviceGroup {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("group_id")
private String groupId;
@TableField("group_name")
private String groupName;
@TableField("project_id")
private String projectId;
@TableField("execution_mode")
private ExecutionMode executionMode; // SERIAL / PARALLEL
@TableField("execution_config")
private String executionConfig;
@TableField("dependencies")
private String dependencies; // JSON格式的依赖关系
// 获取设备组中的所有设备
public List<DeviceConfig> getDevices() {
return deviceGroupService.getDevicesByGroupId(groupId);
}
// 获取执行顺序
public List<DeviceConfig> getExecutionSequence() {
return deviceGroupService.getDevicesByOrder(groupId);
}
}
/**
* 设备交互逻辑接口
*/
public interface DeviceInteraction {
/**
* 执行交互逻辑
*/
InteractionResult execute(InteractionContext context);
/**
* 验证前置条件
*/
boolean validatePreCondition(InteractionContext context);
/**
* 获取设备类型
*/
String getDeviceType();
/**
* 获取默认配置
*/
DeviceInteractionConfig getDefaultConfig();
}
@Component("上大车Interaction")
public class 上大车Interaction implements DeviceInteraction {
@Override
public InteractionResult execute(InteractionContext context) {
上大车Config config = context.getConfig(上大车Config.class);
try {
// 1. 验证前置条件
if (!validatePreCondition(context)) {
return InteractionResult.fail("前置条件验证失败");
}
// 2. 获取车辆规格(可配置)
VehicleSpec vehicle = context.getVehicleSpec();
double vehicleCapacity = config.getVehicleCapacity(); // 默认6000mm
// 3. 检查车辆容量
if (vehicle.getMaxCapacity() > vehicleCapacity) {
return InteractionResult.fail("车辆容量超出限制: " + vehicle.getMaxCapacity());
}
// 4. 获取当前玻璃信息
GlassSpec currentGlass = context.getCurrentGlass();
// 5. 计算装载空间
double usedCapacity = calculateUsedCapacity(context);
double remainingCapacity = vehicleCapacity - usedCapacity;
if (remainingCapacity >= currentGlass.getLength()) {
// 6. 分配车辆空间
allocateVehicleSpace(context, currentGlass);
// 7. 节拍控制(可配置间隔时间)
sleep(config.getGlassIntervalMs()); // 默认1000ms
// 8. 触发PLC写入
triggerPlcWrite(context, "车辆装载", currentGlass);
return InteractionResult.success("上大车成功",
Map.of("remainingCapacity", remainingCapacity,
"allocatedGlass", currentGlass));
}
return InteractionResult.wait("等待下一辆车上大车",
Map.of("remainingCapacity", remainingCapacity));
} catch (Exception e) {
log.error("上大车交互执行失败", e);
return InteractionResult.fail("执行异常: " + e.getMessage());
}
}
@Override
public boolean validatePreCondition(InteractionContext context) {
// 验证车辆信息、玻璃信息、PLC连接等
return context.getVehicleSpec() != null &&
context.getCurrentGlass() != null &&
validatePlcConnection(context.getDeviceId());
}
@Override
public String getDeviceType() {
return "上大车";
}
}
@Component("大理片Interaction")
public class 大理片Interaction implements DeviceInteraction {
@Override
public InteractionResult execute(InteractionContext context) {
大理片Config config = context.getConfig(大理片Config.class);
try {
// 1. 获取上大车阶段传递的数据
List<GlassSpec> glassesFromVehicle = context.getSharedData("glassesFromVehicle", List.class);
VehicleSpec vehicleInfo = context.getSharedData("vehicleInfo", VehicleSpec.class);
if (glassesFromVehicle == null || glassesFromVehicle.isEmpty()) {
return InteractionResult.wait("等待上大车数据");
}
// 2. 与MES大理片信息比对验证
List<GlassSpec> mesGlasses = fetchMesGlassesInfo(vehicleInfo.getVehicleId());
for (GlassSpec vehicleGlass : glassesFromVehicle) {
boolean matched = false;
for (GlassSpec mesGlass : mesGlasses) {
if (isGlassMatched(vehicleGlass, mesGlass, config)) {
matched = true;
break;
}
}
if (!matched && config.isGlassMatchingEnabled()) {
return InteractionResult.fail("玻璃信息不匹配: " + vehicleGlass.getGlassId());
}
}
// 3. 批量处理(可配置)
if (config.isBatchProcessing()) {
return processBatch(glassesFromVehicle, context, config);
} else {
return processIndividual(glassesFromVehicle, context, config);
}
} catch (Exception e) {
log.error("大理片交互执行失败", e);
return InteractionResult.fail("执行异常: " + e.getMessage());
}
}
private InteractionResult processBatch(List<GlassSpec> glasses, InteractionContext context, 大理片Config config) {
// 批量处理逻辑
for (GlassSpec glass : glasses) {
// 每片玻璃处理间隔
sleep(config.getProcessingInterval()); // 默认2000ms
triggerPlcWrite(context, "大理片处理", glass);
}
// 传递处理结果到下一个设备
context.setSharedData("processedGlasses", glasses);
return InteractionResult.success("大理片批量处理完成",
Map.of("processedCount", glasses.size()));
}
}
@Service
public class TaskExecutionEngine {
@Resource
private DeviceGroupService deviceGroupService;
/**
* 执行多设备联合测试
*/
@Transactional
public MultiDeviceTaskResult executeMultiDeviceTask(String groupId, TaskParameters parameters) {
DeviceGroup group = deviceGroupService.getDeviceGroupById(groupId);
if (group == null) {
throw new RuntimeException("设备组不存在: " + groupId);
}
// 1. 创建任务记录
MultiDeviceTask task = createTaskRecord(group, parameters);
try {
task.setStatus("RUNNING");
task.setStartTime(new Date());
// 2. 构建交互上下文
InteractionContext context = buildInteractionContext(group, parameters);
// 3. 按执行模式执行
MultiDeviceTaskResult result;
if (group.getExecutionMode() == ExecutionMode.SERIAL) {
result = executeSerialDevices(group, context, task);
} else {
result = executeParallelDevices(group, context, task);
}
// 4. 更新任务结果
task.setStatus(result.isSuccess() ? "COMPLETED" : "FAILED");
task.setEndTime(new Date());
task.setResultData(JsonUtils.toJson(result));
return result;
} catch (Exception e) {
log.error("多设备任务执行失败: {}", groupId, e);
task.setStatus("FAILED");
task.setEndTime(new Date());
task.setErrorMessage(e.getMessage());
return MultiDeviceTaskResult.fail("任务执行失败: " + e.getMessage());
}
}
/**
* 串行设备执行:上大车 → 大理片 → 玻璃存储
*/
private MultiDeviceTaskResult executeSerialDevices(DeviceGroup group, InteractionContext context, MultiDeviceTask task) {
MultiDeviceTaskResult finalResult = new MultiDeviceTaskResult();
List<DeviceConfig> executionSequence = group.getExecutionSequence();
for (int i = 0; i < executionSequence.size(); i++) {
DeviceConfig device = executionSequence.get(i);
TaskStep step = createTaskStep(task.getTaskId(), i + 1, device);
try {
step.setStatus("RUNNING");
step.setStartTime(new Date());
// 设置当前设备上下文
context.setCurrentDevice(device);
context.setDeviceId(device.getDeviceId());
// 执行设备交互逻辑
DeviceInteraction interaction = getInteraction(device.getDeviceType());
InteractionResult stepResult = interaction.execute(context);
step.setEndTime(new Date());
step.setDurationMs(System.currentTimeMillis() - step.getStartTime().getTime());
if (stepResult.isSuccess()) {
step.setStatus("COMPLETED");
step.setOutputData(JsonUtils.toJson(stepResult.getData()));
// 传递数据到下一个设备
passDataToNextDevice(context, device, stepResult);
finalResult.addStepResult(device.getDeviceName(), stepResult);
} else {
step.setStatus("FAILED");
step.setErrorMessage(stepResult.getMessage());
finalResult.addStepResult(device.getDeviceName(), stepResult);
finalResult.fail("设备 " + device.getDeviceName() + " 执行失败: " + stepResult.getMessage());
break;
}
} catch (Exception e) {
step.setStatus("FAILED");
step.setErrorMessage(e.getMessage());
step.setEndTime(new Date());
log.error("设备执行异常: {}", device.getDeviceName(), e);
finalResult.fail("设备 " + device.getDeviceName() + " 执行异常: " + e.getMessage());
break;
}
}
return finalResult;
}
/**
* 数据传递到下一个设备
*/
private void passDataToNextDevice(InteractionContext context, DeviceConfig currentDevice, InteractionResult result) {
String deviceType = currentDevice.getDeviceType();
if ("上大车".equals(deviceType)) {
// 上大车 → 大理片:传递玻璃列表和车辆信息
context.setSharedData("glassesFromVehicle", result.getData("glasses"));
context.setSharedData("vehicleInfo", result.getData("vehicle"));
} else if ("大理片".equals(deviceType)) {
// 大理片 → 玻璃存储:传递处理完成的玻璃
context.setSharedData("processedGlasses", result.getData("processedGlasses"));
}
}
}
<template>
<div class="device-management">
<!-- 设备列表 -->
<div class="device-list">
<el-table :data="devices" style="width: 100%">
<el-table-column prop="deviceName" label="设备名称" />
<el-table-column prop="deviceType" label="设备类型" />
<el-table-column prop="plcIp" label="PLC IP" />
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="{ row }">
<el-button @click="editDevice(row)">编辑</el-button>
<el-button @click="testConnection(row)">测试连接</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 设备配置表单 -->
<el-dialog v-model="showConfigDialog" title="设备配置">
<el-form :model="deviceForm" label-width="120px">
<el-form-item label="设备ID">
<el-input v-model="deviceForm.deviceId" />
</el-form-item>
<el-form-item label="设备名称">
<el-input v-model="deviceForm.deviceName" />
</el-form-item>
<el-form-item label="设备类型">
<el-select v-model="deviceForm.deviceType">
<el-option label="上大车设备" value="上大车" />
<el-option label="大理片设备" value="大理片" />
<el-option label="玻璃存储设备" value="玻璃存储" />
</el-select>
</el-form-item>
<el-form-item label="PLC配置">
<div class="plc-config">
<el-input v-model="deviceForm.plcIp" placeholder="PLC IP地址" />
<el-select v-model="deviceForm.plcType" placeholder="PLC类型">
<el-option label="S7-1200" value="S7-1200" />
<el-option label="S7-1500" value="S7-1500" />
</el-select>
</div>
</el-form-item>
<!-- 设备特定配置 -->
<div v-if="deviceForm.deviceType === '上大车'">
<el-form-item label="车辆容量(mm)">
<el-input-number v-model="deviceForm.config.vehicleCapacity" :min="1000" :max="12000" />
</el-form-item>
<el-form-item label="玻璃间隔(ms)">
<el-input-number v-model="deviceForm.config.glassIntervalMs" :min="100" :max="10000" />
</el-form-item>
</div>
<div v-if="deviceForm.deviceType === '大理片'">
<el-form-item label="启用玻璃比对">
<el-switch v-model="deviceForm.config.glassMatchingEnabled" />
</el-form-item>
<el-form-item label="批量处理">
<el-switch v-model="deviceForm.config.batchProcessing" />
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button @click="showConfigDialog = false">取消</el-button>
<el-button type="primary" @click="saveDevice">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
return {
devices: [],
showConfigDialog: false,
deviceForm: {
deviceId: '',
deviceName: '',
deviceType: '',
plcIp: '',
plcType: '',
config: {
vehicleCapacity: 6000, // 默认6米
glassIntervalMs: 1000, // 默认1秒
glassMatchingEnabled: true,
batchProcessing: true
}
}
}
},
methods: {
async loadDevices() {
// 加载设备列表
const response = await this.$api.device.getDeviceList();
this.devices = response.data;
},
async saveDevice() {
try {
if (this.deviceForm.deviceId) {
await this.$api.device.updateDevice(this.deviceForm);
} else {
await this.$api.device.createDevice(this.deviceForm);
}
this.$message.success('保存成功');
this.showConfigDialog = false;
this.loadDevices();
} catch (error) {
this.$message.error('保存失败: ' + error.message);
}
}
}
}
</script>
<template>
<div class="device-group-management">
<!-- 设备组列表 -->
<div class="group-list">
<div class="header">
<h3>设备组配置</h3>
<el-button type="primary" @click="createGroup">新建设备组</el-button>
</div>
<div class="groups">
<el-card v-for="group in deviceGroups" :key="group.groupId" class="group-card">
<template #header>
<div class="card-header">
<span>{{ group.groupName }}</span>
<el-tag :type="group.executionMode === 'SERIAL' ? 'primary' : 'success'">
{{ group.executionMode === 'SERIAL' ? '串行执行' : '并行执行' }}
</el-tag>
</div>
</template>
<div class="group-content">
<!-- 设备拓扑图 -->
<div class="topology">
<div v-for="(device, index) in group.devices" :key="device.deviceId" class="device-node">
<div class="device-box" :class="device.deviceType">
<div class="device-name">{{ device.deviceName }}</div>
<div class="device-type">{{ device.deviceType }}</div>
</div>
<div v-if="index < group.devices.length - 1" class="arrow">→</div>
</div>
</div>
<!-- 依赖关系 -->
<div class="dependencies" v-if="group.dependencies">
<h4>设备依赖关系:</h4>
<div v-for="(deps, device) in group.dependencies" :key="device" class="dependency-item">
{{ device }} 依赖于: {{ deps.join(', ') }}
</div>
</div>
</div>
<template #footer>
<div class="card-footer">
<el-button @click="editGroup(group)">编辑</el-button>
<el-button @click="startTest(group)">开始测试</el-button>
<el-button @click="deleteGroup(group)">删除</el-button>
</div>
</template>
</el-card>
</div>
</div>
<!-- 设备组配置对话框 -->
<el-dialog v-model="showGroupDialog" title="设备组配置" width="800px">
<el-form :model="groupForm" label-width="120px">
<el-form-item label="设备组名称">
<el-input v-model="groupForm.groupName" />
</el-form-item>
<el-form-item label="关联项目">
<el-select v-model="groupForm.projectId">
<el-option v-for="project in projects" :key="project.id"
:label="project.projectName" :value="project.id" />
</el-select>
</el-form-item>
<el-form-item label="执行模式">
<el-radio-group v-model="groupForm.executionMode">
<el-radio label="SERIAL">串行执行</el-radio>
<el-radio label="PARALLEL">并行执行</el-radio>
</el-radio-group>
</el-form-item>
<!-- 设备选择 -->
<el-form-item label="包含设备">
<el-transfer
v-model="selectedDevices"
:data="availableDevices"
:titles="['可用设备', '已选设备']"
@change="updateDeviceOrder" />
</el-form-item>
<!-- 执行顺序调整 -->
<el-form-item label="执行顺序" v-if="groupForm.executionMode === 'SERIAL'">
<div class="execution-order">
<div v-for="(deviceId, index) in deviceOrder" :key="deviceId" class="order-item">
<span>{{ index + 1 }}. {{ getDeviceName(deviceId) }}</span>
<el-button-group>
<el-button @click="moveUp(index)" :disabled="index === 0">↑</el-button>
<el-button @click="moveDown(index)" :disabled="index === deviceOrder.length - 1">↓</el-button>
</el-button-group>
</div>
</div>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
return {
deviceGroups: [],
availableDevices: [],
selectedDevices: [],
deviceOrder: [],
showGroupDialog: false,
groupForm: {
groupId: '',
groupName: '',
projectId: '',
executionMode: 'SERIAL'
}
}
},
methods: {
async loadDeviceGroups() {
const response = await this.$api.deviceGroup.getGroupList();
this.deviceGroups = response.data;
},
async startTest(group) {
try {
// 跳转到测试执行页面
this.$router.push({
name: 'MultiDeviceTest',
query: { groupId: group.groupId }
});
} catch (error) {
this.$message.error('启动测试失败: ' + error.message);
}
}
}
}
</script>
<template>
<div class="multi-device-test">
<!-- 测试配置 -->
<div class="test-config">
<el-card>
<template #header>
<h3>测试配置 - {{ groupInfo.groupName }}</h3>
</template>
<div class="config-content">
<div class="execution-mode">
<el-tag :type="groupInfo.executionMode === 'SERIAL' ? 'primary' : 'success'">
{{ groupInfo.executionMode === 'SERIAL' ? '串行执行' : '并行执行' }}
</el-tag>
</div>
<div class="parameters">
<h4>测试参数:</h4>
<el-form label-width="150px">
<el-form-item label="车辆规格">
<el-input-number v-model="testParams.vehicleLength" :min="1000" :max="12000" /> mm
</el-form-item>
<el-form-item label="测试玻璃数量">
<el-input-number v-model="testParams.glassCount" :min="1" :max="50" />
</el-form-item>
<el-form-item label="执行间隔">
<el-input-number v-model="testParams.executionInterval" :min="500" :max="10000" /> ms
</el-form-item>
</el-form>
</div>
<div class="test-controls">
<el-button type="primary" size="large" @click="startTest" :loading="isRunning">
{{ isRunning ? '测试执行中...' : '开始测试' }}
</el-button>
<el-button @click="stopTest" :disabled="!isRunning">停止测试</el-button>
<el-button @click="pauseTest" :disabled="!isRunning">暂停测试</el-button>
</div>
</div>
</el-card>
</div>
<!-- 执行监控 -->
<div class="execution-monitor" v-if="isRunning || currentTask">
<el-card>
<template #header>
<h3>执行监控</h3>
</template>
<div class="monitor-content">
<!-- 任务进度 -->
<div class="task-progress">
<el-progress
:percentage="getOverallProgress()"
:status="getTaskStatus()" />
<div class="progress-info">
步骤 {{ currentStep }} / {{ totalSteps }} - {{ getCurrentStepName() }}
</div>
</div>
<!-- 设备状态 -->
<div class="device-status-grid">
<div v-for="device in groupInfo.devices" :key="device.deviceId"
class="device-status-card">
<div class="device-header">
<span class="device-name">{{ device.deviceName }}</span>
<el-tag :type="getDeviceStatusType(device.deviceId)">
{{ getDeviceStatus(device.deviceId) }}
</el-tag>
</div>
<div class="device-details" v-if="getDeviceDetails(device.deviceId)">
<div class="detail-item">
<span>当前玻璃:</span> {{ getDeviceDetails(device.deviceId).currentGlass || '-' }}
</div>
<div class="detail-item">
<span>处理数量:</span> {{ getDeviceDetails(device.deviceId).processedCount || 0 }}
</div>
<div class="detail-item">
<span>剩余容量:</span> {{ getDeviceDetails(device.deviceId).remainingCapacity || '-' }} mm
</div>
</div>
<!-- 实时日志 -->
<div class="device-logs">
<div v-for="log in getDeviceLogs(device.deviceId)" :key="log.timestamp"
class="log-item" :class="log.level">
<span class="log-time">{{ formatTime(log.timestamp) }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</div>
</div>
<!-- 数据流可视化 -->
<div class="data-flow">
<h4>数据流状态:</h4>
<div class="flow-diagram">
<div v-for="(device, index) in groupInfo.devices" :key="device.deviceId" class="flow-node">
<div class="node" :class="['device-' + device.deviceType.toLowerCase(), getNodeStatus(device.deviceId)]">
{{ device.deviceName }}
</div>
<div class="data-indicator" v-if="index < groupInfo.devices.length - 1">
<div class="data-flow-arrow" :class="getFlowStatus(device.deviceId, index + 1)">
{{ getFlowData(device.deviceId) }}
</div>
</div>
</div>
</div>
</div>
</div>
</el-card>
</div>
<!-- 结果分析 -->
<div class="test-results" v-if="testResults">
<el-card>
<template #header>
<h3>测试结果</h3>
</template>
<div class="results-content">
<!-- 总体结果 -->
<div class="overall-result">
<el-result
:icon="testResults.success ? 'success' : 'error'"
:title="testResults.success ? '测试成功' : '测试失败'"
:sub-title="testResults.message">
<template #extra>
<div class="result-stats">
<div class="stat-item">
<span class="label">执行时间:</span>
<span class="value">{{ formatDuration(testResults.duration) }}</span>
</div>
<div class="stat-item">
<span class="label">处理玻璃:</span>
<span class="value">{{ testResults.processedGlassCount }} 片</span>
</div>
<div class="stat-item">
<span class="label">成功率:</span>
<span class="value">{{ testResults.successRate }}%</span>
</div>
</div>
</template>
</el-result>
</div>
<!-- 详细结果 -->
<div class="detailed-results">
<h4>各设备执行详情:</h4>
<el-collapse>
<el-collapse-item v-for="(result, deviceName) in testResults.deviceResults"
:key="deviceName" :title="deviceName">
<div class="device-result-detail">
<div class="result-summary">
<el-tag :type="result.success ? 'success' : 'error'">
{{ result.success ? '执行成功' : '执行失败' }}
</el-tag>
<span class="duration">耗时: {{ formatDuration(result.duration) }}</span>
</div>
<div class="result-data">
<h5>输出数据:</h5>
<pre>{{ JSON.stringify(result.outputData, null, 2) }}</pre>
</div>
<div class="result-logs">
<h5>执行日志:</h5>
<div class="log-list">
<div v-for="log in result.logs" :key="log.timestamp" class="log-line">
<span class="log-time">{{ formatTime(log.timestamp) }}</span>
<span class="log-level">{{ log.level }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
<!-- 导出结果 -->
<div class="export-results">
<el-button @click="exportResults('json')">导出JSON</el-button>
<el-button @click="exportResults('excel')">导出Excel</el-button>
<el-button @click="exportResults('pdf')">导出PDF报告</el-button>
</div>
</div>
</el-card>
</div>
</div>
</template>
通过这个扩展方案,MES Test Project将具备完整的多设备联合测试能力,支持复杂的生产流程自动化测试。方案基于现有架构设计,风险可控,实施难度适中,能够很好地满足业务需求。
建议优先实现第一阶段的基础架构,然后逐步完善交互逻辑和前端界面,确保每个阶段都能交付可用的功能。