huang
2025-12-02 628aa6a42e587e9f337e213f87f922fc2ab2af02
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
@@ -5,30 +5,87 @@
        <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>
      </div>
      <el-button type="primary" :disabled="!group" :loading="loading" @click="handleSubmit">
        <el-icon><Promotion /></el-icon>
        启动测试
      </el-button>
      <div class="action-buttons">
        <el-button
          type="danger"
          plain
          :disabled="!group || !loadDeviceId || loadDeviceLoading"
          :loading="clearLoading"
          @click="handleClearPlc"
        >
          <el-icon><Delete /></el-icon>
          清空PLC
        </el-button>
        <el-button type="primary" :disabled="!group" :loading="loading" @click="handleSubmit">
          <el-icon><Promotion /></el-icon>
          启动测试
        </el-button>
      </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进行测试(代替卧转立扫码);如果不输入,将从数据库读取最近扫码的玻璃ID进行测试"
          show-word-limit
          :maxlength="5000"
        />
        <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-item label="位置编码">
        <el-input v-model="form.positionCode" placeholder="例如:POS1" />
      <el-divider content-position="left">执行配置</el-divider>
      <el-form-item label="单片间隔 (秒)">
        <el-input-number
          v-model="form.glassIntervalSeconds"
          :min="0"
          :max="60"
          :step="0.1"
          :precision="1"
          placeholder="每个玻璃ID之间的间隔时间"
        />
        <div class="form-tip">多个玻璃ID时,每个玻璃ID传递之间的间隔时间(秒),用于模拟玻璃每片运动的时间。0表示一次性全部传递</div>
      </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" />
        <el-input-number
          v-model="form.executionInterval"
          :min="100"
          :max="10000"
          :step="100"
          placeholder="设备操作间隔时间"
        />
        <div class="form-tip">每个设备操作之间的间隔时间(毫秒)</div>
      </el-form-item>
      <el-form-item label="超时时间 (分钟)">
        <el-input-number
          v-model="form.timeoutMinutes"
          :min="1"
          :max="60"
          :step="1"
          placeholder="任务超时时间"
        />
        <div class="form-tip">任务执行的最大超时时间</div>
      </el-form-item>
      <el-form-item label="重试次数">
        <el-input-number
          v-model="form.retryCount"
          :min="0"
          :max="10"
          :step="1"
          placeholder="失败重试次数"
        />
        <div class="form-tip">设备操作失败时的最大重试次数</div>
      </el-form-item>
    </el-form>
  </div>
@@ -37,8 +94,9 @@
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Promotion } from '@element-plus/icons-vue'
import { Delete, Promotion } from '@element-plus/icons-vue'
import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask'
import { deviceGroupApi, deviceInteractionApi } from '@/api/device/deviceManagement'
const props = defineProps({
  group: {
@@ -48,20 +106,56 @@
})
const emit = defineEmits(['task-started'])
//配置默认值
const form = reactive({
  positionCode: '',
  storagePosition: null,
  executionInterval: 1000
  glassIntervalSeconds: 10, // 单片间隔,默认10秒
  executionInterval: 1000,
  timeoutMinutes: 1,
  retryCount: 3
})
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)
const clearLoading = ref(false)
const loadDeviceId = ref(null)
const loadDeviceName = ref('')
const loadDeviceLoading = ref(false)
watch(
  () => props.group,
  () => {
    glassIdsInput.value = ''
    fetchLoadDevice()
  }
)
@@ -73,32 +167,150 @@
    .filter((item) => item.length > 0)
})
const normalizeType = (type) => (type || '').trim().toUpperCase()
const fetchLoadDevice = async () => {
  loadDeviceId.value = null
  loadDeviceName.value = ''
  if (!props.group) {
    return
  }
  const groupId = props.group.id || props.group.groupId
  if (!groupId) {
    return
  }
  loadDeviceLoading.value = true
  try {
    const response = await deviceGroupApi.getGroupDevices(groupId)
    const rawList = response?.data
    const deviceList = Array.isArray(rawList)
      ? rawList
      : Array.isArray(rawList?.records)
      ? rawList.records
      : Array.isArray(rawList?.data)
      ? rawList.data
      : []
    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}`
    }
  } catch (error) {
    console.error('加载设备信息失败:', error)
    ElMessage.error(error?.message || '获取设备信息失败')
  } finally {
    loadDeviceLoading.value = false
  }
}
const handleSubmit = async () => {
  if (!props.group) {
    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 glassIntervalMs = form.glassIntervalSeconds != null && form.glassIntervalSeconds !== undefined
      ? Math.round(form.glassIntervalSeconds * 1000)
      : 1000
    const parameters = {
      glassIds: glassIds.value.length > 0 ? glassIds.value : [],
      glassIntervalMs: glassIntervalMs,
      executionInterval: form.executionInterval || 1000
    }
    // 设备特定配置已移除,如有需要可在此扩展
    if (form.timeoutMinutes) {
      parameters.timeoutMinutes = form.timeoutMinutes
    }
    if (form.retryCount !== null) {
      parameters.retryCount = form.retryCount
    }
    // 异步启动任务,立即返回,不阻塞
    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 {
    loading.value = false
  }
}
const handleClearPlc = async () => {
  if (!props.group) {
    ElMessage.warning('请先选择设备组')
    return
  }
  if (!loadDeviceId.value) {
    ElMessage.warning('未找到对应设备,无法清空PLC')
    return
  }
  try {
    clearLoading.value = true
    const response = await deviceInteractionApi.executeOperation({
      deviceId: loadDeviceId.value,
      operation: 'clearPlc',
      params: {}
    })
    if (response?.code !== 200) {
      throw new Error(response?.message || 'PLC清空失败')
    }
    const result = response?.data
    if (result?.success) {
      ElMessage.success(result?.message || 'PLC已清空')
      glassIdsInput.value = ''
    } else {
      throw new Error(result?.message || 'PLC清空失败')
    }
  } catch (error) {
    console.error('清空PLC失败:', error)
    ElMessage.error(error?.message || 'PLC清空失败')
  } finally {
    clearLoading.value = false
  }
}
</script>
@@ -131,5 +343,24 @@
.panel-header .warning {
  color: #f56c6c;
}
.panel-header .sub-info {
  margin-top: 4px;
  color: #606266;
  font-size: 12px;
}
.action-buttons {
  display: flex;
  gap: 12px;
  align-items: center;
}
.form-tip {
  font-size: 12px;
  color: #909399;
  margin-top: 4px;
  line-height: 1.4;
}
</style>