feat: 执行任务
This commit is contained in:
parent
7590d5a986
commit
2d2854814b
@ -8,5 +8,5 @@ VITE_APP_ENV = 'development'
|
|||||||
VITE_APP_BASE_API = '/dev-api'
|
VITE_APP_BASE_API = '/dev-api'
|
||||||
|
|
||||||
VITE_API_URL = http://192.168.28.10: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
|
||||||
|
|
||||||
|
|||||||
@ -14,4 +14,4 @@ VITE_BUILD_COMPRESS = gzip
|
|||||||
VITE_NODE_RED_URL = 'http://10.148.108.59:13080'
|
VITE_NODE_RED_URL = 'http://10.148.108.59:13080'
|
||||||
|
|
||||||
VITE_API_URL = http://192.168.28.10: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
|
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||||
VITE_BUILD_COMPRESS = gzip
|
VITE_BUILD_COMPRESS = gzip
|
||||||
VITE_API_URL = http://10.148.121.100:13080
|
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
|
||||||
@ -87,9 +87,8 @@ app.use(ElementPlus, {
|
|||||||
|
|
||||||
|
|
||||||
app.use(WebSocketPlugin, {
|
app.use(WebSocketPlugin, {
|
||||||
// url: import.meta.env.VITE_WS_URL,
|
url: import.meta.env.VITE_WS_URL,
|
||||||
url: 'ws://192.168.0.201:13080/ws',
|
// url: 'ws://192.168.0.201:13080/ws',
|
||||||
// url: 'ws://10.148.209.5:13080/ws',
|
|
||||||
userId: 'your_user_id'
|
userId: 'your_user_id'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -10,14 +10,18 @@
|
|||||||
<div class="task-content">
|
<div class="task-content">
|
||||||
<div class="task-title">{{ task.taskName }}</div>
|
<div class="task-title">{{ task.taskName }}</div>
|
||||||
<div class="task-actions">
|
<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 === '1'" name="pause" :size="24" color="#FFB300"
|
||||||
<SvgIcon class="icon" v-if="task.status === '3' || task.status === '5'" name="start" :size="24" color="#00FF88" @click="startTask(task)" />
|
@click="pauseTask(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 === '3' || task.status === '5'" name="start"
|
||||||
<SvgIcon class="icon" v-if="task.status === '5'" name="remove" :size="24" color="#FFB300" @click="removeTask(task)" />
|
: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>
|
</div>
|
||||||
<div class="robot">机器人:{{ task.robotName }}</div>
|
<div class="robot">机器人:{{ task.robotName }}</div>
|
||||||
<el-progress :color="customColorMethod" :percentage="60" />
|
<el-progress :color="customColorMethod" :percentage="parseFloat(taskRunningInfo[task.id]?.progress.toFixed(2)) || 0" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -28,25 +32,33 @@
|
|||||||
<div class="label" :class="{ active: activePanel === 'history' }" @click="activePanel = 'history'">历史记录
|
<div class="label" :class="{ active: activePanel === 'history' }" @click="activePanel = 'history'">历史记录
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="context-container">
|
||||||
<div v-if="activePanel === 'current'">
|
<div v-if="activePanel === 'current'">
|
||||||
<!-- 当前任务内容 -->
|
<!-- 当前任务内容 -->
|
||||||
<div class="task">
|
<div class="task" v-for="runTask in runingTaskList">
|
||||||
<div class="task-header">
|
<div class="task-header">
|
||||||
|
<div class="header-container">
|
||||||
<div class="task-info">
|
<div class="task-info">
|
||||||
<div class="title">厂区巡检</div>
|
<div class="title">{{ runTask.taskName }}</div>
|
||||||
<div class="id">ID: 123456</div>
|
<div class="id">ID: {{ runTask.taskId }}</div>
|
||||||
</div>
|
|
||||||
<div>执行中</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="robot">
|
<div class="robot">
|
||||||
<div>机器人:xxxx</div>
|
<div>机器人:{{ runTask.robotName }}</div>
|
||||||
<div>IP: 192.168.1.100</div>
|
<div>开始时间: {{ formatDate(runTask.startTime) }}</div>
|
||||||
<div>开始时间: 2026-06-01 10:00:00</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-tag :type="runTask.status === '3' ? 'primary' : 'warning'">
|
||||||
|
{{ taskStatusMap[runTask.status] || '未知状态' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="progress-label">执行进度</div>
|
<div class="progress-label">执行进度</div>
|
||||||
<el-progress :color="customColorMethod" :percentage="60" />
|
<el-progress :color="customColorMethod"
|
||||||
<taskSteps />
|
: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 class="run-log-container">
|
||||||
<div>执行日志</div>
|
<div>执行日志</div>
|
||||||
<div class="log-container">
|
<div class="log-container">
|
||||||
@ -98,7 +110,7 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<pagination v-show="total > 0" v-model:limit="queryParams.pageSize"
|
<pagination v-show="total > 0" v-model:limit="queryParams.pageSize"
|
||||||
v-model:page="queryParams.pageNum" :total="total" @pagination="getList" />
|
v-model:page="queryParams.pageNum" :total="total" @pagination="getHistoryList" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -109,7 +121,8 @@
|
|||||||
<el-dialog title="添加任务" v-model="open" width="500px" append-to-body>
|
<el-dialog title="添加任务" v-model="open" width="500px" append-to-body>
|
||||||
<el-form ref="runTaskRef" :model="form" :rules="rules" label-width="100px">
|
<el-form ref="runTaskRef" :model="form" :rules="rules" label-width="100px">
|
||||||
<el-form-item label="选择任务" prop="taskId">
|
<el-form-item label="选择任务" prop="taskId">
|
||||||
<el-select v-model="form.taskId" placeholder="请选择任务" clearable style="width: 100%">
|
<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-option v-for="task in taskList" :key="task.id" :label="task.taskName" :value="task.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -133,15 +146,17 @@
|
|||||||
import SvgIcon from "@/components/SvgIcon";
|
import SvgIcon from "@/components/SvgIcon";
|
||||||
import taskSteps from "./task-steps.vue";
|
import taskSteps from "./task-steps.vue";
|
||||||
import { useContainerHeight } from "@/hooks/tableHeight";
|
import { useContainerHeight } from "@/hooks/tableHeight";
|
||||||
import { getTaskList } from "@/api/inspection/task";
|
import { getTaskList, getWaypointByTaskId } from "@/api/inspection/task";
|
||||||
import { getRobotList } from "@/api/inspection/robot";
|
import { getRobotList } from "@/api/inspection/robot";
|
||||||
import { onMounted } from "vue";
|
import { onMounted, onUnmounted } from "vue";
|
||||||
import { addRunTask, getRunTaskList, startRunTask, pauseRunTask, stopRunTask, resumeRunTask, deleteRunTask } from "@/api/inspection/runTask";
|
import { addRunTask, getRunTaskList, startRunTask, pauseRunTask, stopRunTask, resumeRunTask, deleteRunTask } from "@/api/inspection/runTask";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { formatDate } from "@/utils/index";
|
import { formatDate } from "@/utils/index";
|
||||||
|
import { debounce } from 'lodash'; // 引入 lodash 的防抖函数
|
||||||
|
|
||||||
const topContainerRef = ref();
|
const topContainerRef = ref();
|
||||||
const containerHeight = useContainerHeight(topContainerRef);
|
const containerHeight = useContainerHeight(topContainerRef);
|
||||||
|
const socket = inject('ws');
|
||||||
|
|
||||||
const taskStatusMap = {
|
const taskStatusMap = {
|
||||||
'0': '已完成',
|
'0': '已完成',
|
||||||
@ -176,10 +191,11 @@ const getTaskData = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRobotData = () => {
|
const getRobotData = (mapId = null) => {
|
||||||
getRobotList({
|
getRobotList({
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10000,
|
pageSize: 10000,
|
||||||
|
currentMapId: mapId
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
robotList.value = res.rows;
|
robotList.value = res.rows;
|
||||||
})
|
})
|
||||||
@ -190,6 +206,15 @@ const open = ref(false);
|
|||||||
const addTask = () => {
|
const addTask = () => {
|
||||||
open.value = true;
|
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 runTaskRef = ref();
|
||||||
const confirmTaskDialog = () => {
|
const confirmTaskDialog = () => {
|
||||||
runTaskRef.value.validate((valid) => {
|
runTaskRef.value.validate((valid) => {
|
||||||
@ -210,18 +235,31 @@ const cancelTaskDialog = () => {
|
|||||||
open.value = false;
|
open.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const runingTaskList = ref([])
|
||||||
|
/**
|
||||||
|
* 获取任务队列
|
||||||
|
*/
|
||||||
const getList = () => {
|
const getList = () => {
|
||||||
loading.value = true;
|
|
||||||
getRunTaskList({
|
getRunTaskList({
|
||||||
statusList: ['1', '3', '5'],
|
statusList: ['1', '3', '5'],
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
runTaskList.value = res.rows;
|
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
|
||||||
}
|
}
|
||||||
loading.value = false;
|
|
||||||
}).catch(() => {
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const customColorMethod = (percentage) => {
|
const customColorMethod = (percentage) => {
|
||||||
@ -293,6 +331,9 @@ const removeTask = async (task) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取历史执行任务
|
||||||
|
*/
|
||||||
const getHistoryList = () => {
|
const getHistoryList = () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
getRunTaskList({
|
getRunTaskList({
|
||||||
@ -310,11 +351,73 @@ const getHistoryList = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(() => {
|
onMounted(() => {
|
||||||
getTaskData();
|
getTaskData();
|
||||||
getRobotData();
|
getRobotData();
|
||||||
getList();
|
getList();
|
||||||
getHistoryList();
|
getHistoryList();
|
||||||
|
subscribeDebounced(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
subscribeDebounced(false)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -404,16 +507,26 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-container {
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.task {
|
.task {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
.task-header {
|
.task-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
.task-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 40px;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -433,15 +546,18 @@ onMounted(() => {
|
|||||||
color: #666;
|
color: #666;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.progress-label {
|
.progress-label {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.run-log-container {
|
.run-log-container {
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
.log-container {
|
.log-container {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|||||||
@ -2,16 +2,26 @@
|
|||||||
<el-scrollbar ref="scrollbarRef" style="width: 100%;" class="step-scrollbar">
|
<el-scrollbar ref="scrollbarRef" style="width: 100%;" class="step-scrollbar">
|
||||||
<div class="step-wrapper" ref="stepWrapperRef">
|
<div class="step-wrapper" ref="stepWrapperRef">
|
||||||
<el-steps :active="activeStep" finish-status="success">
|
<el-steps :active="activeStep" finish-status="success">
|
||||||
<el-step v-for="value in 20" :key="value" :title="'点位' + value" :description="'点位' + value + '描述'" />
|
<el-step v-for="item in list" :key="value" :title="item.waypointName" :description="item.remark" />
|
||||||
</el-steps>
|
</el-steps>
|
||||||
</div>
|
</div>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
list: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
activeStep: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const scrollbarRef = ref(null)
|
const scrollbarRef = ref(null)
|
||||||
const stepWrapperRef = ref(null)
|
const stepWrapperRef = ref(null)
|
||||||
|
|
||||||
const activeStep = ref(10)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 滚动到当前激活的步骤
|
* 滚动到当前激活的步骤
|
||||||
@ -27,10 +37,10 @@ const scrollToActiveStep = () => {
|
|||||||
|
|
||||||
// 获取所有 step 元素
|
// 获取所有 step 元素
|
||||||
const steps = stepWrapperRef.value?.querySelectorAll('.el-step')
|
const steps = stepWrapperRef.value?.querySelectorAll('.el-step')
|
||||||
if (!steps || !steps[activeStep.value]) return
|
if (!steps || !steps[props.activeStep]) return
|
||||||
|
|
||||||
// 计算激活 step 的位置
|
// 计算激活 step 的位置
|
||||||
const activeEl = steps[activeStep.value]
|
const activeEl = steps[props.activeStep]
|
||||||
const stepLeft = activeEl.offsetLeft
|
const stepLeft = activeEl.offsetLeft
|
||||||
const stepWidth = activeEl.offsetWidth
|
const stepWidth = activeEl.offsetWidth
|
||||||
|
|
||||||
@ -55,13 +65,16 @@ setTimeout(() => {
|
|||||||
.step-wrapper {
|
.step-wrapper {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
/* 让wrapper宽度由内容决定 */
|
/* 让wrapper宽度由内容决定 */
|
||||||
min-width: 100%;
|
width: 100%;
|
||||||
/* 至少和容器一样宽 */
|
/* 至少和容器一样宽 */
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
/* 防止 flex 项换行 */
|
/* 防止 flex 项换行 */
|
||||||
|
|
||||||
|
margin: 10px 0 20px 0;
|
||||||
|
|
||||||
:deep(.el-steps) {
|
:deep(.el-steps) {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
width: calc(100% - 150px);
|
||||||
|
|
||||||
.el-step {
|
.el-step {
|
||||||
flex-shrink: 0 !important;
|
flex-shrink: 0 !important;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user