// import { campApi, taskApi } from '../../api/index'; // pages/camp_list/index.js import { campApi } from '../../config/camp_api.js'; import { questionApi } from '../../config/question_api.js'; // 引入模块化功能 import * as utils from './modules/utils.js'; import * as dataFetcher from './modules/data-fetcher.js'; import * as videoController from './modules/video-controller.js'; import * as taskHandler from './modules/task-handler.js'; import * as campService from './modules/camp-service.js'; import * as orderService from './modules/order-service.js'; Page({ data: { campId: null, campInfo: { title: '营地详情', // 添加默认值 desc: '', cover_image: '' }, isImportant: true, // 默认显示"重点打卡"(粉色) loading: true, courseList: [], sections: [], // 将根据课程数据动态生成 hasJoined: false, // 是否已加入营地 bannerVideoUrl: '', showVideo: false, sectionId: null, // 添加 sectionId currentPlayingTask: null, // 当前正在播放的任务 currentSelectedTaskId: null, originBannerContent: '', tasksLoading: [], // 新增:每个章节的任务loading状态 hasLogged10Percent: false, // 新增:用于只打印一次 currentVideoCompletionPercent: 100, videoCompletionMap: {}, currentPlayingTaskId: null, currentPlayingTaskType: null, isAutoPlay: true, // 添加标记,用于区分首次加载和返回 hasVideoPlayed: false, // 新增:标记视频是否已经播放过 isVideoTaskPlaying: false, // 标记视频任务是否正在播放中 videoTitle: '', campStatus: '', online: wx.getStorageSync('online'), topContentType: 'INTRO_TYPE_NONE', introType: 'INTRO_TYPE_NONE', accessList: [], // 小节可访问列表 currentSection: null, // 当前小节信息(从 is_current 或 current_section_id 获取) _isDataLoaded: false, // 标记数据是否已加载,避免 onLoad 和 onShow 重复请求 // 自定义导航栏相关 statusBarHeight: 20, navBarHeight: 44, navBarTitle: '打卡营', pageContainerShow: true, // 用于拦截右滑/边框返回,与点击返回统一逻辑 navTotalHeight: 64, // statusBarHeight + navBarHeight,page-container 从该高度开始 sectionUnlockAtMap: {}, // 小节 id -> 可解锁时间 Unix 秒(来自 can_unlock_section 的 unlock_at) sectionUnlockTexts: [] // 与 courseList 同序,小节倒计时文案,用于 wxml 显示 }, onLoad: function (options) { var that = this; var id = options.id; // 获取系统信息,设置自定义导航栏高度 try { var systemInfo = wx.getSystemInfoSync(); this.setData({ statusBarHeight: systemInfo.statusBarHeight || 20, navBarHeight: 44, navTotalHeight: (systemInfo.statusBarHeight || 20) + 44 }); } catch (e) {} // 检查是否已登录 var wxuserid = wx.getStorageSync('wxuserid'); if (!wxuserid || wxuserid === '' || wxuserid === null || wxuserid === undefined) { // 未登录,显示提示框 wx.showModal({ title: '提示', content: '您还未登录,请先登录后再查看打卡营详情', showCancel: true, confirmText: '去登录', success: function (res) { if (res.confirm) { // 用户点击确认,跳转到登录页,并传递返回参数 var loginUrl = '/pages/userlogin/userlogin?redirect_url=' + encodeURIComponent('/pages/camp_detail/index'); if (id) { loginUrl += '&camp_id=' + id; } wx.navigateTo({ url: loginUrl }); } } }); return; } if (id) { this.setData({ campId: id, hasVideoPlayed: false, // 重置视频播放状态 videoTitle: '打卡营介绍', _isDataLoaded: false // 重置加载标记 }); // 清除可能残留的旧视频播放状态(退出时不保存视频任务状态,始终显示营地介绍) try { wx.removeStorageSync('campVideoPlaybackState'); } catch (e) {} utils.getScreenSize(this); // 首次加载时获取所有数据 dataFetcher.fetchCampData(this, id); } else { wx.showToast({ title: '营地ID不存在', icon: 'none' }); setTimeout(function () { wx.navigateBack(); }, 1500); } }, onShow: function () { // 检查是否已登录 var wxuserid = wx.getStorageSync('wxuserid'); if (!wxuserid || wxuserid === '' || wxuserid === null || wxuserid === undefined) { // 未登录,跳转到登录页,并传递返回参数 var loginUrl = '/pages/userlogin/userlogin?redirect_url=' + encodeURIComponent('/pages/camp_detail/index'); var campId = this.data.campId; if (campId) { loginUrl += '&camp_id=' + campId; } wx.navigateTo({ url: loginUrl }); return; } // 如果数据已加载过,刷新任务状态(从子页面返回时) // 使用聚合接口 camp_detail_with_user_status 获取最新的任务状态 if (this.data._isDataLoaded && this.data.campId) { dataFetcher.fetchCampData(this, this.data.campId); } // 如果视频上下文存在且不是首次加载,则暂停视频 if (this.videoContext && !this.data.isAutoPlay) { this.videoContext.pause(); } }, // refreshTaskStatus 已移至 modules/data-fetcher.js // 获取课程列表数据,只更新课程状态 // fetchCampCourses: function (campId) { // var that = this; // if (!campId) return; // return new Promise(function (resolve, reject) { // campApi.getCampCourses(campId) // .then(function (coursesRes) { // var courseList = []; // if (coursesRes.success === true && coursesRes.sections && Array.isArray(coursesRes.sections)) { // // 新格式:数据在根级的 sections 字段,需要映射字段名 // courseList = (coursesRes.sections || []).map(function(section) { // return { // course_id: section.id || section.course_id, // course_title: section.title || section.course_title || '未知课程', // id: section.id, // title: section.title, // price: section.price_fen || section.price || 0, // price_fen: section.price_fen || 0, // is_started: section.is_started || false, // is_completed: section.is_completed || false, // is_purchased: section.is_purchased || false, // number: section.section_number || section.number || 0, // section_number: section.section_number || 0, // tasks: section.tasks || [], // require_previous_section: section.require_previous_section || false, // time_interval_type: section.time_interval_type, // time_interval_value: section.time_interval_value || 0 // }; // }); // } // // 深拷贝课程列表 // var newCourseList = JSON.parse(JSON.stringify(courseList)); // // 保持当前展开状态和任务状态 // if (that.data.courseList && that.data.courseList.length > 0) { // newCourseList.forEach(function (newCourse, index) { // var oldCourse = that.data.courseList[index]; // if (oldCourse) { // // 保持展开状态 // newCourse.expanded = oldCourse.expanded; // // 保持任务状态 // if (newCourse.tasks && oldCourse.tasks) { // newCourse.tasks = newCourse.tasks.map(function (newTask) { // var oldTask = oldCourse.tasks.find(function (t) { // return t.task_id === newTask.task_id; // }); // if (oldTask) { // return Object.assign({}, newTask, { // status: oldTask.status, // detail: oldTask.detail // }); // } // return newTask; // }); // } // } // }); // } // // 只更新课程列表 // that.setData({ // courseList: newCourseList // }); // utils.generateExpandedStatus(that); // resolve(); // }) // .catch(function (error) { // reject(error); // }); // }); // }, // // 获取营地详情数据,只更新打卡营状态 // fetchCampDetail: function (campId) { // var that = this; // if (!campId) return; // return new Promise(function (resolve, reject) { // campApi.getCampDetail(campId) // .then(function (detailRes) { // // 兼容两种响应格式: // // 1. { code: 200, data: { ... } } - 旧格式 // // 2. { success: true, camp: { ... } } - 新格式(proto定义) // if (detailRes.success === true && detailRes.camp) { // // 新格式:数据在根级的 camp 字段 // var campData = detailRes.camp; // that.setData({ // campStatus: (function(s){ // s = (s||'').toString(); // if (s === 'Completed' || s === 'COMPLETED' || s === 'completed') return 'Completed'; // if (s === 'InProgress' || s === 'IN_PROGRESS' || s === 'in_progress') return 'InProgress'; // if (s === 'NotStarted' || s === 'NOT_STARTED' || s === 'not_started') return 'NotStarted'; // return 'NotStarted'; // })(campData.status) // }); // } else if (detailRes.code === 200 && detailRes.data) { // // 旧格式:数据在 data 字段 // that.setData({ // campStatus: (function(s){ // s = (s||'').toString(); // if (s === 'Completed' || s === 'COMPLETED' || s === 'completed') return 'Completed'; // if (s === 'InProgress' || s === 'IN_PROGRESS' || s === 'in_progress') return 'InProgress'; // if (s === 'NotStarted' || s === 'NOT_STARTED' || s === 'not_started') return 'NotStarted'; // return 'NotStarted'; // })(detailRes.data.status) // }); // } // resolve(); // }) // .catch(function (error) { // reject(error); // }); // }); // }, // fetchCampData 已移至 modules/data-fetcher.js // 加入营地 joinCamp: function () { var that = this; var campId = this.data.campId; if (!campId) return; wx.showLoading({ title: '加入中...' }); campApi.joinCamp(campId) .then(function (res) { wx.hideLoading(); if (res.success === true) { wx.showToast({ title: res.message || '加入成功', icon: 'success' }); that.setData({ hasJoined: true }); // 加入成功后,自动开启第一小节(购买第一小节) // 直接使用聚合接口返回的数据,无需再次请求 var courseList = that.data.courseList || []; if (!courseList || courseList.length === 0) { // 如果还没有数据,重新获取 dataFetcher.fetchCampData(that, campId); return; } // 选择第一小节:优先按 section_number 升序,否则取数组第一个 var first = courseList.slice().sort(function(a,b){ var an = a.section_number || a.number || 0; var bn = b.section_number || b.number || 0; return an - bn; })[0]; var firstSectionId = first && (first.id || first.section_id || first.course_id); if (!firstSectionId) { dataFetcher.fetchCampData(that, campId); return; } campApi.purchaseSection(campId, firstSectionId) .then(function(purRes){ if (purRes && (purRes.success === true || purRes.code === 200)) { wx.showToast({ title: purRes.message || '已开启第一小节', icon: 'success' }); } else { wx.showToast({ title: (purRes && purRes.message) || '开启第一小节失败', icon: 'none' }); } }) .finally(function(){ dataFetcher.fetchCampData(that, campId); }); } else { wx.showToast({ title: res.message || '加入失败', icon: 'none' }); } }) .catch(function (err) { wx.hideLoading(); wx.showToast({ title: '加入失败', icon: 'none' }); }); }, // 检查并开启小节(如果不在 accessList 中) checkAndUnlockSection: function (sectionId, callback, showPaymentModal) { // 需要传递 createOrderAndPay 方法给 campService var originalCreateOrderAndPay = this.createOrderAndPay; if (!originalCreateOrderAndPay) { // 如果主文件中没有,使用 orderService 的方法 this.createOrderAndPay = function(sectionId, priceFen, section) { return orderService.createOrderAndPay(this, sectionId, priceFen, section); }; } return campService.checkAndUnlockSection(this, sectionId, callback, showPaymentModal); }, // 创建订单并支付 createOrderAndPay: function (sectionId, priceFen, section) { return orderService.createOrderAndPay(this, sectionId, priceFen, section); }, // 切换章节展开/收起 toggleSection: function (e) { campService.toggleSection(this, e); }, // 获取任务列表并刷新状态 async refreshTaskListStatus(course_id, index, forceRefresh) { var that = this; var courseList = this.data.courseList; var tasksLoading = this.data.tasksLoading; // 检查 course_id 是否有效(包括 0, null, undefined, 空字符串) if (!course_id || course_id === 0 || course_id === '0' || course_id === 'undefined') { // 取消 loading var newTasksLoading = tasksLoading.slice(); newTasksLoading[index] = false; that.setData({ tasksLoading: newTasksLoading }); wx.showToast({ title: '小节ID无效', icon: 'none' }); return; } // 如果需要强制刷新,重新获取整个营地数据 if (forceRefresh === true) { var campId = that.data.campId; if (campId) { // 重新获取整个营地数据(静默更新,不显示 loading) dataFetcher.fetchCampData(that, campId, true); return; } } // 标记本章节为 loading var newTasksLoading = tasksLoading.slice(); newTasksLoading[index] = true; that.setData({ tasksLoading: newTasksLoading }); try { // 直接使用聚合接口返回的任务列表,无需再次请求 var currentCourse = courseList[index]; if (!currentCourse) { throw new Error('小节不存在'); } // 从聚合接口返回的数据中获取任务列表 var tasks = currentCourse.tasks || []; // 构建任务进度映射(从任务的 progress 字段中提取) var progressMap = {}; tasks.forEach(function(task) { if (task && task.progress) { var taskId = task.id || task.task_id || task.taskId; if (taskId) { progressMap[String(taskId)] = task.progress; } } }); if (tasks.length > 0) { // 深拷贝当前课程列表 var newCourseList = JSON.parse(JSON.stringify(courseList)); // 直接使用接口返回的任务数据,不做映射 newCourseList[index].tasks = tasks; // 注意:小节的 is_completed 状态应该直接使用接口返回的值 // 不要通过任务状态推断,因为接口已经提供了准确的完成状态 // 这里只更新任务列表,不修改 is_completed 字段 // 更新视频完成度映射与任务状态 var videoCompletionPercent = null; var videoCompletionMap = Object.assign({}, that.data.videoCompletionMap || {}); tasks.forEach(function (task) { if (!task) return; var taskIdKey = task.id || task.task_id || task.taskId; var stringKey = taskIdKey ? String(taskIdKey) : ''; var resolvedPercent = utils.resolveVideoCompletionPercent(task); if (stringKey) { if (resolvedPercent !== null) { videoCompletionMap[stringKey] = resolvedPercent; } // 优先使用接口返回的 status 字段 // 状态值:NotStarted, InProgress, Completed, Reviewing, Rejected if (task.status) { // 规范化状态值(确保大小写正确) var normalizedStatus = String(task.status).trim(); if (normalizedStatus === 'NotStarted' || normalizedStatus === 'InProgress' || normalizedStatus === 'Completed' || normalizedStatus === 'Reviewing' || normalizedStatus === 'Rejected') { task.status = normalizedStatus; } else { // 如果状态值不在预期范围内,尝试转换 var statusUpper = normalizedStatus.toUpperCase(); if (statusUpper.indexOf('COMPLETED') !== -1) { task.status = 'Completed'; } else if (statusUpper.indexOf('REVIEWING') !== -1) { task.status = 'Reviewing'; } else if (statusUpper.indexOf('REJECTED') !== -1) { task.status = 'Rejected'; } else if (statusUpper.indexOf('INPROGRESS') !== -1 || statusUpper.indexOf('IN_PROGRESS') !== -1) { task.status = 'InProgress'; } else if (statusUpper.indexOf('NOTSTARTED') !== -1 || statusUpper.indexOf('NOT_STARTED') !== -1) { task.status = 'NotStarted'; } else { // 无法识别,默认为 NotStarted task.status = 'NotStarted'; } } } else { // 如果没有 status 字段,根据 progress 数据推断状态(兼容旧逻辑) var progress = progressMap[stringKey]; if (progress) { task.progress = progress; // 判断任务类型(只有主观题和申论题需要审核) var taskType = String(task.task_type || '').toLowerCase(); var isReviewableTask = taskType === 'subjective' || taskType === 'essay' || task.need_review === true; if (isReviewableTask) { // 主观题和申论题:优先检查审核状态(无论是否完成) var reviewStatus = progress.review_status; if (reviewStatus) { var reviewStatusUpper = String(reviewStatus).toUpperCase(); // 检查审核状态(支持大小写) if (reviewStatusUpper === 'REJECTED' || reviewStatus === 'rejected' || reviewStatusUpper === 'REVIEW_STATUS_REJECTED') { task.status = 'Rejected'; } else if (reviewStatusUpper === 'APPROVED' || reviewStatus === 'approved' || reviewStatusUpper === 'REVIEW_STATUS_APPROVED') { // 审核通过且已完成,才显示为 Completed if (progress.is_completed === true || progress.is_completed === 1) { task.status = 'Completed'; } else { task.status = 'InProgress'; } } else if (reviewStatusUpper === 'PENDING' || reviewStatus === 'pending' || reviewStatusUpper === 'REVIEW_STATUS_PENDING') { // 待审核状态 if (progress.is_completed === true || progress.is_completed === 1) { task.status = 'Reviewing'; } else { task.status = 'InProgress'; } } else { // 其他情况,根据完成状态判断 if (progress.is_completed === true || progress.is_completed === 1) { task.status = 'Reviewing'; } else { task.status = 'InProgress'; } } } else { // 没有审核状态,根据完成状态判断 if (progress.is_completed === true || progress.is_completed === 1) { task.status = 'Reviewing'; } else { task.status = 'InProgress'; } } } else { // 其他任务类型(图文、视频、客观题):直接根据完成状态判断,不检查审核状态 if (progress.is_completed === true || progress.is_completed === 1) { task.status = 'Completed'; } else { task.status = 'InProgress'; } } } else { task.status = 'NotStarted'; } } } else { // 没有 taskId,确保有默认状态 if (!task.status) { task.status = 'NotStarted'; } } if (task.task_type === 'TASK_TYPE_VIDEO' && resolvedPercent !== null) { videoCompletionPercent = resolvedPercent; } }); // 一次性更新所有数据 var updateData = { courseList: newCourseList, videoCompletionMap: videoCompletionMap }; if (videoCompletionPercent !== null) { updateData.currentVideoCompletionPercent = videoCompletionPercent; } that.setData(updateData); } else { // 如果没有任务,确保 tasks 字段存在 var newCourseList = JSON.parse(JSON.stringify(courseList)); if (!newCourseList[index].tasks) { newCourseList[index].tasks = []; that.setData({ courseList: newCourseList }); } } // 取消 loading newTasksLoading[index] = false; that.setData({ tasksLoading: newTasksLoading }); } catch (e) { // 取消 loading newTasksLoading[index] = false; that.setData({ tasksLoading: newTasksLoading }); wx.showToast({ title: '获取任务列表失败', icon: 'none' }); } }, // 视频播放结束 onVideoEnded: function () { videoController.onVideoEnded(this); }, // 视频播放错误 onVideoError: function () { videoController.onVideoError(this); }, // 视频开始播放 onVideoPlay: function () { videoController.onVideoPlay(this); }, // 视频元数据加载完成 onVideoLoadedMetadata: function () { // 视频元数据加载完成,可以安全地操作视频 // 如果设置了自动播放标记,尝试播放(作为备用方案) var that = this; if (that.data._pendingVideoPlay && that.data.isAutoPlay) { setTimeout(function() { try { if (that.videoContext) { var playResult = that.videoContext.play(); // 检查 play() 是否返回 Promise(某些版本可能不返回) if (playResult && typeof playResult.catch === 'function') { playResult.catch(function(err) { }); } } } catch (e) { } }, 200); } }, // 视频可以播放时(数据已加载足够) onVideoCanPlay: function () { var that = this; // 如果设置了自动播放标记,则开始播放(作为主要播放逻辑) if (that.data._pendingVideoPlay && that.data.isAutoPlay) { setTimeout(function() { try { if (that.videoContext) { // 尝试播放视频 var playResult = that.videoContext.play(); // 检查 play() 是否返回 Promise(某些版本可能不返回) if (playResult && typeof playResult.catch === 'function') { playResult.catch(function(err) { // 播放失败不影响,清除标记即可 that.setData({ _pendingVideoPlay: false }); }); } // 清除自动播放标记(无论成功与否) that.setData({ _pendingVideoPlay: false }); } else { // videoContext 不存在,清除标记 that.setData({ _pendingVideoPlay: false }); } } catch (e) { that.setData({ _pendingVideoPlay: false }); } }, 100); } }, // 视频暂停 onVideoPause: function () { videoController.onVideoPause(this); }, // 任务 点击 逻辑 handleTaskClick: function (e) { var that = this; // 如果有视频正在播放,点击其他任务时清空播放状态(不阻断点击流程) if (that.data.showVideo && that.data.currentPlayingTaskId) { var clickedTaskId = e.currentTarget.dataset.taskId; if (String(clickedTaskId) !== String(that.data.currentPlayingTaskId)) { // 清除保存的视频恢复状态(不保留,用户主动切换任务) try { wx.removeStorageSync('campVideoPlaybackState'); } catch (e) {} } } // 修改已发送标记 that.setData({ hasVideoPlayed: false, // 标记视频已经播放过 isAutoPlay: true, // 标记不是自动播放 hasLogged10Percent: false }); var isCurrent = e.currentTarget.dataset.isCurrent; var taskId = e.currentTarget.dataset.taskId; // 注意:wxml 中传递的是 data-section-id,微信小程序会转换为 sectionId var sectionId = e.currentTarget.dataset.sectionId; var number = e.currentTarget.dataset.number; var courseList = this.data.courseList; var hasJoined = this.data.hasJoined; // 展示等待 wx.showLoading({ title: '请稍等...', }); // 判断当前课程 是否已经开启(更健壮的匹配:优先使用 sectionId,再按 id/course_id,最后退回 number) var currentCourse = null; // 优先使用 sectionId(从 wxml 的 data-section-id 传递过来) var targetId = sectionId; if (courseList && courseList.length > 0) { // 优先通过 ID 匹配 if (targetId) { currentCourse = courseList.find(function (t) { return ( String(t.id) === String(targetId) || String(t.course_id) === String(targetId) || String(t.section_id) === String(targetId) ); }); } // 如果通过 ID 找不到,再通过 section_number 匹配 if (!currentCourse && number !== undefined && number !== null) { currentCourse = courseList.find(function (t) { var tNumber = t.section_number || t.number; return tNumber === number; }); } } if (!currentCourse) { wx.hideLoading(); wx.showToast({ title: '未找到对应小节', icon: 'none' }); return; } // 使用找到的小节的 ID 作为后续的 sectionId var actualSectionId = currentCourse.id || currentCourse.course_id || currentCourse.section_id || targetId; // 时间间隔未到:若该小节在 sectionUnlockAtMap 中,则禁止进入并弹出倒计时提示 var sectionUnlockAtMap = this.data.sectionUnlockAtMap || {}; if (sectionUnlockAtMap[actualSectionId]) { var unlockAt = sectionUnlockAtMap[actualSectionId]; var nowSec = Math.floor(Date.now() / 1000); var left = unlockAt - nowSec; var msg = '时间到了才能开启'; if (left > 0) { // 自然天(明日解锁):不显示具体倒计时 var unlockDate = new Date(unlockAt * 1000); var today = new Date(); var tomorrowStart = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1, 0, 0, 0, 0); var tomorrowEnd = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2, 0, 0, 0, 0) - 1; if (unlockDate.getTime() >= tomorrowStart.getTime() && unlockDate.getTime() < tomorrowEnd) { msg = '任务将于明日才能开启'; } else { var h = Math.floor(left / 3600); var m = Math.floor((left % 3600) / 60); if (h > 0) { msg = '时间到了才能开启,该小节将在 ' + h + ' 小时 ' + m + ' 分钟后解锁'; } else if (m > 0) { msg = '时间到了才能开启,该小节将在 ' + m + ' 分钟后解锁'; } else { msg = '时间到了才能开启,请稍候'; } } } wx.hideLoading(); wx.showToast({ title: msg, icon: 'none', duration: 2500 }); return; } // 检查打卡营是否开启 if (!hasJoined) { wx.showToast({ title: '请先开启打卡营', icon: 'error', success: function () { setTimeout(function () { wx.hideLoading(); }, 1000); } }); // wx.hideLoading(); return; } // 当前小节 开启 则 进行前置任务判断 if (currentCourse.is_started) { // 验证:判断上一小节是否完成(使用 is_completed 字段) // 【已注释】暂时禁用上一小节完成验证 var prevCourse = null; var currentSectionNumber = currentCourse.section_number || currentCourse.number || 0; var requirePrevious = currentCourse.require_previous_section || false; console.log('验证条件:', { currentSectionNumber: currentSectionNumber, requirePrevious: requirePrevious, needCheck: currentSectionNumber > 1 || requirePrevious }); // 如果当前小节不是第一小节,或者 require_previous_section 为 true,则需要检查上一小节 if (currentSectionNumber > 1 || requirePrevious) { console.log('需要检查上一小节,开始查找...'); if (courseList && courseList.length > 0) { console.log('课程列表长度:', courseList.length); // 先根据当前课程在列表中的位置来找上一节 var currentIndex = courseList.findIndex(function (t) { return t === currentCourse; }); console.log('当前小节在列表中的索引:', currentIndex); var prevIndex = currentIndex > 0 ? currentIndex - 1 : -1; if (prevIndex >= 0) { prevCourse = courseList[prevIndex]; console.log('通过索引找到上一小节:', { index: prevIndex, id: prevCourse.id, title: prevCourse.title, section_number: prevCourse.section_number, is_completed: prevCourse.is_completed }); } else { console.log('无法通过索引找到,尝试通过 section_number 查找...'); // 如果无法通过索引找到,则按小节序号 section_number 回退上一节 var prevSectionNumber = currentSectionNumber - 1; console.log('查找上一小节序号:', prevSectionNumber); prevCourse = courseList.find(function (t) { var tSectionNumber = t.section_number || t.number || 0; var match = tSectionNumber === prevSectionNumber; if (match) { console.log('找到匹配的小节:', { id: t.id, title: t.title, section_number: tSectionNumber, is_completed: t.is_completed }); } return match; }); } } // 如果找不到上一小节,但当前小节不是第一小节,说明数据异常 if (!prevCourse && currentSectionNumber > 1) { console.error('❌ 找不到上一小节,但当前小节不是第一小节!'); wx.hideLoading(); wx.showToast({ title: '数据异常,无法找到上一小节', icon: 'error' }); return; } if (prevCourse) { console.log('上一小节信息:', { id: prevCourse.id, title: prevCourse.title, section_number: prevCourse.section_number, is_completed: prevCourse.is_completed, is_completed_type: typeof prevCourse.is_completed, is_completed_value: prevCourse.is_completed }); // 如果上一小节存在且未完成,则阻止跳转 if (!prevCourse.is_completed) { console.log('❌ 上一小节未完成,阻止跳转'); wx.hideLoading(); wx.showToast({ title: '请先完成上一小节', icon: 'error' }); return; } else { console.log('✅ 上一小节已完成,允许继续'); } } else { console.log('✅ 没有上一小节(可能是第一小节),允许继续'); } } else { console.log('✅ 当前是第一小节且不需要前置小节,允许继续'); } that.setData({ currentSelectedTaskId: taskId, }); // 判断是否有前置任务未完成 var prerequisites = e.currentTarget.dataset.task.prerequisites; var isCompleted = true; var taskTitle = ''; if (prerequisites && prerequisites.length > 0) { prerequisites.forEach(function (item) { var course = that.data.courseList.find(function (t) { return String(t.course_id) === String(actualSectionId) || String(t.id) === String(actualSectionId) || String(t.section_id) === String(actualSectionId); }); if (!course || !course.tasks) return; course.tasks.forEach(function (task) { // 支持 id 和 task_id 两种字段名 var taskId = task.id || task.task_id; if (taskId && (String(taskId) === String(item) || parseInt(taskId) === parseInt(item))) { // 使用 status 字段判断任务是否完成 // 状态值:NotStarted, InProgress, Completed, Reviewing, Rejected if (task.status !== 'Completed') { isCompleted = false; taskTitle = task.task_title || task.title || '前置任务'; } } }); }); if (!isCompleted) { wx.hideLoading(); wx.showModal({ title: '提示', content: '请先完成前置任务\n👇👇👇\n《' + taskTitle + '》', showCancel: false, confirmText: '知道了' }); return; } } // 检查任务是否可以开始(前置任务逐个完成逻辑) var taskData = e.currentTarget.dataset.task || {}; if (taskData.can_start === false) { wx.hideLoading(); wx.showToast({ title: '请先完成上一个任务', icon: 'none', duration: 2000 }); return; } // 处理任务跳转 that.navigateToTaskPage(e); return; } else { // 当前小节未开启:进行完整验证 // 1. 判断上一小节是否完成 // 【已注释】暂时禁用上一小节完成验证 var prevCourse = null; var currentSectionNumber = currentCourse.section_number || currentCourse.number || 0; var requirePrevious = currentCourse.require_previous_section || false; console.log('验证条件:', { currentSectionNumber: currentSectionNumber, requirePrevious: requirePrevious, needCheck: currentSectionNumber > 1 || requirePrevious }); // 如果当前小节不是第一小节,或者 require_previous_section 为 true,则需要检查上一小节 if (currentSectionNumber > 1 || requirePrevious) { console.log('需要检查上一小节,开始查找...'); if (courseList && courseList.length > 0) { console.log('课程列表长度:', courseList.length); // 先根据当前课程在列表中的位置来找上一节 var currentIndex = courseList.findIndex(function (t) { return t === currentCourse; }); console.log('当前小节在列表中的索引:', currentIndex); var prevIndex = currentIndex > 0 ? currentIndex - 1 : -1; if (prevIndex >= 0) { prevCourse = courseList[prevIndex]; console.log('通过索引找到上一小节:', { index: prevIndex, id: prevCourse.id, title: prevCourse.title, section_number: prevCourse.section_number, is_completed: prevCourse.is_completed }); } else { console.log('无法通过索引找到,尝试通过 section_number 查找...'); // 如果无法通过索引找到,则按小节序号 section_number 回退上一节 var prevSectionNumber = currentSectionNumber - 1; console.log('查找上一小节序号:', prevSectionNumber); prevCourse = courseList.find(function (t) { var tSectionNumber = t.section_number || t.number || 0; var match = tSectionNumber === prevSectionNumber; if (match) { console.log('找到匹配的小节:', { id: t.id, title: t.title, section_number: tSectionNumber, is_completed: t.is_completed }); } return match; }); } } // 如果找不到上一小节,但当前小节不是第一小节,说明数据异常 if (!prevCourse && currentSectionNumber > 1) { console.error('❌ 找不到上一小节,但当前小节不是第一小节!'); wx.hideLoading(); wx.showToast({ title: '数据异常,无法找到上一小节', icon: 'error' }); return; } if (prevCourse) { console.log('上一小节信息:', { id: prevCourse.id, title: prevCourse.title, section_number: prevCourse.section_number, is_completed: prevCourse.is_completed, is_completed_type: typeof prevCourse.is_completed, is_completed_value: prevCourse.is_completed }); // 验证1:判断上一小节是否完成 if (!prevCourse.is_completed) { console.log('❌ 上一小节未完成,阻止操作'); wx.hideLoading(); wx.showToast({ title: '请先完成上一小节', icon: 'error' }); return; } else { console.log('✅ 上一小节已完成,允许继续'); } } else { console.log('✅ 没有上一小节(可能是第一小节),允许继续'); } } else { console.log('✅ 当前是第一小节且不需要前置小节,允许继续'); } // 验证2:判断当前小节是否已经开启 if (currentCourse.is_started) { // 如果已经开启,应该走上面的逻辑,这里不应该执行到 wx.hideLoading(); wx.showToast({ title: '小节已开启,请重试', icon: 'none' }); return; } // 验证3:判断当前小节是否需要收费 var needPayment = false; var price = currentCourse.price_fen || currentCourse.price || 0; var isPurchased = currentCourse.is_purchased || false; var sectionId = currentCourse.id || currentCourse.course_id || currentCourse.section_id; // 如果价格大于0且未支付,则需要收费 if (price > 0 && !isPurchased) { needPayment = true; } // 根据验证结果,提示用户并引导开启小节 if (needPayment) { // 需要付费,调用支付流程 wx.hideLoading(); var priceYuan = (price / 100).toFixed(2); wx.showModal({ title: '提示', content: '开启小节👉' + (currentCourse.title || currentCourse.course_title || '本小节') + '?\n需要支付' + priceYuan + '元', success: function (res) { if (res.confirm) { // 用户确认,开始支付流程 wx.showLoading({ title: '准备支付...' }); that.createOrderAndPay(sectionId, price, currentCourse) .then(function (success) { wx.hideLoading(); if (success) { // 刷新数据 dataFetcher.fetchCampData(that, that.data.campId); } }) .catch(function (err) { wx.hideLoading(); }); } } }); } else { // 免费或已支付,直接调用 purchaseSection 开启小节 wx.hideLoading(); wx.showModal({ title: '提示', content: '开启小节👉' + (currentCourse.title || currentCourse.course_title || '本小节') + '?', success: function (res) { if (res.confirm) { // 用户确认,开启小节 wx.showLoading({ title: '开启中...' }); campApi.purchaseSection(that.data.campId, sectionId) .then(function (purchaseRes) { wx.hideLoading(); if (purchaseRes && (purchaseRes.success === true || purchaseRes.code === 200)) { wx.showToast({ title: purchaseRes.message || '已开启小节', icon: 'success' }); // 刷新数据 dataFetcher.fetchCampData(that, that.data.campId); } else { wx.showToast({ title: (purchaseRes && purchaseRes.message) || '开启小节失败', icon: 'none' }); } }) .catch(function (err) { wx.hideLoading(); wx.showToast({ title: '开启小节失败', icon: 'none' }); }); } } }); } return; } }, onHide: function () { // 清除保存的视频恢复状态(页面隐藏说明用户跳转到了其他任务页,不需要恢复) try { wx.removeStorageSync('campVideoPlaybackState'); } catch (e) {} // 暂停视频播放(页面隐藏时立即暂停,避免跳转时出现错误提示) videoController.pauseVideo(this); // 恢复原始 banner 内容(topContent + topContentType 一起恢复,避免类型和内容不匹配导致黑屏) var originalIntroType = this.data.introType || 'INTRO_TYPE_NONE'; this.setData({ showVideo: false, topContent: this.data.backTopContent, topContentType: originalIntroType, isAutoPlay: false, isVideoTaskPlaying: false, currentPlayingTaskId: null, currentPlayingTaskType: null }); }, onReady: function () { // 获取视频上下文 this.videoContext = wx.createVideoContext('bannerVideo', this); }, // 添加全屏事件处理函数 onFullScreenChange: function (e) { var fullScreen = e.detail.fullScreen; var videoContext = this.videoContext; if (fullScreen) { // 进入全屏时,确保视频继续播放 videoContext.play(); // 添加全屏类名 videoContext.addClass('fullscreen'); } else { // 退出全屏时,移除全屏类名 videoContext.removeClass('fullscreen'); } }, startTopVideo: function () { videoController.startTopVideo(this); }, // 自定义导航栏返回按钮点击(与边框滑动返回共用同一套逻辑,见 onPageContainerBeforeLeave) onNavBackTap: function () { var that = this; if (that._handleBackIntent()) { return; } wx.navigateBack({ fail: function () { wx.switchTab({ url: '/pages/index/index' }); } }); }, /** * 统一处理“返回意图”:若正在播放视频任务则退出视频并留在本页,否则允许返回。 * @returns {boolean} true=已处理且留在本页(如退出视频任务),false=应执行 navigateBack */ _handleBackIntent: function () { var that = this; if (that.data.isVideoTaskPlaying && that.data.currentPlayingTaskId) { try { if (that.videoContext) { that.videoContext.pause(); } } catch (e) {} var originalIntroType = that.data.introType || 'INTRO_TYPE_NONE'; var originalIntroContent = that.data.backTopContent || ''; var shouldShowVideo = originalIntroType === 'INTRO_TYPE_VIDEO' && originalIntroContent; that.setData({ showVideo: false }, function () { setTimeout(function () { that.setData({ topContent: originalIntroContent, topContentType: originalIntroType, showVideo: !!shouldShowVideo, isVideoTaskPlaying: false, currentPlayingTaskId: null, currentPlayingTaskType: null, currentSelectedTaskId: null, hasLogged10Percent: false, isAutoPlay: false, _pendingVideoPlay: false, videoTitle: '打卡营介绍' }, function () { if (shouldShowVideo) { that.videoContext = wx.createVideoContext('bannerVideo', that); } }); }, 50); }); wx.showToast({ title: '已退出视频任务', icon: 'none' }); return true; } return false; }, // 边框/右滑返回时由 page-container 触发,与点击返回保持一致逻辑 onPageContainerBeforeLeave: function () { var that = this; if (that._handleBackIntent()) { that.setData({ pageContainerShow: true }); return; } that.setData({ pageContainerShow: false }, function () { wx.navigateBack({ fail: function () { wx.switchTab({ url: '/pages/index/index' }); } }); }); }, onUnload: function () { this.stopUnlockCountdownTimer(); // 清除残留的视频播放状态 try { wx.removeStorageSync('campVideoPlaybackState'); } catch (e) {} // 页面卸载时暂停并清理视频 try { if (this.videoContext) { this.videoContext.pause(); this.videoContext = null; } } catch (e) { } // 清理视频相关状态 this.setData({ showVideo: false, bannerVideoUrl: '', isVideoTaskPlaying: false }); }, // 将剩余秒数格式化为 "X天 HH:MM:SS" 或 "HH:MM:SS";若解锁时间是明日(自然天),则返回固定文案不显示具体倒计时 _formatUnlockCountdown: function (seconds, unlockAtUnix) { if (seconds <= 0) return ''; // 自然天:解锁时间在“明日”则只显示“任务将于明日才能开启” if (unlockAtUnix != null) { var unlockDate = new Date(unlockAtUnix * 1000); var today = new Date(); var tomorrowStart = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1, 0, 0, 0, 0); var tomorrowEnd = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 2, 0, 0, 0, 0) - 1; if (unlockDate.getTime() >= tomorrowStart.getTime() && unlockDate.getTime() < tomorrowEnd) { return '任务将于明日才能开启'; } } var d = Math.floor(seconds / 86400); var h = Math.floor((seconds % 86400) / 3600); var m = Math.floor((seconds % 3600) / 60); var s = seconds % 60; var pad = function (n) { return n < 10 ? '0' + n : String(n); }; var timeStr = pad(h) + ':' + pad(m) + ':' + pad(s); return d > 0 ? d + '天 ' + timeStr : timeStr; }, _unlockCountdownTimerId: null, startUnlockCountdownTimer: function () { var that = this; that.stopUnlockCountdownTimer(); function tick() { var map = that.data.sectionUnlockAtMap || {}; var courseList = that.data.courseList || []; if (Object.keys(map).length === 0) { that.stopUnlockCountdownTimer(); return; } var now = Math.floor(Date.now() / 1000); var nextMap = {}; var texts = []; for (var i = 0; i < courseList.length; i++) { var id = courseList[i].id; var unlockAt = map[id]; if (!unlockAt) { texts.push(''); continue; } var left = unlockAt - now; if (left <= 0) { texts.push(''); continue; } nextMap[id] = unlockAt; var text = that._formatUnlockCountdown(left, unlockAt); texts.push(text === '任务将于明日才能开启' ? text : ('解锁倒计时 ' + text)); } that.setData({ sectionUnlockAtMap: nextMap, sectionUnlockTexts: texts }); if (Object.keys(nextMap).length === 0) { that.stopUnlockCountdownTimer(); } } tick(); that._unlockCountdownTimerId = setInterval(tick, 1000); }, stopUnlockCountdownTimer: function () { if (this._unlockCountdownTimerId) { clearInterval(this._unlockCountdownTimerId); this._unlockCountdownTimerId = null; } }, // 导航到任务页面 navigateToTaskPage: function (e) { taskHandler.navigateToTaskPage(this, e); }, // 执行任务跳转逻辑 executeTaskNavigation: function (campId, sectionId, taskId, taskType, taskData, taskTitle, taskStatus, taskDetail) { taskHandler.executeTaskNavigation(this, campId, sectionId, taskId, taskType, taskData, taskTitle, taskStatus, taskDetail); }, // 监听视频播放进度 请求接口 async onVideoTimeUpdate(e) { // 记录当前播放位置(用于退出时保存状态) this._videoCurrentTime = e.detail.currentTime || 0; await videoController.onVideoTimeUpdate(this, e); }, // 视频任务如何处理 handleVideoTask: function (sendParams) { videoController.handleVideoTask(this, sendParams); }, // 客观题任务处理 handleObjectiveTask: function (sendParams) { taskHandler.handleObjectiveTask(this, sendParams); }, // 主观题任务处理 handleSubjectiveTask: function (sendParams) { taskHandler.handleSubjectiveTask(this, sendParams); }, // 图文任务处理 handleTextImageTask: function (sendParams) { taskHandler.handleTextImageTask(this, sendParams); }, // 申论题任务处理 handleEssayTask: function (sendParams) { taskHandler.handleEssayTask(this, sendParams); }, // 开启打卡营 或者 重启打卡营 toggleCardType: function () { var that = this; if (!this.data.hasJoined) { wx.showModal({ title: '提示', content: '免费开启打卡营😘', success: function (res) { if (res.confirm) { that.joinCamp(); } } }); return; } console.log('this.data.campStatus', this.data.campStatus); console.log('this.data.hasJoined', this.data.hasJoined); // 打卡营进行中时,点击卡片不弹出重启提示 if (this.data.campStatus === 'InProgress' || (this.data.hasJoined && this.data.campStatus === 'NotStarted')) { return; } that.restartCamp(); this.setData({ isImportant: !this.data.isImportant }); }, // 重启打卡营 restartCamp: function () { campService.restartCamp(this); }, // 更新小节状态并刷新数据(已移除 startCourse 接口调用) startCourse: function (sectionId, price) { var that = this; // 更新课程列表中的课程状态(支持多种ID字段名) var sectionIdStr = String(sectionId); var updatedCourseList = this.data.courseList.map(function (item) { var itemId = String(item.id || item.course_id || item.section_id || ''); if (itemId === sectionIdStr) { return Object.assign({}, item, { is_started: true }); } return item; }); that.setData({ courseList: updatedCourseList }); // 更新对应的小节展开状态 var courseIndex = that.data.courseList.findIndex(function (course) { var sectionIdMatch = String(course.id || course.course_id || course.section_id || ''); return sectionIdMatch === sectionIdStr; }); if (courseIndex !== -1) { var sections = that.data.sections.slice(); sections[courseIndex] = Object.assign({}, sections[courseIndex], { expanded: true // 自动展开刚开启的课程 }); that.setData({ sections }); } // 刷新数据以确保任务列表和状态正确 dataFetcher.fetchCampData(that, that.data.campId); }, // 发起支付请求 async requestPayment(sectionId) { var that = this; var paymentRes = await campApi.createCampPayment(this.data.campId, sectionId); if (paymentRes.code == 200) { var params = paymentRes.data.pay_params; var orderId = paymentRes.data.order_id; wx.requestPayment({ nonceStr: params.nonceStr, package: params.package, paySign: params.paySign, timeStamp: params.timeStamp, signType: 'MD5', success: function () { // 支付请求成功,提示用户 wx.showToast({ title: '支付处理中,请稍后查看', icon: 'none', duration: 2000 }); // 延迟刷新数据,等待后端处理支付回调(微信支付回调通常需要几秒) setTimeout(function () { // 刷新数据以检查支付状态 dataFetcher.fetchCampData(that, that.data.campId); }, 3000); }, fail: function (err) { // 用户取消支付时,调用取消支付接口 if (err.errMsg === 'requestPayment:fail cancel') { campApi.cancelCampPayment(that.data.campId, orderId); wx.showToast({ title: '已取消支付', icon: 'none' }); } else { wx.showToast({ title: '支付失败', icon: 'none' }); } } }); } else { wx.showToast({ title: paymentRes.message || '创建支付订单失败', icon: 'none' }); } }, // getScreenSize, generateExpandedStatus, resolveVideoCompletionPercent 已移至 modules/utils.js /** * 更新任务状态(立即更新UI) * @param {String} taskId - 任务ID * @param {String} status - 任务状态 */ updateTaskStatus: function(taskId, status) { utils.updateTaskStatus(this, taskId, status); } });