|
@@ -0,0 +1,2305 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="page">
|
|
|
|
|
+ <!-- 步骤头部 -->
|
|
|
|
|
+ <view class="header">
|
|
|
|
|
+ <view class="title-row">
|
|
|
|
|
+ <text class="title">新增作业</text>
|
|
|
|
|
+ <text class="device-id">设备:{{ jobState.machineCode || '未知设备' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="step-row">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="step in steps"
|
|
|
|
|
+ :key="step.id"
|
|
|
|
|
+ class="step-item"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="step-index"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ active: currentStep === step.id,
|
|
|
|
|
+ done: currentStep > step.id
|
|
|
|
|
+ }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text v-if="currentStep > step.id">✓</text>
|
|
|
|
|
+ <text v-else>{{ step.id }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="step-texts">
|
|
|
|
|
+ <text class="step-title">{{ step.title }}</text>
|
|
|
|
|
+ <text class="step-sub">{{ step.sub }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 主体内容 -->
|
|
|
|
|
+ <view class="content">
|
|
|
|
|
+ <!-- Step 1: 选择作业区域类型 & 路线类型 -->
|
|
|
|
|
+ <view v-if="currentStep === 1" class="step-block">
|
|
|
|
|
+ <view class="card select-card">
|
|
|
|
|
+ <view class="select-title">选择作业区域类型</view>
|
|
|
|
|
+ <view class="select-sub">
|
|
|
|
|
+ 不同区域形状将影响后续路线生成方式,请根据实际地块选择。
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="select-tabs">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="item in areaTypes"
|
|
|
|
|
+ :key="item.value"
|
|
|
|
|
+ class="select-tab"
|
|
|
|
|
+ :class="{ active: selectedAreaType === item.value }"
|
|
|
|
|
+ @click="selectAreaType(item.value)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="select-tab-label">{{ item.label }}</text>
|
|
|
|
|
+ <text class="select-tab-sub" v-if="item.desc">
|
|
|
|
|
+ {{ item.desc }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="card select-card">
|
|
|
|
|
+ <view class="select-title">选择路线类型</view>
|
|
|
|
|
+ <view class="select-sub">
|
|
|
|
|
+ 依据区域形状推荐的路线类型,后端将按所选类型生成具体作业路线。
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="select-tabs">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="route in availableRouteTypes"
|
|
|
|
|
+ :key="route.value"
|
|
|
|
|
+ class="select-tab small"
|
|
|
|
|
+ :class="{ active: selectedRouteType === route.value }"
|
|
|
|
|
+ @click="selectRouteType(route.value)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="select-tab-label">{{ route.label }}</text>
|
|
|
|
|
+ <text class="select-tab-sub" v-if="route.desc">
|
|
|
|
|
+ {{ route.desc }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="card tips-card">
|
|
|
|
|
+ <view class="tips-title">说明</view>
|
|
|
|
|
+ <view class="tips-content">
|
|
|
|
|
+ <text>
|
|
|
|
|
+ - 本步骤仅选择区域类型与路线类型,下一步将进入地图打点新增作业区域。
|
|
|
|
|
+ </text>
|
|
|
|
|
+ <text>
|
|
|
|
|
+ - 当前选择会随作业一起提交至后端,用于指导路线生成策略。
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Step 2: 地图打点 -->
|
|
|
|
|
+ <view v-else-if="currentStep === 2" class="step-block">
|
|
|
|
|
+ <!-- 地图占位 -->
|
|
|
|
|
+ <view class="map-card">
|
|
|
|
|
+ <view class="map-header">
|
|
|
|
|
+ <text class="map-title">地图预览</text>
|
|
|
|
|
+ <view class="map-header-actions">
|
|
|
|
|
+ <button class="btn-location" @click="manualLocation" :loading="locating">
|
|
|
|
|
+ <text class="btn-location-text">{{ locating ? '定位中...' : '📍 定位' }}</text>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="map-sub">通过遥控器移动设备,在地图上逐点记录</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="map-body">
|
|
|
|
|
+ <!-- 始终渲染地图容器以避免渲染时序问题;未加载脚本时容器为空白 -->
|
|
|
|
|
+ <view id="mapContainer"></view>
|
|
|
|
|
+ <text v-if="!amapLoaded" class="map-placeholder">
|
|
|
|
|
+ 地图占位(H5 平台会加载高德地图 SDK){{
|
|
|
|
|
+ '\n'
|
|
|
|
|
+ }}当前模式:{{ modeLabel }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="map-footer">
|
|
|
|
|
+ <text class="map-hint">
|
|
|
|
|
+ 提示:请使用遥控器移动设备到拐点位置,再点击“新增点”记录坐标。
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 模式切换 -->
|
|
|
|
|
+ <view class="mode-card">
|
|
|
|
|
+ <view class="mode-title-row">
|
|
|
|
|
+ <text class="mode-title">点位类型</text>
|
|
|
|
|
+ <text class="mode-sub">在不同模式下分别记录作业区域、障碍物和返航点</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="mode-tabs">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="item in modes"
|
|
|
|
|
+ :key="item.value"
|
|
|
|
|
+ class="mode-tab"
|
|
|
|
|
+ :class="{ active: mode === item.value }"
|
|
|
|
|
+ @click="switchMode(item.value)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="mode-tab-label">{{ item.label }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 控制面板 -->
|
|
|
|
|
+ <view class="panel-card">
|
|
|
|
|
+ <view class="panel-row">
|
|
|
|
|
+ <button class="btn primary" @click="addPoint">新增点</button>
|
|
|
|
|
+ <button class="btn ghost" @click="undoPoint" :disabled="!canUndo">
|
|
|
|
|
+ 撤销
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="btn ghost"
|
|
|
|
|
+ v-if="mode === 'obstacle'"
|
|
|
|
|
+ @click="finishCurrentObstacle"
|
|
|
|
|
+ :disabled="!currentObstacle.length"
|
|
|
|
|
+ >
|
|
|
|
|
+ 完成当前障碍物
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="panel-desc">
|
|
|
|
|
+ <text v-if="mode === 'area'">
|
|
|
|
|
+ 作业区域至少需要 3 个点,建议为近似矩形。
|
|
|
|
|
+ </text>
|
|
|
|
|
+ <text v-else-if="mode === 'obstacle'">
|
|
|
|
|
+ 每个障碍物可记录多个点,点击“完成当前障碍物”结束本组记录。
|
|
|
|
|
+ </text>
|
|
|
|
|
+ <text v-else>
|
|
|
|
|
+ 仅允许 1 个返航点,重复新增会覆盖已有点。
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 已记录点列表 -->
|
|
|
|
|
+ <view class="list-card">
|
|
|
|
|
+ <view class="list-title-row">
|
|
|
|
|
+ <text class="list-title">已记录点位</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <scroll-view scroll-y class="list-scroll">
|
|
|
|
|
+ <!-- 作业区域点 -->
|
|
|
|
|
+ <view class="list-group">
|
|
|
|
|
+ <view class="list-group-header">
|
|
|
|
|
+ <text class="list-group-title">作业区域点({{ jobState.areaPoints.length }})</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-if="jobState.areaPoints.length"
|
|
|
|
|
+ class="point-list"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(p, index) in jobState.areaPoints"
|
|
|
|
|
+ :key="'area-' + index"
|
|
|
|
|
+ class="point-item"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="point-label">P{{ index + 1 }}</text>
|
|
|
|
|
+ <text class="point-coord">
|
|
|
|
|
+ {{ formatPoint(p) }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ <text class="point-time">{{ formatPointTime(p.timestamp) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-else class="empty-row">
|
|
|
|
|
+ <text>暂未记录作业区域点</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 障碍物点 -->
|
|
|
|
|
+ <view class="list-group">
|
|
|
|
|
+ <view class="list-group-header">
|
|
|
|
|
+ <text class="list-group-title">
|
|
|
|
|
+ 障碍物({{ jobState.obstacles.length + (currentObstacle.length ? 1 : 0) }} 组)
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-if="jobState.obstacles.length || currentObstacle.length"
|
|
|
|
|
+ class="obstacle-list"
|
|
|
|
|
+ >
|
|
|
|
|
+ <!-- 已完成的障碍物组 -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(obs, oIdx) in jobState.obstacles"
|
|
|
|
|
+ :key="'obs-' + oIdx"
|
|
|
|
|
+ class="obstacle-item"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="obstacle-title">障碍物 {{ oIdx + 1 }}({{ obs.length }} 点)</text>
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(p, pIdx) in obs"
|
|
|
|
|
+ :key="'obs-' + oIdx + '-' + pIdx"
|
|
|
|
|
+ class="point-item small"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="point-label">O{{ oIdx + 1 }}-{{ pIdx + 1 }}</text>
|
|
|
|
|
+ <text class="point-coord">
|
|
|
|
|
+ {{ formatPoint(p) }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 正在录入中的障碍物(未点击“完成当前障碍物”之前也要实时回显) -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-if="currentObstacle.length"
|
|
|
|
|
+ class="obstacle-item"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="obstacle-title">障碍物 {{ jobState.obstacles.length + 1 }}(录入中,{{ currentObstacle.length }} 点)</text>
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(p, pIdx) in currentObstacle"
|
|
|
|
|
+ :key="'obs-current-' + pIdx"
|
|
|
|
|
+ class="point-item small"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="point-label">O{{ jobState.obstacles.length + 1 }}-{{ pIdx + 1 }}</text>
|
|
|
|
|
+ <text class="point-coord">
|
|
|
|
|
+ {{ formatPoint(p) }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-else class="empty-row">
|
|
|
|
|
+ <text>暂未记录障碍物点</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 返航点 -->
|
|
|
|
|
+ <view class="list-group">
|
|
|
|
|
+ <view class="list-group-header">
|
|
|
|
|
+ <text class="list-group-title">返航点</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="jobState.returnPoint" class="point-item">
|
|
|
|
|
+ <text class="point-label">R</text>
|
|
|
|
|
+ <text class="point-coord">
|
|
|
|
|
+ {{ formatPoint(jobState.returnPoint) }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ <text class="point-time">
|
|
|
|
|
+ {{ formatPointTime(jobState.returnPoint.timestamp) }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-else class="empty-row">
|
|
|
|
|
+ <text>暂未设置返航点</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </scroll-view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Step 3: 起点选择 -->
|
|
|
|
|
+ <view v-else-if="currentStep === 3" class="step-block">
|
|
|
|
|
+ <view class="map-card small">
|
|
|
|
|
+ <view class="map-header">
|
|
|
|
|
+ <text class="map-title">选择起点</text>
|
|
|
|
|
+ <text class="map-sub">
|
|
|
|
|
+ 从已记录的作业区域点中选择作业起点,可通过左右切换预览。
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="map-body">
|
|
|
|
|
+ <text class="map-placeholder">
|
|
|
|
|
+ 这里显示作业区域示意图(占位){{
|
|
|
|
|
+ '\n'
|
|
|
|
|
+ }}当前起点:P{{ currentStartDisplay }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="panel-card">
|
|
|
|
|
+ <view class="panel-row center">
|
|
|
|
|
+ <button class="btn ghost" @click="prevStart" :disabled="!canChangeStart">
|
|
|
|
|
+ 上一个
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <view class="start-index">
|
|
|
|
|
+ <text class="start-index-text">
|
|
|
|
|
+ 起点:P{{ currentStartDisplay }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <button class="btn ghost" @click="nextStart" :disabled="!canChangeStart">
|
|
|
|
|
+ 下一个
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="panel-desc">
|
|
|
|
|
+ <text>
|
|
|
|
|
+ 提示:起点将决定设备的初始行进方向与作业顺序,后端会基于该起点生成具体路线。
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="list-card">
|
|
|
|
|
+ <view class="list-title-row">
|
|
|
|
|
+ <text class="list-title">作业区域点列表</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <scroll-view scroll-y class="list-scroll">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(p, index) in jobState.areaPoints"
|
|
|
|
|
+ :key="'start-' + index"
|
|
|
|
|
+ class="point-item"
|
|
|
|
|
+ :class="{ active: index === jobState.startPointIndex }"
|
|
|
|
|
+ @click="setStartIndex(index)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="point-label">P{{ index + 1 }}</text>
|
|
|
|
|
+ <text class="point-coord">{{ formatPoint(p) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </scroll-view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Step 4: 作业信息确认 -->
|
|
|
|
|
+ <view v-else-if="currentStep === 4" class="step-block">
|
|
|
|
|
+ <view class="card confirm-card">
|
|
|
|
|
+ <view class="confirm-title">
|
|
|
|
|
+ <text>作业基本信息</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="form-item required">
|
|
|
|
|
+ <text class="label">作业名称</text>
|
|
|
|
|
+ <input
|
|
|
|
|
+ class="input"
|
|
|
|
|
+ v-model="jobState.jobName"
|
|
|
|
|
+ placeholder="请输入作业名称,如“Test device - 北区作业”"
|
|
|
|
|
+ />
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="form-item required">
|
|
|
|
|
+ <text class="label">地块ID</text>
|
|
|
|
|
+ <input
|
|
|
|
|
+ class="input"
|
|
|
|
|
+ v-model="jobState.fieldId"
|
|
|
|
|
+ placeholder="请输入地块ID"
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ />
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="form-item required">
|
|
|
|
|
+ <text class="label">路径宽度(厘米)</text>
|
|
|
|
|
+ <input
|
|
|
|
|
+ class="input"
|
|
|
|
|
+ v-model="jobState.pathWidth"
|
|
|
|
|
+ placeholder="请输入路径宽度,单位:厘米"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ />
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="summary-row">
|
|
|
|
|
+ <text class="summary-label">作业区域类型</text>
|
|
|
|
|
+ <text class="summary-value">{{ areaTypeLabel }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="summary-row">
|
|
|
|
|
+ <text class="summary-label">路线类型</text>
|
|
|
|
|
+ <text class="summary-value">{{ routeTypeLabel }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="summary-row">
|
|
|
|
|
+ <text class="summary-label">作业区域点</text>
|
|
|
|
|
+ <text class="summary-value">{{ jobState.areaPoints.length }} 个</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="summary-row">
|
|
|
|
|
+ <text class="summary-label">障碍物</text>
|
|
|
|
|
+ <text class="summary-value">
|
|
|
|
|
+ {{ jobState.obstacles.length }} 组
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="summary-row">
|
|
|
|
|
+ <text class="summary-label">返航点</text>
|
|
|
|
|
+ <text class="summary-value">
|
|
|
|
|
+ {{ jobState.returnPoint ? '已设置' : '未设置' }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="summary-row">
|
|
|
|
|
+ <text class="summary-label">起点索引</text>
|
|
|
|
|
+ <text class="summary-value">
|
|
|
|
|
+ P{{ currentStartDisplay }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="card tips-card">
|
|
|
|
|
+ <view class="tips-title">说明</view>
|
|
|
|
|
+ <view class="tips-content">
|
|
|
|
|
+ <text>
|
|
|
|
|
+ - 前端仅负责记录点位与基本配置,并在本页面完成数据完整性校验。
|
|
|
|
|
+ </text>
|
|
|
|
|
+ <text>
|
|
|
|
|
+ - 路线生成、几何合法性校验以及调度逻辑由后端 `/api/job/create` 负责处理。
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 步骤导航 -->
|
|
|
|
|
+ <view class="footer">
|
|
|
|
|
+ <button class="btn ghost" @click="prevStep" :disabled="currentStep === 1">
|
|
|
|
|
+ 上一步
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="btn primary"
|
|
|
|
|
+ v-if="currentStep < 4"
|
|
|
|
|
+ @click="nextStep"
|
|
|
|
|
+ >
|
|
|
|
|
+ 下一步
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="btn primary"
|
|
|
|
|
+ v-else
|
|
|
|
|
+ :loading="submitting"
|
|
|
|
|
+ @click="submitJob"
|
|
|
|
|
+ >
|
|
|
|
|
+ 完成并提交
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { createJob, getRealtimeData } from '@/api/services/job.js'
|
|
|
|
|
+import coordinateUtils from '@/utils/coordinateUtils.js'
|
|
|
|
|
+
|
|
|
|
|
+ // 地图实例将保存在组件 data 的 `map` 字段中(见 data() 中的 map: null)
|
|
|
|
|
+ // 我们在组件方法里设置一个绑定到全局回调的适配器,以便 AMap 的 script callback 能够调用组件内的初始化方法。
|
|
|
|
|
+// 简单模拟一个“当前设备坐标”,后续可替换为真实位置上报
|
|
|
|
|
+function mockCurrentPoint(baseIndex = 0) {
|
|
|
|
|
+ const now = Date.now()
|
|
|
|
|
+ const lng = 120.0 + (baseIndex % 10) * 0.0001
|
|
|
|
|
+ const lat = 30.0 + (baseIndex % 10) * 0.0001
|
|
|
|
|
+ return {
|
|
|
|
|
+ lng,
|
|
|
|
|
+ lat,
|
|
|
|
|
+ timestamp: now
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ currentStep: 1,
|
|
|
|
|
+ steps: [
|
|
|
|
|
+ { id: 1, title: '区域与路线', sub: '选择作业区域类型与路线类型' },
|
|
|
|
|
+ { id: 2, title: '打点建模', sub: '作业区域 / 障碍物 / 返航点' },
|
|
|
|
|
+ { id: 3, title: '选择起点', sub: '确定作业起点位置' },
|
|
|
|
|
+ { id: 4, title: '信息确认', sub: '填写作业名称并提交' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ // 区域类型与路线类型
|
|
|
|
|
+ areaTypes: [
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'loopArea',
|
|
|
|
|
+ label: '回字形区域',
|
|
|
|
|
+ desc: '规则四边形地块,适合标准往返或回字形路线',
|
|
|
|
|
+ routes: [{ value: 'loop', label: '回字形路线(loop)' }]
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'bowArea',
|
|
|
|
|
+ label: '弓子形区域',
|
|
|
|
|
+ desc: '一侧为弧形或不规则,适合弓字形或自适应路线',
|
|
|
|
|
+ routes: [{ value: 'bow', label: '弓子形路线(bow)' }]
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'customArea',
|
|
|
|
|
+ label: '自定义区域',
|
|
|
|
|
+ desc: '任意多边形地块,路线由后端自适应规划',
|
|
|
|
|
+ routes: [{ value: 'custom', label: '自定义路线(custom)' }]
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ value: 'ridgeArea',
|
|
|
|
|
+ label: '垄沟区域',
|
|
|
|
|
+ desc: '存在大量垄沟或等距行的地块',
|
|
|
|
|
+ routes: [{ value: 'ridge', label: '垄沟路线(ridge)' }]
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ selectedAreaType: 'loopArea',
|
|
|
|
|
+ selectedRouteType: 'loop',
|
|
|
|
|
+ mode: 'area', // area | obstacle | return
|
|
|
|
|
+ modes: [
|
|
|
|
|
+ { value: 'area', label: '作业区域点' },
|
|
|
|
|
+ { value: 'obstacle', label: '障碍物点' },
|
|
|
|
|
+ { value: 'return', label: '返航点' }
|
|
|
|
|
+ ],
|
|
|
|
|
+ currentObstacle: [],
|
|
|
|
|
+ submitting: false,
|
|
|
|
|
+ locating: false,
|
|
|
|
|
+ // 高德地图相关
|
|
|
|
|
+ map: null,
|
|
|
|
|
+ amapLoaded: false,
|
|
|
|
|
+ markers: [],
|
|
|
|
|
+ geolocation: null, // 高德定位实例
|
|
|
|
|
+ obstacleMarkers: [], // 障碍物标记数组
|
|
|
|
|
+ returnMarker: null, // 返航点标记
|
|
|
|
|
+
|
|
|
|
|
+ // 设备实时位置轮询(用于“遥控车 + 实时打点”)
|
|
|
|
|
+ realtimeMarker: null,
|
|
|
|
|
+ realtimeTimer: null,
|
|
|
|
|
+ lastReportTime: null,
|
|
|
|
|
+ latestRealtimeLngLat: null,
|
|
|
|
|
+ polling: false,
|
|
|
|
|
+ // 回字形(loopArea) 编辑相关
|
|
|
|
|
+ loopMarkers: [],
|
|
|
|
|
+ loopPolygon: null,
|
|
|
|
|
+ loopReplaceIndex: null,
|
|
|
|
|
+ // TODO: 将下面的占位 KEY 替换为真实的高德地图 Key(仅 H5 生效)
|
|
|
|
|
+ amapKey: '9f2cac7ea18905dd3830cf7360a43a35',
|
|
|
|
|
+ jscode: '41af52e416d1fd1b15020dac066cec86',
|
|
|
|
|
+ jobState: {
|
|
|
|
|
+ machineCode: '',
|
|
|
|
|
+ areaType: 'loopArea',
|
|
|
|
|
+ routeType: 'loop',
|
|
|
|
|
+ areaPoints: [],
|
|
|
|
|
+ obstacles: [],
|
|
|
|
|
+ returnPoint: null,
|
|
|
|
|
+ startPointIndex: 0,
|
|
|
|
|
+ jobName: '',
|
|
|
|
|
+ fieldId: '',
|
|
|
|
|
+ pathWidth: 100
|
|
|
|
|
+ }
|
|
|
|
|
+ ,
|
|
|
|
|
+ // 回字形编辑相关图形对象(外圈逻辑已移除)
|
|
|
|
|
+ areaPolygon: null
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onLoad(options) {
|
|
|
|
|
+ const { machineCode, id } = options || {}
|
|
|
|
|
+ if (machineCode) {
|
|
|
|
|
+ this.jobState.machineCode = machineCode
|
|
|
|
|
+ this.jobState.machineId = id
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // UniApp 页面就绪生命周期(H5 平台可在此初始化地图)
|
|
|
|
|
+ onReady() {
|
|
|
|
|
+ // 在页面就绪时再加载 AMap 脚本(确保 DOM 容器存在)
|
|
|
|
|
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
|
|
|
+ // eslint-disable-next-line no-console
|
|
|
|
|
+ console.log('[job-create] onReady - loading AMap script')
|
|
|
|
|
+ this.loadScript()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ modeLabel() {
|
|
|
|
|
+ const m = this.modes.find(m => m.value === this.mode)
|
|
|
|
|
+ return m ? m.label : ''
|
|
|
|
|
+ },
|
|
|
|
|
+ canUndo() {
|
|
|
|
|
+ if (this.mode === 'area') {
|
|
|
|
|
+ return this.jobState.areaPoints.length > 0
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.mode === 'obstacle') {
|
|
|
|
|
+ return this.currentObstacle.length > 0
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.mode === 'return') {
|
|
|
|
|
+ return !!this.jobState.returnPoint
|
|
|
|
|
+ }
|
|
|
|
|
+ return false
|
|
|
|
|
+ },
|
|
|
|
|
+ canChangeStart() {
|
|
|
|
|
+ return this.jobState.areaPoints.length > 0
|
|
|
|
|
+ },
|
|
|
|
|
+ currentStartDisplay() {
|
|
|
|
|
+ if (!this.jobState.areaPoints.length) return '-'
|
|
|
|
|
+ return this.jobState.startPointIndex + 1
|
|
|
|
|
+ },
|
|
|
|
|
+ // 展示当前区域类型与路线类型名称
|
|
|
|
|
+ areaTypeLabel() {
|
|
|
|
|
+ const a = this.areaTypes.find(a => a.value === this.jobState.areaType)
|
|
|
|
|
+ return a ? a.label : ''
|
|
|
|
|
+ },
|
|
|
|
|
+ availableRouteTypes() {
|
|
|
|
|
+ const a = this.areaTypes.find(a => a.value === this.selectedAreaType)
|
|
|
|
|
+ return a ? a.routes : []
|
|
|
|
|
+ },
|
|
|
|
|
+ routeTypeLabel() {
|
|
|
|
|
+ const list = this.areaTypes.reduce((acc, cur) => {
|
|
|
|
|
+ if (cur.routes && cur.routes.length) {
|
|
|
|
|
+ acc.push(...cur.routes)
|
|
|
|
|
+ }
|
|
|
|
|
+ return acc
|
|
|
|
|
+ }, [])
|
|
|
|
|
+ const r = list.find(r => r.value === this.jobState.routeType)
|
|
|
|
|
+ return r ? r.label : this.jobState.routeType
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onUnload() {
|
|
|
|
|
+ // 页面销毁时停止轮询
|
|
|
|
|
+ this.clearRealtimePolling()
|
|
|
|
|
+ // 清理所有地图标记
|
|
|
|
|
+ this.clearAllMarkers()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ loadScript() { // 挂载动态js
|
|
|
|
|
+ // 绑定全局回调,AMap 脚本会调用 window.mapInit
|
|
|
|
|
+ window.mapInit = () => {
|
|
|
|
|
+ this._createMapWhenReady()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var script = document.createElement('script');
|
|
|
|
|
+ script.src = `https://webapi.amap.com/maps?v=2.0&key=${this.amapKey}&callback=mapInit`;
|
|
|
|
|
+ document.body.appendChild(script);
|
|
|
|
|
+ this.amapLoaded = true
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ _createMapWhenReady() {
|
|
|
|
|
+ const createWhenReady = () => {
|
|
|
|
|
+ const container = document.getElementById('mapContainer')
|
|
|
|
|
+ if (!container) {
|
|
|
|
|
+ // 容器尚未渲染,延迟重试
|
|
|
|
|
+ setTimeout(createWhenReady, 200)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建基础地图实例并保存在组件 data.map 中
|
|
|
|
|
+ const defaultCenter = [113.382, 22.5211]
|
|
|
|
|
+
|
|
|
|
|
+ const createMapWithCenter = centerArr => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.map = new AMap.Map('mapContainer', {
|
|
|
|
|
+ center: centerArr || defaultCenter,
|
|
|
|
|
+ zoom: 16
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('[job-create] create map failed', err)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 添加卫星图层
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (AMap.TileLayer && typeof AMap.TileLayer.Satellite === 'function') {
|
|
|
|
|
+ const sat = new AMap.TileLayer.Satellite()
|
|
|
|
|
+ this.map.add(sat)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (layerErr) {
|
|
|
|
|
+ console.warn('[job-create] satellite layer failed', layerErr)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 添加工具栏
|
|
|
|
|
+ try {
|
|
|
|
|
+ AMap.plugin('AMap.ToolBar', () => {
|
|
|
|
|
+ const toolbar = new AMap.ToolBar()
|
|
|
|
|
+ if (this.map && typeof this.map.addControl === 'function') {
|
|
|
|
|
+ this.map.addControl(toolbar)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch (pluginErr) {
|
|
|
|
|
+ console.warn('[job-create] toolbar plugin failed', pluginErr)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 加载高德定位插件(异步加载)
|
|
|
|
|
+ AMap.plugin('AMap.Geolocation', () => {
|
|
|
|
|
+ this.geolocation = new AMap.Geolocation({
|
|
|
|
|
+ enableHighAccuracy: true,
|
|
|
|
|
+ timeout: 10000,
|
|
|
|
|
+ maximumAge: 0, // 不使用缓存,每次都获取最新位置
|
|
|
|
|
+ convert: true, // 自动偏移坐标,偏移后的坐标为高德坐标
|
|
|
|
|
+ showButton: false,
|
|
|
|
|
+ showMarker: false,
|
|
|
|
|
+ showCircle: false,
|
|
|
|
|
+ panToLocation: false,
|
|
|
|
|
+ zoomToAccuracy: false,
|
|
|
|
|
+ // 优先使用浏览器定位,失败后使用IP定位
|
|
|
|
|
+ noIpLocate: 0,
|
|
|
|
|
+ // 使用精确定位
|
|
|
|
|
+ GeoLocationFirst: true
|
|
|
|
|
+ })
|
|
|
|
|
+ console.log('[job-create] 高德定位插件加载完成')
|
|
|
|
|
+
|
|
|
|
|
+ // 插件加载完成后立即尝试定位
|
|
|
|
|
+ this.tryAutoLocation()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 绑定地图点击事件
|
|
|
|
|
+ if (this.map && typeof this.map.on === 'function') {
|
|
|
|
|
+ this.map.on('click', e => {
|
|
|
|
|
+ const lng = e.lnglat && (e.lnglat.lng || (e.lnglat.getLng && e.lnglat.getLng()))
|
|
|
|
|
+ const lat = e.lnglat && (e.lnglat.lat || (e.lnglat.getLat && e.lnglat.getLat()))
|
|
|
|
|
+ if (lng == null || lat == null) return
|
|
|
|
|
+ this.onMapClick({ lng, lat })
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 地图初始化完成后,如果已选择设备,则开始轮询实时位置
|
|
|
|
|
+ this.setupRealtimePolling()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建地图(使用默认中心点)
|
|
|
|
|
+ console.log('[job-create] 创建地图使用默认中心点:', defaultCenter)
|
|
|
|
|
+ createMapWithCenter(defaultCenter)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ createWhenReady()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 自动定位(插件加载完成后调用)
|
|
|
|
|
+ tryAutoLocation() {
|
|
|
|
|
+ console.log('[job-create] 开始自动定位...')
|
|
|
|
|
+
|
|
|
|
|
+ this._getCurrentLocation(
|
|
|
|
|
+ res => {
|
|
|
|
|
+ console.log('[job-create] 自动定位成功:', res)
|
|
|
|
|
+ const { longitude, latitude } = res || {}
|
|
|
|
|
+ if (longitude != null && latitude != null) {
|
|
|
|
|
+ console.log('[job-create] 使用定位坐标:', [longitude, latitude])
|
|
|
|
|
+ this._centerMapToLngLat(longitude, latitude, 16)
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '已定位到您的位置',
|
|
|
|
|
+ icon: 'success',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.warn('[job-create] 定位返回坐标无效')
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ err => {
|
|
|
|
|
+ console.warn('[job-create] 自动定位失败:', err)
|
|
|
|
|
+ // 不显示错误提示,避免干扰用户
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ },
|
|
|
|
|
+ // 尝试获取设备定位并居中地图(优先使用 uni.getLocation,回退到 navigator 或模拟点)
|
|
|
|
|
+ _getDeviceLocationAndCenter() {
|
|
|
|
|
+ const doCenter = (lng, lat) => {
|
|
|
|
|
+ if (!lng && lng !== 0) return
|
|
|
|
|
+ if (!lat && lat !== 0) return
|
|
|
|
|
+ this._centerMapToLngLat(lng, lat, 18)
|
|
|
|
|
+ // 在当前位置添加一个简单标记(不影响现有图层)
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (this.map && typeof AMap !== 'undefined' && AMap.Marker) {
|
|
|
|
|
+ new AMap.Marker({
|
|
|
|
|
+ position: [lng, lat],
|
|
|
|
|
+ map: this.map
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ // eslint-disable-next-line no-console
|
|
|
|
|
+ console.warn('[job-create] add marker failed', err)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 使用高德定位
|
|
|
|
|
+ this._getCurrentLocation(
|
|
|
|
|
+ res => {
|
|
|
|
|
+ console.log("当前定位", res);
|
|
|
|
|
+ const { longitude, latitude } = res || {}
|
|
|
|
|
+ if (longitude != null && latitude != null) {
|
|
|
|
|
+ doCenter(longitude, latitude)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const p = this.getMapCenterPoint() || mockCurrentPoint(0)
|
|
|
|
|
+ doCenter(p.lng, p.lat)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ () => {
|
|
|
|
|
+ const p = this.getMapCenterPoint() || mockCurrentPoint(0)
|
|
|
|
|
+ doCenter(p.lng, p.lat)
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ // 最后回退到地图中心或模拟点
|
|
|
|
|
+ const p = this.getMapCenterPoint() || mockCurrentPoint(0)
|
|
|
|
|
+ doCenter(p.lng, p.lat)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 将地图居中到指定经纬度并设置缩放(安全调用)
|
|
|
|
|
+ _centerMapToLngLat(lng, lat, zoom = 18) {
|
|
|
|
|
+ if (!this.map) return
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (typeof this.map.setCenter === 'function') {
|
|
|
|
|
+ this.map.setCenter([lng, lat])
|
|
|
|
|
+ }
|
|
|
|
|
+ if (typeof this.map.setZoom === 'function' && typeof zoom === 'number') {
|
|
|
|
|
+ this.map.setZoom(zoom)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ // eslint-disable-next-line no-console
|
|
|
|
|
+ console.warn('[job-create] center map failed', err)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // AMap 加载由页面顶部的全局 callback `mapInit` 与 `loadScript()` 管理
|
|
|
|
|
+
|
|
|
|
|
+ // 使用高德定位获取当前位置
|
|
|
|
|
+ _getCurrentLocation(successCallback, failCallback, retryCount = 0) {
|
|
|
|
|
+ if (!this.geolocation) {
|
|
|
|
|
+ // 如果还未初始化,等待一段时间后重试,最多重试5次
|
|
|
|
|
+ if (retryCount < 5) {
|
|
|
|
|
+ console.log(`[job-create] 高德定位未初始化,${retryCount + 1}秒后重试...`)
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this._getCurrentLocation(successCallback, failCallback, retryCount + 1)
|
|
|
|
|
+ }, 1000)
|
|
|
|
|
+ return
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.warn('[job-create] 高德定位未初始化,重试失败')
|
|
|
|
|
+ if (failCallback) failCallback(new Error('高德定位未初始化'))
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[job-create] 开始调用高德定位 getCurrentPosition...')
|
|
|
|
|
+
|
|
|
|
|
+ this.geolocation.getCurrentPosition((status, result) => {
|
|
|
|
|
+ console.log('[job-create] 定位回调 status:', status, 'result:', result)
|
|
|
|
|
+
|
|
|
|
|
+ if (status === 'complete') {
|
|
|
|
|
+ const { lng, lat } = result.position
|
|
|
|
|
+ console.log('[job-create] 高德定位成功:', { lng, lat })
|
|
|
|
|
+ if (successCallback) {
|
|
|
|
|
+ successCallback({
|
|
|
|
|
+ longitude: lng,
|
|
|
|
|
+ latitude: lat
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.error('[job-create] 高德定位失败:', result)
|
|
|
|
|
+ let errorMsg = '定位失败'
|
|
|
|
|
+ let errorDetail = ''
|
|
|
|
|
+
|
|
|
|
|
+ switch(result.info) {
|
|
|
|
|
+ case 'FAILED':
|
|
|
|
|
+ errorMsg = '定位失败,请检查网络连接'
|
|
|
|
|
+ errorDetail = '可能原因:网络问题或GPS信号弱'
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'NOT_SUPPORTED':
|
|
|
|
|
+ errorMsg = '浏览器不支持定位功能'
|
|
|
|
|
+ errorDetail = '请使用支持地理定位的现代浏览器'
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'PERMISSION_DENIED':
|
|
|
|
|
+ errorMsg = '定位权限被拒绝'
|
|
|
|
|
+ errorDetail = 'HTTPS环境下需要用户授权定位权限,HTTP环境下浏览器会直接拒绝'
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'PERMISSION_GRANTED':
|
|
|
|
|
+ errorMsg = '定位权限已获取但定位失败'
|
|
|
|
|
+ errorDetail = '可能是GPS信号问题'
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'TIMEOUT':
|
|
|
|
|
+ errorMsg = '定位请求超时'
|
|
|
|
|
+ errorDetail = '请检查网络连接或GPS信号'
|
|
|
|
|
+ break
|
|
|
|
|
+ default:
|
|
|
|
|
+ errorMsg = `定位失败: ${result.info}`
|
|
|
|
|
+ errorDetail = result.message || ''
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.warn('[job-create] 定位失败详情:', errorMsg, errorDetail)
|
|
|
|
|
+
|
|
|
|
|
+ if (failCallback) {
|
|
|
|
|
+ failCallback({
|
|
|
|
|
+ code: result.info,
|
|
|
|
|
+ message: errorMsg,
|
|
|
|
|
+ detail: errorDetail
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 从地图获取中心点(回退到模拟坐标)
|
|
|
|
|
+ getMapCenterPoint() {
|
|
|
|
|
+ if (this.map && typeof this.map.getCenter === 'function') {
|
|
|
|
|
+ const c = this.map.getCenter()
|
|
|
|
|
+ // AMap 返回对象通常包含 lng/lat
|
|
|
|
|
+ return {
|
|
|
|
|
+ lng: c.lng || (c.lng === 0 ? 0 : c.getLng && c.getLng()),
|
|
|
|
|
+ lat: c.lat || (c.lat === 0 ? 0 : c.getLat && c.getLat()),
|
|
|
|
|
+ timestamp: Date.now()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null
|
|
|
|
|
+ },
|
|
|
|
|
+ // ---------- end AMap ----------
|
|
|
|
|
+
|
|
|
|
|
+ // 区域类型与路线类型选择
|
|
|
|
|
+ selectAreaType(val) {
|
|
|
|
|
+ this.selectedAreaType = val
|
|
|
|
|
+ const a = this.areaTypes.find(item => item.value === val)
|
|
|
|
|
+ if (a && a.routes && a.routes.length) {
|
|
|
|
|
+ this.selectedRouteType = a.routes[0].value
|
|
|
|
|
+ }
|
|
|
|
|
+ this.jobState.areaType = this.selectedAreaType
|
|
|
|
|
+ this.jobState.routeType = this.selectedRouteType
|
|
|
|
|
+ },
|
|
|
|
|
+ selectRouteType(val) {
|
|
|
|
|
+ this.selectedRouteType = val
|
|
|
|
|
+ this.jobState.routeType = val
|
|
|
|
|
+ },
|
|
|
|
|
+ // 模式切换
|
|
|
|
|
+ switchMode(val) {
|
|
|
|
|
+ this.mode = val
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ===================== 设备实时位置轮询(参考 job-detail) =====================
|
|
|
|
|
+ setupRealtimePolling() {
|
|
|
|
|
+ // 仅 Step2 打点建模时需要实时位置
|
|
|
|
|
+ if (this.currentStep !== 2) return
|
|
|
|
|
+
|
|
|
|
|
+ const deviceId = this.jobState.machineId || this.jobState.machineCode
|
|
|
|
|
+ if (!deviceId) return
|
|
|
|
|
+ if (!this.amapLoaded || !this.map) return
|
|
|
|
|
+
|
|
|
|
|
+ // 避免重复开启
|
|
|
|
|
+ if (this.realtimeTimer) return
|
|
|
|
|
+
|
|
|
|
|
+ // 立刻拉一次
|
|
|
|
|
+ this.fetchRealtimeAndUpdate()
|
|
|
|
|
+
|
|
|
|
|
+ this.realtimeTimer = setInterval(() => {
|
|
|
|
|
+ this.fetchRealtimeAndUpdate()
|
|
|
|
|
+ }, 3000)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ clearRealtimePolling() {
|
|
|
|
|
+ if (this.realtimeTimer) {
|
|
|
|
|
+ clearInterval(this.realtimeTimer)
|
|
|
|
|
+ this.realtimeTimer = null
|
|
|
|
|
+ }
|
|
|
|
|
+ this.polling = false
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async fetchRealtimeAndUpdate() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const deviceId = this.jobState.machineCode
|
|
|
|
|
+ if (!deviceId) return
|
|
|
|
|
+
|
|
|
|
|
+ this.polling = true
|
|
|
|
|
+ const res = await getRealtimeData(deviceId)
|
|
|
|
|
+ const payload = res && res.data && (res.data.data || res.data)
|
|
|
|
|
+ if (!payload) return
|
|
|
|
|
+
|
|
|
|
|
+ const reportTime = payload.reportTime
|
|
|
|
|
+ if (reportTime && this.lastReportTime && reportTime < this.lastReportTime) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (reportTime) this.lastReportTime = reportTime
|
|
|
|
|
+
|
|
|
|
|
+ const pt = payload.currentPoint
|
|
|
|
|
+ if (!pt || pt.x == null || pt.y == null) return
|
|
|
|
|
+
|
|
|
|
|
+ const lngLat = [pt.x, pt.y]
|
|
|
|
|
+ this.latestRealtimeLngLat = lngLat
|
|
|
|
|
+
|
|
|
|
|
+ this.updateRealtimeMarker(lngLat)
|
|
|
|
|
+
|
|
|
|
|
+ // 轻量跟随:视野中心跟随车,不频繁 fitView
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (this.map && typeof this.map.setCenter === 'function') {
|
|
|
|
|
+ this.map.setCenter(lngLat)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ // eslint-disable-next-line no-console
|
|
|
|
|
+ console.warn('[job-create] fetchRealtimeAndUpdate failed', e)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.polling = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ updateRealtimeMarker(lngLat) {
|
|
|
|
|
+ if (!this.map || !this.amapLoaded) return
|
|
|
|
|
+ if (!lngLat || lngLat.length !== 2) return
|
|
|
|
|
+
|
|
|
|
|
+ if (!this.realtimeMarker) {
|
|
|
|
|
+ this.realtimeMarker = new AMap.Marker({
|
|
|
|
|
+ map: this.map,
|
|
|
|
|
+ position: lngLat
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.realtimeMarker.setPosition(lngLat)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 新增点(使用模拟坐标/地图中心点)
|
|
|
|
|
+ addPoint() {
|
|
|
|
|
+ // Step2 的“新增点”需要使用设备实时位置:
|
|
|
|
|
+ // - 轮询拿到的 latestRealtimeLngLat 优先
|
|
|
|
|
+ // - 回退到地图中心点(手动拖拽视野时)
|
|
|
|
|
+ // - 再回退到 mock
|
|
|
|
|
+ const realtimeLngLat = this.latestRealtimeLngLat
|
|
|
|
|
+ const realtimePoint = realtimeLngLat && realtimeLngLat.length === 2
|
|
|
|
|
+ ? { lng: realtimeLngLat[0], lat: realtimeLngLat[1], timestamp: Date.now() }
|
|
|
|
|
+ : null
|
|
|
|
|
+
|
|
|
|
|
+ // 回退:地图中心点
|
|
|
|
|
+ let centerPoint = null
|
|
|
|
|
+ if (this.amapLoaded && this.map) {
|
|
|
|
|
+ centerPoint = this.getMapCenterPoint()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const fallbackPoint = centerPoint || mockCurrentPoint(0)
|
|
|
|
|
+ const point = realtimePoint || fallbackPoint
|
|
|
|
|
+
|
|
|
|
|
+ if (this.mode === 'area') {
|
|
|
|
|
+ this.onMapClick({ lng: point.lng, lat: point.lat })
|
|
|
|
|
+ } else if (this.mode === 'obstacle') {
|
|
|
|
|
+ this.currentObstacle.push(point)
|
|
|
|
|
+ // 添加障碍物标记
|
|
|
|
|
+ this.addObstacleMarker(point, this.jobState.obstacles.length, this.currentObstacle.length - 1)
|
|
|
|
|
+ } else if (this.mode === 'return') {
|
|
|
|
|
+ if (this.jobState.returnPoint) {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '覆盖返航点',
|
|
|
|
|
+ content: '已存在返航点,是否覆盖为当前设备位置?',
|
|
|
|
|
+ success: res => {
|
|
|
|
|
+ if (res.confirm) {
|
|
|
|
|
+ this.jobState.returnPoint = point
|
|
|
|
|
+ // 更新返航点标记
|
|
|
|
|
+ this.updateReturnMarker(point)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.jobState.returnPoint = point
|
|
|
|
|
+ // 添加返航点标记
|
|
|
|
|
+ this.updateReturnMarker(point)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 撤销点
|
|
|
|
|
+ undoPoint() {
|
|
|
|
|
+ if (!this.canUndo) return
|
|
|
|
|
+ if (this.mode === 'area') {
|
|
|
|
|
+ // if (this.selectedAreaType === 'loopArea') {
|
|
|
|
|
+ // 删除最后一个点,同时删除对应 draggable marker
|
|
|
|
|
+ const lastIdx = this.jobState.areaPoints.length - 1
|
|
|
|
|
+ if (lastIdx >= 0) {
|
|
|
|
|
+ this.jobState.areaPoints.pop()
|
|
|
|
|
+ const m = this.loopMarkers.pop()
|
|
|
|
|
+ if (m && typeof m.setMap === 'function') {
|
|
|
|
|
+ try { m.setMap(null) } catch (e) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ // 更新 polygon 或清除
|
|
|
|
|
+ if (this.jobState.areaPoints.length >= 3) {
|
|
|
|
|
+ this.recomputeLoopPolygon()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (this.loopPolygon && typeof this.loopPolygon.setMap === 'function') {
|
|
|
|
|
+ try { this.loopPolygon.setMap(null) } catch (e) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ this.loopPolygon = null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // }
|
|
|
|
|
+ // else {
|
|
|
|
|
+ // // 非回字形:删除最后一个点,并同步删除最后一个 marker + 重绘 polygon
|
|
|
|
|
+ // const lastIdx = this.jobState.areaPoints.length - 1
|
|
|
|
|
+ // if (lastIdx >= 0) {
|
|
|
|
|
+ // this.jobState.areaPoints.pop()
|
|
|
|
|
+ // const m = this.markers && this.markers.length ? this.markers.pop() : null
|
|
|
|
|
+ // if (m && typeof m.setMap === 'function') {
|
|
|
|
|
+ // try { m.setMap(null) } catch (e) {}
|
|
|
|
|
+ // }
|
|
|
|
|
+ // }
|
|
|
|
|
+
|
|
|
|
|
+ // if (this.jobState.areaPoints.length >= 3) {
|
|
|
|
|
+ // this.drawAreaPolygon()
|
|
|
|
|
+ // } else if (this.areaPolygon) {
|
|
|
|
|
+ // try { this.areaPolygon.setMap(null) } catch (e) {}
|
|
|
|
|
+ // this.areaPolygon = null
|
|
|
|
|
+ // }
|
|
|
|
|
+ // }
|
|
|
|
|
+ } else if (this.mode === 'obstacle') {
|
|
|
|
|
+ this.currentObstacle.pop()
|
|
|
|
|
+ // 删除最后一个障碍物标记
|
|
|
|
|
+ this.removeLastObstacleMarker()
|
|
|
|
|
+ } else if (this.mode === 'return') {
|
|
|
|
|
+ this.jobState.returnPoint = null
|
|
|
|
|
+ // 删除返航点标记
|
|
|
|
|
+ this.clearReturnMarker()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 完成当前障碍物
|
|
|
|
|
+ finishCurrentObstacle() {
|
|
|
|
|
+ if (!this.currentObstacle.length) return
|
|
|
|
|
+ this.jobState.obstacles.push([...this.currentObstacle])
|
|
|
|
|
+ this.currentObstacle = []
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '已保存一组障碍物',
|
|
|
|
|
+ icon: 'success'
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 将“录入中”的障碍物也并入 jobState.obstacles(用于步骤切换/提交前兜底)
|
|
|
|
|
+ flushCurrentObstacle() {
|
|
|
|
|
+ if (this.currentObstacle && this.currentObstacle.length) {
|
|
|
|
|
+ this.jobState.obstacles.push([...this.currentObstacle])
|
|
|
|
|
+ this.currentObstacle = []
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 步骤导航
|
|
|
|
|
+ prevStep() {
|
|
|
|
|
+ if (this.currentStep === 1) return
|
|
|
|
|
+ this.currentStep -= 1
|
|
|
|
|
+
|
|
|
|
|
+ // 离开 Step2 时停止轮询,避免后台持续请求
|
|
|
|
|
+ if (this.currentStep !== 2) {
|
|
|
|
|
+ this.clearRealtimePolling()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ nextStep() {
|
|
|
|
|
+ // Step1 校验:区域类型与路线类型必选
|
|
|
|
|
+ if (this.currentStep === 1) {
|
|
|
|
|
+ if (!this.selectedAreaType || !this.selectedRouteType) {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '请选择作业区域类型和路线类型',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.jobState.areaType = this.selectedAreaType
|
|
|
|
|
+ this.jobState.routeType = this.selectedRouteType
|
|
|
|
|
+ }
|
|
|
|
|
+ // Step2 校验:作业区域点 / 返航点
|
|
|
|
|
+ if (this.currentStep === 2) {
|
|
|
|
|
+ if (this.jobState.areaPoints.length < 3) {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '作业区域点至少需要 3 个',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 离开打点页前,把“录入中”的障碍物也保存下来,避免忘记点“完成当前障碍物”
|
|
|
|
|
+ this.flushCurrentObstacle()
|
|
|
|
|
+
|
|
|
|
|
+ if (!this.jobState.returnPoint) {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '请先设置返航点',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // Step3 校验:需存在作业区域点
|
|
|
|
|
+ if (this.currentStep === 3) {
|
|
|
|
|
+ if (!this.jobState.areaPoints.length) {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '请先在上一步记录作业区域点',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.currentStep < 4) {
|
|
|
|
|
+ this.currentStep += 1
|
|
|
|
|
+
|
|
|
|
|
+ // 进入 Step2 时开启轮询(地图可能已初始化)
|
|
|
|
|
+ if (this.currentStep === 2) {
|
|
|
|
|
+ this.setupRealtimePolling()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 离开 Step2 时关闭轮询
|
|
|
|
|
+ this.clearRealtimePolling()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 起点选择
|
|
|
|
|
+ setStartIndex(index) {
|
|
|
|
|
+ if (!this.jobState.areaPoints.length) return
|
|
|
|
|
+ this.jobState.startPointIndex = index
|
|
|
|
|
+ },
|
|
|
|
|
+ prevStart() {
|
|
|
|
|
+ if (!this.canChangeStart) return
|
|
|
|
|
+ const total = this.jobState.areaPoints.length
|
|
|
|
|
+ this.jobState.startPointIndex =
|
|
|
|
|
+ (this.jobState.startPointIndex - 1 + total) % total
|
|
|
|
|
+ },
|
|
|
|
|
+ nextStart() {
|
|
|
|
|
+ if (!this.canChangeStart) return
|
|
|
|
|
+ const total = this.jobState.areaPoints.length
|
|
|
|
|
+ this.jobState.startPointIndex =
|
|
|
|
|
+ (this.jobState.startPointIndex + 1) % total
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 手动定位
|
|
|
|
|
+ async manualLocation() {
|
|
|
|
|
+ if (this.locating) return
|
|
|
|
|
+ this.locating = true
|
|
|
|
|
+
|
|
|
|
|
+ const doCenter = (lng, lat) => {
|
|
|
|
|
+ if (!lng && lng !== 0) return
|
|
|
|
|
+ if (!lat && lat !== 0) return
|
|
|
|
|
+ this._centerMapToLngLat(lng, lat, 18)
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '已定位到您的位置',
|
|
|
|
|
+ icon: 'success',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 检查是否为HTTPS环境
|
|
|
|
|
+ const isSecureContext = window.isSecureContext || window.location.protocol === 'https:'
|
|
|
|
|
+ if (!isSecureContext) {
|
|
|
|
|
+ console.warn('[job-create] 非HTTPS环境,浏览器可能拒绝定位请求')
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '定位提示',
|
|
|
|
|
+ content: '当前为HTTP环境,浏览器可能拒绝定位请求。建议使用HTTPS访问或使用IP定位。',
|
|
|
|
|
+ showCancel: false
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 使用高德定位
|
|
|
|
|
+ await new Promise((resolve, reject) => {
|
|
|
|
|
+ this._getCurrentLocation(
|
|
|
|
|
+ res => {
|
|
|
|
|
+ const { longitude, latitude } = res || {}
|
|
|
|
|
+ if (longitude != null && latitude != null) {
|
|
|
|
|
+ doCenter(longitude, latitude)
|
|
|
|
|
+ resolve()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ reject(new Error('无效坐标'))
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ reject
|
|
|
|
|
+ )
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('[job-create] 手动定位失败:', err)
|
|
|
|
|
+ let errorMsg = '定位失败'
|
|
|
|
|
+ let showDetail = false
|
|
|
|
|
+
|
|
|
|
|
+ if (err.message) {
|
|
|
|
|
+ errorMsg = err.message
|
|
|
|
|
+ }
|
|
|
|
|
+ if (err.detail) {
|
|
|
|
|
+ showDetail = true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (showDetail) {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: errorMsg,
|
|
|
|
|
+ content: err.detail || '请检查浏览器定位权限设置,或确保使用HTTPS访问',
|
|
|
|
|
+ showCancel: false
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: errorMsg,
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 3000
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.locating = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 提交作业
|
|
|
|
|
+ async submitJob() {
|
|
|
|
|
+ if (!this.jobState.jobName) {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '请填写作业名称',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!this.jobState.fieldId) {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '请填写地块ID',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!this.jobState.pathWidth) {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '请填写路径宽度',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.jobState.areaPoints.length < 3) {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '作业区域点至少需要 3 个',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 提交前兜底:把“录入中”的障碍物也并入 obstacles
|
|
|
|
|
+ this.flushCurrentObstacle()
|
|
|
|
|
+
|
|
|
|
|
+ // 将高德坐标转换为WGS84坐标
|
|
|
|
|
+ const convertedAreaPoints = coordinateUtils.convertPointsToWgs84(this.jobState.areaPoints)
|
|
|
|
|
+ const convertedObstacles = this.jobState.obstacles.map(obstacleGroup =>
|
|
|
|
|
+ coordinateUtils.convertPointsToWgs84(obstacleGroup)
|
|
|
|
|
+ )
|
|
|
|
|
+ const convertedReturnPoint = this.jobState.returnPoint ?
|
|
|
|
|
+ coordinateUtils.convertPointToWgs84(this.jobState.returnPoint) : undefined
|
|
|
|
|
+
|
|
|
|
|
+ // 映射 areaType 到数字
|
|
|
|
|
+ const areaTypeMap = {
|
|
|
|
|
+ 'loopArea': 1, // 回字形
|
|
|
|
|
+ 'bowArea': 2, // 弓字形
|
|
|
|
|
+ 'customArea': 3, // 自定义
|
|
|
|
|
+ 'ridgeArea': 4 // 垄沟
|
|
|
|
|
+ }
|
|
|
|
|
+ console.log("this.jobState",this.jobState);
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ // deviceId: this.jobState.machineCode,
|
|
|
|
|
+ deviceId: this.jobState.machineId,
|
|
|
|
|
+ fieldId: parseInt(this.jobState.fieldId),
|
|
|
|
|
+ taskName: this.jobState.jobName,
|
|
|
|
|
+ areaType: areaTypeMap[this.jobState.areaType] || 1,
|
|
|
|
|
+ waypoints: convertedAreaPoints.map(p => ({ lng: p.lng, lat: p.lat })),
|
|
|
|
|
+ obstacles: convertedObstacles.flat().map(p => ({ lng: p.lng, lat: p.lat })),
|
|
|
|
|
+ returnPoint: convertedReturnPoint ? { lng: convertedReturnPoint.lng, lat: convertedReturnPoint.lat } : undefined,
|
|
|
|
|
+ pathWidth: parseInt(this.jobState.pathWidth)
|
|
|
|
|
+ }
|
|
|
|
|
+ console.log("payload",payload);
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ this.submitting = true
|
|
|
|
|
+ uni.showLoading({ title: '提交中...' })
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await createJob(payload)
|
|
|
|
|
+ console.log("res收到尽快发货",res);
|
|
|
|
|
+
|
|
|
|
|
+ const { data } = res || {}
|
|
|
|
|
+ if (data && data.code === 200) {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '作业创建成功',
|
|
|
|
|
+ icon: 'success'
|
|
|
|
|
+ })
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ uni.navigateBack()
|
|
|
|
|
+ }, 800)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: (data && data.msg) || '提交失败',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('创建作业失败', err)
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '网络异常或接口未就绪',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.submitting = false
|
|
|
|
|
+ uni.hideLoading()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 区域编辑逻辑(支持所有区域类型)
|
|
|
|
|
+ // - 点击地图依次记录点(或替换已选点)
|
|
|
|
|
+ // - 回字形区域使用可拖拽 Marker,其他区域使用普通 Marker
|
|
|
|
|
+ // - 点击某个点可以进入“替换模式”,下一次地图点击会替换该点
|
|
|
|
|
+ onMapClick({ lng, lat } = {}) {
|
|
|
|
|
+ if (!lng && lng !== 0) return
|
|
|
|
|
+ if (!lat && lat !== 0) return
|
|
|
|
|
+
|
|
|
|
|
+ // 确保在区域编辑模式下
|
|
|
|
|
+ if (this.mode !== 'area') return
|
|
|
|
|
+
|
|
|
|
|
+ const newPoint = { lng, lat, timestamp: Date.now() }
|
|
|
|
|
+
|
|
|
|
|
+ // 回字形区域特殊处理(支持拖拽和替换)
|
|
|
|
|
+
|
|
|
|
|
+ // 替换模式下替换指定点
|
|
|
|
|
+ if (this.loopReplaceIndex != null && this.loopReplaceIndex >= 0 && this.loopReplaceIndex < this.jobState.areaPoints.length) {
|
|
|
|
|
+ this.jobState.areaPoints.splice(this.loopReplaceIndex, 1, newPoint)
|
|
|
|
|
+ const m = this.loopMarkers[this.loopReplaceIndex]
|
|
|
|
|
+ if (m && typeof m.setPosition === 'function') {
|
|
|
|
|
+ try { m.setPosition([lng, lat]) } catch (e) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ this.loopReplaceIndex = null
|
|
|
|
|
+ if (this.jobState.areaPoints.length >= 3) this.recomputeLoopPolygon()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 正常添加点
|
|
|
|
|
+ this.jobState.areaPoints.push(newPoint)
|
|
|
|
|
+ const idx = this.jobState.areaPoints.length - 1
|
|
|
|
|
+ this.createOrUpdateLoopMarker(idx, newPoint)
|
|
|
|
|
+ if (this.jobState.areaPoints.length >= 3) {
|
|
|
|
|
+ this.recomputeLoopPolygon()
|
|
|
|
|
+ }
|
|
|
|
|
+ // }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ createOrUpdateLoopMarker(index, point) {
|
|
|
|
|
+
|
|
|
|
|
+ if (!this.map || typeof AMap === 'undefined') return
|
|
|
|
|
+ const pos = [point.lng, point.lat]
|
|
|
|
|
+ let marker = this.loopMarkers[index]
|
|
|
|
|
+ if (marker) {
|
|
|
|
|
+ try { marker.setPosition(pos) } catch (e) {}
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ // Use smaller icon size
|
|
|
|
|
+ let icon = null
|
|
|
|
|
+ try {
|
|
|
|
|
+ icon = new AMap.Icon({
|
|
|
|
|
+ image: "static/icons/poi-marker-default.png",
|
|
|
|
|
+ size: new AMap.Size(20, 28),
|
|
|
|
|
+ imageSize: new AMap.Size(20, 28)
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ icon = "static/icons/poi-marker-default.png"
|
|
|
|
|
+ }
|
|
|
|
|
+ marker = new AMap.Marker({
|
|
|
|
|
+ position: pos,
|
|
|
|
|
+ map: this.map,
|
|
|
|
|
+ draggable: true,
|
|
|
|
|
+ icon,
|
|
|
|
|
+ offset: new AMap.Pixel(-10, -28)
|
|
|
|
|
+ })
|
|
|
|
|
+ // add simple label showing index (P1, P2...)
|
|
|
|
|
+ try {
|
|
|
|
|
+ const labelContent = `<div class="p-marker-label">P${index + 1}</div>`
|
|
|
|
|
+ if (typeof marker.setLabel === 'function') {
|
|
|
|
|
+ marker.setLabel({
|
|
|
|
|
+ content: labelContent,
|
|
|
|
|
+ offset: new AMap.Pixel(10, -36)
|
|
|
|
|
+ })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ marker.label = { content: labelContent }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ // drag end -> update point and polygon
|
|
|
|
|
+ marker.on('dragend', e => {
|
|
|
|
|
+ const p = marker.getPosition()
|
|
|
|
|
+ const lngv = p.lng || (p.getLng && p.getLng())
|
|
|
|
|
+ const latv = p.lat || (p.getLat && p.getLat())
|
|
|
|
|
+ if (lngv == null || latv == null) return
|
|
|
|
|
+ // Use Vue.set / $set to ensure reactivity for array index assignment
|
|
|
|
|
+ if (typeof this.$set === 'function') {
|
|
|
|
|
+ this.$set(this.jobState.areaPoints, index, { lng: lngv, lat: latv, timestamp: Date.now() })
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.jobState.areaPoints[index] = { lng: lngv, lat: latv, timestamp: Date.now() }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.jobState.areaPoints.length >= 3) this.recomputeLoopPolygon()
|
|
|
|
|
+ })
|
|
|
|
|
+ // click -> enter replace mode for this index
|
|
|
|
|
+ marker.on('click', () => {
|
|
|
|
|
+ this.loopReplaceIndex = index
|
|
|
|
|
+ uni.showToast({ title: `已选中 P${index + 1},下一次点击将替换该点`, icon: 'none' })
|
|
|
|
|
+ })
|
|
|
|
|
+ this.loopMarkers[index] = marker
|
|
|
|
|
+ // refresh labels for all markers to keep numbering consistent
|
|
|
|
|
+ this.refreshLoopMarkerLabels()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ recomputeLoopPolygon() {
|
|
|
|
|
+ if (!this.map) return
|
|
|
|
|
+ if (!this.jobState.areaPoints || this.jobState.areaPoints.length < 3) {
|
|
|
|
|
+ if (this.loopPolygon && typeof this.loopPolygon.setMap === 'function') {
|
|
|
|
|
+ try { this.loopPolygon.setMap(null) } catch (e) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ this.loopPolygon = null
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Outer path: ensure order as [ [lng,lat], ... ]
|
|
|
|
|
+ const outer = this.jobState.areaPoints.map(p => [p.lng, p.lat])
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (this.loopPolygon) {
|
|
|
|
|
+ this.loopPolygon.setPath(outer)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.loopPolygon = new AMap.Polygon({
|
|
|
|
|
+ map: this.map,
|
|
|
|
|
+ path: outer,
|
|
|
|
|
+ strokeColor: '#3bb44a',
|
|
|
|
|
+ strokeWeight: 2,
|
|
|
|
|
+ fillColor: '#3bb44a',
|
|
|
|
|
+ fillOpacity: 0.15
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ // eslint-disable-next-line no-console
|
|
|
|
|
+ console.warn('[job-create] recompute loop polygon failed', err)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ refreshLoopMarkerLabels() {
|
|
|
|
|
+ if (!this.loopMarkers || !this.loopMarkers.length) return
|
|
|
|
|
+ this.loopMarkers.forEach((m, i) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const content = `<div class="p-marker-label">P${i + 1}</div>`
|
|
|
|
|
+ if (typeof m.setLabel === 'function') {
|
|
|
|
|
+ m.setLabel({ content, offset: new AMap.Pixel(10, -36) })
|
|
|
|
|
+ } else if (m.label) {
|
|
|
|
|
+ m.label.content = content
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 清空所有 loop 图形(用于删除区域)
|
|
|
|
|
+ clearLoopGraphics() {
|
|
|
|
|
+ if (this.loopMarkers && this.loopMarkers.length) {
|
|
|
|
|
+ this.loopMarkers.forEach(m => {
|
|
|
|
|
+ try { m.setMap(null) } catch (e) {}
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ this.loopMarkers = []
|
|
|
|
|
+ if (this.loopPolygon && typeof this.loopPolygon.setMap === 'function') {
|
|
|
|
|
+ try { this.loopPolygon.setMap(null) } catch (e) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ this.loopPolygon = null
|
|
|
|
|
+ this.loopReplaceIndex = null
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 删除某一个点(根据需求:删除后清空整个区域)
|
|
|
|
|
+ deleteAreaPoints() {
|
|
|
|
|
+ this.jobState.areaPoints = []
|
|
|
|
|
+ this.clearLoopGraphics()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 辅助显示
|
|
|
|
|
+ formatPoint(p) {
|
|
|
|
|
+ if (!p) return '--'
|
|
|
|
|
+ return `${p.lng.toFixed(5)}, ${p.lat.toFixed(5)}`
|
|
|
|
|
+ },
|
|
|
|
|
+ formatPointTime(ts) {
|
|
|
|
|
+ if (!ts) return ''
|
|
|
|
|
+ const d = new Date(ts)
|
|
|
|
|
+ const h = `${d.getHours()}`.padStart(2, '0')
|
|
|
|
|
+ const m = `${d.getMinutes()}`.padStart(2, '0')
|
|
|
|
|
+ const s = `${d.getSeconds()}`.padStart(2, '0')
|
|
|
|
|
+ return `${h}:${m}:${s}`
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加障碍物标记
|
|
|
|
|
+ addObstacleMarker(point, obstacleGroupIndex, pointIndex) {
|
|
|
|
|
+ if (!this.map || !this.amapLoaded || typeof AMap === 'undefined') return
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const pos = [point.lng, point.lat]
|
|
|
|
|
+
|
|
|
|
|
+ // 创建障碍物图标(使用不同颜色区分)
|
|
|
|
|
+ let icon = null
|
|
|
|
|
+ try {
|
|
|
|
|
+ icon = new AMap.Icon({
|
|
|
|
|
+ image: "static/icons/poi-marker-default.png",
|
|
|
|
|
+ size: new AMap.Size(16, 22),
|
|
|
|
|
+ imageSize: new AMap.Size(16, 22)
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ icon = "static/icons/poi-marker-default.png"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const marker = new AMap.Marker({
|
|
|
|
|
+ position: pos,
|
|
|
|
|
+ map: this.map,
|
|
|
|
|
+ icon,
|
|
|
|
|
+ offset: new AMap.Pixel(-8, -22)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 添加标签显示障碍物编号
|
|
|
|
|
+ try {
|
|
|
|
|
+ const labelContent = `<div class="obstacle-marker-label">O${obstacleGroupIndex + 1}-${pointIndex + 1}</div>`
|
|
|
|
|
+ if (typeof marker.setLabel === 'function') {
|
|
|
|
|
+ marker.setLabel({
|
|
|
|
|
+ content: labelContent,
|
|
|
|
|
+ offset: new AMap.Pixel(8, -28)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+
|
|
|
|
|
+ // 保存标记到数组
|
|
|
|
|
+ if (!this.obstacleMarkers[obstacleGroupIndex]) {
|
|
|
|
|
+ this.obstacleMarkers[obstacleGroupIndex] = []
|
|
|
|
|
+ }
|
|
|
|
|
+ this.obstacleMarkers[obstacleGroupIndex].push(marker)
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.warn('[job-create] add obstacle marker failed', err)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 删除最后一个障碍物标记
|
|
|
|
|
+ removeLastObstacleMarker() {
|
|
|
|
|
+ const currentGroupIndex = this.jobState.obstacles.length
|
|
|
|
|
+ if (this.obstacleMarkers[currentGroupIndex] && this.obstacleMarkers[currentGroupIndex].length > 0) {
|
|
|
|
|
+ const marker = this.obstacleMarkers[currentGroupIndex].pop()
|
|
|
|
|
+ if (marker && typeof marker.setMap === 'function') {
|
|
|
|
|
+ try {
|
|
|
|
|
+ marker.setMap(null)
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 更新返航点标记
|
|
|
|
|
+ updateReturnMarker(point) {
|
|
|
|
|
+ if (!this.map || !this.amapLoaded || typeof AMap === 'undefined') return
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const pos = [point.lng, point.lat]
|
|
|
|
|
+
|
|
|
|
|
+ // 如果已存在返航点标记,先删除
|
|
|
|
|
+ this.clearReturnMarker()
|
|
|
|
|
+
|
|
|
|
|
+ // 创建返航点图标(使用特殊颜色)
|
|
|
|
|
+ let icon = null
|
|
|
|
|
+ try {
|
|
|
|
|
+ icon = new AMap.Icon({
|
|
|
|
|
+ image: "static/icons/poi-marker-default.png",
|
|
|
|
|
+ size: new AMap.Size(20, 28),
|
|
|
|
|
+ imageSize: new AMap.Size(20, 28)
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ icon = "static/icons/poi-marker-default.png"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.returnMarker = new AMap.Marker({
|
|
|
|
|
+ position: pos,
|
|
|
|
|
+ map: this.map,
|
|
|
|
|
+ icon,
|
|
|
|
|
+ offset: new AMap.Pixel(-10, -28)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 添加标签
|
|
|
|
|
+ try {
|
|
|
|
|
+ const labelContent = '<div class="return-marker-label">返航点</div>'
|
|
|
|
|
+ if (typeof this.returnMarker.setLabel === 'function') {
|
|
|
|
|
+ this.returnMarker.setLabel({
|
|
|
|
|
+ content: labelContent,
|
|
|
|
|
+ offset: new AMap.Pixel(10, -36)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.warn('[job-create] update return marker failed', err)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 清除返航点标记
|
|
|
|
|
+ clearReturnMarker() {
|
|
|
|
|
+ if (this.returnMarker && typeof this.returnMarker.setMap === 'function') {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.returnMarker.setMap(null)
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ this.returnMarker = null
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 清理所有标记(页面卸载时调用)
|
|
|
|
|
+ clearAllMarkers() {
|
|
|
|
|
+ // 清理作业区域标记
|
|
|
|
|
+ if (this.loopMarkers && this.loopMarkers.length) {
|
|
|
|
|
+ this.loopMarkers.forEach(m => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (m && typeof m.setMap === 'function') {
|
|
|
|
|
+ m.setMap(null)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ })
|
|
|
|
|
+ this.loopMarkers = []
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 清理障碍物标记
|
|
|
|
|
+ if (this.obstacleMarkers && this.obstacleMarkers.length) {
|
|
|
|
|
+ this.obstacleMarkers.forEach(group => {
|
|
|
|
|
+ if (group && group.length) {
|
|
|
|
|
+ group.forEach(m => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (m && typeof m.setMap === 'function') {
|
|
|
|
|
+ m.setMap(null)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ this.obstacleMarkers = []
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 清理返航点标记
|
|
|
|
|
+ this.clearReturnMarker()
|
|
|
|
|
+
|
|
|
|
|
+ // 清理实时位置标记
|
|
|
|
|
+ if (this.realtimeMarker && typeof this.realtimeMarker.setMap === 'function') {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.realtimeMarker.setMap(null)
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ this.realtimeMarker = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 清理多边形
|
|
|
|
|
+ if (this.loopPolygon && typeof this.loopPolygon.setMap === 'function') {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.loopPolygon.setMap(null)
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ this.loopPolygon = null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.page {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ background-color: #f6f9f7;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.header {
|
|
|
|
|
+ padding: 24rpx 28rpx 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.title-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: baseline;
|
|
|
|
|
+ margin-bottom: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.title {
|
|
|
|
|
+ font-size: 34rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #2c3e50;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.device-id {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #8c9396;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ padding: 10rpx 8rpx;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ background-color: #eef6f0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-item {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-index {
|
|
|
|
|
+ width: 36rpx;
|
|
|
|
|
+ height: 36rpx;
|
|
|
|
|
+ border-radius: 18rpx;
|
|
|
|
|
+ border: 2rpx solid #b0c4b8;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #7f8c8d;
|
|
|
|
|
+ margin-right: 10rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-index.active {
|
|
|
|
|
+ background: linear-gradient(135deg, #3bb44a, #66cc6a);
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ border-color: transparent;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-index.done {
|
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
|
+ color: #3bb44a;
|
|
|
|
|
+ border-color: #3bb44a;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-texts {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-title {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #2c3e50;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-sub {
|
|
|
|
|
+ font-size: 20rpx;
|
|
|
|
|
+ color: #8c9396;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ padding: 10rpx 24rpx 130rpx;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-block {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 18rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 区域类型 & 路线类型选择卡片 */
|
|
|
|
|
+.card.select-card {
|
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
|
+ border-radius: 20rpx;
|
|
|
|
|
+ padding: 20rpx 22rpx 16rpx;
|
|
|
|
|
+ box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.03);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.select-title {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #2c3e50;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.select-sub {
|
|
|
|
|
+ margin-top: 6rpx;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #8c9396;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.select-tabs {
|
|
|
|
|
+ margin-top: 14rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.select-tab {
|
|
|
|
|
+ min-width: 46%;
|
|
|
|
|
+ padding: 14rpx 16rpx;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ background-color: #f3f5f7;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.select-tab.small {
|
|
|
|
|
+ min-width: 30%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.select-tab-label {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #2c3e50;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.select-tab-sub {
|
|
|
|
|
+ margin-top: 4rpx;
|
|
|
|
|
+ font-size: 20rpx;
|
|
|
|
|
+ color: #8c9396;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.select-tab.active {
|
|
|
|
|
+ background: linear-gradient(135deg, #3bb44a, #66cc6a);
|
|
|
|
|
+ box-shadow: 0 4rpx 12rpx rgba(59, 180, 74, 0.26);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.select-tab.active .select-tab-label,
|
|
|
|
|
+.select-tab.active .select-tab-sub {
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-card {
|
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
|
+ border-radius: 20rpx;
|
|
|
|
|
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-card.small .map-body {
|
|
|
|
|
+ height: 260rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-header {
|
|
|
|
|
+ padding: 20rpx 22rpx 10rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-header-actions {
|
|
|
|
|
+ margin-left: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-title {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #2c3e50;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-sub {
|
|
|
|
|
+ margin-top: 4rpx;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #8c9396;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-body {
|
|
|
|
|
+ height: 320rpx;
|
|
|
|
|
+ background-color: #d3dce6;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-placeholder {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ white-space: pre-line;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* AMap 容器样式(确保占满 map-body) */
|
|
|
|
|
+#mapContainer {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-footer {
|
|
|
|
|
+ padding: 12rpx 18rpx 16rpx;
|
|
|
|
|
+ background-color: #f7faf8;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-hint {
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #8c9396;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mode-card {
|
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
|
+ border-radius: 20rpx;
|
|
|
|
|
+ padding: 18rpx 20rpx 14rpx;
|
|
|
|
|
+ box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.03);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mode-title-row {
|
|
|
|
|
+ margin-bottom: 10rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mode-title {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #2c3e50;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mode-sub {
|
|
|
|
|
+ margin-top: 4rpx;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #8c9396;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mode-tabs {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mode-tab {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ padding: 12rpx 0;
|
|
|
|
|
+ border-radius: 30rpx;
|
|
|
|
|
+ background-color: #f3f5f7;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mode-tab.active {
|
|
|
|
|
+ background: linear-gradient(135deg, #3bb44a, #66cc6a);
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ box-shadow: 0 4rpx 12rpx rgba(59, 180, 74, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-card {
|
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
|
+ border-radius: 20rpx;
|
|
|
|
|
+ padding: 18rpx 20rpx 14rpx;
|
|
|
|
|
+ box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.03);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-row.center {
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-desc {
|
|
|
|
|
+ margin-top: 8rpx;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #8c9396;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.list-card {
|
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
|
+ border-radius: 20rpx;
|
|
|
|
|
+ padding: 18rpx 20rpx 10rpx;
|
|
|
|
|
+ box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.03);
|
|
|
|
|
+ max-height: 420rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.list-title-row {
|
|
|
|
|
+ margin-bottom: 6rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.list-title {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #2c3e50;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.list-scroll {
|
|
|
|
|
+ max-height: 360rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.list-group {
|
|
|
|
|
+ margin-top: 10rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.list-group-header {
|
|
|
|
|
+ margin-bottom: 4rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.list-group-title {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #555;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.point-list {
|
|
|
|
|
+ margin-top: 4rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.point-item {
|
|
|
|
|
+ padding: 8rpx 4rpx;
|
|
|
|
|
+ border-bottom: 1rpx solid #f0f0f0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.point-item.small {
|
|
|
|
|
+ padding-left: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.point-item.active {
|
|
|
|
|
+ background-color: #f0f9f2;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.point-label {
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #3bb44a;
|
|
|
|
|
+ margin-right: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.point-coord {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.point-time {
|
|
|
|
|
+ font-size: 20rpx;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.obstacle-item {
|
|
|
|
|
+ margin-top: 6rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.obstacle-title {
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ margin-bottom: 2rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty-row {
|
|
|
|
|
+ padding: 12rpx 0;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.start-index {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.start-index-text {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #2c3e50;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.card.confirm-card {
|
|
|
|
|
+ padding: 22rpx 24rpx 10rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.confirm-title {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #2c3e50;
|
|
|
|
|
+ margin-bottom: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-item {
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.form-item.required .label::after {
|
|
|
|
|
+ content: '*';
|
|
|
|
|
+ color: #ff4d4f;
|
|
|
|
|
+ margin-left: 4rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.label {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #555;
|
|
|
|
|
+ margin-bottom: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.input {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 80rpx;
|
|
|
|
|
+ line-height: 80rpx;
|
|
|
|
|
+ padding: 14rpx 18rpx;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ background-color: #f7f7f7;
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.summary-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 8rpx 0;
|
|
|
|
|
+ border-bottom: 1rpx solid #f0f0f0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.summary-label {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #777;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.summary-value {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tips-card {
|
|
|
|
|
+ padding: 20rpx 22rpx 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tips-title {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #2c3e50;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ margin-bottom: 6rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tips-content text {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ margin-top: 4rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.footer {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ padding: 12rpx 26rpx 24rpx;
|
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
|
+ box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ height: 84rpx;
|
|
|
|
|
+ line-height: 84rpx;
|
|
|
|
|
+ border-radius: 42rpx;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn.primary {
|
|
|
|
|
+ background: linear-gradient(135deg, #3bb44a, #66cc6a);
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn.ghost {
|
|
|
|
|
+ background-color: #f4f5f7;
|
|
|
|
|
+ color: #555;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn:disabled {
|
|
|
|
|
+ opacity: 0.5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn-location {
|
|
|
|
|
+ padding: 8rpx 16rpx;
|
|
|
|
|
+ border-radius: 20rpx;
|
|
|
|
|
+ background-color: #f0f9f2;
|
|
|
|
|
+ border: 1rpx solid #3bb44a;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #3bb44a;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ min-width: 120rpx;
|
|
|
|
|
+ height: 56rpx;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.btn-location-text {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #3bb44a;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 任务标题行新增作业按钮样式(与详情页保持风格一致) */
|
|
|
|
|
+.task-title-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-title-left {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-title-text {
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #2c3e50;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-title-sub {
|
|
|
|
|
+ margin-top: 4rpx;
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #8c9396;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-add-btn {
|
|
|
|
|
+ width: 60rpx;
|
|
|
|
|
+ height: 60rpx;
|
|
|
|
|
+ border-radius: 30rpx;
|
|
|
|
|
+ background: linear-gradient(135deg, #3bb44a, #66cc6a);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ box-shadow: 0 4rpx 12rpx rgba(59, 180, 74, 0.3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-add-plus {
|
|
|
|
|
+ font-size: 40rpx;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.p-marker-label {
|
|
|
|
|
+ background: rgba(59,180,74,0.95);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 18rpx;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.obstacle-marker-label {
|
|
|
|
|
+ background: rgba(255, 152, 0, 0.95);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 16rpx;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.return-marker-label {
|
|
|
|
|
+ background: rgba(33, 150, 243, 0.95);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 18rpx;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|
|
|
|
|
+
|
|
|
|
|
+
|