| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221 |
- <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 setup>
- import { ref, reactive, computed, nextTick } from 'vue'
- import { onLoad, onReady, onUnload } from '@dcloudio/uni-app'
- import { createJob, getRealtimeData } from '@/api/services/job.js'
- import coordinateUtils from '@/utils/coordinateUtils.js'
- // ==================== Reactive State ====================
- // Simple values
- const currentStep = ref(1)
- const selectedAreaType = ref('loopArea')
- const selectedRouteType = ref('loop')
- const mode = ref('area')
- const submitting = ref(false)
- const locating = ref(false)
- const map = ref(null)
- const amapLoaded = ref(false)
- const geolocation = ref(null)
- const realtimeMarker = ref(null)
- const realtimeTimer = ref(null)
- const lastReportTime = ref(null)
- const latestRealtimeLngLat = ref(null)
- const polling = ref(false)
- const loopPolygon = ref(null)
- const loopReplaceIndex = ref(null)
- const areaPolygon = ref(null)
- const returnMarker = ref(null)
- const amapKey = ref('9f2cac7ea18905dd3830cf7360a43a35')
- const jscode = ref('41af52e416d1fd1b15020dac066cec86')
- // Arrays
- const currentObstacle = ref([])
- const markers = ref([])
- const obstacleMarkers = ref([])
- const loopMarkers = ref([])
- // Complex state object
- const jobState = reactive({
- machineCode: '',
- machineId: '',
- areaType: 'loopArea',
- routeType: 'loop',
- areaPoints: [],
- obstacles: [],
- returnPoint: null,
- startPointIndex: 0,
- jobName: '',
- fieldId: '',
- pathWidth: 100
- })
- // Constants
- const steps = ref([
- { id: 1, title: '区域与路线', sub: '选择作业区域类型与路线类型' },
- { id: 2, title: '打点建模', sub: '作业区域 / 障碍物 / 返航点' },
- { id: 3, title: '选择起点', sub: '确定作业起点位置' },
- { id: 4, title: '信息确认', sub: '填写作业名称并提交' }
- ])
- const areaTypes = ref([
- {
- 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)' }]
- }
- ])
- const modes = ref([
- { value: 'area', label: '作业区域点' },
- { value: 'obstacle', label: '障碍物点' },
- { value: 'return', label: '返航点' }
- ])
- // ==================== Computed Properties ====================
- const modeLabel = computed(() => {
- const m = modes.value.find(m => m.value === mode.value)
- return m ? m.label : ''
- })
- const canUndo = computed(() => {
- if (mode.value === 'area') {
- return jobState.areaPoints.length > 0
- }
- if (mode.value === 'obstacle') {
- return currentObstacle.value.length > 0
- }
- if (mode.value === 'return') {
- return !!jobState.returnPoint
- }
- return false
- })
- const canChangeStart = computed(() => {
- return jobState.areaPoints.length > 0
- })
- const currentStartDisplay = computed(() => {
- if (!jobState.areaPoints.length) return '-'
- return jobState.startPointIndex + 1
- })
- const areaTypeLabel = computed(() => {
- const a = areaTypes.value.find(a => a.value === jobState.areaType)
- return a ? a.label : ''
- })
- const availableRouteTypes = computed(() => {
- const a = areaTypes.value.find(a => a.value === selectedAreaType.value)
- return a ? a.routes : []
- })
- const routeTypeLabel = computed(() => {
- const list = areaTypes.value.reduce((acc, cur) => {
- if (cur.routes && cur.routes.length) {
- acc.push(...cur.routes)
- }
- return acc
- }, [])
- const r = list.find(r => r.value === jobState.routeType)
- return r ? r.label : jobState.routeType
- })
- // ==================== Helper Functions ====================
- // Simple mock function for current point
- 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
- }
- }
- // ==================== Lifecycle Hooks ====================
- onLoad((options) => {
- const { machineCode, id } = options || {}
- if (machineCode) {
- jobState.machineCode = machineCode
- jobState.machineId = id
- }
- })
- onReady(() => {
- if (typeof window !== 'undefined' && typeof document !== 'undefined') {
- console.log('[job-create] onReady - loading AMap script')
- loadScript()
- }
- })
- onUnload(() => {
- clearRealtimePolling()
- clearAllMarkers()
- })
- // ==================== Map Functions ====================
- const loadScript = () => {
- // #ifdef H5
- window.mapInit = () => {
- _createMapWhenReady()
- }
- const script = document.createElement('script')
- script.src = `https://webapi.amap.com/maps?v=2.0&key=${amapKey.value}&callback=mapInit`
- document.body.appendChild(script)
- amapLoaded.value = true
- // #endif
- // #ifndef H5
- console.warn('当前平台不支持动态加载高德地图脚本,请使用 uni-app 地图组件')
- // #endif
- }
- const _createMapWhenReady = () => {
- const createWhenReady = () => {
- // #ifdef H5
- const container = document.getElementById('mapContainer')
- if (!container) {
- setTimeout(createWhenReady, 200)
- return
- }
- // #endif
- const defaultCenter = [113.382, 22.5211]
- const createMapWithCenter = centerArr => {
- try {
- map.value = 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()
- map.value.add(sat)
- }
- } catch (layerErr) {
- console.warn('[job-create] satellite layer failed', layerErr)
- }
- try {
- AMap.plugin('AMap.ToolBar', () => {
- const toolbar = new AMap.ToolBar()
- if (map.value && typeof map.value.addControl === 'function') {
- map.value.addControl(toolbar)
- }
- })
- } catch (pluginErr) {
- console.warn('[job-create] toolbar plugin failed', pluginErr)
- }
- AMap.plugin('AMap.Geolocation', () => {
- geolocation.value = new AMap.Geolocation({
- enableHighAccuracy: true,
- timeout: 10000,
- maximumAge: 0,
- convert: true,
- showButton: false,
- showMarker: false,
- showCircle: false,
- panToLocation: false,
- zoomToAccuracy: false,
- noIpLocate: 0,
- GeoLocationFirst: true
- })
- console.log('[job-create] 高德定位插件加载完成')
-
- tryAutoLocation()
- })
- if (map.value && typeof map.value.on === 'function') {
- map.value.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
- onMapClick({ lng, lat })
- })
- }
- setupRealtimePolling()
- }
- console.log('[job-create] 创建地图使用默认中心点:', defaultCenter)
- createMapWithCenter(defaultCenter)
- }
- createWhenReady()
- }
- const tryAutoLocation = () => {
- console.log('[job-create] 开始自动定位...')
-
- _getCurrentLocation(
- res => {
- console.log('[job-create] 自动定位成功:', res)
- const { longitude, latitude } = res || {}
- if (longitude != null && latitude != null) {
- console.log('[job-create] 使用定位坐标:', [longitude, latitude])
- _centerMapToLngLat(longitude, latitude, 16)
- uni.showToast({
- title: '已定位到您的位置',
- icon: 'success',
- duration: 2000
- })
- } else {
- console.warn('[job-create] 定位返回坐标无效')
- }
- },
- err => {
- console.warn('[job-create] 自动定位失败:', err)
- }
- )
- }
- const _getDeviceLocationAndCenter = () => {
- const doCenter = (lng, lat) => {
- if (!lng && lng !== 0) return
- if (!lat && lat !== 0) return
- _centerMapToLngLat(lng, lat, 18)
- try {
- if (map.value && typeof AMap !== 'undefined' && AMap.Marker) {
- new AMap.Marker({
- position: [lng, lat],
- map: map.value
- })
- }
- } catch (err) {
- console.warn('[job-create] add marker failed', err)
- }
- }
- _getCurrentLocation(
- res => {
- console.log("当前定位", res)
- const { longitude, latitude } = res || {}
- if (longitude != null && latitude != null) {
- doCenter(longitude, latitude)
- } else {
- const p = getMapCenterPoint() || mockCurrentPoint(0)
- doCenter(p.lng, p.lat)
- }
- },
- () => {
- const p = getMapCenterPoint() || mockCurrentPoint(0)
- doCenter(p.lng, p.lat)
- }
- )
- const p = getMapCenterPoint() || mockCurrentPoint(0)
- doCenter(p.lng, p.lat)
- }
- const _centerMapToLngLat = (lng, lat, zoom = 18) => {
- if (!map.value) return
- try {
- if (typeof map.value.setCenter === 'function') {
- map.value.setCenter([lng, lat])
- }
- if (typeof map.value.setZoom === 'function' && typeof zoom === 'number') {
- map.value.setZoom(zoom)
- }
- } catch (err) {
- console.warn('[job-create] center map failed', err)
- }
- }
- const _getCurrentLocation = (successCallback, failCallback, retryCount = 0) => {
- if (!geolocation.value) {
- if (retryCount < 5) {
- console.log(`[job-create] 高德定位未初始化,${retryCount + 1}秒后重试...`)
- setTimeout(() => {
- _getCurrentLocation(successCallback, failCallback, retryCount + 1)
- }, 1000)
- return
- } else {
- console.warn('[job-create] 高德定位未初始化,重试失败')
- if (failCallback) failCallback(new Error('高德定位未初始化'))
- return
- }
- }
- console.log('[job-create] 开始调用高德定位 getCurrentPosition...')
-
- geolocation.value.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
- })
- }
- }
- })
- }
- const getMapCenterPoint = () => {
- if (map.value && typeof map.value.getCenter === 'function') {
- const c = map.value.getCenter()
- 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
- }
- // ==================== UI Interaction Functions ====================
- const selectAreaType = (val) => {
- selectedAreaType.value = val
- const a = areaTypes.value.find(item => item.value === val)
- if (a && a.routes && a.routes.length) {
- selectedRouteType.value = a.routes[0].value
- }
- jobState.areaType = selectedAreaType.value
- jobState.routeType = selectedRouteType.value
- }
- const selectRouteType = (val) => {
- selectedRouteType.value = val
- jobState.routeType = val
- }
- const switchMode = (val) => {
- mode.value = val
- }
- // ==================== Real-time Polling Functions ====================
- const setupRealtimePolling = () => {
- if (currentStep.value !== 2) return
- const deviceId = jobState.machineId || jobState.machineCode
- if (!deviceId) return
- if (!amapLoaded.value || !map.value) return
- if (realtimeTimer.value) return
- fetchRealtimeAndUpdate()
- realtimeTimer.value = setInterval(() => {
- fetchRealtimeAndUpdate()
- }, 3000)
- }
- const clearRealtimePolling = () => {
- if (realtimeTimer.value) {
- clearInterval(realtimeTimer.value)
- realtimeTimer.value = null
- }
- polling.value = false
- }
- const fetchRealtimeAndUpdate = async () => {
- try {
- const deviceId = jobState.machineCode
- if (!deviceId) return
- polling.value = 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 && lastReportTime.value && reportTime < lastReportTime.value) {
- return
- }
- if (reportTime) lastReportTime.value = reportTime
- const pt = payload.currentPoint
- if (!pt || pt.x == null || pt.y == null) return
- const lngLat = [pt.x, pt.y]
- latestRealtimeLngLat.value = lngLat
- updateRealtimeMarker(lngLat)
- try {
- if (map.value && typeof map.value.setCenter === 'function') {
- map.value.setCenter(lngLat)
- }
- } catch (e) {}
- } catch (e) {
- console.warn('[job-create] fetchRealtimeAndUpdate failed', e)
- } finally {
- polling.value = false
- }
- }
- const updateRealtimeMarker = (lngLat) => {
- if (!map.value || !amapLoaded.value) return
- if (!lngLat || lngLat.length !== 2) return
- if (!realtimeMarker.value) {
- realtimeMarker.value = new AMap.Marker({
- map: map.value,
- position: lngLat
- })
- } else {
- realtimeMarker.value.setPosition(lngLat)
- }
- }
- // ==================== Point Management Functions ====================
- const addPoint = () => {
- const realtimeLngLat = latestRealtimeLngLat.value
- const realtimePoint = realtimeLngLat && realtimeLngLat.length === 2
- ? { lng: realtimeLngLat[0], lat: realtimeLngLat[1], timestamp: Date.now() }
- : null
- let centerPoint = null
- if (amapLoaded.value && map.value) {
- centerPoint = getMapCenterPoint()
- }
- const fallbackPoint = centerPoint || mockCurrentPoint(0)
- const point = realtimePoint || fallbackPoint
- if (mode.value === 'area') {
- onMapClick({ lng: point.lng, lat: point.lat })
- } else if (mode.value === 'obstacle') {
- currentObstacle.value.push(point)
- addObstacleMarker(point, jobState.obstacles.length, currentObstacle.value.length - 1)
- } else if (mode.value === 'return') {
- if (jobState.returnPoint) {
- uni.showModal({
- title: '覆盖返航点',
- content: '已存在返航点,是否覆盖为当前设备位置?',
- success: res => {
- if (res.confirm) {
- jobState.returnPoint = point
- updateReturnMarker(point)
- }
- }
- })
- } else {
- jobState.returnPoint = point
- updateReturnMarker(point)
- }
- }
- }
- const undoPoint = () => {
- if (!canUndo.value) return
- if (mode.value === 'area') {
- const lastIdx = jobState.areaPoints.length - 1
- if (lastIdx >= 0) {
- jobState.areaPoints.pop()
- const m = loopMarkers.value.pop()
- if (m && typeof m.setMap === 'function') {
- try { m.setMap(null) } catch (e) {}
- }
- if (jobState.areaPoints.length >= 3) {
- recomputeLoopPolygon()
- } else {
- if (loopPolygon.value && typeof loopPolygon.value.setMap === 'function') {
- try { loopPolygon.value.setMap(null) } catch (e) {}
- }
- loopPolygon.value = null
- }
- }
- } else if (mode.value === 'obstacle') {
- currentObstacle.value.pop()
- removeLastObstacleMarker()
- } else if (mode.value === 'return') {
- jobState.returnPoint = null
- clearReturnMarker()
- }
- }
- const finishCurrentObstacle = () => {
- if (!currentObstacle.value.length) return
- jobState.obstacles.push([...currentObstacle.value])
- currentObstacle.value = []
- uni.showToast({
- title: '已保存一组障碍物',
- icon: 'success'
- })
- }
- const flushCurrentObstacle = () => {
- if (currentObstacle.value && currentObstacle.value.length) {
- jobState.obstacles.push([...currentObstacle.value])
- currentObstacle.value = []
- }
- }
- // ==================== Step Navigation Functions ====================
- const prevStep = () => {
- if (currentStep.value === 1) return
- currentStep.value -= 1
- if (currentStep.value !== 2) {
- clearRealtimePolling()
- }
- }
- const nextStep = () => {
- if (currentStep.value === 1) {
- if (!selectedAreaType.value || !selectedRouteType.value) {
- uni.showToast({
- title: '请选择作业区域类型和路线类型',
- icon: 'none'
- })
- return
- }
- jobState.areaType = selectedAreaType.value
- jobState.routeType = selectedRouteType.value
- }
-
- if (currentStep.value === 2) {
- if (jobState.areaPoints.length < 3) {
- uni.showToast({
- title: '作业区域点至少需要 3 个',
- icon: 'none'
- })
- return
- }
- flushCurrentObstacle()
- if (!jobState.returnPoint) {
- uni.showToast({
- title: '请先设置返航点',
- icon: 'none'
- })
- return
- }
- }
-
- if (currentStep.value === 3) {
- if (!jobState.areaPoints.length) {
- uni.showToast({
- title: '请先在上一步记录作业区域点',
- icon: 'none'
- })
- return
- }
- }
-
- if (currentStep.value < 4) {
- currentStep.value += 1
- if (currentStep.value === 2) {
- setupRealtimePolling()
- } else {
- clearRealtimePolling()
- }
- }
- }
- // ==================== Start Point Selection Functions ====================
- const setStartIndex = (index) => {
- if (!jobState.areaPoints.length) return
- jobState.startPointIndex = index
- }
- const prevStart = () => {
- if (!canChangeStart.value) return
- const total = jobState.areaPoints.length
- jobState.startPointIndex =
- (jobState.startPointIndex - 1 + total) % total
- }
- const nextStart = () => {
- if (!canChangeStart.value) return
- const total = jobState.areaPoints.length
- jobState.startPointIndex =
- (jobState.startPointIndex + 1) % total
- }
- // ==================== Manual Location Function ====================
- const manualLocation = async () => {
- if (locating.value) return
- locating.value = true
- const doCenter = (lng, lat) => {
- if (!lng && lng !== 0) return
- if (!lat && lat !== 0) return
- _centerMapToLngLat(lng, lat, 18)
- uni.showToast({
- title: '已定位到您的位置',
- icon: 'success',
- duration: 2000
- })
- }
- try {
- 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) => {
- _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 {
- locating.value = false
- }
- }
- // ==================== Submit Job Function ====================
- const submitJob = async () => {
- if (!jobState.jobName) {
- uni.showToast({
- title: '请填写作业名称',
- icon: 'none'
- })
- return
- }
- if (!jobState.fieldId) {
- uni.showToast({
- title: '请填写地块ID',
- icon: 'none'
- })
- return
- }
- if (!jobState.pathWidth) {
- uni.showToast({
- title: '请填写路径宽度',
- icon: 'none'
- })
- return
- }
- if (jobState.areaPoints.length < 3) {
- uni.showToast({
- title: '作业区域点至少需要 3 个',
- icon: 'none'
- })
- return
- }
- flushCurrentObstacle()
- const convertedAreaPoints = coordinateUtils.convertPointsToWgs84(jobState.areaPoints)
- const convertedObstacles = jobState.obstacles.map(obstacleGroup =>
- coordinateUtils.convertPointsToWgs84(obstacleGroup)
- )
- const convertedReturnPoint = jobState.returnPoint ?
- coordinateUtils.convertPointToWgs84(jobState.returnPoint) : undefined
- const areaTypeMap = {
- 'loopArea': 1,
- 'bowArea': 2,
- 'customArea': 3,
- 'ridgeArea': 4
- }
-
- console.log("jobState", jobState)
-
- const payload = {
- deviceId: jobState.machineId,
- fieldId: parseInt(jobState.fieldId),
- taskName: jobState.jobName,
- areaType: areaTypeMap[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(jobState.pathWidth)
- }
-
- console.log("payload", payload)
- submitting.value = 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 {
- submitting.value = false
- uni.hideLoading()
- }
- }
- // ==================== Map Click Handler ====================
- const onMapClick = ({ lng, lat } = {}) => {
- if (!lng && lng !== 0) return
- if (!lat && lat !== 0) return
-
- if (mode.value !== 'area') return
- const newPoint = { lng, lat, timestamp: Date.now() }
- if (loopReplaceIndex.value != null && loopReplaceIndex.value >= 0 && loopReplaceIndex.value < jobState.areaPoints.length) {
- jobState.areaPoints.splice(loopReplaceIndex.value, 1, newPoint)
- const m = loopMarkers.value[loopReplaceIndex.value]
- if (m && typeof m.setPosition === 'function') {
- try { m.setPosition([lng, lat]) } catch (e) {}
- }
- loopReplaceIndex.value = null
- if (jobState.areaPoints.length >= 3) recomputeLoopPolygon()
- return
- }
-
- jobState.areaPoints.push(newPoint)
- const idx = jobState.areaPoints.length - 1
- createOrUpdateLoopMarker(idx, newPoint)
- if (jobState.areaPoints.length >= 3) {
- recomputeLoopPolygon()
- }
- }
- const createOrUpdateLoopMarker = (index, point) => {
- if (!map.value || typeof AMap === 'undefined') return
- const pos = [point.lng, point.lat]
- let marker = loopMarkers.value[index]
- if (marker) {
- try { marker.setPosition(pos) } catch (e) {}
- return
- }
-
- 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: map.value,
- draggable: true,
- icon,
- offset: new AMap.Pixel(-10, -28)
- })
-
- 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) {}
-
- 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
- jobState.areaPoints[index] = { lng: lngv, lat: latv, timestamp: Date.now() }
- if (jobState.areaPoints.length >= 3) recomputeLoopPolygon()
- })
-
- marker.on('click', () => {
- loopReplaceIndex.value = index
- uni.showToast({ title: `已选中 P${index + 1},下一次点击将替换该点`, icon: 'none' })
- })
-
- loopMarkers.value[index] = marker
- refreshLoopMarkerLabels()
- }
- const recomputeLoopPolygon = () => {
- if (!map.value) return
- if (!jobState.areaPoints || jobState.areaPoints.length < 3) {
- if (loopPolygon.value && typeof loopPolygon.value.setMap === 'function') {
- try { loopPolygon.value.setMap(null) } catch (e) {}
- }
- loopPolygon.value = null
- return
- }
- const outer = jobState.areaPoints.map(p => [p.lng, p.lat])
- try {
- if (loopPolygon.value) {
- loopPolygon.value.setPath(outer)
- } else {
- loopPolygon.value = new AMap.Polygon({
- map: map.value,
- path: outer,
- strokeColor: '#3bb44a',
- strokeWeight: 2,
- fillColor: '#3bb44a',
- fillOpacity: 0.15
- })
- }
- } catch (err) {
- console.warn('[job-create] recompute loop polygon failed', err)
- }
- }
- const refreshLoopMarkerLabels = () => {
- if (!loopMarkers.value || !loopMarkers.value.length) return
- loopMarkers.value.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) {}
- })
- }
- const clearLoopGraphics = () => {
- if (loopMarkers.value && loopMarkers.value.length) {
- loopMarkers.value.forEach(m => {
- try { m.setMap(null) } catch (e) {}
- })
- }
- loopMarkers.value = []
- if (loopPolygon.value && typeof loopPolygon.value.setMap === 'function') {
- try { loopPolygon.value.setMap(null) } catch (e) {}
- }
- loopPolygon.value = null
- loopReplaceIndex.value = null
- }
- const deleteAreaPoints = () => {
- jobState.areaPoints = []
- clearLoopGraphics()
- }
- // ==================== Formatting Functions ====================
- const formatPoint = (p) => {
- if (!p) return '--'
- return `${p.lng.toFixed(5)}, ${p.lat.toFixed(5)}`
- }
- const 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}`
- }
- // ==================== Obstacle Marker Functions ====================
- const addObstacleMarker = (point, obstacleGroupIndex, pointIndex) => {
- if (!map.value || !amapLoaded.value || 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: map.value,
- 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 (!obstacleMarkers.value[obstacleGroupIndex]) {
- obstacleMarkers.value[obstacleGroupIndex] = []
- }
- obstacleMarkers.value[obstacleGroupIndex].push(marker)
- } catch (err) {
- console.warn('[job-create] add obstacle marker failed', err)
- }
- }
- const removeLastObstacleMarker = () => {
- const currentGroupIndex = jobState.obstacles.length
- if (obstacleMarkers.value[currentGroupIndex] && obstacleMarkers.value[currentGroupIndex].length > 0) {
- const marker = obstacleMarkers.value[currentGroupIndex].pop()
- if (marker && typeof marker.setMap === 'function') {
- try {
- marker.setMap(null)
- } catch (e) {}
- }
- }
- }
- // ==================== Return Marker Functions ====================
- const updateReturnMarker = (point) => {
- if (!map.value || !amapLoaded.value || typeof AMap === 'undefined') return
-
- try {
- const pos = [point.lng, point.lat]
-
- 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"
- }
- returnMarker.value = new AMap.Marker({
- position: pos,
- map: map.value,
- icon,
- offset: new AMap.Pixel(-10, -28)
- })
- try {
- const labelContent = '<div class="return-marker-label">返航点</div>'
- if (typeof returnMarker.value.setLabel === 'function') {
- returnMarker.value.setLabel({
- content: labelContent,
- offset: new AMap.Pixel(10, -36)
- })
- }
- } catch (e) {}
- } catch (err) {
- console.warn('[job-create] update return marker failed', err)
- }
- }
- const clearReturnMarker = () => {
- if (returnMarker.value && typeof returnMarker.value.setMap === 'function') {
- try {
- returnMarker.value.setMap(null)
- } catch (e) {}
- }
- returnMarker.value = null
- }
- // ==================== Cleanup Functions ====================
- const clearAllMarkers = () => {
- if (loopMarkers.value && loopMarkers.value.length) {
- loopMarkers.value.forEach(m => {
- try {
- if (m && typeof m.setMap === 'function') {
- m.setMap(null)
- }
- } catch (e) {}
- })
- loopMarkers.value = []
- }
- if (obstacleMarkers.value && obstacleMarkers.value.length) {
- obstacleMarkers.value.forEach(group => {
- if (group && group.length) {
- group.forEach(m => {
- try {
- if (m && typeof m.setMap === 'function') {
- m.setMap(null)
- }
- } catch (e) {}
- })
- }
- })
- obstacleMarkers.value = []
- }
- clearReturnMarker()
- if (realtimeMarker.value && typeof realtimeMarker.value.setMap === 'function') {
- try {
- realtimeMarker.value.setMap(null)
- } catch (e) {}
- realtimeMarker.value = null
- }
- if (loopPolygon.value && typeof loopPolygon.value.setMap === 'function') {
- try {
- loopPolygon.value.setMap(null)
- } catch (e) {}
- loopPolygon.value = 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>
|