Compare commits

...

3 Commits

Author SHA1 Message Date
2d2854814b feat: 执行任务 2026-06-05 14:50:29 +08:00
7590d5a986 feat: 绑定点位 2026-06-05 09:07:44 +08:00
8e53faad52 feat: 执行任务 2026-06-04 16:41:29 +08:00
13 changed files with 911 additions and 77 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View 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'
})
}

View 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

View 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

View 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

View 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

View File

@ -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'
});

View File

@ -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);

View 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>

View 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>

View File

@ -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.valueid
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>