ai-plan.js 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550
  1. // pages/ai-plan/ai-plan.js
  2. const app = getApp();
  3. const AMapKey = 'ad2afc314b741d5520274e419fda8655'; // 请替换为实际的高德地图Key
  4. Page({
  5. data: {
  6. currentStep: 1,
  7. planGenerated: false,
  8. selectedDays: 3,
  9. selectedCity: '',
  10. selectedInterests: ['patriotism'],
  11. isRegenerating: false,
  12. planData: null,
  13. selectedTransport: 'public',
  14. customRequirements: '',
  15. dayOptions: [
  16. { label: '1天', value: 1 },
  17. { label: '2天', value: 2 },
  18. { label: '3天', value: 3 },
  19. { label: '4天', value: 4 },
  20. { label: '5天', value: 5 },
  21. { label: '6天', value: 6 },
  22. ],
  23. cityOptions: [],
  24. interestOptions: [
  25. { label: '革命历史', value: 'history' },
  26. { label: '红色教育', value: 'education' },
  27. { label: '党史学习', value: 'party-history' },
  28. { label: '爱国主义', value: 'patriotism' }
  29. ],
  30. transportOptions: [
  31. { label: '公共交通', value: 'public', icon: '/images/icons/bus.png' },
  32. { label: '自驾', value: 'drive', icon: '/images/icons/car.png' },
  33. { label: '步行', value: 'walk', icon: '/images/icons/walk.png' },
  34. { label: '骑行', value: 'bike', icon: '/images/icons/bike.png' }
  35. ],
  36. isLoading: false,
  37. apiStatus: {
  38. citiesLoaded: false,
  39. attractionsLoaded: false
  40. },
  41. showAttractionDetail: false,
  42. currentAttraction: null,
  43. attractionDetail: null,
  44. lastGeneratedAt: null,
  45. isRegenerated: false,
  46. mapData: {
  47. markers: [],
  48. polyline: [],
  49. latitude: 39.9042,
  50. longitude: 116.4074,
  51. scale: 12,
  52. showMap: false,
  53. includePoints: []
  54. },
  55. currentMapDay: 0,
  56. mapLoaded: false,
  57. isFavorite: false,
  58. favoriteLoading: false,
  59. },
  60. onLoad() {
  61. this.initPage();
  62. this.loadInitialData();
  63. this.initMapControls();
  64. // 检查是否有需要恢复的状态
  65. const pageState = wx.getStorageSync('aiPlanPageState');
  66. if (pageState && wx.getStorageSync('token')) {
  67. // 如果有保存的状态且用户已登录,恢复状态
  68. this.restorePageState();
  69. }
  70. },
  71. onShow() {
  72. // 页面显示时检查是否是从登录页面返回
  73. const token = wx.getStorageSync('token');
  74. const pageState = wx.getStorageSync('aiPlanPageState');
  75. if (token && pageState) {
  76. // 用户已登录且有保存的状态,恢复状态并继续操作
  77. this.restorePageState();
  78. }
  79. },
  80. initPage() {
  81. const date = new Date();
  82. this.setData({
  83. currentDate: `${date.getMonth() + 1}月${date.getDate()}日`
  84. });
  85. this.initDefaultImages();
  86. },
  87. initDefaultImages() {
  88. this.setData({
  89. transportOptions: this.data.transportOptions.map(item => ({
  90. ...item,
  91. icon: this.checkImageExists(item.icon) ? item.icon : '/images/default-transport.png'
  92. }))
  93. });
  94. },
  95. loadInitialData() {
  96. this.getCities();
  97. },
  98. // 获取城市列表
  99. getCities() {
  100. this.setData({
  101. isLoading: true,
  102. cityOptions: []
  103. });
  104. wx.request({
  105. url: app.globalData.apiBaseUrl + '/api/cities/',
  106. method: 'GET',
  107. success: (res) => {
  108. if (res.statusCode === 200) {
  109. const rawData = res.data.data || res.data;
  110. const cityOptions = rawData.map(city => ({
  111. label: city.name || '未知城市',
  112. value: String(city.id || '0'),
  113. disabled: city.is_available === false,
  114. location: {
  115. latitude: city.latitude || 39.9042,
  116. longitude: city.longitude || 116.4074
  117. }
  118. }));
  119. this.setData({
  120. cityOptions,
  121. 'apiStatus.citiesLoaded': true
  122. });
  123. } else {
  124. this.handleApiError(res);
  125. }
  126. },
  127. fail: this.handleNetworkError,
  128. complete: () => {
  129. this.setData({ isLoading: false });
  130. }
  131. });
  132. },
  133. // 检查图片是否存在
  134. checkImageExists(path) {
  135. try {
  136. const fs = wx.getFileSystemManager();
  137. return fs.accessSync(path) === undefined;
  138. } catch (e) {
  139. return false;
  140. }
  141. },
  142. // 从API获取景点图片
  143. fetchAttractionImage(attraction) {
  144. return new Promise((resolve, reject) => {
  145. // 获取城市名称
  146. let cityName = '济南'; // 默认值
  147. if (attraction.city_id && this.data.cityOptions) {
  148. const city = this.data.cityOptions.find(c => c.value === attraction.city_id);
  149. if (city) cityName = city.label.replace(/市$/, '');
  150. }
  151. wx.request({
  152. url: `${app.globalData.apiBaseUrl}/api/attractions/image/`,
  153. method: 'GET',
  154. data: {
  155. name: attraction.name,
  156. city: cityName
  157. },
  158. success: (res) => {
  159. if (res.data.status === 'success' && res.data.image_url) {
  160. // 检查URL是否完整,如果不完整则补全
  161. const imageUrl = res.data.image_url.startsWith('http') ?
  162. res.data.image_url :
  163. `${app.globalData.mediaBaseUrl}${res.data.image_url}`;
  164. resolve(imageUrl);
  165. } else {
  166. reject(new Error('未获取到有效图片'));
  167. }
  168. },
  169. fail: (err) => {
  170. reject(err);
  171. }
  172. });
  173. });
  174. },
  175. // 处理图片加载错误
  176. handleImageError(e) {
  177. const { id, name, city } = e.currentTarget.dataset;
  178. console.log('图片加载失败:', name, id);
  179. // 设置加载状态
  180. this.setData({
  181. 'currentAttraction.isLoadingImage': true
  182. });
  183. // 异步调用API获取新图片
  184. this.fetchAttractionImage({
  185. id: id,
  186. name: name,
  187. city_id: city
  188. }).then(imageUrl => {
  189. this.updateAttractionImage(id, name, imageUrl);
  190. }).catch(err => {
  191. console.error('获取图片失败:', err);
  192. this.setData({
  193. 'currentAttraction.image': '/images/image-load-failed.jpg',
  194. 'currentAttraction.isLoadingImage': false
  195. });
  196. });
  197. },
  198. // 更新景点图片
  199. updateAttractionImage(id, name, newImageUrl) {
  200. // 更新当前景点图片
  201. if (this.data.currentAttraction &&
  202. (this.data.currentAttraction.id === id || this.data.currentAttraction.name === name)) {
  203. this.setData({
  204. 'currentAttraction.image': newImageUrl,
  205. 'currentAttraction.isLoadingImage': false
  206. });
  207. }
  208. // 更新planData中的对应景点图片
  209. if (this.data.planData && this.data.planData.days) {
  210. const updatedPlan = JSON.parse(JSON.stringify(this.data.planData));
  211. let updated = false;
  212. for (const day of updatedPlan.days) {
  213. for (const attraction of day.attractions) {
  214. if ((id && attraction.id === id) || (name && attraction.name === name)) {
  215. attraction.image = newImageUrl;
  216. updated = true;
  217. break;
  218. }
  219. }
  220. if (updated) break;
  221. }
  222. if (updated) {
  223. this.setData({ planData: updatedPlan });
  224. }
  225. }
  226. },
  227. // 图片加载成功处理
  228. handleImageLoad(e) {
  229. console.log('图片加载成功', e.currentTarget.dataset.name);
  230. this.setData({
  231. 'currentAttraction.isLoadingImage': false
  232. });
  233. },
  234. // 选择天数
  235. selectDays(e) {
  236. this.setData({ selectedDays: e.currentTarget.dataset.value });
  237. },
  238. // 切换城市选择
  239. toggleCity(e) {
  240. const value = String(e.currentTarget.dataset.value);
  241. this.setData({
  242. selectedCity: this.data.selectedCity === value ? '' : value
  243. });
  244. },
  245. // 切换兴趣爱好
  246. toggleInterest(e) {
  247. const value = e.currentTarget.dataset.value;
  248. const index = this.data.selectedInterests.indexOf(value);
  249. let newInterests = [...this.data.selectedInterests];
  250. if (index === -1) {
  251. newInterests.push(value);
  252. } else {
  253. newInterests.splice(index, 1);
  254. }
  255. this.setData({ selectedInterests: newInterests });
  256. },
  257. // 选择交通方式
  258. selectTransport(e) {
  259. this.setData({ selectedTransport: e.currentTarget.dataset.value });
  260. },
  261. // 更新自定义需求
  262. updateCustomRequirements(e) {
  263. this.setData({ customRequirements: e.detail.value });
  264. },
  265. // 验证输入
  266. validateInputs() {
  267. if (!this.data.selectedCity) {
  268. wx.showToast({ title: '请至少选择一个城市', icon: 'none' });
  269. return false;
  270. }
  271. if (this.data.selectedDays < 1) {
  272. wx.showToast({ title: '请选择有效天数', icon: 'none' });
  273. return false;
  274. }
  275. return true;
  276. },
  277. // 下一步
  278. goToNextStep() {
  279. if (this.data.currentStep === 1) {
  280. if (!this.validateInputs()) return;
  281. // 检查登录状态
  282. const token = wx.getStorageSync('token');
  283. if (!token) {
  284. // 保存当前页面状态,用于登录后恢复
  285. this.savePageState();
  286. // 跳转到登录页并携带当前页面路径和参数
  287. wx.navigateTo({
  288. url: `/pages/login/login?redirect=${encodeURIComponent('/pages/ai-plan/ai-plan')}&action=generate`
  289. });
  290. return;
  291. }
  292. this.setData({ currentStep: 2 });
  293. this.generatePlanWithAI();
  294. } else if (this.data.currentStep === 2) {
  295. this.setData({ currentStep: 3 });
  296. }
  297. },
  298. // 保存页面状态
  299. savePageState() {
  300. const pageState = {
  301. selectedDays: this.data.selectedDays,
  302. selectedCity: this.data.selectedCity,
  303. selectedInterests: this.data.selectedInterests,
  304. selectedTransport: this.data.selectedTransport,
  305. customRequirements: this.data.customRequirements,
  306. currentStep: this.data.currentStep
  307. };
  308. wx.setStorageSync('aiPlanPageState', pageState);
  309. },
  310. // 恢复页面状态
  311. restorePageState() {
  312. const pageState = wx.getStorageSync('aiPlanPageState');
  313. if (pageState) {
  314. this.setData({
  315. selectedDays: pageState.selectedDays,
  316. selectedCity: pageState.selectedCity,
  317. selectedInterests: pageState.selectedInterests,
  318. selectedTransport: pageState.selectedTransport,
  319. customRequirements: pageState.customRequirements,
  320. currentStep: pageState.currentStep
  321. });
  322. // 清除保存的状态
  323. wx.removeStorageSync('aiPlanPageState');
  324. // 如果是在第二步,继续生成计划
  325. if (pageState.currentStep === 1) {
  326. this.setData({ currentStep: 2 });
  327. this.generatePlanWithAI();
  328. }
  329. }
  330. },
  331. // 生成行程
  332. generatePlanWithAI() {
  333. if (this.data.isLoading) return;
  334. const token = wx.getStorageSync('token');
  335. console.log('token:', token);
  336. if (!token) {
  337. wx.showToast({
  338. title: '请先登录',
  339. icon: 'none'
  340. });
  341. // 保存当前页面状态
  342. this.savePageState();
  343. // 跳转到登录页并携带当前页面路径
  344. wx.navigateTo({
  345. url: `/pages/login/login?redirect=${encodeURIComponent('/pages/ai-plan/ai-plan')}`
  346. });
  347. return;
  348. }
  349. if (!this.validateInputs()) return;
  350. this.setData({
  351. isLoading: true,
  352. planGenerated: false,
  353. planData: null
  354. });
  355. const requestData = {
  356. city_ids: [Number(this.data.selectedCity)],
  357. days: Number(this.data.selectedDays),
  358. interests: [...this.data.selectedInterests, 'red-tourism'],
  359. transport: this.data.selectedTransport,
  360. custom_requirements: this.data.customRequirements
  361. };
  362. wx.request({
  363. url: app.globalData.apiBaseUrl + '/api/red-tourism/plan/',
  364. method: 'POST',
  365. data: requestData,
  366. header: {
  367. 'Content-Type': 'application/json',
  368. 'Authorization': 'Token ' + wx.getStorageSync('token')
  369. },
  370. success: (res) => {
  371. if (res.statusCode === 401) {
  372. // 处理token过期情况
  373. wx.removeStorageSync('token');
  374. wx.showToast({
  375. title: '登录已过期,请重新登录',
  376. icon: 'none'
  377. });
  378. wx.navigateTo({
  379. url: '/pages/login/login'
  380. });
  381. return;
  382. }
  383. console.log('API返回原始数据:', JSON.stringify(res.data));
  384. if (res.data?.days?.[0]?.attractions?.[0]) {
  385. console.log('首个景点数据结构:', res.data.days[0].attractions[0]);
  386. }
  387. if (res.statusCode === 200) {
  388. if (res.data && res.data.status === 'success' && res.data.data) {
  389. this.processAndShowPlan(res.data.data);
  390. } else {
  391. this.handleError('返回数据格式不正确');
  392. }
  393. } else {
  394. this.handleError(res.data?.message || `请求失败: ${res.statusCode}`);
  395. }
  396. },
  397. fail: (err) => {
  398. if (err.statusCode === 401) {
  399. wx.showToast({
  400. title: '登录已过期,请重新登录',
  401. icon: 'none'
  402. });
  403. // 清除无效的 token
  404. wx.removeStorageSync('token');
  405. // 跳转到登录页面
  406. wx.navigateTo({
  407. url: '/pages/login/login'
  408. });
  409. } else {
  410. this.handleError('网络请求失败,请检查连接');
  411. }
  412. },
  413. complete: () => {
  414. wx.hideLoading();
  415. this.setData({ isLoading: false });
  416. }
  417. });
  418. },
  419. // 处理并显示行程数据
  420. async processAndShowPlan(planData, isRegenerate = false) {
  421. try {
  422. // 获取所有景点的坐标
  423. const processedPlan = await this.getAttractionsCoordinates(planData);
  424. // 标准化处理后的数据
  425. const normalizedData = this.normalizePlanData(processedPlan);
  426. console.log('标准化后的数据:', JSON.stringify(normalizedData, null, 2));
  427. // 处理地图数据
  428. await this.processMapData(normalizedData.days);
  429. // 更新页面数据
  430. this.setData({
  431. planData: normalizedData,
  432. planGenerated: true,
  433. isLoading: false,
  434. lastGeneratedAt: new Date().toISOString(),
  435. isRegenerated: isRegenerate,
  436. mapLoaded: true,
  437. currentMapDay: -1 // 默认显示所有景点
  438. });
  439. } catch (error) {
  440. console.error('处理行程数据时出错:', error);
  441. this.handleError('处理行程数据时出错');
  442. }
  443. },
  444. // 通过高德地图API获取景点坐标
  445. async getAttractionsCoordinates(planData) {
  446. const days = planData.days || [];
  447. const processedDays = [];
  448. for (const day of days) {
  449. const processedAttractions = [];
  450. for (const attraction of day.attractions) {
  451. try {
  452. // 如果已经有坐标则跳过
  453. if (attraction.location && attraction.location.latitude && attraction.location.longitude) {
  454. processedAttractions.push(attraction);
  455. continue;
  456. }
  457. // 优先通过高德地图API获取坐标
  458. let location;
  459. try {
  460. location = await this.getLocationFromAMap(attraction.name, attraction.address);
  461. console.log(`通过高德地图API获取到${attraction.name}的坐标:`, location);
  462. } catch (amapError) {
  463. console.warn(`高德地图API获取${attraction.name}坐标失败:`, amapError);
  464. // 如果高德地图API失败,检查API返回的原始坐标
  465. if (attraction.latitude && attraction.longitude) {
  466. location = {
  467. latitude: parseFloat(attraction.latitude),
  468. longitude: parseFloat(attraction.longitude)
  469. };
  470. console.log(`使用API返回的原始坐标:`, location);
  471. } else {
  472. // 如果都没有,使用默认坐标
  473. throw new Error('无可用坐标数据');
  474. }
  475. }
  476. // 更新景点坐标
  477. processedAttractions.push({
  478. ...attraction,
  479. location: {
  480. latitude: location.latitude,
  481. longitude: location.longitude
  482. }
  483. });
  484. } catch (error) {
  485. console.error(`获取景点${attraction.name}坐标失败:`, error);
  486. // 使用默认坐标
  487. processedAttractions.push({
  488. ...attraction,
  489. location: {
  490. latitude: 39.9042, // 默认北京坐标
  491. longitude: 116.4074
  492. },
  493. _coord_status: 'default'
  494. });
  495. }
  496. }
  497. processedDays.push({
  498. ...day,
  499. attractions: processedAttractions
  500. });
  501. }
  502. return {
  503. ...planData,
  504. days: processedDays
  505. };
  506. },
  507. // 调用高德地图API获取坐标
  508. getLocationFromAMap(name, address) {
  509. return new Promise((resolve, reject) => {
  510. // 构建查询参数
  511. const keywords = encodeURIComponent(name);
  512. const city = address ? encodeURIComponent(address.split('市')[0] || '') : '';
  513. wx.request({
  514. url: `https://restapi.amap.com/v3/place/text?keywords=${keywords}&city=${city}&output=json&key=${AMapKey}`,
  515. method: 'GET',
  516. success: (res) => {
  517. if (res.data.status === '1' && res.data.pois && res.data.pois.length > 0) {
  518. const location = res.data.pois[0].location.split(',');
  519. resolve({
  520. latitude: parseFloat(location[1]),
  521. longitude: parseFloat(location[0]),
  522. _coord_source: 'amap'
  523. });
  524. } else {
  525. reject(new Error('未找到该景点的坐标信息'));
  526. }
  527. },
  528. fail: (err) => {
  529. reject(new Error(`高德地图API请求失败: ${err.errMsg}`));
  530. }
  531. });
  532. });
  533. },
  534. // 标准化行程数据
  535. normalizePlanData(planData) {
  536. // 默认行程模板
  537. const DEFAULT_PLAN = {
  538. title: '红色文化之旅',
  539. description: '通过参观革命历史遗址学习党史',
  540. statistics: {
  541. red_attractions_count: 0,
  542. total_attractions: 0,
  543. educational_points_count: 0,
  544. total_hours: 0
  545. },
  546. red_tourism_tips: [
  547. "请着装整洁,保持肃穆",
  548. "建议提前学习相关历史知识"
  549. ],
  550. suitable_for: "适合党员干部、学生团体等",
  551. days: [],
  552. education_goals: [
  553. "传承红色基因",
  554. "学习革命历史",
  555. "培养爱国情怀"
  556. ]
  557. };
  558. // 深度合并默认数据和API返回数据
  559. const normalizedData = this.deepMerge(DEFAULT_PLAN, planData);
  560. // 处理每日行程
  561. normalizedData.days = normalizedData.days.map((day, dayIndex) => {
  562. // 处理当天景点
  563. const attractions = day.attractions.map((attr, attrIndex) => {
  564. // 生成唯一ID
  565. const attractionId = attr.id || `${dayIndex}-${attrIndex}`;
  566. // 处理图片路径
  567. const cleanName = attr.name ? attr.name.replace(/[^\w\u4e00-\u9fa5]/g, '') : '';
  568. const imageUrl = attr.image || `${app.globalData.mediaBaseUrl}/attractions/${encodeURIComponent(cleanName)}.png`;
  569. // 处理坐标
  570. let location = {
  571. latitude: 36.6667, // 济南纬度
  572. longitude: 117.0000
  573. };
  574. if (attr.location) {
  575. location = {
  576. latitude: parseFloat(attr.location.latitude) || location.latitude,
  577. longitude: parseFloat(attr.location.longitude) || location.longitude
  578. };
  579. } else if (attr.latitude && attr.longitude) {
  580. location = {
  581. latitude: parseFloat(attr.latitude),
  582. longitude: parseFloat(attr.longitude)
  583. };
  584. }
  585. // 返回标准化后的景点数据
  586. return {
  587. id: attractionId,
  588. name: attr.name || '红色教育基地',
  589. description: attr.description || '红色教育基地,具有重要的历史教育意义',
  590. image: imageUrl,
  591. is_red_tourism: attr.is_red_tourism !== false,
  592. visit_time: attr.visit_time || this.generateVisitTime(attrIndex),
  593. duration: attr.duration || 120,
  594. address: attr.address || '地址信息待补充',
  595. open_hours: attr.open_hours || "09:00-17:00",
  596. ticket_info: attr.ticket_info || "凭身份证免费参观",
  597. history_significance: attr.history_significance || "革命历史重要遗址",
  598. location: location,
  599. educational_value: attr.educational_value || "高",
  600. visiting_etiquette: attr.visiting_etiquette || [
  601. "请保持庄严肃穆",
  602. "勿大声喧哗",
  603. "纪念馆内请勿拍照"
  604. ]
  605. };
  606. });
  607. // 生成当日总结
  608. const summary = day.summary || this.generateDaySummary(
  609. day.theme || '红色教育',
  610. attractions.filter(a => a.is_red_tourism)
  611. );
  612. // 返回标准化后的每日数据
  613. return {
  614. day: day.day || dayIndex + 1,
  615. theme: day.theme || '红色教育',
  616. transport: day.transport || this.data.selectedTransport || '公共交通',
  617. attractions: attractions,
  618. summary: summary,
  619. schedule: this.createDailySchedule(attractions, day.transport),
  620. travel_tips: day.travel_tips || [
  621. "建议穿着舒适鞋子",
  622. "携带饮用水和防晒用品"
  623. ]
  624. };
  625. });
  626. // 计算统计数据
  627. normalizedData.statistics = {
  628. red_attractions_count: normalizedData.days.reduce((count, day) =>
  629. count + day.attractions.filter(a => a.is_red_tourism).length, 0),
  630. total_attractions: normalizedData.days.reduce((count, day) =>
  631. count + day.attractions.length, 0),
  632. educational_points_count: normalizedData.days.reduce((count, day) =>
  633. count + day.attractions.filter(a => a.is_red_tourism).length * 2, 0),
  634. total_hours: normalizedData.days.length * 8 // 每天按8小时计算
  635. };
  636. return normalizedData;
  637. },
  638. // 辅助方法:获取图片处理统计信息
  639. getImageResolutionStats(days) {
  640. const stats = {
  641. total: 0,
  642. resolved: 0,
  643. default_used: 0,
  644. missing_ids: []
  645. };
  646. days.forEach(day => {
  647. day.attractions.forEach(attraction => {
  648. stats.total++;
  649. if (attraction.image !== '/images/default-attraction.jpg') {
  650. stats.resolved++;
  651. } else {
  652. stats.default_used++;
  653. if (!attraction.id) stats.missing_ids.push('unknown');
  654. }
  655. });
  656. });
  657. return stats;
  658. },
  659. // 辅助方法:生成每日总结
  660. generateDaySummary(theme, redAttractions) {
  661. const attractionNames = redAttractions.map(a => a.name).join('、');
  662. return `今日主题为"${theme}",参观了${redAttractions.length}个红色教育基地${
  663. attractionNames ? `,包括${attractionNames}` : ''
  664. }。通过实地参观学习,加深了对革命历史的理解和认识。`;
  665. },
  666. // 辅助方法:创建每日行程安排
  667. createDailySchedule(attractions, transport) {
  668. const timeSlots = ["09:00", "11:00", "14:00", "16:00"];
  669. return attractions.map((att, index) => ({
  670. time: timeSlots[index] || "10:00",
  671. duration: att.duration || 120,
  672. attraction: {
  673. id: att.id,
  674. name: att.name,
  675. image: att.image,
  676. is_red_tourism: att.is_red_tourism
  677. },
  678. transport: index < attractions.length - 1 ? transport : null,
  679. transport_duration: "约30分钟",
  680. transport_cost: this.getTransportCost(transport),
  681. notes: [
  682. `建议参观时间:${att.visit_time || '1-2小时'}`,
  683. att.ticket_info || '门票信息:免费'
  684. ]
  685. }));
  686. },
  687. // 辅助方法:获取交通费用估算
  688. getTransportCost(transportType) {
  689. const costs = {
  690. 'public': '约5-10元/人',
  691. 'drive': '油费约20-50元',
  692. 'walk': '免费',
  693. 'bike': '免费'
  694. };
  695. return costs[transportType] || '费用待定';
  696. },
  697. // 辅助方法:生成参观时间
  698. generateVisitTime(index) {
  699. const times = ["09:00-11:00", "11:00-13:00", "14:00-16:00", "16:00-18:00"];
  700. return times[index] || "10:00-12:00";
  701. },
  702. // 辅助方法:深度合并对象
  703. deepMerge(target, source) {
  704. if (typeof source !== 'object' || source === null) {
  705. return target;
  706. }
  707. for (const key in source) {
  708. if (source.hasOwnProperty(key)) {
  709. if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
  710. if (!target[key]) {
  711. target[key] = {};
  712. }
  713. this.deepMerge(target[key], source[key]);
  714. } else {
  715. target[key] = source[key];
  716. }
  717. }
  718. }
  719. return target;
  720. },
  721. // 处理地图数据
  722. processMapData(days, selectedDay = -1) {
  723. if (!days || !days.length) {
  724. console.warn('没有有效的行程数据');
  725. return;
  726. }
  727. // 定义不同天数的颜色
  728. const dayColors = [
  729. '#e54d42', // 红色 - 第一天
  730. '#f37b1d', // 橙色 - 第二天
  731. '#1cbbb4', // 青色 - 第三天
  732. '#0081ff', // 蓝色 - 第四天
  733. '#6739b6', // 紫色 - 第五天
  734. '#9c26b0', // 紫红色 - 第六天
  735. '#e03997' // 粉红色 - 第七天
  736. ];
  737. // 准备地图数据
  738. const markers = [];
  739. const polylines = [];
  740. const includePoints = [];
  741. // 处理每一天的景点
  742. days.forEach((day, dayIndex) => {
  743. // 如果指定了天数且不是当前天数,则跳过
  744. if (selectedDay !== -1 && dayIndex !== selectedDay) return;
  745. const dayColor = dayColors[dayIndex % dayColors.length];
  746. const dayPoints = [];
  747. // 处理当天景点
  748. day.attractions.forEach((attraction, attrIndex) => {
  749. const markerId = dayIndex * 100 + attrIndex;
  750. // 创建标记点
  751. markers.push({
  752. id: markerId,
  753. latitude: attraction.location.latitude,
  754. longitude: attraction.location.longitude,
  755. iconPath: '/images/marker-default.png',
  756. width: 24,
  757. height: 24,
  758. callout: {
  759. content: `第${day.day}天 ${attrIndex + 1}. ${attraction.name}`,
  760. color: '#333',
  761. fontSize: 14,
  762. borderRadius: 4,
  763. display: 'BYCLICK'
  764. },
  765. customData: {
  766. dayIndex,
  767. attrIndex,
  768. dayNumber: day.day
  769. },
  770. label: {
  771. content: `第${day.day}天 ${attrIndex + 1}`,
  772. color: dayColor,
  773. fontSize: 12,
  774. bgColor: '#ffffff',
  775. padding: 4,
  776. borderRadius: 4,
  777. borderWidth: 1,
  778. borderColor: dayColor
  779. }
  780. });
  781. dayPoints.push({
  782. latitude: attraction.location.latitude,
  783. longitude: attraction.location.longitude
  784. });
  785. });
  786. // 添加当天路线
  787. if (dayPoints.length > 1) {
  788. polylines.push({
  789. points: dayPoints,
  790. color: dayColor,
  791. width: 4,
  792. arrowLine: true,
  793. dottedLine: false
  794. });
  795. }
  796. // 添加到总集合
  797. includePoints.push(...dayPoints);
  798. });
  799. // 计算地图中心点和缩放级别
  800. const { center, scale } = this.calculateMapViewport(includePoints);
  801. // 更新地图数据
  802. this.setData({
  803. 'mapData.markers': markers,
  804. 'mapData.polyline': polylines,
  805. 'mapData.latitude': center.latitude,
  806. 'mapData.longitude': center.longitude,
  807. 'mapData.scale': scale,
  808. 'mapData.showMap': true,
  809. 'mapData.includePoints': includePoints,
  810. 'mapData.dayColors': dayColors
  811. });
  812. // 播放路线动画
  813. this.playRouteAnimation();
  814. },
  815. // 计算地图视野
  816. calculateMapViewport(points) {
  817. if (!points || points.length === 0) {
  818. return {
  819. center: { latitude: 39.9042, longitude: 116.4074 },
  820. scale: 12
  821. };
  822. }
  823. // 计算所有点的边界
  824. const lats = points.map(p => p.latitude);
  825. const lngs = points.map(p => p.longitude);
  826. const minLat = Math.min(...lats);
  827. const maxLat = Math.max(...lats);
  828. const minLng = Math.min(...lngs);
  829. const maxLng = Math.max(...lngs);
  830. // 计算中心点
  831. const center = {
  832. latitude: (minLat + maxLat) / 2,
  833. longitude: (minLng + maxLng) / 2
  834. };
  835. // 计算缩放级别
  836. const latRange = maxLat - minLat;
  837. const lngRange = maxLng - minLng;
  838. const maxRange = Math.max(latRange, lngRange);
  839. // 根据范围大小调整缩放级别
  840. let scale = 15 - Math.floor(maxRange * 15);
  841. scale = Math.max(10, Math.min(scale, 17));
  842. return { center, scale };
  843. },
  844. // 初始化地图控件
  845. initMapControls() {
  846. this.mapCtx = wx.createMapContext('routeMap', this);
  847. // 确保这些方法已经定义
  848. if (typeof this.handleRegionChange === 'function' &&
  849. typeof this.handleMarkerTap === 'function') {
  850. this.setData({
  851. handleRegionChange: this.handleRegionChange.bind(this),
  852. handleMarkerTap: this.handleMarkerTap.bind(this)
  853. });
  854. } else {
  855. console.error('地图事件处理方法未定义');
  856. // 提供默认的空函数
  857. this.setData({
  858. handleRegionChange: () => {},
  859. handleMarkerTap: () => {}
  860. });
  861. }
  862. },
  863. handleRegionChange(e) {
  864. console.log('地图区域变化:', e);
  865. // 可以在这里添加地图区域变化时的处理逻辑
  866. },
  867. // 景点详情显示处理
  868. handleMarkerTap(e) {
  869. const markerId = e.detail.markerId;
  870. const marker = this.data.mapData.markers.find(m => m.id === markerId);
  871. if (!marker || !this.data.planData || !this.data.planData.days) {
  872. console.error('无法找到标记或行程数据');
  873. return;
  874. }
  875. const { dayIndex, attrIndex } = marker.customData;
  876. const attraction = this.data.planData.days[dayIndex].attractions[attrIndex];
  877. if (!attraction) {
  878. console.error('无法找到景点数据');
  879. return;
  880. }
  881. // 显示加载状态
  882. this.setData({
  883. showAttractionDetail: true,
  884. currentAttraction: {
  885. ...attraction,
  886. isLoadingImage: true // 添加加载状态
  887. },
  888. attractionDetail: {
  889. name: attraction.name,
  890. image: '/images/placeholder-image.jpg', // 先显示占位图
  891. description: attraction.description,
  892. history: attraction.history_significance,
  893. address: attraction.address,
  894. open_hours: attraction.open_hours,
  895. ticket_info: attraction.ticket_info,
  896. visiting_etiquette: attraction.visiting_etiquette || [
  897. "请保持庄严肃穆",
  898. "勿大声喧哗",
  899. "纪念馆内请勿拍照"
  900. ],
  901. location: attraction.location
  902. }
  903. });
  904. // 自动获取景点图片
  905. this.fetchAttractionImage(attraction)
  906. .then(imageUrl => {
  907. // 更新景点图片
  908. this.setData({
  909. 'currentAttraction.image': imageUrl,
  910. 'currentAttraction.isLoadingImage': false,
  911. 'attractionDetail.image': imageUrl
  912. });
  913. // 同时更新planData中的对应景点图片
  914. this.updateAttractionImage(attraction.id, attraction.name, imageUrl);
  915. })
  916. .catch(err => {
  917. console.error('获取景点图片失败:', err);
  918. // 使用默认图片
  919. this.setData({
  920. 'currentAttraction.image': '/images/default-attraction.jpg',
  921. 'currentAttraction.isLoadingImage': false,
  922. 'attractionDetail.image': '/images/default-attraction.jpg'
  923. });
  924. });
  925. },
  926. // 切换地图显示的天数
  927. switchMapDay(e) {
  928. const dayIndex = e.currentTarget.dataset.day;
  929. this.setData({
  930. currentMapDay: dayIndex
  931. });
  932. // 重新处理地图数据,只显示选中天数的景点
  933. this.processMapData(this.data.planData.days, dayIndex);
  934. },
  935. // 地图缩放控制
  936. handleZoomIn() {
  937. this.setData({
  938. 'mapData.scale': Math.min(this.data.mapData.scale + 1, 18)
  939. });
  940. },
  941. handleZoomOut() {
  942. this.setData({
  943. 'mapData.scale': Math.max(this.data.mapData.scale - 1, 10)
  944. });
  945. },
  946. // 重新定位到所有景点
  947. resetToAllAttractions() {
  948. this.setData({
  949. currentMapDay: -1
  950. });
  951. // 重新处理地图数据,显示所有景点
  952. this.processMapData(this.data.planData.days);
  953. },
  954. // 播放路线动画
  955. playRouteAnimation() {
  956. if (this.data.mapData.polyline.length === 0) return;
  957. // 初始状态:所有路线半透明
  958. const transparentPolylines = this.data.mapData.polyline.map(line => ({
  959. ...line,
  960. color: line.color + '80' // 添加透明度
  961. }));
  962. this.setData({
  963. 'mapData.polyline': transparentPolylines
  964. });
  965. // 逐个显示路线
  966. let i = 0;
  967. const timer = setInterval(() => {
  968. if (i >= this.data.mapData.polyline.length) {
  969. clearInterval(timer);
  970. return;
  971. }
  972. const updatedPolylines = [...this.data.mapData.polyline];
  973. updatedPolylines[i] = {
  974. ...updatedPolylines[i],
  975. color: this.data.mapData.polyline[i].color.replace('80', '') // 移除透明度
  976. };
  977. this.setData({
  978. 'mapData.polyline': updatedPolylines
  979. });
  980. i++;
  981. }, 500);
  982. },
  983. // 导航到景点
  984. navigateToAttraction() {
  985. if (!this.data.currentAttraction || !this.data.currentAttraction.location) {
  986. wx.showToast({ title: '无法获取景点位置', icon: 'none' });
  987. return;
  988. }
  989. const { latitude, longitude } = this.data.currentAttraction.location;
  990. const name = this.data.currentAttraction.name || '红色教育基地';
  991. wx.openLocation({
  992. latitude,
  993. longitude,
  994. name,
  995. scale: 18
  996. });
  997. },
  998. // 生成每日总结
  999. generateDaySummary(day, attractions) {
  1000. const redAttractions = attractions.filter(att => att.is_red_tourism);
  1001. const attractionNames = redAttractions.map(att => att.name).join('、');
  1002. return `今日参观了${redAttractions.length}个红色教育基地${
  1003. attractionNames ? `,包括${attractionNames}` : ''
  1004. },${day.summary || '通过实地参观加深了对革命历史的理解。'}`;
  1005. },
  1006. // 创建每日行程安排
  1007. createDailySchedule(attractions, transport) {
  1008. const timeSlots = ["09:00", "11:00", "14:00", "16:00"];
  1009. return attractions.map((att, index) => ({
  1010. time: timeSlots[index] || "10:00",
  1011. duration: 120,
  1012. attraction: att,
  1013. transport: index < attractions.length - 1 ? transport : null,
  1014. transport_duration: "约30分钟",
  1015. transport_cost: this.getTransportCost(transport)
  1016. }));
  1017. },
  1018. // 获取交通费用估算
  1019. getTransportCost(transportType) {
  1020. const costs = {
  1021. 'public': '约5-10元/人',
  1022. 'drive': '油费约20-50元',
  1023. 'walk': '免费',
  1024. 'bike': '免费'
  1025. };
  1026. return costs[transportType] || '费用待定';
  1027. },
  1028. // 计算统计数据
  1029. calculateStatistics(days) {
  1030. let redCount = 0;
  1031. let totalHours = 0;
  1032. days.forEach(day => {
  1033. redCount += day.attractions.filter(a => a.is_red_tourism).length;
  1034. totalHours += 8; // 每天按8小时计算
  1035. });
  1036. return {
  1037. red_attractions_count: redCount,
  1038. learning_points: redCount * 2,
  1039. total_hours: totalHours,
  1040. total_days: days.length
  1041. };
  1042. },
  1043. // 获取红色景点图片
  1044. getRedTourismImage(isRed) {
  1045. const images = {
  1046. true: '/images/red-attraction.jpg',
  1047. false: '/images/normal-attraction.jpg'
  1048. };
  1049. return images[isRed] || '/images/default-attraction.jpg';
  1050. },
  1051. // 生成参观时间
  1052. generateVisitTime(index) {
  1053. const times = ["09:00", "11:00", "14:00", "16:00"];
  1054. return times[index] || "10:00";
  1055. },
  1056. // 深度合并对象
  1057. deepMerge(target, source) {
  1058. if (typeof source !== 'object' || source === null) {
  1059. return target;
  1060. }
  1061. for (const key in source) {
  1062. if (source.hasOwnProperty(key)) {
  1063. if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
  1064. if (!target[key]) {
  1065. target[key] = {};
  1066. }
  1067. this.deepMerge(target[key], source[key]);
  1068. } else {
  1069. target[key] = source[key];
  1070. }
  1071. }
  1072. }
  1073. return target;
  1074. },
  1075. // 显示景点详情
  1076. async showAttractionDetail(e) {
  1077. const attraction = e.currentTarget.dataset.attraction;
  1078. // 设置当前景点信息,并标记图片正在加载
  1079. this.setData({
  1080. showAttractionDetail: true,
  1081. currentAttraction: {
  1082. ...attraction,
  1083. isLoadingImage: true,
  1084. image: '/images/placeholder-image.jpg' // 临时占位图
  1085. },
  1086. isFavorite: false // 先设置为false,等异步检查完成后再更新
  1087. });
  1088. // 自动获取景点图片
  1089. this.fetchAttractionImage(attraction).then(imageUrl => {
  1090. this.setData({
  1091. 'currentAttraction.image': imageUrl,
  1092. 'currentAttraction.isLoadingImage': false
  1093. });
  1094. }).catch(err => {
  1095. console.error('获取景点图片失败:', err);
  1096. this.setData({
  1097. 'currentAttraction.isLoadingImage': false,
  1098. 'currentAttraction.image': '/images/image-load-failed.jpg' // 加载失败的图片
  1099. });
  1100. });
  1101. },
  1102. // 计算两点间距离
  1103. calculateDistance(loc1, loc2) {
  1104. if (!loc1 || !loc2) return 0;
  1105. const R = 6371; // 地球半径(km)
  1106. const dLat = (loc2.latitude - loc1.latitude) * Math.PI / 180;
  1107. const dLon = (loc2.longitude - loc1.longitude) * Math.PI / 180;
  1108. const a =
  1109. Math.sin(dLat/2) * Math.sin(dLat/2) +
  1110. Math.cos(loc1.latitude * Math.PI / 180) *
  1111. Math.cos(loc2.latitude * Math.PI / 180) *
  1112. Math.sin(dLon/2) * Math.sin(dLon/2);
  1113. const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  1114. return R * c;
  1115. },
  1116. // 导航到景点
  1117. navigateToAttraction() {
  1118. const { currentAttraction } = this.data;
  1119. if (!currentAttraction || !currentAttraction.location) {
  1120. wx.showToast({ title: '无法获取景点位置', icon: 'none' });
  1121. return;
  1122. }
  1123. wx.showLoading({ title: '准备导航中', mask: true });
  1124. // 检查位置权限
  1125. wx.authorize({
  1126. scope: 'scope.userLocation',
  1127. success: () => {
  1128. wx.openLocation({
  1129. latitude: currentAttraction.location.latitude,
  1130. longitude: currentAttraction.location.longitude,
  1131. name: currentAttraction.name,
  1132. scale: 18,
  1133. complete: () => {
  1134. wx.hideLoading();
  1135. }
  1136. });
  1137. },
  1138. fail: () => {
  1139. wx.hideLoading();
  1140. wx.showModal({
  1141. title: '权限不足',
  1142. content: '需要获取您的位置信息才能导航',
  1143. confirmText: '去设置',
  1144. success: (res) => {
  1145. if (res.confirm) {
  1146. wx.openSetting();
  1147. }
  1148. }
  1149. });
  1150. }
  1151. });
  1152. },
  1153. // 关闭弹窗
  1154. closeAttractionDetail() {
  1155. this.setData({ showAttractionDetail: false });
  1156. },
  1157. // 查找景点对象
  1158. findAttractionById(id) {
  1159. if (!this.data.planData || !this.data.planData.days) return null;
  1160. for (const day of this.data.planData.days) {
  1161. for (const attr of day.attractions) {
  1162. if (attr.id === id) return attr;
  1163. }
  1164. }
  1165. return null;
  1166. },
  1167. // 保存行程
  1168. savePlan() {
  1169. if (!this.data.planData) {
  1170. wx.showToast({ title: '没有可保存的行程', icon: 'none' });
  1171. return;
  1172. }
  1173. wx.showLoading({ title: '保存中...', mask: true });
  1174. this.setData({ isLoading: true });
  1175. const planData = this.data.planData;
  1176. // 获取本地存储的token
  1177. const token = wx.getStorageSync('token');
  1178. if (!token) {
  1179. wx.showToast({ title: '请先登录', icon: 'none' });
  1180. wx.hideLoading();
  1181. this.setData({ isLoading: false });
  1182. return;
  1183. }
  1184. wx.request({
  1185. url: app.globalData.apiBaseUrl + '/api/red-tourism/save-plan/',
  1186. method: 'POST',
  1187. data: {
  1188. plan_data: planData,
  1189. city_ids: [Number(this.data.selectedCity)] // 添加城市ID
  1190. },
  1191. header: {
  1192. 'Content-Type': 'application/json',
  1193. 'Authorization': 'Token ' + wx.getStorageSync('token') // 添加token到请求头
  1194. },
  1195. success: (res) => {
  1196. if (res.statusCode === 201) {
  1197. wx.showToast({
  1198. title: '保存成功',
  1199. icon: 'success',
  1200. duration: 2000
  1201. });
  1202. this.setData({ currentStep: 3 });
  1203. } else {
  1204. this.handleApiError(res);
  1205. }
  1206. },
  1207. fail: (err) => {
  1208. this.handleNetworkError(err);
  1209. },
  1210. complete: () => {
  1211. wx.hideLoading();
  1212. this.setData({ isLoading: false });
  1213. }
  1214. });
  1215. },
  1216. // 处理API错误
  1217. handleApiError(res) {
  1218. let errorMsg = '保存失败,请稍后再试';
  1219. if (res.data?.message) {
  1220. errorMsg = res.data.message;
  1221. } else if (res.statusCode === 401) {
  1222. errorMsg = '请先登录';
  1223. // 可以在这里添加跳转到登录页面的逻辑
  1224. wx.navigateTo({
  1225. url: '/pages/login/login'
  1226. });
  1227. } else if (res.statusCode === 400) {
  1228. if (res.data?.errors) {
  1229. errorMsg = Object.values(res.data.errors).join('\n');
  1230. } else {
  1231. errorMsg = '数据格式不正确';
  1232. }
  1233. }
  1234. wx.showToast({
  1235. title: errorMsg,
  1236. icon: 'none',
  1237. duration: 3000
  1238. });
  1239. },
  1240. // 处理网络错误
  1241. handleNetworkError(err) {
  1242. let errorMsg = '网络连接失败';
  1243. if (err.errMsg.includes('timeout')) {
  1244. errorMsg = '请求超时,请检查网络连接';
  1245. }
  1246. wx.showToast({
  1247. title: errorMsg,
  1248. icon: 'none',
  1249. duration: 2000
  1250. });
  1251. },
  1252. // 重新生成行程
  1253. regeneratePlan() {
  1254. if (this.data.isRegenerating) return;
  1255. this.setData({ isRegenerating: true });
  1256. wx.showLoading({ title: '正在重新规划...', mask: true });
  1257. // 修正请求数据结构
  1258. const requestData = {
  1259. plan_id: this.data.planData?.id || null,
  1260. city_ids: [Number(this.data.selectedCity)],
  1261. days: Number(this.data.selectedDays),
  1262. interests: [...this.data.selectedInterests, 'red-tourism'],
  1263. transport: this.data.selectedTransport,
  1264. custom_requirements: this.data.customRequirements,
  1265. previous_plan: this.data.planData || { days: [] } // 确保有这个字段
  1266. };
  1267. console.log('重新规划请求数据:', JSON.stringify(requestData));
  1268. wx.request({
  1269. url: app.globalData.apiBaseUrl + '/api/red-tourism/regenerate/',
  1270. method: 'POST',
  1271. data: requestData,
  1272. header: {
  1273. 'Content-Type': 'application/json',
  1274. 'Authorization': 'Token ' + wx.getStorageSync('token')
  1275. },
  1276. success: (res) => {
  1277. console.log('重新规划响应:', res);
  1278. if (res.statusCode === 200) {
  1279. if (res.data && res.data.status === 'success' && res.data.data) {
  1280. this.processAndShowPlan(res.data.data, true);
  1281. wx.showToast({
  1282. title: '重新生成成功',
  1283. icon: 'success',
  1284. duration: 2000
  1285. });
  1286. } else {
  1287. this.handleError(res.data?.message || '返回数据格式不正确');
  1288. }
  1289. } else {
  1290. // 显示详细的错误信息
  1291. let errorMsg = `请求失败: ${res.statusCode}`;
  1292. if (res.data && res.data.errors) {
  1293. errorMsg += ` - ${JSON.stringify(res.data.errors)}`;
  1294. }
  1295. this.handleError(errorMsg);
  1296. }
  1297. },
  1298. fail: (err) => {
  1299. console.error('重新规划请求失败:', err);
  1300. this.handleError('网络请求失败,请检查连接');
  1301. },
  1302. complete: () => {
  1303. this.setData({ isRegenerating: false });
  1304. wx.hideLoading();
  1305. }
  1306. });
  1307. },
  1308. // 查看完整行程
  1309. viewPlan() {
  1310. wx.navigateTo({
  1311. url: '/pages/plan-detail/plan-detail?plan=' +
  1312. encodeURIComponent(JSON.stringify(this.data.planData)) +
  1313. '&is_red_tourism=true'
  1314. });
  1315. },
  1316. // 分享行程
  1317. sharePlan() {
  1318. if (!this.data.planData) return;
  1319. wx.showShareMenu({ withShareTicket: true });
  1320. },
  1321. onShareAppMessage() {
  1322. if (!this.data.planData) return {};
  1323. return {
  1324. title: this.data.planData.title || '我的红色之旅行程',
  1325. path: '/pages/plan-detail/plan-detail?plan=' +
  1326. encodeURIComponent(JSON.stringify(this.data.planData)) +
  1327. '&is_red_tourism=true',
  1328. imageUrl: this.data.planData.days[0]?.attractions[0]?.image ||
  1329. '/images/red-tourism-share.jpg'
  1330. };
  1331. },
  1332. // 错误处理
  1333. handleError(message) {
  1334. console.error('Error:', message);
  1335. wx.showToast({
  1336. title: message,
  1337. icon: 'none',
  1338. duration: 2000
  1339. });
  1340. this.setData({
  1341. currentStep: 1,
  1342. isLoading: false,
  1343. planGenerated: false
  1344. });
  1345. },
  1346. handleApiError(res) {
  1347. let errorMsg = '请求失败,请稍后再试';
  1348. if (res.data?.message) {
  1349. errorMsg = res.data.message;
  1350. } else if (res.statusCode === 400) {
  1351. errorMsg = '输入数据有误,请检查后重试';
  1352. } else if (res.statusCode === 500) {
  1353. errorMsg = '服务器错误,请稍后再试';
  1354. }
  1355. wx.showToast({ title: errorMsg, icon: 'none' });
  1356. this.setData({ isLoading: false });
  1357. },
  1358. handleNetworkError(err) {
  1359. let errorMsg = '网络连接失败';
  1360. if (err.errMsg.includes('timeout')) {
  1361. errorMsg = '请求超时,请检查网络连接';
  1362. }
  1363. wx.showToast({ title: errorMsg, icon: 'none' });
  1364. this.setData({ isLoading: false });
  1365. },
  1366. // 返回上一步
  1367. navigateBack() {
  1368. if (this.data.currentStep > 1) {
  1369. this.setData({ currentStep: this.data.currentStep - 1 });
  1370. } else {
  1371. wx.navigateBack();
  1372. }
  1373. },
  1374. viewPlan() {
  1375. wx.navigateTo({ url: '/pages/wodexingcheng/wodexingcheng' });
  1376. },
  1377. });