电速宝
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

index.js 41KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322
  1. const api = require('../../../api/index.js');
  2. const app = getApp(); // 在文件顶部获取app实例
  3. Page({
  4. data: {
  5. // 地图中心坐标
  6. longitude: 0,
  7. latitude: 0,
  8. // 标记点(包含起点、终点)
  9. markers: [],
  10. // 轨迹线(实际轨迹)
  11. polyline: [{
  12. points: [],
  13. color: "#ff0000", // 已走过的路线颜色:红色
  14. width: 6,
  15. dottedLine: false
  16. }],
  17. // 规划路线
  18. plannedRoute: [{
  19. points: [],
  20. color: "#2f693c", // 未走的规划路线颜色:绿色
  21. width: 4,
  22. dottedLine: false,
  23. arrowLine: true
  24. }],
  25. // 追踪状态
  26. isTracking: false,
  27. // 信息提示文本
  28. infoText: "",
  29. // 位置更新计时器
  30. locationTimer: null, // 这个计时器现在只由司机端后台定位使用
  31. // 终点位置
  32. destination: null,
  33. // 路线规划状态
  34. routePlanned: false,
  35. // 腾讯位置服务Key
  36. qqMapKey: 'VRGBZ-ZFRHB-SKIUP-NHHJ3-TXGJT-ZIFG3',
  37. // 偏离阈值(米)和偏离状态
  38. deviationThreshold: 50, // 超过50米视为偏离
  39. isDeviated: false,
  40. // 重新规划相关
  41. isReplanning: false, // 是否正在重新规划
  42. deviationCount: 0, // 连续偏离计数
  43. replanThreshold: 3, // 连续偏离多少次后重新规划
  44. orderdata: [],
  45. optionsid: '',
  46. showpayment: false, // 结算弹窗显示状态
  47. showCodePopup: false, // 验证码弹窗显示状态
  48. countdown: 60, // 验证码倒计时
  49. canResend: false, // 是否可重新发送
  50. userdata: [],
  51. moretype: false,
  52. // 拖动
  53. cardHeight: 'min', // 初始高度档位:min/mid/max
  54. currentHeight: 0, // 当前实际高度(px)
  55. startY: 0, // 触摸起始Y坐标
  56. maskOpacity: 0.3, // 遮罩透明度
  57. windowHeight: 0, // 屏幕可用高度(px)
  58. // 三个固定高度(max改为屏幕高度)
  59. heightConfig: {
  60. min: 400, // 最低高度(仅显示价格和路线简要)
  61. mid: 520, // 中间高度(显示行程详情)
  62. max: 0 // 占位,将在onLoad中设置为屏幕高度
  63. },
  64. costdetails: false,
  65. trackPoints: [],
  66. distance: '',
  67. obtainindex: 0,
  68. driverLocation: {}, // 司机位置(对象)
  69. mapInitialized: false, // 标记地图是否初始化
  70. isDriver: false // 标记是否为司机角色
  71. },
  72. onReady() {
  73. // 创建地图上下文
  74. this.mapCtx = wx.createMapContext('map');
  75. },
  76. //
  77. onLoad(options) {
  78. // 监听WebSocket消息
  79. wx.$on('wsMessage', this.detailsWsMessage);
  80. // 获取屏幕可用高度(不含导航栏)
  81. const systemInfo = wx.getSystemInfoSync();
  82. const windowHeight = systemInfo.windowHeight;
  83. // 更新heightConfig,max设为屏幕高度
  84. this.setData({
  85. windowHeight,
  86. heightConfig: {
  87. ...this.data.heightConfig,
  88. max: windowHeight
  89. },
  90. currentHeight: this.data.heightConfig.min, // 初始高度为min档
  91. isDriver: wx.getStorageSync('user').operationRole == 4 // 初始化司机角色标记
  92. });
  93. this.setData({
  94. userdata: wx.getStorageSync('user') || {},
  95. optionsid: options.id
  96. });
  97. console.log('司机端模式:', this.data.isDriver);
  98. // 引入SDK核心类
  99. this.qqmapsdk = require('../../../libs/qqmap-wx-jssdk.min.js');
  100. // 初始化SDK
  101. this.mapSdk = new this.qqmapsdk({
  102. key: this.data.qqMapKey
  103. });
  104. // 初始化地图,获取当前位置和工单信息
  105. this.getoneworkorderdetailss();
  106. // this.getoneworkorderdetails();
  107. },
  108. onUnload() {
  109. // 移除WebSocket消息监听
  110. wx.$off('wsMessage', this.detailsWsMessage);
  111. // 页面卸载时清理资源
  112. if (this.data.locationTimer) {
  113. clearInterval(this.data.locationTimer);
  114. }
  115. // 如果是司机角色,停止后台定位
  116. if (this.data.isDriver) {
  117. // wx.stopLocationUpdateBackground().catch(err => console.error('停止后台定位失败', err));
  118. // wx.offLocationChange(); // 移除位置监听
  119. }
  120. },
  121. /**
  122. * 处理接收到的 WebSocket 消息
  123. * @param {Object} message 从 app.js 广播过来的消息对象
  124. */
  125. // 可用,位置,容量-
  126. detailsWsMessage(message) {
  127. console.log('收到 WebSocket 消息:', message);
  128. if (!message || !message.api) {
  129. console.warn('无效的WebSocket消息:', message);
  130. return;
  131. }
  132. // 根据消息的api字段进行不同的处理
  133. switch (message.api) {
  134. case '/sysworkorder/selectworkorderId':
  135. // 收到工单列表更新的消息,刷新当前工单信息
  136. // 这个消息通常意味着工单状态可能发生了变化(如分配、完成等)
  137. this.getoneworkorderdetails();
  138. break;
  139. case '/sysworkorder/selectworkorderredis':
  140. // 收到坐标上传的消息,意味着司机位置有更新
  141. // 这个消息可以由司机自己发出,也可以由服务器推送给乘客
  142. // 为了数据一致性,直接刷新轨迹
  143. this.historytrajectory();
  144. break;
  145. case '/sysworkorder/selectworkorderId':
  146. // 收到工单状态更新的消息(例如,乘客确认上车、到达目的地等)
  147. // 刷新工单信息以获取最新状态
  148. this.getoneworkorderdetails();
  149. break;
  150. // 你可以根据实际的业务需求,添加更多的case
  151. // case 'other_api_path':
  152. // // 处理其他类型的消息
  153. // break;
  154. default:
  155. // 对于未知类型的消息,可以打印日志以便调试
  156. console.log('收到未知类型的WebSocket消息, api:', message.api);
  157. // 作为一个备选方案,也可以默认刷新工单信息,确保数据最新
  158. // this.getoneworkorderdetails();
  159. break;
  160. }
  161. },
  162. getoneworkorderdetailss() {
  163. const data = {
  164. workorderId: this.data.optionsid
  165. };
  166. api.request(`/sysworkorder/selectworkorderId`, 'post', data, { isPublic: false })
  167. .then((data) => {
  168. console.log('工单数据:', data.data);
  169. if (data.code == 200) {
  170. this.setData({
  171. orderdata: data.data
  172. });
  173. // 保存旧的工单类型,用于比较状态变化
  174. const oldWorkorderType = this.data.orderdata.workorderType;
  175. // **核心修改点:调用 app.js 的方法来控制定位上传**
  176. if (this.data.isDriver) {
  177. // 如果工单状态变为进行中 (1-4),且之前未在上传,则启动上传
  178. if (data.data.workorderType >= 1 && data.data.workorderType <= 2) {
  179. app.startDriverWorkorderLocation(this.data.optionsid);
  180. this.showInfo('已开启实时位置上传');
  181. }
  182. }
  183. this.initMap();
  184. }
  185. })
  186. .catch((err) => {
  187. this.showInfo('获取工单信息失败');
  188. });
  189. },
  190. getoneworkorderdetails() {
  191. const data = {
  192. workorderId: this.data.optionsid
  193. };
  194. api.request(`/sysworkorder/selectworkorderId`, 'post', data, { isPublic: false })
  195. .then((data) => {
  196. console.log('工单数据:', data.data);
  197. if (data.code == 200) {
  198. // 保存旧的工单类型,用于比较状态变化
  199. this.setData({
  200. orderdata: data.data
  201. });
  202. this.setDestination();
  203. // this.setData({
  204. // markers: [],
  205. // polyline: [{ points: [] }],
  206. // plannedRoute: [{ points: [] }],
  207. // driverLocation: {}
  208. // });
  209. }
  210. })
  211. .catch((err) => {
  212. console.error('获取工单信息失败:', err);
  213. this.showInfo('获取工单信息失败');
  214. });
  215. },
  216. // 初始化地图(优化司机端逻辑)
  217. initMap() {
  218. // 获取用户当前位置(司机端优先使用自身位置作为地图中心)
  219. wx.getLocation({
  220. type: 'gcj02', // 腾讯地图坐标体系
  221. success: (res) => {
  222. const { longitude, latitude } = res;
  223. this.setData({
  224. longitude,
  225. latitude,
  226. });
  227. // 司机端逻辑:仅在工单已分配时开启后台定位(workorderType >= 1)
  228. if (this.data.isDriver && this.data.orderdata.workorderType >= 1) {
  229. this.setData({
  230. driverLocation: { longitude, latitude } // 司机端初始化自身位置
  231. })
  232. this.historytrajectory(); // 主动加载历史轨迹
  233. }
  234. this.setDestination();
  235. },
  236. fail: (err) => {
  237. console.error("获取位置失败:", err);
  238. this.showInfo(this.data.isDriver ? "司机定位权限未授权,请开启" : "请授权位置权限以使用地图功能");
  239. // 引导用户开启权限
  240. wx.openSetting({
  241. success: (res) => {
  242. if (res.authSetting['scope.userLocation']) {
  243. this.initMap();
  244. }
  245. }
  246. });
  247. }
  248. });
  249. },
  250. // 设置终点位置(核心修改:未分配工单时隐藏司机标记和轨迹)-
  251. setDestination() {
  252. const ICON_PATH = {
  253. start: "https://esos-iot.com/myminio/project/0b0593293af54ea097b168cea479c25c.png",
  254. end: "https://esos-iot.com/myminio/project/6dc37b15321b462f9ab59c47263dd224.png",
  255. driver: "https://esos-iot.com/myminio/project/f6fba6c83b2d4864a73f5f1d83cda416.png"
  256. };
  257. const MARKER_SIZE = { width: 30, height: 30 };
  258. const MAP_PADDING = [100, 100, 100, 100];
  259. const ROUTE_COLOR = "#ff0000";
  260. const ROUTE_WIDTH = 6;
  261. const requestData = {
  262. workorderId: this.data.optionsid
  263. };
  264. api.request(`/sysworkorder/selectworkorderredis`, 'post', requestData, { isPublic: false })
  265. .then((response) => {
  266. if (response.code !== 200 || response.code==[]) {
  267. this.showInfo("暂无轨迹数据");
  268. return;
  269. }
  270. if (!this.data.orderdata) {
  271. this.showInfo("工单信息加载中...");
  272. return;
  273. }
  274. response.data = [...response.data].reverse();
  275. console.log('Redis轨迹数据:', response.data);
  276. // 关键判断:工单是否处于进行中状态(1-4)
  277. const isWorkorderAssigned = this.data.orderdata.workorderType >= 1 && this.data.orderdata.workorderType <= 2;
  278. // 初始化司机位置为 null
  279. let driverPoint = null;
  280. // 如果工单处于进行中状态
  281. if (isWorkorderAssigned) {
  282. // 优先从最新的响应数据中获取司机位置
  283. // 只有当 response.data 是一个非空数组时,才取第一个元素
  284. if (response.data && Array.isArray(response.data) && response.data.length > 0) {
  285. driverPoint = response.data[response.data.length-1];
  286. } else {
  287. // 如果响应数据为空或格式不正确,则使用页面上已缓存的司机位置
  288. driverPoint = null;
  289. }
  290. }else{
  291. const markers = [];
  292. const endPoint = this.data.orderdata;
  293. if (response.data.length>0) {
  294. // 已分配工单时才显示起点
  295. markers.push({
  296. id: 0,
  297. longitude: response.data[0].longitude,
  298. latitude: response.data[0].latitude,
  299. iconPath: ICON_PATH.start,
  300. ...MARKER_SIZE,
  301. title: "起点"
  302. });
  303. }
  304. // 始终添加终点标记
  305. markers.push({
  306. id: 1,
  307. longitude: endPoint.longitude,
  308. latitude: endPoint.latitude,
  309. iconPath: ICON_PATH.end,
  310. ...MARKER_SIZE,
  311. title: "终点",
  312. name: endPoint.poiName
  313. });
  314. this.setData({
  315. markers,
  316. plannedRoute: [{
  317. points: response.data,
  318. color: "#2f693c", // 未走的规划路线颜色:绿色
  319. width: 4,
  320. dottedLine: false,
  321. arrowLine: true
  322. }]
  323. })
  324. this.mapCtx.includePoints({
  325. points: response.data,
  326. padding: MAP_PADDING
  327. });
  328. return
  329. }
  330. // 对于工单状态为 5 (已完成) 或其他状态,driverPoint 保持为 null
  331. const startPoint = isWorkorderAssigned ? (response.data && response.data.length > 0 ? response.data[0] : null) : null;
  332. const endPoint = this.data.orderdata;
  333. console.log(driverPoint);
  334. // 构建标记点(未分配时不添加司机标记)
  335. const markers = [];
  336. // 已分配工单时才显示起点
  337. if (isWorkorderAssigned && startPoint) {
  338. markers.push({
  339. id: 0,
  340. longitude: startPoint.longitude,
  341. latitude: startPoint.latitude,
  342. iconPath: ICON_PATH.start,
  343. ...MARKER_SIZE,
  344. title: "起点"
  345. });
  346. }
  347. // 始终添加终点标记
  348. markers.push({
  349. id: 1,
  350. longitude: endPoint.longitude,
  351. latitude: endPoint.latitude,
  352. iconPath: ICON_PATH.end,
  353. ...MARKER_SIZE,
  354. title: "终点",
  355. name: endPoint.poiName
  356. });
  357. console.log(driverPoint);
  358. // 已分配工单且有司机位置时,才添加司机标记
  359. if (isWorkorderAssigned && driverPoint) {
  360. markers.push({
  361. id: 2,
  362. longitude: driverPoint.longitude,
  363. latitude: driverPoint.latitude,
  364. iconPath: ICON_PATH.driver,
  365. width: 35,
  366. height: 35,
  367. title: this.data.isDriver ? "我的位置" : "司机正在赶来",
  368. callout: {
  369. content: this.data.isDriver ? "我的位置" : "司机正在赶来",
  370. display: "BYCLICK"
  371. }
  372. });
  373. }
  374. // 构建轨迹线(未分配时不显示轨迹线)
  375. const polyline = [];
  376. if (isWorkorderAssigned) {
  377. if (response.data && response.data.length > 1) {
  378. const trackPoints = response.data.map(point => ({
  379. longitude: point.longitude,
  380. latitude: point.latitude
  381. }));
  382. polyline.push({
  383. points: trackPoints,
  384. color: ROUTE_COLOR,
  385. width: ROUTE_WIDTH,
  386. dottedLine: false
  387. });
  388. } else if (startPoint && endPoint) {
  389. polyline.push({
  390. points: [
  391. { longitude: startPoint.longitude, latitude: startPoint.latitude },
  392. { longitude: endPoint.longitude, latitude: endPoint.latitude }
  393. ],
  394. color: ROUTE_COLOR,
  395. width: ROUTE_WIDTH,
  396. dottedLine: false
  397. });
  398. }
  399. }
  400. // 更新页面数据
  401. this.setData({
  402. markers,
  403. startPoint: isWorkorderAssigned ? startPoint : null,
  404. destination: { longitude: endPoint.longitude, latitude: endPoint.latitude },
  405. polyline
  406. });
  407. // 提示信息区分工单状态
  408. // if (isWorkorderAssigned) {
  409. // if (startPoint) {
  410. // this.showInfo(this.data.isDriver ? "已加载起点、终点和我的位置" : "已设置起点和终点位置");
  411. // } else {
  412. // this.showInfo(this.data.isDriver ? "已加载终点和我的位置(等待起点更新)" : "已加载终点位置(等待起点更新)");
  413. // }
  414. // } else {
  415. // this.showInfo("工单未分配,暂未显示司机信息");
  416. // }
  417. // 地图视野初始化(未分配时仅显示终点)
  418. if (this.mapCtx && !this.data.mapInitialized) {
  419. const includePoints = [{
  420. longitude: endPoint.longitude,
  421. latitude: endPoint.latitude
  422. }];
  423. // 已分配工单时才包含起点和司机位置
  424. if (isWorkorderAssigned) {
  425. if (startPoint) includePoints.push({ longitude: startPoint.longitude, latitude: startPoint.latitude });
  426. if (driverPoint) includePoints.push({ longitude: driverPoint.longitude, latitude: driverPoint.latitude });
  427. }
  428. console.log(includePoints);
  429. this.mapCtx.includePoints({
  430. points: includePoints,
  431. padding: MAP_PADDING
  432. });
  433. this.setData({ mapInitialized: true });
  434. }
  435. // 已分配工单且有起点终点时,才规划路线
  436. if (isWorkorderAssigned && startPoint && endPoint && !this.data.routePlanned) {
  437. this.planRoute();
  438. }
  439. })
  440. .catch((err) => {
  441. console.error('请求轨迹数据失败:', err);
  442. this.showInfo("网络请求失败,请检查网络");
  443. });
  444. },
  445. planRoute(isReplan = false) {
  446. const { startPoint, destination } = this.data;
  447. if (!startPoint || !destination) {
  448. this.showInfo("无法规划路线:起点或终点信息不完整");
  449. return;
  450. }
  451. if (isReplan) {
  452. this.setData({ isReplanning: true });
  453. this.showInfo("正在重新规划路线...");
  454. } else {
  455. this.showInfo("正在规划路线...");
  456. }
  457. this.mapSdk.direction({
  458. mode: 'driving',
  459. policy: 'REAL_TRAFFIC',
  460. from: { latitude: startPoint.latitude, longitude: startPoint.longitude },
  461. to: { latitude: destination.latitude, longitude: destination.longitude },
  462. success: (res) => {
  463. console.log("路线规划结果:", res);
  464. if (res.status == 0 && res.result.routes.length > 0) {
  465. const result = res.result;
  466. const route = result.routes[0];
  467. // 计算预计到达时间
  468. const durationMinutes = Math.ceil(result.routes[0].duration);
  469. const now = new Date();
  470. now.setMinutes(now.getMinutes() + durationMinutes);
  471. // 格式化时间(HH:MM)
  472. const estimatedArrivalTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
  473. const estimatedDuration = `${durationMinutes}分钟`;
  474. // 提取路线点
  475. const coors = route.polyline;
  476. const pl = [];
  477. // 坐标解压(返回的点串坐标,通过前向差分进行压缩)
  478. const kr = 1000000;
  479. for (let i = 2; i < coors.length; i++) {
  480. coors[i] = Number(coors[i - 2]) + Number(coors[i]) / kr;
  481. }
  482. // 将解压后的坐标放入点串数组pl中
  483. for (let i = 0; i < coors.length; i += 2) {
  484. pl.push({
  485. latitude: coors[i],
  486. longitude: coors[i + 1]
  487. });
  488. }
  489. // 更新路线数据
  490. const distance = (result.routes[0].distance / 1000).toFixed(1);
  491. this.setData({
  492. estimatedArrivalTime,
  493. estimatedDuration,
  494. plannedRoute: [{
  495. name: this.data.orderdata.poiName,
  496. points: pl,
  497. color: '#2f693c',
  498. width: 6,
  499. borderColor: '#2f693c',
  500. borderWidth: 1,
  501. arrowLine: true
  502. }],
  503. routePlanned: true,
  504. isReplanning: false,
  505. deviationCount: 0,
  506. distance
  507. });
  508. // 显示规划结果(区分司机/用户端提示)
  509. const message = this.data.isDriver
  510. ? (isReplan ? `已重新规划路线,距离${distance}公里,预计${estimatedDuration}后到达` : `距离${distance}公里,预计${estimatedDuration}后到达`)
  511. : (isReplan ? `已重新规划路线,距离${distance}公里,约${Math.ceil(result.routes[0].duration)}分钟` : `距离${distance}公里,约${Math.ceil(result.routes[0].duration)}分钟`);
  512. console.log(message);
  513. this.showInfo(message);
  514. } else {
  515. this.setData({ isReplanning: false });
  516. this.showInfo("路线规划失败,请重试");
  517. }
  518. },
  519. fail: (err) => {
  520. console.error("路线规划失败:", err);
  521. this.setData({ isReplanning: false });
  522. this.showInfo("路线规划失败,请检查网络");
  523. }
  524. });
  525. },
  526. // 获取历史轨迹线(司机端主动更新自身轨迹)
  527. historytrajectory() {
  528. // 工单未分配时不获取轨迹
  529. if (this.data.orderdata.workorderType < 1) return;
  530. const data = {
  531. workorderId: this.data.optionsid
  532. };
  533. api.request(`/sysworkorder/selectworkorderredis`, 'post', data, { isPublic: false })
  534. .then((response) => {
  535. // 兼容处理:检查返回数据是否有效
  536. if (!response.data || response.data.length === 0) {
  537. console.log('未获取到轨迹数据或轨迹数据为空');
  538. this.handleEmptyTrajectory();
  539. return;
  540. }
  541. if (response.code == 200 && response.data.length > 0) {
  542. response.data = [...response.data].reverse();
  543. console.log('历史轨迹:', response.data);
  544. // 司机端和用户端统一从[0]获取最新位置
  545. this.updateDriverLocation(response.data[response.data.length-1]);
  546. // 如果有多个轨迹点,更新轨迹线
  547. if (response.data.length > 0) {
  548. const trackPoints = response.data.map(point => ({
  549. longitude: point.longitude*1,
  550. latitude: point.latitude*1
  551. }));
  552. this.setData({
  553. polyline: [{
  554. points: trackPoints,
  555. color: "#ff0000",
  556. width: 6,
  557. dottedLine: false
  558. }]
  559. });
  560. console.log(trackPoints);
  561. if (trackPoints.length<2) {
  562. this.mapCtx.includePoints({
  563. points:[this.data.destination,...trackPoints],
  564. padding: [100, 100, 100, 100] // 适配屏幕边距
  565. });
  566. }
  567. } else {
  568. // 如果只有一个轨迹点(通常是司机的当前位置),则只更新司机位置,不绘制轨迹线-
  569. console.log('只有一个轨迹点,不绘制轨迹线');
  570. this.setData({
  571. polyline: [{ points: [] }] // 清空轨迹线
  572. });
  573. }
  574. } else {
  575. // 服务器返回非200状态码或数据格式不正确
  576. // console.warn('获取轨迹数据失败,服务器返回:', response);
  577. this.handleEmptyTrajectory();
  578. }
  579. })
  580. .catch((err) => {
  581. console.error('获取历史轨迹失败:', err);
  582. // 司机端额外提示
  583. if (this.data.isDriver) {
  584. this.showInfo("轨迹更新失败,已自动重试");
  585. }
  586. });
  587. },
  588. /**
  589. * 处理空轨迹数据的情况
  590. */
  591. handleEmptyTrajectory() {
  592. // 清空轨迹线,避免显示旧的轨迹
  593. this.setData({
  594. polyline: [{ points: [] }]
  595. });
  596. // 根据角色给出不同的提示
  597. if (this.data.isDriver) {
  598. // 司机端:可能是刚接单,还未开始上传位置
  599. this.showInfo("尚未记录轨迹,请开始行驶或检查定位");
  600. } else {
  601. // 用户端:司机可能还未出发或未上传位置
  602. this.showInfo("司机尚未开始行驶或位置信息未更新");
  603. }
  604. },
  605. updateDriverLocation(newDriverLocation) {
  606. // 工单未分配时不更新司机位置
  607. if (this.data.orderdata.workorderType < 1) return;
  608. if (!newDriverLocation) return;
  609. // 更新司机位置数据
  610. this.setData({
  611. driverLocation: newDriverLocation
  612. });
  613. // 获取当前标记数组
  614. const markers = [...this.data.markers];
  615. // 找到司机标记(id:2)并更新位置
  616. const driverMarkerIndex = markers.findIndex(marker => marker.id == 2);
  617. if (driverMarkerIndex !== -1) {
  618. // 更新司机标记位置
  619. markers[driverMarkerIndex] = {
  620. ...markers[driverMarkerIndex],
  621. longitude: newDriverLocation.longitude,
  622. latitude: newDriverLocation.latitude
  623. };
  624. this.setData({ markers });
  625. } else {
  626. // 如果司机标记不存在,很可能是地图还没初始化好
  627. // 可以选择重新设置目的地来创建标记,或者给出提示
  628. console.warn('司机标记不存在,尝试重新设置目的地');
  629. // 这里可以加一个防抖(debounce),避免短时间内多次调用
  630. if (!this.setDestinationTimer) {
  631. this.setDestinationTimer = setTimeout(() => {
  632. this.setDestination();
  633. this.setDestinationTimer = null;
  634. }, 500);
  635. }
  636. }
  637. },
  638. // 显示信息提示(区分司机/用户端)
  639. showInfo(text) {
  640. this.setData({ infoText: text });
  641. // 3秒后自动隐藏非状态类信息(司机端状态类信息保留更久)
  642. const delay = this.data.isDriver && (text.includes("定位") || text.includes("轨迹") || text.includes("到达")) ? 5000 : 3000;
  643. if (!text.includes("正在") && !text.includes("距离") && !text.includes("偏离")) {
  644. setTimeout(() => {
  645. if (this.data.infoText == text) {
  646. this.setData({ infoText: "" });
  647. }
  648. }, delay);
  649. }
  650. },
  651. // 导航功能(司机端默认开启导航)
  652. navigation() {
  653. if (!this.data.destination) {
  654. this.showInfo("目的地尚未设置");
  655. return;
  656. }
  657. // 使用在腾讯位置服务申请的key
  658. const key = this.data.qqMapKey;
  659. const referer = '电速宝';
  660. const enableAI = true;
  661. const navigation = 1; // 强制开启导航
  662. const endPoint = JSON.stringify({
  663. name: this.data.orderdata.poiName || '目的地',
  664. latitude: this.data.orderdata.latitude,
  665. longitude: this.data.orderdata.longitude,
  666. });
  667. const layerStyle = 1;
  668. wx.navigateTo({
  669. url: `plugin://route-plan/index?key=${key}&referer=${referer}&endPoint=${endPoint}&enableAI=${enableAI}&navigation=${navigation}&layerStyle=${layerStyle}`,
  670. fail: (err) => {
  671. console.error('导航启动失败', err);
  672. // 1. 判断失败原因:插件未安装/不存在(核心逻辑)
  673. const isPluginNotExist = err.errMsg.includes('plugin not found') || err.errMsg.includes('找不到插件') || err.errMsg.includes('navigateTo:fail');
  674. if (isPluginNotExist) {
  675. // 2. 提示用户下载腾讯地图
  676. wx.showModal({
  677. title: '提示',
  678. content: '未检测到腾讯地图应用,是否前往应用商店下载?',
  679. confirmText: '去下载',
  680. cancelText: '取消',
  681. success: (res) => {
  682. if (res.confirm) {
  683. // 3. 跳转到腾讯地图下载页(区分iOS/Android)
  684. // iOS下载链接(App Store)
  685. const iosUrl = 'https://apps.apple.com/cn/app/%E8%85%BE%E8%AE%AF%E5%9C%B0%E5%9B%BE/id414478124';
  686. // Android下载链接(腾讯应用宝,也可换应用宝/华为应用市场等)
  687. const androidUrl = 'https://a.app.qq.com/o/simple.jsp?pkgname=com.tencent.map';
  688. wx.navigateToMiniProgram({
  689. // 备选方案:直接打开外部下载链接(需小程序配置业务域名)
  690. // 优先用openURL,兼容更多场景
  691. url: '',
  692. fail: () => {
  693. wx.openURL({
  694. url: wx.getSystemInfoSync().system.includes('iOS') ? iosUrl : androidUrl,
  695. fail: () => {
  696. this.showInfo('下载链接打开失败,请手动前往应用商店搜索“腾讯地图”下载');
  697. }
  698. });
  699. }
  700. });
  701. } else {
  702. // 用户取消下载,提示手动导航
  703. this.showInfo(this.data.isDriver ? "导航启动失败,请手动开启腾讯地图导航" : "导航启动失败");
  704. }
  705. }
  706. });
  707. } else {
  708. // 非插件未安装的其他失败原因(如参数错误、网络问题)
  709. this.showInfo(this.data.isDriver ? "导航启动失败,请手动开启" : "导航启动失败");
  710. }
  711. }
  712. });
  713. },
  714. // 封装的提示方法(保持原有逻辑)
  715. showInfo(msg) {
  716. wx.showToast({
  717. title: msg,
  718. icon: 'none',
  719. duration: 2000
  720. });
  721. },
  722. // 重新发送验证码
  723. onResendCode() {
  724. // 调用发送验证码接口
  725. this.showVerificationPopup();
  726. this.startCountdown(); // 重启倒计时
  727. wx.showToast({ title: '验证码已发送', icon: 'none' });
  728. // wx.request({
  729. // url: 'https://your-api.com/sendCode',
  730. // success: () => {
  731. // this.startCountdown(); // 重启倒计时
  732. // wx.showToast({ title: '验证码已发送', icon: 'none' });
  733. // }
  734. // });
  735. },
  736. // 验证码倒计时
  737. startCountdown() {
  738. let countdown = 60;
  739. this.setData({ canResend: false, countdown });
  740. const timer = setInterval(() => {
  741. countdown--;
  742. this.setData({ countdown });
  743. if (countdown <= 0) {
  744. clearInterval(timer);
  745. this.setData({ canResend: true, countdown: 60 });
  746. }
  747. }, 1000);
  748. },
  749. // 显示验证码弹窗(司机端无需验证码,直接执行操作)
  750. showVerificationPopup() {
  751. // 用户端正常显示验证码
  752. const data = { workorderId: this.data.optionsid };
  753. api.request(`/sysworkorder/createverify`, 'post', data, { isPublic: false })
  754. .then((data) => {
  755. if (data.code == 200) {
  756. wx.showToast({ title: data.msg || '验证码已发送', icon: 'none' });
  757. this.setData({
  758. showCodePopup: true,
  759. canResend: false
  760. });
  761. // 启动倒计时
  762. this.startCountdown();
  763. } else {
  764. this.showInfo(data.msg || '获取验证码失败');
  765. }
  766. })
  767. .catch((err) => {
  768. console.error('获取验证码失败:', err);
  769. this.showInfo('获取验证码失败');
  770. });
  771. },
  772. // 关闭弹窗
  773. onCodePopupClose() {
  774. this.setData({ showCodePopup: false });
  775. setTimeout(() => {
  776. const slideComponent = this.selectComponent("#mySlideConfirm");
  777. if (slideComponent) {
  778. slideComponent.reset();
  779. }
  780. }, 1000);
  781. },
  782. // 确认验证码(仅用户端有效)
  783. onCodeConfirm(e) {
  784. if (!this.data.isDriver) {
  785. console.log("确认验证码:", e.detail.code);
  786. this.setData({ showVerification: false });
  787. }
  788. },
  789. // 重新发送验证码(仅用户端有效)
  790. // onResendCode() {
  791. // if (!this.data.isDriver) {
  792. // console.log("重新发送验证码");
  793. // this.showVerificationPopup();
  794. // }
  795. // },
  796. // 验证码输入完成
  797. onCodeInputComplete(e) {
  798. console.log(e);
  799. const code = e.detail.value;
  800. console.log(code);
  801. const data = {
  802. carId: this.data.carId,
  803. verifyCode: code,
  804. workorderId: this.data.orderdata.workorderId
  805. };
  806. api.request(`/sysworkorder/submitworkorder`, 'post', data, { isPublic: false })
  807. .then((data) => {
  808. if (data.code == 200) {
  809. wx.showToast({ title: '验证码正确', icon: 'success' });
  810. this.setData({ showCodePopup: false });
  811. } else {
  812. const codePopup = this.selectComponent('.code-popup');
  813. codePopup.triggerErrorShake();
  814. wx.showToast({ title: res.data.msg || '验证码错误', icon: 'none' });
  815. }
  816. })
  817. .catch((err) => {
  818. console.error('验证请求失败:', err);
  819. this.showInfo('验证请求失败');
  820. setTimeout(() => {
  821. const slideComponent = this.selectComponent("#mySlideConfirm");
  822. if (slideComponent) {
  823. slideComponent.reset();
  824. }
  825. }, 5000);
  826. });
  827. },
  828. onrecognize(e){
  829. console.log(1111111);
  830. const that = this;
  831. // 检查设备是否支持生物认证--
  832. wx.checkIsSupportSoterAuthentication({
  833. success(res) {
  834. console.log(res.supportMode.includes('facial'));
  835. console.log(res.supportMode.includes('fingerPrint'));
  836. if (res.supportMode.includes('facial')) {
  837. // 支持人脸识别(FaceID)
  838. wx.startSoterAuthentication({
  839. requestAuthModes: ['facial'],
  840. challenge:e.currentTarget.dataset.type,
  841. authContent: '请进行人脸识别验证',
  842. success(res) {
  843. console.log('验证成功', res);
  844. that.onSlideSuccess()
  845. },
  846. fail(err) {
  847. setTimeout(() => {
  848. const slideComponent = that.selectComponent("#mySlideConfirm");
  849. if (slideComponent) {
  850. slideComponent.reset();
  851. }
  852. }, 5000);
  853. }
  854. });
  855. }
  856. if(res.supportMode.includes('fingerPrint')) {
  857. // 支持人脸识别(FaceID)
  858. wx.startSoterAuthentication({
  859. requestAuthModes: ['fingerPrint'],
  860. challenge:e.currentTarget.dataset.type,
  861. authContent: '请进行指纹识别验证',
  862. success(res) {
  863. console.log('验证成功', res);
  864. that.onSlideSuccess()
  865. },
  866. fail(err) {
  867. console.log(err);
  868. setTimeout(() => {
  869. const slideComponent = that.selectComponent("#mySlideConfirm");
  870. if (slideComponent) {
  871. slideComponent.reset();
  872. }
  873. }, 5000);
  874. }
  875. });
  876. }
  877. }
  878. });
  879. },
  880. onarrivalposition(){
  881. const data = {
  882. carId: this.data.orderdata.carId,
  883. verifyCode: '',
  884. workorderId: this.data.orderdata.workorderId
  885. };
  886. api.request(`/sysworkorder/submitworkorder`, 'post', data, { isPublic: false })
  887. .then((data) => {
  888. if (data.code == 200) {
  889. setTimeout(() => {
  890. const slideComponent = this.selectComponent("#mySlideConfirm");
  891. if (slideComponent) {
  892. slideComponent.reset();
  893. }
  894. }, 5000);
  895. app.stopDriverLocationUpload(this.data.optionsid);
  896. } else {
  897. this.showInfo(data.msg || '操作失败');
  898. }
  899. })
  900. .catch((err) => {
  901. console.error('操作请求失败:', err);
  902. this.showInfo('操作请求失败');
  903. setTimeout(() => {
  904. const slideComponent = this.selectComponent("#mySlideConfirm");
  905. if (slideComponent) {
  906. slideComponent.reset();
  907. }
  908. }, 5000);
  909. });
  910. },
  911. onSlideSuccess() {
  912. // console.log(e.currentTarget.dataset.type);
  913. // const type = e.currentTarget.dataset.type;
  914. const data = {
  915. carId: this.data.orderdata.carId,
  916. verifyCode: '',
  917. workorderId: this.data.orderdata.workorderId
  918. };
  919. api.request(`/sysworkorder/submitworkorder`, 'post', data, { isPublic: false })
  920. .then((data) => {
  921. if (data.code == 200) {
  922. app.stopDriverWorkorderLocation();
  923. setTimeout(() => {
  924. const slideComponent = this.selectComponent("#mySlideConfirm");
  925. if (slideComponent) {
  926. slideComponent.reset();
  927. }
  928. }, 5000);
  929. } else {
  930. this.showInfo(data.msg || '操作失败');
  931. }
  932. })
  933. .catch((err) => {
  934. console.error('操作请求失败:', err);
  935. this.showInfo('操作请求失败');
  936. setTimeout(() => {
  937. const slideComponent = this.selectComponent("#mySlideConfirm");
  938. if (slideComponent) {
  939. slideComponent.reset();
  940. }
  941. }, 5000);
  942. });
  943. },
  944. ontelephone(e) {
  945. const phoneNumber = e.currentTarget.dataset.phone;
  946. if (!phoneNumber) {
  947. this.showInfo('电话号码无效');
  948. return;
  949. }
  950. wx.showModal({
  951. title: '确认拨打电话',
  952. content: `是否拨打${phoneNumber}?`,
  953. success: (res) => {
  954. if (res.confirm) {
  955. wx.makePhoneCall({
  956. phoneNumber: phoneNumber,
  957. fail: (err) => {
  958. console.error('拨打电话失败', err);
  959. this.showInfo('拨打电话失败');
  960. }
  961. });
  962. }
  963. }
  964. });
  965. },
  966. onmore() {
  967. this.setData({ moretype: !this.data.moretype });
  968. },
  969. oncostdetails() {
  970. this.setData({ costdetails: !this.data.costdetails });
  971. },
  972. // 拖动功能(司机端默认展开行程详情)
  973. handleTouchStart(e) {
  974. this.setData({ startY: e.changedTouches[0].clientY });
  975. },
  976. handleTouchMove(e) {
  977. const currentY = e.changedTouches[0].clientY;
  978. const deltaY = currentY - this.data.startY;
  979. const { min, max } = this.data.heightConfig;
  980. let newHeight = this.data.currentHeight - deltaY;
  981. newHeight = Math.max(min, Math.min(max, newHeight));
  982. let maskOpacity = this.data.maskOpacity;
  983. if (newHeight < max && newHeight > min) {
  984. maskOpacity = 0.3 + (newHeight - min) / (this.data.heightConfig.mid - min) * 0.2;
  985. } else {
  986. maskOpacity = 0;
  987. }
  988. this.setData({ currentHeight: newHeight, maskOpacity, startY: currentY });
  989. },
  990. handleTouchEnd() {
  991. const { currentHeight } = this.data;
  992. const { min, mid, max } = this.data.heightConfig;
  993. const distToMin = Math.abs(currentHeight - min);
  994. const distToMid = Math.abs(currentHeight - mid);
  995. const distToMax = Math.abs(currentHeight - max);
  996. let targetHeight, targetMode;
  997. if (distToMax <= 80) {
  998. targetHeight = max;
  999. targetMode = 'max';
  1000. } else if (distToMin <= 50) {
  1001. targetHeight = min;
  1002. targetMode = 'min';
  1003. } else if (distToMid <= 60) {
  1004. targetHeight = mid;
  1005. targetMode = 'mid';
  1006. } else {
  1007. const minDist = Math.min(distToMin, distToMid, distToMax);
  1008. if (minDist == distToMin) {
  1009. targetHeight = min;
  1010. targetMode = 'min';
  1011. } else if (minDist == distToMid) {
  1012. targetHeight = mid;
  1013. targetMode = 'mid';
  1014. } else {
  1015. targetHeight = max;
  1016. targetMode = 'max';
  1017. }
  1018. }
  1019. this.setData({
  1020. currentHeight: targetHeight,
  1021. cardHeight: targetMode,
  1022. maskOpacity: targetMode == 'mid' ? 0.5 : 0
  1023. });
  1024. },
  1025. closeCard() {
  1026. this.setData({
  1027. currentHeight: this.data.heightConfig.min,
  1028. cardHeight: 'min',
  1029. maskOpacity: 0
  1030. });
  1031. },
  1032. closeFullscreen() {
  1033. this.setData({
  1034. currentHeight: this.data.heightConfig.min,
  1035. cardHeight: 'min',
  1036. maskOpacity: 0
  1037. });
  1038. },
  1039. // 编辑工单
  1040. editorder(){
  1041. wx.navigateTo({
  1042. url: `/package-order/pages/createorder/index?order=${this.data.optionsid}`,
  1043. })
  1044. },
  1045. cancel(){
  1046. let _this = this
  1047. // 在页面的 js 文件中
  1048. wx.showModal({
  1049. title: '提示',
  1050. content: '确定要取消工单吗?',
  1051. success (res) {
  1052. if (res.confirm) {
  1053. console.log('用户点击了确定')
  1054. // 在这里执行确认后的操作selectcarall selectcarall
  1055. let data ={
  1056. workorderId:_this.data.optionsid,
  1057. workorderType:6
  1058. }
  1059. api.request(`/sysworkorder/updateworkorder`, 'post', data, { isPublic: false })
  1060. .then((data) => {
  1061. if (data.code == 200) {
  1062. wx.switchTab({
  1063. url: '/pages/tool/index',
  1064. })
  1065. }
  1066. })
  1067. .catch((err) => {
  1068. this.showInfo('操作失败');
  1069. });
  1070. } else if (res.cancel) {
  1071. this.showInfo('取消操作');
  1072. }
  1073. }
  1074. })
  1075. },
  1076. /**
  1077. * 复制目的地地址
  1078. */
  1079. copyDestination() {
  1080. // 1. 获取目的地名称,做空值校验
  1081. const poiName = this.data.orderdata?.poiName || '';
  1082. if (!poiName.trim()) {
  1083. wx.showToast({
  1084. title: '暂无目的地地址可复制',
  1085. icon: 'none',
  1086. duration: 2000
  1087. });
  1088. return;
  1089. }
  1090. // 2. 调用微信API复制到剪贴板
  1091. wx.setClipboardData({
  1092. data: poiName, // 要复制的地址内容
  1093. success: () => {
  1094. // 复制成功提示
  1095. wx.showToast({
  1096. title: '地址已复制',
  1097. icon: 'success',
  1098. duration: 2000
  1099. });
  1100. },
  1101. fail: (err) => {
  1102. // 复制失败兜底提示
  1103. console.error('复制地址失败', err);
  1104. wx.showToast({
  1105. title: '复制失败,请手动复制',
  1106. icon: 'none',
  1107. duration: 2000
  1108. });
  1109. }
  1110. });
  1111. },
  1112. // 微信结算
  1113. onpay(){
  1114. this.setData({
  1115. showpayment:true
  1116. })
  1117. },
  1118. // 关闭结算弹窗
  1119. onpaymentPopupClose() {
  1120. this.setData({ showpayment: false });
  1121. },
  1122. // 接收结算弹窗输入完成的密码
  1123. onpaymentInputComplete(e) {
  1124. const paymentPwd = e.detail.value; // 获取弹窗输入的6位结算密码
  1125. // 执行父组件的检验逻辑(替换为你的真实接口/校验)
  1126. this.checkPaymentPwd(paymentPwd);
  1127. },
  1128. // 检验结算密码(父组件核心检验逻辑)
  1129. checkPaymentPwd(pwd) {
  1130. console.log(pwd);
  1131. let password = {
  1132. password:pwd
  1133. }
  1134. api.request(`/SysBalance/verifypassword`, 'post', password, { isPublic: false })
  1135. .then((data) => {
  1136. if (data.data) {
  1137. }else{
  1138. // 检验失败:触发弹窗晃动+清空
  1139. const paymentPopup = this.selectComponent('.payment-popup');
  1140. paymentPopup.triggerErrorShake(); // 调用弹窗的失败动效方法
  1141. // 提示错误信息
  1142. wx.showToast({ title:'' || '结算密码错误', icon: 'none' });
  1143. return
  1144. }
  1145. })
  1146. .catch((err) => {
  1147. this.showInfo('操作失败');
  1148. });
  1149. let data ={
  1150. workorderId:this.data.optionsid,
  1151. price:this.data.orderdata.price
  1152. }
  1153. api.request(`/sysworkorder/updatesettlementtype`, 'post', data, { isPublic: false })
  1154. .then((data) => {
  1155. if (data.code == 200) {
  1156. wx.showToast({ title: '结算密码正确', icon: 'success' });
  1157. this.setData({ showpayment: false }); // 关闭弹窗
  1158. this.getoneworkorderdetails();
  1159. // wx.navigateTo({
  1160. // url: `/package-user/pages/privacy/index?name=微信结算`,
  1161. // })
  1162. }
  1163. })
  1164. .catch((err) => {
  1165. this.showInfo('操作失败');
  1166. });
  1167. },
  1168. });