CMVR-BID/src/views/intelligenceTest/runningTask/index.vue
lixiaolong f944d33d58 feat(flow): 添加机械臂使能控制节点并优化任务执行日志显示
- 新增开启机械臂使能和关闭机械臂使能两个服务节点
- 在机器人设备工具类中注册ARM_ENABLE和ARM_DISABLE操作类型
- 优化运行任务页面中的状态判断逻辑,支持多种终端状态
- 为任务日志组件增加任务状态属性传递
- 增强日志组件中检测项状态推导功能,支持重复执行和分析状态显示
- 添加执行上下文解析以正确处理重复检测项的日志归类
- 改进日志详情过滤,排除执行上下文相关字段
- 实现检测项完成状态和分析结果的正确标识与展示
2026-08-18 11:19:52 +08:00

812 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page-container">
<div class="left">
<div class="header">
<div>任务队列</div>
<el-button type="primary" @click="addTask">添加任务</el-button>
</div>
<div class="taskList-container">
<div class="task-item" v-for="task in runTaskList">
<div class="task-content">
<div class="task-title">{{ task.taskName }}</div>
<div class="task-actions">
<SvgIcon class="icon" v-if="task.status == '1'" name="pause" :size="24" color="#FFB300"
@click="pauseTask(task)" />
<SvgIcon class="icon" v-if="task.status == '3' || task.status == '5'" name="start"
:size="24" color="#00FF88" @click="startTask(task)" />
<SvgIcon class="icon" v-if="task.status != '5'" name="stop" :size="24" color="#d81e06"
@click="stopTask(task)" />
<SvgIcon class="icon" v-if="task.status == '5'" name="remove" :size="24" color="#FFB300"
@click="removeTask(task)" />
</div>
</div>
<div class="robot">手机:{{ task.phoneName }}</div>
<div class="robot">车辆:{{ task.vehicleName }}</div>
<el-progress :color="customColorMethod"
:percentage="parseFloat(taskRunningInfo[task.id]?.progress?.toFixed(2)) || 0" />
</div>
</div>
</div>
<div class="right" ref="topContainerRef">
<div class="header">
<div class="label" :class="{ active: activePanel === 'current' }" @click="activePanel = 'current'">当前任务
</div>
<div class="label" :class="{ active: activePanel === 'history' }" @click="activePanel = 'history'">历史记录
</div>
</div>
<div class="context-container">
<div v-if="activePanel === 'current'">
<!-- 当前任务内容 -->
<div class="task" v-for="runTask in runingTaskList" :key="runTask.id"
:data-task-id="runTask.id" :class="{ 'task--selected': selectedTaskId === runTask.id }"
@click="selectedTaskId = runTask.id">
<div class="task-header">
<div class="header-container">
<div class="task-info">
<div class="title">{{ runTask.taskName }}</div>
<div class="id">ID: {{ runTask.taskId }}</div>
</div>
<div class="robot">
<div>手机:{{ runTask.phoneName }}</div>
<div>车辆:{{ runTask.vehicleName }}</div>
<div>开始时间: {{ formatDate(runTask.startTime) }}</div>
</div>
</div>
<div>
<el-tag :type="runTask.status === '3' ? 'primary' : 'warning'">
{{ taskStatusMap[runTask.status] || '未知状态' }}
</el-tag>
</div>
</div>
<div class="progress-label">执行进度</div>
<el-progress :color="customColorMethod"
:percentage="parseFloat(taskRunningInfo[runTask.id]?.progress?.toFixed(2)) || 0" />
<taskSteps :list="pointList_runningTask[runTask.taskId]"
:activeStep="taskRunningInfo[runTask.id]?.activeStep || 0" />
<div class="run-log-container">
<div>执行日志</div>
<!-- 传入任务定义 ID,task-log 会据此查询检测项并按检测项归类当前实例日志。 -->
<taskLog :height="460" :task-id="runTask.taskId" :task-status="runTask.status"
:list="taskLogList[runTask.id]" />
</div>
</div>
</div>
<div v-else-if="activePanel === 'history'">
<div :style="containerHeight">
<el-table v-loading="loading" height="100%" :data="tableDataList">
<el-table-column align="center" label="任务名称" prop="taskName" show-overflow-tooltip />
<el-table-column align="center" label="任务状态" prop="status">
<template #default="scope">
<el-tag :type="scope.row.status === '0' ? 'success' : 'danger'">
{{ taskStatusMap[scope.row.status] || '未知状态' }}
</el-tag>
</template>
</el-table-column>
<el-table-column align="center" label="任务类型" prop="taskType">
<template #default="scope">
<el-tag :type="scope.row.taskType === '1' ? 'primary' : 'success'">
{{ scope.row.taskType === '1' ? '巡检任务' : '讲解任务' }}
</el-tag>
</template>
</el-table-column>
<el-table-column align="center" label="机器人" prop="robotName" show-overflow-tooltip />
<el-table-column align="center" label="开始时间" prop="startTime">
<template #default="scope">
{{ formatDate(scope.row.startTime) }}
</template>
</el-table-column>
<el-table-column align="center" label="结束时间" prop="endTime">
<template #default="scope">
{{ formatDate(scope.row.endTime) }}
</template>
</el-table-column>
<el-table-column align="center" class-name="small-padding fixed-width" label="操作"
width="260">
<template #default="scope">
<el-button icon="Edit" link type="primary" @click="handleOpenLog(scope.row)">
日志
</el-button>
<el-button v-if="['0', '2'].includes(scope.row.status)" icon="RefreshRight" link
type="primary" :loading="rerunningId === scope.row.id" @click="handleRerun(scope.row)">
再次执行
</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNum" :total="total" @pagination="getHistoryList" />
</div>
</div>
</div>
</div>
<!-- 添加任务弹窗 -->
<el-dialog title="添加任务" v-model="open" width="500px" append-to-body>
<el-form ref="runTaskRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="选择任务" prop="taskId">
<el-select v-model="form.taskId" placeholder="请选择任务" clearable style="width: 100%">
<el-option v-for="task in taskList" :key="task.id" :label="task.taskName" :value="task.id" />
</el-select>
</el-form-item>
<el-form-item label="选择手机" prop="phoneId">
<el-select v-model="form.phoneId" placeholder="请选择手机" clearable style="width: 100%">
<el-option v-for="phone in phoneList" :key="phone.id" :label="phone.phoneName"
:value="phone.id" />
</el-select>
</el-form-item>
<el-form-item label="选择车辆" prop="vehicleId">
<el-select v-model="form.vehicleId" placeholder="请选择车辆" clearable style="width: 100%">
<el-option v-for="vehicle in carList" :key="vehicle.id" :label="vehicle.vehicleName"
:value="vehicle.id" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input type="textarea" v-model="form.remark" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="confirmTaskDialog">确 定</el-button>
<el-button @click="cancelTaskDialog">取 消</el-button>
</div>
</template>
</el-dialog>
<el-dialog title="任务执行日志" v-model="logDialogVisible" width="800px" append-to-body>
<!-- 历史弹窗也传入对应任务定义 ID,确保没有实时进度数据时仍能展示完整检测项列表。 -->
<taskLog v-loading="logDialogLoading" :task-id="logDialogTaskId" :task-status="logDialogTaskStatus"
:list="logDialogData" :height="500" />
</el-dialog>
<el-dialog title="本次执行安排" v-model="startDialogVisible" width="820px" append-to-body>
<div class="start-dialog-content">
<div class="start-dialog-tip">手机和车辆沿用任务配置,这里只安排执行流程的机器人和设备。</div>
<div class="start-section-title">机器人和设备</div>
<RuntimeAssignmentsEditor ref="runtimeEditorRef" v-model="runtimeAssignments" :roles="runtimeRoles" />
<div class="start-section-title start-section-title--params">运行参数</div>
<RuntimeParamsEditor ref="runtimeParamsEditorRef" :items="runtimeParamItems" />
</div>
<template #footer>
<el-button @click="startDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="startSubmitting" @click="confirmStartTask">开始执行</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import SvgIcon from "@/components/SvgIcon";
import taskSteps from "./task-steps.vue";
import taskLog from "./task-log.vue";
import { fetchAllPagedRows } from '@/utils/pagedRequest'
import { useContainerHeight } from "@/hooks/tableHeight";
import { getTaskList, getTestCaseByTaskId } from "@/api/intelligenceTest/taskManage";
import { getPhoneList } from "@/api/intelligenceTest/mobilePhone";
import { getVehicleList } from "@/api/intelligenceTest/car";
import { onMounted, onUnmounted } from "vue";
import {
addTaskinstance,
getTaskinstanceList,
startTaskinstance,
rerunTaskinstance,
pauseTaskinstance,
stopTaskinstance,
resumeTaskinstance,
deleteTaskinstance,
getTaskLogListApi
} from "@/api/intelligenceTest/runningTask";
import { ElMessage, ElMessageBox } from "element-plus";
import { formatDate } from "@/utils/index";
import { debounce } from 'lodash'; // 引入 lodash 的防抖函数
import RuntimeAssignmentsEditor from '@/views/flow/components/RuntimeAssignmentsEditor.vue'
import RuntimeParamsEditor from '@/views/flow/components/RuntimeParamsEditor.vue'
import { getFlowParamsApi } from '@/api/device/terminal.js'
import {
createRuntimeAssignments,
firstAssignedRobotId,
loadFlowItemScopedRuntimeRoles,
} from '@/utils/flowExecution'
const topContainerRef = ref();
const containerHeight = useContainerHeight(topContainerRef);
const socket = inject('ws');
const startDialogVisible = ref(false)
const startSubmitting = ref(false)
const pendingStartTask = ref(null)
const runtimeRoles = ref([])
const runtimeAssignments = ref([])
const runtimeEditorRef = ref()
const runtimeParamItems = ref([])
const runtimeParamsEditorRef = ref()
const selectedTaskId = ref('')
const rerunningId = ref('')
const taskStatusMap = {
'0': '已完成',
'1': '执行中',
'2': '失败',
'3': '已暂停',
'4': '已终止',
'5': '未执行',
}
const form = reactive({
taskId: null,
});
const rules = reactive({
taskId: [{ required: true, message: '请选择任务', trigger: 'change' }],
phoneId: [{ required: true, message: '请选择手机', trigger: 'change' }],
vehicleId: [{ required: true, message: '请选择车辆', trigger: 'change' }],
});
const taskList = ref([]);
const phoneList = ref([]);
const runTaskList = ref([]);
const normalizeTaskStatus = (tasks = []) => tasks.map(item => ({
...item,
status: String(item.status),
}))
const getTaskData = () => {
getTaskList({
pageNum: 1,
pageSize: 10000,
}).then((res) => {
taskList.value = res.rows;
})
}
const getPhoneData = (mapId = null) => {
getPhoneList({
pageNum: 1,
pageSize: 10000
}).then((res) => {
phoneList.value = res.rows;
})
}
const carList = ref([])
const getCarData = (mapId = null) => {
getVehicleList({
pageNum: 1,
pageSize: 10000
}).then((res) => {
carList.value = res.rows;
})
}
const open = ref(false);
const addTask = () => {
open.value = true;
}
const runTaskRef = ref();
/**
* 确定添加执行任务
*/
const confirmTaskDialog = () => {
runTaskRef.value.validate((valid) => {
if (valid) {
addTaskinstance(form).then(() => {
ElMessage.success("任务添加成功");
getList();
open.value = false;
runTaskRef.value.resetFields();
}).catch(() => {
ElMessage.error("任务添加失败");
})
}
});
}
const cancelTaskDialog = () => {
open.value = false;
}
const runingTaskList = ref([])
/**
* 获取任务队列
*/
const getList = () => {
return getTaskinstanceList({
statusList: ['1', '3', '5'],
}).then((res) => {
if (res.code === 200) {
const tasks = normalizeTaskStatus(res.rows)
runTaskList.value = tasks;
runingTaskList.value = tasks.filter(item => item.status !== '5')
for (let i = 0; i < runTaskList.value.length; i++) {
getCaseByTaskId(runTaskList.value[i].taskId)
getTaskLogList(runTaskList.value[i].id)
}
}
})
}
const handleRerun = async (task) => {
rerunningId.value = task.id
try {
const res = await rerunTaskinstance(task.id)
if (res.code === 200) {
selectedTaskId.value = String(res.data || '')
activePanel.value = 'current'
await getList()
await nextTick()
document.querySelector(`[data-task-id="${selectedTaskId.value}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
ElMessage.success('已按上次参数创建并启动新任务')
}
} finally {
rerunningId.value = ''
}
}
const pointList_runningTask = ref({})
/**
* 获取正在执行任务的用例列表
* @param taskId
*/
const getCaseByTaskId = async (taskId) => {
const res = await getTestCaseByTaskId(taskId)
if (res.code === 200) {
pointList_runningTask.value[taskId] = res.data
}
}
const taskLogList = ref({})
/**
* 获取正在执行任务的执行日志
* @param taskInstanceId
*/
const getTaskLogList = async (taskInstanceId) => {
const res = await fetchAllPagedRows(getTaskLogListApi, { taskInstanceId })
if (res.code === 200) {
taskLogList.value[taskInstanceId] = res.rows
}
}
/**
* 自定义进度条值不同区间所显示的颜色
* @param percentage
*/
const customColorMethod = (percentage) => {
if (percentage < 30) {
return '#6f7ad3'
}
if (percentage < 70) {
return '#1989fa'
}
return '#67c23a'
}
const activePanel = ref('current');
const tableDataList = ref([]);
const loading = ref(false);
const total = ref(0);
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
});
/**
* 开始执行任务
* @param task
*/
const startTask = async (task) => {
if (String(task.status) === '3') {
let res = await resumeTaskinstance(task.id);
if (res.code === 200) {
ElMessage.success("任务已恢复");
getList();
}
} else {
const taskDefinition = taskList.value.find(item => item.id === task.taskId)
if (!taskDefinition?.taskConfigId) {
ElMessage.warning('当前任务没有关联流程编排')
return
}
try {
const paramsResponse = await getFlowParamsApi(taskDefinition.taskConfigId)
const paramItems = paramsResponse.rows || []
const roles = await loadFlowItemScopedRuntimeRoles(paramItems)
runtimeAssignments.value = createRuntimeAssignments(roles)
runtimeRoles.value = roles
runtimeParamItems.value = paramItems
pendingStartTask.value = task
startDialogVisible.value = true
} catch (error) {
ElMessage.error(error.message || '工作流执行资源加载失败')
}
}
}
const confirmStartTask = async () => {
const assignmentError = runtimeEditorRef.value?.validate()
if (assignmentError) {
ElMessage.warning(assignmentError)
return
}
const paramsValid = await runtimeParamsEditorRef.value?.validate()
if (paramsValid === false) {
ElMessage.warning('请完整填写运行参数')
return
}
startSubmitting.value = true
try {
const robotId = firstAssignedRobotId(runtimeAssignments.value)
const res = await startTaskinstance(pendingStartTask.value.id, {
robotId,
runtimeAssignments: runtimeAssignments.value,
runParams: runtimeParamsEditorRef.value?.getRunParams() || {}
})
if (res.code === 200) {
ElMessage.success('任务已开始')
startDialogVisible.value = false
getList()
}
} finally {
startSubmitting.value = false
}
}
/**
* 暂停执行任务
* @param task
*/
const pauseTask = async (task) => {
let res = await pauseTaskinstance(task.id);
if (res.code === 200) {
ElMessage.success("任务已暂停");
getList();
}
}
/**
* 停止执行任务
* @param task
*/
const stopTask = async (task) => {
let res = await stopTaskinstance(task.id);
if (res.code === 200) {
ElMessage.success("任务已停止");
getList();
}
}
/**
* 删除执行任务
* @param task
*/
const removeTask = async (task) => {
ElMessageBox.confirm('确定要删除该任务吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
let res = await deleteTaskinstance(task.id);
if (res.code === 200) {
ElMessage.success("任务已删除");
getList();
}
}).catch(() => {
// 取消删除
});
}
/**
* 获取历史执行任务
*/
const getHistoryList = () => {
loading.value = true;
getTaskinstanceList({
statusList: ['0', '2', '4'],
pageNum: queryParams.pageNum,
pageSize: queryParams.pageSize,
}).then((res) => {
if (res.code === 200) {
tableDataList.value = normalizeTaskStatus(res.rows);
total.value = res.total;
}
loading.value = false;
}).catch(() => {
loading.value = false;
});
}
const taskRunningInfo = ref({})
const socketChannelCallbacks = ref({})
const channel = 'AimaTaskInstance';
/**
* 订阅websocket
*/
const subscribeDebounced = debounce(async (sub) => {
if (!socket) {
console.warn(`执行任务订阅失败: socket=${!!socket}`);
return;
}
socket.send({
type: 'channel_subscription',
action: sub ? 'subscribe' : 'unsubscribe',
channel,
});
if (sub) {
await nextTick();
// 清理旧回调
if (socketChannelCallbacks.value[channel]) {
socket.off(channel, socketChannelCallbacks.value[channel]);
}
const callback = (data) => {
if (data !== '500') {
const status = Number(data.status)
const terminal = [0, 2, 4].includes(status)
if (!terminal) {
const steps = pointList_runningTask.value[data.taskId] || []
let activeStep = Number.isInteger(data.itemOrder) && data.itemOrder > 0
? data.itemOrder - 1
: Math.max(steps.findIndex(item => item.detectItemId === data.itemId), 0)
if (data.progress == 100) {
activeStep += 1
}
taskRunningInfo.value[data.taskInstanceId] = {
...data,
activeStep
}
if (!taskLogList.value[data.taskInstanceId]) {
taskLogList.value[data.taskInstanceId] = []
}
taskLogList.value[data.taskInstanceId].push({
...data
})
} else {
delete taskRunningInfo.value[data.taskInstanceId]
delete taskLogList.value[data.taskInstanceId]
getList()
getHistoryList()
}
} else {
// 错误处理:取消订阅后重新订阅
subscribeDebounced(false);
subscribeDebounced(true);
}
};
socket.on(channel, callback);
socketChannelCallbacks.value[channel] = callback;
} else {
if (socketChannelCallbacks.value[channel]) {
socket.off(channel, socketChannelCallbacks.value[channel]);
delete socketChannelCallbacks.value[channel];
}
}
}, 300);
const logDialogVisible = ref(false)
const logDialogData = ref([])
// 历史表格行里的 taskId 是任务定义 ID;日志组件使用它查询本次任务包含的全部检测项。
const logDialogTaskId = ref('')
const logDialogTaskStatus = ref(null)
const logDialogLoading = ref(false)
let logDialogRequestId = 0
const handleOpenLog = async (row) => {
const requestId = ++logDialogRequestId
logDialogVisible.value = true
// 在请求实例日志前先更新 taskId,让检测项请求与日志请求并行执行,减少弹窗等待时间。
logDialogTaskId.value = row.taskId
logDialogTaskStatus.value = row.status
logDialogData.value = []
logDialogLoading.value = true
try {
const res = await fetchAllPagedRows(getTaskLogListApi, { taskInstanceId: row.id })
if (requestId === logDialogRequestId && res.code === 200) {
logDialogData.value = res.rows
}
} finally {
if (requestId === logDialogRequestId) logDialogLoading.value = false
}
}
const refreshAfterSocketReconnect = () => {
getList()
getHistoryList()
}
onMounted(() => {
getTaskData();
getPhoneData();
getCarData()
getList();
getHistoryList();
subscribeDebounced(true)
socket?.on('open', refreshAfterSocketReconnect)
})
onUnmounted(() => {
subscribeDebounced(false)
socket?.off('open', refreshAfterSocketReconnect)
})
</script>
<style lang="scss" scoped>
.page-container {
width: 100%;
height: 100%;
display: flex;
gap: 12px;
.left {
width: 25%;
height: 100%;
background: #fff;
padding: 16px;
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16px;
border-bottom: 1px solid #eee;
}
.taskList-container {
margin-top: 16px;
height: calc(100% - 100px);
overflow-y: auto;
.task-item {
padding: 12px;
background: #fafafa;
box-shadow: inset 0 0 0 1px #eee;
border-radius: 8px;
margin-bottom: 16px;
.task-content {
display: flex;
justify-content: space-between;
align-items: center;
.task-title {
font-size: 16px;
font-weight: bold;
}
.task-actions {
display: flex;
gap: 8px;
.icon {
cursor: pointer;
}
}
}
.robot {
margin-top: 8px;
color: #666;
}
}
}
}
.right {
flex: 1;
padding: 16px;
background: #fff;
overflow: hidden;
.header {
display: flex;
margin-bottom: 16px;
border-bottom: 1px solid #eee;
.label {
font-size: 16px;
font-weight: bold;
color: #333;
cursor: pointer;
box-shadow: inset 0 0 0 1px #eee;
padding: 10px;
}
.active {
color: #409eff;
box-shadow: inset 0 0 0 1px #409eff;
}
}
.context-container {
height: calc(100% - 40px);
overflow: auto;
}
.task {
padding: 16px;
background: #fafafa;
border-radius: 20px;
margin-bottom: 12px;
&--selected {
box-shadow: inset 0 0 0 2px var(--el-color-primary);
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
.task-info {
display: flex;
gap: 40px;
.title {
font-size: 18px;
font-weight: bold;
}
.id {
color: #666;
font-size: 14px;
margin: 8px 0;
}
}
.robot {
display: flex;
align-items: center;
gap: 50px;
color: #666;
font-size: 14px;
}
}
.progress-label {
margin-top: 12px;
color: #666;
font-size: 14px;
font-weight: bold;
}
}
}
}
.start-dialog-tip {
margin-bottom: 12px;
padding: 10px 12px;
border-left: 3px solid var(--el-color-primary);
background: var(--el-fill-color-light);
color: var(--el-text-color-secondary);
font-size: 13px;
}
.start-dialog-content {
max-height: 65vh;
padding-right: 6px;
overflow-y: auto;
}
.start-section-title {
margin: 16px 0 10px;
color: var(--el-text-color-primary);
font-size: 15px;
font-weight: 600;
}
.start-section-title--params {
margin-top: 22px;
}
</style>