// pages/ai-plan/ai-plan.js const app = getApp(); Page({ data: { currentStep: 1, planGenerated: false, selectedDays: 3, selectedCities: [], selectedInterests: ['patriotism'], // 默认选中爱国主义 isRegenerating: false, // 是否正在重新生成 planData: null, selectedTransport: 'public', customRequirements: '', dayOptions: [ { label: '1天', value: 1 }, { label: '2天', value: 2 }, { label: '3天', value: 3 }, { label: '4天', value: 4 }, { label: '5天', value: 5 }, { label: '6天', value: 6 }, { label: '7天', value: 7 } ], cityOptions: [], interestOptions: [ { label: '革命历史', value: 'history' }, { label: '红色教育', value: 'education' }, { label: '党史学习', value: 'party-history' }, { label: '爱国主义', value: 'patriotism' } ], transportOptions: [ { label: '公共交通', value: 'public', icon: '/images/icons/bus.png' }, { label: '自驾', value: 'drive', icon: '/images/icons/car.png' }, { label: '步行', value: 'walk', icon: '/images/icons/walk.png' }, { label: '骑行', value: 'bike', icon: '/images/icons/bike.png' } ], planData: null, totalAttractions: 0, currentDate: '', isLoading: false, apiStatus: { citiesLoaded: false, attractionsLoaded: false }, // 新增景点详情相关数据 showAttractionDetail: false, currentAttraction: null, attractionDetail: null }, // 添加 deepMerge 方法 deepMerge(target, source) { // 如果 source 不是对象,直接返回 target if (typeof source !== 'object' || source === null) { return target; } // 遍历 source 的所有属性 for (const key in source) { if (source.hasOwnProperty(key)) { // 如果属性值是对象且不是数组,递归合并 if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) { if (!target[key]) { target[key] = {}; } this.deepMerge(target[key], source[key]); } else { // 否则直接赋值 target[key] = source[key]; } } } return target; }, onLoad() { this.initPage(); this.loadInitialData(); }, initPage() { const date = new Date(); this.setData({ currentDate: `${date.getMonth() + 1}月${date.getDate()}日` }); this.initDefaultImages(); }, initDefaultImages() { this.setData({ transportOptions: this.data.transportOptions.map(item => ({ ...item, icon: this.checkImageExists(item.icon) ? item.icon : '/images/default-transport.png' })) }); }, loadInitialData() { this.getCities(); }, getCities() { this.setData({ isLoading: true, cityOptions: [] }); wx.request({ url: app.globalData.apiBaseUrl + '/api/cities/', method: 'GET', success: (res) => { if (res.statusCode === 200) { const rawData = res.data.data || res.data; const cityOptions = rawData.map(city => ({ label: city.name || '未知城市', value: city.id ? city.id.toString() : '0', disabled: city.is_available === false })); this.setData({ cityOptions, 'apiStatus.citiesLoaded': true }); } else { this.handleApiError(res); } }, fail: this.handleNetworkError, complete: () => { this.setData({ isLoading: false }); } }); }, checkImageExists(path) { try { const fs = wx.getFileSystemManager(); return fs.accessSync(path) === undefined; } catch (e) { return false; } }, handleImageError(e) { const { type, index } = e.currentTarget.dataset; const defaultImages = { transport: '/images/default-transport.png', attraction: '/images/default-attraction.jpg', loading: '/images/loading.gif', success: '/images/success.png', arrow: '/images/icons/arrow.png', checked: '/images/icons/checked.png' }; if (type === 'transport') { const key = `transportOptions[${index}].icon`; this.setData({ [key]: defaultImages.transport }); } else if (type === 'attraction') { const path = `planData.days[${e.currentTarget.dataset.day}].attractions[${index}].image`; this.setData({ [path]: defaultImages.attraction }); } else if (defaultImages[type]) { this.setData({ [e.currentTarget.dataset.src || 'imageUrl']: defaultImages[type] }); } }, selectDays(e) { this.setData({ selectedDays: e.currentTarget.dataset.value }); }, toggleCity(e) { const value = e.currentTarget.dataset.value; const index = this.data.selectedCities.indexOf(value); let newCities = [...this.data.selectedCities]; if (index === -1) { newCities.push(value); } else { newCities.splice(index, 1); } this.setData({ selectedCities: newCities }); }, toggleInterest(e) { const value = e.currentTarget.dataset.value; const index = this.data.selectedInterests.indexOf(value); let newInterests = [...this.data.selectedInterests]; if (index === -1) { newInterests.push(value); } else { newInterests.splice(index, 1); } this.setData({ selectedInterests: newInterests }); }, selectTransport(e) { this.setData({ selectedTransport: e.currentTarget.dataset.value }); }, updateCustomRequirements(e) { this.setData({ customRequirements: e.detail.value }); }, validateInputs() { if (this.data.selectedCities.length === 0) { wx.showToast({ title: '请至少选择一个城市', icon: 'none' }); return false; } if (this.data.selectedDays < 1) { wx.showToast({ title: '请选择有效天数', icon: 'none' }); return false; } return true; }, goToNextStep() { if (this.data.currentStep === 1) { if (!this.validateInputs()) return; this.setData({ currentStep: 2 }); this.generatePlanWithAI(); } else if (this.data.currentStep === 2) { this.setData({ currentStep: 3 }); } }, // 生成行程 generatePlanWithAI() { if (this.data.isLoading) return; // 验证输入 if (this.data.selectedCities.length === 0) { wx.showToast({ title: '请至少选择一个城市', icon: 'none' }); return; } // 显示加载状态 this.setData({ isLoading: true, planGenerated: false, planData: null }); wx.showLoading({ title: 'AI正在规划红色路线...', mask: true }); // 准备请求数据 const requestData = { city_ids: this.data.selectedCities, days: this.data.selectedDays, interests: [...this.data.selectedInterests, 'red-tourism'], transport: this.data.selectedTransport, custom_requirements: this.data.customRequirements }; // 调用API wx.request({ url: app.globalData.apiBaseUrl + '/api/red-tourism/plan/', method: 'POST', data: requestData, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') ? `Bearer ${wx.getStorageSync('token')}` : '' }, success: (res) => { console.log('API响应:', res); if (res.statusCode === 200) { console.log('API返回数据:', JSON.stringify(res.data, null, 2)); // 添加这行 if (res.data && res.data.status === 'success' && res.data.data) { // 处理并显示行程数据 this.processAndShowPlan(res.data.data); } else { this.handleError('返回数据格式不正确'); } } else { this.handleError(res.data?.message || `请求失败: ${res.statusCode}`); } }, fail: (err) => { this.handleError('网络请求失败,请检查连接'); }, complete: () => { wx.hideLoading(); this.setData({ isLoading: false }); } }); }, processAndShowPlan(planData, isRegenerate = false) { // 默认值配置 const DEFAULT_PLAN = { title: '红色文化之旅', description: '通过参观革命历史遗址学习党史', statistics: { red_attractions_count: 0, total_attractions: 0, educational_points_count: 0 }, red_tourism_tips: [ "请着装整洁,保持肃穆", "建议提前学习相关历史知识" ], suitable_for: "适合党员干部、学生团体等", days: [] }; // 深度合并默认值 const processedData = this.deepMerge(DEFAULT_PLAN, planData); // 处理每日行程 processedData.days = (processedData.days || []).map((day, index) => { // 确保景点数据标准化 const attractions = (day.attractions || []).map(att => ({ id: att.id || 0, name: att.name || '红色教育基地', description: att.description || '', image: att.image || '/images/default-red.jpg', is_red_tourism: att.is_red_tourism !== false, educational_value: att.educational_value || '高', visit_time: att.visit_time || '09:00-17:00', ticket_price: att.ticket_price || '免费', address: att.address || '', open_hours: att.open_hours || '09:00-17:00', recommendation: att.recommendation || '建议参观时长1-2小时', history: att.history || '这里是重要的革命历史遗址', tags: att.tags || ['红色旅游'] })); // 处理教育要点 const educational_points = (day.educational_points || []).map(point => { return typeof point === 'string' ? { name: '教育要点', description: point, is_red_tourism: true } : { name: point.name || '教育要点', description: point.description || '', is_red_tourism: point.is_red_tourism !== false }; }); // 计算当日统计 const dayStats = { red_attractions_count: attractions.filter(a => a.is_red_tourism).length, total_attractions: attractions.length, educational_points_count: educational_points.length }; return { day: day.day || index + 1, theme: day.theme || '红色教育', transport: day.transport || '公共交通', meals: day.meals || { breakfast: '酒店早餐', lunch: '景区附近餐厅', dinner: '当地特色餐馆' }, attractions, educational_points, summary: day.summary || '', accommodation: day.accommodation || '当地酒店', statistics: dayStats }; }); // 计算总统计 processedData.statistics = processedData.days.reduce((stats, day) => { stats.red_attractions_count += day.statistics.red_attractions_count; stats.total_attractions += day.statistics.total_attractions; stats.educational_points_count += day.statistics.educational_points_count; return stats; }, { red_attractions_count: 0, total_attractions: 0, educational_points_count: 0 }); // 更新UI this.setData({ planData: processedData, planGenerated: true, isLoading: false, lastGeneratedAt: processedData.generated_at || new Date().toISOString(), isRegenerated: isRegenerate }); }, // 新增方法处理景点数据 processAttractions(attractions) { if (!Array.isArray(attractions)) return []; return attractions.map(att => ({ id: att.id || 0, name: att.name || '红色教育基地', description: att.description || '', image: att.image || '/images/default-red.jpg', is_red_tourism: att.is_red_tourism !== false, educational_value: att.is_red_tourism ? (att.educational_value || '高') : '普通', visit_time: att.visit_time || '09:00-17:00', ticket_price: att.ticket_price || '免费', tags: Array.isArray(att.tags) ? att.tags : ['红色旅游'] })); }, // 错误处理 handleError(message) { console.error('Error:', message); wx.showToast({ title: message, icon: 'none', duration: 2000 }); this.setData({ currentStep: 1, isLoading: false, planGenerated: false }); }, handleApiError(res) { let errorMsg = '生成失败'; if (res.data && res.data.message) { errorMsg = res.data.message; } else if (res.statusCode === 500) { errorMsg = '服务器错误'; } wx.showToast({ title: errorMsg, icon: 'none' }); this.setData({ currentStep: 1 }); // 退回第一步 }, handleNetworkError() { wx.showToast({ title: '网络连接失败', icon: 'none' }); this.setData({ currentStep: 1 }); }, // 计算红色景点数量 calculateRedAttractions(planData) { return planData.days.reduce((total, day) => { return total + (day.attractions || []).filter(a => a.is_red_tourism || (a.tags && a.tags.some(tag => ['红色旅游', '革命', '烈士'].includes(tag))) ).length; }, 0); }, // 提取教育要点 extractEducationalPoints(planData) { const points = new Set(); (planData.days || []).forEach(day => { // 从每日主题中提取 if (day.theme && day.theme.includes('革命')) { points.add('学习' + day.theme); } // 从景点中提取 (day.attractions || []).forEach(attraction => { if (attraction.educational_value) { points.add(attraction.educational_value); } }); }); // 默认教育要点 if (points.size === 0) { return [ "了解革命历史背景", "学习先烈精神", "培养爱国情怀" ]; } return Array.from(points); }, // 格式化红色旅游数据 formatRedTourismPlanData(apiData) { const data = apiData.data || apiData; const defaultRedImage = '/images/red-attraction-default.jpg'; return { title: data.title || `${this.data.selectedDays}天红色之旅`, description: data.description || "重温革命历史,传承红色精神", days: (data.days || []).map(day => ({ day: day.day || 1, theme: day.theme || `红色教育第${day.day}天`, description: day.description || "", transport: day.transport || this.data.selectedTransport, educational_points: day.educational_points || [ "学习革命精神", "传承红色基因", "培养爱国情怀" ], attractions: (day.attractions || []).map(att => ({ id: att.id || 0, name: att.name || "红色教育基地", shortDesc: att.short_desc || att.description || "革命历史教育基地", description: att.description || "", image: att.image || defaultRedImage, tags: att.tags || ['红色旅游'], is_red_tourism: att.tags ? att.tags.some(tag => ['红色旅游', '革命', '烈士'].includes(tag)) : true, educational_value: att.educational_value || "高", visit_time: att.visit_time || "09:00-17:00", ticket_price: att.ticket_price || "免费", open_hours: att.open_hours || "09:00-17:00", address: att.address || "", recommendation: att.recommendation || "建议参观时长1-2小时", history: att.history || "这里是重要的革命历史遗址" })) })), red_tourism_tips: data.red_tourism_tips || [ "请着装整洁,保持肃穆", "可以准备鲜花进行瞻仰", "建议提前学习相关历史知识" ], suitable_for: data.suitable_for || "适合所有人群" }; }, // 错误处理 handleApiError(res) { let errorMsg = '生成红色路线失败'; if (res.data && res.data.message) { errorMsg = res.data.message; } else if (res.statusCode === 400) { errorMsg = '请求参数有误,请检查城市选择'; } else if (res.statusCode === 500) { errorMsg = '服务器处理失败,请稍后再试'; } wx.showToast({ title: errorMsg, icon: 'none', duration: 2000 }); this.setData({ isLoading: false, generatingProgress: 0, currentStep: 1 }); wx.hideLoading(); }, handleNetworkError(err) { console.error('API请求失败:', err); wx.showToast({ title: '网络连接失败,请重试', icon: 'none', duration: 2000 }); this.setData({ isLoading: false, generatingProgress: 0, currentStep: 1 }); wx.hideLoading(); }, // 查看景点详情 viewAttractionDetail(e) { const attractionId = e.currentTarget.dataset.id; const attraction = this.findAttractionById(attractionId); if (attraction) { this.setData({ showAttractionDetail: true, currentAttraction: attraction, attractionDetail: this.getAttractionDetailData(attraction) }); } }, // 获取景点详情数据 getAttractionDetailData(attraction) { return { name: attraction.name, image: attraction.image, description: attraction.description, history: attraction.history, address: attraction.address, open_hours: attraction.open_hours, ticket_price: attraction.ticket_price, recommendation: attraction.recommendation, educational_value: attraction.educational_value, visiting_etiquette: [ "请保持庄严肃穆", "勿大声喧哗", attraction.tags.includes('memorial') ? "纪念馆内请勿拍照" : "" ].filter(item => item), contact: attraction.contact || "暂无联系方式" }; }, // 关闭景点详情 closeAttractionDetail() { this.setData({ showAttractionDetail: false, currentAttraction: null, attractionDetail: null }); }, // 保存行程 savePlan() { if (!this.data.planData || this.data.isLoading) return; this.setData({ isLoading: true }); wx.showLoading({ title: '保存中...', mask: true }); const saveData = { title: this.data.planData.title, description: this.data.planData.description, days: this.data.planData.days.length, is_red_tourism: true, day_plans: this.data.planData.days.map(day => ({ day: day.day, theme: day.theme, description: day.description, transport: day.transport, attractions: day.attractions.map(attr => ({ attraction_id: attr.id, order: attr.order || 1, visit_time: attr.visit_time, notes: attr.notes })) })) }; wx.request({ url: app.globalData.apiBaseUrl + '/api/plans/', method: 'POST', data: saveData, header: { 'Content-Type': 'application/json' }, success: (res) => { if ([200, 201].includes(res.statusCode)) { this.setData({ currentStep: 3 }); wx.showToast({ title: '保存成功', icon: 'success', duration: 2000 }); } else { this.handleApiError(res); } }, fail: this.handleNetworkError, complete: () => { wx.hideLoading(); this.setData({ isLoading: false }); } }); }, // 重新生成方法 regeneratePlan() { if (this.data.isRegenerating) return; this.setData({ isRegenerating: true }); wx.showLoading({ title: '正在重新规划...', mask: true }); const requestData = { // city_ids: this.data.selectedCities, // days: this.data.selectedDays, city_ids: this.data.selectedCities.map(Number), // 确保是数字数组 days: Number(this.data.selectedDays), interests: this.data.selectedInterests, transport: this.data.selectedTransport, custom_requirements: this.data.customRequirements, previous_plan: this.data.planData || { days: [] }, // 必须包含原行程 // previous_plan: this.data.planData }; wx.request({ url: app.globalData.apiBaseUrl + '/api/red-tourism/regenerate/', method: 'POST', data: JSON.stringify(requestData), header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') ? `Bearer ${wx.getStorageSync('token')}` : '' }, success: (res) => { console.log('API返回结果:', JSON.stringify(res.data, null, 2)); // 新增的打印语句 if (res.statusCode === 200 && res.data.status === 'success') { // 处理返回的完整景点信息 const planData = this._processPlanData(res.data.data); this.setData({ planData, showDetail: true // 显示详情视图 }); wx.showToast({ title: '重新生成成功', icon: 'success' }); } else { wx.showToast({ title: res.data.message || '重新生成失败', icon: 'none', duration: 2000 }); } }, fail: (err) => { console.error('API请求失败:', err); wx.showToast({ title: '网络请求失败', icon: 'none' }); }, complete: () => { this.setData({ isRegenerating: false }); wx.hideLoading(); } }); }, _processPlanData(planData) { // 确保每个景点都有完整信息 planData.days.forEach(day => { day.attractions.forEach(att => { att.showDetail = false; // 控制详情展开状态 if (!att.image) att.image = '/images/default-attraction.jpg'; if (!att.description) att.description = '这里是红色教育基地,具有重要的历史教育意义'; }); }); return planData; }, findAttractionById(id) { for (const day of this.data.planData.days) { for (const attr of day.attractions) { if (attr.id == id) return attr; } } return null; }, viewPlan() { wx.navigateTo({ url: '/pages/plan-detail/plan-detail?plan=' + encodeURIComponent(JSON.stringify(this.data.planData)) + '&is_red_tourism=true' }); }, sharePlan() { if (!this.data.planData) return; wx.showShareMenu({ withShareTicket: true }); }, onShareAppMessage() { if (!this.data.planData) return {}; return { title: this.data.planData.title || '我的红色之旅行程', path: '/pages/plan-detail/plan-detail?plan=' + encodeURIComponent(JSON.stringify(this.data.planData)) + '&is_red_tourism=true', imageUrl: this.data.planData.days[0]?.attractions[0]?.image || '/images/red-tourism-share.jpg' }; }, handleApiError(res) { let errorMsg = '请求失败,请稍后再试'; if (res.data?.message) { errorMsg = res.data.message; } else if (res.statusCode === 400) { errorMsg = '输入数据有误,请检查后重试'; } else if (res.statusCode === 500) { errorMsg = '服务器错误,请稍后再试'; } wx.showToast({ title: errorMsg, icon: 'none' }); this.setData({ isLoading: false }); }, handleNetworkError(err) { let errorMsg = '网络连接失败'; if (err.errMsg.includes('timeout')) { errorMsg = '请求超时,请检查网络连接'; } wx.showToast({ title: errorMsg, icon: 'none' }); this.setData({ isLoading: false }); }, navigateBack() { if (this.data.currentStep > 1) { this.setData({ currentStep: this.data.currentStep - 1 }); } else { wx.navigateBack(); } }, onShow(){ this.recordFeatureUsage('ai-plan'); }, recordFeatureUsage(featureName) { const app = getApp(); console.log('发送的Token:', app.globalData.userInfo.token); console.log('发送的featureName:', featureName); wx.request({ url: 'http://127.0.0.1:8000/api/record-usage/', method: 'POST', header: { 'Authorization': `Token ${app.globalData.userInfo.token}`, 'Content-Type': 'application/json' // 确保这个请求头存在 }, data: JSON.stringify({ // 关键修改:必须使用JSON.stringify feature_name: featureName // 参数名必须与后端一致 }), success: (res) => { console.log('行为记录响应:', res.data); }, fail: (err) => { console.error('请求失败:', err); } }); }, });