wodexingcheng.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143
  1. const app = getApp()
  2. Page({
  3. data: {
  4. activeTab: 'in_progress', // in_progress, completed, all
  5. inProgressPlans: [], // 进行中行程数据
  6. completedPlans: [], // 已完成行程数据
  7. allPlans: [], // 全部行程数据
  8. completedCurrentPage: 1, // 已完成行程当前页码
  9. completedTotalPages: 1, // 已完成行程总页数
  10. allCurrentPage: 1, // 全部行程当前页码
  11. allTotalPages: 1, // 全部行程总页数
  12. isLoading: false, // 是否正在加载
  13. isLogin: false, // 登录状态
  14. showSpotModal: false, // 是否显示景点详情弹窗
  15. currentSpot: {}, // 当前显示的景点
  16. currentDayIndex: 0, // 当前景点的天数索引
  17. currentSpotIndex: 0, // 当前景点的索引
  18. showCheckinModal: false, // 是否显示打卡弹窗
  19. checkinImage: '', // 打卡图片
  20. checkinNote: '', // 打卡文案
  21. isUploading: false, // 是否正在上传图片
  22. tempCheckinData: {}, // 临时存储打卡数据
  23. latestPlanMapData: { // 地图数据
  24. showMap: false,
  25. markers: [],
  26. polylines: [],
  27. includePoints: [],
  28. longitude: 116.4074,
  29. latitude: 39.9042,
  30. scale: 14
  31. },
  32. currentMapDay: -1, // 当前显示的地图天数
  33. cityOptions: [] // 城市选项
  34. },
  35. onLoad() {
  36. const token = wx.getStorageSync('token')
  37. const userInfo = wx.getStorageSync('userInfo')
  38. if (token && userInfo) {
  39. app.globalData.token = token
  40. app.globalData.userInfo = userInfo
  41. this.setData({ isLogin: true })
  42. this.loadInProgressPlans()
  43. } else {
  44. this.checkLoginStatus()
  45. }
  46. },
  47. onShow() {
  48. if (this.data.isLogin) {
  49. if (this.data.activeTab === 'in_progress') {
  50. this.loadInProgressPlans()
  51. } else if (this.data.activeTab === 'completed') {
  52. this.loadCompletedPlans(this.data.completedCurrentPage)
  53. } else if (this.data.activeTab === 'all') {
  54. this.loadAllPlans(this.data.allCurrentPage)
  55. }
  56. }
  57. },
  58. checkLoginStatus() {
  59. const token = app.globalData.token
  60. if (!token) {
  61. wx.showModal({
  62. title: '未登录',
  63. content: '请先登录查看行程',
  64. confirmText: '去登录',
  65. success: (res) => {
  66. if (res.confirm) {
  67. wx.navigateTo({
  68. url: '/pages/login/login'
  69. })
  70. } else {
  71. wx.switchTab({
  72. url: '/pages/index/index'
  73. })
  74. }
  75. }
  76. })
  77. return
  78. }
  79. this.setData({ isLogin: true })
  80. this.loadInProgressPlans()
  81. },
  82. // 切换选项卡
  83. switchTab(e) {
  84. const tab = e.currentTarget.dataset.tab;
  85. if (this.data.activeTab === tab) return;
  86. this.setData({ activeTab: tab });
  87. if (tab === 'in_progress') {
  88. this.loadInProgressPlans();
  89. } else if (tab === 'completed') {
  90. this.loadCompletedPlans(1);
  91. } else if (tab === 'all') {
  92. this.loadAllPlans(1);
  93. }
  94. },
  95. // 加载进行中行程
  96. loadInProgressPlans() {
  97. if (!app.globalData.token) return
  98. this.setData({ isLoading: true });
  99. wx.request({
  100. url: app.globalData.apiBaseUrl + '/api/user/plans/',
  101. method: 'GET',
  102. data: {
  103. status: 'in_progress',
  104. limit: 1,
  105. timestamp: new Date().getTime()
  106. },
  107. header: {
  108. 'Authorization': 'Token ' + app.globalData.token,
  109. 'Content-Type': 'application/json'
  110. },
  111. success: (res) => {
  112. if (res.statusCode === 200 && res.data.status === 'success') {
  113. this.setData({
  114. inProgressPlans: res.data.data.plans,
  115. });
  116. if (res.data.data.plans.length > 0) {
  117. this.processPlanMapData(res.data.data.plans[0]);
  118. }
  119. } else {
  120. this.handleErrorResponse(res);
  121. }
  122. },
  123. fail: (err) => {
  124. this.handleRequestError(err);
  125. },
  126. complete: () => {
  127. this.setData({ isLoading: false });
  128. }
  129. });
  130. },
  131. // 加载已完成行程
  132. loadCompletedPlans(page = 1) {
  133. if (!app.globalData.token) return;
  134. this.setData({ isLoading: true });
  135. wx.request({
  136. url: app.globalData.apiBaseUrl + '/api/user/plans/',
  137. method: 'GET',
  138. data: {
  139. status: 'completed',
  140. page: page,
  141. page_size: 5
  142. },
  143. header: {
  144. 'Authorization': 'Token ' + app.globalData.token,
  145. 'Content-Type': 'application/json'
  146. },
  147. success: (res) => {
  148. if (res.statusCode === 200 && res.data.status === 'success') {
  149. this.setData({
  150. completedPlans: res.data.data.plans,
  151. completedCurrentPage: page,
  152. completedTotalPages: res.data.data.pagination.total_pages || 1
  153. });
  154. } else {
  155. this.handleErrorResponse(res);
  156. }
  157. },
  158. fail: (err) => {
  159. this.handleRequestError(err);
  160. },
  161. complete: () => {
  162. this.setData({ isLoading: false });
  163. }
  164. });
  165. },
  166. // 加载全部行程
  167. loadAllPlans(page = 1) {
  168. if (!app.globalData.token) return;
  169. this.setData({ isLoading: true });
  170. wx.request({
  171. url: app.globalData.apiBaseUrl + '/api/user/plans/',
  172. method: 'GET',
  173. data: {
  174. status: 'in_progress',
  175. page: page,
  176. page_size: 5
  177. },
  178. header: {
  179. 'Authorization': 'Token ' + app.globalData.token,
  180. 'Content-Type': 'application/json'
  181. },
  182. success: (res) => {
  183. if (res.statusCode === 200 && res.data.status === 'success') {
  184. this.setData({
  185. allPlans: res.data.data.plans,
  186. allCurrentPage: page,
  187. allTotalPages: res.data.data.pagination.total_pages || 1
  188. });
  189. } else {
  190. this.handleErrorResponse(res);
  191. }
  192. },
  193. fail: (err) => {
  194. this.handleRequestError(err);
  195. },
  196. complete: () => {
  197. this.setData({ isLoading: false });
  198. }
  199. });
  200. },
  201. // 已完成行程翻页
  202. changeCompletedPage(e) {
  203. const page = e.currentTarget.dataset.page
  204. if (page < 1 || page > this.data.completedTotalPages) return
  205. this.loadCompletedPlans(page)
  206. },
  207. // 全部行程翻页
  208. changeAllPage(e) {
  209. const page = e.currentTarget.dataset.page
  210. if (page < 1 || page > this.data.allTotalPages) return
  211. this.loadAllPlans(page)
  212. },
  213. // 开始行程
  214. startPlan(e) {
  215. const planId = e.currentTarget.dataset.id
  216. // 切换到进行中选项卡并加载该行程
  217. this.setData({
  218. activeTab: 'in_progress'
  219. }, () => {
  220. // 加载特定行程数据
  221. this.loadSpecificPlan(planId)
  222. })
  223. },
  224. // 加载特定行程
  225. loadSpecificPlan(planId) {
  226. if (!app.globalData.token) return
  227. this.setData({ isLoading: true });
  228. wx.request({
  229. url: app.globalData.apiBaseUrl + '/api/user/plans/' + planId + '/',
  230. method: 'GET',
  231. header: {
  232. 'Authorization': 'Token ' + app.globalData.token,
  233. 'Content-Type': 'application/json'
  234. },
  235. success: (res) => {
  236. if (res.statusCode === 200 && res.data.status === 'success') {
  237. this.setData({
  238. inProgressPlans: [res.data.data.plan],
  239. });
  240. this.processPlanMapData(res.data.data.plan);
  241. } else {
  242. this.handleErrorResponse(res);
  243. }
  244. },
  245. fail: (err) => {
  246. this.handleRequestError(err);
  247. },
  248. complete: () => {
  249. this.setData({ isLoading: false });
  250. }
  251. });
  252. },
  253. // 完成行程
  254. completePlan(e) {
  255. const planId = e.currentTarget.dataset.id
  256. wx.showModal({
  257. title: '确认完成',
  258. content: '确定要结束并保存此行程吗?',
  259. success: (res) => {
  260. if (res.confirm) {
  261. this.markPlanAsCompleted(planId)
  262. }
  263. }
  264. })
  265. },
  266. // 标记行程为已完成
  267. markPlanAsCompleted(planId) {
  268. wx.request({
  269. url: app.globalData.apiBaseUrl + '/api/user/plans/' + planId + '/complete/',
  270. method: 'POST',
  271. header: {
  272. 'Authorization': 'Token ' + app.globalData.token,
  273. 'Content-Type': 'application/json'
  274. },
  275. success: (res) => {
  276. if (res.statusCode === 200 && res.data.status === 'success') {
  277. wx.showToast({
  278. title: '行程已完成',
  279. icon: 'success'
  280. })
  281. // 重新加载数据
  282. this.loadInProgressPlans()
  283. this.loadCompletedPlans(1)
  284. this.loadAllPlans(1)
  285. } else {
  286. this.handleErrorResponse(res)
  287. }
  288. },
  289. fail: (err) => {
  290. this.handleRequestError(err)
  291. }
  292. })
  293. },
  294. // 处理单个行程的地图数据
  295. processPlanMapData(plan) {
  296. if (!plan || !Array.isArray(plan.day_plans) || plan.day_plans.length === 0) {
  297. console.warn('无效的行程数据或没有天数数据', plan);
  298. this.setData({
  299. latestPlanMapData: {
  300. showMap: false,
  301. markers: [],
  302. polylines: [],
  303. includePoints: []
  304. }
  305. });
  306. return;
  307. }
  308. const markers = [];
  309. const polylines = [];
  310. const includePoints = [];
  311. const dayColors = ['#e54d42', '#f37b1d', '#1cbbb4', '#0081ff', '#6739b6', '#9c26b0', '#e03997'];
  312. try {
  313. // 处理每一天的景点
  314. plan.day_plans.forEach((day, dayIndex) => {
  315. if (!day || !Array.isArray(day.spots)) {
  316. console.warn('无效的天数据或没有景点数据', day);
  317. return;
  318. }
  319. const dayColor = dayColors[dayIndex % dayColors.length];
  320. const dayPoints = [];
  321. // 处理当天景点
  322. day.spots.forEach((spot, spotIndex) => {
  323. if (!spot || spot.latitude === undefined || spot.longitude === undefined) {
  324. console.warn('景点缺少位置信息', spot);
  325. return;
  326. }
  327. const markerId = dayIndex * 100 + spotIndex;
  328. markers.push({
  329. id: markerId,
  330. latitude: spot.latitude,
  331. longitude: spot.longitude,
  332. iconPath: spot.has_checked ? '/images/marker.png' : '/images/marker-default.png',
  333. width: 24,
  334. height: 24,
  335. callout: {
  336. content: `第${day.day_number}天 ${spotIndex + 1}. ${spot.name}`,
  337. color: '#333',
  338. fontSize: 14,
  339. borderRadius: 4,
  340. display: 'BYCLICK'
  341. },
  342. customData: {
  343. dayIndex,
  344. spotIndex,
  345. spotId: spot.id,
  346. dayNumber: day.day_number
  347. },
  348. label: {
  349. content: `${dayIndex + 1}-${spotIndex + 1}`,
  350. color: dayColor,
  351. fontSize: 12,
  352. bgColor: '#ffffff',
  353. padding: 4,
  354. borderRadius: 4,
  355. borderWidth: 1,
  356. borderColor: dayColor
  357. }
  358. });
  359. dayPoints.push({
  360. latitude: spot.latitude,
  361. longitude: spot.longitude
  362. });
  363. });
  364. // 添加当天路线
  365. if (dayPoints.length > 1) {
  366. polylines.push({
  367. points: dayPoints,
  368. color: dayColor,
  369. width: 4,
  370. arrowLine: true,
  371. dottedLine: false
  372. });
  373. }
  374. includePoints.push(...dayPoints);
  375. });
  376. // 计算地图中心点和缩放级别
  377. const { center, scale } = this.calculateMapViewport(includePoints);
  378. // 更新地图数据
  379. this.setData({
  380. latestPlanMapData: {
  381. markers,
  382. polylines,
  383. latitude: center.latitude,
  384. longitude: center.longitude,
  385. scale: scale,
  386. showMap: markers.length > 0,
  387. includePoints,
  388. dayColors
  389. }
  390. });
  391. // 播放路线动画
  392. this.playRouteAnimation(polylines);
  393. } catch (error) {
  394. console.error('处理地图数据时出错:', error);
  395. this.setData({
  396. latestPlanMapData: {
  397. showMap: false,
  398. markers: [],
  399. polylines: [],
  400. includePoints: []
  401. }
  402. });
  403. }
  404. },
  405. // 计算地图视野
  406. calculateMapViewport(points) {
  407. if (!points || points.length === 0) {
  408. return {
  409. center: { latitude: 39.9042, longitude: 116.4074 },
  410. scale: 12
  411. };
  412. }
  413. // 计算所有点的边界
  414. const lats = points.map(p => p.latitude);
  415. const lngs = points.map(p => p.longitude);
  416. const minLat = Math.min(...lats);
  417. const maxLat = Math.max(...lats);
  418. const minLng = Math.min(...lngs);
  419. const maxLng = Math.max(...lngs);
  420. // 计算中心点
  421. const center = {
  422. latitude: (minLat + maxLat) / 2,
  423. longitude: (minLng + maxLng) / 2
  424. };
  425. // 计算缩放级别
  426. const latRange = maxLat - minLat;
  427. const lngRange = maxLng - minLng;
  428. const maxRange = Math.max(latRange, lngRange);
  429. // 根据范围大小调整缩放级别
  430. let scale = 15 - Math.floor(maxRange * 15);
  431. scale = Math.max(10, Math.min(scale, 17));
  432. return { center, scale };
  433. },
  434. // 播放路线动画
  435. playRouteAnimation(polylines) {
  436. if (!polylines || polylines.length === 0) return;
  437. // 初始状态:所有路线半透明
  438. const transparentPolylines = polylines.map(line => ({
  439. ...line,
  440. color: line.color + '80' // 添加透明度
  441. }));
  442. this.setData({
  443. 'latestPlanMapData.polylines': transparentPolylines
  444. });
  445. // 逐个显示路线
  446. let i = 0;
  447. const timer = setInterval(() => {
  448. if (i >= polylines.length) {
  449. clearInterval(timer);
  450. return;
  451. }
  452. const updatedPolylines = [...polylines];
  453. updatedPolylines[i] = {
  454. ...updatedPolylines[i],
  455. color: polylines[i].color.replace('80', '') // 移除透明度
  456. };
  457. this.setData({
  458. 'latestPlanMapData.polylines': updatedPolylines
  459. });
  460. i++;
  461. }, 500);
  462. },
  463. // 切换显示指定天数的路线
  464. switchMapDay(e) {
  465. const dayIndex = e.currentTarget.dataset.day;
  466. const dayColors = ['#e54d42', '#f37b1d', '#1cbbb4', '#0081ff', '#6739b6', '#9c26b0', '#e03997'];
  467. this.setData({
  468. currentMapDay: dayIndex
  469. });
  470. if (!this.data.inProgressPlans.length) return;
  471. const plan = this.data.inProgressPlans[0];
  472. const day = plan.day_plans[dayIndex];
  473. const dayColor = dayColors[dayIndex % dayColors.length];
  474. // 1. 准备当天的景点数据
  475. const markers = [];
  476. const includePoints = [];
  477. day.spots.forEach((spot, spotIndex) => {
  478. if (!spot || !spot.latitude || !spot.longitude) return;
  479. const markerId = dayIndex * 100 + spotIndex;
  480. markers.push({
  481. id: markerId,
  482. latitude: spot.latitude,
  483. longitude: spot.longitude,
  484. iconPath: spot.has_checked ? '/images/marker.png' : '/images/marker-default.png',
  485. width: 24,
  486. height: 24,
  487. callout: {
  488. content: `第${day.day_number}天 ${spotIndex + 1}. ${spot.name}`,
  489. color: '#333',
  490. fontSize: 14,
  491. borderRadius: 4,
  492. display: 'BYCLICK'
  493. },
  494. customData: {
  495. dayIndex,
  496. spotIndex,
  497. spotId: spot.id,
  498. dayNumber: day.day_number
  499. },
  500. label: {
  501. content: `${dayIndex + 1}-${spotIndex + 1}`,
  502. color: dayColor,
  503. fontSize: 12,
  504. bgColor: '#ffffff',
  505. padding: 4,
  506. borderRadius: 4,
  507. borderWidth: 1,
  508. borderColor: dayColor
  509. }
  510. });
  511. includePoints.push({
  512. latitude: spot.latitude,
  513. longitude: spot.longitude
  514. });
  515. });
  516. // 2. 生成当天路线
  517. const polylines = [];
  518. if (day.spots.length > 1) {
  519. const points = day.spots
  520. .filter(spot => spot.latitude && spot.longitude)
  521. .map(spot => ({
  522. latitude: spot.latitude,
  523. longitude: spot.longitude
  524. }));
  525. if (points.length > 1) {
  526. polylines.push({
  527. points: points,
  528. color: dayColor, // 使用当天专属颜色
  529. width: 4,
  530. arrowLine: true,
  531. dottedLine: false
  532. });
  533. }
  534. }
  535. // 3. 计算地图视野
  536. const { center, scale } = this.calculateMapViewport(includePoints);
  537. // 4. 更新地图数据
  538. this.setData({
  539. latestPlanMapData: {
  540. markers,
  541. polylines,
  542. latitude: center.latitude,
  543. longitude: center.longitude,
  544. scale,
  545. showMap: markers.length > 0,
  546. includePoints,
  547. dayColors: [dayColor] // 只保留当前天的颜色
  548. }
  549. });
  550. // 5. 播放路线动画
  551. this.playRouteAnimation(polylines);
  552. },
  553. // 重新定位到所有景点
  554. resetToAllAttractions() {
  555. this.setData({
  556. currentMapDay: -1
  557. });
  558. // 重新处理地图数据,显示所有景点
  559. if (this.data.inProgressPlans.length > 0) {
  560. this.processPlanMapData(this.data.inProgressPlans[0]);
  561. }
  562. },
  563. // 地图缩放控制
  564. handleZoomIn() {
  565. this.setData({
  566. 'latestPlanMapData.scale': Math.min(this.data.latestPlanMapData.scale + 1, 18)
  567. });
  568. },
  569. handleZoomOut() {
  570. this.setData({
  571. 'latestPlanMapData.scale': Math.max(this.data.latestPlanMapData.scale - 1, 10)
  572. });
  573. },
  574. // 处理标记点点击
  575. handleMarkerTap(e) {
  576. const markerId = e.detail.markerId;
  577. const marker = this.data.latestPlanMapData.markers.find(m => m.id === markerId);
  578. if (!marker || !this.data.inProgressPlans || this.data.inProgressPlans.length === 0) {
  579. console.error('无法找到标记或行程数据');
  580. return;
  581. }
  582. const { dayIndex, spotIndex, spotId } = marker.customData || {};
  583. const spot = this.data.inProgressPlans[0].day_plans[dayIndex].spots[spotIndex];
  584. if (!spot) {
  585. console.error('无法找到景点数据');
  586. return;
  587. }
  588. // 显示加载状态
  589. this.setData({
  590. showSpotModal: true,
  591. currentSpot: {
  592. ...spot,
  593. isLoadingImage: true // 添加加载状态
  594. },
  595. currentDayIndex: dayIndex,
  596. currentSpotIndex: spotIndex
  597. });
  598. // 获取景点图片
  599. this.fetchAttractionImage(spot).then(imageUrl => {
  600. this.setData({
  601. 'currentSpot.image': imageUrl,
  602. 'currentSpot.isLoadingImage': false
  603. });
  604. }).catch(err => {
  605. console.error('获取景点图片失败:', err);
  606. this.setData({
  607. 'currentSpot.isLoadingImage': false
  608. });
  609. });
  610. },
  611. // 从API获取景点图片
  612. fetchAttractionImage(spot) {
  613. return new Promise((resolve, reject) => {
  614. // 如果已经有有效图片,直接返回
  615. if (spot.image && !spot.image.includes('default')) {
  616. resolve(spot.image);
  617. return;
  618. }
  619. // 获取城市名称
  620. let cityName = '济南'; // 默认值
  621. if (spot.city_id && this.data.cityOptions) {
  622. const city = this.data.cityOptions.find(c => c.value === spot.city_id);
  623. if (city) cityName = city.label.replace(/市$/, '');
  624. }
  625. wx.request({
  626. url: `${app.globalData.apiBaseUrl}/api/attractions/image/`,
  627. method: 'GET',
  628. data: {
  629. name: spot.name,
  630. city: cityName
  631. },
  632. success: (res) => {
  633. if (res.data.status === 'success' && res.data.image_url) {
  634. // 检查URL是否完整,如果不完整则补全
  635. const imageUrl = res.data.image_url.startsWith('http') ?
  636. res.data.image_url :
  637. `${app.globalData.mediaBaseUrl}${res.data.image_url}`;
  638. resolve(imageUrl);
  639. } else {
  640. reject(new Error('未获取到有效图片'));
  641. }
  642. },
  643. fail: (err) => {
  644. reject(err);
  645. }
  646. });
  647. });
  648. },
  649. // 显示景点详情
  650. showSpotDetail(e) {
  651. const dayIndex = e.currentTarget.dataset.day;
  652. const spotIndex = e.currentTarget.dataset.spot;
  653. const spotId = e.currentTarget.dataset.spotid;
  654. const spot = this.data.inProgressPlans[0].day_plans[dayIndex].spots[spotIndex];
  655. // 显示加载状态
  656. this.setData({
  657. showSpotModal: true,
  658. currentSpot: {
  659. ...spot,
  660. isLoadingImage: true // 添加加载状态
  661. },
  662. currentDayIndex: dayIndex,
  663. currentSpotIndex: spotIndex
  664. });
  665. // 获取景点图片
  666. this.fetchAttractionImage(spot).then(imageUrl => {
  667. this.setData({
  668. 'currentSpot.image': imageUrl,
  669. 'currentSpot.isLoadingImage': false
  670. });
  671. }).catch(err => {
  672. console.error('获取景点图片失败:', err);
  673. this.setData({
  674. 'currentSpot.isLoadingImage': false,
  675. 'currentSpot.image': '/images/default-spot.png' // 设置默认图片
  676. });
  677. });
  678. },
  679. // 隐藏景点详情
  680. hideSpotModal() {
  681. this.setData({
  682. showSpotModal: false
  683. });
  684. },
  685. // 打卡景点
  686. checkInSpot(e) {
  687. const dayIndex = e.currentTarget.dataset.day;
  688. const spotIndex = e.currentTarget.dataset.spot;
  689. const spotId = e.currentTarget.dataset.spotid;
  690. const planId = this.data.inProgressPlans[0]?.id;
  691. // 获取当前景点数据
  692. const spot = this.data.inProgressPlans[0].day_plans[dayIndex].spots[spotIndex];
  693. // 获取该景点的所有打卡记录
  694. this.getSpotCheckinRecords(spotId, planId).then(records => {
  695. // 如果有打卡记录,使用最后一条记录的数据
  696. const lastRecord = records.length > 0 ? records[0] : null;
  697. this.setData({
  698. tempCheckinData: {
  699. dayIndex,
  700. spotIndex,
  701. spotId,
  702. planId,
  703. isEdit: spot.has_checked // 标记是否为编辑模式
  704. },
  705. showCheckinModal: true,
  706. checkinImage: lastRecord?.image_url || spot.checkin_images?.[0] || '',
  707. checkinNote: lastRecord?.note || spot.checkin_note || ''
  708. });
  709. }).catch(err => {
  710. console.error('获取打卡记录失败:', err);
  711. // 失败时仍显示基本数据
  712. this.setData({
  713. tempCheckinData: {
  714. dayIndex,
  715. spotIndex,
  716. spotId,
  717. planId,
  718. isEdit: spot.has_checked
  719. },
  720. showCheckinModal: true,
  721. checkinImage: spot.checkin_images?.[0] || '',
  722. checkinNote: spot.checkin_note || ''
  723. });
  724. });
  725. },
  726. // 获取景点的打卡记录
  727. getSpotCheckinRecords(spotId, planId) {
  728. return new Promise((resolve, reject) => {
  729. wx.request({
  730. url: `${app.globalData.apiBaseUrl}/api/user/checkins/`,
  731. method: 'GET',
  732. data: {
  733. spot_id: spotId,
  734. plan_id: planId
  735. },
  736. header: { 'Authorization': 'Token ' + app.globalData.token },
  737. success: (res) => {
  738. if (res.statusCode === 200) {
  739. // 确保返回的数据是当前景点的打卡记录
  740. const filteredRecords = res.data.data?.checkins.filter(record =>
  741. record.spot_id.toString() === spotId.toString()
  742. ) || [];
  743. resolve(filteredRecords);
  744. } else {
  745. reject(new Error(res.data.message || '获取打卡记录失败'));
  746. }
  747. },
  748. fail: (err) => {
  749. reject(err);
  750. }
  751. });
  752. });
  753. },
  754. chooseCheckinImage() {
  755. wx.chooseImage({
  756. count: 1,
  757. sizeType: ['compressed'],
  758. sourceType: ['album', 'camera'],
  759. success: (res) => {
  760. this.setData({
  761. checkinImage: res.tempFilePaths[0],
  762. isUploading: true
  763. });
  764. // 上传图片
  765. this.uploadCheckinImage(res.tempFilePaths[0]);
  766. }
  767. });
  768. },
  769. // 上传打卡图片
  770. uploadCheckinImage(tempFilePath) {
  771. wx.uploadFile({
  772. url: app.globalData.apiBaseUrl + '/api/upload/checkin-image/',
  773. filePath: tempFilePath,
  774. name: 'image',
  775. header: {
  776. 'Authorization': 'Token ' + app.globalData.token,
  777. 'Content-Type': 'multipart/form-data'
  778. },
  779. success: (res) => {
  780. try {
  781. const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
  782. if (data.status === 'success') {
  783. this.setData({
  784. checkinImage: data.data.image_url,
  785. isUploading: false
  786. });
  787. } else {
  788. wx.showToast({ title: '图片上传失败', icon: 'none' });
  789. this.setData({ isUploading: false });
  790. }
  791. } catch (e) {
  792. console.error('解析响应失败:', e);
  793. wx.showToast({ title: '处理响应失败', icon: 'none' });
  794. this.setData({ isUploading: false });
  795. }
  796. },
  797. fail: (err) => {
  798. console.error('上传失败:', err);
  799. wx.showToast({ title: '上传失败', icon: 'none' });
  800. this.setData({ isUploading: false });
  801. }
  802. });
  803. },
  804. // 文案输入处理
  805. onCheckinNoteInput(e) {
  806. this.setData({
  807. checkinNote: e.detail.value
  808. });
  809. },
  810. // 提交打卡
  811. submitCheckin() {
  812. const { dayIndex, spotIndex, spotId, planId, isEdit } = this.data.tempCheckinData;
  813. const { checkinImage, checkinNote } = this.data;
  814. if (!checkinImage) {
  815. wx.showToast({ title: '请上传打卡照片', icon: 'none' });
  816. return;
  817. }
  818. wx.showLoading({ title: isEdit ? '更新打卡中...' : '提交打卡中...', mask: true });
  819. // 统一使用POST方法,后端可以通过操作类型区分
  820. wx.request({
  821. url: `${app.globalData.apiBaseUrl}/api/user/checkin/`,
  822. method: 'POST',
  823. data: {
  824. spot_id: spotId.toString(),
  825. plan_id: planId.toString(),
  826. image_url: checkinImage,
  827. note: checkinNote,
  828. is_edit: isEdit // 添加编辑标记
  829. },
  830. header: {
  831. 'Authorization': 'Token ' + app.globalData.token,
  832. 'Content-Type': 'application/json'
  833. },
  834. success: (res) => {
  835. wx.hideLoading();
  836. // 接受200或201状态码
  837. if (res.statusCode === 200 || res.statusCode === 201) {
  838. try {
  839. // 处理响应数据
  840. const responseData = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
  841. if (responseData.status === 'success') {
  842. // 更新本地数据
  843. const keyPath = `inProgressPlans[0].day_plans[${dayIndex}].spots[${spotIndex}]`;
  844. this.setData({
  845. [keyPath]: {
  846. ...this.data.inProgressPlans[0].day_plans[dayIndex].spots[spotIndex],
  847. has_checked: true,
  848. checkin_images: [checkinImage],
  849. checkin_note: checkinNote
  850. },
  851. showCheckinModal: false,
  852. 'currentSpot.has_checked': true,
  853. 'currentSpot.checkin_images': [checkinImage],
  854. 'currentSpot.checkin_note': checkinNote
  855. });
  856. wx.showToast({
  857. title: isEdit ? '更新成功' : '打卡成功',
  858. icon: 'success',
  859. duration: 2000
  860. });
  861. this.updateMarkerAfterCheckIn(dayIndex, spotIndex);
  862. this.loadInProgressPlans(); // 刷新数据
  863. } else {
  864. throw new Error(responseData.message || '操作失败');
  865. }
  866. } catch (e) {
  867. console.error('处理响应数据失败:', e);
  868. wx.showToast({
  869. title: '处理响应数据失败',
  870. icon: 'none'
  871. });
  872. }
  873. } else {
  874. throw new Error(res.data?.message || `请求失败,状态码: ${res.statusCode}`);
  875. }
  876. },
  877. fail: (err) => {
  878. wx.hideLoading();
  879. console.error('请求失败:', err);
  880. wx.showToast({
  881. title: '网络错误,请检查连接',
  882. icon: 'none'
  883. });
  884. }
  885. });
  886. },
  887. // 关闭打卡弹窗
  888. closeCheckinModal() {
  889. this.setData({
  890. showCheckinModal: false,
  891. checkinImage: '',
  892. checkinNote: ''
  893. });
  894. },
  895. // 更新地图标记点状态
  896. updateMarkerAfterCheckIn(dayIndex, spotIndex) {
  897. const markerId = dayIndex * 100 + spotIndex;
  898. const markers = this.data.latestPlanMapData.markers.map(marker => {
  899. if (marker.id === markerId) {
  900. return {
  901. ...marker,
  902. iconPath: '/images/marker.png',
  903. callout: { // 更新callout内容
  904. ...marker.callout,
  905. content: `已打卡 - ${marker.callout.content}`
  906. }
  907. };
  908. }
  909. return marker;
  910. });
  911. this.setData({ 'latestPlanMapData.markers': markers });
  912. },
  913. // 导航到景点位置
  914. navigateToSpot() {
  915. const { latitude, longitude, name, address } = this.data.currentSpot;
  916. // 验证坐标是否存在
  917. if (!latitude || !longitude) {
  918. wx.showToast({
  919. title: '该地点缺少位置信息',
  920. icon: 'none'
  921. });
  922. return;
  923. }
  924. // 使用微信内置地图
  925. wx.openLocation({
  926. latitude: Number(latitude),
  927. longitude: Number(longitude),
  928. name: name || '目的地',
  929. address: address || '', // 添加详细地址
  930. scale: 18 // 缩放级别
  931. });
  932. },
  933. // 跳转到详情页
  934. navigateToDetail(e) {
  935. const planId = e.currentTarget.dataset.id
  936. wx.navigateTo({
  937. url: `/pages/xingchengxiangqing/xingchengxiangqing?id=${planId}`
  938. })
  939. },
  940. // 跳转到创建行程页
  941. navigateToCreate() {
  942. wx.navigateTo({
  943. url: '/pages/xingchengguihua/xingchengguihua'
  944. })
  945. },
  946. // 分享行程
  947. sharePlan(e) {
  948. const planId = e.currentTarget.dataset.id;
  949. let plan;
  950. // 根据当前选项卡确定要分享的行程
  951. if (this.data.activeTab === 'in_progress' && this.data.inProgressPlans.length > 0) {
  952. plan = this.data.inProgressPlans[0];
  953. } else if (this.data.activeTab === 'completed') {
  954. plan = this.data.completedPlans.find(p => p.id === planId);
  955. } else if (this.data.activeTab === 'all') {
  956. plan = this.data.allPlans.find(p => p.id === planId);
  957. }
  958. if (!plan) return;
  959. wx.showShareMenu({
  960. withShareTicket: true
  961. });
  962. },
  963. onShareAppMessage() {
  964. let plan;
  965. // 根据当前选项卡确定要分享的行程
  966. if (this.data.activeTab === 'in_progress' && this.data.inProgressPlans.length > 0) {
  967. plan = this.data.inProgressPlans[0];
  968. } else if (this.data.activeTab === 'completed' && this.data.completedPlans.length > 0) {
  969. plan = this.data.completedPlans[0];
  970. } else if (this.data.activeTab === 'all' && this.data.allPlans.length > 0) {
  971. plan = this.data.allPlans[0];
  972. }
  973. if (!plan) return {};
  974. return {
  975. title: plan.title || '我的红色之旅行程',
  976. path: `/pages/xingchengxiangqing/xingchengxiangqing?id=${plan.id}`,
  977. imageUrl: plan.day_plans[0]?.spots[0]?.image || '/images/red-tourism-share.jpg'
  978. };
  979. },
  980. // 处理错误响应
  981. handleErrorResponse(res) {
  982. let errorMsg = '加载失败'
  983. if (res.data && res.data.message) {
  984. errorMsg += ': ' + res.data.message
  985. }
  986. if (res.statusCode === 401) {
  987. errorMsg = '登录已过期,请重新登录'
  988. this.showLoginModal()
  989. }
  990. wx.showToast({
  991. title: errorMsg,
  992. icon: 'none'
  993. })
  994. },
  995. // 处理请求错误
  996. handleRequestError(err) {
  997. console.error('请求失败:', err)
  998. wx.showToast({
  999. title: '网络错误,请检查网络连接',
  1000. icon: 'none'
  1001. })
  1002. },
  1003. // 显示登录弹窗
  1004. showLoginModal() {
  1005. wx.showModal({
  1006. title: '登录过期',
  1007. content: '您的登录已过期,请重新登录',
  1008. confirmText: '去登录',
  1009. success: (res) => {
  1010. if (res.confirm) {
  1011. wx.navigateTo({
  1012. url: '/pages/login/login'
  1013. })
  1014. }
  1015. }
  1016. })
  1017. }
  1018. })