huang
2025-11-26 792236ef78c2cdd3a989fb40a7f2e2487c4e17b6
添加各个设备基础可配置参数
15个文件已修改
730 ■■■■■ 已修改文件
mes-processes/mes-plcSend/README.md 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceConfigServiceImpl.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java 77 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/base/WorkstationBaseHandler.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/config/WorkstationLogicConfig.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/DeviceConfigForm.vue 49 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/DeviceConfigList.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/DeviceEditDialog.vue 54 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/LargeGlassConfig.vue 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/LoadVehicleConfig.vue 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationScannerConfig.vue 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationTransferConfig.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/index.js 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/README.md
@@ -214,14 +214,12 @@
- **定时扫描**:可配置扫描间隔(默认 10s)
- **MES数据读取**:当 `mesSend=1` 时,读取玻璃信息(`mesGlassId`、`mesWidth`、`mesHeight`、`workLine`)
- **数据落库**:将玻璃信息保存到 `glass_info` 表
- **自动确认**:读取后自动将 `mesSend` 写回 0
#### 配置参数(extraParams.deviceLogic)
```json
{
  "scanIntervalMs": 10000,
  "workLine": "LINE_001",
  "autoConfirm": true
  "workLine": 1
}
```
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceConfigServiceImpl.java
@@ -135,10 +135,7 @@
        
        // 设备类型过滤
        if (deviceType != null && !deviceType.trim().isEmpty()) {
            List<String> convertedDeviceTypes = convertDeviceTypeFromString(deviceType);
            if (convertedDeviceTypes != null && !convertedDeviceTypes.isEmpty()) {
                wrapper.in(DeviceConfig::getDeviceType, convertedDeviceTypes);
            }
            wrapper.eq(DeviceConfig::getDeviceType, deviceType.trim());
        }
        
        // 设备状态过滤
@@ -312,47 +309,6 @@
        }
    }
    /**
     * 字符串转换为设备类型
     */
    private List<String> convertDeviceTypeFromString(String deviceType) {
        if (deviceType == null) {
            return Collections.emptyList();
        }
        String normalized = deviceType.trim().toLowerCase();
        switch (normalized) {
            case "load_vehicle":
            case "上大车":
            case "上大车设备":
            case "大车设备":
            case "1":
                return Arrays.asList(
                    DeviceConfig.DeviceType.LOAD_VEHICLE,
                    "大车设备"
                );
            case "large_glass":
            case "大理片":
            case "大理片笼":
            case "2":
                return Arrays.asList(
                    DeviceConfig.DeviceType.LARGE_GLASS,
                    "大理片笼"
                );
            case "glass_storage":
            case "玻璃存储":
            case "卧式缓存":
            case "玻璃存储设备":
            case "3":
                return Arrays.asList(
                    DeviceConfig.DeviceType.GLASS_STORAGE,
                    "卧式缓存",
                    "玻璃存储设备"
                );
            default:
                return Collections.emptyList();
        }
    }
    /**
     * 字符串转换为状态
@@ -643,10 +599,7 @@
            
            // 设备类型过滤
        if (deviceType != null && !deviceType.trim().isEmpty()) {
            List<String> convertedDeviceTypes = convertDeviceTypeFromString(deviceType);
            if (convertedDeviceTypes != null && !convertedDeviceTypes.isEmpty()) {
                wrapper.in(DeviceConfig::getDeviceType, convertedDeviceTypes);
            }
            wrapper.eq(DeviceConfig::getDeviceType, deviceType.trim());
        }
            
            // 设备状态过滤
@@ -675,12 +628,42 @@
    @Override
    public List<String> getAllDeviceTypes() {
        List<String> deviceTypes = new ArrayList<>();
        deviceTypes.add("PLC设备");
        deviceTypes.add("传感器设备");
        deviceTypes.add("执行器设备");
        deviceTypes.add("人机界面设备");
        try {
            // 从数据库中查询所有已存在的设备类型(去重)
            LambdaQueryWrapper<DeviceConfig> wrapper = new LambdaQueryWrapper<>();
            wrapper.select(DeviceConfig::getDeviceType);
            wrapper.eq(DeviceConfig::getIsDeleted, 0);
            wrapper.isNotNull(DeviceConfig::getDeviceType);
            wrapper.ne(DeviceConfig::getDeviceType, "");
            List<DeviceConfig> devices = list(wrapper);
            List<String> deviceTypes = devices.stream()
                    .map(DeviceConfig::getDeviceType)
                    .filter(Objects::nonNull)
                    .filter(type -> !type.trim().isEmpty())
                    .distinct()
                    .sorted()
                    .collect(Collectors.toList());
            // 如果数据库中没有设备类型,返回默认类型
            if (deviceTypes.isEmpty()) {
                deviceTypes.add(DeviceConfig.DeviceType.LOAD_VEHICLE);
                deviceTypes.add(DeviceConfig.DeviceType.LARGE_GLASS);
                deviceTypes.add(DeviceConfig.DeviceType.WORKSTATION_SCANNER);
                deviceTypes.add(DeviceConfig.DeviceType.WORKSTATION_TRANSFER);
            }
        return deviceTypes;
        } catch (Exception e) {
            log.error("获取设备类型列表失败", e);
            // 异常时返回默认类型
            List<String> defaultTypes = new ArrayList<>();
            defaultTypes.add(DeviceConfig.DeviceType.LOAD_VEHICLE);
            defaultTypes.add(DeviceConfig.DeviceType.LARGE_GLASS);
            defaultTypes.add(DeviceConfig.DeviceType.WORKSTATION_SCANNER);
            defaultTypes.add(DeviceConfig.DeviceType.WORKSTATION_TRANSFER);
            return defaultTypes;
        }
    }
    @Override
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java
@@ -302,7 +302,14 @@
        Boolean triggerRequest = (Boolean) params.getOrDefault("triggerRequest", autoFeed);
        List<GlassInfo> plannedGlasses = planGlassLoading(glassInfos, vehicleCapacity,
                getLogicParam(logicParams, "defaultGlassLength", 2000));
                deviceConfig.getDeviceId());
        if (plannedGlasses == null) {
            // 玻璃没有长度时返回null表示错误
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("玻璃信息缺少长度数据,无法进行容量计算。请检查MES程序是否正确提供玻璃长度。")
                    .build();
        }
        if (plannedGlasses.isEmpty()) {
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
@@ -551,13 +558,6 @@
        defaultParams.put("taskMonitorIntervalMs", 1000); // 任务监控间隔(毫秒)
        defaultParams.put("mesConfirmTimeoutMs", 30000); // MES确认超时(毫秒)
        
        // 出片任务相关配置
        // outboundSlotRanges: 出片任务的startSlot范围,例如[1, 101]表示格子1~101都是出片任务
        // 如果不配置,则通过判断startSlot是否在positionMapping中来区分进片/出片
        List<Integer> outboundSlotRanges = new ArrayList<>();
        outboundSlotRanges.add(1);   // 最小格子编号
        outboundSlotRanges.add(101); // 最大格子编号
        defaultParams.put("outboundSlotRanges", outboundSlotRanges);
        
        // gridPositionMapping: 格子编号到位置的映射表(可选)
        // 如果不配置,则格子编号直接作为位置值
@@ -644,16 +644,32 @@
        return null;
    }
    /**
     * 规划玻璃装载
     * @param source 源玻璃列表
     * @param vehicleCapacity 车辆容量
     * @param deviceId 设备ID(用于日志)
     * @return 规划后的玻璃列表,如果玻璃没有长度则返回null(用于测试MES程序)
     */
    private List<GlassInfo> planGlassLoading(List<GlassInfo> source,
                                             int vehicleCapacity,
                                             Integer defaultGlassLength) {
                                             String deviceId) {
        List<GlassInfo> planned = new ArrayList<>();
        int usedLength = 0;
        int capacity = Math.max(vehicleCapacity, 1);
        int fallbackLength = defaultGlassLength != null && defaultGlassLength > 0 ? defaultGlassLength : 2000;
        for (GlassInfo info : source) {
            int length = info.getLength() != null && info.getLength() > 0 ? info.getLength() : fallbackLength;
            Integer glassLength = info.getLength();
            if (glassLength == null || glassLength <= 0) {
                // 玻璃没有长度,直接报错(用于测试MES程序)
                log.error("玻璃[{}]缺少长度数据,无法进行容量计算。deviceId={},请检查MES程序是否正确提供玻璃长度。",
                        info.getGlassId(), deviceId);
                return null;
            }
            int length = glassLength;
            if (planned.isEmpty()) {
                planned.add(info.withLength(length));
                usedLength = length;
@@ -1269,15 +1285,19 @@
        // 这里简化处理:如果startSlot不在positionMapping中,且是数字,可能是格子编号
        // 可以通过配置指定格子编号范围,或者通过查找同组设备判断
        
        // 方法3:通过配置指定出片任务的startSlot范围
        // 方法3:通过配置指定车辆运动格子范围(兼容旧配置outboundSlotRanges)
        @SuppressWarnings("unchecked")
        List<Integer> outboundSlotRanges = getLogicParam(logicParams, "outboundSlotRanges", null);
        if (outboundSlotRanges != null && !outboundSlotRanges.isEmpty()) {
            // 如果配置了出片slot范围,检查startSlot是否在范围内
            // 例如:[1, 101] 表示格子1~101都是出片任务
            if (outboundSlotRanges.size() >= 2) {
                int minSlot = outboundSlotRanges.get(0);
                int maxSlot = outboundSlotRanges.get(1);
        List<Integer> vehicleSlotRange = getLogicParam(logicParams, "vehicleSlotRange", null);
        if (vehicleSlotRange == null || vehicleSlotRange.isEmpty()) {
            // 兼容旧配置
            vehicleSlotRange = getLogicParam(logicParams, "outboundSlotRanges", null);
        }
        if (vehicleSlotRange != null && !vehicleSlotRange.isEmpty()) {
            // 如果配置了车辆运动格子范围,检查startSlot是否在范围内
            // 例如:[1, 101] 表示车辆只能在格子1~101之间运动
            if (vehicleSlotRange.size() >= 2) {
                int minSlot = vehicleSlotRange.get(0);
                int maxSlot = vehicleSlotRange.get(1);
                if (startSlot >= minSlot && startSlot <= maxSlot) {
                    return true;
                }
@@ -1361,6 +1381,25 @@
     */
    private TimeCalculation calculateTime(Integer currentPos, Integer startPos, 
                                          Integer targetPos, Map<String, Object> logicParams) {
        // 验证车辆运动格子范围
        @SuppressWarnings("unchecked")
        List<Integer> vehicleSlotRange = getLogicParam(logicParams, "vehicleSlotRange", null);
        if (vehicleSlotRange == null || vehicleSlotRange.isEmpty()) {
            // 兼容旧配置
            vehicleSlotRange = getLogicParam(logicParams, "outboundSlotRanges", null);
        }
        if (vehicleSlotRange != null && vehicleSlotRange.size() >= 2) {
            int minSlot = vehicleSlotRange.get(0);
            int maxSlot = vehicleSlotRange.get(1);
            // 验证startPos和targetPos是否在允许的范围内
            if (startPos != null && (startPos < minSlot || startPos > maxSlot)) {
                log.warn("起始位置 {} 超出车辆运动格子范围 [{}, {}]", startPos, minSlot, maxSlot);
            }
            if (targetPos != null && (targetPos < minSlot || targetPos > maxSlot)) {
                log.warn("目标位置 {} 超出车辆运动格子范围 [{}, {}]", targetPos, minSlot, maxSlot);
            }
        }
        // 获取速度(格/秒,grid/s)
        Double speed = getLogicParam(logicParams, "vehicleSpeed", 1.0);
        if (speed == null || speed <= 0) {
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/base/WorkstationBaseHandler.java
@@ -38,7 +38,6 @@
        config.setScanIntervalMs(getLogicParam(logicParams, "scanIntervalMs", 10_000));
        config.setTransferDelayMs(getLogicParam(logicParams, "transferDelayMs", 30_000));
        config.setVehicleCapacity(getLogicParam(logicParams, "vehicleCapacity", 6000));
        config.setAutoAck(getLogicParam(logicParams, "autoAck", Boolean.TRUE));
        return config;
    }
@@ -69,11 +68,10 @@
        defaults.put("scanIntervalMs", 10_000);
        defaults.put("transferDelayMs", 30_000);
        defaults.put("vehicleCapacity", 6_000);
        defaults.put("autoAck", true);
        try {
            return objectMapper.writeValueAsString(defaults);
        } catch (JsonProcessingException e) {
            return "{\"scanIntervalMs\":10000,\"transferDelayMs\":30000,\"vehicleCapacity\":6000,\"autoAck\":true}";
            return "{\"scanIntervalMs\":10000,\"transferDelayMs\":30000,\"vehicleCapacity\":6000}";
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/config/WorkstationLogicConfig.java
@@ -23,10 +23,5 @@
     * 可装载的最大宽度(mm)
     */
    private Integer vehicleCapacity = 6_000;
    /**
     * 是否自动确认 MES 发送的玻璃信息
     */
    private Boolean autoAck = Boolean.TRUE;
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java
@@ -87,9 +87,6 @@
                return buildResult(deviceConfig, operation, false, "保存玻璃信息失败: " + glassId);
            }
            // 读取到MES数据后,重置mesSend,避免重复消费
            plcDynamicDataService.writePlcField(deviceConfig, "mesSend", 0, serializer);
            String msg = String.format("玻璃[%s] 尺寸[%s x %s] 已接收并入库,workLine=%s",
                    glassId,
                    longSide != null ? longSide + "mm" : "-",
mes-web/src/views/device/DeviceConfigForm.vue
@@ -4,12 +4,13 @@
    <div class="search-section">
      <el-form :model="searchForm" :inline="true" class="search-form">
        <el-form-item label="设备类型">
          <el-select v-model="searchForm.deviceType" placeholder="选择设备类型" clearable>
            <el-option label="PLC控制器" value="PLC控制器" />
            <el-option label="传感器" value="传感器" />
            <el-option label="执行器" value="执行器" />
            <el-option label="控制器" value="控制器" />
            <el-option label="采集器" value="采集器" />
          <el-select v-model="searchForm.deviceType" placeholder="选择设备类型" clearable :loading="deviceTypesLoading">
            <el-option
              v-for="type in deviceTypes"
              :key="type"
              :label="type"
              :value="type"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="设备状态">
@@ -250,6 +251,10 @@
const deviceList = ref([])
const selectedDevices = ref([])
// 设备类型列表
const deviceTypes = ref([])
const deviceTypesLoading = ref(false)
// 搜索表单
const searchForm = reactive({
  deviceType: '',
@@ -272,6 +277,37 @@
const emit = defineEmits(['device-selected', 'refresh-statistics'])
// 方法定义
// 加载设备类型列表
const loadDeviceTypes = async () => {
  if (deviceTypes.value.length > 0) {
    // 如果已经加载过,不再重复加载
    return
  }
  // 所有支持的设备类型(确保包含所有有配置组件的类型)
  const supportedTypes = ['大车设备', '大理片笼', '卧转立扫码', '卧转立']
  try {
    deviceTypesLoading.value = true
    const res = await deviceConfigApi.getDeviceTypes()
    if (res?.data && Array.isArray(res.data)) {
      // 合并数据库中的类型和支持的类型,去重并排序
      const dbTypes = res.data
      const allTypes = [...new Set([...supportedTypes, ...dbTypes])].sort()
      deviceTypes.value = allTypes
    } else {
      // 如果API返回失败,使用默认类型
      deviceTypes.value = supportedTypes
      console.warn('获取设备类型列表失败,使用默认类型')
    }
  } catch (error) {
    console.error('加载设备类型列表失败:', error)
    // 失败时使用默认类型
    deviceTypes.value = supportedTypes
  } finally {
    deviceTypesLoading.value = false
  }
}
const loadDeviceList = async () => {
  try {
    tableLoading.value = true
@@ -502,6 +538,7 @@
// 组件挂载时加载数据
onMounted(() => {
  loadDeviceTypes()
  loadDeviceList()
})
</script>
mes-web/src/views/device/DeviceConfigList.vue
@@ -4,10 +4,13 @@
    <div class="search-section">
      <el-form :model="searchForm" :inline="true" class="search-form">
        <el-form-item label="设备类型">
          <el-select v-model="searchForm.deviceType" placeholder="选择设备类型" clearable>
            <el-option label="大车设备" value="大车设备" />
            <el-option label="大理片笼" value="大理片笼" />
            <el-option label="卧式缓存" value="卧式缓存" />
          <el-select v-model="searchForm.deviceType" placeholder="选择设备类型" clearable :loading="deviceTypesLoading">
            <el-option
              v-for="type in deviceTypes"
              :key="type"
              :label="type"
              :value="type"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="设备状态">
@@ -165,6 +168,10 @@
const selectedDevices = ref([])
const plcOperationLoading = ref(false)
// 设备类型列表
const deviceTypes = ref([])
const deviceTypesLoading = ref(false)
// 搜索表单
const searchForm = reactive({
  deviceType: '',
@@ -183,6 +190,37 @@
const emit = defineEmits(['device-selected', 'refresh-statistics'])
// 方法定义
// 加载设备类型列表
const loadDeviceTypes = async () => {
  if (deviceTypes.value.length > 0) {
    // 如果已经加载过,不再重复加载
    return
  }
  // 所有支持的设备类型(确保包含所有有配置组件的类型)
  const supportedTypes = ['大车设备', '大理片笼', '卧转立扫码设备', '卧转立设备']
  try {
    deviceTypesLoading.value = true
    const res = await deviceConfigApi.getDeviceTypes()
    if (res?.data && Array.isArray(res.data)) {
      // 合并数据库中的类型和支持的类型,去重并排序
      const dbTypes = res.data
      const allTypes = [...new Set([...supportedTypes, ...dbTypes])].sort()
      deviceTypes.value = allTypes
    } else {
      // 如果API返回失败,使用默认类型
      deviceTypes.value = supportedTypes
      console.warn('获取设备类型列表失败,使用默认类型')
    }
  } catch (error) {
    console.error('加载设备类型列表失败:', error)
    // 失败时使用默认类型
    deviceTypes.value = supportedTypes
  } finally {
    deviceTypesLoading.value = false
  }
}
const loadDeviceList = async () => {
  try {
    tableLoading.value = true
@@ -481,6 +519,7 @@
// 组件挂载时加载数据
onMounted(() => {
  loadDeviceTypes()
  loadDeviceList()
})
</script>
mes-web/src/views/device/DeviceEditDialog.vue
@@ -40,11 +40,13 @@
            </el-form-item>
            <el-form-item label="设备类型" prop="deviceType">
              <el-select v-model="deviceForm.deviceType" placeholder="选择设备类型" style="width: 100%;">
                <el-option label="大车设备" value="大车设备" />
                <el-option label="大理片笼" value="大理片笼" />
                <el-option label="卧转立扫码" value="卧转立扫码" />
                <el-option label="卧转立" value="卧转立" />
              <el-select v-model="deviceForm.deviceType" placeholder="选择设备类型" style="width: 100%;" :loading="deviceTypesLoading">
                <el-option
                  v-for="type in deviceTypes"
                  :key="type"
                  :label="type"
                  :value="type"
                />
              </el-select>
            </el-form-item>
@@ -347,6 +349,10 @@
const testing = ref(false)
const testResult = ref(null)
// 设备类型列表
const deviceTypes = ref([])
const deviceTypesLoading = ref(false)
// 设备逻辑参数(根据设备类型动态显示)
const deviceLogicParams = reactive({})
@@ -447,6 +453,8 @@
watch(() => props.modelValue, (newVal) => {
  dialogVisible.value = newVal
  if (newVal) {
    // 加载设备类型列表
    loadDeviceTypes()
    if (isEdit.value && props.deviceData) {
      loadDeviceData(props.deviceData)
    } else {
@@ -490,9 +498,9 @@
  }
  if (value !== 'S7 Communication' && S7_PLC_TYPES.includes(deviceForm.plcType)) {
    ElMessage.warning('S7系列PLC通常使用S7 Communication协议,请确认协议选择是否正确')
      ElMessage.warning('S7系列PLC通常使用S7 Communication协议,请确认协议选择是否正确')
    return
  }
    }
  if (value !== 'Modbus TCP' && MODBUS_PLC_TYPES.includes(deviceForm.plcType)) {
    ElMessage.warning('Modbus 类型PLC通常使用 Modbus TCP 协议,请确认协议选择是否正确')
@@ -500,6 +508,38 @@
}
// 方法定义
// 加载设备类型列表
const loadDeviceTypes = async () => {
  if (deviceTypes.value.length > 0) {
    // 如果已经加载过,不再重复加载
    return
  }
  // 所有支持的设备类型(确保包含所有有配置组件的类型)
  const supportedTypes = ['大车设备', '大理片笼', '卧转立扫码', '卧转立']
  try {
    deviceTypesLoading.value = true
    const res = await deviceConfigApi.getDeviceTypes()
    if (res?.data && Array.isArray(res.data)) {
      // 合并数据库中的类型和支持的类型,去重并排序
      const dbTypes = res.data
      const allTypes = [...new Set([...supportedTypes, ...dbTypes])].sort()
      deviceTypes.value = allTypes
    } else {
      // 如果API返回失败,使用默认类型
      deviceTypes.value = supportedTypes
      console.warn('获取设备类型列表失败,使用默认类型')
    }
  } catch (error) {
    console.error('加载设备类型列表失败:', error)
    // 失败时使用默认类型
    deviceTypes.value = supportedTypes
    ElMessage.warning('加载设备类型列表失败,使用默认类型')
  } finally {
    deviceTypesLoading.value = false
  }
}
const parseJsonSafe = (str, defaultValue = null) => {
  if (!str) return defaultValue
  try {
mes-web/src/views/device/components/DeviceLogicConfig/LargeGlassConfig.vue
@@ -1,21 +1,14 @@
<template>
  <div class="large-glass-config">
    <el-form-item label="格子范围配置">
    <el-form-item label="笼子格子配置">
    </el-form-item>
      <div class="grid-ranges">
        <div
          v-for="(range, index) in config.gridRanges"
          :key="index"
          class="grid-range-item"
        >
          <el-input-number
            v-model="range.row"
            :min="1"
            :max="100"
            :step="1"
            style="width: 100px; margin-right: 10px;"
            placeholder="行号"
          />
          <span>行:</span>
          <span style="margin-right: 10px;">笼子{{ range.row }}:</span>
          <el-input-number
            v-model="range.start"
            :min="1"
@@ -43,11 +36,10 @@
          </el-button>
        </div>
        <el-button type="primary" size="small" @click="addGridRange">
          添加格子范围
          添加笼子
        </el-button>
      </div>
      <span class="form-tip">配置每行的格子范围,例如:第一行1~52格,第二行53~101格</span>
    </el-form-item>
      <span class="form-tip">配置每个笼子的格子范围,例如:笼子1是1~52格,笼子2是53~101格。</span>
    <el-row :gutter="20">
      <el-col :span="8">
@@ -116,11 +108,18 @@
// 监听props变化
watch(() => props.modelValue, (newVal) => {
  if (newVal && Object.keys(newVal).length > 0) {
    let gridRanges = newVal.gridRanges || [
      { row: 1, start: 1, end: 52 },
      { row: 2, start: 53, end: 101 }
    ]
    // 确保每个范围都有row字段(如果没有则自动生成)
    gridRanges = gridRanges.map((range, index) => ({
      ...range,
      row: range.row || (index + 1)
    }))
    config.value = {
      gridRanges: newVal.gridRanges || [
        { row: 1, start: 1, end: 52 },
        { row: 2, start: 53, end: 101 }
      ],
      gridRanges: gridRanges,
      gridLength: newVal.gridLength ?? 2000,
      gridWidth: newVal.gridWidth ?? 1500,
      gridThickness: newVal.gridThickness ?? 5
@@ -136,13 +135,13 @@
// 格子范围相关方法
const addGridRange = () => {
  const maxRow = config.value.gridRanges.length > 0
    ? Math.max(...config.value.gridRanges.map(r => r.row))
    ? Math.max(...config.value.gridRanges.map(r => r.row || 0))
    : 0
  const lastEnd = config.value.gridRanges.length > 0
    ? Math.max(...config.value.gridRanges.map(r => r.end))
    ? Math.max(...config.value.gridRanges.map(r => r.end || 0))
    : 0
  config.value.gridRanges.push({
    row: maxRow + 1,
    row: maxRow + 1,  // 自动生成笼子编号
    start: lastEnd + 1,
    end: lastEnd + 50
  })
mes-web/src/views/device/components/DeviceLogicConfig/LoadVehicleConfig.vue
@@ -30,15 +30,16 @@
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="玻璃间隔(ms)">
        <el-form-item label="玻璃间隔(秒)">
          <el-input-number
            v-model="config.glassIntervalMs"
            :min="100"
            :max="10000"
            :step="100"
            v-model="glassIntervalSeconds"
            :min="0.1"
            :max="10"
            :step="0.1"
            :precision="1"
            style="width: 100%;"
          />
          <span class="form-tip">玻璃上料间隔时间(毫秒)</span>
          <span class="form-tip">玻璃上料间隔时间(秒)</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
@@ -75,7 +76,7 @@
            :min="1"
            :max="1000"
            :step="1"
            style="width: 48%;"
            style="width: 40%;"
            placeholder="最小"
          />
          <span style="margin: 0 2%;">~</span>
@@ -84,7 +85,7 @@
            :min="1"
            :max="1000"
            :step="1"
            style="width: 48%;"
            style="width: 40%;"
            placeholder="最大"
          />
          <span class="form-tip">运动距离范围(格子)</span>
@@ -94,42 +95,44 @@
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="空闲监控间隔(ms)">
        <el-form-item label="空闲监控间隔(秒)">
          <el-input-number
            v-model="config.idleMonitorIntervalMs"
            :min="500"
            :max="10000"
            :step="100"
            v-model="idleMonitorIntervalSeconds"
            :min="0.5"
            :max="10"
            :step="0.1"
            :precision="1"
            style="width: 100%;"
          />
          <span class="form-tip">空闲状态监控间隔,默认2000ms</span>
          <span class="form-tip">空闲状态监控间隔,默认2秒</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="任务监控间隔(ms)">
        <el-form-item label="任务监控间隔(秒)">
          <el-input-number
            v-model="config.taskMonitorIntervalMs"
            :min="500"
            :max="10000"
            :step="100"
            v-model="taskMonitorIntervalSeconds"
            :min="0.5"
            :max="10"
            :step="0.1"
            :precision="1"
            style="width: 100%;"
          />
          <span class="form-tip">任务执行监控间隔,默认1000ms</span>
          <span class="form-tip">任务执行监控间隔,默认1秒</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="MES确认超时(ms)">
        <el-form-item label="MES确认超时(秒)">
          <el-input-number
            v-model="config.mesConfirmTimeoutMs"
            :min="5000"
            :max="300000"
            :step="1000"
            v-model="mesConfirmTimeoutSeconds"
            :min="5"
            :max="300"
            :step="1"
            style="width: 100%;"
          />
          <span class="form-tip">等待MES确认的超时时间,默认30000ms</span>
          <span class="form-tip">等待MES确认的超时时间,默认30秒</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
@@ -191,27 +194,6 @@
      </div>
      <span class="form-tip">将MES编号(如900/901)映射为实际位置值(格子)</span>
    </el-form-item>
    <el-form-item label="出片任务格子范围">
      <el-input-number
        v-model="config.outboundSlotRanges[0]"
        :min="1"
        :max="1000"
        :step="1"
        style="width: 48%;"
        placeholder="最小格子编号"
      />
      <span style="margin: 0 2%;">~</span>
      <el-input-number
        v-model="config.outboundSlotRanges[1]"
        :min="1"
        :max="1000"
        :step="1"
        style="width: 48%;"
        placeholder="最大格子编号"
      />
      <span class="form-tip">出片任务的startSlot范围,例如[1, 101]表示格子1~101都是出片任务</span>
    </el-form-item>
  </div>
</template>
@@ -241,12 +223,17 @@
  mesConfirmTimeoutMs: 30000,
  autoFeed: true,
  maxRetryCount: 5,
  positionMapping: {},
  outboundSlotRanges: [1, 101]
  positionMapping: {}
})
// 位置映射的键数组
const mappingKeys = ref([])
// 时间字段(秒)- 用于前端显示和输入
const glassIntervalSeconds = ref(1.0)
const idleMonitorIntervalSeconds = ref(2.0)
const taskMonitorIntervalSeconds = ref(1.0)
const mesConfirmTimeoutSeconds = ref(30)
// 监听props变化
watch(() => props.modelValue, (newVal) => {
@@ -264,16 +251,51 @@
      mesConfirmTimeoutMs: newVal.mesConfirmTimeoutMs ?? 30000,
      autoFeed: newVal.autoFeed ?? true,
      maxRetryCount: newVal.maxRetryCount ?? 5,
      positionMapping: newVal.positionMapping || {},
      outboundSlotRanges: newVal.outboundSlotRanges || [1, 101]
      positionMapping: newVal.positionMapping || {}
    }
    // 将毫秒转换为秒用于显示
    glassIntervalSeconds.value = (config.value.glassIntervalMs ?? 1000) / 1000
    idleMonitorIntervalSeconds.value = (config.value.idleMonitorIntervalMs ?? 2000) / 1000
    taskMonitorIntervalSeconds.value = (config.value.taskMonitorIntervalMs ?? 1000) / 1000
    mesConfirmTimeoutSeconds.value = (config.value.mesConfirmTimeoutMs ?? 30000) / 1000
    mappingKeys.value = Object.keys(config.value.positionMapping)
  }
}, { immediate: true, deep: true })
// 监听config变化,同步到父组件
watch(config, (newVal) => {
  emit('update:modelValue', { ...newVal })
// 监听秒字段变化,转换为毫秒并更新config
watch(glassIntervalSeconds, (val) => {
  config.value.glassIntervalMs = Math.round(val * 1000)
  emit('update:modelValue', { ...config.value })
})
watch(idleMonitorIntervalSeconds, (val) => {
  config.value.idleMonitorIntervalMs = Math.round(val * 1000)
  emit('update:modelValue', { ...config.value })
})
watch(taskMonitorIntervalSeconds, (val) => {
  config.value.taskMonitorIntervalMs = Math.round(val * 1000)
  emit('update:modelValue', { ...config.value })
})
watch(mesConfirmTimeoutSeconds, (val) => {
  config.value.mesConfirmTimeoutMs = Math.round(val * 1000)
  emit('update:modelValue', { ...config.value })
})
// 监听config其他字段变化,同步到父组件
watch(() => [
  config.value.vehicleCapacity,
  config.value.vehicleSpeed,
  config.value.defaultGlassLength,
  config.value.homePosition,
  config.value.minRange,
  config.value.maxRange,
  config.value.autoFeed,
  config.value.maxRetryCount,
  config.value.positionMapping
], () => {
  emit('update:modelValue', { ...config.value })
}, { deep: true })
// 位置映射相关方法
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationScannerConfig.vue
@@ -2,15 +2,15 @@
  <div class="workstation-scanner-config">
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="扫码间隔(ms)">
        <el-form-item label="扫码间隔(秒)">
          <el-input-number
            v-model="config.scanIntervalMs"
            :min="1000"
            :max="60000"
            :step="1000"
            v-model="scanIntervalSeconds"
            :min="1"
            :max="60"
            :step="1"
            style="width: 100%;"
          />
          <span class="form-tip">定时扫描MES写区的时间间隔,默认10000ms(10秒)</span>
          <span class="form-tip">定时扫描MES写区的时间间隔,默认10秒</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
@@ -23,15 +23,6 @@
            style="width: 100%;"
          />
          <span class="form-tip">产线编号,用于过滤玻璃信息</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="自动确认">
          <el-switch v-model="config.autoAck" />
          <span class="form-tip">是否自动确认MES发送的玻璃信息(回写mesSend=0)</span>
        </el-form-item>
      </el-col>
    </el-row>
@@ -53,24 +44,35 @@
// 配置数据
const config = ref({
  scanIntervalMs: 10000,
  workLine: null,
  autoAck: true
  workLine: null
})
// 时间字段(秒)- 用于前端显示和输入
const scanIntervalSeconds = ref(10)
// 监听props变化
watch(() => props.modelValue, (newVal) => {
  if (newVal && Object.keys(newVal).length > 0) {
    config.value = {
      scanIntervalMs: newVal.scanIntervalMs ?? 10000,
      workLine: newVal.workLine ?? null,
      autoAck: newVal.autoAck ?? true
      workLine: newVal.workLine ?? null
    }
    // 将毫秒转换为秒用于显示
    scanIntervalSeconds.value = (config.value.scanIntervalMs ?? 10000) / 1000
  }
}, { immediate: true, deep: true })
// 监听config变化,同步到父组件
watch(config, (newVal) => {
  emit('update:modelValue', { ...newVal })
// 监听秒字段变化,转换为毫秒并更新config
watch(scanIntervalSeconds, (val) => {
  config.value.scanIntervalMs = Math.round(val * 1000)
  emit('update:modelValue', { ...config.value })
})
// 监听config其他字段变化,同步到父组件
watch(() => [
  config.value.workLine
], () => {
  emit('update:modelValue', { ...config.value })
}, { deep: true })
</script>
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationTransferConfig.vue
@@ -2,27 +2,27 @@
  <div class="workstation-transfer-config">
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="扫码间隔(ms)">
        <el-form-item label="扫码间隔(秒)">
          <el-input-number
            v-model="config.scanIntervalMs"
            :min="1000"
            :max="60000"
            :step="1000"
            v-model="scanIntervalSeconds"
            :min="1"
            :max="60"
            :step="1"
            style="width: 100%;"
          />
          <span class="form-tip">定时查询最近扫码玻璃的时间间隔,默认10000ms(10秒)</span>
          <span class="form-tip">定时查询最近扫码玻璃的时间间隔,默认10秒</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="缓冲判定时间(ms)">
        <el-form-item label="缓冲判定时间(秒)">
          <el-input-number
            v-model="config.transferDelayMs"
            :min="5000"
            :max="120000"
            :step="1000"
            v-model="transferDelaySeconds"
            :min="5"
            :max="120"
            :step="1"
            style="width: 100%;"
          />
          <span class="form-tip">30秒内无新玻璃扫码则判定为最后一片,默认30000ms(30秒)</span>
          <span class="form-tip">30秒内无新玻璃扫码则判定为最后一片,默认30秒</span>
        </el-form-item>
      </el-col>
    </el-row>
@@ -41,15 +41,15 @@
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="监控间隔(ms)">
        <el-form-item label="监控间隔(秒)">
          <el-input-number
            v-model="config.monitorIntervalMs"
            :min="1000"
            :max="60000"
            :step="1000"
            v-model="monitorIntervalSeconds"
            :min="1"
            :max="60"
            :step="1"
            style="width: 100%;"
          />
          <span class="form-tip">批次处理监控间隔,默认使用scanIntervalMs</span>
          <span class="form-tip">批次处理监控间隔,默认使用扫码间隔</span>
        </el-form-item>
      </el-col>
    </el-row>
@@ -80,15 +80,6 @@
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="自动确认">
          <el-switch v-model="config.autoAck" />
          <span class="form-tip">是否自动确认MES发送的玻璃信息</span>
        </el-form-item>
      </el-col>
    </el-row>
  </div>
</template>
@@ -111,9 +102,13 @@
  vehicleCapacity: 6000,
  monitorIntervalMs: 10000,
  workLine: null,
  inPosition: null,
  autoAck: true
  inPosition: null
})
// 时间字段(秒)- 用于前端显示和输入
const scanIntervalSeconds = ref(10)
const transferDelaySeconds = ref(30)
const monitorIntervalSeconds = ref(10)
// 监听props变化
watch(() => props.modelValue, (newVal) => {
@@ -124,15 +119,43 @@
      vehicleCapacity: newVal.vehicleCapacity ?? 6000,
      monitorIntervalMs: newVal.monitorIntervalMs ?? newVal.scanIntervalMs ?? 10000,
      workLine: newVal.workLine ?? null,
      inPosition: newVal.inPosition ?? null,
      autoAck: newVal.autoAck ?? true
      inPosition: newVal.inPosition ?? null
    }
    // 将毫秒转换为秒用于显示
    scanIntervalSeconds.value = (config.value.scanIntervalMs ?? 10000) / 1000
    transferDelaySeconds.value = (config.value.transferDelayMs ?? 30000) / 1000
    monitorIntervalSeconds.value = (config.value.monitorIntervalMs ?? 10000) / 1000
  }
}, { immediate: true, deep: true })
// 监听config变化,同步到父组件
watch(config, (newVal) => {
  emit('update:modelValue', { ...newVal })
// 监听秒字段变化,转换为毫秒并更新config
watch(scanIntervalSeconds, (val) => {
  config.value.scanIntervalMs = Math.round(val * 1000)
  // 如果monitorIntervalMs未设置,则使用scanIntervalMs
  if (!props.modelValue?.monitorIntervalMs) {
    config.value.monitorIntervalMs = config.value.scanIntervalMs
    monitorIntervalSeconds.value = val
  }
  emit('update:modelValue', { ...config.value })
})
watch(transferDelaySeconds, (val) => {
  config.value.transferDelayMs = Math.round(val * 1000)
  emit('update:modelValue', { ...config.value })
})
watch(monitorIntervalSeconds, (val) => {
  config.value.monitorIntervalMs = Math.round(val * 1000)
  emit('update:modelValue', { ...config.value })
})
// 监听config其他字段变化,同步到父组件
watch(() => [
  config.value.vehicleCapacity,
  config.value.workLine,
  config.value.inPosition
], () => {
  emit('update:modelValue', { ...config.value })
}, { deep: true })
</script>
mes-web/src/views/device/components/DeviceLogicConfig/index.js
@@ -12,11 +12,8 @@
export const deviceTypeComponentMap = {
  '大车设备': LoadVehicleConfig,
  '大理片笼': LargeGlassConfig,
  '卧转立扫码': WorkstationScannerConfig,
  '卧转立': WorkstationTransferConfig,
  // 兼容旧名称
  '上大车': LoadVehicleConfig,
  '大理片': LargeGlassConfig
  '卧转立扫码设备': WorkstationScannerConfig,
  '卧转立设备': WorkstationTransferConfig
}
// 导出所有组件
@@ -29,6 +26,19 @@
// 根据设备类型获取对应的配置组件
export function getDeviceConfigComponent(deviceType) {
  return deviceTypeComponentMap[deviceType] || null
  if (!deviceType) {
    return null
  }
  // 去除首尾空格
  const trimmedType = deviceType.trim()
  // 直接匹配
  if (deviceTypeComponentMap[trimmedType]) {
    return deviceTypeComponentMap[trimmedType]
  }
  // 如果找不到,输出警告(开发环境)
  if (process.env.NODE_ENV === 'development') {
    console.warn(`未找到设备类型「${trimmedType}」对应的配置组件,可用类型:`, Object.keys(deviceTypeComponentMap))
  }
  return null
}
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
@@ -40,46 +40,6 @@
        </div>
      </el-form-item>
      
      <el-divider content-position="left">设备特定配置</el-divider>
      <el-form-item label="位置编码">
        <el-input
          v-model="form.positionCode"
          placeholder="例如:POS1"
          clearable
        />
        <div class="form-tip">上大车设备的位置编码</div>
      </el-form-item>
      <el-form-item label="位置值">
        <el-input-number
          v-model="form.positionValue"
          :min="0"
          :max="9999"
          placeholder="位置数值"
        />
        <div class="form-tip">上大车设备的位置数值</div>
      </el-form-item>
      <el-form-item label="存储位置">
        <el-input-number
          v-model="form.storagePosition"
          :min="1"
          :max="200"
          placeholder="存储位置编号"
        />
        <div class="form-tip">玻璃存储设备的存储位置</div>
      </el-form-item>
      <el-form-item label="处理类型">
        <el-select v-model="form.processType" placeholder="选择处理类型" clearable>
          <el-option label="标准处理" :value="1" />
          <el-option label="快速处理" :value="2" />
          <el-option label="慢速处理" :value="3" />
        </el-select>
        <div class="form-tip">大理片设备的处理类型</div>
      </el-form-item>
      <el-divider content-position="left">执行配置</el-divider>
      
      <el-form-item label="执行间隔 (ms)">
@@ -135,10 +95,6 @@
const emit = defineEmits(['task-started'])
const form = reactive({
  positionCode: '',
  positionValue: null,
  storagePosition: null,
  processType: null,
  executionInterval: 1000,
  timeoutMinutes: 30,
  retryCount: 3
@@ -260,19 +216,7 @@
      executionInterval: form.executionInterval || 1000
    }
    
    // 添加可选参数
    if (form.positionCode) {
      parameters.positionCode = form.positionCode
    }
    if (form.positionValue !== null) {
      parameters.positionValue = form.positionValue
    }
    if (form.storagePosition !== null) {
      parameters.storagePosition = form.storagePosition
    }
    if (form.processType !== null) {
      parameters.processType = form.processType
    }
    // 设备特定配置已移除,如有需要可在此扩展
    if (form.timeoutMinutes) {
      parameters.timeoutMinutes = form.timeoutMinutes
    }
@@ -296,12 +240,8 @@
        emit('task-started')
      }, 500)
      
      // 重置表单(保留部分配置),方便继续启动其他设备组
      // 重置表单(保留执行配置),方便继续启动其他设备组
      glassIdsInput.value = ''
      form.positionCode = ''
      form.positionValue = null
      form.storagePosition = null
      form.processType = null
      
      // 提示用户可以继续启动其他设备组
      ElMessage.info('可以继续选择其他设备组启动测试,多个设备组将并行执行')
@@ -329,9 +269,7 @@
    const response = await deviceInteractionApi.executeOperation({
      deviceId: loadDeviceId.value,
      operation: 'clearGlass',
      params: {
        positionCode: form.positionCode || null
      }
      params: {}
    })
    if (response?.code !== 200) {
      throw new Error(response?.message || 'PLC清空失败')