Compare commits
3 Commits
e9717c85e2
...
2d2854814b
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d2854814b | |||
| 7590d5a986 | |||
| 8e53faad52 |
@ -8,5 +8,5 @@ VITE_APP_ENV = 'development'
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
|
||||
VITE_API_URL = http://192.168.28.10:13080
|
||||
VITE_WS_URL = ws://192.168.28.10:13080/ws
|
||||
VITE_WS_URL = ws://192.168.0.201:13080/ws
|
||||
|
||||
|
||||
@ -14,4 +14,4 @@ VITE_BUILD_COMPRESS = gzip
|
||||
VITE_NODE_RED_URL = 'http://10.148.108.59:13080'
|
||||
|
||||
VITE_API_URL = http://192.168.28.10:13080
|
||||
VITE_WS_URL = ws://192.168.28.10:13080/ws
|
||||
VITE_WS_URL = ws://192.168.0.201:13080/ws
|
||||
|
||||
@ -10,4 +10,4 @@ VITE_APP_BASE_API = '/stage-api'
|
||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||
VITE_BUILD_COMPRESS = gzip
|
||||
VITE_API_URL = http://10.148.121.100:13080
|
||||
VITE_WS_URL = ws://10.148.121.100:13080/ws
|
||||
VITE_WS_URL = ws://192.168.0.201:13080/ws
|
||||
67
src/api/inspection/runTask.js
Normal file
67
src/api/inspection/runTask.js
Normal file
@ -0,0 +1,67 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getRunTaskList(params) {
|
||||
return request({
|
||||
url: '/inspection/taskInstance/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getRunTaskDetail(id) {
|
||||
return request({
|
||||
url: `/inspection/taskInstance/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function addRunTask(data) {
|
||||
return request({
|
||||
url: '/inspection/taskInstance',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRunTask(data) {
|
||||
return request({
|
||||
url: '/inspection/taskInstance',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRunTask(id) {
|
||||
return request({
|
||||
url: `/inspection/taskInstance/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function startRunTask(id) {
|
||||
return request({
|
||||
url: `/inspection/taskInstance/start/${id}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function stopRunTask(id) {
|
||||
return request({
|
||||
url: `/inspection/taskInstance/stop/${id}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function pauseRunTask(id) {
|
||||
return request({
|
||||
url: `/inspection/taskInstance/pause/${id}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function resumeRunTask(id) {
|
||||
return request({
|
||||
url: `/inspection/taskInstance/resume/${id}`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
6
src/assets/icons/svg/pause.svg
Normal file
6
src/assets/icons/svg/pause.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1780372291375" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5453" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
|
||||
<path d="M624.941176 360.929882c-12.408471 0-22.528 13.432471-22.528 30.117647v301.176471c0 16.624941 10.119529 30.117647 22.528 30.117647h45.296942c12.408471 0 22.528-13.492706 22.528-30.117647v-301.176471c0-16.685176-10.119529-30.117647-22.528-30.117647h-45.296942z m-271.058823 0c-12.408471 0-22.528 13.432471-22.528 30.117647v301.176471c0 16.624941 10.119529 30.117647 22.528 30.117647h45.296941c12.408471 0 22.528-13.492706 22.528-30.117647v-301.176471c0-16.685176-10.119529-30.117647-22.528-30.117647h-45.296941z" fill="currentColor" p-id="5454"></path>
|
||||
<path d="M250.036706 138.059294a141.673412 141.673412 0 0 0-141.552941 141.552941v524.047059a141.673412 141.673412 0 0 0 141.552941 141.552941h524.047059a141.673412 141.673412 0 0 0 141.552941-141.552941v-524.047059a141.673412 141.673412 0 0 0-141.552941-141.552941h-524.047059z m524.047059 885.458824h-524.047059a220.099765 220.099765 0 0 1-219.858824-219.858824v-524.047059a220.099765 220.099765 0 0 1 219.858824-219.858823h524.047059a220.099765 220.099765 0 0 1 219.858823 219.858823v524.047059a220.099765 220.099765 0 0 1-219.858823 219.858824z" fill="currentColor" p-id="5455"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
src/assets/icons/svg/remove.svg
Normal file
1
src/assets/icons/svg/remove.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1780472431337" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19258" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M814.29 136.567H207.664c-55.752 0-101.274 13.3-101.274 56.776v26.086h808.663v-26.086c0.511-42.965-45.011-56.776-100.763-56.776" p-id="19259" fill="#2c2c2c"></path><path d="M723.245 191.808l-19.948-140.66c-3.58-27.62-29.667-50.125-57.287-50.125H376.456c-28.132 0-53.707 22.505-57.799 50.126l-19.948 141.17c-3.58 27.621 15.856 22.506 43.477 22.506h337.07c28.133-0.511 47.57 4.604 43.989-23.017z m-360.6-28.643L377.99 51.66h265.463l15.344 111.505H362.645zM831.17 282.342H190.785c-36.827 0-64.959 30.177-61.378 67.005l55.24 607.648c3.58 36.827 36.316 67.005 73.655 67.005h505.35c36.828 0 70.074-30.178 73.655-67.005l55.24-607.137c3.58-36.827-24.04-67.516-61.378-67.516zM376.456 953.415H245.514l-43.476-592.816h174.418v592.816z m221.474 0H423.512V360.599H597.93v592.816z m177.487 0H646.01V360.599h172.883l-43.476 592.816z" p-id="19260" fill="#2c2c2c"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
6
src/assets/icons/svg/start.svg
Normal file
6
src/assets/icons/svg/start.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1780371806721" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1692" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
|
||||
<path d="M703.146667 479.146667L469.333333 285.866667a42.666667 42.666667 0 0 0-69.973333 32.853333v386.56a42.666667 42.666667 0 0 0 69.973333 32.853333l232.106667-193.28a42.666667 42.666667 0 0 0 1.706667-65.706666z" fill="currentColor" p-id="1693"></path>
|
||||
<path d="M250.036706 138.059294a141.673412 141.673412 0 0 0-141.552941 141.552941v524.047059a141.673412 141.673412 0 0 0 141.552941 141.552941h524.047059a141.673412 141.673412 0 0 0 141.552941-141.552941v-524.047059a141.673412 141.673412 0 0 0-141.552941-141.552941h-524.047059z m524.047059 885.458824h-524.047059a220.099765 220.099765 0 0 1-219.858824-219.858824v-524.047059a220.099765 220.099765 0 0 1 219.858824-219.858823h524.047059a220.099765 220.099765 0 0 1 219.858823 219.858823v524.047059a220.099765 220.099765 0 0 1-219.858823 219.858824z" fill="currentColor" p-id="5455"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
6
src/assets/icons/svg/stop.svg
Normal file
6
src/assets/icons/svg/stop.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1780378438689" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15665" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
|
||||
<path d="M877.696 0H146.24A146.24 146.24 0 0 0 0 146.24v731.456A146.24 146.24 0 0 0 146.24 1024h731.456A146.24 146.24 0 0 0 1024 877.696V146.24A146.24 146.24 0 0 0 877.696 0z m73.152 877.696c0 40.448-32.768 73.216-73.216 73.216H146.24a73.216 73.216 0 0 1-73.152-73.216V146.24c0-40.384 32.768-73.152 73.152-73.152h731.456c40.448 0 73.216 32.768 73.216 73.152v731.456z" fill="currentColor" p-id="15666"></path>
|
||||
<path d="M688.512 644.224a40.32 40.32 0 0 1-40.384 40.32H371.072a40.32 40.32 0 0 1-40.32-40.32V367.168a40.32 40.32 0 0 1 40.32-40.32h277.056a40.32 40.32 0 0 1 40.384 40.32v277.056z" fill="currentColor" p-id="15667"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 966 B |
@ -87,9 +87,8 @@ app.use(ElementPlus, {
|
||||
|
||||
|
||||
app.use(WebSocketPlugin, {
|
||||
// url: import.meta.env.VITE_WS_URL,
|
||||
url: 'ws://192.168.0.201:13080/ws',
|
||||
// url: 'ws://10.148.209.5:13080/ws',
|
||||
url: import.meta.env.VITE_WS_URL,
|
||||
// url: 'ws://192.168.0.201:13080/ws',
|
||||
userId: 'your_user_id'
|
||||
});
|
||||
|
||||
|
||||
@ -225,9 +225,6 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="参数配置" width="1080px">
|
||||
<EditTable></EditTable>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -257,7 +254,6 @@ const { proxy } = getCurrentInstance();
|
||||
|
||||
const tableDataList = ref([]);
|
||||
const open = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const loading = ref(false);
|
||||
const showSearch = ref(true);
|
||||
|
||||
|
||||
592
src/views/inspection/runTask/index.vue
Normal file
592
src/views/inspection/runTask/index.vue
Normal file
@ -0,0 +1,592 @@
|
||||
<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.robotName }}</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">
|
||||
<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.robotName }}</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_runintTask[runTask.taskId]"
|
||||
:activeStep="taskRunningInfo[runTask.id]?.activeStep || 0" />
|
||||
<div class="run-log-container">
|
||||
<div>执行日志</div>
|
||||
<div class="log-container">
|
||||
<div class="log" v-for="value, index in 4">
|
||||
<div class="time">11:02:06</div>
|
||||
<div class="context">移动机器人至点位{{ index + 1 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</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="handleUpdate(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%"
|
||||
@change="taskChange">
|
||||
<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="robotId">
|
||||
<el-select v-model="form.robotId" placeholder="请选择机器人" clearable style="width: 100%">
|
||||
<el-option v-for="robot in robotList" :key="robot.id" :label="robot.robotName"
|
||||
:value="robot.id" />
|
||||
</el-select>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import SvgIcon from "@/components/SvgIcon";
|
||||
import taskSteps from "./task-steps.vue";
|
||||
import { useContainerHeight } from "@/hooks/tableHeight";
|
||||
import { getTaskList, getWaypointByTaskId } from "@/api/inspection/task";
|
||||
import { getRobotList } from "@/api/inspection/robot";
|
||||
import { onMounted, onUnmounted } from "vue";
|
||||
import { addRunTask, getRunTaskList, startRunTask, pauseRunTask, stopRunTask, resumeRunTask, deleteRunTask } from "@/api/inspection/runTask";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { formatDate } from "@/utils/index";
|
||||
import { debounce } from 'lodash'; // 引入 lodash 的防抖函数
|
||||
|
||||
const topContainerRef = ref();
|
||||
const containerHeight = useContainerHeight(topContainerRef);
|
||||
const socket = inject('ws');
|
||||
|
||||
const taskStatusMap = {
|
||||
'0': '已完成',
|
||||
'1': '执行中',
|
||||
'2': '失败',
|
||||
'3': '已暂停',
|
||||
'4': '已终止',
|
||||
'5': '未执行',
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
taskId: null,
|
||||
robotId: null,
|
||||
});
|
||||
|
||||
const rules = reactive({
|
||||
taskId: [{ required: true, message: '请选择任务', trigger: 'change' }],
|
||||
robotId: [{ required: true, message: '请选择机器人', trigger: 'change' }],
|
||||
});
|
||||
|
||||
const taskList = ref([]);
|
||||
const robotList = ref([]);
|
||||
|
||||
const runTaskList = ref([]);
|
||||
|
||||
const getTaskData = () => {
|
||||
getTaskList({
|
||||
pageNum: 1,
|
||||
pageSize: 10000,
|
||||
}).then((res) => {
|
||||
taskList.value = res.rows;
|
||||
})
|
||||
}
|
||||
|
||||
const getRobotData = (mapId = null) => {
|
||||
getRobotList({
|
||||
pageNum: 1,
|
||||
pageSize: 10000,
|
||||
currentMapId: mapId
|
||||
}).then((res) => {
|
||||
robotList.value = res.rows;
|
||||
})
|
||||
}
|
||||
|
||||
const open = ref(false);
|
||||
|
||||
const addTask = () => {
|
||||
open.value = true;
|
||||
}
|
||||
|
||||
const taskChange = (value) => {
|
||||
const task = taskList.value.find(item => item.id === value)
|
||||
const mapId = task.mapId
|
||||
form.robotId = null;
|
||||
robotList.value = []
|
||||
getRobotData(mapId)
|
||||
}
|
||||
|
||||
const runTaskRef = ref();
|
||||
const confirmTaskDialog = () => {
|
||||
runTaskRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
addRunTask(form).then(() => {
|
||||
ElMessage.success("任务添加成功");
|
||||
getList();
|
||||
open.value = false;
|
||||
runTaskRef.value.resetFields();
|
||||
}).catch(() => {
|
||||
ElMessage.error("任务添加失败");
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const cancelTaskDialog = () => {
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
|
||||
const runingTaskList = ref([])
|
||||
/**
|
||||
* 获取任务队列
|
||||
*/
|
||||
const getList = () => {
|
||||
getRunTaskList({
|
||||
statusList: ['1', '3', '5'],
|
||||
}).then((res) => {
|
||||
if (res.code === 200) {
|
||||
runTaskList.value = res.rows;
|
||||
runingTaskList.value = res.rows.filter(item => item.status !== '5')
|
||||
for (let i = 0; i < runTaskList.value.length; i++) {
|
||||
getPointByTaskId(runTaskList.value[i].taskId)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const pointList_runintTask = ref({})
|
||||
const getPointByTaskId = async (taskId) => {
|
||||
const res = await getWaypointByTaskId(taskId)
|
||||
if (res.code === 200) {
|
||||
pointList_runintTask.value[taskId] = res.data
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const startTask = async (task) => {
|
||||
if (task.status === '3') {
|
||||
let res = await resumeRunTask(task.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("任务已恢复");
|
||||
getList();
|
||||
}
|
||||
} else {
|
||||
let res = await startRunTask(task.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("任务已开始");
|
||||
getList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pauseTask = async (task) => {
|
||||
let res = await pauseRunTask(task.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("任务已暂停");
|
||||
getList();
|
||||
}
|
||||
}
|
||||
|
||||
const stopTask = async (task) => {
|
||||
let res = await stopRunTask(task.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("任务已停止");
|
||||
getList();
|
||||
}
|
||||
}
|
||||
|
||||
const removeTask = async (task) => {
|
||||
ElMessageBox.confirm('确定要删除该任务吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
let res = await deleteRunTask(task.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("任务已删除");
|
||||
getList();
|
||||
}
|
||||
}).catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取历史执行任务
|
||||
*/
|
||||
const getHistoryList = () => {
|
||||
loading.value = true;
|
||||
getRunTaskList({
|
||||
statusList: ['0', '2', '4'],
|
||||
pageNum: queryParams.pageNum,
|
||||
pageSize: queryParams.pageSize,
|
||||
}).then((res) => {
|
||||
if (res.code === 200) {
|
||||
tableDataList.value = res.rows;
|
||||
total.value = res.total;
|
||||
}
|
||||
loading.value = false;
|
||||
}).catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
const taskRunningInfo = ref({})
|
||||
|
||||
const socketChannelCallbacks = ref({})
|
||||
const channel = 'InspectionTaskInstance';
|
||||
|
||||
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]);
|
||||
console.log('清理旧回调:', channel);
|
||||
}
|
||||
|
||||
const callback = (data) => {
|
||||
if (data !== '500') {
|
||||
if (data.status !== 0) {
|
||||
let activeStep = pointList_runintTask.value[data.taskId].findIndex(item => item.detectItemId === data.itemId) || 0
|
||||
if (data.progress == 100) {
|
||||
activeStep += 1
|
||||
}
|
||||
taskRunningInfo.value[data.insId] = {
|
||||
...data,
|
||||
activeStep
|
||||
}
|
||||
} else {
|
||||
delete taskRunningInfo.value[data.insId]
|
||||
getList()
|
||||
}
|
||||
console.log('taskRunningInfo', taskRunningInfo.value)
|
||||
} 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);
|
||||
|
||||
onMounted(() => {
|
||||
getTaskData();
|
||||
getRobotData();
|
||||
getList();
|
||||
getHistoryList();
|
||||
subscribeDebounced(true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
subscribeDebounced(false)
|
||||
})
|
||||
</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;
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.run-log-container {
|
||||
|
||||
.log-container {
|
||||
margin-top: 12px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
|
||||
.log {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.time {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.context {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
88
src/views/inspection/runTask/task-steps.vue
Normal file
88
src/views/inspection/runTask/task-steps.vue
Normal file
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<el-scrollbar ref="scrollbarRef" style="width: 100%;" class="step-scrollbar">
|
||||
<div class="step-wrapper" ref="stepWrapperRef">
|
||||
<el-steps :active="activeStep" finish-status="success">
|
||||
<el-step v-for="item in list" :key="value" :title="item.waypointName" :description="item.remark" />
|
||||
</el-steps>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
activeStep: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const scrollbarRef = ref(null)
|
||||
const stepWrapperRef = ref(null)
|
||||
|
||||
|
||||
/**
|
||||
* 滚动到当前激活的步骤
|
||||
*/
|
||||
const scrollToActiveStep = () => {
|
||||
nextTick(() => {
|
||||
// 获取 el-scrollbar 的可视区域宽度
|
||||
const scrollbarEl = scrollbarRef.value?.$el
|
||||
const wrapEl = scrollbarEl?.querySelector('.el-scrollbar__wrap')
|
||||
if (!wrapEl) return
|
||||
|
||||
const viewWidth = wrapEl.clientWidth
|
||||
|
||||
// 获取所有 step 元素
|
||||
const steps = stepWrapperRef.value?.querySelectorAll('.el-step')
|
||||
if (!steps || !steps[props.activeStep]) return
|
||||
|
||||
// 计算激活 step 的位置
|
||||
const activeEl = steps[props.activeStep]
|
||||
const stepLeft = activeEl.offsetLeft
|
||||
const stepWidth = activeEl.offsetWidth
|
||||
|
||||
// 让激活 step 居中显示
|
||||
const scrollLeft = stepLeft - viewWidth / 2 + stepWidth / 2
|
||||
console.log('滚动位置:', scrollLeft)
|
||||
// 使用 el-scrollbar 提供的方法滚动
|
||||
scrollbarRef.value.setScrollLeft(Math.max(0, scrollLeft))
|
||||
})
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
scrollToActiveStep()
|
||||
}, 1000)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.step-scrollbar {
|
||||
margin-top: 12px;
|
||||
white-space: nowrap;
|
||||
|
||||
.step-wrapper {
|
||||
display: inline-block;
|
||||
/* 让wrapper宽度由内容决定 */
|
||||
width: 100%;
|
||||
/* 至少和容器一样宽 */
|
||||
white-space: nowrap;
|
||||
/* 防止 flex 项换行 */
|
||||
|
||||
margin: 10px 0 20px 0;
|
||||
|
||||
:deep(.el-steps) {
|
||||
flex-wrap: nowrap;
|
||||
width: calc(100% - 150px);
|
||||
|
||||
.el-step {
|
||||
flex-shrink: 0 !important;
|
||||
/* 禁止步骤项被压缩 */
|
||||
min-width: 150px;
|
||||
/* 每个步骤的最小宽度,按需调整 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -43,6 +43,7 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="所属地图" prop="mapName" show-overflow-tooltip />
|
||||
<el-table-column align="center" label="创建人" prop="createBy" show-overflow-tooltip />
|
||||
<el-table-column align="center" label="创建时间" prop="createTime" show-overflow-tooltip />
|
||||
<el-table-column align="center" class-name="small-padding fixed-width" label="操作" width="260">
|
||||
@ -69,11 +70,26 @@
|
||||
<el-input v-model="form.taskName" placeholder="请输入任务名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="任务类型" prop="taskType">
|
||||
<el-select v-model="queryParams.taskType" clearable placeholder="请选择任务类型" style="width: 100%">
|
||||
<el-select v-model="form.taskType" clearable placeholder="请选择任务类型" style="width: 100%">
|
||||
<el-option label="巡检任务" value="1" />
|
||||
<el-option label="讲解任务" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属地图" prop="mapId">
|
||||
<el-select
|
||||
v-model="form.mapId"
|
||||
clearable
|
||||
placeholder="请选择所属地图"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in mapList"
|
||||
:key="item.id"
|
||||
:label="item.mapName"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" :rows="2" placeholder="请输入备注" type="textarea" />
|
||||
</el-form-item>
|
||||
@ -88,22 +104,54 @@
|
||||
|
||||
<!-- 点位绑定弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" title="点位绑定" width="800px">
|
||||
<el-table ref="pointRef" v-loading="loading" height="100%" :data="pointTableDataList"
|
||||
@row-click="handleRowClick">
|
||||
<el-table-column align="center" type="selection" width="55" />
|
||||
<el-table-column align="center" label="点位名称" prop="waypointName" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
align="center"
|
||||
label="所属地图"
|
||||
prop="mapName"
|
||||
:filters="[
|
||||
{ text: '厂区', value: '3e2116b5e15c4e999079fbe11bbb6392' },
|
||||
{ text: '车间', value: '269a6d3b480f4f11a7d36326885c931b' }
|
||||
]"
|
||||
:filter-method="filterHandler"
|
||||
show-overflow-tooltip />
|
||||
<el-table-column align="center" label="点位描述" prop="remark" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<el-row>
|
||||
<TableSearch :queryParams="pointQueryParams" :showSearch="showSearch" label-width="90px" @refresh="resetBindPointQuery" @search="handleBindPointQuery">
|
||||
<template #one>
|
||||
<el-col :lg="12" :md="12" :sm="24" :xl="12" :xs="24" :xxl="12">
|
||||
<el-form-item label="点位名称" prop="waypointName">
|
||||
<el-input v-model="pointQueryParams.waypointName" clearable placeholder="请输入点位名称"
|
||||
style="width: 100%" @keyup.enter="handleBindPointQuery" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
</TableSearch>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<div style="height: 55vh">
|
||||
<div class="task-title">待选择点位</div>
|
||||
|
||||
<draggable :animation="340" :forceFallback="true" :list="taskLeftList" class="draggable-group"
|
||||
ghostClass="dragClass" group="people" itemKey="name">
|
||||
<template #item="{ element, index }">
|
||||
<div class="list-group-item">
|
||||
<el-icon>
|
||||
<Tools />
|
||||
</el-icon>
|
||||
{{ element.waypointName }}
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div style="height: 55vh">
|
||||
<div class="task-title">点位排序</div>
|
||||
<draggable :animation="340" :forceFallback="true" :list="taskRightList" class="draggable-group"
|
||||
ghostClass="dragClass" group="people" itemKey="name">
|
||||
<template #item="{ element, index }">
|
||||
<div class="list-group-item">
|
||||
{{ index + 1 }} 、{{ element.waypointName }}
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="custom-block">
|
||||
<div class="mb5">1、将待选择点位拖拽至点位排序中</div>
|
||||
<div>2、点位排序中可拖拽自定义其顺序</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="bindPoint">确 定</el-button>
|
||||
@ -121,6 +169,9 @@ import TableSearch from "@/components/TableSearch/index.vue";
|
||||
|
||||
import { useContainerHeight } from "@/hooks/tableHeight";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import draggable from "vuedraggable";
|
||||
import { reactive, ref } from "vue";
|
||||
import { getMapList } from "@/api/inspection/map";
|
||||
|
||||
const topContainerRef = ref();
|
||||
const containerHeight = useContainerHeight(topContainerRef);
|
||||
@ -148,6 +199,12 @@ const data = reactive({
|
||||
taskName: [
|
||||
{ required: true, message: "任务名称不能为空", trigger: "blur" },
|
||||
],
|
||||
taskType: [
|
||||
{ required: true, message: "请选择任务类型", trigger: "blur" },
|
||||
],
|
||||
mapId: [
|
||||
{ required: true, message: "请选择所属地图", trigger: "blur" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@ -160,8 +217,9 @@ function getList() {
|
||||
tableDataList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// 取消按钮
|
||||
@ -194,6 +252,17 @@ function handleAdd() {
|
||||
title.value = "添加任务";
|
||||
}
|
||||
|
||||
const mapList = ref([]);
|
||||
// 获取地图列表
|
||||
const getMapArr = () => {
|
||||
getMapList({
|
||||
pageNum: 1,
|
||||
pageSize: 10000,
|
||||
}).then((response) => {
|
||||
mapList.value = response.rows;
|
||||
});
|
||||
}
|
||||
|
||||
/** 修改按钮操作 */
|
||||
function handleUpdate(row) {
|
||||
form.value = { ...row };
|
||||
@ -239,74 +308,78 @@ function handleDelete(row) {
|
||||
});
|
||||
}
|
||||
|
||||
const pointQueryParams = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 1000000,
|
||||
waypointName: null,
|
||||
mapId: null
|
||||
})
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleBindPointQuery() {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetBindPointQuery() {
|
||||
handleBindPointQuery();
|
||||
}
|
||||
|
||||
const taskId = ref(null);
|
||||
const handleBindPoint = async (row) => {
|
||||
dialogVisible.value = true;
|
||||
taskId.value = row.id;
|
||||
await getPointTableTable();
|
||||
getBoundPoints(taskId.value);
|
||||
pointQueryParams.mapId = row.mapId;
|
||||
getBindPointByTaskId(row.id)
|
||||
getPointListData()
|
||||
};
|
||||
|
||||
const getBoundPoints = (taskId) => {
|
||||
getWaypointByTaskId(taskId).then((res) => {
|
||||
const boundPointIds = res.data.map((point) => point.id);
|
||||
// 这里需要等点位数据加载完成后再设置选中状态
|
||||
nextTick(() => {
|
||||
if (pointRef.value) {
|
||||
pointRef.value.clearSelection(); // 先清除之前的选择状态
|
||||
pointTableDataList.value.forEach((point) => {
|
||||
if (boundPointIds.includes(point.id)) {
|
||||
pointRef.value.toggleRowSelection(point, true); // 设置选中状态
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const pointTableDataList = ref([]);
|
||||
|
||||
const getPointTableTable = async () => {
|
||||
loading.value = true;
|
||||
const res = await getPointList({
|
||||
pageNum: 1,
|
||||
pageSize: 10000,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
pointTableDataList.value = res.rows;
|
||||
/** 查询点位列表 */
|
||||
const getPointListData = async () => {
|
||||
const res = await getPointList(pointQueryParams)
|
||||
if (res.code === 200) {
|
||||
let ids = []
|
||||
console.log(taskRightList.value)
|
||||
if (taskRightList.value) {
|
||||
ids = taskRightList.value.map(item => item.id)
|
||||
}
|
||||
loading.value = false;
|
||||
taskLeftList.value = res.rows.filter(item => {
|
||||
return !ids.includes(item.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const pointRef = ref();
|
||||
|
||||
const handleRowClick = (row) => {
|
||||
pointRef.value.toggleRowSelection(row);
|
||||
};
|
||||
|
||||
const filterHandler = (value,row,column) => {
|
||||
return row["mapId"] === value
|
||||
const getBindPointByTaskId = async (taskId) => {
|
||||
const res = await getWaypointByTaskId(taskId)
|
||||
if (res.code === 200) {
|
||||
taskRightList.value = res.data;
|
||||
}
|
||||
}
|
||||
|
||||
const bindPoint = () => {
|
||||
// 这里可以调用绑定点位的接口,传递ids.value和当前任务的id
|
||||
const rows = pointRef.value.getSelectionRows(); // 绑定完成后清除选择状态
|
||||
const ids = rows.map((row) => row.id);
|
||||
bindWaypointsToTask(taskId.value, ids).then(() => {
|
||||
ElMessage.success("绑定成功");
|
||||
pointRef.value?.clearSelection();
|
||||
cancelPoint();
|
||||
});
|
||||
const taskLeftList = ref([])
|
||||
const taskRightList = ref([])
|
||||
|
||||
const bindPoint = async () => {
|
||||
if (taskRightList.value.length === 0) {
|
||||
ElMessage.warning("请选择点位")
|
||||
return
|
||||
}
|
||||
const ids = taskRightList.value.map(item => item.id)
|
||||
const res = await bindWaypointsToTask(taskId.value, ids)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("绑定点位成功")
|
||||
cancelPoint()
|
||||
}
|
||||
};
|
||||
|
||||
const cancelPoint = () => {
|
||||
dialogVisible.value = false;
|
||||
taskId.value = null;
|
||||
pointRef.value?.clearFilter()
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
getMapArr();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user