2026-07-28 09:22:22 +08:00
|
|
|
|
<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">
|
2026-08-18 13:32:49 +08:00
|
|
|
|
<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)">
|
2026-07-28 09:22:22 +08:00
|
|
|
|
<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"
|
2026-08-18 13:32:49 +08:00
|
|
|
|
@click.stop="pauseTask(task)" />
|
2026-07-28 09:22:22 +08:00
|
|
|
|
<SvgIcon class="icon" v-if="task.status == '3' || task.status == '5'" name="start"
|
2026-08-18 13:32:49 +08:00
|
|
|
|
:size="24" color="#00FF88" @click.stop="startTask(task)" />
|
2026-07-28 09:22:22 +08:00
|
|
|
|
<SvgIcon class="icon" v-if="task.status != '5'" name="stop" :size="24" color="#d81e06"
|
2026-08-18 13:32:49 +08:00
|
|
|
|
@click.stop="stopTask(task)" />
|
2026-07-28 09:22:22 +08:00
|
|
|
|
<SvgIcon class="icon" v-if="task.status == '5'" name="remove" :size="24" color="#FFB300"
|
2026-08-18 13:32:49 +08:00
|
|
|
|
@click.stop="removeTask(task)" />
|
2026-07-28 09:22:22 +08:00
|
|
|
|
</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>
|
2026-08-18 13:45:45 +08:00
|
|
|
|
<div ref="contextContainerRef" class="context-container">
|
2026-07-28 09:22:22 +08:00
|
|
|
|
<div v-if="activePanel === 'current'">
|
2026-08-18 13:32:49 +08:00
|
|
|
|
<!-- 当前任务区域只展示左侧队列选中的任务,不再同时渲染全部执行中任务。 -->
|
|
|
|
|
|
<div v-if="selectedRunTask" :key="selectedRunTask.id" class="task"
|
|
|
|
|
|
:data-task-id="selectedRunTask.id">
|
2026-07-28 09:22:22 +08:00
|
|
|
|
<div class="task-header">
|
|
|
|
|
|
<div class="header-container">
|
|
|
|
|
|
<div class="task-info">
|
2026-08-18 13:32:49 +08:00
|
|
|
|
<div class="title">{{ selectedRunTask.taskName }}</div>
|
|
|
|
|
|
<div class="id">ID: {{ selectedRunTask.taskId }}</div>
|
2026-07-28 09:22:22 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="robot">
|
2026-08-18 13:32:49 +08:00
|
|
|
|
<div>手机:{{ selectedRunTask.phoneName }}</div>
|
|
|
|
|
|
<div>车辆:{{ selectedRunTask.vehicleName }}</div>
|
|
|
|
|
|
<div>开始时间: {{ formatDate(selectedRunTask.startTime) }}</div>
|
2026-07-28 09:22:22 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-08-18 13:32:49 +08:00
|
|
|
|
<el-tag :type="selectedRunTask.status === '3' ? 'primary' : 'warning'">
|
|
|
|
|
|
{{ taskStatusMap[selectedRunTask.status] || '未知状态' }}
|
2026-07-28 09:22:22 +08:00
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="progress-label">执行进度</div>
|
|
|
|
|
|
<el-progress :color="customColorMethod"
|
2026-08-18 13:32:49 +08:00
|
|
|
|
:percentage="parseFloat(taskRunningInfo[selectedRunTask.id]?.progress?.toFixed(2)) || 0" />
|
|
|
|
|
|
<taskSteps :list="pointList_runningTask[selectedRunTask.taskId]"
|
|
|
|
|
|
:activeStep="taskRunningInfo[selectedRunTask.id]?.activeStep || 0" />
|
2026-08-18 13:45:45 +08:00
|
|
|
|
<div ref="taskLogContainerRef" class="run-log-container">
|
|
|
|
|
|
<div ref="taskLogTitleRef">执行日志</div>
|
2026-08-18 11:32:34 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
taskId 用于查询检测项,taskStatus 用于在完成/失败/终止后推导最后一个已执行检测项的状态;
|
|
|
|
|
|
两者与实例日志分开传入,避免把任务级字段重复写进每一条日志。
|
|
|
|
|
|
-->
|
2026-08-18 13:45:45 +08:00
|
|
|
|
<taskLog :height="taskLogHeight" :task-id="selectedRunTask.taskId"
|
2026-08-18 13:32:49 +08:00
|
|
|
|
:task-status="selectedRunTask.status" :list="taskLogList[selectedRunTask.id]" />
|
2026-07-28 09:22:22 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-08-18 13:32:49 +08:00
|
|
|
|
<el-empty v-else description="暂无任务" />
|
2026-07-28 09:22:22 +08:00
|
|
|
|
</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>
|
2026-08-13 17:54:04 +08:00
|
|
|
|
<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>
|
2026-07-28 09:22:22 +08:00
|
|
|
|
</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>
|
|
|
|
|
|
|
2026-08-17 17:10:59 +08:00
|
|
|
|
<el-dialog title="任务执行日志" v-model="logDialogVisible" width="800px" append-to-body>
|
2026-08-18 11:32:34 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
历史弹窗没有 WebSocket 实时上下文,因此同时传入任务定义 ID 和历史终态;
|
|
|
|
|
|
日志组件据此补齐未执行项,并正确区分最后一项是完成、失败还是终止。
|
|
|
|
|
|
-->
|
2026-08-18 11:19:52 +08:00
|
|
|
|
<taskLog v-loading="logDialogLoading" :task-id="logDialogTaskId" :task-status="logDialogTaskStatus"
|
|
|
|
|
|
:list="logDialogData" :height="500" />
|
2026-07-28 09:22:22 +08:00
|
|
|
|
</el-dialog>
|
2026-08-11 16:50:30 +08:00
|
|
|
|
|
2026-08-12 16:45:48 +08:00
|
|
|
|
<el-dialog title="本次执行安排" v-model="startDialogVisible" width="820px" append-to-body>
|
2026-08-12 13:32:00 +08:00
|
|
|
|
<div class="start-dialog-content">
|
|
|
|
|
|
<div class="start-dialog-tip">手机和车辆沿用任务配置,这里只安排执行流程的机器人和设备。</div>
|
2026-08-12 16:45:48 +08:00
|
|
|
|
<div class="start-section-title">机器人和设备</div>
|
2026-08-12 13:32:00 +08:00
|
|
|
|
<RuntimeAssignmentsEditor ref="runtimeEditorRef" v-model="runtimeAssignments" :roles="runtimeRoles" />
|
2026-08-12 16:45:48 +08:00
|
|
|
|
<div class="start-section-title start-section-title--params">运行参数</div>
|
|
|
|
|
|
<RuntimeParamsEditor ref="runtimeParamsEditorRef" :items="runtimeParamItems" />
|
2026-08-12 13:32:00 +08:00
|
|
|
|
</div>
|
2026-08-11 16:50:30 +08:00
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="startDialogVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" :loading="startSubmitting" @click="confirmStartTask">开始执行</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
2026-07-28 09:22:22 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import SvgIcon from "@/components/SvgIcon";
|
|
|
|
|
|
import taskSteps from "./task-steps.vue";
|
|
|
|
|
|
import taskLog from "./task-log.vue";
|
2026-08-17 16:05:42 +08:00
|
|
|
|
import { fetchAllPagedRows } from '@/utils/pagedRequest'
|
2026-07-28 09:22:22 +08:00
|
|
|
|
import { useContainerHeight } from "@/hooks/tableHeight";
|
|
|
|
|
|
import { getTaskList, getTestCaseByTaskId } from "@/api/intelligenceTest/taskManage";
|
|
|
|
|
|
import { getPhoneList } from "@/api/intelligenceTest/mobilePhone";
|
|
|
|
|
|
import { getVehicleList } from "@/api/intelligenceTest/car";
|
2026-08-18 13:32:49 +08:00
|
|
|
|
import { computed, onMounted, onUnmounted } from "vue";
|
2026-08-18 13:45:45 +08:00
|
|
|
|
import { useElementBounding, useElementSize } from '@vueuse/core'
|
2026-07-28 09:22:22 +08:00
|
|
|
|
import {
|
|
|
|
|
|
addTaskinstance,
|
|
|
|
|
|
getTaskinstanceList,
|
|
|
|
|
|
startTaskinstance,
|
2026-08-13 17:54:04 +08:00
|
|
|
|
rerunTaskinstance,
|
2026-07-28 09:22:22 +08:00
|
|
|
|
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 的防抖函数
|
2026-08-11 16:50:30 +08:00
|
|
|
|
import RuntimeAssignmentsEditor from '@/views/flow/components/RuntimeAssignmentsEditor.vue'
|
2026-08-12 16:45:48 +08:00
|
|
|
|
import RuntimeParamsEditor from '@/views/flow/components/RuntimeParamsEditor.vue'
|
|
|
|
|
|
import { getFlowParamsApi } from '@/api/device/terminal.js'
|
2026-08-12 09:31:45 +08:00
|
|
|
|
import {
|
|
|
|
|
|
createRuntimeAssignments,
|
|
|
|
|
|
firstAssignedRobotId,
|
2026-08-12 16:45:48 +08:00
|
|
|
|
loadFlowItemScopedRuntimeRoles,
|
2026-08-12 09:31:45 +08:00
|
|
|
|
} from '@/utils/flowExecution'
|
2026-07-28 09:22:22 +08:00
|
|
|
|
|
|
|
|
|
|
const topContainerRef = ref();
|
|
|
|
|
|
const containerHeight = useContainerHeight(topContainerRef);
|
2026-08-18 13:45:45 +08:00
|
|
|
|
const contextContainerRef = ref(null)
|
|
|
|
|
|
const taskLogContainerRef = ref(null)
|
|
|
|
|
|
const taskLogTitleRef = ref(null)
|
|
|
|
|
|
|
|
|
|
|
|
// 通过元素边界实时获取当前任务内容区底部和日志容器顶部,避免依赖固定的 460px。
|
|
|
|
|
|
const { bottom: contextContainerBottom } = useElementBounding(contextContainerRef)
|
|
|
|
|
|
const { top: taskLogContainerTop } = useElementBounding(taskLogContainerRef)
|
|
|
|
|
|
const { height: taskLogTitleHeight } = useElementSize(taskLogTitleRef)
|
|
|
|
|
|
|
|
|
|
|
|
const TASK_LOG_MARGIN_TOP = 12
|
|
|
|
|
|
const CURRENT_TASK_BOTTOM_PADDING = 16
|
|
|
|
|
|
const MIN_TASK_LOG_HEIGHT = 150
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 计算当前任务页面中日志组件可以使用的剩余高度。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 计算基准是右侧内容区的底部,而不是窗口高度,因此页面外层布局、浏览器窗口
|
|
|
|
|
|
* 尺寸或右侧面板高度发生变化时都能得到正确结果。日志容器顶部到内容区底部之间,
|
|
|
|
|
|
* 还需要扣除“执行日志”标题高度、taskLog 组件自身的上间距,以及当前任务卡片的
|
|
|
|
|
|
* 底部内边距,确保日志不会覆盖任务卡片边界。元素尚未挂载时使用最低高度作为初始值,
|
|
|
|
|
|
* 待 VueUse 完成首次测量后会自动更新为实际剩余高度。
|
|
|
|
|
|
*/
|
|
|
|
|
|
const taskLogHeight = computed(() => {
|
|
|
|
|
|
const remainingHeight = contextContainerBottom.value
|
|
|
|
|
|
- taskLogContainerTop.value
|
|
|
|
|
|
- taskLogTitleHeight.value
|
|
|
|
|
|
- TASK_LOG_MARGIN_TOP
|
|
|
|
|
|
- CURRENT_TASK_BOTTOM_PADDING
|
|
|
|
|
|
|
|
|
|
|
|
return Math.max(MIN_TASK_LOG_HEIGHT, Math.floor(remainingHeight))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-28 09:22:22 +08:00
|
|
|
|
const socket = inject('ws');
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const startDialogVisible = ref(false)
|
|
|
|
|
|
const startSubmitting = ref(false)
|
|
|
|
|
|
const pendingStartTask = ref(null)
|
|
|
|
|
|
const runtimeRoles = ref([])
|
|
|
|
|
|
const runtimeAssignments = ref([])
|
|
|
|
|
|
const runtimeEditorRef = ref()
|
2026-08-12 16:45:48 +08:00
|
|
|
|
const runtimeParamItems = ref([])
|
|
|
|
|
|
const runtimeParamsEditorRef = ref()
|
2026-08-13 17:54:04 +08:00
|
|
|
|
const selectedTaskId = ref('')
|
|
|
|
|
|
const rerunningId = ref('')
|
2026-07-28 09:22:22 +08:00
|
|
|
|
|
|
|
|
|
|
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([]);
|
|
|
|
|
|
|
2026-08-18 13:32:49 +08:00
|
|
|
|
// 任务实例 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'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 09:40:16 +08:00
|
|
|
|
const normalizeTaskStatus = (tasks = []) => tasks.map(item => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
status: String(item.status),
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
2026-07-28 09:22:22 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-08-18 13:32:49 +08:00
|
|
|
|
* 获取任务队列,并维护当前选中任务:
|
|
|
|
|
|
* 1. 首次加载默认选择队列第一条;
|
|
|
|
|
|
* 2. 刷新后原任务仍在队列中则保持选择;
|
|
|
|
|
|
* 3. 原任务已结束或被删除时回退到新的第一条,队列为空时清空选择。
|
2026-07-28 09:22:22 +08:00
|
|
|
|
*/
|
|
|
|
|
|
const getList = () => {
|
2026-08-13 17:54:04 +08:00
|
|
|
|
return getTaskinstanceList({
|
2026-07-28 09:22:22 +08:00
|
|
|
|
statusList: ['1', '3', '5'],
|
|
|
|
|
|
}).then((res) => {
|
|
|
|
|
|
if (res.code === 200) {
|
2026-08-13 09:40:16 +08:00
|
|
|
|
const tasks = normalizeTaskStatus(res.rows)
|
|
|
|
|
|
runTaskList.value = tasks;
|
2026-08-18 13:32:49 +08:00
|
|
|
|
if (!tasks.some(isSelectedTask)) {
|
|
|
|
|
|
selectedTaskId.value = tasks[0]?.id ?? ''
|
|
|
|
|
|
}
|
2026-07-28 09:22:22 +08:00
|
|
|
|
for (let i = 0; i < runTaskList.value.length; i++) {
|
|
|
|
|
|
getCaseByTaskId(runTaskList.value[i].taskId)
|
|
|
|
|
|
getTaskLogList(runTaskList.value[i].id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 17:54:04 +08:00
|
|
|
|
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 = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 09:22:22 +08:00
|
|
|
|
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) => {
|
2026-08-17 16:05:42 +08:00
|
|
|
|
const res = await fetchAllPagedRows(getTaskLogListApi, { taskInstanceId })
|
2026-07-28 09:22:22 +08:00
|
|
|
|
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) => {
|
2026-08-13 09:40:16 +08:00
|
|
|
|
if (String(task.status) === '3') {
|
2026-07-28 09:22:22 +08:00
|
|
|
|
let res = await resumeTaskinstance(task.id);
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
ElMessage.success("任务已恢复");
|
|
|
|
|
|
getList();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const taskDefinition = taskList.value.find(item => item.id === task.taskId)
|
|
|
|
|
|
if (!taskDefinition?.taskConfigId) {
|
|
|
|
|
|
ElMessage.warning('当前任务没有关联流程编排')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-12 09:31:45 +08:00
|
|
|
|
try {
|
2026-08-12 16:45:48 +08:00
|
|
|
|
const paramsResponse = await getFlowParamsApi(taskDefinition.taskConfigId)
|
|
|
|
|
|
const paramItems = paramsResponse.rows || []
|
|
|
|
|
|
const roles = await loadFlowItemScopedRuntimeRoles(paramItems)
|
2026-08-12 09:31:45 +08:00
|
|
|
|
runtimeAssignments.value = createRuntimeAssignments(roles)
|
|
|
|
|
|
runtimeRoles.value = roles
|
2026-08-12 16:45:48 +08:00
|
|
|
|
runtimeParamItems.value = paramItems
|
2026-08-12 09:31:45 +08:00
|
|
|
|
pendingStartTask.value = task
|
|
|
|
|
|
startDialogVisible.value = true
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ElMessage.error(error.message || '工作流执行资源加载失败')
|
|
|
|
|
|
}
|
2026-08-11 16:50:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const confirmStartTask = async () => {
|
|
|
|
|
|
const assignmentError = runtimeEditorRef.value?.validate()
|
|
|
|
|
|
if (assignmentError) {
|
|
|
|
|
|
ElMessage.warning(assignmentError)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-12 16:45:48 +08:00
|
|
|
|
const paramsValid = await runtimeParamsEditorRef.value?.validate()
|
|
|
|
|
|
if (paramsValid === false) {
|
|
|
|
|
|
ElMessage.warning('请完整填写运行参数')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-11 16:50:30 +08:00
|
|
|
|
startSubmitting.value = true
|
|
|
|
|
|
try {
|
2026-08-12 09:31:45 +08:00
|
|
|
|
const robotId = firstAssignedRobotId(runtimeAssignments.value)
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const res = await startTaskinstance(pendingStartTask.value.id, {
|
|
|
|
|
|
robotId,
|
2026-08-12 16:45:48 +08:00
|
|
|
|
runtimeAssignments: runtimeAssignments.value,
|
|
|
|
|
|
runParams: runtimeParamsEditorRef.value?.getRunParams() || {}
|
2026-08-11 16:50:30 +08:00
|
|
|
|
})
|
2026-07-28 09:22:22 +08:00
|
|
|
|
if (res.code === 200) {
|
2026-08-11 16:50:30 +08:00
|
|
|
|
ElMessage.success('任务已开始')
|
|
|
|
|
|
startDialogVisible.value = false
|
|
|
|
|
|
getList()
|
2026-07-28 09:22:22 +08:00
|
|
|
|
}
|
2026-08-11 16:50:30 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
startSubmitting.value = false
|
2026-07-28 09:22:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 暂停执行任务
|
|
|
|
|
|
* @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 () => {
|
2026-08-12 16:45:24 +08:00
|
|
|
|
let res = await deleteTaskinstance(task.id);
|
2026-07-28 09:22:22 +08:00
|
|
|
|
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) {
|
2026-08-13 09:40:16 +08:00
|
|
|
|
tableDataList.value = normalizeTaskStatus(res.rows);
|
2026-07-28 09:22:22 +08:00
|
|
|
|
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') {
|
2026-08-18 11:32:34 +08:00
|
|
|
|
// 统一处理数字和字符串状态。0=完成、2=失败、4=终止,均属于任务实例终态。
|
|
|
|
|
|
// 终态消息只触发列表刷新,不作为普通节点日志追加,否则会产生一个无法归属检测项的伪日志。
|
2026-08-18 11:19:52 +08:00
|
|
|
|
const status = Number(data.status)
|
|
|
|
|
|
const terminal = [0, 2, 4].includes(status)
|
|
|
|
|
|
if (!terminal) {
|
2026-08-12 16:45:48 +08:00
|
|
|
|
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)
|
2026-07-28 09:22:22 +08:00
|
|
|
|
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 {
|
2026-08-18 11:32:34 +08:00
|
|
|
|
// 清除实时缓存后重新查询任务队列和历史记录,让最终状态与后端持久化结果保持一致。
|
2026-07-28 09:22:22 +08:00
|
|
|
|
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([])
|
2026-08-18 10:18:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 历史表格行里的 taskId 是任务定义 ID;日志组件使用它查询本次任务包含的全部检测项。
|
|
|
|
|
|
const logDialogTaskId = ref('')
|
2026-08-18 11:32:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 保存当前历史实例的终态,供日志组件判断最后一个产生过日志的检测项是完成、失败还是被终止。
|
2026-08-18 11:19:52 +08:00
|
|
|
|
const logDialogTaskStatus = ref(null)
|
2026-08-17 16:05:42 +08:00
|
|
|
|
const logDialogLoading = ref(false)
|
|
|
|
|
|
let logDialogRequestId = 0
|
2026-07-28 09:22:22 +08:00
|
|
|
|
const handleOpenLog = async (row) => {
|
2026-08-17 16:05:42 +08:00
|
|
|
|
const requestId = ++logDialogRequestId
|
2026-07-28 09:22:22 +08:00
|
|
|
|
logDialogVisible.value = true
|
2026-08-18 10:18:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 在请求实例日志前先更新 taskId,让检测项请求与日志请求并行执行,减少弹窗等待时间。
|
|
|
|
|
|
logDialogTaskId.value = row.taskId
|
2026-08-18 11:32:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 状态与 taskId 同步切换,防止连续打开不同历史任务时短暂沿用上一个实例的状态。
|
2026-08-18 11:19:52 +08:00
|
|
|
|
logDialogTaskStatus.value = row.status
|
2026-08-17 16:05:42 +08:00
|
|
|
|
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
|
2026-07-28 09:22:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-14 13:31:37 +08:00
|
|
|
|
const refreshAfterSocketReconnect = () => {
|
|
|
|
|
|
getList()
|
|
|
|
|
|
getHistoryList()
|
|
|
|
|
|
}
|
2026-07-28 09:22:22 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
getTaskData();
|
|
|
|
|
|
getPhoneData();
|
|
|
|
|
|
getCarData()
|
|
|
|
|
|
getList();
|
|
|
|
|
|
getHistoryList();
|
|
|
|
|
|
subscribeDebounced(true)
|
2026-08-14 13:31:37 +08:00
|
|
|
|
socket?.on('open', refreshAfterSocketReconnect)
|
2026-07-28 09:22:22 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
subscribeDebounced(false)
|
2026-08-14 13:31:37 +08:00
|
|
|
|
socket?.off('open', refreshAfterSocketReconnect)
|
2026-07-28 09:22:22 +08:00
|
|
|
|
})
|
|
|
|
|
|
</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;
|
2026-08-18 13:32:49 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-07-28 09:22:22 +08:00
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-11 16:50:30 +08:00
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2026-08-12 13:32:00 +08:00
|
|
|
|
|
|
|
|
|
|
.start-dialog-content {
|
|
|
|
|
|
max-height: 65vh;
|
|
|
|
|
|
padding-right: 6px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
2026-08-12 16:45:48 +08:00
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2026-08-11 16:50:30 +08:00
|
|
|
|
</style>
|