duidui_mini_program/pages/camp_detail/index.js
2026-03-27 10:41:46 +08:00

1506 lines
49 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 + navBarHeightpage-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);
}
});