mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
@@ -5,7 +5,7 @@
        <h3>多设备测试编排</h3>
        <p v-if="group">当前设备组:{{ group.groupName }}({{ group.deviceCount || '-' }} 台设备)</p>
        <p v-else class="warning">请先在左侧选择一个设备组</p>
        <p v-if="group && loadDeviceName" class="sub-info">上大车设备:{{ loadDeviceName }}</p>
        <p v-if="group && loadDeviceName" class="sub-info">当前设备:{{ loadDeviceName }}</p>
      </div>
      <div class="action-buttons">
        <el-button
@@ -25,25 +25,29 @@
      </div>
    </div>
    <el-form :model="form" label-width="120px">
      <el-form-item label="玻璃ID列表">
    <el-form :model="form" label-width="120px" :rules="rules" ref="formRef">
      <el-form-item label="玻璃ID列表" prop="glassIds">
        <el-input
          v-model="glassIdsInput"
          type="textarea"
          :rows="4"
          placeholder="请输入玻璃条码,支持多行或逗号分隔"
          placeholder="可选:输入玻璃ID,将使用输入的ID进行测试"
          show-word-limit
          :maxlength="5000"
        />
      </el-form-item>
      <el-form-item label="位置编码">
        <el-input v-model="form.positionCode" placeholder="例如:POS1" />
      </el-form-item>
      <el-form-item label="存储位置">
        <el-input-number v-model="form.storagePosition" :min="1" :max="200" />
      </el-form-item>
      <el-form-item label="执行间隔 (ms)">
        <el-input-number v-model="form.executionInterval" :min="100" :max="10000" />
        <div class="form-tip">
          <span v-if="glassIds.length > 0">已输入 {{ glassIds.length }} 个玻璃ID(测试模式:使用输入的ID)</span>
          <span v-else>未输入玻璃ID(正常模式:将从数据库读取最近扫码的玻璃ID)</span>
        </div>
      </el-form-item>
    </el-form>
    <!-- 设备组拓扑图 -->
    <GroupTopology
      v-if="group"
      :group="group"
      class="topology-section"
    />
  </div>
</template>
@@ -53,6 +57,7 @@
import { Delete, Promotion } from '@element-plus/icons-vue'
import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask'
import { deviceGroupApi, deviceInteractionApi } from '@/api/device/deviceManagement'
import GroupTopology from '../DeviceGroup/GroupTopology.vue'
const props = defineProps({
  group: {
@@ -62,12 +67,38 @@
})
const emit = defineEmits(['task-started'])
//配置默认值
const form = reactive({})
const form = reactive({
  positionCode: '',
  storagePosition: null,
  executionInterval: 1000
})
const formRef = ref(null)
const rules = {
  glassIds: [
    {
      validator: (rule, value, callback) => {
        // 如果输入了玻璃ID,则进行验证;如果没有输入,则允许(将从数据库读取)
        if (glassIds.value.length === 0) {
          // 允许为空,将从数据库读取最近扫码的玻璃ID
          callback()
        } else if (glassIds.value.length > 100) {
          callback(new Error('玻璃ID数量不能超过100个'))
        } else {
          // 验证玻璃ID格式
          const invalidIds = glassIds.value.filter(id => {
            // 简单的格式验证:不能为空,长度在1-50之间
            return !id || id.length === 0 || id.length > 50
          })
          if (invalidIds.length > 0) {
            callback(new Error(`存在无效的玻璃ID格式,请检查`))
          } else {
            callback()
          }
        }
      },
      trigger: 'blur'
    }
  ]
}
const glassIdsInput = ref('')
const loading = ref(false)
@@ -92,6 +123,8 @@
    .filter((item) => item.length > 0)
})
const normalizeType = (type) => (type || '').trim().toUpperCase()
const fetchLoadDevice = async () => {
  loadDeviceId.value = null
  loadDeviceName.value = ''
@@ -113,9 +146,15 @@
      : Array.isArray(rawList?.data)
      ? rawList.data
      : []
    const targetDevice =
      deviceList.find((item) => (item.deviceType || '').toUpperCase() === 'LOAD_VEHICLE') ||
      deviceList[0]
    const scannerDevice = deviceList.find((item) => {
      const type = normalizeType(item.deviceType)
      return type.includes('SCANNER') || type.includes('扫码')
    })
    const loadVehicleDevice = deviceList.find((item) => {
      const type = normalizeType(item.deviceType)
      return type.includes('LOAD_VEHICLE') || type.includes('大车')
    })
    const targetDevice = scannerDevice || loadVehicleDevice || deviceList[0]
    if (targetDevice && targetDevice.id) {
      loadDeviceId.value = targetDevice.id
      loadDeviceName.value = targetDevice.deviceName || targetDevice.deviceCode || `ID: ${targetDevice.id}`
@@ -133,23 +172,49 @@
    ElMessage.warning('请先选择设备组')
    return
  }
  if (glassIds.value.length === 0) {
    ElMessage.warning('请至少输入一个玻璃ID')
  // 表单验证
  if (!formRef.value) return
  try {
    await formRef.value.validate()
  } catch (error) {
    ElMessage.warning('请检查表单输入')
    return
  }
  try {
    loading.value = true
    await multiDeviceTaskApi.startTask({
    // 构建任务参数
    // 如果输入了玻璃ID,使用输入的;如果没有输入,glassIds为空数组,后端会从数据库读取
    const parameters = {
      glassIds: glassIds.value.length > 0 ? glassIds.value : []
    }
    // 异步启动任务,立即返回,不阻塞
    const response = await multiDeviceTaskApi.startTask({
      groupId: props.group.id || props.group.groupId,
      parameters: {
        glassIds: glassIds.value,
        positionCode: form.positionCode || null,
        storagePosition: form.storagePosition,
        executionInterval: form.executionInterval
      }
      parameters
    })
    ElMessage.success('任务已启动')
    emit('task-started')
    const task = response?.data
    if (task && task.taskId) {
      ElMessage.success(`任务已启动(异步执行): ${task.taskId}`)
      emit('task-started', task)
      // 立即刷新监控列表,显示新启动的任务
      setTimeout(() => {
        emit('task-started')
      }, 500)
      // 重置表单(保留执行配置),方便继续启动其他设备组
      glassIdsInput.value = ''
      // 提示用户可以继续启动其他设备组
      ElMessage.info('可以继续选择其他设备组启动测试,多个设备组将并行执行')
    } else {
      ElMessage.warning('任务启动响应异常')
    }
  } catch (error) {
    ElMessage.error(error?.message || '任务启动失败')
  } finally {
@@ -163,17 +228,15 @@
    return
  }
  if (!loadDeviceId.value) {
    ElMessage.warning('未找到上大车设备,无法清空PLC')
    ElMessage.warning('未找到对应设备,无法清空PLC')
    return
  }
  try {
    clearLoading.value = true
    const response = await deviceInteractionApi.executeOperation({
      deviceId: loadDeviceId.value,
      operation: 'clearGlass',
      params: {
        positionCode: form.positionCode || null
      }
      operation: 'clearPlc',
      params: {}
    })
    if (response?.code !== 200) {
      throw new Error(response?.message || 'PLC清空失败')
@@ -234,5 +297,16 @@
  gap: 12px;
  align-items: center;
}
.form-tip {
  font-size: 12px;
  color: #909399;
  margin-top: 4px;
  line-height: 1.4;
}
.topology-section {
  margin-top: 24px;
}
</style>