1506 lines
49 KiB
JavaScript
1506 lines
49 KiB
JavaScript
// 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);
|
||
}
|
||
});
|