电速宝
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

index.js 41KB

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