ai-plan.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. // pages/ai-plan/ai-plan.js
  2. const app = getApp();
  3. Page({
  4. data: {
  5. currentStep: 1,
  6. planGenerated: false,
  7. selectedDays: 3,
  8. selectedCities: [],
  9. selectedInterests: ['patriotism'], // 默认选中爱国主义
  10. isRegenerating: false, // 是否正在重新生成
  11. planData: null,
  12. selectedTransport: 'public',
  13. customRequirements: '',
  14. dayOptions: [
  15. { label: '1天', value: 1 },
  16. { label: '2天', value: 2 },
  17. { label: '3天', value: 3 },
  18. { label: '4天', value: 4 },
  19. { label: '5天', value: 5 },
  20. { label: '6天', value: 6 },
  21. { label: '7天', value: 7 }
  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. planData: null,
  37. totalAttractions: 0,
  38. currentDate: '',
  39. isLoading: false,
  40. apiStatus: {
  41. citiesLoaded: false,
  42. attractionsLoaded: false
  43. },
  44. // 新增景点详情相关数据
  45. showAttractionDetail: false,
  46. currentAttraction: null,
  47. attractionDetail: null
  48. },
  49. // 添加 deepMerge 方法
  50. deepMerge(target, source) {
  51. // 如果 source 不是对象,直接返回 target
  52. if (typeof source !== 'object' || source === null) {
  53. return target;
  54. }
  55. // 遍历 source 的所有属性
  56. for (const key in source) {
  57. if (source.hasOwnProperty(key)) {
  58. // 如果属性值是对象且不是数组,递归合并
  59. if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
  60. if (!target[key]) {
  61. target[key] = {};
  62. }
  63. this.deepMerge(target[key], source[key]);
  64. } else {
  65. // 否则直接赋值
  66. target[key] = source[key];
  67. }
  68. }
  69. }
  70. return target;
  71. },
  72. onLoad() {
  73. this.initPage();
  74. this.loadInitialData();
  75. },
  76. initPage() {
  77. const date = new Date();
  78. this.setData({
  79. currentDate: `${date.getMonth() + 1}月${date.getDate()}日`
  80. });
  81. this.initDefaultImages();
  82. },
  83. initDefaultImages() {
  84. this.setData({
  85. transportOptions: this.data.transportOptions.map(item => ({
  86. ...item,
  87. icon: this.checkImageExists(item.icon) ? item.icon : '/images/default-transport.png'
  88. }))
  89. });
  90. },
  91. loadInitialData() {
  92. this.getCities();
  93. },
  94. getCities() {
  95. this.setData({
  96. isLoading: true,
  97. cityOptions: []
  98. });
  99. wx.request({
  100. url: app.globalData.apiBaseUrl + '/api/cities/',
  101. method: 'GET',
  102. success: (res) => {
  103. if (res.statusCode === 200) {
  104. const rawData = res.data.data || res.data;
  105. const cityOptions = rawData.map(city => ({
  106. label: city.name || '未知城市',
  107. value: city.id ? city.id.toString() : '0',
  108. disabled: city.is_available === false
  109. }));
  110. this.setData({
  111. cityOptions,
  112. 'apiStatus.citiesLoaded': true
  113. });
  114. } else {
  115. this.handleApiError(res);
  116. }
  117. },
  118. fail: this.handleNetworkError,
  119. complete: () => {
  120. this.setData({ isLoading: false });
  121. }
  122. });
  123. },
  124. checkImageExists(path) {
  125. try {
  126. const fs = wx.getFileSystemManager();
  127. return fs.accessSync(path) === undefined;
  128. } catch (e) {
  129. return false;
  130. }
  131. },
  132. handleImageError(e) {
  133. const { type, index } = e.currentTarget.dataset;
  134. const defaultImages = {
  135. transport: '/images/default-transport.png',
  136. attraction: '/images/default-attraction.jpg',
  137. loading: '/images/loading.gif',
  138. success: '/images/success.png',
  139. arrow: '/images/icons/arrow.png',
  140. checked: '/images/icons/checked.png'
  141. };
  142. if (type === 'transport') {
  143. const key = `transportOptions[${index}].icon`;
  144. this.setData({ [key]: defaultImages.transport });
  145. } else if (type === 'attraction') {
  146. const path = `planData.days[${e.currentTarget.dataset.day}].attractions[${index}].image`;
  147. this.setData({ [path]: defaultImages.attraction });
  148. } else if (defaultImages[type]) {
  149. this.setData({ [e.currentTarget.dataset.src || 'imageUrl']: defaultImages[type] });
  150. }
  151. },
  152. selectDays(e) {
  153. this.setData({ selectedDays: e.currentTarget.dataset.value });
  154. },
  155. toggleCity(e) {
  156. const value = e.currentTarget.dataset.value;
  157. const index = this.data.selectedCities.indexOf(value);
  158. let newCities = [...this.data.selectedCities];
  159. if (index === -1) {
  160. newCities.push(value);
  161. } else {
  162. newCities.splice(index, 1);
  163. }
  164. this.setData({ selectedCities: newCities });
  165. },
  166. toggleInterest(e) {
  167. const value = e.currentTarget.dataset.value;
  168. const index = this.data.selectedInterests.indexOf(value);
  169. let newInterests = [...this.data.selectedInterests];
  170. if (index === -1) {
  171. newInterests.push(value);
  172. } else {
  173. newInterests.splice(index, 1);
  174. }
  175. this.setData({ selectedInterests: newInterests });
  176. },
  177. selectTransport(e) {
  178. this.setData({ selectedTransport: e.currentTarget.dataset.value });
  179. },
  180. updateCustomRequirements(e) {
  181. this.setData({ customRequirements: e.detail.value });
  182. },
  183. validateInputs() {
  184. if (this.data.selectedCities.length === 0) {
  185. wx.showToast({ title: '请至少选择一个城市', icon: 'none' });
  186. return false;
  187. }
  188. if (this.data.selectedDays < 1) {
  189. wx.showToast({ title: '请选择有效天数', icon: 'none' });
  190. return false;
  191. }
  192. return true;
  193. },
  194. goToNextStep() {
  195. if (this.data.currentStep === 1) {
  196. if (!this.validateInputs()) return;
  197. this.setData({ currentStep: 2 });
  198. this.generatePlanWithAI();
  199. } else if (this.data.currentStep === 2) {
  200. this.setData({ currentStep: 3 });
  201. }
  202. },
  203. // 生成行程
  204. generatePlanWithAI() {
  205. if (this.data.isLoading) return;
  206. // 验证输入
  207. if (this.data.selectedCities.length === 0) {
  208. wx.showToast({ title: '请至少选择一个城市', icon: 'none' });
  209. return;
  210. }
  211. // 显示加载状态
  212. this.setData({
  213. isLoading: true,
  214. planGenerated: false,
  215. planData: null
  216. });
  217. wx.showLoading({
  218. title: 'AI正在规划红色路线...',
  219. mask: true
  220. });
  221. // 准备请求数据
  222. const requestData = {
  223. city_ids: this.data.selectedCities,
  224. days: this.data.selectedDays,
  225. interests: [...this.data.selectedInterests, 'red-tourism'],
  226. transport: this.data.selectedTransport,
  227. custom_requirements: this.data.customRequirements
  228. };
  229. // 调用API
  230. wx.request({
  231. url: app.globalData.apiBaseUrl + '/api/red-tourism/plan/',
  232. method: 'POST',
  233. data: requestData,
  234. header: {
  235. 'Content-Type': 'application/json',
  236. 'Authorization': wx.getStorageSync('token') ? `Bearer ${wx.getStorageSync('token')}` : ''
  237. },
  238. success: (res) => {
  239. console.log('API响应:', res);
  240. if (res.statusCode === 200) {
  241. console.log('API返回数据:', JSON.stringify(res.data, null, 2)); // 添加这行
  242. if (res.data && res.data.status === 'success' && res.data.data) {
  243. // 处理并显示行程数据
  244. this.processAndShowPlan(res.data.data);
  245. } else {
  246. this.handleError('返回数据格式不正确');
  247. }
  248. } else {
  249. this.handleError(res.data?.message || `请求失败: ${res.statusCode}`);
  250. }
  251. },
  252. fail: (err) => {
  253. this.handleError('网络请求失败,请检查连接');
  254. },
  255. complete: () => {
  256. wx.hideLoading();
  257. this.setData({ isLoading: false });
  258. }
  259. });
  260. },
  261. processAndShowPlan(planData, isRegenerate = false) {
  262. // 默认值配置
  263. const DEFAULT_PLAN = {
  264. title: '红色文化之旅',
  265. description: '通过参观革命历史遗址学习党史',
  266. statistics: {
  267. red_attractions_count: 0,
  268. total_attractions: 0,
  269. educational_points_count: 0
  270. },
  271. red_tourism_tips: [
  272. "请着装整洁,保持肃穆",
  273. "建议提前学习相关历史知识"
  274. ],
  275. suitable_for: "适合党员干部、学生团体等",
  276. days: []
  277. };
  278. // 深度合并默认值
  279. const processedData = this.deepMerge(DEFAULT_PLAN, planData);
  280. // 处理每日行程
  281. processedData.days = (processedData.days || []).map((day, index) => {
  282. // 确保景点数据标准化
  283. const attractions = (day.attractions || []).map(att => ({
  284. id: att.id || 0,
  285. name: att.name || '红色教育基地',
  286. description: att.description || '',
  287. image: att.image || '/images/default-red.jpg',
  288. is_red_tourism: att.is_red_tourism !== false,
  289. educational_value: att.educational_value || '高',
  290. visit_time: att.visit_time || '09:00-17:00',
  291. ticket_price: att.ticket_price || '免费',
  292. address: att.address || '',
  293. open_hours: att.open_hours || '09:00-17:00',
  294. recommendation: att.recommendation || '建议参观时长1-2小时',
  295. history: att.history || '这里是重要的革命历史遗址',
  296. tags: att.tags || ['红色旅游']
  297. }));
  298. // 处理教育要点
  299. const educational_points = (day.educational_points || []).map(point => {
  300. return typeof point === 'string'
  301. ? { name: '教育要点', description: point, is_red_tourism: true }
  302. : {
  303. name: point.name || '教育要点',
  304. description: point.description || '',
  305. is_red_tourism: point.is_red_tourism !== false
  306. };
  307. });
  308. // 计算当日统计
  309. const dayStats = {
  310. red_attractions_count: attractions.filter(a => a.is_red_tourism).length,
  311. total_attractions: attractions.length,
  312. educational_points_count: educational_points.length
  313. };
  314. return {
  315. day: day.day || index + 1,
  316. theme: day.theme || '红色教育',
  317. transport: day.transport || '公共交通',
  318. meals: day.meals || {
  319. breakfast: '酒店早餐',
  320. lunch: '景区附近餐厅',
  321. dinner: '当地特色餐馆'
  322. },
  323. attractions,
  324. educational_points,
  325. summary: day.summary || '',
  326. accommodation: day.accommodation || '当地酒店',
  327. statistics: dayStats
  328. };
  329. });
  330. // 计算总统计
  331. processedData.statistics = processedData.days.reduce((stats, day) => {
  332. stats.red_attractions_count += day.statistics.red_attractions_count;
  333. stats.total_attractions += day.statistics.total_attractions;
  334. stats.educational_points_count += day.statistics.educational_points_count;
  335. return stats;
  336. }, {
  337. red_attractions_count: 0,
  338. total_attractions: 0,
  339. educational_points_count: 0
  340. });
  341. // 更新UI
  342. this.setData({
  343. planData: processedData,
  344. planGenerated: true,
  345. isLoading: false,
  346. lastGeneratedAt: processedData.generated_at || new Date().toISOString(),
  347. isRegenerated: isRegenerate
  348. });
  349. },
  350. // 新增方法处理景点数据
  351. processAttractions(attractions) {
  352. if (!Array.isArray(attractions)) return [];
  353. return attractions.map(att => ({
  354. id: att.id || 0,
  355. name: att.name || '红色教育基地',
  356. description: att.description || '',
  357. image: att.image || '/images/default-red.jpg',
  358. is_red_tourism: att.is_red_tourism !== false,
  359. educational_value: att.is_red_tourism ? (att.educational_value || '高') : '普通',
  360. visit_time: att.visit_time || '09:00-17:00',
  361. ticket_price: att.ticket_price || '免费',
  362. tags: Array.isArray(att.tags) ? att.tags : ['红色旅游']
  363. }));
  364. },
  365. // 错误处理
  366. handleError(message) {
  367. console.error('Error:', message);
  368. wx.showToast({
  369. title: message,
  370. icon: 'none',
  371. duration: 2000
  372. });
  373. this.setData({
  374. currentStep: 1,
  375. isLoading: false,
  376. planGenerated: false
  377. });
  378. },
  379. handleApiError(res) {
  380. let errorMsg = '生成失败';
  381. if (res.data && res.data.message) {
  382. errorMsg = res.data.message;
  383. } else if (res.statusCode === 500) {
  384. errorMsg = '服务器错误';
  385. }
  386. wx.showToast({ title: errorMsg, icon: 'none' });
  387. this.setData({ currentStep: 1 }); // 退回第一步
  388. },
  389. handleNetworkError() {
  390. wx.showToast({ title: '网络连接失败', icon: 'none' });
  391. this.setData({ currentStep: 1 });
  392. },
  393. // 计算红色景点数量
  394. calculateRedAttractions(planData) {
  395. return planData.days.reduce((total, day) => {
  396. return total + (day.attractions || []).filter(a =>
  397. a.is_red_tourism ||
  398. (a.tags && a.tags.some(tag => ['红色旅游', '革命', '烈士'].includes(tag)))
  399. ).length;
  400. }, 0);
  401. },
  402. // 提取教育要点
  403. extractEducationalPoints(planData) {
  404. const points = new Set();
  405. (planData.days || []).forEach(day => {
  406. // 从每日主题中提取
  407. if (day.theme && day.theme.includes('革命')) {
  408. points.add('学习' + day.theme);
  409. }
  410. // 从景点中提取
  411. (day.attractions || []).forEach(attraction => {
  412. if (attraction.educational_value) {
  413. points.add(attraction.educational_value);
  414. }
  415. });
  416. });
  417. // 默认教育要点
  418. if (points.size === 0) {
  419. return [
  420. "了解革命历史背景",
  421. "学习先烈精神",
  422. "培养爱国情怀"
  423. ];
  424. }
  425. return Array.from(points);
  426. },
  427. // 格式化红色旅游数据
  428. formatRedTourismPlanData(apiData) {
  429. const data = apiData.data || apiData;
  430. const defaultRedImage = '/images/red-attraction-default.jpg';
  431. return {
  432. title: data.title || `${this.data.selectedDays}天红色之旅`,
  433. description: data.description || "重温革命历史,传承红色精神",
  434. days: (data.days || []).map(day => ({
  435. day: day.day || 1,
  436. theme: day.theme || `红色教育第${day.day}天`,
  437. description: day.description || "",
  438. transport: day.transport || this.data.selectedTransport,
  439. educational_points: day.educational_points || [
  440. "学习革命精神",
  441. "传承红色基因",
  442. "培养爱国情怀"
  443. ],
  444. attractions: (day.attractions || []).map(att => ({
  445. id: att.id || 0,
  446. name: att.name || "红色教育基地",
  447. shortDesc: att.short_desc || att.description || "革命历史教育基地",
  448. description: att.description || "",
  449. image: att.image || defaultRedImage,
  450. tags: att.tags || ['红色旅游'],
  451. is_red_tourism: att.tags ?
  452. att.tags.some(tag => ['红色旅游', '革命', '烈士'].includes(tag)) : true,
  453. educational_value: att.educational_value || "高",
  454. visit_time: att.visit_time || "09:00-17:00",
  455. ticket_price: att.ticket_price || "免费",
  456. open_hours: att.open_hours || "09:00-17:00",
  457. address: att.address || "",
  458. recommendation: att.recommendation || "建议参观时长1-2小时",
  459. history: att.history || "这里是重要的革命历史遗址"
  460. }))
  461. })),
  462. red_tourism_tips: data.red_tourism_tips || [
  463. "请着装整洁,保持肃穆",
  464. "可以准备鲜花进行瞻仰",
  465. "建议提前学习相关历史知识"
  466. ],
  467. suitable_for: data.suitable_for || "适合所有人群"
  468. };
  469. },
  470. // 错误处理
  471. handleApiError(res) {
  472. let errorMsg = '生成红色路线失败';
  473. if (res.data && res.data.message) {
  474. errorMsg = res.data.message;
  475. } else if (res.statusCode === 400) {
  476. errorMsg = '请求参数有误,请检查城市选择';
  477. } else if (res.statusCode === 500) {
  478. errorMsg = '服务器处理失败,请稍后再试';
  479. }
  480. wx.showToast({
  481. title: errorMsg,
  482. icon: 'none',
  483. duration: 2000
  484. });
  485. this.setData({
  486. isLoading: false,
  487. generatingProgress: 0,
  488. currentStep: 1
  489. });
  490. wx.hideLoading();
  491. },
  492. handleNetworkError(err) {
  493. console.error('API请求失败:', err);
  494. wx.showToast({
  495. title: '网络连接失败,请重试',
  496. icon: 'none',
  497. duration: 2000
  498. });
  499. this.setData({
  500. isLoading: false,
  501. generatingProgress: 0,
  502. currentStep: 1
  503. });
  504. wx.hideLoading();
  505. },
  506. // 查看景点详情
  507. viewAttractionDetail(e) {
  508. const attractionId = e.currentTarget.dataset.id;
  509. const attraction = this.findAttractionById(attractionId);
  510. if (attraction) {
  511. this.setData({
  512. showAttractionDetail: true,
  513. currentAttraction: attraction,
  514. attractionDetail: this.getAttractionDetailData(attraction)
  515. });
  516. }
  517. },
  518. // 获取景点详情数据
  519. getAttractionDetailData(attraction) {
  520. return {
  521. name: attraction.name,
  522. image: attraction.image,
  523. description: attraction.description,
  524. history: attraction.history,
  525. address: attraction.address,
  526. open_hours: attraction.open_hours,
  527. ticket_price: attraction.ticket_price,
  528. recommendation: attraction.recommendation,
  529. educational_value: attraction.educational_value,
  530. visiting_etiquette: [
  531. "请保持庄严肃穆",
  532. "勿大声喧哗",
  533. attraction.tags.includes('memorial') ? "纪念馆内请勿拍照" : ""
  534. ].filter(item => item),
  535. contact: attraction.contact || "暂无联系方式"
  536. };
  537. },
  538. // 关闭景点详情
  539. closeAttractionDetail() {
  540. this.setData({
  541. showAttractionDetail: false,
  542. currentAttraction: null,
  543. attractionDetail: null
  544. });
  545. },
  546. // 保存行程
  547. savePlan() {
  548. if (!this.data.planData || this.data.isLoading) return;
  549. this.setData({ isLoading: true });
  550. wx.showLoading({ title: '保存中...', mask: true });
  551. const saveData = {
  552. title: this.data.planData.title,
  553. description: this.data.planData.description,
  554. days: this.data.planData.days.length,
  555. is_red_tourism: true,
  556. day_plans: this.data.planData.days.map(day => ({
  557. day: day.day,
  558. theme: day.theme,
  559. description: day.description,
  560. transport: day.transport,
  561. attractions: day.attractions.map(attr => ({
  562. attraction_id: attr.id,
  563. order: attr.order || 1,
  564. visit_time: attr.visit_time,
  565. notes: attr.notes
  566. }))
  567. }))
  568. };
  569. wx.request({
  570. url: app.globalData.apiBaseUrl + '/api/plans/',
  571. method: 'POST',
  572. data: saveData,
  573. header: {
  574. 'Content-Type': 'application/json'
  575. },
  576. success: (res) => {
  577. if ([200, 201].includes(res.statusCode)) {
  578. this.setData({ currentStep: 3 });
  579. wx.showToast({
  580. title: '保存成功',
  581. icon: 'success',
  582. duration: 2000
  583. });
  584. } else {
  585. this.handleApiError(res);
  586. }
  587. },
  588. fail: this.handleNetworkError,
  589. complete: () => {
  590. wx.hideLoading();
  591. this.setData({ isLoading: false });
  592. }
  593. });
  594. },
  595. // 重新生成方法
  596. regeneratePlan() {
  597. if (this.data.isRegenerating) return;
  598. this.setData({ isRegenerating: true });
  599. wx.showLoading({ title: '正在重新规划...', mask: true });
  600. const requestData = {
  601. // city_ids: this.data.selectedCities,
  602. // days: this.data.selectedDays,
  603. city_ids: this.data.selectedCities.map(Number), // 确保是数字数组
  604. days: Number(this.data.selectedDays),
  605. interests: this.data.selectedInterests,
  606. transport: this.data.selectedTransport,
  607. custom_requirements: this.data.customRequirements,
  608. previous_plan: this.data.planData || { days: [] }, // 必须包含原行程
  609. // previous_plan: this.data.planData
  610. };
  611. wx.request({
  612. url: app.globalData.apiBaseUrl + '/api/red-tourism/regenerate/',
  613. method: 'POST',
  614. data: JSON.stringify(requestData),
  615. header: {
  616. 'Content-Type': 'application/json',
  617. 'Authorization': wx.getStorageSync('token') ? `Bearer ${wx.getStorageSync('token')}` : ''
  618. },
  619. success: (res) => {
  620. console.log('API返回结果:', JSON.stringify(res.data, null, 2)); // 新增的打印语句
  621. if (res.statusCode === 200 && res.data.status === 'success') {
  622. // 处理返回的完整景点信息
  623. const planData = this._processPlanData(res.data.data);
  624. this.setData({
  625. planData,
  626. showDetail: true // 显示详情视图
  627. });
  628. wx.showToast({ title: '重新生成成功', icon: 'success' });
  629. } else {
  630. wx.showToast({
  631. title: res.data.message || '重新生成失败',
  632. icon: 'none',
  633. duration: 2000
  634. });
  635. }
  636. },
  637. fail: (err) => {
  638. console.error('API请求失败:', err);
  639. wx.showToast({ title: '网络请求失败', icon: 'none' });
  640. },
  641. complete: () => {
  642. this.setData({ isRegenerating: false });
  643. wx.hideLoading();
  644. }
  645. });
  646. },
  647. _processPlanData(planData) {
  648. // 确保每个景点都有完整信息
  649. planData.days.forEach(day => {
  650. day.attractions.forEach(att => {
  651. att.showDetail = false; // 控制详情展开状态
  652. if (!att.image) att.image = '/images/default-attraction.jpg';
  653. if (!att.description) att.description = '这里是红色教育基地,具有重要的历史教育意义';
  654. });
  655. });
  656. return planData;
  657. },
  658. findAttractionById(id) {
  659. for (const day of this.data.planData.days) {
  660. for (const attr of day.attractions) {
  661. if (attr.id == id) return attr;
  662. }
  663. }
  664. return null;
  665. },
  666. viewPlan() {
  667. wx.navigateTo({
  668. url: '/pages/plan-detail/plan-detail?plan=' +
  669. encodeURIComponent(JSON.stringify(this.data.planData)) +
  670. '&is_red_tourism=true'
  671. });
  672. },
  673. sharePlan() {
  674. if (!this.data.planData) return;
  675. wx.showShareMenu({ withShareTicket: true });
  676. },
  677. onShareAppMessage() {
  678. if (!this.data.planData) return {};
  679. return {
  680. title: this.data.planData.title || '我的红色之旅行程',
  681. path: '/pages/plan-detail/plan-detail?plan=' +
  682. encodeURIComponent(JSON.stringify(this.data.planData)) +
  683. '&is_red_tourism=true',
  684. imageUrl: this.data.planData.days[0]?.attractions[0]?.image ||
  685. '/images/red-tourism-share.jpg'
  686. };
  687. },
  688. handleApiError(res) {
  689. let errorMsg = '请求失败,请稍后再试';
  690. if (res.data?.message) {
  691. errorMsg = res.data.message;
  692. } else if (res.statusCode === 400) {
  693. errorMsg = '输入数据有误,请检查后重试';
  694. } else if (res.statusCode === 500) {
  695. errorMsg = '服务器错误,请稍后再试';
  696. }
  697. wx.showToast({ title: errorMsg, icon: 'none' });
  698. this.setData({ isLoading: false });
  699. },
  700. handleNetworkError(err) {
  701. let errorMsg = '网络连接失败';
  702. if (err.errMsg.includes('timeout')) {
  703. errorMsg = '请求超时,请检查网络连接';
  704. }
  705. wx.showToast({ title: errorMsg, icon: 'none' });
  706. this.setData({ isLoading: false });
  707. },
  708. navigateBack() {
  709. if (this.data.currentStep > 1) {
  710. this.setData({ currentStep: this.data.currentStep - 1 });
  711. } else {
  712. wx.navigateBack();
  713. }
  714. },
  715. onShow(){
  716. this.recordFeatureUsage('ai-plan');
  717. },
  718. recordFeatureUsage(featureName) {
  719. const app = getApp();
  720. console.log('发送的Token:', app.globalData.userInfo.token);
  721. console.log('发送的featureName:', featureName);
  722. wx.request({
  723. url: 'http://127.0.0.1:8000/api/record-usage/',
  724. method: 'POST',
  725. header: {
  726. 'Authorization': `Token ${app.globalData.userInfo.token}`,
  727. 'Content-Type': 'application/json' // 确保这个请求头存在
  728. },
  729. data: JSON.stringify({ // 关键修改:必须使用JSON.stringify
  730. feature_name: featureName // 参数名必须与后端一致
  731. }),
  732. success: (res) => {
  733. console.log('行为记录响应:', res.data);
  734. },
  735. fail: (err) => {
  736. console.error('请求失败:', err);
  737. }
  738. });
  739. },
  740. });