<template>
|
<div class="task-orchestration">
|
<div class="panel-header">
|
<div>
|
<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>
|
<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" :rules="rules" ref="formRef">
|
<el-form-item label="玻璃ID列表" prop="glassIds">
|
<el-input
|
v-model="glassIdsInput"
|
type="textarea"
|
:rows="4"
|
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-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="执行间隔 (ms)">
|
<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>
|
</template>
|
|
<script setup>
|
import { computed, reactive, ref, watch } from 'vue'
|
import { ElMessage } from 'element-plus'
|
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: {
|
type: Object,
|
default: null
|
}
|
})
|
|
const emit = defineEmits(['task-started'])
|
//配置默认值
|
const form = reactive({
|
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()
|
}
|
)
|
|
const glassIds = computed(() => {
|
if (!glassIdsInput.value) return []
|
return glassIdsInput.value
|
.split(/[\n,,]/)
|
.map((item) => item.trim())
|
.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 (!formRef.value) return
|
try {
|
await formRef.value.validate()
|
} catch (error) {
|
ElMessage.warning('请检查表单输入')
|
return
|
}
|
|
try {
|
loading.value = true
|
|
// 构建任务参数
|
// 如果输入了玻璃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
|
})
|
|
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>
|
|
<style scoped>
|
.task-orchestration {
|
background: #fff;
|
border-radius: 12px;
|
padding: 20px;
|
box-shadow: 0 8px 32px rgba(15, 18, 63, 0.08);
|
}
|
|
.panel-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 16px;
|
}
|
|
.panel-header h3 {
|
margin: 0;
|
}
|
|
.panel-header p {
|
margin: 4px 0 0;
|
color: #909399;
|
font-size: 13px;
|
}
|
|
.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>
|