CMVR-IOT-UI/src/views/intelligenceTest/runningTask/index.vue

886 lines
32 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 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.stop="pauseTask(task)" />
<SvgIcon class="icon" v-if="task.status == '3' || task.status == '5'" name="start"
:size="24" color="#00FF88" @click.stop="startTask(task)" />
<SvgIcon class="icon" v-if="task.status != '5'" name="stop" :size="24" color="#d81e06"
@click.stop="stopTask(task)" />
<SvgIcon class="icon" v-if="task.status == '5'" name="remove" :size="24" color="#FFB300"
@click.stop="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 ref="contextContainerRef" class="context-container">
<div v-if="activePanel === 'current'">
<!-- 当前任务区域只展示左侧队列选中的任务,不再同时渲染全部执行中任务。 -->
<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">{{ selectedRunTask.taskName }}</div>
<div class="id">ID: {{ selectedRunTask.taskId }}</div>
</div>
<div class="robot">
<div>手机:{{ selectedRunTask.phoneName }}</div>
<div>车辆:{{ selectedRunTask.vehicleName }}</div>
<div>开始时间: {{ formatDate(selectedRunTask.startTime) }}</div>
</div>
</div>
<div>
<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[selectedRunTask.id]?.progress?.toFixed(2)) || 0" />
<taskSteps :list="pointList_runningTask[selectedRunTask.taskId]"
:activeStep="taskRunningInfo[selectedRunTask.id]?.activeStep || 0" />
<div ref="taskLogContainerRef" class="run-log-container">
<div ref="taskLogTitleRef">执行日志</div>
<!--
taskId 用于查询检测项,taskStatus 用于在完成/失败/终止后推导最后一个已执行检测项的状态;
两者与实例日志分开传入,避免把任务级字段重复写进每一条日志。
-->
<taskLog :height="taskLogHeight" :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">
<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>
<!--
历史弹窗没有 WebSocket 实时上下文,因此同时传入任务定义 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 { computed, onMounted, onUnmounted } from "vue";
import { useElementBounding, useElementSize } from '@vueuse/core'
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 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))
})
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([]);
// 任务实例 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),
}))
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;
}
/**
* 获取任务队列,并维护当前选中任务:
* 1. 首次加载默认选择队列第一条;
* 2. 刷新后原任务仍在队列中则保持选择;
* 3. 原任务已结束或被删除时回退到新的第一条,队列为空时清空选择。
*/
const getList = () => {
return getTaskinstanceList({
statusList: ['1', '3', '5'],
}).then((res) => {
if (res.code === 200) {
const tasks = normalizeTaskStatus(res.rows)
runTaskList.value = tasks;
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)
}
}
})
}
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') {
// 统一处理数字和字符串状态。0=完成、2=失败、4=终止,均属于任务实例终态。
// 终态消息只触发列表刷新,不作为普通节点日志追加,否则会产生一个无法归属检测项的伪日志。
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
// 状态与 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;
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;
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;
}
}
}
}
.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>