feat: 按选中任务展示运行详情

This commit is contained in:
zhanghao 2026-08-18 13:32:49 +08:00
parent 239065a7d3
commit 6a24cd17cc
2 changed files with 115 additions and 56 deletions

View File

@ -6,18 +6,20 @@
<el-button type="primary" @click="addTask">添加任务</el-button>
</div>
<div class="taskList-container">
<div class="task-item" v-for="task in runTaskList">
<div v-for="task in runTaskList" :key="task.id" class="task-item"
:class="{ 'is-selected': isSelectedTask(task) }"
:aria-current="isSelectedTask(task) ? 'true' : undefined" @click="selectTask(task)">
<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)" />
@click.stop="pauseTask(task)" />
<SvgIcon class="icon" v-if="task.status === '3' || task.status === '5'" name="start"
:size="24" color="#00FF88" @click="startTask(task)" />
:size="24" color="#00FF88" @click.stop="startTask(task)" />
<SvgIcon class="icon" v-if="task.status != '5'" name="stop" :size="24" color="#d81e06"
@click="stopTask(task)" />
@click.stop="stopTask(task)" />
<SvgIcon class="icon" v-if="task.status === '5'" name="remove" :size="24" color="#FFB300"
@click="removeTask(task)" />
@click.stop="removeTask(task)" />
</div>
</div>
<div class="robot">主要机器人:{{ task.robotName || '执行时安排' }}</div>
@ -35,38 +37,38 @@
</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 v-if="selectedRunTask" :key="selectedRunTask.id" class="task"
:data-task-id="selectedRunTask.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 class="title">{{ selectedRunTask.taskName }}</div>
<div class="id">ID: {{ selectedRunTask.taskId }}</div>
</div>
<div class="robot">
<div>主要机器人:{{ runTask.robotName || '按流程角色安排' }}</div>
<div>开始时间: {{ formatDate(runTask.startTime) }}</div>
<div>主要机器人:{{ selectedRunTask.robotName || '按流程角色安排' }}</div>
<div>开始时间: {{ formatDate(selectedRunTask.startTime) }}</div>
</div>
</div>
<div>
<el-tag :type="runTask.status === '3' ? 'primary' : 'warning'">
{{ taskStatusMap[runTask.status] || '未知状态' }}
<el-tag :type="selectedRunTask.status === '3' ? 'primary' : 'warning'">
{{ taskStatusMap[selectedRunTask.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" />
:percentage="parseFloat(taskRunningInfo[selectedRunTask.id]?.progress?.toFixed(2)) || 0" />
<taskSteps :list="pointList_runningTask[selectedRunTask.taskId]"
:activeStep="taskRunningInfo[selectedRunTask.id]?.activeStep || 0" />
<div class="run-log-container">
<div>执行日志</div>
<taskLog :list="taskLogList[runTask.id]" />
<taskLog :list="taskLogList[selectedRunTask.id]" />
</div>
</div>
<el-empty v-else description="暂无任务" />
</div>
<div v-else-if="activePanel === 'history'">
<div :style="containerHeight">
@ -170,7 +172,7 @@ import taskSteps from "./task-steps.vue";
import taskLog from "./task-log.vue";
import { useContainerHeight } from "@/hooks/tableHeight";
import { getTaskList, getWaypointByTaskId } from "@/api/inspection/task";
import { onMounted, onUnmounted } from "vue";
import { computed, onMounted, onUnmounted } from "vue";
import { addRunTask, getRunTaskList, startRunTask, rerunRunTask, pauseRunTask, stopRunTask, resumeRunTask, deleteRunTask, getTaskLogListApi } from "@/api/inspection/runTask";
import { ElMessage, ElMessageBox } from "element-plus";
import { formatDate } from "@/utils/index";
@ -220,6 +222,23 @@ const rules = reactive({
const taskList = ref([]);
const runTaskList = ref([]);
// 接口和 WebSocket 可能分别以数字、字符串返回任务实例 ID,统一转换后再判断是否为选中任务。
const isSelectedTask = (task) => String(task?.id ?? '') === String(selectedTaskId.value)
// 当前任务详情直接从完整任务队列中取得,因此未执行、执行中和已暂停任务都可以被选中查看。
const selectedRunTask = computed(() => runTaskList.value.find(isSelectedTask) || null)
/**
* 点击左侧任务卡片时切换右侧当前任务详情,并从历史记录面板返回当前任务面板。
* 卡片内的启动、暂停、停止和删除图标均使用 .stop 阻止冒泡,执行操作时不会意外改变当前选择。
*
* @param {Object} task 左侧任务队列中被点击的任务实例
*/
const selectTask = (task) => {
selectedTaskId.value = task.id
activePanel.value = 'current'
}
const normalizeTaskStatus = (tasks = []) => tasks.map(item => ({
...item,
status: String(item.status),
@ -264,9 +283,12 @@ const cancelTaskDialog = () => {
}
const runingTaskList = ref([])
/**
* 获取任务队列
* 获取任务队列并维护当前选中的任务:
* 1. 页面首次加载时默认选中队列第一条任务;
* 2. 队列刷新后,如果原任务仍存在,则保留用户当前选择;
* 3. 如果原任务已经结束或被删除,则选择刷新后队列中的第一条任务;
* 4. 如果任务队列为空,则清空选择并由模板展示空状态。
*/
const getList = () => {
return getRunTaskList({
@ -275,7 +297,9 @@ const getList = () => {
if (res.code === 200) {
const tasks = normalizeTaskStatus(res.rows)
runTaskList.value = tasks;
runingTaskList.value = tasks.filter(item => item.status !== '5')
if (!tasks.some(isSelectedTask)) {
selectedTaskId.value = tasks[0]?.id ?? ''
}
for (let i = 0; i < runTaskList.value.length; i++) {
getPointByTaskId(runTaskList.value[i].taskId)
getTaskLogList(runTaskList.value[i].id)
@ -618,6 +642,17 @@ onUnmounted(() => {
box-shadow: inset 0 0 0 1px #eee;
border-radius: 8px;
margin-bottom: 16px;
cursor: pointer;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
&:hover {
background: var(--el-fill-color-light);
}
&.is-selected {
background: var(--el-color-primary-light-9);
box-shadow: inset 0 0 0 2px var(--el-color-primary);
}
.task-content {
display: flex;
@ -684,10 +719,6 @@ onUnmounted(() => {
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;

View File

@ -6,18 +6,20 @@
<el-button type="primary" @click="addTask">添加任务</el-button>
</div>
<div class="taskList-container">
<div class="task-item" v-for="task in runTaskList">
<div v-for="task in runTaskList" :key="task.id" class="task-item"
:class="{ 'is-selected': isSelectedTask(task) }"
:aria-current="isSelectedTask(task) ? 'true' : undefined" @click="selectTask(task)">
<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)" />
@click.stop="pauseTask(task)" />
<SvgIcon class="icon" v-if="task.status == '3' || task.status == '5'" name="start"
:size="24" color="#00FF88" @click="startTask(task)" />
:size="24" color="#00FF88" @click.stop="startTask(task)" />
<SvgIcon class="icon" v-if="task.status != '5'" name="stop" :size="24" color="#d81e06"
@click="stopTask(task)" />
@click.stop="stopTask(task)" />
<SvgIcon class="icon" v-if="task.status == '5'" name="remove" :size="24" color="#FFB300"
@click="removeTask(task)" />
@click.stop="removeTask(task)" />
</div>
</div>
<div class="robot">手机:{{ task.phoneName }}</div>
@ -36,44 +38,44 @@
</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 v-if="selectedRunTask" :key="selectedRunTask.id" class="task"
:data-task-id="selectedRunTask.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 class="title">{{ selectedRunTask.taskName }}</div>
<div class="id">ID: {{ selectedRunTask.taskId }}</div>
</div>
<div class="robot">
<div>手机:{{ runTask.phoneName }}</div>
<div>车辆:{{ runTask.vehicleName }}</div>
<div>开始时间: {{ formatDate(runTask.startTime) }}</div>
<div>手机:{{ selectedRunTask.phoneName }}</div>
<div>车辆:{{ selectedRunTask.vehicleName }}</div>
<div>开始时间: {{ formatDate(selectedRunTask.startTime) }}</div>
</div>
</div>
<div>
<el-tag :type="runTask.status === '3' ? 'primary' : 'warning'">
{{ taskStatusMap[runTask.status] || '未知状态' }}
<el-tag :type="selectedRunTask.status === '3' ? 'primary' : 'warning'">
{{ taskStatusMap[selectedRunTask.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" />
:percentage="parseFloat(taskRunningInfo[selectedRunTask.id]?.progress?.toFixed(2)) || 0" />
<taskSteps :list="pointList_runningTask[selectedRunTask.taskId]"
:activeStep="taskRunningInfo[selectedRunTask.id]?.activeStep || 0" />
<div class="run-log-container">
<div>执行日志</div>
<!--
taskId 用于查询检测项,taskStatus 用于在完成/失败/终止后推导最后一个已执行检测项的状态;
两者与实例日志分开传入,避免把任务级字段重复写进每一条日志。
-->
<taskLog :height="460" :task-id="runTask.taskId" :task-status="runTask.status"
:list="taskLogList[runTask.id]" />
<taskLog :height="460" :task-id="selectedRunTask.taskId"
:task-status="selectedRunTask.status" :list="taskLogList[selectedRunTask.id]" />
</div>
</div>
<el-empty v-else description="暂无任务" />
</div>
<div v-else-if="activePanel === 'history'">
<div :style="containerHeight">
@ -191,7 +193,7 @@ 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 { computed, onMounted, onUnmounted } from "vue";
import {
addTaskinstance,
getTaskinstanceList,
@ -253,6 +255,21 @@ const phoneList = ref([]);
const runTaskList = ref([]);
// 任务实例 ID 在接口和 WebSocket 中可能分别是数字或字符串,统一按字符串比较选中状态。
const isSelectedTask = (task) => String(task?.id ?? '') === String(selectedTaskId.value)
// 右侧当前任务始终从完整任务队列中取选中的一条,包括尚未执行、执行中和已暂停任务。
const selectedRunTask = computed(() => runTaskList.value.find(isSelectedTask) || null)
/**
* 点击左侧任务卡片时切换右侧详情,并自动回到“当前任务”面板。
* 卡片内部操作按钮使用 .stop 阻止冒泡,因此执行暂停/启动等命令时不会意外切换选择。
*/
const selectTask = (task) => {
selectedTaskId.value = task.id
activePanel.value = 'current'
}
const normalizeTaskStatus = (tasks = []) => tasks.map(item => ({
...item,
status: String(item.status),
@ -317,9 +334,11 @@ const cancelTaskDialog = () => {
}
const runingTaskList = ref([])
/**
* 获取任务队列
* 获取任务队列,并维护当前选中任务:
* 1. 首次加载默认选择队列第一条;
* 2. 刷新后原任务仍在队列中则保持选择;
* 3. 原任务已结束或被删除时回退到新的第一条,队列为空时清空选择。
*/
const getList = () => {
return getTaskinstanceList({
@ -328,7 +347,9 @@ const getList = () => {
if (res.code === 200) {
const tasks = normalizeTaskStatus(res.rows)
runTaskList.value = tasks;
runingTaskList.value = tasks.filter(item => item.status !== '5')
if (!tasks.some(isSelectedTask)) {
selectedTaskId.value = tasks[0]?.id ?? ''
}
for (let i = 0; i < runTaskList.value.length; i++) {
getCaseByTaskId(runTaskList.value[i].taskId)
getTaskLogList(runTaskList.value[i].id)
@ -684,6 +705,17 @@ onUnmounted(() => {
box-shadow: inset 0 0 0 1px #eee;
border-radius: 8px;
margin-bottom: 16px;
cursor: pointer;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
&:hover {
background: var(--el-fill-color-light);
}
&.is-selected {
background: var(--el-color-primary-light-9);
box-shadow: inset 0 0 0 2px var(--el-color-primary);
}
.task-content {
display: flex;
@ -750,10 +782,6 @@ onUnmounted(() => {
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;