|
|
@@ -0,0 +1,4280 @@
|
|
|
+<template>
|
|
|
+ <div class="navigation-container">
|
|
|
+ <div class="map-stage" ref="mapStage">
|
|
|
+ <div class="main-content">
|
|
|
+ <!-- 当前操作类型标记 -->
|
|
|
+ <div class="hand-ment-mark">
|
|
|
+ <el-tag type="danger" effect="dark" size="small" v-if="nowHandMenu">{{ nowHandMenu }}</el-tag>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 障碍物警告提示 -->
|
|
|
+ <div class="obstacle-warning" v-if="obstacleWarning.show" :class="'obstacle-' + obstacleWarning.level">
|
|
|
+ <div class="warning-content">
|
|
|
+ <div class="warning-icon">
|
|
|
+ <i class="el-icon-warning-outline" v-if="obstacleWarning.level === 'warning'"></i>
|
|
|
+ <i class="el-icon-error" v-if="obstacleWarning.level === 'danger'"></i>
|
|
|
+ <i class="el-icon-info" v-if="obstacleWarning.level === 'info'"></i>
|
|
|
+ </div>
|
|
|
+ <div class="warning-text">
|
|
|
+ <div class="warning-title">障碍物检测</div>
|
|
|
+ <div class="warning-message">{{ obstacleWarning.message }}</div>
|
|
|
+ <div class="obstacle-distances" v-if="obstacleData.left > 0 || obstacleData.front > 0 || obstacleData.right > 0">
|
|
|
+ <span v-if="obstacleData.left > 0" class="distance-item">
|
|
|
+ 左: {{ obstacleData.left.toFixed(1) }}m
|
|
|
+ </span>
|
|
|
+ <span v-if="obstacleData.front > 0" class="distance-item">
|
|
|
+ 前: {{ obstacleData.front.toFixed(1) }}m
|
|
|
+ </span>
|
|
|
+ <span v-if="obstacleData.right > 0" class="distance-item">
|
|
|
+ 右: {{ obstacleData.right.toFixed(1) }}m
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="warning-close" @click="closeObstacleWarning">
|
|
|
+ <i class="el-icon-close"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 遇障停车提示(基于run_state) -->
|
|
|
+ <div class="obstacle-stop-warning" v-if="obstacleStopState.showWarning">
|
|
|
+ <div class="stop-warning-content">
|
|
|
+ <div class="countdown-circle">
|
|
|
+ <div class="countdown-number">
|
|
|
+ {{ obstacleStopState.replanTimeLeft > 999 ? '999+' : obstacleStopState.replanTimeLeft }}
|
|
|
+ </div>
|
|
|
+ <div class="countdown-label">秒</div>
|
|
|
+ </div>
|
|
|
+ <div class="stop-warning-text">
|
|
|
+ <div class="stop-warning-title">遇障停车</div>
|
|
|
+ <div class="stop-warning-message">检测到前方障碍物,系统将自动重规划路径</div>
|
|
|
+ <div class="stop-warning-state">当前状态: {{ obstacleStopState.runStateText }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="stop-warning-actions">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ @click="handleReplanRequest"
|
|
|
+ :disabled="obstacleStopState.isReplanning"
|
|
|
+ >
|
|
|
+ {{ obstacleStopState.isReplanning ? '重规划中...' : '立即重规划' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 地图组件 -->
|
|
|
+ <OlMap
|
|
|
+ ref="olmap"
|
|
|
+ :width="olWidth + 'px'"
|
|
|
+ :height="olHeight + 'px'"
|
|
|
+ backgroundColor="#F5F5F5"
|
|
|
+ :mapName="mapName"
|
|
|
+ :pointSwitch="settingParams.pointId"
|
|
|
+ :baseLayerShow="settingParams.baseMap" :robotPoseData="laserPositionData"
|
|
|
+ :pointSelectionEnabled="selectPointMode" :poseInitEnable="initPoseMode" @addNowPoint="addNowPoint"
|
|
|
+ :isRobotFollow="settingParams.follow" @initNavigationResult="initNavigationResult"
|
|
|
+ :showPointcloud="settingParams.pointCloud"
|
|
|
+ :showRoadNetwork="settingParams.showRoadNetwork"
|
|
|
+ :showDefaultControls="false"></OlMap>
|
|
|
+
|
|
|
+ <!-- 左侧工具条浮层 -->
|
|
|
+ <MapToolbar
|
|
|
+ class="nav-toolbar"
|
|
|
+ preset="nav"
|
|
|
+ :selectedKey="selectedKey"
|
|
|
+ :hasRobotPosition="true"
|
|
|
+ :isConnected="true"
|
|
|
+ :isBusy="false"
|
|
|
+ :isFullscreen="isFullscreen"
|
|
|
+ @zoom-in="onZoomIn"
|
|
|
+ @zoom-out="onZoomOut"
|
|
|
+ @center-robot="onCenterRobot"
|
|
|
+ @toggle-fullscreen="onToggleFullscreen"
|
|
|
+ @confirm-init="handleConfirmInit"
|
|
|
+ @confirm-reboot="handleConfirmReboot"
|
|
|
+ @confirm-stop="handleConfirmStop"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 右侧信息面板浮层 -->
|
|
|
+ <RightPanel
|
|
|
+ mode="nav"
|
|
|
+ panelType="nav"
|
|
|
+ :overlay="true"
|
|
|
+ v-model:visible="rightVisible"
|
|
|
+ :realtime-info="realtimeInfo"
|
|
|
+ :waypoint-list="waypoints"
|
|
|
+ :task-list="tasks"
|
|
|
+ :setting-params="settingParams"
|
|
|
+ :emergency-stop-enabled="emergencyStopEnabled"
|
|
|
+ :navigation-stack-status="navigationStackStatus"
|
|
|
+ :current-navigation-task="currentNavigationTask"
|
|
|
+ :navigation-status="navigationStatus"
|
|
|
+ :is-navigating="isNavigating"
|
|
|
+ @wp-select="onWpSelect"
|
|
|
+ @wp-send="onWpSend"
|
|
|
+ @wp-create="onWpCreate"
|
|
|
+ @wp-edit="onWpEdit"
|
|
|
+ @wp-remove="onWpRemove"
|
|
|
+ @wp-move-up="onWpMoveUp"
|
|
|
+ @wp-move-down="onWpMoveDown"
|
|
|
+ @wp-batch-remove="onWpBatchRemove"
|
|
|
+ @wp-goto="onWpGoto"
|
|
|
+ @wp-goto-single="onWpGotoSingle"
|
|
|
+ @wp-create-task="onWpCreateTask"
|
|
|
+ @wp-selection-change="onWpSelectionChange"
|
|
|
+ @map-select-mode-change="onMapSelectModeChange"
|
|
|
+ @task-view="onTaskView"
|
|
|
+ @task-edit="onTaskEdit"
|
|
|
+ @task-start="onTaskStart"
|
|
|
+ @task-pause="onTaskPause"
|
|
|
+ @task-resume="onTaskResume"
|
|
|
+ @task-stop="onTaskStop"
|
|
|
+ @task-remove="onTaskRemove"
|
|
|
+ @setting-change="onSettingChange"
|
|
|
+ @emergency-stop-release="executeEmergencyStopRelease"
|
|
|
+ @navigation-pause="handleNavigationPause"
|
|
|
+ @navigation-resume="handleNavigationResume"
|
|
|
+ @navigation-stop="handleNavigationStop"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 目标点编辑对话框 -->
|
|
|
+ <el-dialog
|
|
|
+ title="目标点编辑"
|
|
|
+ v-model="pointEditDiaShow"
|
|
|
+ width="480px"
|
|
|
+ @close="clearActionDia"
|
|
|
+ class="waypoint-edit-dialog"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ center
|
|
|
+ >
|
|
|
+ <div class="dialog-content">
|
|
|
+ <el-form :model="pointEditData" label-width="90px" size="small" class="waypoint-form">
|
|
|
+ <div class="form-section">
|
|
|
+ <h4 class="section-title">
|
|
|
+ <i class="el-icon-location-outline"></i>
|
|
|
+ 位置信息
|
|
|
+ </h4>
|
|
|
+ <el-form-item label="X坐标(m)">
|
|
|
+ <el-input v-model="pointEditData.x" placeholder="请输入X坐标值" class="coordinate-input">
|
|
|
+ </el-input>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="Y坐标(m)">
|
|
|
+ <el-input v-model="pointEditData.y" placeholder="请输入Y坐标值" class="coordinate-input">
|
|
|
+ </el-input>
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-section">
|
|
|
+ <h4 class="section-title">
|
|
|
+ <i class="el-icon-guide"></i>
|
|
|
+ 路径配置
|
|
|
+ </h4>
|
|
|
+ <el-form-item label="规划类型">
|
|
|
+ <el-select v-model="pointEditData.type" placeholder="请选择路径类型" style="width: 100%">
|
|
|
+ <el-option v-for="item in planOptions" :key="item.value" :label="item.label" :value="item.value">
|
|
|
+ <span style="float: left">{{ item.label }}</span>
|
|
|
+ <span style="float: right; color: #8492a6; font-size: 13px">{{ item.value === 0 ? '自由规划' : '路网约束' }}</span>
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-section">
|
|
|
+ <h4 class="section-title">
|
|
|
+ <i class="el-icon-setting"></i>
|
|
|
+ 动作配置
|
|
|
+ <el-button type="text" size="small" @click="appendActionMenu" class="add-action-btn">
|
|
|
+ <i class="el-icon-plus"></i><span>添加动作</span>
|
|
|
+ </el-button>
|
|
|
+ </h4>
|
|
|
+ <div class="action-list">
|
|
|
+ <div v-for="(item, index) in pointEditData.actionMenuList" :key="index" class="action-item">
|
|
|
+ <div class="action-header">
|
|
|
+ <span class="action-index">{{ index + 1 }}</span>
|
|
|
+ <el-select v-model="item.value" placeholder="请选择动作" size="small" @change="changeAction(index)" style="flex: 1;">
|
|
|
+ <el-option v-for="option in actionOptions" :key="option.value" :label="option.label" :value="option.value"></el-option>
|
|
|
+ </el-select>
|
|
|
+ <el-button v-if="index > 0" type="text" size="small" @click="removeActionMenu(index)" class="remove-btn">
|
|
|
+ <i class="el-icon-close"></i>
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div v-if="'other' in item" class="action-params">
|
|
|
+ <el-input v-model="item.other" placeholder="等待时间" size="small" style="width: 120px;">
|
|
|
+ <template #append>秒</template>
|
|
|
+ </el-input>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <span class="dialog-footer">
|
|
|
+ <el-button @click="pointEditDiaShow = false">取 消</el-button>
|
|
|
+ <el-button type="primary" @click="submitEditPoint">
|
|
|
+ <i class="el-icon-check"></i> 保存修改
|
|
|
+ </el-button>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 创建任务对话框 -->
|
|
|
+ <el-dialog
|
|
|
+ title="创建任务"
|
|
|
+ v-model="taskGenerateDiaShow"
|
|
|
+ width="480px"
|
|
|
+ @close="closeTaskGenerate"
|
|
|
+ class="task-create-dialog"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ center
|
|
|
+ >
|
|
|
+ <div class="dialog-content">
|
|
|
+ <el-form :model="generateTaskParam" label-width="90px" size="small" class="task-form">
|
|
|
+ <div class="form-section">
|
|
|
+ <h4 class="section-title">
|
|
|
+ <i class="el-icon-s-order"></i>
|
|
|
+ 任务信息
|
|
|
+ </h4>
|
|
|
+ <el-form-item label="任务名称">
|
|
|
+ <el-input v-model="generateTaskParam.taskName" placeholder="请输入任务名称" class="task-input">
|
|
|
+ </el-input>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="执行次数">
|
|
|
+ <el-input-number
|
|
|
+ v-model="generateTaskParam.count"
|
|
|
+ controls-position="right"
|
|
|
+ :min="1"
|
|
|
+ :max="100"
|
|
|
+ class="task-input-number"
|
|
|
+ style="width: 100%"
|
|
|
+ ></el-input-number>
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-section">
|
|
|
+ <h4 class="section-title">
|
|
|
+ <i class="el-icon-time"></i>
|
|
|
+ 执行计划
|
|
|
+ </h4>
|
|
|
+ <el-form-item label="开始时间">
|
|
|
+ <el-time-picker
|
|
|
+ v-model="generateTaskParam.time"
|
|
|
+ format="HH:mm"
|
|
|
+ value-format="HH:mm:ss"
|
|
|
+ :picker-options="{
|
|
|
+ selectableRange: '00:00:00 - 23:59:59'
|
|
|
+ }"
|
|
|
+ placeholder="选择时间"
|
|
|
+ class="task-time-picker"
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ </el-time-picker>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="执行日期">
|
|
|
+ <el-checkbox-group v-model="generateTaskParam.date" class="task-date-group">
|
|
|
+ <el-checkbox label="1">周一</el-checkbox>
|
|
|
+ <el-checkbox label="2">周二</el-checkbox>
|
|
|
+ <el-checkbox label="3">周三</el-checkbox>
|
|
|
+ <el-checkbox label="4">周四</el-checkbox>
|
|
|
+ <el-checkbox label="5">周五</el-checkbox>
|
|
|
+ <el-checkbox label="6">周六</el-checkbox>
|
|
|
+ <el-checkbox label="7">周日</el-checkbox>
|
|
|
+ </el-checkbox-group>
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <span class="dialog-footer">
|
|
|
+ <el-button @click="closeTaskGenerate()">取 消</el-button>
|
|
|
+ <el-button type="primary" @click="submitTaskGenerate">
|
|
|
+ <i class="el-icon-check"></i> 创建任务
|
|
|
+ </el-button>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 任务编辑对话框 -->
|
|
|
+ <el-dialog
|
|
|
+ title="编辑任务"
|
|
|
+ v-model="taskEditDiaShow"
|
|
|
+ width="480px"
|
|
|
+ @close="closeTaskEdit"
|
|
|
+ class="task-create-dialog"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ center
|
|
|
+ >
|
|
|
+ <div class="dialog-content">
|
|
|
+ <el-form :model="editTaskParam" label-width="90px" size="small" class="task-form">
|
|
|
+ <div class="form-section">
|
|
|
+ <h4 class="section-title">
|
|
|
+ <i class="el-icon-s-order"></i>
|
|
|
+ 任务信息
|
|
|
+ </h4>
|
|
|
+ <el-form-item label="任务名称">
|
|
|
+ <el-input v-model="editTaskParam.taskName" placeholder="请输入任务名称" class="task-input">
|
|
|
+ </el-input>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="执行次数">
|
|
|
+ <el-input-number
|
|
|
+ v-model="editTaskParam.count"
|
|
|
+ controls-position="right"
|
|
|
+ :min="1"
|
|
|
+ :max="100"
|
|
|
+ class="task-input-number"
|
|
|
+ style="width: 100%"
|
|
|
+ ></el-input-number>
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-section">
|
|
|
+ <h4 class="section-title">
|
|
|
+ <i class="el-icon-time"></i>
|
|
|
+ 执行计划
|
|
|
+ </h4>
|
|
|
+ <el-form-item label="开始时间">
|
|
|
+ <el-time-picker
|
|
|
+ v-model="editTaskParam.time"
|
|
|
+ format="HH:mm"
|
|
|
+ value-format="HH:mm:ss"
|
|
|
+ :picker-options="{
|
|
|
+ selectableRange: '00:00:00 - 23:59:59'
|
|
|
+ }"
|
|
|
+ placeholder="选择时间"
|
|
|
+ class="task-time-picker"
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ </el-time-picker>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="执行日期">
|
|
|
+ <el-checkbox-group v-model="editTaskParam.date" class="task-date-group">
|
|
|
+ <el-checkbox label="1">周一</el-checkbox>
|
|
|
+ <el-checkbox label="2">周二</el-checkbox>
|
|
|
+ <el-checkbox label="3">周三</el-checkbox>
|
|
|
+ <el-checkbox label="4">周四</el-checkbox>
|
|
|
+ <el-checkbox label="5">周五</el-checkbox>
|
|
|
+ <el-checkbox label="6">周六</el-checkbox>
|
|
|
+ <el-checkbox label="7">周日</el-checkbox>
|
|
|
+ </el-checkbox-group>
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <span class="dialog-footer">
|
|
|
+ <el-button @click="closeTaskEdit()">取 消</el-button>
|
|
|
+ <el-button type="primary" @click="submitTaskEdit">
|
|
|
+ <i class="el-icon-check"></i> 保存修改
|
|
|
+ </el-button>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, reactive, computed, watch, onMounted, onBeforeUnmount, onActivated, nextTick } from 'vue'
|
|
|
+import { useRoute } from 'vue-router'
|
|
|
+import OlMap from "@/components/OlMap"
|
|
|
+import MapToolbar from "../components/shared/MapToolbar.vue"
|
|
|
+import RightPanel from "../components/shared/RightPanel.vue"
|
|
|
+import { FullscreenOperations, RobotPositionUtils } from "@/utils/map-operations"
|
|
|
+import { listTask, addTask, delTask, getTask, updateTask, updateTaskStatus } from "@/api/map/task"
|
|
|
+import { initWebSocket, disconnect, sendMessage } from "@/utils/websocket"
|
|
|
+import {
|
|
|
+ initPose,
|
|
|
+ initPoseByNid,
|
|
|
+ startNavigation,
|
|
|
+ stopNavigation,
|
|
|
+ restartNavigation,
|
|
|
+ startNavStandard,
|
|
|
+ gotoTargetByCoord,
|
|
|
+ gotoTarget,
|
|
|
+ requestPlanning,
|
|
|
+ startTask,
|
|
|
+ pauseTask,
|
|
|
+ resumeTask,
|
|
|
+ cancelTask,
|
|
|
+ replan,
|
|
|
+ emergencyStop,
|
|
|
+ releaseEmergencyStop
|
|
|
+} from "@/api/robot/mqtt"
|
|
|
+
|
|
|
+// 获取路由
|
|
|
+const route = useRoute()
|
|
|
+
|
|
|
+// WebSocket设备ID
|
|
|
+const wsDeviceId = ref('ld000001')
|
|
|
+
|
|
|
+// Refs
|
|
|
+const mapStage = ref(null)
|
|
|
+const olmap = ref(null)
|
|
|
+
|
|
|
+const nowHandMenu = ref('')
|
|
|
+const mapName = ref(route.params.mapName || '')
|
|
|
+const laserPositionData = reactive({
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ angle: 0
|
|
|
+})
|
|
|
+
|
|
|
+// 设置相关
|
|
|
+const settingDrawer = ref(false)
|
|
|
+const pointDrawer = ref(false)
|
|
|
+const taskDrawer = ref(false)
|
|
|
+const pointEditDiaShow = ref(false)
|
|
|
+const taskGenerateDiaShow = ref(false)
|
|
|
+const taskEditDiaShow = ref(false)
|
|
|
+const taskViewDiaShow = ref(false)
|
|
|
+
|
|
|
+const selectPointMode = ref(false)
|
|
|
+const initPoseMode = ref(false)
|
|
|
+
|
|
|
+const settingParams = reactive({
|
|
|
+ pointCloud: false,
|
|
|
+ baseMap: true,
|
|
|
+ pointId: false,
|
|
|
+ follow: false,
|
|
|
+ network: false,
|
|
|
+ showRoadNetwork: true
|
|
|
+})
|
|
|
+
|
|
|
+const single = ref(true)
|
|
|
+const multiple = ref(true)
|
|
|
+const taskDataList = ref([])
|
|
|
+
|
|
|
+// 目标点编辑数据
|
|
|
+const pointEditData = reactive({
|
|
|
+ id: '',
|
|
|
+ x: '',
|
|
|
+ y: '',
|
|
|
+ type: '',
|
|
|
+ actionMenuList: []
|
|
|
+})
|
|
|
+
|
|
|
+// 生成任务参数
|
|
|
+const generateTaskParam = reactive({
|
|
|
+ taskId: '',
|
|
|
+ taskName: '',
|
|
|
+ count: 1,
|
|
|
+ time: '',
|
|
|
+ date: [],
|
|
|
+ selectedWaypoints: []
|
|
|
+})
|
|
|
+
|
|
|
+// 编辑任务参数
|
|
|
+const editTaskParam = reactive({
|
|
|
+ originalTaskName: '',
|
|
|
+ taskId: '',
|
|
|
+ taskName: '',
|
|
|
+ count: 1,
|
|
|
+ time: '',
|
|
|
+ date: [],
|
|
|
+ selectedWaypoints: []
|
|
|
+})
|
|
|
+
|
|
|
+const taskViewData = ref({})
|
|
|
+const pointList = ref([])
|
|
|
+const pointIds = ref([])
|
|
|
+
|
|
|
+// 选项
|
|
|
+const planOptions = ref([
|
|
|
+ { label: '自由路径', value: 0 },
|
|
|
+ { label: '路网路径', value: 1 }
|
|
|
+])
|
|
|
+
|
|
|
+const actionOptions = ref([
|
|
|
+ { label: '原地等待', value: 0 },
|
|
|
+ { label: '开始录制', value: 1 },
|
|
|
+ { label: '结束录制', value: 2 },
|
|
|
+ { label: '添加建图轨迹', value: 3 },
|
|
|
+ { label: '挂钩挂载', value: 4 },
|
|
|
+ { label: '挂钩卸载', value: 5 }
|
|
|
+])
|
|
|
+
|
|
|
+const olWidth = ref(0)
|
|
|
+const olHeight = ref(0)
|
|
|
+
|
|
|
+const panelVisible = ref(true)
|
|
|
+const rightVisible = ref(true)
|
|
|
+const activeTab = ref('info')
|
|
|
+const lastTab = ref('info')
|
|
|
+
|
|
|
+const mockTasks = ref([
|
|
|
+ { id: 1, name: '巡检任务A', nodes: 5, status: 'idle' },
|
|
|
+ { id: 2, name: '运输任务B', nodes: 3, status: 'running' },
|
|
|
+ { id: 3, name: '清扫任务C', nodes: 8, status: 'paused' }
|
|
|
+])
|
|
|
+
|
|
|
+const realtimeInfo = reactive({
|
|
|
+ currentMap: route.params.mapName || 'Unknown',
|
|
|
+ currentTask: '',
|
|
|
+ speed: '',
|
|
|
+ speedCommand: '',
|
|
|
+ coordinates: '',
|
|
|
+ heading: '',
|
|
|
+ totalDistance: '',
|
|
|
+ registrationError: '',
|
|
|
+ batteryLevel: '',
|
|
|
+ batteryDetails: {
|
|
|
+ capacity: 0,
|
|
|
+ temperature: 0,
|
|
|
+ voltage: 0,
|
|
|
+ current: 0,
|
|
|
+ charging: false,
|
|
|
+ other: []
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const robotPoseData = reactive({
|
|
|
+ x: 1.813,
|
|
|
+ y: -63.931,
|
|
|
+ angle: 0.000
|
|
|
+})
|
|
|
+
|
|
|
+const isConnected = ref(true)
|
|
|
+const isBusy = ref(false)
|
|
|
+const isFullscreen = ref(false)
|
|
|
+
|
|
|
+const navigationStackStatus = ref('unknown')
|
|
|
+const emergencyStopEnabled = ref(false)
|
|
|
+
|
|
|
+const fullscreenCleanup = ref(null)
|
|
|
+
|
|
|
+// 目标点相关
|
|
|
+const selectedWaypointIds = ref([])
|
|
|
+const waypointSingle = ref(true)
|
|
|
+const waypointMultiple = ref(true)
|
|
|
+
|
|
|
+// 导航相关
|
|
|
+const waypoints = ref([])
|
|
|
+const currentNavigationTask = ref(null)
|
|
|
+const isNavigating = ref(false)
|
|
|
+const navigationStatus = ref('idle')
|
|
|
+const lastGotoRequest = ref(null)
|
|
|
+
|
|
|
+// 任务相关
|
|
|
+const currentExecutingTask = ref(null)
|
|
|
+const taskQueue = ref([])
|
|
|
+const isTaskExecuting = ref(false)
|
|
|
+const taskExecutionStatus = ref('idle')
|
|
|
+const currentTaskWaypointIndex = ref(0)
|
|
|
+const taskRealtimeInfo = reactive({
|
|
|
+ odom: { total: 0, remain: 0 },
|
|
|
+ time: { total: 0, remain: 0, duration: 0 },
|
|
|
+ driveMode: 'auto',
|
|
|
+ autoReady: true
|
|
|
+})
|
|
|
+const lastTaskExecRequest = ref(null)
|
|
|
+const tasks = ref([])
|
|
|
+
|
|
|
+const currentViewTaskWaypointIds = ref([])
|
|
|
+const taskViewAutoCloseTimer = ref(null)
|
|
|
+const taskListTimer = ref(null)
|
|
|
+
|
|
|
+// 定时任务
|
|
|
+const scheduledTasksToday = ref([])
|
|
|
+const scheduledTasksExecuted = ref([])
|
|
|
+const currentScheduledTask = ref(null)
|
|
|
+const lastTaskStartTime = ref({})
|
|
|
+const autoExecuteTimer = ref(null)
|
|
|
+const autoExecuteEnabled = ref(true)
|
|
|
+
|
|
|
+// 障碍物检测
|
|
|
+const obstacleData = reactive({
|
|
|
+ left: 0,
|
|
|
+ front: 0,
|
|
|
+ right: 0,
|
|
|
+ timestamp: 0
|
|
|
+})
|
|
|
+
|
|
|
+const obstacleWarning = reactive({
|
|
|
+ show: false,
|
|
|
+ level: 'info',
|
|
|
+ message: '',
|
|
|
+ direction: '',
|
|
|
+ distance: 0,
|
|
|
+ autoHideTimer: null
|
|
|
+})
|
|
|
+
|
|
|
+const obstacleSettings = reactive({
|
|
|
+ warningDistance: 3.0,
|
|
|
+ dangerDistance: 1.0,
|
|
|
+ autoHideDelay: 5000,
|
|
|
+ enableWarning: true
|
|
|
+})
|
|
|
+
|
|
|
+const obstacleStopState = reactive({
|
|
|
+ counter: 0,
|
|
|
+ replanTimeLeft: 0,
|
|
|
+ showWarning: false,
|
|
|
+ isReplanning: false,
|
|
|
+ runState: 0,
|
|
|
+ runStateText: '未知'
|
|
|
+})
|
|
|
+
|
|
|
+// Computed
|
|
|
+const selectedKey = computed(() => {
|
|
|
+ if (initPoseMode.value) return 'init-pose'
|
|
|
+ return ''
|
|
|
+})
|
|
|
+
|
|
|
+const robotPosition = computed(() => {
|
|
|
+ if (robotPoseData && (robotPoseData.x !== 0 || robotPoseData.y !== 0)) {
|
|
|
+ return [robotPoseData.x, robotPoseData.y]
|
|
|
+ }
|
|
|
+ return null
|
|
|
+})
|
|
|
+
|
|
|
+const hasValidRobotPosition = computed(() => {
|
|
|
+ return !!robotPosition.value
|
|
|
+})
|
|
|
+
|
|
|
+const isMapReady = computed(() => {
|
|
|
+ return !!getMapInstance()
|
|
|
+})
|
|
|
+
|
|
|
+const navigationStatusText = computed(() => {
|
|
|
+ switch(navigationStatus.value) {
|
|
|
+ case 'idle': return '空闲'
|
|
|
+ case 'planning': return '规划中'
|
|
|
+ case 'navigating': return '导航中'
|
|
|
+ case 'arrived': return '已到达'
|
|
|
+ case 'failed': return '失败'
|
|
|
+ default: return '未知'
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const canStartNavigation = computed(() => {
|
|
|
+ return !isNavigating.value && navigationStatus.value !== 'planning'
|
|
|
+})
|
|
|
+
|
|
|
+const currentNavigationTarget = computed(() => {
|
|
|
+ if (!currentNavigationTask.value) return null
|
|
|
+ const waypoint = currentNavigationTask.value.waypoint
|
|
|
+ return `${waypoint.name || '目标点' + waypoint.id} (${waypoint.x}, ${waypoint.y})`
|
|
|
+})
|
|
|
+
|
|
|
+const taskExecutionStatusText = computed(() => {
|
|
|
+ switch(taskExecutionStatus.value) {
|
|
|
+ case 'idle': return '空闲'
|
|
|
+ case 'executing': return '执行中'
|
|
|
+ case 'paused': return '暂停'
|
|
|
+ case 'completed': return '已完成'
|
|
|
+ case 'failed': return '失败'
|
|
|
+ case 'cancelled': return '已取消'
|
|
|
+ default: return '未知'
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const currentTaskDescription = computed(() => {
|
|
|
+ if (!currentExecutingTask.value) return null
|
|
|
+ const task = currentExecutingTask.value
|
|
|
+ const progress = `${currentTaskWaypointIndex.value}/${task.points?.length || 0}`
|
|
|
+ return `${task.taskName} (${progress})`
|
|
|
+})
|
|
|
+
|
|
|
+const canStartNewTask = computed(() => {
|
|
|
+ return !isTaskExecuting.value && !isNavigating.value
|
|
|
+})
|
|
|
+
|
|
|
+const taskProgressInfo = computed(() => {
|
|
|
+ if (!currentExecutingTask.value || !taskRealtimeInfo) return null
|
|
|
+ return {
|
|
|
+ taskName: currentExecutingTask.value.taskName,
|
|
|
+ waypointProgress: `${currentTaskWaypointIndex.value}/${currentExecutingTask.value.points?.length || 0}`,
|
|
|
+ totalDistance: `${(taskRealtimeInfo.odom.total / 1000).toFixed(2)}km`,
|
|
|
+ remainDistance: `${(taskRealtimeInfo.odom.remain / 1000).toFixed(2)}km`,
|
|
|
+ totalTime: formatSeconds(taskRealtimeInfo.time.total),
|
|
|
+ remainTime: formatSeconds(taskRealtimeInfo.time.remain),
|
|
|
+ duration: formatSeconds(taskRealtimeInfo.time.duration),
|
|
|
+ driveMode: taskRealtimeInfo.driveMode === 'auto' ? '自动' : '手动',
|
|
|
+ autoReady: taskRealtimeInfo.autoReady
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const todayScheduleInfo = computed(() => {
|
|
|
+ const pendingTasks = scheduledTasksToday.value.filter(
|
|
|
+ taskName => !scheduledTasksExecuted.value.includes(taskName) &&
|
|
|
+ (!currentScheduledTask.value || currentScheduledTask.value.name !== taskName)
|
|
|
+ )
|
|
|
+ return {
|
|
|
+ total: scheduledTasksToday.value.length,
|
|
|
+ executed: scheduledTasksExecuted.value.length,
|
|
|
+ running: currentScheduledTask.value ? 1 : 0,
|
|
|
+ pending: pendingTasks.length,
|
|
|
+ currentRunning: currentScheduledTask.value,
|
|
|
+ pendingList: pendingTasks
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const hasScheduleRunning = computed(() => {
|
|
|
+ return currentScheduledTask.value !== null
|
|
|
+})
|
|
|
+
|
|
|
+const scheduledTasksPending = computed(() => {
|
|
|
+ return scheduledTasksToday.value.filter(
|
|
|
+ taskName => !scheduledTasksExecuted.value.includes(taskName) &&
|
|
|
+ (!currentScheduledTask.value || currentScheduledTask.value.name !== taskName)
|
|
|
+ )
|
|
|
+})
|
|
|
+
|
|
|
+// Watch
|
|
|
+watch(() => route.params.mapName, (newMapName, oldMapName) => {
|
|
|
+ console.log("mapName变化检测:", { newMapName, oldMapName });
|
|
|
+
|
|
|
+ if (newMapName && newMapName !== mapName.value) {
|
|
|
+ console.log(`路由变化,更新mapName: ${oldMapName} -> ${newMapName}`)
|
|
|
+ mapName.value = newMapName
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+watch(panelVisible, () => {
|
|
|
+ nextTick(() => {
|
|
|
+ updateOlCss()
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+watch(selectPointMode, (newVal) => {
|
|
|
+ if (newVal) {
|
|
|
+ initPoseMode.value = false
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+watch(initPoseMode, (newVal) => {
|
|
|
+ if (newVal) {
|
|
|
+ selectPointMode.value = false
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+watch(() => realtimeInfo.coordinates, (newCoordinates) => {
|
|
|
+ if (newCoordinates) {
|
|
|
+ const position = RobotPositionUtils.parseCoordinates(newCoordinates)
|
|
|
+ if (position) {
|
|
|
+ robotPoseData.x = position.x
|
|
|
+ robotPoseData.y = position.y
|
|
|
+ }
|
|
|
+ }
|
|
|
+}, { immediate: true })
|
|
|
+
|
|
|
+// 生命周期
|
|
|
+onMounted(() => {
|
|
|
+ console.log("route.params.mapName",route.params.mapName);
|
|
|
+
|
|
|
+ // 使用 nextTick 确保 DOM 尺寸已计算
|
|
|
+ nextTick(() => {
|
|
|
+ updateOlCss()
|
|
|
+ // 地图容器尺寸初始化后再次调用确保生效
|
|
|
+ setTimeout(() => {
|
|
|
+ updateOlCss()
|
|
|
+ // 调用 OlMap 组件的 updateMapSize 确保地图正确初始化
|
|
|
+ if (olmap.value && olmap.value.updateMapSize) {
|
|
|
+ olmap.value.updateMapSize()
|
|
|
+ }
|
|
|
+ const map = getMapInstance()
|
|
|
+ if (map) {
|
|
|
+ map.updateSize()
|
|
|
+ }
|
|
|
+ }, 200)
|
|
|
+ })
|
|
|
+
|
|
|
+ window.addEventListener('resize', updateOlCss)
|
|
|
+
|
|
|
+ // 初始化WebSocket连接
|
|
|
+ initWebSocketConnection()
|
|
|
+
|
|
|
+ loadTaskList()
|
|
|
+ startTaskListTimer()
|
|
|
+
|
|
|
+ fullscreenCleanup.value = FullscreenOperations.addFullscreenListener((fsIsFullscreen) => {
|
|
|
+ isFullscreen.value = fsIsFullscreen
|
|
|
+ nextTick(() => {
|
|
|
+ updateOlCss()
|
|
|
+ const map = getMapInstance()
|
|
|
+ if (map) {
|
|
|
+ map.updateSize()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ setInterval(() => {
|
|
|
+ calculateTodayScheduledTasks()
|
|
|
+ }, 60 * 60 * 1000)
|
|
|
+
|
|
|
+ startAutoExecuteTimer()
|
|
|
+})
|
|
|
+
|
|
|
+onActivated(() => {
|
|
|
+ console.log('导航页面被激活')
|
|
|
+
|
|
|
+ const newMapName = route.params.mapName || ''
|
|
|
+ console.log("mapName变化检测",route.params.mapName);
|
|
|
+
|
|
|
+ if (mapName.value !== newMapName) {
|
|
|
+ console.log(`更新mapName: ${mapName.value} -> ${newMapName}`)
|
|
|
+ mapName.value = newMapName
|
|
|
+ }
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ updateOlCss()
|
|
|
+
|
|
|
+ // 调用 OlMap 组件的 updateMapSize 确保地图正确初始化
|
|
|
+ if (olmap.value && olmap.value.updateMapSize) {
|
|
|
+ olmap.value.updateMapSize()
|
|
|
+ }
|
|
|
+
|
|
|
+ const map = getMapInstance()
|
|
|
+ if (map) {
|
|
|
+ setTimeout(() => {
|
|
|
+ map.updateSize()
|
|
|
+ console.log('地图尺寸已更新')
|
|
|
+ }, 100)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.refresh) {
|
|
|
+ setTimeout(() => {
|
|
|
+ olmap.value.refresh()
|
|
|
+ }, 150)
|
|
|
+ }
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ window.removeEventListener('resize', updateOlCss)
|
|
|
+
|
|
|
+ if (fullscreenCleanup.value) {
|
|
|
+ fullscreenCleanup.value()
|
|
|
+ }
|
|
|
+
|
|
|
+ if (taskViewAutoCloseTimer.value) {
|
|
|
+ clearTimeout(taskViewAutoCloseTimer.value)
|
|
|
+ }
|
|
|
+
|
|
|
+ stopTaskListTimer()
|
|
|
+ stopAutoExecuteTimer()
|
|
|
+
|
|
|
+ if (obstacleWarning.autoHideTimer) {
|
|
|
+ clearTimeout(obstacleWarning.autoHideTimer)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 断开WebSocket连接
|
|
|
+ disconnect()
|
|
|
+})
|
|
|
+
|
|
|
+// 方法
|
|
|
+const goto = () => {
|
|
|
+ // 原有功能已移至其他方法
|
|
|
+}
|
|
|
+
|
|
|
+// WebSocket消息处理
|
|
|
+const handleWsMessage = (type, message) => {
|
|
|
+ console.log("主题:",type);
|
|
|
+ switch (type) {
|
|
|
+ case 'pose':
|
|
|
+ handleLaserPose(message)
|
|
|
+ break
|
|
|
+ case 'task':
|
|
|
+ handleTaskRealtimeInfoFromWs(message)
|
|
|
+ break
|
|
|
+ case 'trajectory':
|
|
|
+ handleTrajectoryDataFromWs(message)
|
|
|
+ break
|
|
|
+ case 'arrive':
|
|
|
+ handleArriveEventFromWs(message)
|
|
|
+ break
|
|
|
+ case 'navigation':
|
|
|
+ handleNavigationResponseFromWs(message)
|
|
|
+ break
|
|
|
+ case 'planning':
|
|
|
+ handlePlanningResponseFromWs(message)
|
|
|
+ break
|
|
|
+ case 'raw':
|
|
|
+ // 处理通过WebSocket转发的原始数据
|
|
|
+ if (message.topic && message.topic.includes('battery')) {
|
|
|
+ handleBatteryDataFromRaw(message)
|
|
|
+ }
|
|
|
+ break
|
|
|
+ case 'asm':
|
|
|
+ handleAsmResponseFromWs(message)
|
|
|
+ break
|
|
|
+ case 'battery':
|
|
|
+ // 处理电池信息
|
|
|
+ handleBatteryInfoFromWs(message)
|
|
|
+ break
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化WebSocket连接
|
|
|
+const initWebSocketConnection = () => {
|
|
|
+ initWebSocket({
|
|
|
+ // deviceId: wsDeviceId.value,
|
|
|
+ onConnect: () => {
|
|
|
+ console.log('[Navigation] WebSocket连接成功')
|
|
|
+ },
|
|
|
+ onDisconnect: () => {
|
|
|
+ console.log('[Navigation] WebSocket连接断开')
|
|
|
+ },
|
|
|
+ onMessage: (type, message) => {
|
|
|
+ handleWsMessage(type, message)
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ console.error('[Navigation] WebSocket错误:', error)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const handleInitReply = (message) => {
|
|
|
+ // 初始化回复处理
|
|
|
+}
|
|
|
+
|
|
|
+// 通过WebSocket发送消息的辅助函数
|
|
|
+const sendWsMessage = (topic, data) => {
|
|
|
+ const destination = `${topic}`
|
|
|
+ sendMessage(destination, data)
|
|
|
+}
|
|
|
+
|
|
|
+// WebSocket版本的位姿消息处理
|
|
|
+const handleLaserPoseFromWs = (message) => {
|
|
|
+ console.log("接受到pose消息",message);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const pose = message.data || message
|
|
|
+ const { xyz, rpy } = pose
|
|
|
+
|
|
|
+ const oldX = laserPositionData.x
|
|
|
+ const oldY = laserPositionData.y
|
|
|
+
|
|
|
+ laserPositionData.x = xyz[0]
|
|
|
+ laserPositionData.y = xyz[1]
|
|
|
+ laserPositionData.angle = rpy[2]
|
|
|
+
|
|
|
+ realtimeInfo.coordinates = `(${xyz[0]}, ${xyz[1]}, ${xyz[2] || 0})`
|
|
|
+ realtimeInfo.heading = rpy[2] + '°'
|
|
|
+
|
|
|
+ if (pose.velocity || pose.heading) {
|
|
|
+ realtimeInfo.speed = (pose.heading || 0) + 'm/s'
|
|
|
+ realtimeInfo.speedCommand = (pose.velocity || 0) + 'm/s'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (pose.odometer !== undefined) {
|
|
|
+ realtimeInfo.totalDistance = pose.odometer + 'm'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (pose.confidence !== undefined) {
|
|
|
+ realtimeInfo.registrationError = pose.confidence
|
|
|
+ }
|
|
|
+
|
|
|
+ const positionChanged = Math.abs(xyz[0] - oldX) > 0.005 || Math.abs(xyz[1] - oldY) > 0.005
|
|
|
+
|
|
|
+ if (positionChanged && olmap.value && olmap.value.currentTrajectory) {
|
|
|
+ const currentTrajectory = olmap.value.currentTrajectory
|
|
|
+ if (currentTrajectory && currentTrajectory.length > 0) {
|
|
|
+ const distanceToTarget = calculateDistanceToTrajectory([laserPositionData.x, laserPositionData.y], currentTrajectory)
|
|
|
+ const progress = calculateCurrentProgress(currentTrajectory)
|
|
|
+ if (progress > 0) {
|
|
|
+ progressInfo.progress = progress
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[Navigation] 解析位姿消息失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// WebSocket版本的任务实时信息处理
|
|
|
+const handleTaskRealtimeInfoFromWs = (message) => {
|
|
|
+ try {
|
|
|
+ const data = message.payload || message
|
|
|
+ if (data.battery !== undefined) {
|
|
|
+ realtimeInfo.batteryLevel = (data.battery * 100).toFixed(2) + '%'
|
|
|
+ }
|
|
|
+ if (data.state !== undefined) {
|
|
|
+ realtimeInfo.robotState = data.state
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[Navigation] 解析任务实时信息失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// WebSocket版本的轨迹数据处理
|
|
|
+const handleTrajectoryDataFromWs = (message) => {
|
|
|
+ handleTrajectoryData(message)
|
|
|
+}
|
|
|
+
|
|
|
+// WebSocket版本的到达事件处理
|
|
|
+const handleArriveEventFromWs = (message) => {
|
|
|
+ handleArriveEvent(message)
|
|
|
+}
|
|
|
+
|
|
|
+// WebSocket版本的导航响应处理
|
|
|
+const handleNavigationResponseFromWs = (message) => {
|
|
|
+ const data = message.payload || message
|
|
|
+ // 处理导航相关的响应
|
|
|
+ if (data.success !== undefined) {
|
|
|
+ if (data.action === 'start') {
|
|
|
+ handleNavigationStartReply(data)
|
|
|
+ } else if (data.action === 'stop') {
|
|
|
+ handleNavigationStopReply(data)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// WebSocket版本的规划响应处理
|
|
|
+const handlePlanningResponseFromWs = (message) => {
|
|
|
+ handlePlanResponse(message)
|
|
|
+}
|
|
|
+
|
|
|
+// WebSocket版本的ASM响应处理
|
|
|
+const handleAsmResponseFromWs = (message) => {
|
|
|
+ const data = message.payload || message
|
|
|
+ if (data.result !== undefined) {
|
|
|
+ handleNavigationRestartReply(data)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理电池信息
|
|
|
+const handleBatteryInfoFromWs = (message) => {
|
|
|
+ try {
|
|
|
+ const data = message.data || message
|
|
|
+ if (data.capacity !== undefined) {
|
|
|
+ // capacity范围0.0~1.0,转换为百分比
|
|
|
+ const capacityPercent = (data.capacity * 100).toFixed(0)
|
|
|
+ realtimeInfo.batteryLevel = capacityPercent + '%'
|
|
|
+
|
|
|
+ // 更新电池详细信息
|
|
|
+ if (realtimeInfo.batteryDetails === undefined) {
|
|
|
+ realtimeInfo.batteryDetails = {}
|
|
|
+ }
|
|
|
+ realtimeInfo.batteryDetails.capacity = data.capacity
|
|
|
+ realtimeInfo.batteryDetails.temperature = data.temperature
|
|
|
+ realtimeInfo.batteryDetails.voltage = data.voltage
|
|
|
+ realtimeInfo.batteryDetails.current = data.current
|
|
|
+ realtimeInfo.batteryDetails.charging = data.charging
|
|
|
+ realtimeInfo.batteryDetails.other = data.other
|
|
|
+
|
|
|
+ console.log(`[Navigation] 电池信息更新: ${capacityPercent}%, 温度: ${data.temperature}°C, 电压: ${data.voltage}V, 充电: ${data.charging}`)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[Navigation] 解析电池信息失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理原始数据中的电池信息
|
|
|
+const handleBatteryDataFromRaw = (message) => {
|
|
|
+ try {
|
|
|
+ const data = message.payload || message
|
|
|
+ if (data.capacity !== undefined) {
|
|
|
+ const capacityPercent = (data.capacity * 100).toFixed(0)
|
|
|
+ realtimeInfo.batteryLevel = capacityPercent + '%'
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[Navigation] 解析原始电池信息失败:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleLaserPose = (message) => {
|
|
|
+ try {
|
|
|
+ const data = message.data || message
|
|
|
+ const { xyz, rpy, blh, heading } = data
|
|
|
+
|
|
|
+ const oldX = laserPositionData.x
|
|
|
+ const oldY = laserPositionData.y
|
|
|
+
|
|
|
+ laserPositionData.x = xyz[0]
|
|
|
+ laserPositionData.y = xyz[1]
|
|
|
+ laserPositionData.angle = rpy[2]
|
|
|
+
|
|
|
+ realtimeInfo.coordinates = `(${xyz[0]}, ${xyz[1]}, ${xyz[2]})`
|
|
|
+ realtimeInfo.heading = rpy[2] + '°'
|
|
|
+ realtimeInfo.speed = data.heading !== null ? data.heading : 0 + 'm/s'
|
|
|
+ realtimeInfo.speedCommand = data.velocity !== null ? data.velocity : 0 + 'm/s'
|
|
|
+ realtimeInfo.totalDistance = data.odometer !== null ? data.odometer : 0 + 'm'
|
|
|
+ realtimeInfo.registrationError = data.confidence
|
|
|
+
|
|
|
+ if (data.runState !== null && data.runState !== undefined) {
|
|
|
+ obstacleStopState.runState = data.runState
|
|
|
+ obstacleStopState.runStateText = getRunStateText(data.runState)
|
|
|
+
|
|
|
+ if (data.runState === 4) {
|
|
|
+ if (obstacleStopState.counter < 25) {
|
|
|
+ obstacleStopState.counter++
|
|
|
+ }
|
|
|
+ if (obstacleStopState.isReplanning) {
|
|
|
+ obstacleStopState.isReplanning = false
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (obstacleStopState.counter > 0) {
|
|
|
+ obstacleStopState.counter--
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (data.replan !== undefined) {
|
|
|
+ obstacleStopState.replanTimeLeft = data.replan
|
|
|
+ }
|
|
|
+
|
|
|
+ if (obstacleStopState.counter >= 20) {
|
|
|
+ if (!obstacleStopState.showWarning) {
|
|
|
+ obstacleStopState.showWarning = true
|
|
|
+ console.log('检测到稳定遇障状态,显示警告')
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (obstacleStopState.showWarning) {
|
|
|
+ obstacleStopState.showWarning = false
|
|
|
+ console.log('遇障状态解除,隐藏警告')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const positionChanged = Math.abs(xyz[0] - oldX) > 0.005 || Math.abs(xyz[1] - oldY) > 0.005
|
|
|
+
|
|
|
+ if (positionChanged && olmap.value && olmap.value.currentTrajectory) {
|
|
|
+ const currentTrajectory = olmap.value.currentTrajectory
|
|
|
+ const reachedTarget = checkIfReachedTarget(currentTrajectory)
|
|
|
+
|
|
|
+ if (!reachedTarget) {
|
|
|
+ console.log(`机器人位置更新,重新绘制实时轨迹: (${xyz[0].toFixed(3)}, ${xyz[1].toFixed(3)})`)
|
|
|
+ if (olmap.value.updateRealtimeTrajectory) {
|
|
|
+ olmap.value.updateRealtimeTrajectory([xyz[0], xyz[1]])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error("解析失败:", e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handlePlanResponse = (message) => {
|
|
|
+ console.log("路径规划响应:", message)
|
|
|
+ if (message.status === 'ok' && message.args && message.args.length > 0) {
|
|
|
+ const planData = message.args[0]
|
|
|
+ console.log("规划路径数据:", planData)
|
|
|
+
|
|
|
+ if (planData.path && planData.path.length > 0) {
|
|
|
+ const plannedPath = planData.path.map(point => {
|
|
|
+ if (planData.div) {
|
|
|
+ return [point[0] / planData.div, point[1] / planData.div]
|
|
|
+ }
|
|
|
+ return [point[0], point[1]]
|
|
|
+ })
|
|
|
+
|
|
|
+ console.log("绘制规划路径,点数:", plannedPath.length)
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.clearTrajectory) {
|
|
|
+ olmap.value.clearTrajectory()
|
|
|
+ console.log('已清除旧轨迹,准备绘制新的规划路径')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.drawTrajectory) {
|
|
|
+ olmap.value.drawTrajectory(plannedPath, {
|
|
|
+ totalPoints: plannedPath.length,
|
|
|
+ currentProgress: 0,
|
|
|
+ robotPosition: [laserPositionData.x, laserPositionData.y]
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.success(`路径规划成功!规划路径共 ${plannedPath.length} 个点`)
|
|
|
+ } else {
|
|
|
+ ElMessage.success('路径规划成功!')
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ ElMessage.error('路径规划失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleTrajectoryData = (message) => {
|
|
|
+ console.log("接收到轨迹数据:", message)
|
|
|
+
|
|
|
+ if (!isNavigating.value && !currentNavigationTask.value) {
|
|
|
+ console.log('非导航状态,忽略轨迹数据(可能是retain消息)')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (message.args && message.args.length > 0) {
|
|
|
+ const trajectoryData = message.args[0]
|
|
|
+ const { trj, idx, total, div } = trajectoryData
|
|
|
+
|
|
|
+ const realTrajectory = trj.map(point => [
|
|
|
+ point[0] / div,
|
|
|
+ point[1] / div
|
|
|
+ ])
|
|
|
+
|
|
|
+ const currentProgress = calculateCurrentProgress(realTrajectory)
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.clearTrajectory) {
|
|
|
+ olmap.value.clearTrajectory()
|
|
|
+ console.log('已清除旧轨迹,准备绘制新的实时轨迹')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.drawTrajectory) {
|
|
|
+ olmap.value.drawTrajectory(realTrajectory, {
|
|
|
+ totalPoints: total,
|
|
|
+ indices: idx,
|
|
|
+ currentProgress: currentProgress,
|
|
|
+ robotPosition: [laserPositionData.x, laserPositionData.y]
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log("轨迹绘制完成,总点数:", total, "轨迹点数:", realTrajectory.length)
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理轨迹数据失败:", error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleReplanReply = (message) => {
|
|
|
+ console.log("重规划响应:", message)
|
|
|
+ obstacleStopState.isReplanning = false
|
|
|
+
|
|
|
+ if (message.status === 'ok') {
|
|
|
+ ElMessage.success('重规划成功')
|
|
|
+ } else {
|
|
|
+ ElMessage.error('重规划失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleGotoReply = (message) => {
|
|
|
+ console.log("前往目标点响应:", message)
|
|
|
+ try {
|
|
|
+ if (message.status === 'ok') {
|
|
|
+ if (lastGotoRequest.value && message.pub_timestamp === lastGotoRequest.value.timestamp) {
|
|
|
+ navigationStatus.value = 'navigating'
|
|
|
+ isNavigating.value = true
|
|
|
+ ElMessage.success('机器人已接收前往指令,开始导航')
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ navigationStatus.value = 'failed'
|
|
|
+ isNavigating.value = false
|
|
|
+ ElMessage.error('前往目标点请求失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理goto响应失败:", error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleArriveEvent = (message) => {
|
|
|
+ console.log("到达目标点事件:", message)
|
|
|
+ try {
|
|
|
+ if (message.args && message.args.length > 0) {
|
|
|
+ const arriveData = message.args[0]
|
|
|
+ const { status, coord, nid, error } = arriveData
|
|
|
+
|
|
|
+ if (status === 'ok') {
|
|
|
+ navigationStatus.value = 'arrived'
|
|
|
+ isNavigating.value = false
|
|
|
+
|
|
|
+ const targetInfo = coord && coord.length > 0 ?
|
|
|
+ `(${coord[0][0]}, ${coord[0][1]})` :
|
|
|
+ (nid && nid.length > 0 ? `点位${nid[0]}` : '目标点')
|
|
|
+
|
|
|
+ if (currentExecutingTask.value && currentNavigationTask.value?.taskContext) {
|
|
|
+ handleTaskWaypointArrival()
|
|
|
+ } else {
|
|
|
+ ElMessage.success(`已成功到达目标点 ${targetInfo}!`)
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.clearTrajectory) {
|
|
|
+ setTimeout(() => {
|
|
|
+ olmap.value.clearTrajectory()
|
|
|
+ settingParams.showRoadNetwork = true
|
|
|
+ console.log('轨迹已自动清除')
|
|
|
+ }, 2000)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ currentNavigationTask.value = null
|
|
|
+ lastGotoRequest.value = null
|
|
|
+ } else if (status === 'fail') {
|
|
|
+ navigationStatus.value = 'failed'
|
|
|
+ isNavigating.value = false
|
|
|
+
|
|
|
+ let errorMessage = '前往目标点失败'
|
|
|
+ if (error) {
|
|
|
+ switch (error) {
|
|
|
+ case 21: errorMessage = '前往目标点失败:规划失败'; break
|
|
|
+ case 22: errorMessage = '前往目标点失败:偏离车道线'; break
|
|
|
+ case 23: errorMessage = '前往目标点失败:任务提前终止'; break
|
|
|
+ case 24: errorMessage = '前往目标点失败:定位异常'; break
|
|
|
+ default: errorMessage = `前往目标点失败:未知错误(${error})`
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.error(errorMessage)
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.clearTrajectory) {
|
|
|
+ olmap.value.clearTrajectory()
|
|
|
+ settingParams.showRoadNetwork = true
|
|
|
+ }
|
|
|
+
|
|
|
+ currentNavigationTask.value = null
|
|
|
+ lastGotoRequest.value = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理到达事件失败:", error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleTaskExecReply = (message) => {
|
|
|
+ console.log("任务执行响应:", message)
|
|
|
+ try {
|
|
|
+ if (message.status === 'ok') {
|
|
|
+ if (lastTaskExecRequest.value && message.pub_timestamp === lastTaskExecRequest.value.timestamp) {
|
|
|
+ taskExecutionStatus.value = 'executing'
|
|
|
+ isTaskExecuting.value = true
|
|
|
+ ElMessage.success(`任务 "${currentExecutingTask.value.taskName}" 已开始执行`)
|
|
|
+ executeNextWaypoint()
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ taskExecutionStatus.value = 'failed'
|
|
|
+ isTaskExecuting.value = false
|
|
|
+ ElMessage.error('任务执行失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理任务执行响应失败:", error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleTaskCompleteEvent = (message) => {
|
|
|
+ console.log("任务完成事件 - 原始消息:", message)
|
|
|
+ try {
|
|
|
+ if (message.args && message.args.length > 0) {
|
|
|
+ const completeData = message.args[0]
|
|
|
+ let { name, status } = completeData
|
|
|
+
|
|
|
+ const hasEncodingIssue = name && /[\uFFFD\u00C0-\u00FF]{2,}/.test(name)
|
|
|
+ if (hasEncodingIssue) {
|
|
|
+ console.warn("检测到编码问题,任务名称包含乱码:", name)
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log("解析后的任务名称:", name, "状态:", status)
|
|
|
+
|
|
|
+ let currentTask = null
|
|
|
+ let taskNameToDisplay = name
|
|
|
+
|
|
|
+ if (currentExecutingTask.value) {
|
|
|
+ currentTask = currentExecutingTask.value
|
|
|
+ taskNameToDisplay = currentTask.taskName
|
|
|
+ console.log("使用 currentExecutingTask 引用匹配任务:", currentTask.taskName)
|
|
|
+ } else {
|
|
|
+ currentTask = tasks.value.find(t => t.taskName === name)
|
|
|
+ if (!currentTask && hasEncodingIssue) {
|
|
|
+ const runningTasks = tasks.value.filter(t => t.status === 'running' || t.status === 'paused')
|
|
|
+ if (runningTasks.length === 1) {
|
|
|
+ currentTask = runningTasks[0]
|
|
|
+ taskNameToDisplay = currentTask.taskName
|
|
|
+ console.log("通过运行状态匹配到任务:", currentTask.taskName, "(原name为乱码)")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (status === 'ok') {
|
|
|
+ taskExecutionStatus.value = 'completed'
|
|
|
+ isTaskExecuting.value = false
|
|
|
+ ElMessage.success(`任务 "${taskNameToDisplay}" 已成功完成!`)
|
|
|
+
|
|
|
+ if (currentTask && scheduledTasksToday.value.includes(currentTask.taskName)) {
|
|
|
+ if (!scheduledTasksExecuted.value.includes(currentTask.taskName)) {
|
|
|
+ scheduledTasksExecuted.value.push(currentTask.taskName)
|
|
|
+ console.log(`定时任务完成: ${currentTask.taskName}`)
|
|
|
+ }
|
|
|
+ currentScheduledTask.value = null
|
|
|
+ }
|
|
|
+
|
|
|
+ currentExecutingTask.value = null
|
|
|
+ currentTaskWaypointIndex.value = 0
|
|
|
+ lastTaskExecRequest.value = null
|
|
|
+
|
|
|
+ if (currentTask) {
|
|
|
+ currentTask.status = 'completed'
|
|
|
+ currentTask.executionStatus = 'completed'
|
|
|
+ currentTask.currentWaypointIndex = 0
|
|
|
+
|
|
|
+ const taskInList = tasks.value.find(t => t.taskName === currentTask.taskName || t.taskId === currentTask.taskId)
|
|
|
+ if (taskInList && taskInList !== currentTask) {
|
|
|
+ taskInList.status = 'completed'
|
|
|
+ taskInList.executionStatus = 'completed'
|
|
|
+ taskInList.currentWaypointIndex = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ const taskNameForReset = currentTask.taskName
|
|
|
+ setTimeout(() => {
|
|
|
+ const taskToReset = tasks.value.find(t => t.taskName === taskNameForReset)
|
|
|
+ if (taskToReset && taskToReset.status === 'completed') {
|
|
|
+ taskToReset.status = 'idle'
|
|
|
+ taskToReset.executionStatus = 'idle'
|
|
|
+ }
|
|
|
+ }, 5000)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.clearTrajectory) {
|
|
|
+ setTimeout(() => {
|
|
|
+ olmap.value.clearTrajectory()
|
|
|
+ settingParams.showRoadNetwork = true
|
|
|
+ }, 3000)
|
|
|
+ }
|
|
|
+ } else if (status === 'fail') {
|
|
|
+ taskExecutionStatus.value = 'failed'
|
|
|
+ isTaskExecuting.value = false
|
|
|
+ ElMessage.error(`任务 "${taskNameToDisplay}" 执行失败`)
|
|
|
+
|
|
|
+ currentExecutingTask.value = null
|
|
|
+ currentTaskWaypointIndex.value = 0
|
|
|
+ lastTaskExecRequest.value = null
|
|
|
+
|
|
|
+ if (currentTask) {
|
|
|
+ currentTask.status = 'error'
|
|
|
+ currentTask.executionStatus = 'failed'
|
|
|
+ currentTask.currentWaypointIndex = 0
|
|
|
+
|
|
|
+ const taskInList = tasks.value.find(t => t.taskName === currentTask.taskName || t.taskId === currentTask.taskId)
|
|
|
+ if (taskInList && taskInList !== currentTask) {
|
|
|
+ taskInList.status = 'error'
|
|
|
+ taskInList.executionStatus = 'failed'
|
|
|
+ taskInList.currentWaypointIndex = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ const taskNameForReset = currentTask.taskName
|
|
|
+ setTimeout(() => {
|
|
|
+ const taskToReset = tasks.value.find(t => t.taskName === taskNameForReset)
|
|
|
+ if (taskToReset && taskToReset.status === 'error') {
|
|
|
+ taskToReset.status = 'idle'
|
|
|
+ taskToReset.executionStatus = 'idle'
|
|
|
+ }
|
|
|
+ }, 5000)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.clearTrajectory) {
|
|
|
+ olmap.value.clearTrajectory()
|
|
|
+ settingParams.showRoadNetwork = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理任务完成事件失败:", error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleTaskRealtimeInfo = (message) => {
|
|
|
+ try {
|
|
|
+ if (message.args && message.args.length > 0) {
|
|
|
+ const realtimeData = message.args[0]
|
|
|
+ const { odom, time, drive_mode, auto_ready } = realtimeData
|
|
|
+
|
|
|
+ taskRealtimeInfo.odom.total = odom?.tottal || 0
|
|
|
+ taskRealtimeInfo.odom.remain = odom?.remain || 0
|
|
|
+ taskRealtimeInfo.time.total = time?.total || 0
|
|
|
+ taskRealtimeInfo.time.remain = time?.remain || 0
|
|
|
+ taskRealtimeInfo.time.duration = time?.duration || 0
|
|
|
+ taskRealtimeInfo.driveMode = drive_mode || 'auto'
|
|
|
+ taskRealtimeInfo.autoReady = auto_ready !== undefined ? auto_ready : true
|
|
|
+
|
|
|
+ if (currentExecutingTask.value) {
|
|
|
+ realtimeInfo.currentTask = currentExecutingTask.value.taskName
|
|
|
+ realtimeInfo.totalDistance = `${(odom?.tottal / 1000).toFixed(2)}km` || ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理任务实时信息失败:", error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleTaskStartReply = (message) => {
|
|
|
+ console.log("任务开始响应:", message)
|
|
|
+ try {
|
|
|
+ if (message.status === 'ok') {
|
|
|
+ ElMessage.success('任务已成功开始执行')
|
|
|
+
|
|
|
+ if (currentExecutingTask.value) {
|
|
|
+ const taskName = currentExecutingTask.value.taskName
|
|
|
+ currentExecutingTask.value.status = 'running'
|
|
|
+ currentExecutingTask.value.executionStatus = 'executing'
|
|
|
+
|
|
|
+ const taskInList = tasks.value.find(t => t.taskId === currentExecutingTask.value.taskId)
|
|
|
+ if (taskInList) {
|
|
|
+ taskInList.status = 'running'
|
|
|
+ taskInList.executionStatus = 'executing'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (scheduledTasksToday.value.includes(taskName)) {
|
|
|
+ currentScheduledTask.value = {
|
|
|
+ name: taskName,
|
|
|
+ startTime: new Date().getTime(),
|
|
|
+ status: 'running'
|
|
|
+ }
|
|
|
+ lastTaskStartTime.value[taskName] = new Date().getTime()
|
|
|
+ updateTaskListScheduleStatus()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ taskExecutionStatus.value = 'failed'
|
|
|
+ isTaskExecuting.value = false
|
|
|
+
|
|
|
+ if (currentExecutingTask.value) {
|
|
|
+ currentExecutingTask.value.status = 'idle'
|
|
|
+ currentExecutingTask.value.executionStatus = 'failed'
|
|
|
+
|
|
|
+ const taskInList = tasks.value.find(t => t.taskId === currentExecutingTask.value.taskId)
|
|
|
+ if (taskInList) {
|
|
|
+ taskInList.status = 'idle'
|
|
|
+ taskInList.executionStatus = 'failed'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.error('任务开始失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理任务开始响应失败:", error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleTaskPauseReply = (message) => {
|
|
|
+ console.log("暂停任务响应:", message)
|
|
|
+ if (message.status === 'ok') {
|
|
|
+ taskExecutionStatus.value = 'paused'
|
|
|
+
|
|
|
+ if (currentExecutingTask.value) {
|
|
|
+ currentExecutingTask.value.status = 'paused'
|
|
|
+ currentExecutingTask.value.executionStatus = 'paused'
|
|
|
+
|
|
|
+ const taskInList = tasks.value.find(t => t.taskId === currentExecutingTask.value.taskId)
|
|
|
+ if (taskInList) {
|
|
|
+ taskInList.status = 'paused'
|
|
|
+ taskInList.executionStatus = 'paused'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.success('任务已暂停')
|
|
|
+ } else {
|
|
|
+ ElMessage.error('任务暂停失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleTaskResumeReply = (message) => {
|
|
|
+ console.log("继续任务响应:", message)
|
|
|
+ if (message.status === 'ok') {
|
|
|
+ taskExecutionStatus.value = 'executing'
|
|
|
+
|
|
|
+ if (currentExecutingTask.value) {
|
|
|
+ currentExecutingTask.value.status = 'running'
|
|
|
+ currentExecutingTask.value.executionStatus = 'executing'
|
|
|
+
|
|
|
+ const taskInList = tasks.value.find(t => t.taskId === currentExecutingTask.value.taskId)
|
|
|
+ if (taskInList) {
|
|
|
+ taskInList.status = 'running'
|
|
|
+ taskInList.executionStatus = 'executing'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.success('任务已继续执行')
|
|
|
+ } else {
|
|
|
+ ElMessage.error('任务继续失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleTaskCancelReply = (message) => {
|
|
|
+ console.log("取消任务响应:", message)
|
|
|
+ if (message.status === 'ok') {
|
|
|
+ taskExecutionStatus.value = 'cancelled'
|
|
|
+ isTaskExecuting.value = false
|
|
|
+
|
|
|
+ const cancelledTaskName = currentExecutingTask.value?.taskName
|
|
|
+
|
|
|
+ currentExecutingTask.value = null
|
|
|
+ currentTaskWaypointIndex.value = 0
|
|
|
+ lastTaskExecRequest.value = null
|
|
|
+
|
|
|
+ if (cancelledTaskName) {
|
|
|
+ const taskInList = tasks.value.find(t => t.taskName === cancelledTaskName)
|
|
|
+ if (taskInList) {
|
|
|
+ taskInList.status = 'idle'
|
|
|
+ taskInList.executionStatus = 'idle'
|
|
|
+ taskInList.currentWaypointIndex = 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.success('任务已取消')
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.clearTrajectory) {
|
|
|
+ olmap.value.clearTrajectory()
|
|
|
+ settingParams.showRoadNetwork = true
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ ElMessage.error('任务取消失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleNavigationStartReply = (message) => {
|
|
|
+ console.log("导航启动响应:", message)
|
|
|
+ try {
|
|
|
+ if (message.status === 'ok') {
|
|
|
+ navigationStackStatus.value = 'started'
|
|
|
+ ElMessage.success('导航系统已启动')
|
|
|
+ } else {
|
|
|
+ ElMessage.error('导航系统启动失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理导航启动响应失败:", error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleNavigationStopReply = (message) => {
|
|
|
+ console.log("导航停止响应:", message)
|
|
|
+ try {
|
|
|
+ if (message.status === 'ok') {
|
|
|
+ navigationStackStatus.value = 'stopped'
|
|
|
+ isNavigating.value = false
|
|
|
+ navigationStatus.value = 'idle'
|
|
|
+ currentNavigationTask.value = null
|
|
|
+ lastGotoRequest.value = null
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.clearTrajectory) {
|
|
|
+ olmap.value.clearTrajectory()
|
|
|
+ settingParams.showRoadNetwork = true
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.success('导航系统已停止')
|
|
|
+ } else {
|
|
|
+ ElMessage.error('导航系统停止失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理导航停止响应失败:", error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleNavigationRestartReply = (message) => {
|
|
|
+ console.log("导航重启响应:", message)
|
|
|
+ try {
|
|
|
+ if (message.status === 'ok') {
|
|
|
+ navigationStackStatus.value = 'started'
|
|
|
+ isNavigating.value = false
|
|
|
+ navigationStatus.value = 'idle'
|
|
|
+ currentNavigationTask.value = null
|
|
|
+ lastGotoRequest.value = null
|
|
|
+ isTaskExecuting.value = false
|
|
|
+ taskExecutionStatus.value = 'idle'
|
|
|
+ currentExecutingTask.value = null
|
|
|
+ currentTaskWaypointIndex.value = 0
|
|
|
+
|
|
|
+ ElMessage.success('导航系统已重启')
|
|
|
+ isBusy.value = false
|
|
|
+ } else {
|
|
|
+ ElMessage.error('导航系统重启失败')
|
|
|
+ isBusy.value = false
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理导航重启响应失败:", error)
|
|
|
+ isBusy.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleEmergencyStopReply = (message) => {
|
|
|
+ console.log("急停控制响应:", message)
|
|
|
+ try {
|
|
|
+ if (message.status === 'ok') {
|
|
|
+ ElMessage.success('急停指令已执行')
|
|
|
+ } else {
|
|
|
+ ElMessage.error('急停指令执行失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理急停控制响应失败:", error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleEmergencyStopStatus = (message) => {
|
|
|
+ try {
|
|
|
+ if (message.args && message.args.length > 0) {
|
|
|
+ const stopEnabled = message.args[0]
|
|
|
+ const oldStatus = emergencyStopEnabled.value
|
|
|
+ emergencyStopEnabled.value = stopEnabled
|
|
|
+
|
|
|
+ if (oldStatus !== stopEnabled) {
|
|
|
+ if (stopEnabled) {
|
|
|
+ ElMessage.warning('车辆已启用急停状态')
|
|
|
+ isNavigating.value = false
|
|
|
+ navigationStatus.value = 'idle'
|
|
|
+ currentNavigationTask.value = null
|
|
|
+ isTaskExecuting.value = false
|
|
|
+ taskExecutionStatus.value = 'idle'
|
|
|
+ } else {
|
|
|
+ ElMessage.info('车辆急停状态已解除')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理急停状态失败:", error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleObstacleData = (message) => {
|
|
|
+ try {
|
|
|
+ if (message.args && message.args.length > 0) {
|
|
|
+ const obstacleInfo = message.args[0]
|
|
|
+ const obsDistance = obstacleInfo.obs_distanse || obstacleInfo.obs_distance
|
|
|
+
|
|
|
+ if (obsDistance && Array.isArray(obsDistance) && obsDistance.length >= 3) {
|
|
|
+ obstacleData.left = obsDistance[0][0] || 0
|
|
|
+ obstacleData.front = obsDistance[1][0] || 0
|
|
|
+ obstacleData.right = obsDistance[2][0] || 0
|
|
|
+ obstacleData.timestamp = message.timestamp || Date.now()
|
|
|
+
|
|
|
+ checkObstacleWarning()
|
|
|
+
|
|
|
+ console.log("障碍物数据更新:", {
|
|
|
+ left: obstacleData.left + 'm',
|
|
|
+ front: obstacleData.front + 'm',
|
|
|
+ right: obstacleData.right + 'm'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理障碍物数据失败:", error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const checkObstacleWarning = () => {
|
|
|
+ if (!obstacleSettings.enableWarning) return
|
|
|
+
|
|
|
+ const { left, front, right } = obstacleData
|
|
|
+ const { warningDistance, dangerDistance } = obstacleSettings
|
|
|
+
|
|
|
+ let minDistance = Infinity
|
|
|
+ let minDirection = ''
|
|
|
+
|
|
|
+ if (left > 0 && left < minDistance) {
|
|
|
+ minDistance = left
|
|
|
+ minDirection = '左侧'
|
|
|
+ }
|
|
|
+ if (front > 0 && front < minDistance) {
|
|
|
+ minDistance = front
|
|
|
+ minDirection = '前方'
|
|
|
+ }
|
|
|
+ if (right > 0 && right < minDistance) {
|
|
|
+ minDistance = right
|
|
|
+ minDirection = '右侧'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (minDistance !== Infinity) {
|
|
|
+ let level = 'info'
|
|
|
+ let msg = ''
|
|
|
+
|
|
|
+ if (minDistance <= dangerDistance) {
|
|
|
+ level = 'danger'
|
|
|
+ msg = `危险!${minDirection}发现障碍物,距离仅${minDistance.toFixed(1)}米`
|
|
|
+ } else if (minDistance <= warningDistance) {
|
|
|
+ level = 'warning'
|
|
|
+ msg = `注意!${minDirection}发现障碍物,距离${minDistance.toFixed(1)}米`
|
|
|
+ } else {
|
|
|
+ hideObstacleWarning()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ showObstacleWarning(level, msg, minDirection, minDistance)
|
|
|
+ } else {
|
|
|
+ hideObstacleWarning()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const showObstacleWarning = (level, message, direction, distance) => {
|
|
|
+ if (obstacleWarning.autoHideTimer) {
|
|
|
+ clearTimeout(obstacleWarning.autoHideTimer)
|
|
|
+ }
|
|
|
+
|
|
|
+ obstacleWarning.show = true
|
|
|
+ obstacleWarning.level = level
|
|
|
+ obstacleWarning.message = message
|
|
|
+ obstacleWarning.direction = direction
|
|
|
+ obstacleWarning.distance = distance
|
|
|
+ obstacleWarning.autoHideTimer = null
|
|
|
+
|
|
|
+ if (level !== 'danger') {
|
|
|
+ obstacleWarning.autoHideTimer = setTimeout(() => {
|
|
|
+ hideObstacleWarning()
|
|
|
+ }, obstacleSettings.autoHideDelay)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const hideObstacleWarning = () => {
|
|
|
+ if (obstacleWarning.autoHideTimer) {
|
|
|
+ clearTimeout(obstacleWarning.autoHideTimer)
|
|
|
+ }
|
|
|
+
|
|
|
+ obstacleWarning.show = false
|
|
|
+ obstacleWarning.level = 'info'
|
|
|
+ obstacleWarning.message = ''
|
|
|
+ obstacleWarning.direction = ''
|
|
|
+ obstacleWarning.distance = 0
|
|
|
+ obstacleWarning.autoHideTimer = null
|
|
|
+}
|
|
|
+
|
|
|
+const closeObstacleWarning = () => {
|
|
|
+ hideObstacleWarning()
|
|
|
+}
|
|
|
+
|
|
|
+const getRunStateText = (runState) => {
|
|
|
+ const stateMap = {
|
|
|
+ 1: '正常行驶',
|
|
|
+ 2: '暂停状态',
|
|
|
+ 3: '停车待命',
|
|
|
+ 4: '遇障停车',
|
|
|
+ 5: '交通管制等待',
|
|
|
+ 6: '定位切换等待',
|
|
|
+ 7: '人工接管',
|
|
|
+ 8: '绕障状态'
|
|
|
+ }
|
|
|
+ return stateMap[runState] || '未知状态'
|
|
|
+}
|
|
|
+
|
|
|
+const handleReplanRequest = async () => {
|
|
|
+ if (obstacleStopState.isReplanning) {
|
|
|
+ ElMessage.warning('正在请求重规划,请稍候...')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await replan()
|
|
|
+ obstacleStopState.isReplanning = true
|
|
|
+ ElMessage.success('重规划指令已发送')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('重规划请求失败:', error)
|
|
|
+ ElMessage.error('重规划请求失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const calculateTodayScheduledTasks = () => {
|
|
|
+ if (!tasks.value || tasks.value.length === 0) return
|
|
|
+
|
|
|
+ const scheduledTasksList = tasks.value.filter(task => {
|
|
|
+ return task.shouldRunToday && task.scheduledTime !== null
|
|
|
+ })
|
|
|
+
|
|
|
+ scheduledTasksList.sort((a, b) => {
|
|
|
+ if (!a.scheduledTime || !b.scheduledTime) return 0
|
|
|
+ return a.scheduledTime.getTime() - b.scheduledTime.getTime()
|
|
|
+ })
|
|
|
+
|
|
|
+ scheduledTasksToday.value = scheduledTasksList.map(task => task.taskName)
|
|
|
+
|
|
|
+ console.log(`今日应执行的定时任务 (${scheduledTasksToday.value.length}):`,
|
|
|
+ scheduledTasksList.map(t => `${t.taskName} (${t.scheduledTimeStr})`))
|
|
|
+
|
|
|
+ scheduledTasksList.forEach(task => {
|
|
|
+ console.log(`⏰ ${task.taskName}:`, {
|
|
|
+ time: task.scheduledTimeStr,
|
|
|
+ weekDays: task.scheduledWeekDays,
|
|
|
+ points: task.points.length,
|
|
|
+ count: task.count
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ updateTaskListScheduleStatus()
|
|
|
+}
|
|
|
+
|
|
|
+const updateTaskListScheduleStatus = () => {
|
|
|
+ if (!tasks.value || tasks.value.length === 0) return
|
|
|
+
|
|
|
+ tasks.value.forEach(task => {
|
|
|
+ task.scheduleStatus = null
|
|
|
+ task.scheduleProgress = null
|
|
|
+
|
|
|
+ if (!scheduledTasksToday.value.includes(task.taskName)) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (currentScheduledTask.value && task.taskName === currentScheduledTask.value.name) {
|
|
|
+ task.scheduleStatus = 'schedule-running'
|
|
|
+ if (currentExecutingTask.value && currentExecutingTask.value.taskName === task.taskName) {
|
|
|
+ task.scheduleProgress = `${currentTaskWaypointIndex.value}/${currentExecutingTask.value.points?.length || 0}`
|
|
|
+ }
|
|
|
+ } else if (scheduledTasksExecuted.value.includes(task.taskName)) {
|
|
|
+ task.scheduleStatus = 'schedule-executed'
|
|
|
+ } else {
|
|
|
+ task.scheduleStatus = 'schedule-pending'
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const startAutoExecuteTimer = () => {
|
|
|
+ if (!autoExecuteEnabled.value) {
|
|
|
+ console.log('定时任务自动执行功能已禁用')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ stopAutoExecuteTimer()
|
|
|
+
|
|
|
+ autoExecuteTimer.value = setInterval(() => {
|
|
|
+ checkAndExecuteScheduledTasks()
|
|
|
+ }, 30 * 1000)
|
|
|
+
|
|
|
+ console.log('定时任务自动执行功能已启动(每30秒检查一次)')
|
|
|
+ checkAndExecuteScheduledTasks()
|
|
|
+}
|
|
|
+
|
|
|
+const stopAutoExecuteTimer = () => {
|
|
|
+ if (autoExecuteTimer.value) {
|
|
|
+ clearInterval(autoExecuteTimer.value)
|
|
|
+ autoExecuteTimer.value = null
|
|
|
+ console.log('定时任务自动执行功能已停止')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const checkAndExecuteScheduledTasks = () => {
|
|
|
+ if (!autoExecuteEnabled.value || !tasks.value || tasks.value.length === 0) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isTaskExecuting.value || isNavigating.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const now = new Date()
|
|
|
+ const currentTime = now.getTime()
|
|
|
+
|
|
|
+ const tasksToExecute = tasks.value.filter(task => {
|
|
|
+ if (!task.shouldRunToday || !task.scheduledTime) return false
|
|
|
+ if (scheduledTasksExecuted.value.includes(task.taskName)) return false
|
|
|
+ if (currentScheduledTask.value && currentScheduledTask.value.name === task.taskName) return false
|
|
|
+
|
|
|
+ const scheduledTimeMs = task.scheduledTime.getTime()
|
|
|
+ const timeDiff = currentTime - scheduledTimeMs
|
|
|
+
|
|
|
+ return timeDiff >= 0 && timeDiff < 5 * 60 * 1000
|
|
|
+ })
|
|
|
+
|
|
|
+ tasksToExecute.sort((a, b) => a.scheduledTime.getTime() - b.scheduledTime.getTime())
|
|
|
+
|
|
|
+ if (tasksToExecute.length > 0) {
|
|
|
+ const taskToExecute = tasksToExecute[0]
|
|
|
+ console.log(`自动执行定时任务: ${taskToExecute.taskName} (预定时间: ${taskToExecute.scheduledTimeStr})`)
|
|
|
+ executeScheduledTask(taskToExecute)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const executeScheduledTask = async (task) => {
|
|
|
+ if (!task || !task.points || task.points.length === 0) {
|
|
|
+ console.error('任务数据不完整,无法执行:', task.taskName)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(`开始执行定时任务: ${task.taskName}`)
|
|
|
+ console.log(`包含 ${task.points.length} 个目标点`)
|
|
|
+ console.log(`执行次数: ${task.count}`)
|
|
|
+
|
|
|
+ try {
|
|
|
+ const timestamp = new Date().getTime()
|
|
|
+
|
|
|
+ currentExecutingTask.value = task
|
|
|
+ currentTaskWaypointIndex.value = 0
|
|
|
+ lastTaskExecRequest.value = { roadmap: mapName.value, task: task.taskName }
|
|
|
+ isTaskExecuting.value = true
|
|
|
+ taskExecutionStatus.value = 'executing'
|
|
|
+
|
|
|
+ task.status = 'running'
|
|
|
+ task.executionStatus = 'executing'
|
|
|
+
|
|
|
+ // 发送启动任务请求
|
|
|
+ await startTask(mapName.value, task.taskName)
|
|
|
+
|
|
|
+ ElMessage.success(`自动执行定时任务: ${task.taskName}`)
|
|
|
+
|
|
|
+ console.log(`定时任务 "${task.taskName}" 执行指令已发送`)
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`执行定时任务 "${task.taskName}" 失败:`, error)
|
|
|
+ ElMessage.error(`执行定时任务失败: ${error.message}`)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const calculateCurrentProgress = (trajectory) => {
|
|
|
+ if (!trajectory || trajectory.length === 0) return 0
|
|
|
+
|
|
|
+ const robotPos = [robotPoseData.x, robotPoseData.y]
|
|
|
+ let totalDistance = 0
|
|
|
+
|
|
|
+ for (let i = 0; i < trajectory.length - 1; i++) {
|
|
|
+ const segmentLength = Math.sqrt(
|
|
|
+ Math.pow(trajectory[i + 1][0] - trajectory[i][0], 2) +
|
|
|
+ Math.pow(trajectory[i + 1][1] - trajectory[i][1], 2)
|
|
|
+ )
|
|
|
+ totalDistance += segmentLength
|
|
|
+ }
|
|
|
+
|
|
|
+ if (totalDistance === 0) return 0
|
|
|
+
|
|
|
+ let bestProjectionDistance = 0
|
|
|
+ let minDistanceToTrajectory = Infinity
|
|
|
+
|
|
|
+ let accumulatedDistance = 0
|
|
|
+ for (let i = 0; i < trajectory.length - 1; i++) {
|
|
|
+ const segmentStart = trajectory[i]
|
|
|
+ const segmentEnd = trajectory[i + 1]
|
|
|
+ const segmentLength = Math.sqrt(
|
|
|
+ Math.pow(segmentEnd[0] - segmentStart[0], 2) +
|
|
|
+ Math.pow(segmentEnd[1] - segmentStart[1], 2)
|
|
|
+ )
|
|
|
+
|
|
|
+ const projection = projectPointToLineSegment(robotPos, segmentStart, segmentEnd)
|
|
|
+ const distanceToSegment = projection.distance
|
|
|
+
|
|
|
+ if (distanceToSegment < minDistanceToTrajectory) {
|
|
|
+ minDistanceToTrajectory = distanceToSegment
|
|
|
+ const projectionDistanceOnSegment = Math.sqrt(
|
|
|
+ Math.pow(projection.point[0] - segmentStart[0], 2) +
|
|
|
+ Math.pow(projection.point[1] - segmentStart[1], 2)
|
|
|
+ )
|
|
|
+ bestProjectionDistance = accumulatedDistance + projectionDistanceOnSegment
|
|
|
+ }
|
|
|
+
|
|
|
+ accumulatedDistance += segmentLength
|
|
|
+ }
|
|
|
+
|
|
|
+ const progressRatio = bestProjectionDistance / totalDistance
|
|
|
+ const progressIndex = Math.floor(progressRatio * (trajectory.length - 1))
|
|
|
+
|
|
|
+ if (minDistanceToTrajectory < 5.0) {
|
|
|
+ console.log(`机器人轨迹进度: ${progressIndex}/${trajectory.length-1}, 距轨迹距离: ${minDistanceToTrajectory.toFixed(2)}m`)
|
|
|
+ return Math.min(progressIndex, trajectory.length - 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ return olmap.value?.trajectoryProgress || 0
|
|
|
+}
|
|
|
+
|
|
|
+const projectPointToLineSegment = (point, lineStart, lineEnd) => {
|
|
|
+ const [px, py] = point
|
|
|
+ const [x1, y1] = lineStart
|
|
|
+ const [x2, y2] = lineEnd
|
|
|
+
|
|
|
+ const A = px - x1
|
|
|
+ const B = py - y1
|
|
|
+ const C = x2 - x1
|
|
|
+ const D = y2 - y1
|
|
|
+
|
|
|
+ const dot = A * C + B * D
|
|
|
+ const lenSq = C * C + D * D
|
|
|
+
|
|
|
+ if (lenSq === 0) {
|
|
|
+ return {
|
|
|
+ point: [x1, y1],
|
|
|
+ distance: Math.sqrt(A * A + B * B)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let param = dot / lenSq
|
|
|
+ param = Math.max(0, Math.min(1, param))
|
|
|
+
|
|
|
+ const projectionX = x1 + param * C
|
|
|
+ const projectionY = y1 + param * D
|
|
|
+
|
|
|
+ const distance = Math.sqrt(
|
|
|
+ Math.pow(px - projectionX, 2) + Math.pow(py - projectionY, 2)
|
|
|
+ )
|
|
|
+
|
|
|
+ return {
|
|
|
+ point: [projectionX, projectionY],
|
|
|
+ distance: distance
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const checkIfReachedTarget = (trajectory) => {
|
|
|
+ if (!trajectory || trajectory.length === 0) return false
|
|
|
+
|
|
|
+ if (navigationStatus.value === 'arrived' || navigationStatus.value === 'failed') {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ const robotPos = [laserPositionData.x, laserPositionData.y]
|
|
|
+ const targetPos = trajectory[trajectory.length - 1]
|
|
|
+
|
|
|
+ const distanceToTarget = Math.sqrt(
|
|
|
+ Math.pow(targetPos[0] - robotPos[0], 2) + Math.pow(targetPos[1] - robotPos[1], 2)
|
|
|
+ )
|
|
|
+
|
|
|
+ if (distanceToTarget < 1.0 && isNavigating.value) {
|
|
|
+ console.log(`本地检测: 机器人接近目标点,距离: ${distanceToTarget.toFixed(2)}m,等待WebSocket到达确认`)
|
|
|
+ }
|
|
|
+
|
|
|
+ return false
|
|
|
+}
|
|
|
+
|
|
|
+const publishMsg = () => {}
|
|
|
+
|
|
|
+const updateOlCss = () => {
|
|
|
+ const element = mapStage.value
|
|
|
+ if (element) {
|
|
|
+ const newWidth = element.offsetWidth
|
|
|
+ const newHeight = element.offsetHeight
|
|
|
+ // 调试日志:检测容器尺寸变化
|
|
|
+ if (newWidth === 0 || newHeight === 0) {
|
|
|
+ console.warn('[updateOlCss] 容器尺寸为0,可能需要等待DOM渲染完成', { width: newWidth, height: newHeight })
|
|
|
+ }
|
|
|
+ olWidth.value = newWidth
|
|
|
+ olHeight.value = newHeight
|
|
|
+ } else {
|
|
|
+ console.warn('[updateOlCss] mapStage element 为空')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const getMapInstance = () => {
|
|
|
+ return olmap.value && olmap.value.map ? olmap.value.map : null
|
|
|
+}
|
|
|
+
|
|
|
+const openDraSetting = () => {
|
|
|
+ initPoseMode.value = false
|
|
|
+ settingDrawer.value = !settingDrawer.value
|
|
|
+ if (settingDrawer.value) {
|
|
|
+ closeDra()
|
|
|
+ nowHandMenu.value = '功能菜单操作'
|
|
|
+ } else {
|
|
|
+ nowHandMenu.value = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const openPoint = () => {
|
|
|
+ initPoseMode.value = false
|
|
|
+ pointDrawer.value = !pointDrawer.value
|
|
|
+ if (pointDrawer.value) {
|
|
|
+ closeDra()
|
|
|
+ nowHandMenu.value = '目标点操作'
|
|
|
+ } else {
|
|
|
+ nowHandMenu.value = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const openTask = () => {
|
|
|
+ initPoseMode.value = false
|
|
|
+ taskDrawer.value = !taskDrawer.value
|
|
|
+ if (taskDrawer.value) {
|
|
|
+ closeDra()
|
|
|
+ nowHandMenu.value = '任务操作'
|
|
|
+ } else {
|
|
|
+ nowHandMenu.value = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const closeDra = (type) => {
|
|
|
+ settingDrawer.value = false
|
|
|
+ pointDrawer.value = false
|
|
|
+ taskDrawer.value = false
|
|
|
+ if (type == 'hand') {
|
|
|
+ activeTab.value = -1
|
|
|
+ }
|
|
|
+ nowHandMenu.value = ''
|
|
|
+}
|
|
|
+
|
|
|
+const handleSelectionChange = (selection) => {
|
|
|
+ pointIds.value = selection.map(item => item.id)
|
|
|
+ single.value = selection.length !== 1
|
|
|
+ multiple.value = !selection.length
|
|
|
+}
|
|
|
+
|
|
|
+const editPoint = (row) => {
|
|
|
+ pointEditDiaShow.value = true
|
|
|
+ pointEditData.id = row.id
|
|
|
+ pointEditData.x = row.x
|
|
|
+ pointEditData.y = row.y
|
|
|
+ pointEditData.type = row.type
|
|
|
+ pointEditData.actionMenuList = row.action
|
|
|
+}
|
|
|
+
|
|
|
+const removePoint = (row) => {
|
|
|
+ const idArr = []
|
|
|
+ const ids = row?.id || pointIds.value
|
|
|
+ if (ids) {
|
|
|
+ idArr.push(...(Array.isArray(ids) ? ids : [ids]))
|
|
|
+ idArr.forEach(id => {
|
|
|
+ let indexToRemove = pointList.value.findIndex(item => item?.id === id)
|
|
|
+ if (indexToRemove !== -1) {
|
|
|
+ pointList.value.splice(indexToRemove, 1)
|
|
|
+ if (pointList.value.length < 1) {
|
|
|
+ olmap.value?.restIdNum()
|
|
|
+ }
|
|
|
+ olmap.value?.removeCalibrationById(id)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const moveUp = () => {
|
|
|
+ const index = pointList.value.findIndex(point => point.id == pointIds.value[0])
|
|
|
+ if (index !== -1 && index > 0) {
|
|
|
+ const temp = pointList.value[index]
|
|
|
+ pointList.value[index] = pointList.value[index - 1]
|
|
|
+ pointList.value[index - 1] = temp
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const moveDown = () => {
|
|
|
+ const index = pointList.value.findIndex(point => point.id == pointIds.value[0])
|
|
|
+ if (index !== -1 && index < pointList.value.length - 1) {
|
|
|
+ const temp = pointList.value[index]
|
|
|
+ pointList.value[index] = pointList.value[index + 1]
|
|
|
+ pointList.value[index + 1] = temp
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const appendActionMenu = () => {
|
|
|
+ pointEditData.actionMenuList.push({ value: 0, other: 0 })
|
|
|
+}
|
|
|
+
|
|
|
+const removeActionMenu = (index) => {
|
|
|
+ pointEditData.actionMenuList.splice(index, 1)
|
|
|
+}
|
|
|
+
|
|
|
+const changeAction = (index) => {
|
|
|
+ const item = pointEditData.actionMenuList[index]
|
|
|
+ if (item && 'other' in item) {
|
|
|
+ delete item.other
|
|
|
+ } else {
|
|
|
+ item.other = 0
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const clearActionDia = () => {
|
|
|
+ pointEditData.id = ''
|
|
|
+ pointEditData.x = ''
|
|
|
+ pointEditData.y = ''
|
|
|
+ pointEditData.type = ''
|
|
|
+ pointEditData.actionMenuList = []
|
|
|
+}
|
|
|
+
|
|
|
+const submitEditPoint = () => {
|
|
|
+ pointEditDiaShow.value = false
|
|
|
+ const point = waypoints.value.find(item => item.id === pointEditData.id)
|
|
|
+ if (point) {
|
|
|
+ point.x = pointEditData.x
|
|
|
+ point.y = pointEditData.y
|
|
|
+ point.type = pointEditData.type
|
|
|
+ point.action = [...pointEditData.actionMenuList]
|
|
|
+ ElMessage.success("目标点数据已修改")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const submitTaskGenerate = async () => {
|
|
|
+ const orderedPoints = generateTaskParam.selectedWaypoints || []
|
|
|
+ if (!generateTaskParam.taskName || orderedPoints.length === 0) {
|
|
|
+ ElMessage.warning('请完善任务数据(任务名称和目标点不能为空)!')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const coordinates = orderedPoints.map(point => [
|
|
|
+ parseFloat(point.x),
|
|
|
+ parseFloat(point.y),
|
|
|
+ 0
|
|
|
+ ])
|
|
|
+
|
|
|
+ const plan = orderedPoints.map(point => {
|
|
|
+ return point.type === 1 ? 'route' : 'free'
|
|
|
+ })
|
|
|
+
|
|
|
+ const action = orderedPoints.map(point => {
|
|
|
+ if (!point.action || point.action.length === 0) {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+ return point.action.map(act => {
|
|
|
+ if (act.value === 0 && 'other' in act) {
|
|
|
+ return { type: 'wait', duration: parseFloat(act.other) || 0 }
|
|
|
+ } else {
|
|
|
+ const actionTypes = ['wait', 'start_record', 'stop_record', 'add_trajectory', 'hook_mount', 'hook_unmount']
|
|
|
+ return { type: actionTypes[act.value] || 'wait' }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ let cron = ''
|
|
|
+ if (generateTaskParam.time && generateTaskParam.date.length > 0) {
|
|
|
+ let hours, minutes
|
|
|
+ if (typeof generateTaskParam.time === 'string') {
|
|
|
+ const timeParts = generateTaskParam.time.split(':')
|
|
|
+ hours = timeParts[0]
|
|
|
+ minutes = timeParts[1]
|
|
|
+ } else {
|
|
|
+ const timeObj = new Date(generateTaskParam.time)
|
|
|
+ hours = String(timeObj.getHours()).padStart(2, '0')
|
|
|
+ minutes = String(timeObj.getMinutes()).padStart(2, '0')
|
|
|
+ }
|
|
|
+ const weekdays = generateTaskParam.date.sort().join(',')
|
|
|
+ cron = `00 ${minutes} ${hours} ? * ${weekdays} *`
|
|
|
+ }
|
|
|
+
|
|
|
+ const taskData = {
|
|
|
+ map: mapName.value,
|
|
|
+ path: generateTaskParam.taskName,
|
|
|
+ coord_type: 'local',
|
|
|
+ cron: cron,
|
|
|
+ coord: coordinates,
|
|
|
+ plan: plan,
|
|
|
+ action: [[]],
|
|
|
+ repeate: generateTaskParam.count || 1
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await addTask(taskData)
|
|
|
+ console.log("创建任务响应:", response)
|
|
|
+
|
|
|
+ if (response.status === true) {
|
|
|
+ ElMessage.success(`任务创建成功!任务包含 ${orderedPoints.length} 个目标点`)
|
|
|
+ await loadTaskList()
|
|
|
+ restGenerateParam()
|
|
|
+ taskGenerateDiaShow.value = false
|
|
|
+ selectedWaypointIds.value = []
|
|
|
+ console.log("任务创建成功:", taskData)
|
|
|
+ } else {
|
|
|
+ ElMessage.error('任务创建失败: ' + (response.msg || '未知错误'))
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("创建任务失败:", error)
|
|
|
+ ElMessage.error('任务创建失败: ' + (error.message || '未知错误'))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const initNavigation = () => {
|
|
|
+ closeDra()
|
|
|
+ initPoseMode.value = !initPoseMode.value
|
|
|
+ nowHandMenu.value = initPoseMode.value ? '初始化导航' : ''
|
|
|
+ selectPointMode.value = false
|
|
|
+}
|
|
|
+
|
|
|
+const restNavigation = async () => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('将重启当前导航, 是否继续?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ ElMessage.success('导航已重启!')
|
|
|
+ } catch {
|
|
|
+ // 用户取消
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const offNavigation = async () => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('将关闭当前导航, 是否继续?', '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ ElMessage.success('导航已关闭!')
|
|
|
+ } catch {
|
|
|
+ // 用户取消
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const closeTaskGenerate = () => {
|
|
|
+ taskGenerateDiaShow.value = false
|
|
|
+ restGenerateParam()
|
|
|
+}
|
|
|
+
|
|
|
+const restGenerateParam = () => {
|
|
|
+ generateTaskParam.taskId = ''
|
|
|
+ generateTaskParam.taskName = ''
|
|
|
+ generateTaskParam.count = 1
|
|
|
+ generateTaskParam.time = ''
|
|
|
+ generateTaskParam.date = []
|
|
|
+ generateTaskParam.selectedWaypoints = []
|
|
|
+}
|
|
|
+
|
|
|
+const removeTaskItem = async (data) => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('删除名为' + data.taskName + '的任务', '删除', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ ElMessage.success('已删除!')
|
|
|
+ taskDataList.value = taskDataList.value.filter(task => task.taskId !== data.taskId)
|
|
|
+ } catch {
|
|
|
+ // 用户取消
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const executeTask = async (data) => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('开始执行任务' + data.taskName + '?', '执行', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ ElMessage.success('任务已开始执行!')
|
|
|
+ taskDataList.value.forEach(task => {
|
|
|
+ if (task.taskId === data.taskId) {
|
|
|
+ task.status = 0
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } catch {
|
|
|
+ // 用户取消
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const test = () => {
|
|
|
+ console.log(settingParams)
|
|
|
+}
|
|
|
+
|
|
|
+const mapSelectEle = (type) => {
|
|
|
+ if (type == 'open') {
|
|
|
+ selectPointMode.value = true
|
|
|
+ initPoseMode.value = false
|
|
|
+ ElMessage.success('已开启选择模式')
|
|
|
+ } else {
|
|
|
+ selectPointMode.value = false
|
|
|
+ ElMessage.info('已关闭选择模式')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const addNowPoint = (currentCoordinate, currentPlace) => {
|
|
|
+ const coordData = {
|
|
|
+ id: currentCoordinate[0],
|
|
|
+ name: `目标点${currentCoordinate[0]}`,
|
|
|
+ x: currentCoordinate[1].toFixed(3),
|
|
|
+ y: currentCoordinate[2].toFixed(3),
|
|
|
+ placeX: currentPlace[0].toFixed(3),
|
|
|
+ placeY: currentPlace[1].toFixed(3),
|
|
|
+ type: 1,
|
|
|
+ action: [{ value: 0, other: 0 }]
|
|
|
+ }
|
|
|
+ waypoints.value.push(coordData)
|
|
|
+ ElMessage.success(`已添加目标点: (${coordData.x}, ${coordData.y})`)
|
|
|
+}
|
|
|
+
|
|
|
+const initNavigationResult = (position, yaw, nid) => {
|
|
|
+ console.log("=== 位姿初始化 ===")
|
|
|
+ console.log("位置:", `x=${position[0].toFixed(3)}, y=${position[1].toFixed(3)}`)
|
|
|
+ console.log("朝向角(弧度):", yaw.toFixed(4))
|
|
|
+ console.log("朝向角(度):", (yaw * 180 / Math.PI).toFixed(2) + "°")
|
|
|
+ console.log("点位ID:", nid || "null")
|
|
|
+
|
|
|
+ // 根据是否有nid选择使用哪个接口
|
|
|
+ if (nid) {
|
|
|
+ initPoseByNid(nid).then(() => {
|
|
|
+ ElMessage.success({
|
|
|
+ message: `位姿初始化成功!位置: (${position[0].toFixed(2)}, ${position[1].toFixed(2)}), 朝向: ${(yaw * 180 / Math.PI).toFixed(1)}°`,
|
|
|
+ duration: 3000
|
|
|
+ })
|
|
|
+ }).catch(error => {
|
|
|
+ console.error('位姿初始化失败:', error)
|
|
|
+ ElMessage.error('位姿初始化失败')
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ initPose(position[0], position[1], yaw).then(() => {
|
|
|
+ ElMessage.success({
|
|
|
+ message: `位姿初始化成功!位置: (${position[0].toFixed(2)}, ${position[1].toFixed(2)}), 朝向: ${(yaw * 180 / Math.PI).toFixed(1)}°`,
|
|
|
+ duration: 3000
|
|
|
+ })
|
|
|
+ }).catch(error => {
|
|
|
+ console.error('位姿初始化失败:', error)
|
|
|
+ ElMessage.error('位姿初始化失败')
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ initPoseMode.value = false
|
|
|
+ nowHandMenu.value = ''
|
|
|
+}
|
|
|
+
|
|
|
+const onTabChange = (tabKey) => {
|
|
|
+ activeTab.value = tabKey
|
|
|
+ lastTab.value = tabKey
|
|
|
+
|
|
|
+ if (tabKey === 'points' && selectPointMode.value) {
|
|
|
+ // 保持选点模式
|
|
|
+ } else if (tabKey !== 'points') {
|
|
|
+ selectPointMode.value = false
|
|
|
+ nowHandMenu.value = ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const toggleSelectPointMode = () => {
|
|
|
+ selectPointMode.value = !selectPointMode.value
|
|
|
+ initPoseMode.value = false
|
|
|
+ nowHandMenu.value = selectPointMode.value ? '选点模式' : ''
|
|
|
+
|
|
|
+ if (selectPointMode.value) {
|
|
|
+ ElMessage.success('已进入选点模式')
|
|
|
+ } else {
|
|
|
+ ElMessage.info('已退出选点模式')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const clearAllPoints = async () => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('确定要清空所有点位吗?', '确认清空', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ waypoints.value = []
|
|
|
+ if (olmap.value && olmap.value.restIdNum) {
|
|
|
+ olmap.value.restIdNum()
|
|
|
+ }
|
|
|
+ ElMessage.success('已清空所有点位')
|
|
|
+ } catch {
|
|
|
+ // 用户取消
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleZoomIn = () => {
|
|
|
+ try {
|
|
|
+ const map = getMapInstance()
|
|
|
+ if (map) {
|
|
|
+ const view = map.getView()
|
|
|
+ const currentZoom = view.getZoom()
|
|
|
+ view.animate({
|
|
|
+ zoom: currentZoom + 1,
|
|
|
+ duration: 250
|
|
|
+ })
|
|
|
+ } else if (olmap.value && olmap.value.zoomIn) {
|
|
|
+ olmap.value.zoomIn()
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('地图放大失败:', error)
|
|
|
+ ElMessage.warning('地图放大失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleZoomOut = () => {
|
|
|
+ try {
|
|
|
+ const map = getMapInstance()
|
|
|
+ if (map) {
|
|
|
+ const view = map.getView()
|
|
|
+ const currentZoom = view.getZoom()
|
|
|
+ view.animate({
|
|
|
+ zoom: Math.max(currentZoom - 1, 1),
|
|
|
+ duration: 250
|
|
|
+ })
|
|
|
+ } else if (olmap.value && olmap.value.zoomOut) {
|
|
|
+ olmap.value.zoomOut()
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('地图缩小失败:', error)
|
|
|
+ ElMessage.warning('地图缩小失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleCenterToRobot = () => {
|
|
|
+ try {
|
|
|
+ const map = getMapInstance()
|
|
|
+ if (map) {
|
|
|
+ const view = map.getView()
|
|
|
+
|
|
|
+ let centerPoint = [robotPoseData.x, robotPoseData.y]
|
|
|
+ if (robotPoseData.x === 0 && robotPoseData.y === 0) {
|
|
|
+ centerPoint = view.getCenter()
|
|
|
+ console.log('使用地图中心点作为居中位置:', centerPoint)
|
|
|
+ } else {
|
|
|
+ console.log('使用机器人位置作为居中位置:', centerPoint)
|
|
|
+ }
|
|
|
+
|
|
|
+ view.animate({
|
|
|
+ center: centerPoint,
|
|
|
+ zoom: Math.max(view.getZoom(), 15),
|
|
|
+ duration: 500
|
|
|
+ })
|
|
|
+ ElMessage.success('已居中到机器人位置')
|
|
|
+ } else if (olmap.value && olmap.value.centerToRobot) {
|
|
|
+ olmap.value.centerToRobot()
|
|
|
+ ElMessage.success('已居中到机器人位置')
|
|
|
+ } else {
|
|
|
+ console.warn('无法获取地图实例')
|
|
|
+ ElMessage.warning('地图未就绪,无法居中')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('定位机器人失败:', error)
|
|
|
+ console.log('调试信息:', {
|
|
|
+ robotPoseData: robotPoseData,
|
|
|
+ robotPosition: robotPosition.value,
|
|
|
+ mapReady: !!getMapInstance()
|
|
|
+ })
|
|
|
+ ElMessage.warning('定位机器人失败: ' + error.message)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleToggleFullscreen = () => {
|
|
|
+ const mapContainer = mapStage.value
|
|
|
+ if (!mapContainer) {
|
|
|
+ ElMessage.error('无法找到地图容器')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (FullscreenOperations.toggleFullscreen(mapContainer)) {
|
|
|
+ // 全屏切换成功
|
|
|
+ } else {
|
|
|
+ ElMessage.error('浏览器不支持全屏功能')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleConfirmInit = () => {
|
|
|
+ initPoseMode.value = true
|
|
|
+ selectPointMode.value = false
|
|
|
+ nowHandMenu.value = '初始化导航'
|
|
|
+ ElMessage.success('已进入位姿初始化模式')
|
|
|
+}
|
|
|
+
|
|
|
+const handleConfirmReboot = async () => {
|
|
|
+ isBusy.value = true
|
|
|
+ try {
|
|
|
+ await startNavStandard(mapName.value)
|
|
|
+ ElMessage.success('导航重启指令已发送')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('导航重启请求失败:', error)
|
|
|
+ ElMessage.error('导航重启请求失败')
|
|
|
+ }
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ if (isBusy.value) {
|
|
|
+ isBusy.value = false
|
|
|
+ ElMessage.warning('导航重启响应超时,请检查系统状态')
|
|
|
+ }
|
|
|
+ }, 15000)
|
|
|
+}
|
|
|
+
|
|
|
+const handleConfirmStop = async () => {
|
|
|
+ selectPointMode.value = false
|
|
|
+ initPoseMode.value = false
|
|
|
+ nowHandMenu.value = ''
|
|
|
+
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('请选择要执行的操作:', '操作选择', {
|
|
|
+ confirmButtonText: '急停',
|
|
|
+ cancelButtonText: '停止导航',
|
|
|
+ distinguishCancelAndClose: true,
|
|
|
+ type: 'warning',
|
|
|
+ customClass: 'stop-action-dialog'
|
|
|
+ })
|
|
|
+ executeEmergencyStop()
|
|
|
+ } catch (action) {
|
|
|
+ if (action === 'cancel') {
|
|
|
+ executeNavigationStop()
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const executeEmergencyStop = async () => {
|
|
|
+ try {
|
|
|
+ await emergencyStop(true)
|
|
|
+ ElMessage.warning('急停指令已发送')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('急停请求失败:', error)
|
|
|
+ ElMessage.error('急停请求失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const executeNavigationStop = async () => {
|
|
|
+ try {
|
|
|
+ await stopNavigation()
|
|
|
+ ElMessage.info('导航停止指令已发送')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('导航停止请求失败:', error)
|
|
|
+ ElMessage.error('导航停止请求失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const executeEmergencyStopRelease = async () => {
|
|
|
+ try {
|
|
|
+ await releaseEmergencyStop()
|
|
|
+ ElMessage.success('解除急停指令已发送')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('解除急停请求失败:', error)
|
|
|
+ ElMessage.error('解除急停请求失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleNavigationPause = async () => {
|
|
|
+ if (!isNavigating.value || navigationStatus.value === 'paused') {
|
|
|
+ ElMessage.warning('当前没有正在执行的导航任务')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await pauseTask()
|
|
|
+ navigationStatus.value = 'paused'
|
|
|
+ ElMessage.success('导航已暂停')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('暂停导航请求失败:', error)
|
|
|
+ ElMessage.error('暂停导航请求失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleNavigationResume = async () => {
|
|
|
+ if (!currentNavigationTask.value || navigationStatus.value !== 'paused') {
|
|
|
+ ElMessage.warning('没有可恢复的导航任务')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await resumeTask()
|
|
|
+ navigationStatus.value = 'navigating'
|
|
|
+ ElMessage.success('导航已恢复')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('恢复导航请求失败:', error)
|
|
|
+ ElMessage.error('恢复导航请求失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleNavigationStop = async () => {
|
|
|
+ if (!isNavigating.value && navigationStatus.value === 'idle') {
|
|
|
+ ElMessage.warning('当前没有导航任务')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('确定要停止当前导航任务吗?', '确认停止', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+
|
|
|
+ await cancelTask()
|
|
|
+
|
|
|
+ isNavigating.value = false
|
|
|
+ navigationStatus.value = 'idle'
|
|
|
+ currentNavigationTask.value = null
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.clearTrajectory) {
|
|
|
+ olmap.value.clearTrajectory()
|
|
|
+ settingParams.showRoadNetwork = true
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.success('导航任务已停止')
|
|
|
+ } catch {
|
|
|
+ // 用户取消
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const showCreateTaskDialog = () => {
|
|
|
+ ElMessage.info('创建任务功能待接入')
|
|
|
+}
|
|
|
+
|
|
|
+// const startTask = (task) => {
|
|
|
+// task.status = 'running'
|
|
|
+// ElMessage.success(`任务 "${task.name}" 已开始执行`)
|
|
|
+// }
|
|
|
+
|
|
|
+/* const pauseTask = (task) => {
|
|
|
+ task.status = 'paused'
|
|
|
+ ElMessage.warning(`任务 "${task.name}" 已暂停`)
|
|
|
+}
|
|
|
+
|
|
|
+const resumeTask = (task) => {
|
|
|
+ task.status = 'running'
|
|
|
+ ElMessage.success(`任务 "${task.name}" 已继续执行`)
|
|
|
+}
|
|
|
+
|
|
|
+const cancelTask = async (task) => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(`确定要取消任务 "${task.name}" 吗?`, '确认取消', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ task.status = 'idle'
|
|
|
+ ElMessage.info(`任务 "${task.name}" 已取消`)
|
|
|
+ } catch {
|
|
|
+ // 用户取消
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const toggleInitPoseMode = () => {
|
|
|
+ initPoseMode.value = !initPoseMode.value
|
|
|
+ selectPointMode.value = false
|
|
|
+ nowHandMenu.value = initPoseMode.value ? '初始化导航' : ''
|
|
|
+
|
|
|
+ if (initPoseMode.value) {
|
|
|
+ ElMessage.success('已进入位姿初始化模式')
|
|
|
+ } else {
|
|
|
+ ElMessage.info('已退出位姿初始化模式')
|
|
|
+ }
|
|
|
+} */
|
|
|
+
|
|
|
+const onRestart = () => {
|
|
|
+ handleConfirmReboot()
|
|
|
+}
|
|
|
+
|
|
|
+const onStop = () => {
|
|
|
+ handleConfirmStop()
|
|
|
+}
|
|
|
+
|
|
|
+const onInit = () => {
|
|
|
+ handleConfirmInit()
|
|
|
+}
|
|
|
+
|
|
|
+const onZoomIn = () => {
|
|
|
+ handleZoomIn()
|
|
|
+}
|
|
|
+
|
|
|
+const onZoomOut = () => {
|
|
|
+ handleZoomOut()
|
|
|
+}
|
|
|
+
|
|
|
+const onCenterRobot = () => {
|
|
|
+ handleCenterToRobot()
|
|
|
+}
|
|
|
+
|
|
|
+const onToggleFullscreen = () => {
|
|
|
+ handleToggleFullscreen()
|
|
|
+}
|
|
|
+
|
|
|
+const getTaskStatusText = (status) => {
|
|
|
+ const statusMap = {
|
|
|
+ 'idle': '空闲',
|
|
|
+ 'running': '执行中',
|
|
|
+ 'paused': '暂停'
|
|
|
+ }
|
|
|
+ return statusMap[status] || '未知'
|
|
|
+}
|
|
|
+
|
|
|
+const onWpSelect = (waypoint) => {
|
|
|
+ console.log('选择目标点:', waypoint)
|
|
|
+ ElMessage.success(`已选择目标点: ${waypoint.name}`)
|
|
|
+}
|
|
|
+
|
|
|
+const onWpSend = (waypoint) => {
|
|
|
+ console.log('发送目标点:', waypoint)
|
|
|
+ ElMessage.success(`已发送目标点: ${waypoint.name}`)
|
|
|
+}
|
|
|
+
|
|
|
+const onWpCreate = () => {
|
|
|
+ console.log('创建目标点')
|
|
|
+ ElMessage.info('创建目标点功能待实现')
|
|
|
+}
|
|
|
+
|
|
|
+const onWpEdit = (waypoint) => {
|
|
|
+ console.log('编辑目标点:', waypoint)
|
|
|
+ pointEditDiaShow.value = true
|
|
|
+ pointEditData.id = waypoint.id
|
|
|
+ pointEditData.x = waypoint.x
|
|
|
+ pointEditData.y = waypoint.y
|
|
|
+ pointEditData.type = waypoint.type
|
|
|
+ pointEditData.actionMenuList = waypoint.action ? [...waypoint.action] : [{ value: 0, other: 0 }]
|
|
|
+}
|
|
|
+
|
|
|
+const onWpRemove = (waypoint) => {
|
|
|
+ console.log('删除目标点:', waypoint)
|
|
|
+ waypoints.value = waypoints.value.filter(wp => wp.id !== waypoint.id)
|
|
|
+ selectedWaypointIds.value = selectedWaypointIds.value.filter(id => id !== waypoint.id)
|
|
|
+ olmap.value?.removeIconHtmlById("calibration-" + waypoint.id)
|
|
|
+ ElMessage.success(`已删除目标点: ${waypoint.name || '目标点'}`)
|
|
|
+}
|
|
|
+
|
|
|
+const onWpMoveUp = () => {
|
|
|
+ if (selectedWaypointIds.value.length !== 1) return
|
|
|
+
|
|
|
+ const selectedId = selectedWaypointIds.value[0]
|
|
|
+ const index = waypoints.value.findIndex(wp => wp.id === selectedId)
|
|
|
+
|
|
|
+ if (index > 0) {
|
|
|
+ const temp = waypoints.value[index]
|
|
|
+ waypoints.value[index] = waypoints.value[index - 1]
|
|
|
+ waypoints.value[index - 1] = temp
|
|
|
+ ElMessage.success('目标点已上移')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onWpMoveDown = () => {
|
|
|
+ if (selectedWaypointIds.value.length !== 1) return
|
|
|
+
|
|
|
+ const selectedId = selectedWaypointIds.value[0]
|
|
|
+ const index = waypoints.value.findIndex(wp => wp.id === selectedId)
|
|
|
+
|
|
|
+ if (index < waypoints.value.length - 1) {
|
|
|
+ const temp = waypoints.value[index]
|
|
|
+ waypoints.value[index] = waypoints.value[index + 1]
|
|
|
+ waypoints.value[index + 1] = temp
|
|
|
+ ElMessage.success('目标点已下移')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onWpBatchRemove = async () => {
|
|
|
+ if (selectedWaypointIds.value.length === 0) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(`确定要删除选中的 ${selectedWaypointIds.value.length} 个目标点吗?`, '批量删除', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+
|
|
|
+ const waypointsToRemove = waypoints.value.filter(wp => selectedWaypointIds.value.includes(wp.id))
|
|
|
+
|
|
|
+ waypointsToRemove.forEach(wp => {
|
|
|
+ olmap.value?.removeIconHtmlById("calibration-" + wp.id)
|
|
|
+ })
|
|
|
+
|
|
|
+ waypoints.value = waypoints.value.filter(wp => !selectedWaypointIds.value.includes(wp.id))
|
|
|
+ selectedWaypointIds.value = []
|
|
|
+ ElMessage.success('目标点删除成功')
|
|
|
+ } catch {
|
|
|
+ // 用户取消
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onWpGoto = async () => {
|
|
|
+ if (selectedWaypointIds.value.length !== 1) return
|
|
|
+
|
|
|
+ if (isNavigating.value) {
|
|
|
+ ElMessage.warning('机器人正在导航中,请等待任务完成后再发送新任务')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const selectedWaypoint = waypoints.value.find(wp => wp.id === selectedWaypointIds.value[0])
|
|
|
+ console.log('前往目标点:', selectedWaypoint)
|
|
|
+
|
|
|
+ if (selectedWaypoint) {
|
|
|
+ if (olmap.value && olmap.value.clearTrajectory) {
|
|
|
+ olmap.value.clearTrajectory()
|
|
|
+ console.log('已清除之前的轨迹')
|
|
|
+ }
|
|
|
+
|
|
|
+ const timestamp = new Date().getTime()
|
|
|
+
|
|
|
+ currentNavigationTask.value = {
|
|
|
+ waypoint: selectedWaypoint,
|
|
|
+ timestamp: timestamp,
|
|
|
+ status: 'planning'
|
|
|
+ }
|
|
|
+ lastGotoRequest.value = { roadmap: mapName.value, coord: [[Number(selectedWaypoint.x), Number(selectedWaypoint.y)]] }
|
|
|
+ navigationStatus.value = 'planning'
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 发送路径规划请求
|
|
|
+ await requestPlanning(mapName.value, Number(selectedWaypoint.x), Number(selectedWaypoint.y))
|
|
|
+ // 发送前往目标点请求
|
|
|
+ await gotoTarget(mapName.value, Number(selectedWaypoint.x), Number(selectedWaypoint.y))
|
|
|
+ ElMessage.success(`正在发送前往指令: ${selectedWaypoint.name || '目标点' + selectedWaypoint.id} (${selectedWaypoint.x}, ${selectedWaypoint.y})`)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发送导航指令失败:', error)
|
|
|
+ ElMessage.error('发送导航指令失败')
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onWpGotoSingle = async (waypoint) => {
|
|
|
+ if (isNavigating.value) {
|
|
|
+ ElMessage.warning('机器人正在导航中,请等待任务完成后再发送新任务')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.clearTrajectory) {
|
|
|
+ olmap.value.clearTrajectory()
|
|
|
+ console.log('已清除之前的轨迹')
|
|
|
+ }
|
|
|
+
|
|
|
+ const timestamp = new Date().getTime()
|
|
|
+
|
|
|
+ currentNavigationTask.value = {
|
|
|
+ waypoint: waypoint,
|
|
|
+ timestamp: timestamp,
|
|
|
+ status: 'planning'
|
|
|
+ }
|
|
|
+ lastGotoRequest.value = { roadmap: mapName.value, coord: [[Number(waypoint.x), Number(waypoint.y)]] }
|
|
|
+ navigationStatus.value = 'planning'
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 发送路径规划请求
|
|
|
+ await requestPlanning(mapName.value, Number(waypoint.x), Number(waypoint.y))
|
|
|
+ // 发送前往目标点请求
|
|
|
+ await gotoTarget(mapName.value, Number(waypoint.x), Number(waypoint.y))
|
|
|
+ ElMessage.success(`正在发送前往指令: ${waypoint.name || '目标点' + waypoint.id} (${waypoint.x}, ${waypoint.y})`)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发送导航指令失败:', error)
|
|
|
+ ElMessage.error('发送导航指令失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const cancelCurrentNavigation = async () => {
|
|
|
+ if (!isNavigating.value) {
|
|
|
+ ElMessage.info('当前没有正在进行的导航任务')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm('确定要取消当前的导航任务吗?', '取消导航', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+
|
|
|
+ navigationStatus.value = 'idle'
|
|
|
+ isNavigating.value = false
|
|
|
+ currentNavigationTask.value = null
|
|
|
+ lastGotoRequest.value = null
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.clearTrajectory) {
|
|
|
+ olmap.value.clearTrajectory()
|
|
|
+ settingParams.showRoadNetwork = true
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.success('导航任务已取消')
|
|
|
+ console.log('用户取消了导航任务')
|
|
|
+ } catch {
|
|
|
+ // 用户取消
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const executeNextWaypoint = async () => {
|
|
|
+ if (!currentExecutingTask.value || !currentExecutingTask.value.points) {
|
|
|
+ console.error('没有正在执行的任务或任务没有目标点')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const waypointList = currentExecutingTask.value.points
|
|
|
+ const currentIndex = currentTaskWaypointIndex.value
|
|
|
+
|
|
|
+ if (currentIndex >= waypointList.length) {
|
|
|
+ console.log('所有目标点已执行完成')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentWaypoint = waypointList[currentIndex]
|
|
|
+ console.log(`执行目标点 ${currentIndex + 1}/${waypointList.length}:`, currentWaypoint)
|
|
|
+
|
|
|
+ if (olmap.value && olmap.value.clearTrajectory) {
|
|
|
+ olmap.value.clearTrajectory()
|
|
|
+ console.log('已清除之前的轨迹,准备执行下一个目标点')
|
|
|
+ }
|
|
|
+
|
|
|
+ const timestamp = new Date().getTime()
|
|
|
+
|
|
|
+ currentNavigationTask.value = {
|
|
|
+ waypoint: currentWaypoint,
|
|
|
+ timestamp: timestamp,
|
|
|
+ taskContext: {
|
|
|
+ taskId: currentExecutingTask.value.taskId,
|
|
|
+ waypointIndex: currentIndex,
|
|
|
+ totalWaypoints: waypointList.length
|
|
|
+ }
|
|
|
+ }
|
|
|
+ lastGotoRequest.value = { roadmap: mapName.value, coord: [[Number(currentWaypoint.x), Number(currentWaypoint.y)]] }
|
|
|
+ navigationStatus.value = 'planning'
|
|
|
+ isNavigating.value = true
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 发送路径规划请求
|
|
|
+ await requestPlanning(mapName.value, Number(currentWaypoint.x), Number(currentWaypoint.y))
|
|
|
+ // 发送前往目标点请求
|
|
|
+ await gotoTarget(mapName.value, Number(currentWaypoint.x), Number(currentWaypoint.y))
|
|
|
+ ElMessage.info(`正在前往第 ${currentIndex + 1} 个目标点: ${currentWaypoint.name || '目标点' + currentWaypoint.id} (${currentWaypoint.x}, ${currentWaypoint.y})`)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发送导航指令失败:', error)
|
|
|
+ ElMessage.error('发送导航指令失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleTaskWaypointArrival = () => {
|
|
|
+ if (!currentExecutingTask.value) return
|
|
|
+
|
|
|
+ currentTaskWaypointIndex.value++
|
|
|
+ const totalWaypoints = currentExecutingTask.value.points.length
|
|
|
+
|
|
|
+ console.log(`目标点到达,进度: ${currentTaskWaypointIndex.value}/${totalWaypoints}`)
|
|
|
+
|
|
|
+ if (currentTaskWaypointIndex.value >= totalWaypoints) {
|
|
|
+ ElMessage.success(`任务 "${currentExecutingTask.value.taskName}" 的所有目标点已完成!`)
|
|
|
+ console.log('任务所有目标点执行完成')
|
|
|
+ } else {
|
|
|
+ ElMessage.success(`第 ${currentTaskWaypointIndex.value} 个目标点已到达,继续前往下一个目标点...`)
|
|
|
+ setTimeout(() => {
|
|
|
+ executeNextWaypoint()
|
|
|
+ }, 2000)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onWpCreateTask = () => {
|
|
|
+ if (selectedWaypointIds.value.length === 0) {
|
|
|
+ ElMessage.warning('请先选择要添加到任务中的目标点')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const selectedWaypointsList = waypoints.value.filter(wp => selectedWaypointIds.value.includes(wp.id))
|
|
|
+ console.log(`准备使用 ${selectedWaypointsList.length} 个目标点创建任务`, selectedWaypointsList)
|
|
|
+
|
|
|
+ generateTaskParam.selectedWaypoints = selectedWaypointsList
|
|
|
+ taskGenerateDiaShow.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const onWpSelectionChange = (selection) => {
|
|
|
+ selectedWaypointIds.value = selection.map(wp => wp.id)
|
|
|
+ waypointSingle.value = selection.length !== 1
|
|
|
+ waypointMultiple.value = selection.length === 0
|
|
|
+ console.log('目标点选择变更:', selectedWaypointIds.value)
|
|
|
+}
|
|
|
+
|
|
|
+const onMapSelectModeChange = (isActive) => {
|
|
|
+ selectPointMode.value = isActive
|
|
|
+ initPoseMode.value = false
|
|
|
+
|
|
|
+ if (isActive) {
|
|
|
+ ElMessage.success('已开启地图选点模式,点击地图添加目标点')
|
|
|
+ } else {
|
|
|
+ ElMessage.info('已关闭地图选点模式')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const clearTaskViewWaypoints = () => {
|
|
|
+ if (taskViewAutoCloseTimer.value) {
|
|
|
+ clearTimeout(taskViewAutoCloseTimer.value)
|
|
|
+ taskViewAutoCloseTimer.value = null
|
|
|
+ }
|
|
|
+
|
|
|
+ if (olmap.value && currentViewTaskWaypointIds.value.length > 0) {
|
|
|
+ currentViewTaskWaypointIds.value.forEach(waypointId => {
|
|
|
+ if (olmap.value.removeIconHtmlById) {
|
|
|
+ olmap.value.removeIconHtmlById(waypointId)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ currentViewTaskWaypointIds.value = []
|
|
|
+ console.log('已清除任务查看标记点')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onTaskView = async (task) => {
|
|
|
+ console.log('查看任务详情:', task)
|
|
|
+
|
|
|
+ try {
|
|
|
+ clearTaskViewWaypoints()
|
|
|
+
|
|
|
+ const response = await getTask(mapName.value, task.taskName)
|
|
|
+ console.log('任务详情响应:', response)
|
|
|
+
|
|
|
+ if (response && response.coord && response.coord.length > 0) {
|
|
|
+ const waypointList = response.coord.map((coord, index) => ({
|
|
|
+ id: index + 1,
|
|
|
+ name: `点位${index + 1}`,
|
|
|
+ x: coord[0],
|
|
|
+ y: coord[1]
|
|
|
+ }))
|
|
|
+
|
|
|
+ waypointList.forEach((waypoint, index) => {
|
|
|
+ if (olmap.value && olmap.value.addHtmlIcon) {
|
|
|
+ const displayId = index + 1
|
|
|
+ olmap.value.addHtmlIcon(displayId, waypoint.x, waypoint.y, '', 'task-view')
|
|
|
+ currentViewTaskWaypointIds.value.push(`calibration-${displayId}`)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ taskViewAutoCloseTimer.value = setTimeout(() => {
|
|
|
+ clearTaskViewWaypoints()
|
|
|
+ }, 10000)
|
|
|
+
|
|
|
+ console.log(`显示任务点位:`, waypointList)
|
|
|
+ } else {
|
|
|
+ ElMessage.warning('该任务没有包含点位数据')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载任务详情失败:', error)
|
|
|
+ ElMessage.error('加载任务详情失败: ' + (error.message || '未知错误'))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onTaskEdit = async (task) => {
|
|
|
+ console.log('编辑任务:', task)
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await getTask(mapName.value, task.taskName)
|
|
|
+ console.log('任务详情响应:', response)
|
|
|
+
|
|
|
+ if (response) {
|
|
|
+ editTaskParam.originalTaskName = task.taskName
|
|
|
+ editTaskParam.taskId = task.taskId
|
|
|
+ editTaskParam.taskName = task.taskName
|
|
|
+ editTaskParam.count = response.repeate || 1
|
|
|
+
|
|
|
+ if (response.cron) {
|
|
|
+ const cronParts = response.cron.split(' ')
|
|
|
+ if (cronParts.length >= 7) {
|
|
|
+ const minutes = String(cronParts[1]).padStart(2, '0')
|
|
|
+ const hours = String(cronParts[2]).padStart(2, '0')
|
|
|
+ const weekdays = cronParts[5]
|
|
|
+
|
|
|
+ editTaskParam.time = `${hours}:${minutes}:00`
|
|
|
+
|
|
|
+ if (weekdays !== '*' && weekdays !== '?') {
|
|
|
+ editTaskParam.date = weekdays.split(',').filter(d => d)
|
|
|
+ } else {
|
|
|
+ editTaskParam.date = []
|
|
|
+ }
|
|
|
+ } else if (cronParts.length >= 5) {
|
|
|
+ const minutes = String(cronParts[0]).padStart(2, '0')
|
|
|
+ const hours = String(cronParts[1]).padStart(2, '0')
|
|
|
+ const weekdays = cronParts[4]
|
|
|
+
|
|
|
+ editTaskParam.time = `${hours}:${minutes}:00`
|
|
|
+
|
|
|
+ if (weekdays !== '*' && weekdays !== '?') {
|
|
|
+ editTaskParam.date = weekdays.split(',').filter(d => d)
|
|
|
+ } else {
|
|
|
+ editTaskParam.date = []
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ editTaskParam.time = ''
|
|
|
+ editTaskParam.date = []
|
|
|
+ }
|
|
|
+
|
|
|
+ editTaskParam.selectedWaypoints = (response.coord || []).map((coord, index) => ({
|
|
|
+ id: index + 1,
|
|
|
+ name: `目标点${index + 1}`,
|
|
|
+ x: coord[0].toString(),
|
|
|
+ y: coord[1].toString(),
|
|
|
+ type: response.plan && response.plan[index] === 'route' ? 1 : 0,
|
|
|
+ action: response.action && response.action[index] ?
|
|
|
+ convertApiActionToEditFormat(response.action[index]) :
|
|
|
+ [{ value: 0, other: 0 }]
|
|
|
+ }))
|
|
|
+
|
|
|
+ taskEditDiaShow.value = true
|
|
|
+ } else {
|
|
|
+ ElMessage.error('获取任务详情失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载任务详情失败:', error)
|
|
|
+ ElMessage.error('加载任务详情失败: ' + (error.message || '未知错误'))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const convertApiActionToEditFormat = (actions) => {
|
|
|
+ if (!actions || actions.length === 0) {
|
|
|
+ return [{ value: 0, other: 0 }]
|
|
|
+ }
|
|
|
+
|
|
|
+ return actions.map(action => {
|
|
|
+ const actionTypeMap = {
|
|
|
+ 'wait': 0,
|
|
|
+ 'start_record': 1,
|
|
|
+ 'stop_record': 2,
|
|
|
+ 'add_trajectory': 3,
|
|
|
+ 'hook_mount': 4,
|
|
|
+ 'hook_unmount': 5
|
|
|
+ }
|
|
|
+
|
|
|
+ const value = actionTypeMap[action.type] || 0
|
|
|
+ const result = { value }
|
|
|
+
|
|
|
+ if (action.type === 'wait' && action.duration !== undefined) {
|
|
|
+ result.other = action.duration
|
|
|
+ }
|
|
|
+
|
|
|
+ return result
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const closeTaskEdit = () => {
|
|
|
+ taskEditDiaShow.value = false
|
|
|
+ resetEditTaskParam()
|
|
|
+}
|
|
|
+
|
|
|
+const resetEditTaskParam = () => {
|
|
|
+ editTaskParam.originalTaskName = ''
|
|
|
+ editTaskParam.taskId = ''
|
|
|
+ editTaskParam.taskName = ''
|
|
|
+ editTaskParam.count = 1
|
|
|
+ editTaskParam.time = ''
|
|
|
+ editTaskParam.date = []
|
|
|
+ editTaskParam.selectedWaypoints = []
|
|
|
+}
|
|
|
+
|
|
|
+const submitTaskEdit = async () => {
|
|
|
+ const orderedPoints = editTaskParam.selectedWaypoints || []
|
|
|
+
|
|
|
+ if (!editTaskParam.taskName) {
|
|
|
+ ElMessage.warning('请输入任务名称')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (orderedPoints.length === 0) {
|
|
|
+ ElMessage.warning('任务必须包含至少一个目标点')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const coordinates = orderedPoints.map(point => [
|
|
|
+ parseFloat(point.x),
|
|
|
+ parseFloat(point.y),
|
|
|
+ 0
|
|
|
+ ])
|
|
|
+
|
|
|
+ const plan = orderedPoints.map(point => {
|
|
|
+ return point.type === 1 ? 'route' : 'free'
|
|
|
+ })
|
|
|
+
|
|
|
+ let cron = ''
|
|
|
+ if (editTaskParam.time && editTaskParam.date.length > 0) {
|
|
|
+ let hours, minutes
|
|
|
+ if (typeof editTaskParam.time === 'string') {
|
|
|
+ const timeParts = editTaskParam.time.split(':')
|
|
|
+ hours = timeParts[0]
|
|
|
+ minutes = timeParts[1]
|
|
|
+ } else {
|
|
|
+ const timeObj = new Date(editTaskParam.time)
|
|
|
+ hours = String(timeObj.getHours()).padStart(2, '0')
|
|
|
+ minutes = String(timeObj.getMinutes()).padStart(2, '0')
|
|
|
+ }
|
|
|
+ const weekdays = editTaskParam.date.sort().join(',')
|
|
|
+ cron = `00 ${minutes} ${hours} ? * ${weekdays} *`
|
|
|
+ }
|
|
|
+
|
|
|
+ const taskData = {
|
|
|
+ taskId: editTaskParam.taskId,
|
|
|
+ map: mapName.value,
|
|
|
+ taskName: editTaskParam.taskName,
|
|
|
+ path: editTaskParam.taskName,
|
|
|
+ coord_type: 'local',
|
|
|
+ cron: cron,
|
|
|
+ executeTime: editTaskParam.time || null,
|
|
|
+ executeWeekdays: editTaskParam.date.join(',') || null,
|
|
|
+ coord: coordinates,
|
|
|
+ plan: plan,
|
|
|
+ action: [[]],
|
|
|
+ repeate: editTaskParam.count || 1
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await updateTask(taskData)
|
|
|
+ console.log("更新任务响应:", response)
|
|
|
+
|
|
|
+ if (response.status === true) {
|
|
|
+ ElMessage.success('任务更新成功')
|
|
|
+ await loadTaskList()
|
|
|
+ closeTaskEdit()
|
|
|
+ } else {
|
|
|
+ ElMessage.error('更新任务失败: ' + (response.msg || '未知错误'))
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("更新任务失败:", error)
|
|
|
+ ElMessage.error('更新任务失败: ' + (error.message || '未知错误'))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onTaskStart = async (task) => {
|
|
|
+ console.log('开始任务:', task)
|
|
|
+
|
|
|
+ if (isTaskExecuting.value) {
|
|
|
+ ElMessage.warning('已有任务正在执行,请先完成或取消当前任务')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await getTask(mapName.value, task.taskName)
|
|
|
+ console.log('任务详情响应:', response)
|
|
|
+
|
|
|
+ if (response && response.coord && response.coord.length > 0) {
|
|
|
+ const waypointList = response.coord.map((coord, index) => ({
|
|
|
+ id: index + 1,
|
|
|
+ name: `目标点${index + 1}`,
|
|
|
+ x: coord[0],
|
|
|
+ y: coord[1],
|
|
|
+ type: response.plan && response.plan[index] === 'route' ? 1 : 0,
|
|
|
+ action: response.action && response.action[index] ? response.action[index] : []
|
|
|
+ }))
|
|
|
+
|
|
|
+ task.points = waypointList
|
|
|
+ task.totalWaypoints = waypointList.length
|
|
|
+
|
|
|
+ const timestamp = new Date().getTime()
|
|
|
+
|
|
|
+ currentExecutingTask.value = task
|
|
|
+ currentTaskWaypointIndex.value = 0
|
|
|
+ lastTaskExecRequest.value = { roadmap: mapName.value, task: task.taskName }
|
|
|
+ isTaskExecuting.value = true
|
|
|
+ taskExecutionStatus.value = 'executing'
|
|
|
+
|
|
|
+ task.status = 'running'
|
|
|
+ task.executionStatus = 'executing'
|
|
|
+
|
|
|
+ // 发送启动任务请求
|
|
|
+ await startTask(mapName.value, task.taskName)
|
|
|
+
|
|
|
+ ElMessage.success(`正在启动任务 "${task.taskName}",包含 ${waypointList.length} 个目标点`)
|
|
|
+ } else {
|
|
|
+ ElMessage.error('任务没有包含目标点,无法执行')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取任务详情失败:', error)
|
|
|
+ ElMessage.error('获取任务详情失败: ' + (error.message || '未知错误'))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onTaskPause = async (task) => {
|
|
|
+ console.log('暂停任务:', task)
|
|
|
+
|
|
|
+ if (task.status !== 'running') {
|
|
|
+ ElMessage.warning('该任务当前未在执行中')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await pauseTask()
|
|
|
+ task.status = 'paused'
|
|
|
+ ElMessage.info('任务已暂停')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('暂停任务失败:', error)
|
|
|
+ ElMessage.error('暂停任务失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onTaskStop = async (task) => {
|
|
|
+ console.log('停止任务:', task)
|
|
|
+
|
|
|
+ if (!isTaskExecuting.value || currentExecutingTask.value?.taskId !== task.taskId) {
|
|
|
+ ElMessage.warning('该任务当前未在执行中')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(`确定要取消任务 "${task.taskName}" 吗?`, '取消任务', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+
|
|
|
+ await cancelTask()
|
|
|
+
|
|
|
+ console.log("发送取消任务请求成功")
|
|
|
+ } catch {
|
|
|
+ // 用户取消
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onTaskRemove = async (task) => {
|
|
|
+ console.log('删除任务:', task)
|
|
|
+
|
|
|
+ if (isTaskExecuting.value && currentExecutingTask.value?.taskId === task.taskId) {
|
|
|
+ ElMessage.warning('任务正在执行中,无法删除。请先停止任务。')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(`确定要删除任务 "${task.taskName}" 吗?`, '删除任务', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await delTask({
|
|
|
+ map: mapName.value,
|
|
|
+ path: task.taskName,
|
|
|
+ taskId: task.taskId || null
|
|
|
+ })
|
|
|
+
|
|
|
+ if (response.status === true) {
|
|
|
+ ElMessage.success(`任务 "${task.taskName}" 已删除`)
|
|
|
+ await loadTaskList()
|
|
|
+ } else {
|
|
|
+ ElMessage.error('删除任务失败: ' + (response.msg || '未知错误'))
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("删除任务失败:", error)
|
|
|
+ ElMessage.error('删除任务失败: ' + (error.message || '未知错误'))
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ // 用户取消
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onTaskResume = async (task) => {
|
|
|
+ console.log('继续任务:', task)
|
|
|
+
|
|
|
+ if (task.status !== 'paused') {
|
|
|
+ ElMessage.warning('该任务当前不在暂停状态')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await resumeTask()
|
|
|
+ task.status = 'running'
|
|
|
+ ElMessage.success('任务已继续')
|
|
|
+ } catch (error) {
|
|
|
+ console.error('继续任务失败:', error)
|
|
|
+ ElMessage.error('继续任务失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const getTaskStatusTextByStatus = (status) => {
|
|
|
+ const statusMap = {
|
|
|
+ 0: '运行中',
|
|
|
+ 1: '空闲',
|
|
|
+ 'idle': '空闲',
|
|
|
+ 'running': '运行中',
|
|
|
+ 'paused': '暂停',
|
|
|
+ 'completed': '已完成',
|
|
|
+ 'error': '失败'
|
|
|
+ }
|
|
|
+ return statusMap[status] || '未知'
|
|
|
+}
|
|
|
+
|
|
|
+const getTaskStatusClass = (status) => {
|
|
|
+ const statusClassMap = {
|
|
|
+ 0: 'status-running',
|
|
|
+ 1: 'status-idle',
|
|
|
+ 'idle': 'status-idle',
|
|
|
+ 'running': 'status-running',
|
|
|
+ 'paused': 'status-paused',
|
|
|
+ 'completed': 'status-completed',
|
|
|
+ 'error': 'status-error'
|
|
|
+ }
|
|
|
+ return statusClassMap[status] || 'status-unknown'
|
|
|
+}
|
|
|
+
|
|
|
+const formatTime = (time) => {
|
|
|
+ if (!time) return '--'
|
|
|
+ if (typeof time === 'string') return time
|
|
|
+ if (time instanceof Date) {
|
|
|
+ return time.toLocaleTimeString('zh-CN', {
|
|
|
+ hour12: false,
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return '--'
|
|
|
+}
|
|
|
+
|
|
|
+const formatDate = (dateArray) => {
|
|
|
+ if (!dateArray || !Array.isArray(dateArray) || dateArray.length === 0) return '--'
|
|
|
+
|
|
|
+ const dayNames = {
|
|
|
+ '1': '周一',
|
|
|
+ '2': '周二',
|
|
|
+ '3': '周三',
|
|
|
+ '4': '周四',
|
|
|
+ '5': '周五',
|
|
|
+ '6': '周六',
|
|
|
+ '7': '周日'
|
|
|
+ }
|
|
|
+
|
|
|
+ return dateArray.map(day => dayNames[day] || day).join(', ')
|
|
|
+}
|
|
|
+
|
|
|
+const formatSeconds = (seconds) => {
|
|
|
+ if (!seconds || seconds < 0) return '00:00:00'
|
|
|
+
|
|
|
+ const hours = Math.floor(seconds / 3600)
|
|
|
+ const minutes = Math.floor((seconds % 3600) / 60)
|
|
|
+ const secs = Math.floor(seconds % 60)
|
|
|
+
|
|
|
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
|
|
+}
|
|
|
+
|
|
|
+const onSettingChange = (setting) => {
|
|
|
+ console.log('功能设置变更:', setting)
|
|
|
+ settingParams[setting.key] = setting.value
|
|
|
+
|
|
|
+ switch(setting.key) {
|
|
|
+ case 'pointCloud':
|
|
|
+ ElMessage.info(`点云显示已${setting.value ? '开启' : '关闭'}`)
|
|
|
+ break
|
|
|
+ case 'baseMap':
|
|
|
+ ElMessage.info(`底图显示已${setting.value ? '开启' : '关闭'}`)
|
|
|
+ break
|
|
|
+ case 'pointId':
|
|
|
+ ElMessage.info(`点ID显示已${setting.value ? '开启' : '关闭'}`)
|
|
|
+ break
|
|
|
+ case 'follow':
|
|
|
+ ElMessage.info(`位置跟随已${setting.value ? '开启' : '关闭'}`)
|
|
|
+ break
|
|
|
+ case 'network':
|
|
|
+ ElMessage.info(`网络邻居显示已${setting.value ? '开启' : '关闭'}`)
|
|
|
+ break
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const loadTaskList = async () => {
|
|
|
+ try {
|
|
|
+ const response = await listTask(mapName.value)
|
|
|
+ console.log("任务列表:",response);
|
|
|
+
|
|
|
+ if (response.status && response.paths && Array.isArray(response.paths)) {
|
|
|
+ const oldTasks = tasks.value || []
|
|
|
+
|
|
|
+ // 新的API返回格式: paths数组每个元素是包含完整信息的对象
|
|
|
+ const taskPromises = response.paths.map(async (taskItem) => {
|
|
|
+ const taskName = taskItem.taskName || taskItem.path
|
|
|
+ const existingTask = oldTasks.find(t => t.taskName === taskName)
|
|
|
+
|
|
|
+ let taskObj = {
|
|
|
+ taskId: taskItem.taskId || 0,
|
|
|
+ taskName: taskName,
|
|
|
+ status: taskItem.status || 'idle',
|
|
|
+ count: taskItem.repeatCount || taskItem.repeate || 1,
|
|
|
+ time: taskItem.executeTime || '',
|
|
|
+ date: taskItem.executeWeekdays ? taskItem.executeWeekdays.split(',') : [],
|
|
|
+ cron: taskItem.cron || '',
|
|
|
+ points: [],
|
|
|
+ executionStatus: 'idle',
|
|
|
+ currentWaypointIndex: 0,
|
|
|
+ totalWaypoints: 0,
|
|
|
+ scheduledTime: null,
|
|
|
+ scheduledTimeStr: '',
|
|
|
+ scheduledWeekDays: [],
|
|
|
+ shouldRunToday: false
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析cron表达式获取定时信息
|
|
|
+ if (taskItem.cron) {
|
|
|
+ const cronInfo = parseCronExpression(taskItem.cron)
|
|
|
+ taskObj.scheduledTimeStr = cronInfo.timeStr || taskObj.time
|
|
|
+ taskObj.scheduledWeekDays = cronInfo.weekDays
|
|
|
+
|
|
|
+ if (cronInfo.hours !== null && cronInfo.minutes !== null) {
|
|
|
+ const scheduledTime = new Date()
|
|
|
+ scheduledTime.setHours(cronInfo.hours, cronInfo.minutes, 0, 0)
|
|
|
+ taskObj.scheduledTime = scheduledTime
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentDay = new Date().getDay() || 7
|
|
|
+ taskObj.shouldRunToday = cronInfo.weekDays.includes(String(currentDay))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析目标点坐标
|
|
|
+ if (taskItem.coord && Array.isArray(taskItem.coord)) {
|
|
|
+ taskObj.points = taskItem.coord.map((coord, idx) => ({
|
|
|
+ id: idx + 1,
|
|
|
+ x: coord[0],
|
|
|
+ y: coord[1],
|
|
|
+ z: coord[2] || 0,
|
|
|
+ type: taskItem.plan && taskItem.plan[idx] === 'route' ? 1 : 0,
|
|
|
+ action: taskItem.action && taskItem.action[idx] || []
|
|
|
+ }))
|
|
|
+ taskObj.totalWaypoints = taskObj.points.length
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保留运行中任务的执行状态
|
|
|
+ if (currentExecutingTask.value && currentExecutingTask.value.taskName === taskName) {
|
|
|
+ taskObj.status = currentExecutingTask.value.status || 'running'
|
|
|
+ taskObj.executionStatus = currentExecutingTask.value.executionStatus || 'executing'
|
|
|
+ taskObj.currentWaypointIndex = currentTaskWaypointIndex.value || 0
|
|
|
+ if (currentExecutingTask.value.points) {
|
|
|
+ taskObj.points = currentExecutingTask.value.points
|
|
|
+ }
|
|
|
+ } else if (existingTask && existingTask.status !== 'idle') {
|
|
|
+ console.log(`保留任务状态: ${taskName} - ${existingTask.status}`)
|
|
|
+ taskObj.status = existingTask.status
|
|
|
+ taskObj.executionStatus = existingTask.executionStatus
|
|
|
+ taskObj.currentWaypointIndex = existingTask.currentWaypointIndex
|
|
|
+ }
|
|
|
+
|
|
|
+ return taskObj
|
|
|
+ })
|
|
|
+
|
|
|
+ tasks.value = await Promise.all(taskPromises)
|
|
|
+ console.log("任务列表加载成功,包含完整cron配置:", tasks.value.length, "个任务")
|
|
|
+ calculateTodayScheduledTasks()
|
|
|
+ } else {
|
|
|
+ console.error("加载任务列表失败,响应:", response)
|
|
|
+ tasks.value = []
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("加载任务列表异常:", error)
|
|
|
+ tasks.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const parseCronExpression = (cronStr) => {
|
|
|
+ const result = {
|
|
|
+ timeStr: '',
|
|
|
+ hours: null,
|
|
|
+ minutes: null,
|
|
|
+ weekDays: []
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!cronStr) return result
|
|
|
+
|
|
|
+ try {
|
|
|
+ const parts = cronStr.split(' ')
|
|
|
+
|
|
|
+ if (parts.length >= 6) {
|
|
|
+ const minutes = parts[1]
|
|
|
+ const hours = parts[2]
|
|
|
+ const weekdays = parts[5]
|
|
|
+
|
|
|
+ if (hours !== '*' && hours !== '?' && minutes !== '*' && minutes !== '?') {
|
|
|
+ result.hours = parseInt(hours)
|
|
|
+ result.minutes = parseInt(minutes)
|
|
|
+ result.timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`
|
|
|
+ }
|
|
|
+
|
|
|
+ if (weekdays !== '*' && weekdays !== '?') {
|
|
|
+ result.weekDays = weekdays.split(',').map(d => d.trim()).filter(d => d)
|
|
|
+ }
|
|
|
+ } else if (parts.length >= 5) {
|
|
|
+ const minutes = parts[0]
|
|
|
+ const hours = parts[1]
|
|
|
+ const weekdays = parts[4]
|
|
|
+
|
|
|
+ if (hours !== '*' && hours !== '?' && minutes !== '*' && minutes !== '?') {
|
|
|
+ result.hours = parseInt(hours)
|
|
|
+ result.minutes = parseInt(minutes)
|
|
|
+ result.timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:00`
|
|
|
+ }
|
|
|
+
|
|
|
+ if (weekdays !== '*' && weekdays !== '?') {
|
|
|
+ result.weekDays = weekdays.split(',').map(d => d.trim()).filter(d => d)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("解析cron表达式失败:", cronStr, error)
|
|
|
+ }
|
|
|
+
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+const startTaskListTimer = () => {
|
|
|
+ stopTaskListTimer()
|
|
|
+
|
|
|
+ taskListTimer.value = setInterval(() => {
|
|
|
+ loadTaskList()
|
|
|
+ }, 5000)
|
|
|
+
|
|
|
+ console.log('任务列表定时刷新已启动(每5秒)')
|
|
|
+}
|
|
|
+
|
|
|
+const stopTaskListTimer = () => {
|
|
|
+ if (taskListTimer.value) {
|
|
|
+ clearInterval(taskListTimer.value)
|
|
|
+ taskListTimer.value = null
|
|
|
+ console.log('任务列表定时刷新已停止')
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.point-edit-span {
|
|
|
+ display: block;
|
|
|
+ margin: 10px 0;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer {
|
|
|
+ height: 100%;
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 100%;
|
|
|
+ border-radius: 0 0 12px 0;
|
|
|
+ border-left: 1px solid #F0F0F0;
|
|
|
+ padding: 8px 15px;
|
|
|
+ border-right: 1px solid #ececec;
|
|
|
+ border-bottom: 1px solid #ececec;
|
|
|
+ overflow-y: auto;
|
|
|
+ background-color: #fff;
|
|
|
+ z-index: 1000;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-close {
|
|
|
+ position: absolute;
|
|
|
+ right: 3px;
|
|
|
+ top: 3px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer-title {
|
|
|
+ position: absolute;
|
|
|
+ top: -23px;
|
|
|
+ left: -8px;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #838383;
|
|
|
+}
|
|
|
+
|
|
|
+.drawer p {
|
|
|
+ font-size: 13px;
|
|
|
+ border-left: 5px #D1D1D1 solid;
|
|
|
+ padding-left: 5px;
|
|
|
+ margin: 8px 0;
|
|
|
+ border-radius: 3px 0 0 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.img-container {
|
|
|
+ text-align: center;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border-radius: 7px;
|
|
|
+ background: linear-gradient(135deg, #00bcd4, #009688);
|
|
|
+ cursor: pointer;
|
|
|
+ width: 70%;
|
|
|
+ aspect-ratio: 1;
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.img-container:hover {
|
|
|
+ transform: scale(1.02);
|
|
|
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
|
|
|
+ background-color: #00796b;
|
|
|
+}
|
|
|
+
|
|
|
+.img-container:active {
|
|
|
+ transform: scale(0.98);
|
|
|
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
|
|
+ background-color: #004d40;
|
|
|
+}
|
|
|
+
|
|
|
+.img-container img {
|
|
|
+ opacity: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.img-container span {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #ffffff;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.img-container.active {
|
|
|
+ background: linear-gradient(135deg, #007d8d, #004b43);
|
|
|
+}
|
|
|
+
|
|
|
+.explore-unit {
|
|
|
+ margin-left: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-dialog__body) {
|
|
|
+ padding: 20px 20px 0 20px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.download-map .el-dialog__body) {
|
|
|
+ padding: 10px 20px 0 20px !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-table--medium .el-table__cell) {
|
|
|
+ padding: 6px 0;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.action-menu .action-menu_input .el-input__inner) {
|
|
|
+ padding: 0 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.task-status-tag {
|
|
|
+ margin-left: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.drawer .el-collapse-item__header) {
|
|
|
+ height: 38px;
|
|
|
+ line-height: 38px;
|
|
|
+ color: #767676;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.drawer .el-collapse-item__content) {
|
|
|
+ text-align: left;
|
|
|
+ padding-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.drawer .el-collapse) {
|
|
|
+ border: 1px solid #EBEEF5;
|
|
|
+ padding: 0 8px;
|
|
|
+ border-radius: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.collapse-content-div {
|
|
|
+ margin-top: 0;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.collapse-content-div .el-button--mini) {
|
|
|
+ padding: 4px 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.hand-ment-mark {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 12px;
|
|
|
+ left: 12px;
|
|
|
+ z-index: 1000;
|
|
|
+}
|
|
|
+
|
|
|
+.obstacle-warning {
|
|
|
+ position: absolute;
|
|
|
+ top: 20px;
|
|
|
+ right: 20px;
|
|
|
+ z-index: 2000;
|
|
|
+ min-width: 320px;
|
|
|
+ max-width: 400px;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ animation: slideInRight 0.3s ease-out;
|
|
|
+
|
|
|
+ &.obstacle-info {
|
|
|
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.95) 0%, rgba(99, 102, 241, 0.95) 100%);
|
|
|
+ border: 1px solid rgba(59, 130, 246, 0.3);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.obstacle-warning {
|
|
|
+ background: linear-gradient(135deg, rgba(245, 158, 11, 0.95) 0%, rgba(251, 191, 36, 0.95) 100%);
|
|
|
+ border: 1px solid rgba(245, 158, 11, 0.3);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.obstacle-danger {
|
|
|
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.95) 0%, rgba(248, 113, 113, 0.95) 100%);
|
|
|
+ border: 1px solid rgba(239, 68, 68, 0.3);
|
|
|
+ animation: pulse 2s infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ .warning-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ padding: 16px 20px;
|
|
|
+ color: white;
|
|
|
+
|
|
|
+ .warning-icon {
|
|
|
+ margin-right: 12px;
|
|
|
+ margin-top: 2px;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ i {
|
|
|
+ font-size: 20px;
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .warning-text {
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ .warning-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .warning-message {
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.4;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ color: rgba(255, 255, 255, 0.95);
|
|
|
+ }
|
|
|
+
|
|
|
+ .obstacle-distances {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+
|
|
|
+ .distance-item {
|
|
|
+ font-size: 11px;
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 12px;
|
|
|
+ color: white;
|
|
|
+ font-weight: 500;
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.3);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .warning-close {
|
|
|
+ margin-left: 8px;
|
|
|
+ margin-top: 2px;
|
|
|
+ cursor: pointer;
|
|
|
+ padding: 4px;
|
|
|
+ border-radius: 50%;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ i {
|
|
|
+ font-size: 16px;
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes slideInRight {
|
|
|
+ from {
|
|
|
+ transform: translateX(100%);
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ transform: translateX(0);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes pulse {
|
|
|
+ 0%, 100% {
|
|
|
+ transform: scale(1);
|
|
|
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ transform: scale(1.02);
|
|
|
+ box-shadow: 0 12px 32px rgba(239, 68, 68, 0.3);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.obstacle-stop-warning {
|
|
|
+ position: absolute;
|
|
|
+ top: 80px;
|
|
|
+ right: 20px;
|
|
|
+ z-index: 2000;
|
|
|
+ min-width: 380px;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: white;
|
|
|
+ box-shadow: 0 8px 32px rgba(239, 68, 68, 0.2);
|
|
|
+ border: 2px solid #FF5555;
|
|
|
+ animation: slideInRight 0.3s ease-out, breathe 2s infinite ease-in-out;
|
|
|
+
|
|
|
+ .stop-warning-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 16px;
|
|
|
+ gap: 16px;
|
|
|
+
|
|
|
+ .countdown-circle {
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 80px;
|
|
|
+ height: 80px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: linear-gradient(135deg, #FF5555 0%, #FF8080 100%);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: white;
|
|
|
+ box-shadow: 0 4px 12px rgba(255, 85, 85, 0.3);
|
|
|
+
|
|
|
+ .countdown-number {
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: bold;
|
|
|
+ line-height: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .countdown-label {
|
|
|
+ font-size: 12px;
|
|
|
+ margin-top: 4px;
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .stop-warning-text {
|
|
|
+ flex: 1;
|
|
|
+
|
|
|
+ .stop-warning-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #FF5555;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stop-warning-message {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #666;
|
|
|
+ line-height: 1.4;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stop-warning-state {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .stop-warning-actions {
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ .el-button {
|
|
|
+ padding: 8px 16px;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes breathe {
|
|
|
+ 0%, 100% {
|
|
|
+ box-shadow: 0 8px 32px rgba(239, 68, 68, 0.2);
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ box-shadow: 0 12px 40px rgba(239, 68, 68, 0.35);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.notification__title {
|
|
|
+ font-size: 1.2rem;
|
|
|
+}
|
|
|
+
|
|
|
+.navigation-container {
|
|
|
+ width: 100%;
|
|
|
+ min-height: calc(100vh - 84px);
|
|
|
+ overflow: hidden;
|
|
|
+ position: relative;
|
|
|
+ background: var(--color-bg-secondary);
|
|
|
+
|
|
|
+ .map-stage {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: calc(100vh - 84px);
|
|
|
+ min-height: 600px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: var(--color-bg-secondary);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.nav-toolbar {
|
|
|
+ position: absolute;
|
|
|
+ left: 16px;
|
|
|
+ top: 96px;
|
|
|
+ z-index: 50;
|
|
|
+}
|
|
|
+
|
|
|
+.main-menu {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.waypoint-edit-dialog) {
|
|
|
+ .el-dialog {
|
|
|
+ border-radius: 12px !important;
|
|
|
+ overflow: hidden !important;
|
|
|
+ margin-top: 0 !important;
|
|
|
+ margin-bottom: 0 !important;
|
|
|
+ position: fixed !important;
|
|
|
+ top: 50% !important;
|
|
|
+ left: 50% !important;
|
|
|
+ transform: translate(-50%, -50%) !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-dialog__header {
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ color: white;
|
|
|
+ padding: 20px 24px 16px;
|
|
|
+ margin: 0;
|
|
|
+ border-radius: 12px 12px 0 0 !important;
|
|
|
+
|
|
|
+ .el-dialog__title {
|
|
|
+ color: white;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-dialog__close {
|
|
|
+ color: white;
|
|
|
+ font-size: 18px;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #f0f0f0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-dialog__body {
|
|
|
+ padding: 24px;
|
|
|
+ background: #f8fafc;
|
|
|
+ margin: 0 !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-dialog__footer {
|
|
|
+ padding: 16px 24px 24px;
|
|
|
+ background: #f8fafc;
|
|
|
+ border-top: 1px solid #e2e8f0;
|
|
|
+ border-radius: 0 0 12px 12px !important;
|
|
|
+ margin: 0 !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.dialog-content {
|
|
|
+ .form-section {
|
|
|
+ background: white;
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 24px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
+ border: 1px solid #f1f5f9;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .section-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin: 0 0 20px 0;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #2d3748;
|
|
|
+ border-bottom: 2px solid #e2e8f0;
|
|
|
+ padding-bottom: 10px;
|
|
|
+
|
|
|
+ i {
|
|
|
+ margin-right: 10px;
|
|
|
+ color: #667eea;
|
|
|
+ font-size: 18px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .add-action-btn {
|
|
|
+ margin-left: 12px !important;
|
|
|
+ display: inline-flex !important;
|
|
|
+ align-items: center !important;
|
|
|
+ padding: 4px 8px !important;
|
|
|
+
|
|
|
+ i {
|
|
|
+ margin-right: 4px !important;
|
|
|
+ font-size: 12px !important;
|
|
|
+ color: #667eea !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ span {
|
|
|
+ font-size: 12px !important;
|
|
|
+ color: #667eea !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .waypoint-form {
|
|
|
+ .el-form-item {
|
|
|
+ margin-bottom: 18px;
|
|
|
+ display: flex !important;
|
|
|
+ align-items: center !important;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-form-item__content {
|
|
|
+ flex: 1 !important;
|
|
|
+ margin-left: 0 !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .coordinate-input {
|
|
|
+ margin-bottom: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-form-item__label {
|
|
|
+ font-weight: 600 !important;
|
|
|
+ color: #2d3748 !important;
|
|
|
+ padding-right: 12px !important;
|
|
|
+ min-width: 100px !important;
|
|
|
+ font-size: 14px !important;
|
|
|
+ line-height: 44px !important;
|
|
|
+ height: 44px !important;
|
|
|
+ display: flex !important;
|
|
|
+ align-items: center !important;
|
|
|
+ margin-bottom: 0 !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .coordinate-input {
|
|
|
+ .el-input__inner {
|
|
|
+ border-radius: 8px !important;
|
|
|
+ border: 1px solid #e2e8f0 !important;
|
|
|
+ height: 44px !important;
|
|
|
+ font-size: 15px !important;
|
|
|
+ padding: 0 16px !important;
|
|
|
+ background: #ffffff !important;
|
|
|
+ transition: all 0.2s ease !important;
|
|
|
+
|
|
|
+ &:focus {
|
|
|
+ border-color: #667eea !important;
|
|
|
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
|
|
|
+ background: #fafbff !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: #cbd5e0 !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|