navigation.vue 106 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743
  1. <template>
  2. <div class="navigation-container">
  3. <div class="map-stage" ref="mapStage">
  4. <div class="main-content">
  5. <!-- 当前操作类型标记 -->
  6. <div class="hand-ment-mark">
  7. <el-tag type="danger" effect="dark" size="mini" v-if="nowHandMenu">{{ nowHandMenu }}</el-tag>
  8. </div>
  9. <!-- 地图组件 - 完全保持原样 -->
  10. <OlMap ref="olmap" :width="olWidth + 'px'" :height="olHeight + 'px'" backgroundColor="#F5F5F5" :mapName="mapName"
  11. :pointSwitch="settingParams.pointId" :baseLayerShow="settingParams.baseMap" :robotPoseData="laserPositionData"
  12. :pointSelectionEnabled="selectPointMode" :poseInitEnable="initPoseMode" @addNowPoint="addNowPoint"
  13. :isRobotFollow="settingParams.follow" @initNavigationResult="initNavigationResult"
  14. :showDefaultControls="false"></OlMap>
  15. <!-- 左侧工具条浮层 -->
  16. <MapToolbar
  17. class="nav-toolbar"
  18. preset="nav"
  19. :selectedKey="selectedKey"
  20. :hasRobotPosition="true"
  21. :isConnected="true"
  22. :isBusy="false"
  23. :isFullscreen="isFullscreen"
  24. @zoom-in="onZoomIn"
  25. @zoom-out="onZoomOut"
  26. @center-robot="onCenterRobot"
  27. @toggle-fullscreen="onToggleFullscreen"
  28. @confirm-init="handleConfirmInit"
  29. @confirm-reboot="handleConfirmReboot"
  30. @confirm-stop="handleConfirmStop"
  31. />
  32. <!-- 右侧信息面板浮层 -->
  33. <RightPanel
  34. mode="nav"
  35. panelType="nav"
  36. :overlay="true"
  37. :visible.sync="rightVisible"
  38. :realtime-info="realtimeInfo"
  39. :waypoint-list="waypoints"
  40. :task-list="tasks"
  41. :setting-params="settingParams"
  42. :emergency-stop-enabled="emergencyStopEnabled"
  43. :navigation-stack-status="navigationStackStatus"
  44. @wp-select="onWpSelect"
  45. @wp-send="onWpSend"
  46. @wp-create="onWpCreate"
  47. @wp-edit="onWpEdit"
  48. @wp-remove="onWpRemove"
  49. @wp-move-up="onWpMoveUp"
  50. @wp-move-down="onWpMoveDown"
  51. @wp-batch-remove="onWpBatchRemove"
  52. @wp-goto="onWpGoto"
  53. @wp-goto-single="onWpGotoSingle"
  54. @wp-create-task="onWpCreateTask"
  55. @wp-selection-change="onWpSelectionChange"
  56. @map-select-mode-change="onMapSelectModeChange"
  57. @task-view="onTaskView"
  58. @task-start="onTaskStart"
  59. @task-pause="onTaskPause"
  60. @task-stop="onTaskStop"
  61. @task-remove="onTaskRemove"
  62. @setting-change="onSettingChange"
  63. @emergency-stop-release="executeEmergencyStopRelease"
  64. />
  65. </div>
  66. <!-- 目标点编辑对话框 -->
  67. <el-dialog
  68. title="目标点编辑"
  69. :visible.sync="pointEditDiaShow"
  70. width="480px"
  71. @close="clearActionDia"
  72. class="waypoint-edit-dialog"
  73. :close-on-click-modal="false"
  74. center
  75. >
  76. <div class="dialog-content">
  77. <el-form :model="pointEditData" label-width="90px" size="small" class="waypoint-form">
  78. <div class="form-section">
  79. <h4 class="section-title">
  80. <i class="el-icon-location-outline"></i>
  81. 位置信息
  82. </h4>
  83. <!-- 分两行布局,给输入框更多空间 -->
  84. <el-form-item label="X坐标(m)">
  85. <el-input v-model="pointEditData.x" placeholder="请输入X坐标值" class="coordinate-input">
  86. </el-input>
  87. </el-form-item>
  88. <el-form-item label="Y坐标(m)">
  89. <el-input v-model="pointEditData.y" placeholder="请输入Y坐标值" class="coordinate-input">
  90. </el-input>
  91. </el-form-item>
  92. </div>
  93. <div class="form-section">
  94. <h4 class="section-title">
  95. <i class="el-icon-guide"></i>
  96. 路径配置
  97. </h4>
  98. <el-form-item label="规划类型">
  99. <el-select v-model="pointEditData.type" placeholder="请选择路径类型" style="width: 100%">
  100. <el-option v-for="item in planOptions" :key="item.value" :label="item.label" :value="item.value">
  101. <span style="float: left">{{ item.label }}</span>
  102. <span style="float: right; color: #8492a6; font-size: 13px">{{ item.value === 0 ? '自由规划' : '路网约束' }}</span>
  103. </el-option>
  104. </el-select>
  105. </el-form-item>
  106. </div>
  107. <div class="form-section">
  108. <h4 class="section-title">
  109. <i class="el-icon-setting"></i>
  110. 动作配置
  111. <el-button type="text" size="mini" @click="appendActionMenu" class="add-action-btn">
  112. <i class="el-icon-plus"></i><span>添加动作</span>
  113. </el-button>
  114. </h4>
  115. <div class="action-list">
  116. <div v-for="(item, index) in pointEditData.actionMenuList" :key="index" class="action-item">
  117. <div class="action-header">
  118. <span class="action-index">{{ index + 1 }}</span>
  119. <el-select v-model="item.value" placeholder="请选择动作" size="small" @change="changeAction(index)" style="flex: 1;">
  120. <el-option v-for="option in actionOptions" :key="option.value" :label="option.label" :value="option.value"></el-option>
  121. </el-select>
  122. <el-button v-if="index > 0" type="text" size="mini" @click="removeActionMenu(index)" class="remove-btn">
  123. <i class="el-icon-close"></i>
  124. </el-button>
  125. </div>
  126. <div v-if="'other' in item" class="action-params">
  127. <el-input v-model="item.other" placeholder="等待时间" size="small" style="width: 120px;">
  128. <template slot="append">秒</template>
  129. </el-input>
  130. </div>
  131. </div>
  132. </div>
  133. </div>
  134. </el-form>
  135. </div>
  136. <span slot="footer" class="dialog-footer">
  137. <el-button @click="pointEditDiaShow = false" size="medium">取 消</el-button>
  138. <el-button type="primary" @click="submitEditPoint" size="medium">
  139. <i class="el-icon-check"></i> 保存修改
  140. </el-button>
  141. </span>
  142. </el-dialog>
  143. <!-- 创建任务对话框 -->
  144. <el-dialog
  145. title="创建任务"
  146. :visible.sync="taskGenerateDiaShow"
  147. width="480px"
  148. @close="closeTaskGenerate"
  149. class="task-create-dialog"
  150. :close-on-click-modal="false"
  151. center
  152. >
  153. <div class="dialog-content">
  154. <el-form :model="generateTaskParam" label-width="90px" size="small" class="task-form">
  155. <div class="form-section">
  156. <h4 class="section-title">
  157. <i class="el-icon-s-order"></i>
  158. 任务信息
  159. </h4>
  160. <el-form-item label="任务名称">
  161. <el-input v-model="generateTaskParam.taskName" placeholder="请输入任务名称" class="task-input">
  162. </el-input>
  163. </el-form-item>
  164. <el-form-item label="执行次数">
  165. <el-input-number
  166. v-model="generateTaskParam.count"
  167. controls-position="right"
  168. :min="1"
  169. :max="100"
  170. class="task-input-number"
  171. style="width: 100%"
  172. ></el-input-number>
  173. </el-form-item>
  174. </div>
  175. <div class="form-section">
  176. <h4 class="section-title">
  177. <i class="el-icon-time"></i>
  178. 执行计划
  179. </h4>
  180. <el-form-item label="开始时间">
  181. <el-time-picker
  182. v-model="generateTaskParam.time"
  183. :picker-options="{
  184. selectableRange: '00:00:00 - 23:59:59'
  185. }"
  186. placeholder="选择时间"
  187. class="task-time-picker"
  188. style="width: 100%"
  189. >
  190. </el-time-picker>
  191. </el-form-item>
  192. <el-form-item label="执行日期">
  193. <el-checkbox-group v-model="generateTaskParam.date" class="task-date-group">
  194. <el-checkbox label="1">周一</el-checkbox>
  195. <el-checkbox label="2">周二</el-checkbox>
  196. <el-checkbox label="3">周三</el-checkbox>
  197. <el-checkbox label="4">周四</el-checkbox>
  198. <el-checkbox label="5">周五</el-checkbox>
  199. <el-checkbox label="6">周六</el-checkbox>
  200. <el-checkbox label="7">周日</el-checkbox>
  201. </el-checkbox-group>
  202. </el-form-item>
  203. </div>
  204. </el-form>
  205. </div>
  206. <span slot="footer" class="dialog-footer">
  207. <el-button @click="closeTaskGenerate()" size="medium">取 消</el-button>
  208. <el-button type="primary" @click="submitTaskGenerate" size="medium">
  209. <i class="el-icon-check"></i> 创建任务
  210. </el-button>
  211. </span>
  212. </el-dialog>
  213. <!-- 任务查看对话框 -->
  214. <el-dialog
  215. title="任务详情"
  216. :visible.sync="taskViewDiaShow"
  217. width="480px"
  218. class="task-view-dialog"
  219. :close-on-click-modal="false"
  220. center
  221. >
  222. <div class="dialog-content">
  223. <el-form :model="taskViewData" label-width="90px" size="small" class="task-view-form">
  224. <div class="form-section">
  225. <h4 class="section-title">
  226. <i class="el-icon-s-order"></i>
  227. 任务信息
  228. </h4>
  229. <el-form-item label="任务名称">
  230. <span class="form-text">{{ taskViewData.taskName || '--' }}</span>
  231. </el-form-item>
  232. <el-form-item label="执行次数">
  233. <span class="form-text">{{ taskViewData.count || '--' }}次</span>
  234. </el-form-item>
  235. <el-form-item label="任务状态">
  236. <span class="form-text status-text" :class="getTaskStatusClass(taskViewData.status)">
  237. {{ getTaskStatusText(taskViewData.status) }}
  238. </span>
  239. </el-form-item>
  240. </div>
  241. <div class="form-section">
  242. <h4 class="section-title">
  243. <i class="el-icon-time"></i>
  244. 执行计划
  245. </h4>
  246. <el-form-item label="开始时间">
  247. <span class="form-text">{{ formatTime(taskViewData.time) }}</span>
  248. </el-form-item>
  249. <el-form-item label="执行日期">
  250. <span class="form-text">{{ formatDate(taskViewData.date) }}</span>
  251. </el-form-item>
  252. </div>
  253. </el-form>
  254. </div>
  255. <span slot="footer" class="dialog-footer">
  256. <el-button @click="taskViewDiaShow = false" size="medium">关 闭</el-button>
  257. </span>
  258. </el-dialog>
  259. </div>
  260. <MqttComp ref="mqtt" :topics="topics" @message-received="onMessage" />
  261. </div>
  262. </template>
  263. <script>
  264. import OlMap from "@/components/OlMap";
  265. import { MapToolbar, RightPanel } from "./components/shared";
  266. import { FullscreenOperations, RobotPositionUtils } from "@/utils/map-operations";
  267. import MqttComp from "@/components/Mqtt/mqttComp.vue";
  268. export default {
  269. name: "NavigationPage",
  270. components: {
  271. OlMap,
  272. MapToolbar,
  273. RightPanel,
  274. MqttComp
  275. },
  276. data() {
  277. return {
  278. topics:[
  279. { topic: this.$mqttPrefix+'/localization/action/init/reply', qos: 2, retain: false },
  280. { topic:this.$mqttPrefix + '/localization/pose'},
  281. { topic: this.$mqttPrefix + '/sensor/battery'},
  282. // 路径规划相关主题
  283. { topic: this.$mqttPrefix + '/planning/service/plan/response', qos: 2, retain: false },
  284. { topic: this.$mqttPrefix + '/planning/trajectory/2d/compact', qos: 2, retain: true },
  285. { topic: this.$mqttPrefix + '/planning/action/replan/reply', qos: 2, retain: false },
  286. // 任务下发相关主题
  287. { topic: this.$mqttPrefix + '/task/target/action/goto/reply', qos: 2, retain: false },
  288. { topic: this.$mqttPrefix + '/task/target/event/arrive', qos: 2, retain: true },
  289. // 任务管理相关主题
  290. { topic: this.$mqttPrefix + '/task/agent/action/exec/reply', qos: 2, retain: false },
  291. { topic: this.$mqttPrefix + '/task/agent/event/complete', qos: 2, retain: false },
  292. { topic: this.$mqttPrefix + '/task/realtime/info', qos: 0, retain: false },
  293. // 任务控制相关主题
  294. { topic: this.$mqttPrefix + '/task/procedure/action/pause/reply', qos: 2, retain: false },
  295. { topic: this.$mqttPrefix + '/task/procedure/action/resume/reply', qos: 2, retain: false },
  296. { topic: this.$mqttPrefix + '/task/procedure/action/cancel/reply', qos: 2, retain: false },
  297. // 导航控制相关主题
  298. { topic: this.$mqttPrefix + '/navigation/stack/action/start/reply', qos: 2, retain: false },
  299. { topic: this.$mqttPrefix + '/navigation/stack/action/stop/reply', qos: 2, retain: false },
  300. { topic: this.$mqttPrefix + '/navigation/stack/action/restart/reply', qos: 2, retain: false },
  301. // 急停控制相关主题
  302. { topic: this.$mqttPrefix + '/control/vehicle/action/stop/reply', qos: 2, retain: false },
  303. { topic: this.$mqttPrefix + '/control/vehicle/property/stop', qos: 2, retain: true }
  304. ],
  305. nowHandMenu: '',
  306. mapName: this.$route.params.mapName || '', // 地图名称
  307. // 激光定位数据
  308. laserPositionData: {
  309. x: 0,
  310. y: 0,
  311. angle: 0
  312. },
  313. activeIndex: -1, // 默认为没有激活的项
  314. settingDrawer: false,
  315. pointDrawer: false,
  316. taskDrawer: false,
  317. pointEditDiaShow: false,
  318. taskGenerateDiaShow: false,
  319. taskViewDiaShow: false,
  320. // 是否开启地图选点
  321. pointSelectionEnabled: false,
  322. // 是否开启位置初始化功能
  323. poseInitEnable: false,
  324. // 功能设置参数
  325. settingParams: {
  326. pointCloud: false,
  327. baseMap: true,
  328. pointId: false,
  329. follow: false,
  330. network: false
  331. },
  332. // 非单个禁用
  333. single: true,
  334. // 非多个禁用
  335. multiple: true,
  336. taskDataList: [],
  337. // 目标点的编辑数据
  338. pointEditData: {
  339. id: '',
  340. x: '',
  341. y: '',
  342. type: '',
  343. // 添加动作菜单列表 (other:追加参数,例如原定等待选项的等待时间)
  344. actionMenuList: []
  345. },
  346. // 生成任务的参数
  347. generateTaskParam: {
  348. taskId: '',
  349. taskName: '',
  350. count: 1,
  351. time: '',
  352. date: [] // 1-7分别指代周一到周末
  353. },
  354. // 任务查看数据
  355. taskViewData: {},
  356. // 点位列表
  357. pointList: [],
  358. pointIds: [],
  359. // 路径类型
  360. planOptions: [
  361. { label: '自由路径', value: 0 },
  362. { label: '路网路径', value: 1 }
  363. ],
  364. // 动作类型
  365. actionOptions: [
  366. { label: '原地等待', value: 0 },
  367. { label: '开始录制', value: 1 },
  368. { label: '结束录制', value: 2 },
  369. { label: '添加建图轨迹', value: 3 },
  370. { label: '挂钩挂载', value: 4 },
  371. { label: '挂钩卸载', value: 5 }
  372. ],
  373. olWidth: 0, // 用于存储宽度的变量
  374. olHeight: 0,
  375. nowHandMenu: '',
  376. // 新UI相关状态
  377. panelVisible: true, // 右侧面板是否可见(旧的,保留兼容)
  378. rightVisible: true, // 导航页右侧面板是否可见
  379. activeTab: 'info', // 当前激活的tab
  380. lastTab: 'info', // 上次激活的tab
  381. selectPointMode: false, // 选点模式
  382. initPoseMode: false, // 初始化位姿模式
  383. // Mock任务数据
  384. mockTasks: [
  385. { id: 1, name: '巡检任务A', nodes: 5, status: 'idle' },
  386. { id: 2, name: '运输任务B', nodes: 3, status: 'running' },
  387. { id: 3, name: '清扫任务C', nodes: 8, status: 'paused' }
  388. ],
  389. // 实时信息数据
  390. realtimeInfo: {
  391. currentMap: this.$route.params.mapName || 'Unknown',
  392. currentTask: '',
  393. speed: '',
  394. speedCommand: '',
  395. coordinates: '',
  396. heading: '',
  397. totalDistance: '',
  398. registrationError: '',
  399. batteryLevel: ''
  400. },
  401. // 机器人位姿数据(用于OlMap组件)
  402. robotPoseData: {
  403. x: 1.813,
  404. y: -63.931,
  405. angle: 0.000
  406. },
  407. // 连接和状态
  408. isConnected: true,
  409. isBusy: false,
  410. isFullscreen: false,
  411. // 导航和急停状态
  412. navigationStackStatus: 'unknown', // unknown, started, stopped
  413. emergencyStopEnabled: false, // 急停状态
  414. // 全屏监听清理函数
  415. fullscreenCleanup: null,
  416. // 目标点相关状态
  417. selectedWaypointIds: [], // 选中的目标点ID列表
  418. waypointSingle: true, // 是否单个选中
  419. waypointMultiple: true, // 是否多个选中
  420. pointEditDiaShow: false, // 目标点编辑对话框显示状态
  421. // 目标点的编辑数据
  422. pointEditData: {
  423. id: '',
  424. x: '',
  425. y: '',
  426. type: '',
  427. // 添加动作菜单列表 (other:追加参数,例如原定等待选项的等待时间)
  428. actionMenuList: []
  429. },
  430. // 路径类型选项
  431. planOptions: [
  432. { label: '自由路径', value: 0 },
  433. { label: '路网路径', value: 1 }
  434. ],
  435. // 动作类型选项
  436. actionOptions: [
  437. { label: '原地等待', value: 0 },
  438. { label: '开始录制', value: 1 },
  439. { label: '结束录制', value: 2 },
  440. { label: '添加建图轨迹', value: 3 },
  441. { label: '挂钩挂载', value: 4 },
  442. { label: '挂钩卸载', value: 5 }
  443. ],
  444. // === 导航页专用数据 ===
  445. waypoints: [],
  446. // 任务下发状态跟踪
  447. currentNavigationTask: null, // 当前导航任务
  448. isNavigating: false, // 是否正在导航
  449. navigationStatus: 'idle', // idle, planning, navigating, arrived, failed
  450. lastGotoRequest: null, // 最后一次goto请求,用于匹配响应
  451. // 任务管理系统
  452. currentExecutingTask: null, // 当前执行的任务
  453. taskQueue: [], // 任务队列
  454. isTaskExecuting: false, // 是否正在执行任务
  455. taskExecutionStatus: 'idle', // idle, executing, paused, completed, failed, cancelled
  456. currentTaskWaypointIndex: 0, // 当前任务执行到的目标点索引
  457. taskRealtimeInfo: {
  458. odom: { total: 0, remain: 0 },
  459. time: { total: 0, remain: 0, duration: 0 },
  460. driveMode: 'auto',
  461. autoReady: true
  462. }, // 任务实时信息
  463. lastTaskExecRequest: null, // 最后一次任务执行请求
  464. tasks: [
  465. {
  466. taskId: 1,
  467. taskName: '巡检任务Alpha',
  468. status: 'idle',
  469. count: 3,
  470. time: new Date('2024-01-01 09:00:00'),
  471. date: ['1', '3', '5'],
  472. points: [
  473. { id: 101, name: '目标点1', x: '1.234', y: '2.345' },
  474. { id: 102, name: '目标点2', x: '3.456', y: '4.567' }
  475. ]
  476. },
  477. {
  478. taskId: 2,
  479. taskName: '运输任务Beta',
  480. status: 'running',
  481. count: 1,
  482. time: new Date('2024-01-01 14:30:00'),
  483. date: ['2', '4'],
  484. points: [
  485. { id: 103, name: '目标点3', x: '5.678', y: '6.789' }
  486. ]
  487. },
  488. {
  489. taskId: 3,
  490. taskName: '清扫任务Gamma',
  491. status: 'paused',
  492. count: 5,
  493. time: new Date('2024-01-01 16:00:00'),
  494. date: ['1', '2', '3', '4', '5'],
  495. points: [
  496. { id: 104, name: '目标点4', x: '7.890', y: '8.901' },
  497. { id: 105, name: '目标点5', x: '9.012', y: '0.123' },
  498. { id: 106, name: '目标点6', x: '1.345', y: '2.456' }
  499. ]
  500. }
  501. ]
  502. };
  503. },
  504. computed: {
  505. // 当前选中的工具key
  506. selectedKey() {
  507. if (this.initPoseMode) return 'init-pose';
  508. return '';
  509. },
  510. // 机器人位置
  511. robotPosition() {
  512. // 直接使用robotPoseData,与标定页面保持一致
  513. if (this.robotPoseData && (this.robotPoseData.x !== 0 || this.robotPoseData.y !== 0)) {
  514. return [this.robotPoseData.x, this.robotPoseData.y];
  515. }
  516. return null;
  517. },
  518. // 是否有有效的机器人位置
  519. hasValidRobotPosition() {
  520. return !!this.robotPosition;
  521. },
  522. // 地图是否就绪
  523. isMapReady() {
  524. return !!this.getMapInstance();
  525. },
  526. // 导航状态文本
  527. navigationStatusText() {
  528. switch(this.navigationStatus) {
  529. case 'idle': return '空闲';
  530. case 'planning': return '规划中';
  531. case 'navigating': return '导航中';
  532. case 'arrived': return '已到达';
  533. case 'failed': return '失败';
  534. default: return '未知';
  535. }
  536. },
  537. // 是否可以发送新的导航任务
  538. canStartNavigation() {
  539. return !this.isNavigating && this.navigationStatus !== 'planning';
  540. },
  541. // 当前导航目标描述
  542. currentNavigationTarget() {
  543. if (!this.currentNavigationTask) return null;
  544. const waypoint = this.currentNavigationTask.waypoint;
  545. return `${waypoint.name || '目标点' + waypoint.id} (${waypoint.x}, ${waypoint.y})`;
  546. },
  547. // 任务执行状态文本
  548. taskExecutionStatusText() {
  549. switch(this.taskExecutionStatus) {
  550. case 'idle': return '空闲';
  551. case 'executing': return '执行中';
  552. case 'paused': return '暂停';
  553. case 'completed': return '已完成';
  554. case 'failed': return '失败';
  555. case 'cancelled': return '已取消';
  556. default: return '未知';
  557. }
  558. },
  559. // 当前执行任务描述
  560. currentTaskDescription() {
  561. if (!this.currentExecutingTask) return null;
  562. const task = this.currentExecutingTask;
  563. const progress = `${this.currentTaskWaypointIndex}/${task.points?.length || 0}`;
  564. return `${task.taskName} (${progress})`;
  565. },
  566. // 是否可以开始新任务
  567. canStartNewTask() {
  568. return !this.isTaskExecuting && !this.isNavigating;
  569. },
  570. // 任务实时进度信息
  571. taskProgressInfo() {
  572. if (!this.currentExecutingTask || !this.taskRealtimeInfo) return null;
  573. return {
  574. taskName: this.currentExecutingTask.taskName,
  575. waypointProgress: `${this.currentTaskWaypointIndex}/${this.currentExecutingTask.points?.length || 0}`,
  576. totalDistance: `${(this.taskRealtimeInfo.odom.total / 1000).toFixed(2)}km`,
  577. remainDistance: `${(this.taskRealtimeInfo.odom.remain / 1000).toFixed(2)}km`,
  578. totalTime: this.formatSeconds(this.taskRealtimeInfo.time.total),
  579. remainTime: this.formatSeconds(this.taskRealtimeInfo.time.remain),
  580. duration: this.formatSeconds(this.taskRealtimeInfo.time.duration),
  581. driveMode: this.taskRealtimeInfo.driveMode === 'auto' ? '自动' : '手动',
  582. autoReady: this.taskRealtimeInfo.autoReady
  583. };
  584. }
  585. },
  586. created() {
  587. },
  588. mounted() {
  589. // const mapId = this.$route.params.mapId;
  590. this.updateOlCss();
  591. window.addEventListener('resize', this.updateOlCss);
  592. // 地图初始化(无需额外操作,直接使用getMapInstance方法获取地图实例)
  593. // 设置全屏状态监听
  594. this.fullscreenCleanup = FullscreenOperations.addFullscreenListener((isFullscreen) => {
  595. this.isFullscreen = isFullscreen;
  596. // 全屏状态改变后,重新计算地图尺寸
  597. this.$nextTick(() => {
  598. this.updateOlCss();
  599. // 触发地图重新计算尺寸
  600. const map = this.getMapInstance();
  601. if (map) {
  602. map.updateSize();
  603. }
  604. });
  605. });
  606. },
  607. beforeDestroy() {
  608. window.removeEventListener('resize', this.updateOlCss);
  609. // 清理全屏监听
  610. if (this.fullscreenCleanup) {
  611. this.fullscreenCleanup();
  612. }
  613. },
  614. methods: {
  615. goto() {
  616. // if (this.pointIds.length !== 1) {
  617. // this.$message.warning('请选择一个点位进行前往操作');
  618. // return;
  619. // }
  620. // const point = this.pointList.find(p => p.id === this.pointIds[0]);
  621. // if (point) {
  622. // console.log('前往点位:', point);
  623. // this.$refs.mqtt.publish(this.$mqttPrefix + "/task/target/action/goto", {
  624. // "timestamp" : 123456,
  625. // "args" : [
  626. // {
  627. // "roadmap" : this.mapName,
  628. // "coord" : [[Number(point.x), Number(point.y)]]
  629. // }
  630. // ]
  631. // },2,false);
  632. // this.$refs.olmap.drawPoint()
  633. // }
  634. // this.$refs.olmap.drawTrackLine(-15.491 , -57.526,10.209 , -123.223)
  635. },
  636. onMessage({ topic, message }) {
  637. // console.log("收到消息:", topic, message);
  638. if (topic === this.$mqttPrefix + '/localization/action/init/reply') {
  639. this.handleInitReply(message);
  640. } else if (topic === this.$mqttPrefix + '/localization/pose') {
  641. this.handleLaserPose(message);
  642. } else if (topic === this.$mqttPrefix + '/sensor/battery') {
  643. // 处理电池消息
  644. const data = message.args[0];
  645. this.realtimeInfo.batteryLevel = data.capacity + '%' || '0%';
  646. } else if (topic === this.$mqttPrefix + '/planning/service/plan/response') {
  647. // 处理路径规划响应
  648. this.handlePlanResponse(message);
  649. } else if (topic === this.$mqttPrefix + '/planning/trajectory/2d/compact') {
  650. // 处理当前行驶轨迹
  651. this.handleTrajectoryData(message);
  652. } else if (topic === this.$mqttPrefix + '/planning/action/replan/reply') {
  653. // 处理重规划响应
  654. this.handleReplanReply(message);
  655. } else if (topic === this.$mqttPrefix + '/task/target/action/goto/reply') {
  656. // 处理前往目标点响应
  657. this.handleGotoReply(message);
  658. } else if (topic === this.$mqttPrefix + '/task/target/event/arrive') {
  659. // 处理到达目标点事件
  660. this.handleArriveEvent(message);
  661. } else if (topic === this.$mqttPrefix + '/task/agent/action/exec/reply') {
  662. // 处理任务执行响应
  663. this.handleTaskExecReply(message);
  664. } else if (topic === this.$mqttPrefix + '/task/agent/event/complete') {
  665. // 处理任务完成事件
  666. this.handleTaskCompleteEvent(message);
  667. } else if (topic === this.$mqttPrefix + '/task/realtime/info') {
  668. // 处理任务实时信息
  669. this.handleTaskRealtimeInfo(message);
  670. } else if (topic === this.$mqttPrefix + '/task/procedure/action/pause/reply') {
  671. // 处理暂停任务响应
  672. this.handleTaskPauseReply(message);
  673. } else if (topic === this.$mqttPrefix + '/task/procedure/action/resume/reply') {
  674. // 处理继续任务响应
  675. this.handleTaskResumeReply(message);
  676. } else if (topic === this.$mqttPrefix + '/task/procedure/action/cancel/reply') {
  677. // 处理取消任务响应
  678. this.handleTaskCancelReply(message);
  679. } else if (topic === this.$mqttPrefix + '/navigation/stack/action/start/reply') {
  680. // 处理导航启动响应
  681. this.handleNavigationStartReply(message);
  682. } else if (topic === this.$mqttPrefix + '/navigation/stack/action/stop/reply') {
  683. // 处理导航停止响应
  684. this.handleNavigationStopReply(message);
  685. } else if (topic === this.$mqttPrefix + '/navigation/stack/action/restart/reply') {
  686. // 处理导航重启响应
  687. this.handleNavigationRestartReply(message);
  688. } else if (topic === this.$mqttPrefix + '/control/vehicle/action/stop/reply') {
  689. // 处理急停控制响应
  690. this.handleEmergencyStopReply(message);
  691. } else if (topic === this.$mqttPrefix + '/control/vehicle/property/stop') {
  692. // 处理急停状态变化
  693. this.handleEmergencyStopStatus(message);
  694. }
  695. },
  696. handleInitReply(message) {
  697. // 处理初始化回复消息
  698. // console.log("初始化回复:", message);
  699. },
  700. handleLaserPose(message) {
  701. try {
  702. const data = message.args[0];
  703. const {xyz, rpy, blh, heading} = data.pose;
  704. // 保存旧的位置数据,用于比较
  705. const oldX = this.laserPositionData.x;
  706. const oldY = this.laserPositionData.y;
  707. // 激光定位实时数据
  708. this.laserPositionData.x = xyz[0];
  709. this.laserPositionData.y = xyz[1];
  710. this.laserPositionData.angle = rpy[2];
  711. // 实时数据
  712. this.realtimeInfo.coordinates = `(${xyz[0]}, ${xyz[1]}, ${xyz[2]})`;
  713. this.realtimeInfo.heading = rpy[2] + '°'; // 航向角
  714. this.realtimeInfo.speed = data.vel.heading+ 'm/s'; // 速度
  715. this.realtimeInfo.speedCommand = data.vel.enu + 'm/s'; // 速度指令
  716. this.realtimeInfo.totalDistance = data.odometer + 'm'; // 总里程
  717. this.realtimeInfo.registrationError = data.score;
  718. // 检查位置是否有变化 - 降低阈值以实现更频繁的轨迹更新
  719. const positionChanged = Math.abs(xyz[0] - oldX) > 0.005 || Math.abs(xyz[1] - oldY) > 0.005;
  720. // 如果位置发生变化且存在轨迹,更新轨迹进度
  721. if (positionChanged && this.$refs.olmap && this.$refs.olmap.currentTrajectory) {
  722. const currentTrajectory = this.$refs.olmap.currentTrajectory;
  723. // 检查是否到达目标点
  724. const reachedTarget = this.checkIfReachedTarget(currentTrajectory);
  725. // 如果还没到达目标点,实时更新轨迹绘制
  726. if (!reachedTarget) {
  727. // 不再基于进度索引,而是直接基于机器人位置重新绘制轨迹
  728. console.log(`机器人位置更新,重新绘制实时轨迹: (${xyz[0].toFixed(3)}, ${xyz[1].toFixed(3)})`);
  729. if (this.$refs.olmap.updateRealtimeTrajectory) {
  730. this.$refs.olmap.updateRealtimeTrajectory([xyz[0], xyz[1]]);
  731. }
  732. }
  733. }
  734. // 更新实时轨迹(机器人实际行走路径)
  735. // if (this.$refs.olmap && this.$refs.olmap.drawRealTimeTrajectory && (xyz[0] !== 0 || xyz[1] !== 0) && this.settingParams.showRealTimeTrajectory) {
  736. // this.$refs.olmap.drawRealTimeTrajectory([xyz[0], xyz[1]], this.trajectoryStyles.realTime);
  737. // }
  738. /* this.gnssPositionData.longitude = blh[1]; // 经度
  739. this.gnssPositionData.latitude = blh[0]; // 纬度
  740. this.gnssPositionData.angle = heading; // 航向角
  741. this.gnssPositionData.status = data.rtk.star+ '/' + data.rtk.status; // RTK状态 */
  742. } catch (e) {
  743. console.error("解析失败:", e);
  744. }
  745. },
  746. // 处理路径规划响应
  747. handlePlanResponse(message) {
  748. console.log("路径规划响应:", message);
  749. if (message.status === 'ok' && message.args && message.args.length > 0) {
  750. const planData = message.args[0];
  751. console.log("规划路径数据:", planData);
  752. // 这里可以处理规划的路径点,但主要的轨迹绘制会通过trajectory消息来处理
  753. this.$message.success('路径规划成功!');
  754. } else {
  755. this.$message.error('路径规划失败');
  756. }
  757. },
  758. // 处理轨迹数据
  759. handleTrajectoryData(message) {
  760. console.log("接收到轨迹数据:", message);
  761. try {
  762. if (message.args && message.args.length > 0) {
  763. const trajectoryData = message.args[0];
  764. const { trj, idx, total, div } = trajectoryData;
  765. // 转换轨迹数据:真实坐标 = 坐标值/div
  766. const realTrajectory = trj.map(point => [
  767. point[0] / div,
  768. point[1] / div
  769. ]);
  770. // 计算当前机器人在轨迹中的进度
  771. const currentProgress = this.calculateCurrentProgress(realTrajectory);
  772. // 调用地图组件绘制轨迹
  773. if (this.$refs.olmap && this.$refs.olmap.drawTrajectory) {
  774. this.$refs.olmap.drawTrajectory(realTrajectory, {
  775. totalPoints: total,
  776. indices: idx,
  777. currentProgress: currentProgress,
  778. robotPosition: [this.laserPositionData.x, this.laserPositionData.y]
  779. });
  780. }
  781. console.log("轨迹绘制完成,总点数:", total, "轨迹点数:", realTrajectory.length);
  782. }
  783. } catch (error) {
  784. console.error("处理轨迹数据失败:", error);
  785. }
  786. },
  787. // 处理重规划响应
  788. handleReplanReply(message) {
  789. console.log("重规划响应:", message);
  790. if (message.status === 'ok') {
  791. this.$message.success('重规划成功');
  792. } else {
  793. this.$message.error('重规划失败');
  794. }
  795. },
  796. // 处理前往目标点响应
  797. handleGotoReply(message) {
  798. console.log("前往目标点响应:", message);
  799. try {
  800. if (message.status === 'ok') {
  801. // 检查是否匹配当前的goto请求
  802. if (this.lastGotoRequest && message.pub_timestamp === this.lastGotoRequest.timestamp) {
  803. this.navigationStatus = 'navigating';
  804. this.isNavigating = true;
  805. this.$message.success('机器人已接收前往指令,开始导航');
  806. }
  807. } else {
  808. this.navigationStatus = 'failed';
  809. this.isNavigating = false;
  810. this.$message.error('前往目标点请求失败');
  811. }
  812. } catch (error) {
  813. console.error("处理goto响应失败:", error);
  814. }
  815. },
  816. // 处理到达目标点事件
  817. handleArriveEvent(message) {
  818. console.log("到达目标点事件:", message);
  819. try {
  820. if (message.args && message.args.length > 0) {
  821. const arriveData = message.args[0];
  822. const { status, coord, nid, error } = arriveData;
  823. if (status === 'ok') {
  824. // 成功到达目标点
  825. this.navigationStatus = 'arrived';
  826. this.isNavigating = false;
  827. // 显示成功消息
  828. const targetInfo = coord && coord.length > 0 ?
  829. `(${coord[0][0]}, ${coord[0][1]})` :
  830. (nid && nid.length > 0 ? `点位${nid[0]}` : '目标点');
  831. // 检查是否是任务中的目标点到达
  832. if (this.currentExecutingTask && this.currentNavigationTask?.taskContext) {
  833. // 处理任务中的目标点到达
  834. this.handleTaskWaypointArrival();
  835. } else {
  836. // 单独目标点到达
  837. this.$message.success(`已成功到达目标点 ${targetInfo}!`);
  838. // 清除轨迹(2秒后)
  839. if (this.$refs.olmap && this.$refs.olmap.clearTrajectory) {
  840. setTimeout(() => {
  841. this.$refs.olmap.clearTrajectory();
  842. console.log('轨迹已自动清除');
  843. }, 2000);
  844. }
  845. }
  846. // 重置导航任务状态
  847. this.currentNavigationTask = null;
  848. this.lastGotoRequest = null;
  849. } else if (status === 'fail') {
  850. // 到达失败
  851. this.navigationStatus = 'failed';
  852. this.isNavigating = false;
  853. // 根据错误代码显示具体错误信息
  854. let errorMessage = '前往目标点失败';
  855. if (error) {
  856. switch (error) {
  857. case 21:
  858. errorMessage = '前往目标点失败:规划失败';
  859. break;
  860. case 22:
  861. errorMessage = '前往目标点失败:偏离车道线';
  862. break;
  863. case 23:
  864. errorMessage = '前往目标点失败:任务提前终止';
  865. break;
  866. case 24:
  867. errorMessage = '前往目标点失败:定位异常';
  868. break;
  869. default:
  870. errorMessage = `前往目标点失败:未知错误(${error})`;
  871. }
  872. }
  873. this.$message.error(errorMessage);
  874. console.error("导航失败,错误代码:", error);
  875. // 清除轨迹
  876. if (this.$refs.olmap && this.$refs.olmap.clearTrajectory) {
  877. this.$refs.olmap.clearTrajectory();
  878. }
  879. // 重置导航任务状态
  880. this.currentNavigationTask = null;
  881. this.lastGotoRequest = null;
  882. }
  883. }
  884. } catch (error) {
  885. console.error("处理到达事件失败:", error);
  886. }
  887. },
  888. // 处理任务执行响应
  889. handleTaskExecReply(message) {
  890. console.log("任务执行响应:", message);
  891. try {
  892. if (message.status === 'ok') {
  893. // 检查是否匹配当前的任务执行请求
  894. if (this.lastTaskExecRequest && message.pub_timestamp === this.lastTaskExecRequest.timestamp) {
  895. this.taskExecutionStatus = 'executing';
  896. this.isTaskExecuting = true;
  897. this.$message.success(`任务 "${this.currentExecutingTask.taskName}" 已开始执行`);
  898. // 开始执行任务的第一个目标点
  899. this.executeNextWaypoint();
  900. }
  901. } else {
  902. this.taskExecutionStatus = 'failed';
  903. this.isTaskExecuting = false;
  904. this.$message.error('任务执行失败');
  905. }
  906. } catch (error) {
  907. console.error("处理任务执行响应失败:", error);
  908. }
  909. },
  910. // 处理任务完成事件
  911. handleTaskCompleteEvent(message) {
  912. console.log("任务完成事件:", message);
  913. try {
  914. if (message.args && message.args.length > 0) {
  915. const completeData = message.args[0];
  916. const { name, status } = completeData;
  917. if (status === 'ok') {
  918. // 任务成功完成
  919. this.taskExecutionStatus = 'completed';
  920. this.isTaskExecuting = false;
  921. this.$message.success(`任务 "${name}" 已成功完成!`);
  922. // 更新任务列表中的状态
  923. const task = this.tasks.find(t => t.taskName === name);
  924. if (task) {
  925. task.status = 'completed';
  926. }
  927. // 清除轨迹
  928. if (this.$refs.olmap && this.$refs.olmap.clearTrajectory) {
  929. setTimeout(() => {
  930. this.$refs.olmap.clearTrajectory();
  931. console.log('任务完成,轨迹已清除');
  932. }, 3000);
  933. }
  934. } else if (status === 'fail') {
  935. // 任务失败
  936. this.taskExecutionStatus = 'failed';
  937. this.isTaskExecuting = false;
  938. this.$message.error(`任务 "${name}" 执行失败`);
  939. // 更新任务列表中的状态
  940. const task = this.tasks.find(t => t.taskName === name);
  941. if (task) {
  942. task.status = 'failed';
  943. }
  944. // 清除轨迹
  945. if (this.$refs.olmap && this.$refs.olmap.clearTrajectory) {
  946. this.$refs.olmap.clearTrajectory();
  947. }
  948. }
  949. // 重置任务执行状态
  950. this.currentExecutingTask = null;
  951. this.currentTaskWaypointIndex = 0;
  952. this.lastTaskExecRequest = null;
  953. }
  954. } catch (error) {
  955. console.error("处理任务完成事件失败:", error);
  956. }
  957. },
  958. // 处理任务实时信息
  959. handleTaskRealtimeInfo(message) {
  960. // console.log("任务实时信息:", message);
  961. try {
  962. if (message.args && message.args.length > 0) {
  963. const realtimeData = message.args[0];
  964. const { odom, time, drive_mode, auto_ready } = realtimeData;
  965. // 更新任务实时信息
  966. this.taskRealtimeInfo = {
  967. odom: {
  968. total: odom?.tottal || 0,
  969. remain: odom?.remain || 0
  970. },
  971. time: {
  972. total: time?.total || 0,
  973. remain: time?.remain || 0,
  974. duration: time?.duration || 0
  975. },
  976. driveMode: drive_mode || 'auto',
  977. autoReady: auto_ready !== undefined ? auto_ready : true
  978. };
  979. // 如果有正在执行的任务,更新实时信息到右侧面板
  980. if (this.currentExecutingTask) {
  981. this.realtimeInfo.currentTask = this.currentExecutingTask.taskName;
  982. this.realtimeInfo.totalDistance = `${(odom?.tottal / 1000).toFixed(2)}km` || '';
  983. // 可以在这里更新其他实时信息显示
  984. }
  985. }
  986. } catch (error) {
  987. console.error("处理任务实时信息失败:", error);
  988. }
  989. },
  990. // 处理暂停任务响应
  991. handleTaskPauseReply(message) {
  992. console.log("暂停任务响应:", message);
  993. if (message.status === 'ok') {
  994. this.taskExecutionStatus = 'paused';
  995. this.$message.success('任务已暂停');
  996. } else {
  997. this.$message.error('任务暂停失败');
  998. }
  999. },
  1000. // 处理继续任务响应
  1001. handleTaskResumeReply(message) {
  1002. console.log("继续任务响应:", message);
  1003. if (message.status === 'ok') {
  1004. this.taskExecutionStatus = 'executing';
  1005. this.$message.success('任务已继续执行');
  1006. } else {
  1007. this.$message.error('任务继续失败');
  1008. }
  1009. },
  1010. // 处理取消任务响应
  1011. handleTaskCancelReply(message) {
  1012. console.log("取消任务响应:", message);
  1013. if (message.status === 'ok') {
  1014. this.taskExecutionStatus = 'cancelled';
  1015. this.isTaskExecuting = false;
  1016. this.$message.success('任务已取消');
  1017. // 清除轨迹
  1018. if (this.$refs.olmap && this.$refs.olmap.clearTrajectory) {
  1019. this.$refs.olmap.clearTrajectory();
  1020. }
  1021. // 重置任务执行状态
  1022. this.currentExecutingTask = null;
  1023. this.currentTaskWaypointIndex = 0;
  1024. this.lastTaskExecRequest = null;
  1025. } else {
  1026. this.$message.error('任务取消失败');
  1027. }
  1028. },
  1029. // === 导航控制相关MQTT处理方法 ===
  1030. // 处理导航启动响应
  1031. handleNavigationStartReply(message) {
  1032. console.log("导航启动响应:", message);
  1033. try {
  1034. if (message.status === 'ok') {
  1035. this.navigationStackStatus = 'started';
  1036. this.$message.success('导航系统已启动');
  1037. } else {
  1038. this.$message.error('导航系统启动失败');
  1039. console.error("导航启动失败:", message);
  1040. }
  1041. } catch (error) {
  1042. console.error("处理导航启动响应失败:", error);
  1043. }
  1044. },
  1045. // 处理导航停止响应
  1046. handleNavigationStopReply(message) {
  1047. console.log("导航停止响应:", message);
  1048. try {
  1049. if (message.status === 'ok') {
  1050. this.navigationStackStatus = 'stopped';
  1051. // 停止导航时,清除当前的导航任务和轨迹
  1052. this.isNavigating = false;
  1053. this.navigationStatus = 'idle';
  1054. this.currentNavigationTask = null;
  1055. this.lastGotoRequest = null;
  1056. // 清除轨迹
  1057. if (this.$refs.olmap && this.$refs.olmap.clearTrajectory) {
  1058. this.$refs.olmap.clearTrajectory();
  1059. }
  1060. this.$message.success('导航系统已停止');
  1061. } else {
  1062. this.$message.error('导航系统停止失败');
  1063. console.error("导航停止失败:", message);
  1064. }
  1065. } catch (error) {
  1066. console.error("处理导航停止响应失败:", error);
  1067. }
  1068. },
  1069. // 处理导航重启响应
  1070. handleNavigationRestartReply(message) {
  1071. console.log("导航重启响应:", message);
  1072. try {
  1073. if (message.status === 'ok') {
  1074. this.navigationStackStatus = 'started';
  1075. // 重启导航时,重置所有导航相关状态
  1076. this.isNavigating = false;
  1077. this.navigationStatus = 'idle';
  1078. this.currentNavigationTask = null;
  1079. this.lastGotoRequest = null;
  1080. // 重置任务执行状态
  1081. this.isTaskExecuting = false;
  1082. this.taskExecutionStatus = 'idle';
  1083. this.currentExecutingTask = null;
  1084. this.currentTaskWaypointIndex = 0;
  1085. // 清除轨迹
  1086. if (this.$refs.olmap && this.$refs.olmap.clearTrajectory) {
  1087. this.$refs.olmap.clearTrajectory();
  1088. }
  1089. this.$message.success('导航系统已重启');
  1090. this.isBusy = false;
  1091. } else {
  1092. this.$message.error('导航系统重启失败');
  1093. this.isBusy = false;
  1094. console.error("导航重启失败:", message);
  1095. }
  1096. } catch (error) {
  1097. console.error("处理导航重启响应失败:", error);
  1098. this.isBusy = false;
  1099. }
  1100. },
  1101. // 处理急停控制响应
  1102. handleEmergencyStopReply(message) {
  1103. console.log("急停控制响应:", message);
  1104. try {
  1105. if (message.status === 'ok') {
  1106. this.$message.success('急停指令已执行');
  1107. } else {
  1108. this.$message.error('急停指令执行失败');
  1109. console.error("急停控制失败:", message);
  1110. }
  1111. } catch (error) {
  1112. console.error("处理急停控制响应失败:", error);
  1113. }
  1114. },
  1115. // 处理急停状态变化
  1116. handleEmergencyStopStatus(message) {
  1117. // console.log("急停状态:", message);
  1118. try {
  1119. if (message.args && message.args.length > 0) {
  1120. const stopEnabled = message.args[0];
  1121. const oldStatus = this.emergencyStopEnabled;
  1122. this.emergencyStopEnabled = stopEnabled;
  1123. // 只在状态真正变化时显示消息,避免频繁提示
  1124. if (oldStatus !== stopEnabled) {
  1125. if (stopEnabled) {
  1126. this.$message.warning('车辆已启用急停状态');
  1127. // 急停启用时,清除所有导航任务
  1128. this.isNavigating = false;
  1129. this.navigationStatus = 'idle';
  1130. this.currentNavigationTask = null;
  1131. this.isTaskExecuting = false;
  1132. this.taskExecutionStatus = 'idle';
  1133. } else {
  1134. this.$message.info('车辆急停状态已解除');
  1135. }
  1136. }
  1137. }
  1138. } catch (error) {
  1139. console.error("处理急停状态失败:", error);
  1140. }
  1141. },
  1142. // 计算当前机器人在轨迹中的进度
  1143. calculateCurrentProgress(trajectory) {
  1144. if (!trajectory || trajectory.length === 0) return 0;
  1145. const robotPos = [this.robotPoseData.x, this.robotPoseData.y];
  1146. let progressDistance = 0; // 沿轨迹的累计距离
  1147. let totalDistance = 0; // 轨迹总长度
  1148. // 计算轨迹总长度
  1149. for (let i = 0; i < trajectory.length - 1; i++) {
  1150. const segmentLength = Math.sqrt(
  1151. Math.pow(trajectory[i + 1][0] - trajectory[i][0], 2) +
  1152. Math.pow(trajectory[i + 1][1] - trajectory[i][1], 2)
  1153. );
  1154. totalDistance += segmentLength;
  1155. }
  1156. if (totalDistance === 0) return 0;
  1157. let bestProjectionDistance = 0;
  1158. let minDistanceToTrajectory = Infinity;
  1159. // 遍历轨迹的每个线段,找到机器人在轨迹上的投影点
  1160. let accumulatedDistance = 0;
  1161. for (let i = 0; i < trajectory.length - 1; i++) {
  1162. const segmentStart = trajectory[i];
  1163. const segmentEnd = trajectory[i + 1];
  1164. const segmentLength = Math.sqrt(
  1165. Math.pow(segmentEnd[0] - segmentStart[0], 2) +
  1166. Math.pow(segmentEnd[1] - segmentStart[1], 2)
  1167. );
  1168. // 计算机器人到当前线段的投影点
  1169. const projection = this.projectPointToLineSegment(robotPos, segmentStart, segmentEnd);
  1170. const distanceToSegment = projection.distance;
  1171. // 如果这是到轨迹最近的投影点
  1172. if (distanceToSegment < minDistanceToTrajectory) {
  1173. minDistanceToTrajectory = distanceToSegment;
  1174. // 计算投影点在轨迹上的累计距离
  1175. const projectionDistanceOnSegment = Math.sqrt(
  1176. Math.pow(projection.point[0] - segmentStart[0], 2) +
  1177. Math.pow(projection.point[1] - segmentStart[1], 2)
  1178. );
  1179. bestProjectionDistance = accumulatedDistance + projectionDistanceOnSegment;
  1180. }
  1181. accumulatedDistance += segmentLength;
  1182. }
  1183. // 转换为轨迹点索引(基于距离比例)
  1184. const progressRatio = bestProjectionDistance / totalDistance;
  1185. const progressIndex = Math.floor(progressRatio * (trajectory.length - 1));
  1186. // 只有当机器人距离轨迹很近时(5米内),才更新进度
  1187. if (minDistanceToTrajectory < 5.0) {
  1188. console.log(`机器人轨迹进度: ${progressIndex}/${trajectory.length-1}, 距轨迹距离: ${minDistanceToTrajectory.toFixed(2)}m`);
  1189. return Math.min(progressIndex, trajectory.length - 1);
  1190. }
  1191. // 如果离轨迹太远,保持之前的进度
  1192. return this.$refs.olmap?.trajectoryProgress || 0;
  1193. },
  1194. // 计算点到线段的投影
  1195. projectPointToLineSegment(point, lineStart, lineEnd) {
  1196. const [px, py] = point;
  1197. const [x1, y1] = lineStart;
  1198. const [x2, y2] = lineEnd;
  1199. const A = px - x1;
  1200. const B = py - y1;
  1201. const C = x2 - x1;
  1202. const D = y2 - y1;
  1203. const dot = A * C + B * D;
  1204. const lenSq = C * C + D * D;
  1205. if (lenSq === 0) {
  1206. // 线段退化为点
  1207. return {
  1208. point: [x1, y1],
  1209. distance: Math.sqrt(A * A + B * B)
  1210. };
  1211. }
  1212. let param = dot / lenSq;
  1213. // 限制参数在[0,1]范围内,确保投影点在线段上
  1214. param = Math.max(0, Math.min(1, param));
  1215. const projectionX = x1 + param * C;
  1216. const projectionY = y1 + param * D;
  1217. const distance = Math.sqrt(
  1218. Math.pow(px - projectionX, 2) + Math.pow(py - projectionY, 2)
  1219. );
  1220. return {
  1221. point: [projectionX, projectionY],
  1222. distance: distance
  1223. };
  1224. },
  1225. // 检测是否到达目标点(备用检测,主要依赖MQTT到达事件)
  1226. checkIfReachedTarget(trajectory) {
  1227. if (!trajectory || trajectory.length === 0) return false;
  1228. // 如果已经通过MQTT确认到达或失败,不需要本地检测
  1229. if (this.navigationStatus === 'arrived' || this.navigationStatus === 'failed') {
  1230. return true;
  1231. }
  1232. // 备用的本地距离检测(仅用于防止MQTT消息丢失的情况)
  1233. const robotPos = [this.laserPositionData.x, this.laserPositionData.y];
  1234. const targetPos = trajectory[trajectory.length - 1]; // 最后一个点是目标点
  1235. const distanceToTarget = Math.sqrt(
  1236. Math.pow(targetPos[0] - robotPos[0], 2) + Math.pow(targetPos[1] - robotPos[1], 2)
  1237. );
  1238. // 如果距离目标点很近且正在导航,打印调试信息但不执行到达逻辑
  1239. // (到达逻辑由MQTT事件处理)
  1240. if (distanceToTarget < 1.0 && this.isNavigating) {
  1241. console.log(`本地检测: 机器人接近目标点,距离: ${distanceToTarget.toFixed(2)}m,等待MQTT到达确认`);
  1242. }
  1243. return false; // 不执行本地到达逻辑
  1244. },
  1245. publishMsg() {
  1246. },
  1247. updateOlCss() {
  1248. const element = this.$el.querySelector('.map-stage');
  1249. this.olWidth = element.offsetWidth;
  1250. this.olHeight = element.offsetHeight;
  1251. },
  1252. // 地图API适配器
  1253. getMapInstance() {
  1254. return this.$refs.olmap && this.$refs.olmap.map ? this.$refs.olmap.map : null;
  1255. },
  1256. openDraSetting() {
  1257. this.poseInitEnable = false;
  1258. this.activeIndex = this.activeIndex == 0 ? -1 : 0;
  1259. if (this.settingDrawer) {
  1260. this.nowHandMenu = ''
  1261. this.settingDrawer = false;
  1262. return;
  1263. }
  1264. this.closeDra()
  1265. this.nowHandMenu = '功能菜单操作'
  1266. this.settingDrawer = true;
  1267. },
  1268. openPoint() {
  1269. this.poseInitEnable = false
  1270. this.activeIndex = this.activeIndex == 1 ? -1 : 1;
  1271. if (this.pointDrawer) {
  1272. this.pointDrawer = false;
  1273. this.nowHandMenu = ''
  1274. return;
  1275. }
  1276. this.closeDra()
  1277. this.nowHandMenu = '目标点操作'
  1278. this.pointDrawer = true;
  1279. },
  1280. openTask() {
  1281. this.poseInitEnable = false
  1282. this.activeIndex = this.activeIndex == 2 ? -1 : 2;
  1283. if (this.taskDrawer) {
  1284. this.taskDrawer = false;
  1285. this.nowHandMenu = ''
  1286. return;
  1287. }
  1288. this.closeDra()
  1289. this.nowHandMenu = '任务操作'
  1290. this.taskDrawer = true;
  1291. },
  1292. /**
  1293. * 关闭左侧菜单的抽屉
  1294. * type 类型(hand手动,auto自动)
  1295. */
  1296. closeDra(type) {
  1297. this.settingDrawer = false;
  1298. this.pointDrawer = false;
  1299. this.taskDrawer = false;
  1300. if (type == 'hand') {
  1301. this.activeIndex = -1;
  1302. }
  1303. this.nowHandMenu = ''
  1304. },
  1305. handleSelectionChange(selection) {
  1306. this.pointIds = selection.map(item => item.id)
  1307. this.single = selection.length !== 1
  1308. this.multiple = !selection.length
  1309. },
  1310. // 点位编辑
  1311. editPoint(row) {
  1312. this.pointEditDiaShow = true;
  1313. this.pointEditData.id = row.id;
  1314. this.pointEditData.x = row.x;
  1315. this.pointEditData.y = row.y;
  1316. this.pointEditData.type = row.type;
  1317. this.pointEditData.actionMenuList = row.action
  1318. },
  1319. // 点位删除
  1320. removePoint(row) {
  1321. const idArr = [];
  1322. const ids = row?.id || this.pointIds;
  1323. if (ids) {
  1324. idArr.push(...(Array.isArray(ids) ? ids : [ids]));
  1325. idArr.forEach(id => {
  1326. let indexToRemove = this.pointList.findIndex(item => item?.id === id);
  1327. if (indexToRemove !== -1) {
  1328. this.pointList.splice(indexToRemove, 1);
  1329. if (this.pointList.length < 1) {
  1330. // 重置地图的id计数器
  1331. this.$refs.olmap.restIdNum();
  1332. }
  1333. // this.$refs.olmap.removeIconHtmlById("pose-" + id);
  1334. this.$refs.olmap.removeCalibrationById(id)
  1335. }
  1336. });
  1337. }
  1338. },
  1339. // 点位上移
  1340. moveUp() {
  1341. const index = this.pointList.findIndex(point => point.id == this.pointIds[0]);
  1342. if (index !== -1 && index > 0) {
  1343. let temp = this.pointList[index];
  1344. this.$set(this.pointList, index, this.pointList[index - 1]);
  1345. this.$set(this.pointList, index - 1, temp);
  1346. }
  1347. },
  1348. // 点位下移
  1349. moveDown() {
  1350. const index = this.pointList.findIndex(point => point.id == this.pointIds[0]);
  1351. if (index !== -1 && index < this.pointList.length - 1) {
  1352. let temp = this.pointList[index];
  1353. this.$set(this.pointList, index, this.pointList[index + 1]);
  1354. this.$set(this.pointList, index + 1, temp);
  1355. }
  1356. },
  1357. // 追加动作按钮
  1358. appendActionMenu() {
  1359. this.pointEditData.actionMenuList.push({ value: 0, other: 0 })
  1360. },
  1361. // 删除追加动作按钮
  1362. removeActionMenu(index) {
  1363. this.pointEditData.actionMenuList.splice(index, 1);
  1364. },
  1365. // 修改动作下拉值
  1366. changeAction(index) {
  1367. // 判断当前修改后是否是原定等待,如果不是则删除other属性
  1368. let item = this.pointEditData.actionMenuList[index];
  1369. if (item && 'other' in item) {
  1370. delete item.other; // 删除 'other' 属性
  1371. } else {
  1372. item.other = 0;
  1373. }
  1374. },
  1375. clearActionDia() {
  1376. this.pointEditData.id = '';
  1377. this.pointEditData.x = '';
  1378. this.pointEditData.y = '';
  1379. this.pointEditData.type = '';
  1380. this.pointEditData.actionMenuList = [];
  1381. },
  1382. // 提交点位修改
  1383. submitEditPoint() {
  1384. this.pointEditDiaShow = false;
  1385. // 模拟数据修改
  1386. const point = this.pointList.find(item => item.id === this.pointEditData.id);
  1387. if (this.pointEditData.actionMenuList)
  1388. if (point) {
  1389. // 找到对应元素,更新数据(真实情况下发请求修改,然后重新查询点位列表)
  1390. point.x = this.pointEditData.x;
  1391. point.y = this.pointEditData.y;
  1392. point.type = this.pointEditData.type;
  1393. point.action = this.pointEditData.actionMenuList;
  1394. this.$modal.msgSuccess("当前点位数据已修改");
  1395. }
  1396. },
  1397. // 生成任务
  1398. submitTaskGenerate() {
  1399. // 使用保存的选中目标点数据
  1400. const orderedPoints = this.generateTaskParam.selectedWaypoints || [];
  1401. if (!this.generateTaskParam.taskName || orderedPoints.length === 0) {
  1402. this.$message({
  1403. message: '请完善任务数据(任务名称和目标点不能为空)!',
  1404. type: 'warning'
  1405. });
  1406. return;
  1407. }
  1408. // 生成任务ID和时间戳
  1409. this.generateTaskParam.taskId = Math.floor(Math.random() * 10001);
  1410. const timestamp = new Date().getTime();
  1411. let taskData = {
  1412. taskId: this.generateTaskParam.taskId,
  1413. taskName: this.generateTaskParam.taskName,
  1414. count: this.generateTaskParam.count || 1,
  1415. time: this.generateTaskParam.time,
  1416. date: this.generateTaskParam.date || [],
  1417. status: 'idle',
  1418. points: [...orderedPoints], // 深拷贝目标点数据
  1419. createdAt: timestamp,
  1420. executionStatus: 'idle', // idle, executing, paused, completed, failed, cancelled
  1421. currentWaypointIndex: 0,
  1422. totalWaypoints: orderedPoints.length
  1423. }
  1424. // 添加到任务列表
  1425. this.tasks.push(taskData);
  1426. // 重置表单和状态
  1427. this.restGenerateParam();
  1428. this.taskGenerateDiaShow = false;
  1429. this.selectedWaypointIds = [];
  1430. this.$message.success(`多点位任务创建成功!任务包含 ${orderedPoints.length} 个目标点`);
  1431. console.log("创建的任务:", taskData);
  1432. },
  1433. // 初始化导航
  1434. initNavigation() {
  1435. this.closeDra()
  1436. if (this.poseInitEnable) {
  1437. this.poseInitEnable = false;
  1438. this.nowHandMenu = ''
  1439. } else {
  1440. this.poseInitEnable = true;
  1441. this.nowHandMenu = '初始化导航'
  1442. }
  1443. this.pointSelectionEnabled = false;
  1444. this.activeIndex = this.activeIndex == 3 ? -1 : 3;
  1445. },
  1446. // 重启导航
  1447. restNavigation() {
  1448. this.$confirm('将重启当前导航, 是否继续?', '提示', {
  1449. confirmButtonText: '确定',
  1450. cancelButtonText: '取消',
  1451. customClass: 'el-message-box-cust',
  1452. type: 'warning'
  1453. }).then(() => {
  1454. this.$message({
  1455. type: 'success',
  1456. message: '导航已重启!'
  1457. });
  1458. }).catch(() => { });
  1459. },
  1460. // 关闭导航
  1461. offNavigation() {
  1462. this.$confirm('将关闭当前导航, 是否继续?', '提示', {
  1463. confirmButtonText: '确定',
  1464. cancelButtonText: '取消',
  1465. customClass: 'el-message-box-cust',
  1466. type: 'warning'
  1467. }).then(() => {
  1468. this.$message({
  1469. type: 'success',
  1470. message: '导航已关闭!'
  1471. });
  1472. }).catch(() => { });
  1473. },
  1474. // 关闭任务生成弹窗
  1475. closeTaskGenerate() {
  1476. this.taskGenerateDiaShow = false;
  1477. this.restGenerateParam();
  1478. },
  1479. // 重置任务创建弹窗数据
  1480. restGenerateParam() {
  1481. this.generateTaskParam = {
  1482. taskId: '',
  1483. taskName: '',
  1484. count: 1,
  1485. time: '',
  1486. date: [], // 1-7分别指代周一到周末
  1487. selectedWaypoints: [] // 清空选中的目标点
  1488. }
  1489. },
  1490. // 任务删除
  1491. removeTaskItem(data) {
  1492. this.$confirm('删除名为' + data.taskName + '的任务', '删除', {
  1493. confirmButtonText: '确定',
  1494. cancelButtonText: '取消',
  1495. customClass: 'el-message-box-cust',
  1496. type: 'warning'
  1497. }).then(() => {
  1498. this.$message({
  1499. type: 'success',
  1500. message: '已删除!'
  1501. });
  1502. this.taskDataList = this.taskDataList.filter(task => task.taskId !== data.taskId);
  1503. }).catch(() => { });
  1504. },
  1505. // 执行任务
  1506. executeTask(data) {
  1507. this.$confirm('开始执行任务' + data.taskName + '?', '执行', {
  1508. confirmButtonText: '确定',
  1509. cancelButtonText: '取消',
  1510. customClass: 'el-message-box-cust',
  1511. type: 'warning'
  1512. }).then(() => {
  1513. this.$message({
  1514. type: 'success',
  1515. message: '任务已开始执行!'
  1516. });
  1517. this.taskDataList.forEach(task => {
  1518. if (task.taskId === data.taskId) {
  1519. task.status = 0;
  1520. }
  1521. });
  1522. }).catch(() => { });
  1523. },
  1524. test() {
  1525. console.log(this.set);
  1526. },
  1527. // 开启地图选点
  1528. mapSelectEle(type) {
  1529. if (type == 'open') {
  1530. this.pointSelectionEnabled = true;
  1531. this.poseInitEnable = false;
  1532. this.$notify({
  1533. title: '地图选择',
  1534. message: '已开启选择模式',
  1535. type: 'success',
  1536. duration: 1000
  1537. });
  1538. } else {
  1539. this.pointSelectionEnabled = false;
  1540. this.$notify.info({
  1541. title: '关闭选择',
  1542. message: '已关闭选择模式',
  1543. duration: 1000
  1544. });
  1545. }
  1546. },
  1547. // 将当前选择的点位数据添加到点位列表中
  1548. // currentCoordinate 坐标信息 currentPlace 画布位置信息
  1549. addNowPoint(currentCoordinate, currentPlace) {
  1550. let coordData = {
  1551. id: currentCoordinate[0],
  1552. name: `目标点${currentCoordinate[0]}`,
  1553. x: currentCoordinate[1].toFixed(3),
  1554. y: currentCoordinate[2].toFixed(3),
  1555. placeX: currentPlace[0].toFixed(3),
  1556. placeY: currentPlace[1].toFixed(3),
  1557. type: 0,
  1558. action: [{ value: 0, other: 0 }]
  1559. }
  1560. this.waypoints.push(coordData);
  1561. this.$message.success(`已添加目标点: (${coordData.x}, ${coordData.y})`);
  1562. },
  1563. /**
  1564. * 位姿初始化操作绘制的回执
  1565. * @param position 坐标
  1566. * @param angle 角度
  1567. */
  1568. initNavigationResult(position, angle, nid) {
  1569. let num = nid.split("_")[1];// 获取点位id编号
  1570. this.$refs.mqtt.publish(this.$mqttPrefix + "/localization/action/init", {
  1571. "timestamp" : 123456,
  1572. "args" : [
  1573. {"nid" : Number(num)}
  1574. ]
  1575. },2,false
  1576. );
  1577. console.log(position);
  1578. console.log(angle);
  1579. },
  1580. // === 新UI相关方法 ===
  1581. // Tab切换事件
  1582. onTabChange(tabKey) {
  1583. this.activeTab = tabKey;
  1584. this.lastTab = tabKey;
  1585. // 根据Tab自动调整模式
  1586. if (tabKey === 'points' && this.selectPointMode) {
  1587. // 保持选点模式
  1588. } else if (tabKey !== 'points') {
  1589. // 切换到其他Tab时退出选点模式
  1590. this.selectPointMode = false;
  1591. this.nowHandMenu = '';
  1592. }
  1593. },
  1594. // 切换选点模式
  1595. toggleSelectPointMode() {
  1596. this.selectPointMode = !this.selectPointMode;
  1597. this.initPoseMode = false;
  1598. this.nowHandMenu = this.selectPointMode ? '选点模式' : '';
  1599. if (this.selectPointMode) {
  1600. this.$message.success('已进入选点模式');
  1601. } else {
  1602. this.$message.info('已退出选点模式');
  1603. }
  1604. },
  1605. // 清空所有点位
  1606. clearAllPoints() {
  1607. this.$confirm('确定要清空所有点位吗?', '确认清空', {
  1608. confirmButtonText: '确定',
  1609. cancelButtonText: '取消',
  1610. type: 'warning'
  1611. }).then(() => {
  1612. this.pointList = [];
  1613. if (this.$refs.olmap && this.$refs.olmap.restIdNum) {
  1614. this.$refs.olmap.restIdNum();
  1615. }
  1616. this.$message.success('已清空所有点位');
  1617. }).catch(() => {});
  1618. },
  1619. // 地图缩放
  1620. handleZoomIn() {
  1621. try {
  1622. const map = this.getMapInstance();
  1623. if (map) {
  1624. // OpenLayers API
  1625. const view = map.getView();
  1626. const currentZoom = view.getZoom();
  1627. view.animate({
  1628. zoom: currentZoom + 1,
  1629. duration: 250
  1630. });
  1631. } else if (this.$refs.olmap && this.$refs.olmap.zoomIn) {
  1632. // 备用方法
  1633. this.$refs.olmap.zoomIn();
  1634. }
  1635. } catch (error) {
  1636. console.warn('地图放大失败:', error);
  1637. this.$message.warning('地图放大失败');
  1638. }
  1639. },
  1640. handleZoomOut() {
  1641. try {
  1642. const map = this.getMapInstance();
  1643. if (map) {
  1644. // OpenLayers API
  1645. const view = map.getView();
  1646. const currentZoom = view.getZoom();
  1647. view.animate({
  1648. zoom: Math.max(currentZoom - 1, 1),
  1649. duration: 250
  1650. });
  1651. } else if (this.$refs.olmap && this.$refs.olmap.zoomOut) {
  1652. // 备用方法
  1653. this.$refs.olmap.zoomOut();
  1654. }
  1655. } catch (error) {
  1656. console.warn('地图缩小失败:', error);
  1657. this.$message.warning('地图缩小失败');
  1658. }
  1659. },
  1660. // 居中到机器人
  1661. handleCenterToRobot() {
  1662. try {
  1663. const map = this.getMapInstance();
  1664. if (map) {
  1665. // OpenLayers API
  1666. const view = map.getView();
  1667. // 与标定页面保持一致的实现
  1668. let centerPoint = [this.robotPoseData.x, this.robotPoseData.y];
  1669. if (this.robotPoseData.x === 0 && this.robotPoseData.y === 0) {
  1670. // 使用当前地图中心点作为默认位置
  1671. centerPoint = view.getCenter();
  1672. console.log('使用地图中心点作为居中位置:', centerPoint);
  1673. } else {
  1674. console.log('使用机器人位置作为居中位置:', centerPoint);
  1675. }
  1676. view.animate({
  1677. center: centerPoint,
  1678. zoom: Math.max(view.getZoom(), 15),
  1679. duration: 500
  1680. });
  1681. this.$message.success('已居中到机器人位置');
  1682. } else if (this.$refs.olmap && this.$refs.olmap.centerToRobot) {
  1683. // 备用方法
  1684. this.$refs.olmap.centerToRobot();
  1685. this.$message.success('已居中到机器人位置');
  1686. } else {
  1687. console.warn('无法获取地图实例');
  1688. this.$message.warning('地图未就绪,无法居中');
  1689. }
  1690. } catch (error) {
  1691. console.error('定位机器人失败:', error);
  1692. console.log('调试信息:', {
  1693. robotPoseData: this.robotPoseData,
  1694. robotPosition: this.robotPosition,
  1695. mapReady: !!this.getMapInstance()
  1696. });
  1697. this.$message.warning('定位机器人失败: ' + error.message);
  1698. }
  1699. },
  1700. // 切换全屏
  1701. handleToggleFullscreen() {
  1702. const mapContainer = this.$el.querySelector('.map-stage');
  1703. if (!mapContainer) {
  1704. this.$message.error('无法找到地图容器');
  1705. return;
  1706. }
  1707. if (FullscreenOperations.toggleFullscreen(mapContainer)) {
  1708. // 全屏切换成功,状态会通过监听器自动更新
  1709. } else {
  1710. this.$message.error('浏览器不支持全屏功能');
  1711. }
  1712. },
  1713. // 确认初始化
  1714. handleConfirmInit() {
  1715. this.initPoseMode = true;
  1716. this.selectPointMode = false;
  1717. this.nowHandMenu = '初始化导航';
  1718. this.$message.success('已进入位姿初始化模式');
  1719. },
  1720. // 确认重启
  1721. handleConfirmReboot() {
  1722. this.isBusy = true;
  1723. const timestamp = new Date().getTime();
  1724. // 构建导航重启请求,使用当前地图
  1725. const restartRequest = {
  1726. "timestamp": timestamp,
  1727. "args": [this.mapName] // 重启后使用当前地图
  1728. };
  1729. // 发送导航重启MQTT消息
  1730. this.$refs.mqtt.publish(this.$mqttPrefix + "/navigation/stack/action/restart", restartRequest, 2, false);
  1731. this.$message.success('导航重启指令已发送');
  1732. console.log("发送导航重启请求:", restartRequest);
  1733. // 如果15秒内没有收到响应,自动取消忙碌状态
  1734. setTimeout(() => {
  1735. if (this.isBusy) {
  1736. this.isBusy = false;
  1737. this.$message.warning('导航重启响应超时,请检查系统状态');
  1738. }
  1739. }, 15000);
  1740. },
  1741. // 确认停止
  1742. handleConfirmStop() {
  1743. this.selectPointMode = false;
  1744. this.initPoseMode = false;
  1745. this.nowHandMenu = '';
  1746. // 弹出选择对话框,让用户选择是停止导航还是急停
  1747. this.$confirm('请选择要执行的操作:', '操作选择', {
  1748. confirmButtonText: '急停',
  1749. cancelButtonText: '停止导航',
  1750. distinguishCancelAndClose: true,
  1751. type: 'warning',
  1752. customClass: 'stop-action-dialog'
  1753. }).then(() => {
  1754. // 用户选择急停
  1755. this.executeEmergencyStop();
  1756. }).catch((action) => {
  1757. if (action === 'cancel') {
  1758. // 用户选择停止导航
  1759. this.executeNavigationStop();
  1760. }
  1761. // 如果是关闭对话框(action === 'close'),则不执行任何操作
  1762. });
  1763. },
  1764. // 执行急停操作
  1765. executeEmergencyStop() {
  1766. const timestamp = new Date().getTime();
  1767. // 构建急停请求
  1768. const emergencyStopRequest = {
  1769. "timestamp": timestamp,
  1770. "args": [true] // true表示启用急停
  1771. };
  1772. // 发送急停MQTT消息
  1773. this.$refs.mqtt.publish(this.$mqttPrefix + "/control/vehicle/action/stop", emergencyStopRequest, 1, false);
  1774. this.$message.warning('急停指令已发送');
  1775. console.log("发送急停请求:", emergencyStopRequest);
  1776. },
  1777. // 执行导航停止操作
  1778. executeNavigationStop() {
  1779. const timestamp = new Date().getTime();
  1780. // 构建导航停止请求
  1781. const stopRequest = {
  1782. "timestamp": timestamp,
  1783. "args": null
  1784. };
  1785. // 发送导航停止MQTT消息
  1786. this.$refs.mqtt.publish(this.$mqttPrefix + "/navigation/stack/action/stop", stopRequest, 2, false);
  1787. this.$message.info('导航停止指令已发送');
  1788. console.log("发送导航停止请求:", stopRequest);
  1789. },
  1790. // 解除急停(可以在UI中添加按钮调用)
  1791. executeEmergencyStopRelease() {
  1792. const timestamp = new Date().getTime();
  1793. // 构建解除急停请求
  1794. const releaseRequest = {
  1795. "timestamp": timestamp,
  1796. "args": [false] // false表示解除急停
  1797. };
  1798. // 发送解除急停MQTT消息
  1799. this.$refs.mqtt.publish(this.$mqttPrefix + "/control/vehicle/action/stop", releaseRequest, 1, false);
  1800. this.$message.success('解除急停指令已发送');
  1801. console.log("发送解除急停请求:", releaseRequest);
  1802. },
  1803. // 任务相关方法
  1804. showCreateTaskDialog() {
  1805. this.$message.info('创建任务功能待接入');
  1806. },
  1807. startTask(task) {
  1808. task.status = 'running';
  1809. this.$message.success(`任务 "${task.name}" 已开始执行`);
  1810. },
  1811. pauseTask(task) {
  1812. task.status = 'paused';
  1813. this.$message.warning(`任务 "${task.name}" 已暂停`);
  1814. },
  1815. resumeTask(task) {
  1816. task.status = 'running';
  1817. this.$message.success(`任务 "${task.name}" 已继续执行`);
  1818. },
  1819. cancelTask(task) {
  1820. this.$confirm(`确定要取消任务 "${task.name}" 吗?`, '确认取消', {
  1821. confirmButtonText: '确定',
  1822. cancelButtonText: '取消',
  1823. type: 'warning'
  1824. }).then(() => {
  1825. task.status = 'idle';
  1826. this.$message.info(`任务 "${task.name}" 已取消`);
  1827. }).catch(() => {});
  1828. },
  1829. // 切换初始化模式
  1830. toggleInitPoseMode() {
  1831. this.initPoseMode = !this.initPoseMode;
  1832. this.selectPointMode = false;
  1833. this.nowHandMenu = this.initPoseMode ? '初始化导航' : '';
  1834. if (this.initPoseMode) {
  1835. this.$message.success('已进入位姿初始化模式');
  1836. } else {
  1837. this.$message.info('已退出位姿初始化模式');
  1838. }
  1839. },
  1840. // RightPanel 事件处理方法
  1841. onRestart() {
  1842. this.handleConfirmReboot();
  1843. },
  1844. onStop() {
  1845. this.handleConfirmStop();
  1846. },
  1847. onInit() {
  1848. this.handleConfirmInit();
  1849. },
  1850. // MapToolbar 方法别名
  1851. onZoomIn() {
  1852. this.handleZoomIn();
  1853. },
  1854. onZoomOut() {
  1855. this.handleZoomOut();
  1856. },
  1857. onCenterRobot() {
  1858. this.handleCenterToRobot();
  1859. },
  1860. onToggleFullscreen() {
  1861. this.handleToggleFullscreen();
  1862. },
  1863. // 辅助方法
  1864. getTaskStatusText(status) {
  1865. const statusMap = {
  1866. 'idle': '空闲',
  1867. 'running': '执行中',
  1868. 'paused': '暂停'
  1869. };
  1870. return statusMap[status] || '未知';
  1871. },
  1872. // === 导航页RightPanel事件处理 ===
  1873. // 目标点事件
  1874. onWpSelect(waypoint) {
  1875. console.log('选择目标点:', waypoint);
  1876. this.$message.success(`已选择目标点: ${waypoint.name}`);
  1877. },
  1878. onWpSend(waypoint) {
  1879. console.log('发送目标点:', waypoint);
  1880. this.$message.success(`已发送目标点: ${waypoint.name}`);
  1881. },
  1882. onWpCreate() {
  1883. console.log('创建目标点');
  1884. this.$message.info('创建目标点功能待实现');
  1885. },
  1886. onWpEdit(waypoint) {
  1887. console.log('编辑目标点:', waypoint);
  1888. this.pointEditDiaShow = true;
  1889. this.pointEditData.id = waypoint.id;
  1890. this.pointEditData.x = waypoint.x;
  1891. this.pointEditData.y = waypoint.y;
  1892. this.pointEditData.type = waypoint.type;
  1893. this.pointEditData.actionMenuList = waypoint.action ? [...waypoint.action] : [{ value: 0, other: 0 }];
  1894. },
  1895. onWpRemove(waypoint) {
  1896. console.log('删除目标点:', waypoint);
  1897. // 从waypoints数组中删除目标点
  1898. this.waypoints = this.waypoints.filter(wp => wp.id !== waypoint.id);
  1899. // 如果删除的是选中的目标点,也要从选中列表中移除
  1900. this.selectedWaypointIds = this.selectedWaypointIds.filter(id => id !== waypoint.id);
  1901. // if (this.selectedWaypointIds.length < 1) {
  1902. // // 重置地图的id计数器
  1903. // this.$refs.olmap.restIdNum();
  1904. // }
  1905. this.$refs.olmap.removeIconHtmlById("calibration-"+waypoint.id)
  1906. this.$message.success(`已删除目标点: ${waypoint.name || '目标点'}`);
  1907. },
  1908. // 新增的目标点操作方法
  1909. onWpMoveUp() {
  1910. if (this.selectedWaypointIds.length !== 1) return;
  1911. const selectedId = this.selectedWaypointIds[0];
  1912. const index = this.waypoints.findIndex(wp => wp.id === selectedId);
  1913. if (index > 0) {
  1914. // 交换位置
  1915. const temp = this.waypoints[index];
  1916. this.$set(this.waypoints, index, this.waypoints[index - 1]);
  1917. this.$set(this.waypoints, index - 1, temp);
  1918. this.$message.success('目标点已上移');
  1919. }
  1920. },
  1921. onWpMoveDown() {
  1922. if (this.selectedWaypointIds.length !== 1) return;
  1923. const selectedId = this.selectedWaypointIds[0];
  1924. const index = this.waypoints.findIndex(wp => wp.id === selectedId);
  1925. if (index < this.waypoints.length - 1) {
  1926. // 交换位置
  1927. const temp = this.waypoints[index];
  1928. this.$set(this.waypoints, index, this.waypoints[index + 1]);
  1929. this.$set(this.waypoints, index + 1, temp);
  1930. this.$message.success('目标点已下移');
  1931. }
  1932. },
  1933. onWpBatchRemove() {
  1934. if (this.selectedWaypointIds.length === 0) return;
  1935. this.$confirm(`确定要删除选中的 ${this.selectedWaypointIds.length} 个目标点吗?`, '批量删除', {
  1936. confirmButtonText: '确定',
  1937. cancelButtonText: '取消',
  1938. type: 'warning'
  1939. }).then(() => {
  1940. // 删除选中的目标点
  1941. // this.waypoints = this.waypoints.filter(wp => !this.selectedWaypointIds.includes(wp.id));
  1942. // 获取所有即将被删除的 waypoints
  1943. const waypointsToRemove = this.waypoints.filter(
  1944. wp => this.selectedWaypointIds.includes(wp.id)
  1945. );
  1946. // 从地图上移除图标
  1947. waypointsToRemove.forEach(wp => {
  1948. this.$refs.olmap.removeIconHtmlById("calibration-" + wp.id);
  1949. });
  1950. // 从数组中移除这些 waypoints
  1951. this.waypoints = this.waypoints.filter(
  1952. wp => !this.selectedWaypointIds.includes(wp.id)
  1953. );
  1954. this.selectedWaypointIds = [];
  1955. this.selectedWaypoints = [];
  1956. this.$message.success('目标点删除成功');
  1957. }).catch(() => {});
  1958. },
  1959. onWpGoto() {
  1960. if (this.selectedWaypointIds.length !== 1) return;
  1961. // 检查是否正在导航
  1962. if (this.isNavigating) {
  1963. this.$message.warning('机器人正在导航中,请等待任务完成后再发送新任务');
  1964. return;
  1965. }
  1966. const selectedWaypoint = this.waypoints.find(wp => wp.id === this.selectedWaypointIds[0]);
  1967. console.log('前往目标点:', selectedWaypoint);
  1968. if (selectedWaypoint) {
  1969. const timestamp = new Date().getTime();
  1970. // 构建goto请求
  1971. const gotoRequest = {
  1972. "timestamp": timestamp,
  1973. "args": [
  1974. {
  1975. "roadmap": this.mapName,
  1976. "nid": [], // 使用空数组,优先使用coord
  1977. "coord": [
  1978. [Number(selectedWaypoint.x), Number(selectedWaypoint.y)]
  1979. ]
  1980. }
  1981. ]
  1982. };
  1983. // 保存当前任务状态
  1984. this.currentNavigationTask = {
  1985. waypoint: selectedWaypoint,
  1986. timestamp: timestamp,
  1987. status: 'planning'
  1988. };
  1989. this.lastGotoRequest = gotoRequest;
  1990. this.navigationStatus = 'planning';
  1991. // 发送路径规划请求(用于轨迹显示)
  1992. const planRequest = {
  1993. "timestamp": timestamp,
  1994. "args": [
  1995. {
  1996. "roadmap": this.mapName,
  1997. "nid": [],
  1998. "coord": [
  1999. [Number(selectedWaypoint.x), Number(selectedWaypoint.y)]
  2000. ]
  2001. }
  2002. ]
  2003. };
  2004. // 发送MQTT消息
  2005. this.$refs.mqtt.publish(this.$mqttPrefix + "/planning/service/plan/request", planRequest, 2, false);
  2006. this.$refs.mqtt.publish(this.$mqttPrefix + "/task/target/action/goto", gotoRequest, 2, false);
  2007. this.$message.success(`正在发送前往指令: ${selectedWaypoint.name || '目标点' + selectedWaypoint.id} (${selectedWaypoint.x}, ${selectedWaypoint.y})`);
  2008. console.log("发送前往目标点请求:", gotoRequest);
  2009. console.log("发送路径规划请求:", planRequest);
  2010. }
  2011. },
  2012. onWpGotoSingle(waypoint) {
  2013. // 检查是否正在导航
  2014. if (this.isNavigating) {
  2015. this.$message.warning('机器人正在导航中,请等待任务完成后再发送新任务');
  2016. return;
  2017. }
  2018. const timestamp = new Date().getTime();
  2019. // 构建goto请求
  2020. const gotoRequest = {
  2021. "timestamp": timestamp,
  2022. "args": [
  2023. {
  2024. "roadmap": this.mapName,
  2025. "nid": [], // 使用空数组,优先使用coord
  2026. "coord": [
  2027. [Number(waypoint.x), Number(waypoint.y)]
  2028. ]
  2029. }
  2030. ]
  2031. };
  2032. // 保存当前任务状态
  2033. this.currentNavigationTask = {
  2034. waypoint: waypoint,
  2035. timestamp: timestamp,
  2036. status: 'planning'
  2037. };
  2038. this.lastGotoRequest = gotoRequest;
  2039. this.navigationStatus = 'planning';
  2040. // 发送路径规划请求(用于轨迹显示)
  2041. const planRequest = {
  2042. "timestamp": timestamp,
  2043. "args": [
  2044. {
  2045. "roadmap": this.mapName,
  2046. "nid": [],
  2047. "coord": [
  2048. [Number(waypoint.x), Number(waypoint.y)]
  2049. ]
  2050. }
  2051. ]
  2052. };
  2053. // 发送MQTT消息
  2054. this.$refs.mqtt.publish(this.$mqttPrefix + "/planning/service/plan/request", planRequest, 2, false);
  2055. this.$refs.mqtt.publish(this.$mqttPrefix + "/task/target/action/goto", gotoRequest, 2, false);
  2056. this.$message.success(`正在发送前往指令: ${waypoint.name || '目标点' + waypoint.id} (${waypoint.x}, ${waypoint.y})`);
  2057. console.log("发送前往目标点请求:", gotoRequest);
  2058. console.log("发送路径规划请求:", planRequest);
  2059. },
  2060. // 取消当前导航任务
  2061. cancelCurrentNavigation() {
  2062. if (!this.isNavigating) {
  2063. this.$message.info('当前没有正在进行的导航任务');
  2064. return;
  2065. }
  2066. this.$confirm('确定要取消当前的导航任务吗?', '取消导航', {
  2067. confirmButtonText: '确定',
  2068. cancelButtonText: '取消',
  2069. type: 'warning'
  2070. }).then(() => {
  2071. // 重置导航状态
  2072. this.navigationStatus = 'idle';
  2073. this.isNavigating = false;
  2074. this.currentNavigationTask = null;
  2075. this.lastGotoRequest = null;
  2076. // 清除轨迹
  2077. if (this.$refs.olmap && this.$refs.olmap.clearTrajectory) {
  2078. this.$refs.olmap.clearTrajectory();
  2079. }
  2080. this.$message.success('导航任务已取消');
  2081. console.log('用户取消了导航任务');
  2082. }).catch(() => {
  2083. // 用户点击了取消
  2084. });
  2085. },
  2086. // 执行下一个目标点
  2087. executeNextWaypoint() {
  2088. if (!this.currentExecutingTask || !this.currentExecutingTask.points) {
  2089. console.error('没有正在执行的任务或任务没有目标点');
  2090. return;
  2091. }
  2092. const waypoints = this.currentExecutingTask.points;
  2093. const currentIndex = this.currentTaskWaypointIndex;
  2094. if (currentIndex >= waypoints.length) {
  2095. console.log('所有目标点已执行完成');
  2096. return;
  2097. }
  2098. const currentWaypoint = waypoints[currentIndex];
  2099. console.log(`执行目标点 ${currentIndex + 1}/${waypoints.length}:`, currentWaypoint);
  2100. const timestamp = new Date().getTime();
  2101. // 构建goto请求
  2102. const gotoRequest = {
  2103. "timestamp": timestamp,
  2104. "args": [
  2105. {
  2106. "roadmap": this.mapName,
  2107. "nid": [],
  2108. "coord": [
  2109. [Number(currentWaypoint.x), Number(currentWaypoint.y)]
  2110. ]
  2111. }
  2112. ]
  2113. };
  2114. // 发送路径规划请求(用于轨迹显示)
  2115. const planRequest = {
  2116. "timestamp": timestamp,
  2117. "args": [
  2118. {
  2119. "roadmap": this.mapName,
  2120. "nid": [],
  2121. "coord": [
  2122. [Number(currentWaypoint.x), Number(currentWaypoint.y)]
  2123. ]
  2124. }
  2125. ]
  2126. };
  2127. // 保存当前导航状态
  2128. this.currentNavigationTask = {
  2129. waypoint: currentWaypoint,
  2130. timestamp: timestamp,
  2131. taskContext: {
  2132. taskId: this.currentExecutingTask.taskId,
  2133. waypointIndex: currentIndex,
  2134. totalWaypoints: waypoints.length
  2135. }
  2136. };
  2137. this.lastGotoRequest = gotoRequest;
  2138. this.navigationStatus = 'planning';
  2139. this.isNavigating = true;
  2140. // 发送MQTT消息
  2141. this.$refs.mqtt.publish(this.$mqttPrefix + "/planning/service/plan/request", planRequest, 2, false);
  2142. this.$refs.mqtt.publish(this.$mqttPrefix + "/task/target/action/goto", gotoRequest, 2, false);
  2143. this.$message.info(`正在前往第 ${currentIndex + 1} 个目标点: ${currentWaypoint.name || '目标点' + currentWaypoint.id} (${currentWaypoint.x}, ${currentWaypoint.y})`);
  2144. console.log("发送前往目标点请求:", gotoRequest);
  2145. },
  2146. // 处理任务中的目标点到达事件
  2147. handleTaskWaypointArrival() {
  2148. if (!this.currentExecutingTask) return;
  2149. this.currentTaskWaypointIndex++;
  2150. const totalWaypoints = this.currentExecutingTask.points.length;
  2151. console.log(`目标点到达,进度: ${this.currentTaskWaypointIndex}/${totalWaypoints}`);
  2152. if (this.currentTaskWaypointIndex >= totalWaypoints) {
  2153. // 所有目标点都已完成,任务结束
  2154. this.$message.success(`任务 "${this.currentExecutingTask.taskName}" 的所有目标点已完成!`);
  2155. console.log('任务所有目标点执行完成');
  2156. } else {
  2157. // 继续执行下一个目标点
  2158. this.$message.success(`第 ${this.currentTaskWaypointIndex} 个目标点已到达,继续前往下一个目标点...`);
  2159. setTimeout(() => {
  2160. this.executeNextWaypoint();
  2161. }, 2000); // 2秒后继续下一个目标点
  2162. }
  2163. },
  2164. onWpCreateTask() {
  2165. if (this.selectedWaypointIds.length === 0) {
  2166. this.$message.warning('请先选择要添加到任务中的目标点');
  2167. return;
  2168. }
  2169. const selectedWaypoints = this.waypoints.filter(wp => this.selectedWaypointIds.includes(wp.id));
  2170. console.log(`准备使用 ${selectedWaypoints.length} 个目标点创建任务`, selectedWaypoints);
  2171. // 将选中的目标点保存到待创建任务中
  2172. this.generateTaskParam.selectedWaypoints = selectedWaypoints;
  2173. // 打开任务创建对话框
  2174. this.taskGenerateDiaShow = true;
  2175. },
  2176. onWpSelectionChange(selection) {
  2177. this.selectedWaypointIds = selection.map(wp => wp.id);
  2178. this.waypointSingle = selection.length !== 1;
  2179. this.waypointMultiple = selection.length === 0;
  2180. console.log('目标点选择变更:', this.selectedWaypointIds);
  2181. },
  2182. onMapSelectModeChange(isActive) {
  2183. this.selectPointMode = isActive;
  2184. this.initPoseMode = false; // 确保互斥
  2185. if (isActive) {
  2186. this.$message.success('已开启地图选点模式,点击地图添加目标点');
  2187. } else {
  2188. this.$message.info('已关闭地图选点模式');
  2189. }
  2190. },
  2191. // === 目标点编辑相关方法 ===
  2192. // 追加动作按钮
  2193. appendActionMenu() {
  2194. this.pointEditData.actionMenuList.push({ value: 0, other: 0 });
  2195. },
  2196. // 删除追加动作按钮
  2197. removeActionMenu(index) {
  2198. this.pointEditData.actionMenuList.splice(index, 1);
  2199. },
  2200. // 修改动作下拉值
  2201. changeAction(index) {
  2202. // 判断当前修改后是否是原定等待,如果不是则删除other属性
  2203. let item = this.pointEditData.actionMenuList[index];
  2204. if (item.value === 0) {
  2205. // 原地等待需要等待时间参数
  2206. if (!('other' in item)) {
  2207. item.other = 0;
  2208. }
  2209. } else {
  2210. // 其他动作不需要等待时间参数
  2211. if ('other' in item) {
  2212. delete item.other;
  2213. }
  2214. }
  2215. },
  2216. // 提交点位修改
  2217. submitEditPoint() {
  2218. this.pointEditDiaShow = false;
  2219. // 查找要修改的目标点
  2220. const waypoint = this.waypoints.find(item => item.id === this.pointEditData.id);
  2221. if (waypoint) {
  2222. // 更新数据
  2223. waypoint.x = this.pointEditData.x;
  2224. waypoint.y = this.pointEditData.y;
  2225. waypoint.type = this.pointEditData.type;
  2226. waypoint.action = [...this.pointEditData.actionMenuList];
  2227. this.$message.success("目标点数据已修改");
  2228. }
  2229. },
  2230. // 清空编辑对话框数据
  2231. clearActionDia() {
  2232. this.pointEditData.id = '';
  2233. this.pointEditData.x = '';
  2234. this.pointEditData.y = '';
  2235. this.pointEditData.type = '';
  2236. this.pointEditData.actionMenuList = [];
  2237. },
  2238. // 任务事件
  2239. onTaskView(task) {
  2240. console.log('查看任务详情:', task);
  2241. this.taskViewData = { ...task };
  2242. this.taskViewDiaShow = true;
  2243. // 在地图上显示任务的所有目标点
  2244. this.showTaskWaypointsOnMap(task);
  2245. },
  2246. onTaskStart(task) {
  2247. console.log('开始任务:', task);
  2248. // 检查是否有其他任务正在执行
  2249. if (this.isTaskExecuting) {
  2250. this.$message.warning('已有任务正在执行,请先完成或取消当前任务');
  2251. return;
  2252. }
  2253. if (!task.points || task.points.length === 0) {
  2254. this.$message.error('任务没有包含目标点,无法执行');
  2255. return;
  2256. }
  2257. const timestamp = new Date().getTime();
  2258. // 构建任务执行请求
  2259. const taskExecRequest = {
  2260. "timestamp": timestamp,
  2261. "args": [
  2262. {
  2263. "name": task.taskName
  2264. }
  2265. ]
  2266. };
  2267. // 保存任务执行状态
  2268. this.currentExecutingTask = task;
  2269. this.currentTaskWaypointIndex = 0;
  2270. this.lastTaskExecRequest = taskExecRequest;
  2271. this.isTaskExecuting = true;
  2272. this.taskExecutionStatus = 'executing';
  2273. // 更新任务状态
  2274. task.status = 'executing';
  2275. task.executionStatus = 'executing';
  2276. // 发送任务执行请求
  2277. this.$refs.mqtt.publish(this.$mqttPrefix + "/task/agent/action/exec", taskExecRequest, 2, false);
  2278. this.$message.success(`正在启动任务 "${task.taskName}",包含 ${task.points.length} 个目标点`);
  2279. console.log("发送任务执行请求:", taskExecRequest);
  2280. },
  2281. onTaskPause(task) {
  2282. console.log('暂停任务:', task);
  2283. if (!this.isTaskExecuting || this.currentExecutingTask?.taskId !== task.taskId) {
  2284. this.$message.warning('该任务当前未在执行中');
  2285. return;
  2286. }
  2287. const timestamp = new Date().getTime();
  2288. // 构建暂停任务请求
  2289. const pauseRequest = {
  2290. "timestamp": timestamp,
  2291. "args": null
  2292. };
  2293. // 发送暂停任务请求
  2294. this.$refs.mqtt.publish(this.$mqttPrefix + "/task/procedure/action/pause", pauseRequest, 1, false);
  2295. console.log("发送暂停任务请求:", pauseRequest);
  2296. },
  2297. onTaskStop(task) {
  2298. console.log('停止任务:', task);
  2299. if (!this.isTaskExecuting || this.currentExecutingTask?.taskId !== task.taskId) {
  2300. this.$message.warning('该任务当前未在执行中');
  2301. return;
  2302. }
  2303. this.$confirm(`确定要取消任务 "${task.taskName}" 吗?`, '取消任务', {
  2304. confirmButtonText: '确定',
  2305. cancelButtonText: '取消',
  2306. type: 'warning'
  2307. }).then(() => {
  2308. const timestamp = new Date().getTime();
  2309. // 构建取消任务请求
  2310. const cancelRequest = {
  2311. "timestamp": timestamp,
  2312. "args": null
  2313. };
  2314. // 发送取消任务请求
  2315. this.$refs.mqtt.publish(this.$mqttPrefix + "/task/procedure/action/cancel", cancelRequest, 1, false);
  2316. console.log("发送取消任务请求:", cancelRequest);
  2317. }).catch(() => {
  2318. // 用户取消操作
  2319. });
  2320. },
  2321. onTaskRemove(task) {
  2322. console.log('删除任务:', task);
  2323. // 检查任务是否正在执行
  2324. if (this.isTaskExecuting && this.currentExecutingTask?.taskId === task.taskId) {
  2325. this.$message.warning('任务正在执行中,无法删除。请先停止任务。');
  2326. return;
  2327. }
  2328. this.$confirm(`确定要删除任务 "${task.taskName}" 吗?`, '删除任务', {
  2329. confirmButtonText: '确定',
  2330. cancelButtonText: '取消',
  2331. type: 'warning'
  2332. }).then(() => {
  2333. // 从任务列表中移除
  2334. const index = this.tasks.findIndex(t => t.taskId === task.taskId);
  2335. if (index !== -1) {
  2336. this.tasks.splice(index, 1);
  2337. this.$message.success(`任务 "${task.taskName}" 已删除`);
  2338. }
  2339. }).catch(() => {
  2340. // 用户取消操作
  2341. });
  2342. },
  2343. // 继续任务(用于暂停后的恢复)
  2344. onTaskResume(task) {
  2345. console.log('继续任务:', task);
  2346. if (!this.currentExecutingTask || this.currentExecutingTask.taskId !== task.taskId) {
  2347. this.$message.warning('该任务当前未在暂停状态');
  2348. return;
  2349. }
  2350. if (this.taskExecutionStatus !== 'paused') {
  2351. this.$message.warning('任务当前不在暂停状态');
  2352. return;
  2353. }
  2354. const timestamp = new Date().getTime();
  2355. // 构建继续任务请求
  2356. const resumeRequest = {
  2357. "timestamp": timestamp,
  2358. "args": null
  2359. };
  2360. // 发送继续任务请求
  2361. this.$refs.mqtt.publish(this.$mqttPrefix + "/task/procedure/action/resume", resumeRequest, 2, false);
  2362. console.log("发送继续任务请求:", resumeRequest);
  2363. },
  2364. // 在地图上显示任务的目标点
  2365. showTaskWaypointsOnMap(task) {
  2366. if (!task.points || task.points.length === 0) {
  2367. this.$message.info('该任务没有包含目标点');
  2368. return;
  2369. }
  2370. // 清除现有的轨迹和标记
  2371. if (this.$refs.olmap && this.$refs.olmap.clearTrajectory) {
  2372. this.$refs.olmap.clearTrajectory();
  2373. }
  2374. // 在地图上显示所有目标点
  2375. task.points.forEach((waypoint, index) => {
  2376. if (this.$refs.olmap && this.$refs.olmap.addHtmlIcon) {
  2377. // 使用不同的图标样式来区分任务目标点
  2378. this.$refs.olmap.addHtmlIcon(
  2379. `T${index + 1}`,
  2380. parseFloat(waypoint.x),
  2381. parseFloat(waypoint.y),
  2382. '',
  2383. 'task-waypoint'
  2384. );
  2385. }
  2386. });
  2387. this.$message.success(`已在地图上显示任务 "${task.taskName}" 的 ${task.points.length} 个目标点`);
  2388. console.log(`显示任务目标点:`, task.points);
  2389. },
  2390. // 格式化方法
  2391. getTaskStatusText(status) {
  2392. const statusMap = {
  2393. 0: '运行中',
  2394. 1: '空闲',
  2395. 'idle': '空闲',
  2396. 'running': '运行中',
  2397. 'paused': '暂停',
  2398. 'completed': '已完成',
  2399. 'error': '失败'
  2400. }
  2401. return statusMap[status] || '未知'
  2402. },
  2403. getTaskStatusClass(status) {
  2404. const statusClassMap = {
  2405. 0: 'status-running',
  2406. 1: 'status-idle',
  2407. 'idle': 'status-idle',
  2408. 'running': 'status-running',
  2409. 'paused': 'status-paused',
  2410. 'completed': 'status-completed',
  2411. 'error': 'status-error'
  2412. }
  2413. return statusClassMap[status] || 'status-unknown'
  2414. },
  2415. formatTime(time) {
  2416. if (!time) return '--'
  2417. if (typeof time === 'string') return time
  2418. if (time instanceof Date) {
  2419. return time.toLocaleTimeString('zh-CN', {
  2420. hour12: false,
  2421. hour: '2-digit',
  2422. minute: '2-digit'
  2423. })
  2424. }
  2425. return '--'
  2426. },
  2427. formatDate(dateArray) {
  2428. if (!dateArray || !Array.isArray(dateArray) || dateArray.length === 0) return '--'
  2429. const dayNames = {
  2430. '1': '周一',
  2431. '2': '周二',
  2432. '3': '周三',
  2433. '4': '周四',
  2434. '5': '周五',
  2435. '6': '周六',
  2436. '7': '周日'
  2437. }
  2438. return dateArray.map(day => dayNames[day] || day).join(', ')
  2439. },
  2440. // 格式化秒数为时分秒
  2441. formatSeconds(seconds) {
  2442. if (!seconds || seconds < 0) return '00:00:00';
  2443. const hours = Math.floor(seconds / 3600);
  2444. const minutes = Math.floor((seconds % 3600) / 60);
  2445. const secs = Math.floor(seconds % 60);
  2446. return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  2447. },
  2448. // 功能设置变更处理
  2449. onSettingChange(setting) {
  2450. console.log('功能设置变更:', setting);
  2451. this.settingParams[setting.key] = setting.value;
  2452. // 根据设置项类型执行相应操作
  2453. switch(setting.key) {
  2454. case 'pointCloud':
  2455. this.$message.info(`点云显示已${setting.value ? '开启' : '关闭'}`);
  2456. // 这里可以调用地图组件的点云显示/隐藏方法
  2457. break;
  2458. case 'baseMap':
  2459. this.$message.info(`底图显示已${setting.value ? '开启' : '关闭'}`);
  2460. // 这里可以调用地图组件的底图显示/隐藏方法
  2461. break;
  2462. case 'pointId':
  2463. this.$message.info(`点ID显示已${setting.value ? '开启' : '关闭'}`);
  2464. // 这里可以调用地图组件的点ID显示/隐藏方法
  2465. break;
  2466. case 'follow':
  2467. this.$message.info(`位置跟随已${setting.value ? '开启' : '关闭'}`);
  2468. // 这里可以调用地图组件的跟随模式开启/关闭方法
  2469. break;
  2470. case 'network':
  2471. this.$message.info(`网络邻居显示已${setting.value ? '开启' : '关闭'}`);
  2472. // 这里可以调用相关的网络邻居显示/隐藏方法
  2473. break;
  2474. }
  2475. },
  2476. },
  2477. watch: {
  2478. // 监听面板可见性变化,触发地图刷新
  2479. panelVisible() {
  2480. this.$nextTick(() => {
  2481. this.updateOlCss();
  2482. });
  2483. },
  2484. // 监听选点模式变化
  2485. selectPointMode(newVal) {
  2486. if (newVal) {
  2487. this.initPoseMode = false; // 确保互斥
  2488. }
  2489. },
  2490. // 监听初始化模式变化
  2491. initPoseMode(newVal) {
  2492. if (newVal) {
  2493. this.selectPointMode = false; // 确保互斥
  2494. }
  2495. },
  2496. // 监听实时信息变化,同步机器人位姿数据(已移至handleLaserPose直接处理)
  2497. 'realtimeInfo.coordinates': {
  2498. handler(newCoordinates) {
  2499. if (newCoordinates) {
  2500. const position = RobotPositionUtils.parseCoordinates(newCoordinates);
  2501. if (position) {
  2502. this.robotPoseData.x = position.x;
  2503. this.robotPoseData.y = position.y;
  2504. // 角度从heading字段解析,这里先保持不变
  2505. // this.robotPoseData.angle = parseFloat(this.realtimeInfo.heading.replace('°', '')) || 0;
  2506. }
  2507. }
  2508. },
  2509. immediate: true
  2510. }
  2511. }
  2512. };
  2513. </script>
  2514. <style scoped lang="scss">
  2515. .point-edit-span {
  2516. display: block;
  2517. margin: 10px 0;
  2518. font-weight: bold;
  2519. }
  2520. .drawer {
  2521. height: 100%;
  2522. position: absolute;
  2523. top: 0;
  2524. left: 100%;
  2525. /* box-shadow: 4px 4px 12px rgba(201, 201, 201, 0.2); */
  2526. /* 右边和下边的阴影 */
  2527. border-radius: 0 0 12px 0;
  2528. border-left: 1px solid #F0F0F0;
  2529. padding: 8px 15px;
  2530. border-right: 1px solid #ececec;
  2531. border-bottom: 1px solid #ececec;
  2532. overflow-y: auto;
  2533. background-color: #fff;
  2534. z-index: 1000;
  2535. }
  2536. .drawer-close {
  2537. position: absolute;
  2538. right: 3px;
  2539. top: 3px;
  2540. cursor: pointer;
  2541. }
  2542. .drawer-title {
  2543. position: absolute;
  2544. top: -23px;
  2545. left: -8px;
  2546. font-size: 13px;
  2547. font-weight: bold;
  2548. color: #838383;
  2549. }
  2550. .drawer p {
  2551. font-size: 13px;
  2552. border-left: 5px #D1D1D1 solid;
  2553. padding-left: 5px;
  2554. margin: 8px 0;
  2555. border-radius: 3px 0 0 3px;
  2556. }
  2557. .img-container {
  2558. text-align: center;
  2559. display: flex;
  2560. flex-direction: column;
  2561. align-items: center;
  2562. justify-content: center;
  2563. /* 垂直居中子元素 */
  2564. border-radius: 7px;
  2565. background: linear-gradient(135deg, #00bcd4, #009688);
  2566. cursor: pointer;
  2567. width: 70%;
  2568. aspect-ratio: 1;
  2569. /* 设置宽高比为1,即高度和宽度相等 */
  2570. margin-top: 10px;
  2571. }
  2572. .img-container:hover {
  2573. transform: scale(1.02);
  2574. /* 鼠标悬停时放大 */
  2575. box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
  2576. /* 增加阴影效果 */
  2577. background-color: #00796b;
  2578. /* 改变背景颜色 */
  2579. }
  2580. .img-container:active {
  2581. transform: scale(0.98);
  2582. /* 点击时缩小 */
  2583. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
  2584. /* 点击时加深阴影 */
  2585. background-color: #004d40;
  2586. /* 点击时改变背景颜色 */
  2587. filter: brightness(1.1);
  2588. /* 点击时稍微增加亮度 */
  2589. }
  2590. .img-container img {
  2591. opacity: 1;
  2592. }
  2593. .img-container span {
  2594. font-size: 12px;
  2595. color: #ffffff;
  2596. font-weight: bold;
  2597. }
  2598. /* 激活时的样式 */
  2599. .img-container.active {
  2600. background: linear-gradient(135deg, #007d8d, #004b43);
  2601. }
  2602. .explore-unit {
  2603. margin-left: 8px;
  2604. }
  2605. ::v-deep .el-dialog__body {
  2606. padding: 20px 20px 0 20px;
  2607. }
  2608. ::v-deep .download-map .el-dialog__body {
  2609. padding: 10px 20px 0 20px !important;
  2610. }
  2611. ::v-deep .el-table--medium .el-table__cell {
  2612. padding: 6px 0;
  2613. }
  2614. ::v-deep .action-menu .action-menu_input .el-input__inner {
  2615. padding: 0 5px;
  2616. }
  2617. .task-status-tag {
  2618. margin-left: 20px;
  2619. }
  2620. ::v-deep .drawer .el-collapse-item__header {
  2621. height: 38px;
  2622. line-height: 38px;
  2623. color: #767676;
  2624. font-weight: bold;
  2625. }
  2626. ::v-deep .drawer .el-collapse-item__content {
  2627. text-align: left;
  2628. padding-bottom: 10px;
  2629. }
  2630. ::v-deep .drawer .el-collapse {
  2631. border: 1px solid #EBEEF5;
  2632. padding: 0 8px;
  2633. border-radius: 5px;
  2634. }
  2635. .collapse-content-div {
  2636. margin-top: 0;
  2637. }
  2638. ::v-deep .collapse-content-div .el-button--mini {
  2639. padding: 4px 10px;
  2640. }
  2641. .hand-ment-mark {
  2642. position: absolute;
  2643. bottom: 12px;
  2644. left: 12px;
  2645. z-index: 1000;
  2646. }
  2647. .notification__title {
  2648. font-size: 1.2rem;
  2649. /* 默认字体大小 */
  2650. }
  2651. .navigation-container {
  2652. width: 100%;
  2653. min-height: calc(100vh - 84px);
  2654. overflow: hidden;
  2655. position: relative;
  2656. background: var(--color-bg-secondary);
  2657. .map-stage {
  2658. position: relative;
  2659. width: 100%;
  2660. height: calc(100vh - 84px);
  2661. min-height: 600px;
  2662. overflow: hidden;
  2663. background: var(--color-bg-secondary);
  2664. }
  2665. }
  2666. /* 新UI浮层样式 - 重新设计为浮动面板 */
  2667. .nav-toolbar {
  2668. position: absolute;
  2669. left: 16px;
  2670. top: 96px;
  2671. z-index: 50;
  2672. }
  2673. .main-menu {
  2674. display: flex;
  2675. flex-direction: column;
  2676. justify-content: center;
  2677. align-items: center;
  2678. }
  2679. /* 目标点编辑对话框样式 */
  2680. ::v-deep .waypoint-edit-dialog {
  2681. /* 修复问题1:紫色标题栏圆角对齐 */
  2682. .el-dialog {
  2683. border-radius: 12px !important;
  2684. overflow: hidden !important; /* 确保子元素不会超出圆角 */
  2685. margin-top: 0 !important; /* 移除默认的上边距 */
  2686. margin-bottom: 0 !important; /* 移除默认的下边距 */
  2687. /* 确保对话框垂直居中 */
  2688. position: fixed !important;
  2689. top: 50% !important;
  2690. left: 50% !important;
  2691. transform: translate(-50%, -50%) !important;
  2692. }
  2693. .el-dialog__header {
  2694. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  2695. color: white;
  2696. padding: 20px 24px 16px;
  2697. margin: 0;
  2698. border-radius: 12px 12px 0 0 !important; /* 只有上方圆角 */
  2699. .el-dialog__title {
  2700. color: white;
  2701. font-weight: 600;
  2702. font-size: 16px;
  2703. }
  2704. .el-dialog__close {
  2705. color: white;
  2706. font-size: 18px;
  2707. &:hover {
  2708. color: #f0f0f0;
  2709. }
  2710. }
  2711. }
  2712. .el-dialog__body {
  2713. padding: 24px;
  2714. background: #f8fafc;
  2715. margin: 0 !important; /* 确保没有额外边距 */
  2716. }
  2717. .el-dialog__footer {
  2718. padding: 16px 24px 24px;
  2719. background: #f8fafc;
  2720. border-top: 1px solid #e2e8f0;
  2721. border-radius: 0 0 12px 12px !important; /* 只有下方圆角 */
  2722. margin: 0 !important; /* 确保没有额外边距 */
  2723. }
  2724. }
  2725. .dialog-content {
  2726. .form-section {
  2727. background: white;
  2728. border-radius: 10px; /* 稍微增加圆角,与整体设计更协调 */
  2729. padding: 24px; /* 增加内边距,让内容更宽松 */
  2730. margin-bottom: 20px; /* 增加卡片间距 */
  2731. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); /* 稍微增强阴影 */
  2732. border: 1px solid #f1f5f9; /* 添加淡边框 */
  2733. &:last-child {
  2734. margin-bottom: 0;
  2735. }
  2736. .section-title {
  2737. display: flex;
  2738. align-items: center;
  2739. margin: 0 0 20px 0; /* 增加标题与内容的间距 */
  2740. font-size: 15px; /* 稍微增加字体大小 */
  2741. font-weight: 600;
  2742. color: #2d3748;
  2743. border-bottom: 2px solid #e2e8f0;
  2744. padding-bottom: 10px; /* 增加下内边距 */
  2745. i {
  2746. margin-right: 10px; /* 增加图标与文字的间距 */
  2747. color: #667eea;
  2748. font-size: 18px; /* 稍微增加图标大小 */
  2749. }
  2750. /* 添加动作按钮样式优化 */
  2751. .add-action-btn {
  2752. margin-left: 12px !important;
  2753. display: inline-flex !important;
  2754. align-items: center !important;
  2755. padding: 4px 8px !important;
  2756. i {
  2757. margin-right: 4px !important; /* 减少图标与文字的间距 */
  2758. font-size: 12px !important;
  2759. color: #667eea !important;
  2760. display: inline-block !important;
  2761. vertical-align: middle !important;
  2762. }
  2763. span {
  2764. font-size: 12px !important;
  2765. color: #667eea !important;
  2766. line-height: 1 !important;
  2767. vertical-align: middle !important;
  2768. }
  2769. &:hover {
  2770. i, span {
  2771. color: #409eff !important;
  2772. }
  2773. }
  2774. }
  2775. }
  2776. }
  2777. .waypoint-form {
  2778. .el-form-item {
  2779. margin-bottom: 18px; /* 适中的表单项间距 */
  2780. display: flex !important; /* 使用flex布局 */
  2781. align-items: center !important; /* 标签和输入框水平对齐 */
  2782. &:last-child {
  2783. margin-bottom: 0;
  2784. }
  2785. /* 确保输入框容器占用剩余空间 */
  2786. .el-form-item__content {
  2787. flex: 1 !important;
  2788. margin-left: 0 !important; /* 移除默认左边距 */
  2789. }
  2790. }
  2791. /* 坐标输入框特殊样式 */
  2792. .coordinate-input {
  2793. margin-bottom: 4px; /* 坐标输入框之间的间距稍小 */
  2794. }
  2795. .el-form-item__label {
  2796. font-weight: 600 !important; /* 增加字体粗细,更突出 */
  2797. color: #2d3748 !important; /* 更深的颜色,更清晰 */
  2798. padding-right: 12px !important; /* 适当的间距 */
  2799. min-width: 100px !important; /* 稍微增加宽度,适应新的标签 */
  2800. font-size: 14px !important; /* 统一字体大小 */
  2801. line-height: 44px !important; /* 与输入框高度保持一致,实现垂直居中 */
  2802. height: 44px !important; /* 设置标签高度与输入框一致 */
  2803. display: flex !important; /* 使用flex布局 */
  2804. align-items: center !important; /* 垂直居中对齐 */
  2805. margin-bottom: 0 !important; /* 移除默认下边距 */
  2806. }
  2807. /* 优化坐标输入框样式 - 分行布局 */
  2808. .coordinate-input {
  2809. .el-input__inner {
  2810. border-radius: 8px !important; /* 稍微增加圆角 */
  2811. border: 1px solid #e2e8f0 !important;
  2812. height: 44px !important; /* 增加输入框高度,更宽松 */
  2813. font-size: 15px !important; /* 增加字体大小,更易读 */
  2814. padding: 0 16px !important; /* 增加左右内边距 */
  2815. background: #ffffff !important;
  2816. transition: all 0.2s ease !important;
  2817. &:focus {
  2818. border-color: #667eea !important;
  2819. box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
  2820. background: #fafbff !important;
  2821. }
  2822. &:hover {
  2823. border-color: #cbd5e0 !important;
  2824. }
  2825. }
  2826. }
  2827. /* 通用输入框样式 */
  2828. .el-input {
  2829. .el-input__inner {
  2830. border-radius: 6px;
  2831. border: 1px solid #e2e8f0;
  2832. height: 40px !important;
  2833. font-size: 14px !important;
  2834. padding: 0 12px !important;
  2835. &:focus {
  2836. border-color: #667eea;
  2837. box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
  2838. }
  2839. }
  2840. }
  2841. .el-select {
  2842. .el-input__inner {
  2843. border-radius: 6px;
  2844. height: 40px !important; /* 与输入框保持一致的高度 */
  2845. font-size: 14px !important;
  2846. }
  2847. }
  2848. }
  2849. .action-list {
  2850. .action-item {
  2851. background: #f7fafc;
  2852. border: 1px solid #e2e8f0;
  2853. border-radius: 8px;
  2854. padding: 12px;
  2855. margin-bottom: 12px;
  2856. transition: all 0.2s ease;
  2857. &:hover {
  2858. border-color: #cbd5e0;
  2859. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  2860. }
  2861. &:last-child {
  2862. margin-bottom: 0;
  2863. }
  2864. .action-header {
  2865. display: flex;
  2866. align-items: center;
  2867. gap: 12px;
  2868. margin-bottom: 8px;
  2869. .action-index {
  2870. display: flex;
  2871. align-items: center;
  2872. justify-content: center;
  2873. width: 24px;
  2874. height: 24px;
  2875. background: #667eea;
  2876. color: white;
  2877. border-radius: 50%;
  2878. font-size: 12px;
  2879. font-weight: 600;
  2880. flex-shrink: 0;
  2881. }
  2882. .remove-btn {
  2883. color: #e53e3e;
  2884. padding: 4px;
  2885. &:hover {
  2886. background: #fed7d7;
  2887. color: #c53030;
  2888. }
  2889. }
  2890. }
  2891. .action-params {
  2892. padding-left: 36px;
  2893. margin-top: 12px; /* 增加与上方的间距 */
  2894. display: flex !important; /* 使用flex布局 */
  2895. align-items: center !important; /* 垂直居中对齐 */
  2896. flex-wrap: nowrap !important; /* 防止换行 */
  2897. /* 修复问题3:优化等待时间输入框样式 */
  2898. .el-input {
  2899. display: inline-flex !important; /* 改为inline-flex,防止换行 */
  2900. width: 160px !important; /* 稍微增加宽度,给"秒"单位更多空间 */
  2901. align-items: center !important; /* 确保垂直居中 */
  2902. .el-input__inner {
  2903. background: white;
  2904. height: 36px !important; /* 适中的高度 */
  2905. font-size: 14px !important;
  2906. border-radius: 6px !important;
  2907. border: 1px solid #e2e8f0 !important;
  2908. padding: 0 12px !important;
  2909. flex: 1 !important; /* 输入框占用主要空间 */
  2910. &:focus {
  2911. border-color: #667eea;
  2912. box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
  2913. }
  2914. }
  2915. /* 修复问题3:优化"秒"单位的append样式 */
  2916. .el-input-group__append {
  2917. background: #f8fafc !important;
  2918. border-color: #e2e8f0 !important;
  2919. color: #4a5568 !important;
  2920. font-weight: 500 !important;
  2921. padding: 0 12px !important; /* 调整内边距 */
  2922. border-radius: 0 6px 6px 0 !important;
  2923. font-size: 14px !important;
  2924. min-width: 40px !important; /* 增加最小宽度 */
  2925. height: 36px !important; /* 明确设置高度 */
  2926. display: flex !important;
  2927. align-items: center !important;
  2928. justify-content: center !important;
  2929. border-left: none !important; /* 移除左边框,与输入框无缝连接 */
  2930. white-space: nowrap !important; /* 防止文字换行 */
  2931. flex-shrink: 0 !important; /* 防止收缩 */
  2932. }
  2933. }
  2934. }
  2935. }
  2936. }
  2937. }
  2938. ::v-deep .dialog-footer {
  2939. text-align: right;
  2940. .el-button {
  2941. padding: 10px 20px;
  2942. border-radius: 6px;
  2943. font-weight: 500;
  2944. &.el-button--primary {
  2945. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  2946. border: none;
  2947. &:hover {
  2948. opacity: 0.9;
  2949. }
  2950. }
  2951. }
  2952. }
  2953. /* 创建任务对话框样式 */
  2954. ::v-deep .task-create-dialog {
  2955. /* 修复问题1:紫色标题栏圆角对齐 */
  2956. .el-dialog {
  2957. border-radius: 12px !important;
  2958. overflow: hidden !important; /* 确保子元素不会超出圆角 */
  2959. margin-top: 0 !important; /* 移除默认的上边距 */
  2960. margin-bottom: 0 !important; /* 移除默认的下边距 */
  2961. /* 确保对话框垂直居中 */
  2962. position: fixed !important;
  2963. top: 50% !important;
  2964. left: 50% !important;
  2965. transform: translate(-50%, -50%) !important;
  2966. }
  2967. .el-dialog__header {
  2968. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  2969. color: white;
  2970. padding: 20px 24px 16px;
  2971. margin: 0;
  2972. border-radius: 12px 12px 0 0 !important; /* 只有上方圆角 */
  2973. .el-dialog__title {
  2974. color: white;
  2975. font-weight: 600;
  2976. font-size: 16px;
  2977. }
  2978. .el-dialog__close {
  2979. color: white;
  2980. font-size: 18px;
  2981. &:hover {
  2982. color: #f0f0f0;
  2983. }
  2984. }
  2985. }
  2986. .el-dialog__body {
  2987. padding: 24px;
  2988. background: #fafbfc;
  2989. }
  2990. .el-dialog__footer {
  2991. background: white;
  2992. padding: 16px 24px;
  2993. border-radius: 0 0 12px 12px !important; /* 只有下方圆角 */
  2994. border-top: 1px solid #e2e8f0;
  2995. text-align: right;
  2996. .el-button {
  2997. padding: 10px 20px;
  2998. font-weight: 500;
  2999. border-radius: 6px;
  3000. &:not(.el-button--primary) {
  3001. color: #64748b;
  3002. border-color: #cbd5e1;
  3003. background: white;
  3004. &:hover {
  3005. color: #475569;
  3006. border-color: #94a3b8;
  3007. background: #f8fafc;
  3008. }
  3009. }
  3010. &.el-button--primary {
  3011. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  3012. border: none;
  3013. &:hover {
  3014. background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
  3015. }
  3016. }
  3017. }
  3018. }
  3019. }
  3020. .task-form {
  3021. .el-form-item {
  3022. margin-bottom: 18px; /* 适中的表单项间距 */
  3023. display: flex !important; /* 使用flex布局 */
  3024. align-items: center !important; /* 标签和输入框水平对齐 */
  3025. .el-form-item__label {
  3026. color: #374151;
  3027. font-weight: 500;
  3028. line-height: 44px !important; /* 与输入框高度保持一致,实现垂直居中 */
  3029. height: 44px !important; /* 设置标签高度与输入框一致 */
  3030. display: flex !important; /* 使用flex布局 */
  3031. align-items: center !important; /* 垂直居中对齐 */
  3032. margin-bottom: 0 !important; /* 移除默认下边距 */
  3033. }
  3034. .el-form-item__content {
  3035. flex: 1 !important;
  3036. margin-left: 0 !important; /* 移除默认左边距 */
  3037. }
  3038. }
  3039. }
  3040. .task-input {
  3041. .el-input__inner {
  3042. background: white;
  3043. height: 44px !important; /* 统一输入框高度 */
  3044. font-size: 14px !important;
  3045. border-radius: 8px !important; /* 现代化圆角 */
  3046. border: 1px solid #e2e8f0 !important;
  3047. padding: 0 16px !important; /* 增加内边距 */
  3048. transition: all 0.2s ease !important;
  3049. &:focus {
  3050. border-color: #667eea !important;
  3051. box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
  3052. background: white !important;
  3053. }
  3054. &::placeholder {
  3055. color: #9ca3af !important;
  3056. font-size: 14px !important;
  3057. }
  3058. }
  3059. }
  3060. .task-input-number {
  3061. width: 100% !important;
  3062. display: block !important; /* 确保占满整行 */
  3063. .el-input-number__decrease,
  3064. .el-input-number__increase {
  3065. background: #f8fafc !important;
  3066. border-color: #e2e8f0 !important;
  3067. color: #667eea !important;
  3068. width: 32px !important; /* 固定按钮宽度 */
  3069. height: 22px !important; /* 调整按钮高度,让上下按钮都能显示 */
  3070. line-height: 20px !important; /* 调整行高 */
  3071. &:hover {
  3072. background: #667eea !important;
  3073. color: white !important;
  3074. }
  3075. }
  3076. .el-input-number__increase {
  3077. border-radius: 0 8px 0 0 !important; /* 上按钮圆角 */
  3078. }
  3079. .el-input-number__decrease {
  3080. border-radius: 0 0 8px 0 !important; /* 下按钮圆角 */
  3081. }
  3082. .el-input {
  3083. width: 100% !important; /* 确保input容器占满宽度 */
  3084. }
  3085. .el-input__inner {
  3086. background: white !important;
  3087. height: 44px !important;
  3088. font-size: 14px !important;
  3089. border-radius: 8px !important;
  3090. border: 1px solid #e2e8f0 !important;
  3091. padding: 0 68px 0 16px !important; /* 右侧留出按钮空间 */
  3092. text-align: left !important;
  3093. width: 100% !important; /* 确保输入框占满宽度 */
  3094. &:focus {
  3095. border-color: #667eea !important;
  3096. box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
  3097. }
  3098. }
  3099. }
  3100. .task-time-picker {
  3101. width: 100% !important;
  3102. display: block !important; /* 确保占满整行 */
  3103. .el-input {
  3104. width: 100% !important; /* 确保input容器占满宽度 */
  3105. }
  3106. .el-input__inner {
  3107. background: white !important;
  3108. height: 44px !important;
  3109. font-size: 14px !important;
  3110. border-radius: 8px !important;
  3111. border: 1px solid #e2e8f0 !important;
  3112. padding: 0 80px 0 16px !important; /* 大幅增加右侧内边距,从60px到80px */
  3113. width: 100% !important; /* 确保输入框占满宽度 */
  3114. box-sizing: border-box !important; /* 确保padding计算正确 */
  3115. text-overflow: ellipsis !important; /* 文字溢出处理 */
  3116. overflow: hidden !important;
  3117. white-space: nowrap !important;
  3118. &:focus {
  3119. border-color: #667eea !important;
  3120. box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
  3121. }
  3122. &::placeholder {
  3123. color: #9ca3af !important;
  3124. }
  3125. }
  3126. .el-input__suffix {
  3127. right: 24px !important; /* 进一步增加右侧距离 */
  3128. width: 40px !important; /* 增加图标区域宽度 */
  3129. text-align: center !important;
  3130. display: flex !important;
  3131. align-items: center !important;
  3132. justify-content: center !important;
  3133. height: 44px !important; /* 确保高度与输入框一致 */
  3134. .el-input__icon {
  3135. color: #9ca3af !important;
  3136. font-size: 16px !important;
  3137. margin: 0 !important; /* 移除任何默认边距 */
  3138. }
  3139. }
  3140. }
  3141. .task-date-group {
  3142. display: flex !important;
  3143. flex-wrap: wrap !important;
  3144. gap: 8px 16px !important;
  3145. .el-checkbox {
  3146. margin-right: 0 !important;
  3147. margin-bottom: 8px !important;
  3148. .el-checkbox__label {
  3149. color: #374151 !important;
  3150. font-weight: 500 !important;
  3151. font-size: 14px !important;
  3152. padding-left: 8px !important;
  3153. }
  3154. .el-checkbox__input.is-checked {
  3155. .el-checkbox__inner {
  3156. background-color: #667eea !important;
  3157. border-color: #667eea !important;
  3158. }
  3159. }
  3160. .el-checkbox__inner {
  3161. border-color: #d1d5db !important;
  3162. &:hover {
  3163. border-color: #667eea !important;
  3164. }
  3165. }
  3166. }
  3167. }
  3168. /* 任务查看对话框样式 */
  3169. ::v-deep .task-view-dialog {
  3170. .el-dialog {
  3171. border-radius: 12px !important;
  3172. overflow: hidden !important;
  3173. margin-top: 0 !important;
  3174. margin-bottom: 0 !important;
  3175. position: fixed !important;
  3176. top: 50% !important;
  3177. left: 50% !important;
  3178. transform: translate(-50%, -50%) !important;
  3179. }
  3180. .el-dialog__header {
  3181. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  3182. color: white;
  3183. padding: 20px 24px 16px;
  3184. margin: 0;
  3185. border-radius: 12px 12px 0 0 !important;
  3186. .el-dialog__title {
  3187. color: white;
  3188. font-weight: 600;
  3189. font-size: 16px;
  3190. }
  3191. .el-dialog__close {
  3192. color: white;
  3193. font-size: 18px;
  3194. &:hover {
  3195. color: #f0f0f0;
  3196. }
  3197. }
  3198. }
  3199. .el-dialog__body {
  3200. padding: 24px;
  3201. background: #fafbfc;
  3202. }
  3203. .el-dialog__footer {
  3204. background: white;
  3205. padding: 16px 24px;
  3206. border-radius: 0 0 12px 12px !important;
  3207. border-top: 1px solid #e2e8f0;
  3208. text-align: right;
  3209. .el-button {
  3210. padding: 10px 20px;
  3211. font-weight: 500;
  3212. border-radius: 6px;
  3213. color: #64748b;
  3214. border-color: #cbd5e1;
  3215. background: white;
  3216. &:hover {
  3217. color: #475569;
  3218. border-color: #94a3b8;
  3219. background: #f8fafc;
  3220. }
  3221. }
  3222. }
  3223. }
  3224. .task-view-form {
  3225. .el-form-item {
  3226. margin-bottom: 20px;
  3227. display: flex !important;
  3228. align-items: center !important;
  3229. min-height: 32px;
  3230. &:last-child {
  3231. margin-bottom: 20px;
  3232. }
  3233. .el-form-item__label {
  3234. color: #374151;
  3235. font-weight: 500;
  3236. line-height: 1 !important;
  3237. display: flex !important;
  3238. align-items: center !important;
  3239. margin-bottom: 0 !important;
  3240. height: auto !important;
  3241. padding: 0 !important;
  3242. }
  3243. .el-form-item__content {
  3244. flex: 1 !important;
  3245. margin-left: 0 !important;
  3246. display: flex !important;
  3247. align-items: center !important;
  3248. }
  3249. }
  3250. .form-text {
  3251. color: #606266;
  3252. font-size: 14px;
  3253. line-height: 1;
  3254. display: flex;
  3255. align-items: center;
  3256. &.status-text {
  3257. display: inline-flex;
  3258. align-items: center;
  3259. justify-content: center;
  3260. padding: 6px 12px;
  3261. border-radius: 16px;
  3262. font-size: 12px;
  3263. font-weight: 600;
  3264. text-align: center;
  3265. min-width: 60px;
  3266. line-height: 1;
  3267. border: 1px solid transparent;
  3268. margin: 0;
  3269. &.status-idle {
  3270. background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
  3271. color: #0369a1;
  3272. border-color: #bae6fd;
  3273. box-shadow: 0 1px 3px rgba(3, 105, 161, 0.1);
  3274. }
  3275. &.status-running {
  3276. background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
  3277. color: #15803d;
  3278. border-color: #bbf7d0;
  3279. box-shadow: 0 1px 3px rgba(21, 128, 61, 0.1);
  3280. }
  3281. &.status-paused {
  3282. background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
  3283. color: #d97706;
  3284. border-color: #fed7aa;
  3285. box-shadow: 0 1px 3px rgba(217, 119, 6, 0.1);
  3286. }
  3287. &.status-completed {
  3288. background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
  3289. color: #15803d;
  3290. border-color: #bbf7d0;
  3291. box-shadow: 0 1px 3px rgba(21, 128, 61, 0.1);
  3292. }
  3293. &.status-error {
  3294. background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
  3295. color: #dc2626;
  3296. border-color: #fecaca;
  3297. box-shadow: 0 1px 3px rgba(220, 38, 38, 0.1);
  3298. }
  3299. }
  3300. }
  3301. }
  3302. </style>