<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进行测试"
|
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>
|
|
<!-- 设备组拓扑图 -->
|
<GroupTopology
|
v-if="group"
|
:group="group"
|
class="topology-section"
|
/>
|
</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'
|
import GroupTopology from '../DeviceGroup/GroupTopology.vue'
|
|
const props = defineProps({
|
group: {
|
type: Object,
|
default: null
|
}
|
})
|
|
const emit = defineEmits(['task-started'])
|
//配置默认值
|
const form = reactive({})
|
|
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 parameters = {
|
glassIds: glassIds.value.length > 0 ? glassIds.value : []
|
}
|
|
// 异步启动任务,立即返回,不阻塞
|
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;
|
}
|
|
.topology-section {
|
margin-top: 24px;
|
}
|
</style>
|