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

794 lines
33 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 * as paperService from './modules/paper-service.js';
import * as timerService from './modules/timer-service.js';
import { questionApi } from '../../config/question_api.js';
import { campApi } from '../../config/camp_api.js';
Page({
data: {
paperId: null,
taskId: null,
paperInfo: {
title: '',
description: '',
duration_minutes: 0
},
questions: [],
currentQuestionIndex: 0,
loading: true,
answers: [],
materials: [],
materialTitle: '资料',
materialTabLabels: [],
materialScrollHeight: 400,
currentMaterialIndex: 0,
currentQuestionMaterial: null, // 当前题目对应的材料(根据题目的 material_id 查找)
panelY: 0,
panelHeight: 400,
panelYMin: 0,
panelYMax: 400,
movableAreaHeight: 600,
showFullMaterial: false,
materialHeight: 400,
isResizing: false,
startY: 0,
startHeight: 200,
minHeight: 200,
maxHeight: 1000,
currentAnswers: [],
isMultiple: false,
optionMarkers: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'],
optionsClasses: [],
materialStyle: '',
tagStyle: {
img: 'max-width: 100%; height: auto;',
'img.inline': 'display: inline-block; vertical-align: middle; max-height: 1.5em;',
p: 'margin: 0; padding: 0;',
table: 'border-collapse: collapse; margin: 0;',
th: 'border: 1px solid #ccc; padding: 4px 8px;',
td: 'border: 1px solid #ccc; padding: 4px 8px;',
},
courseId: null,
showExplanation: false,
currentExplanation: '',
currentQuestion: null,
questionAnsweredStatus: [],
showQuestionCard: false,
startTime: '',
systemInfo: null,
animation: null,
currentHeight: 400,
resizeAnimation: null,
isPaused: false,
timerText: '00:00:00',
pauseStartTime: null,
showCustomModal: false,
pendingSubmit: false,
swiperHeight: 0,
},
onLoad(options) {
// console.log('页面加载参数:', options);
const { paper_id, task_id, course_id, task_title, camp_id } = options;
const decodedTitle = task_title ? decodeURIComponent(task_title) : '';
wx.setNavigationBarTitle({
title: decodedTitle || '客观题'
});
if (!paper_id) {
wx.showToast({
title: '试卷ID不存在',
icon: 'none'
});
return;
}
// 计算主内容区高度、问题面板高度、拖动边界(单位 px
// 内容区全屏,工具栏为固定覆盖层不占位;问题面板可很高(从资料标题到屏幕底)
try {
const sys = wx.getSystemInfoSync();
const w = sys.windowWidth || 375;
const h = Math.max(0, sys.windowHeight);
const materialHeaderPx = Math.round(160 * (w / 750));
const panelYMin = materialHeaderPx;
// 面板高度 = 从资料标题下沿到屏幕底(很高),可上下拖动
const panelHeight = Math.max(200, h - panelYMin);
// 向下拖时至少保留约 80px 面板可见,便于继续拖动;工具栏盖在面板上
const panelYMax = Math.max(panelYMin, h - 80);
const movableAreaHeight = panelYMax + panelHeight;
// 问题面板默认盖住半屏:初始 y 为屏幕高度一半
const defaultPanelY = Math.max(panelYMin, Math.min(panelYMax, Math.round(h / 2)));
this.setData({
swiperHeight: h,
panelHeight,
panelY: defaultPanelY,
panelYMin,
panelYMax,
movableAreaHeight
});
} catch (e) {}
this.setData({
taskTitle: decodedTitle || '客观题',
campId: camp_id,
paperId: paper_id,
taskId: task_id,
courseId: course_id,
loading: true,
answers: [],
questionAnsweredStatus: [],
startTime: new Date().toISOString(),
});
// 加载题目数据(不包含答案和解析)
this.fetchQuestionPaper(paper_id, task_id)
.then(() => {
setTimeout(() => {
this.setData({ loading: false });
}, 300);
})
.catch(error => {
wx.showToast({
title: '加载试卷失败',
icon: 'none'
});
this.setData({ loading: false });
});
},
onReady() {
// 初始化系统信息
this.systemInfo = wx.getSystemInfoSync();
// 创建动画实例
this.animation = wx.createAnimation({
duration: 0,
timingFunction: 'linear'
});
},
/**
* 根据题目的 material_id 查找对应的材料
* @param {Object} question - 题目对象
* @param {Array} materials - 材料数组(可选,默认使用 this.data.materials
* @returns {Object|null} 对应的材料对象,如果没有则返回 null
*/
findMaterialForQuestion(question, materials) {
const mats = materials || this.data.materials || [];
if (!question || !question.material_id) return null;
return mats.find(m => m.id === question.material_id) || null;
},
/**
* 切换题目时更新当前题目的材料和面板位置
* @param {Number} questionIndex - 题目索引
*/
updateQuestionMaterial(questionIndex) {
const question = this.data.questions[questionIndex];
const currentQuestionMaterial = this.findMaterialForQuestion(question);
const { panelYMin, panelYMax, swiperHeight } = this.data;
// 如果当前题目有材料,面板默认在半屏;没有材料则在顶部
const defaultPanelY = Math.max(panelYMin, Math.min(panelYMax, Math.round(swiperHeight / 2)));
const newPanelY = currentQuestionMaterial ? defaultPanelY : 0;
this.setData({
currentQuestionIndex: questionIndex,
currentQuestion: question,
currentQuestionMaterial,
panelY: newPanelY
});
},
fetchQuestionPaper(paperId, taskId) {
return paperService.fetchQuestionPaper(paperId, taskId).then(paperData => {
const materials = paperData.materials || [];
const materialTitle = materials.length <= 1 ? '资料' : '资料一';
const tabLabels = ['资料一', '资料二', '资料三', '资料四', '资料五', '资料六', '资料七', '资料八', '资料九', '资料十'];
const materialTabLabels = (materials.length > 1)
? materials.map((_, i) => tabLabels[i] || ('资料' + (i + 1)))
: [];
const h = this.data.swiperHeight || 400;
const w = (() => { try { return wx.getSystemInfoSync().windowWidth; } catch (e) { return 375; } })();
const titleBarPx = materials.length > 1
? Math.round(140 * (w / 750))
: Math.round(100 * (w / 750));
const materialScrollHeight = materials.length > 0 ? Math.max(0, h - titleBarPx) : h;
const panelYMin = this.data.panelYMin ?? Math.round(160 * (375 / 750));
const panelHeight = Math.max(200, h - panelYMin);
const panelYMax = Math.max(panelYMin, h - 80);
const movableAreaHeight = panelYMax + panelHeight;
// 有材料时问题面板默认盖住半屏
const defaultPanelY = Math.max(panelYMin, Math.min(panelYMax, Math.round(h / 2)));
const panelY = materials.length > 0 ? defaultPanelY : 0;
// 根据第一道题目的 material_id 查找对应材料
const firstQuestion = paperData.questions[0] || null;
const currentQuestionMaterial = this.findMaterialForQuestion(firstQuestion, materials);
// 如果当前题目没有材料,面板从顶部开始
const actualPanelY = currentQuestionMaterial ? defaultPanelY : 0;
this.setData({
paperInfo: paperData.paperInfo,
questions: paperData.questions,
answers: paperData.answers,
questionAnsweredStatus: paperData.questionAnsweredStatus,
currentQuestionIndex: 0,
currentQuestion: firstQuestion,
materials,
materialTitle,
materialTabLabels,
materialScrollHeight,
currentMaterialIndex: 0,
currentQuestionMaterial,
panelHeight,
panelY: actualPanelY,
panelYMin,
panelYMax,
movableAreaHeight,
loading: false
});
if (!this.data.taskTitle && paperData.paperInfo && paperData.paperInfo.title) {
wx.setNavigationBarTitle({ title: paperData.paperInfo.title });
}
// 试卷加载完成后启动计时器
const durationMinutes = paperData.paperInfo ? (paperData.paperInfo.duration_minutes || 0) : 0;
if (durationMinutes > 0) {
timerService.startTimer(this, durationMinutes, this.data.startTime, () => {
wx.showModal({
title: '时间到',
content: '答题时间已到,将自动提交',
showCancel: false,
success: () => {
this.submitAnswers();
}
});
});
} else {
// 如果没有时长限制,使用简单的计时器(显示已用时间)
this.startSimpleTimer();
}
});
},
stripHtmlTags(html) {
if (!html) return '';
return html.replace(/<\/?[^>]+(>|$)/g, "");
},
onMaterialChange(e) {
const idx = e.detail.current;
const labels = (this.data.materialTabLabels || []);
const materialTitle = labels[idx] || ('资料' + (idx + 1));
this.setData({
currentMaterialIndex: idx,
materialTitle
});
},
onMaterialTabTap(e) {
const index = parseInt(e.currentTarget.dataset.index, 10) || 0;
const labels = (this.data.materialTabLabels || []);
const materialTitle = labels[index] || ('资料' + (index + 1));
this.setData({
currentMaterialIndex: index,
materialTitle
});
},
updateOptionsClasses() {
const { questions, answers } = this.data;
const newOptionsClasses = questions.map((question, qIndex) => {
const questionAnswers = answers[qIndex] || [];
return question.options.map((_, optIndex) =>
questionAnswers.includes(optIndex) ? 'option-selected-single' : ''
);
});
this.setData({
optionsClasses: newOptionsClasses
});
},
handleOptionClick(e) {
// 处理组件触发的事件
const { optionIndex, questionIndex } = e.detail;
const currentQuestion = this.data.questions[questionIndex];
const isSingleChoice = currentQuestion.type === 'single_choice';
const isLastQuestion = questionIndex === this.data.questions.length - 1;
let newAnswers = [...this.data.answers];
if (!newAnswers[questionIndex]) {
newAnswers[questionIndex] = new Array(currentQuestion.options.length).fill(false);
}
if (isSingleChoice) {
newAnswers[questionIndex] = newAnswers[questionIndex].map((_, i) => i === optionIndex);
} else {
newAnswers[questionIndex][optionIndex] = !newAnswers[questionIndex][optionIndex];
}
let newQuestionAnsweredStatus = [...this.data.questionAnsweredStatus];
newQuestionAnsweredStatus[questionIndex] = true;
this.setData({
answers: newAnswers,
questionAnsweredStatus: newQuestionAnsweredStatus
}, () => {
if (isSingleChoice) {
if (isLastQuestion) {
setTimeout(() => {
this.setData({
showQuestionCard: true
});
}, 300);
} else {
const nextIndex = questionIndex + 1;
this.updateQuestionMaterial(nextIndex);
}
}
});
},
getOptionClass(index) {
const { currentQuestionIndex, answers } = this.data;
if (!answers || !answers[currentQuestionIndex]) return '';
return answers[currentQuestionIndex][index] ? 'option-selected' : '';
},
onQuestionTransition() {
// 保持为空
},
onQuestionAnimationFinish(e) {
const index = e.detail.current;
this.setData({
currentQuestionIndex: index,
currentMaterialIndex: 0
});
},
// 简单计时器(显示已用时间,无时长限制)
startSimpleTimer() {
if (this.timerInterval) {
clearInterval(this.timerInterval);
}
this.timerInterval = setInterval(() => {
if (!this.data.isPaused) {
const startTime = new Date(this.data.startTime);
const currentTime = new Date();
const elapsedTime = Math.floor((currentTime - startTime) / 1000);
const hours = Math.floor(elapsedTime / 3600);
const minutes = Math.floor((elapsedTime % 3600) / 60);
const seconds = elapsedTime % 60;
const formattedTime = [
hours.toString().padStart(2, '0'),
minutes.toString().padStart(2, '0'),
seconds.toString().padStart(2, '0')
].join(':');
this.setData({
timerText: formattedTime
});
}
}, 1000);
},
togglePause() {
const newPausedState = !this.data.isPaused;
// 更新暂停状态,控制遮罩显示/隐藏
this.setData({ isPaused: newPausedState });
if (newPausedState) {
// 暂停计时器
const pausedSeconds = timerService.pauseTimer(this);
this._pausedSeconds = pausedSeconds;
} else {
// 恢复计时器
if (this._pausedSeconds !== undefined) {
const durationMinutes = this.data.paperInfo ? (this.data.paperInfo.duration_minutes || 0) : 0;
if (durationMinutes > 0) {
timerService.resumeTimer(this, this._pausedSeconds, () => {
wx.showModal({
title: '时间到',
content: '答题时间已到,将自动提交',
showCancel: false,
success: () => {
this.submitAnswers();
}
});
});
} else {
// 简单计时器恢复
this.startSimpleTimer();
}
}
}
},
showAnswerCard: function () {
// 实现显示答题卡逻辑
},
nextQuestion: function () {
if (this.data.currentQuestionIndex < this.data.questions.length - 1) {
this.updateQuestionMaterial(this.data.currentQuestionIndex + 1);
}
},
onAnswersChange: function () {
console.log('Current answers state:', this.data.answers);
},
isOptionSelected: function (questionIndex, optionIndex) {
const answers = this.data.answers[questionIndex] || [];
return answers.indexOf(optionIndex) !== -1;
},
isQuestionAnswered: function (index) {
const answers = this.data.answers;
console.log('answers:', answers);
if (!answers || !answers[index]) return false;
return answers[index].some(answer => answer === true);
},
onMaterialResizeEnd(e) {
this.setData({
currentMaterialIndex: e.detail.current
});
},
onRichTextTap(e) {
const nodes = e.currentTarget.dataset.content;
if (!nodes || !Array.isArray(nodes)) return;
const images = nodes
.filter(node => node.name === 'img')
.map(node => node.attrs.src)
.filter(url => url && url.startsWith('http'));
if (images.length > 0) {
wx.previewImage({
current: images[0],
urls: images
});
}
},
previewImage(e) {
const { url, urls } = e.currentTarget.dataset;
wx.previewImage({
current: url,
urls: urls || [url]
});
},
onMaterialImageTap(e) {
const url = e.currentTarget.dataset.url;
if (url) {
wx.previewImage({
current: url,
urls: [url]
});
}
},
processRichText(content) {
if (!content) return '';
return content.replace(/<img(.*?)>/g, (match, attrs) => {
const isInline = match.includes('style="display: inline') ||
match.includes('vertical-align: middle');
if (isInline) {
return match.replace('<img', '<img class="inline"');
}
return match;
});
},
onImgTap(e) {
const { src } = e.detail;
if (src) {
wx.previewImage({
current: src,
urls: [src]
});
}
},
onQuestionChange(e) {
const index = e.detail.current;
this.updateQuestionMaterial(index);
},
/** 题目区域内横向滑动时由 question-display 触发(因 catch 导致 swiper 收不到触摸),在此切换题目 */
onQuestionSwipeHorizontal(e) {
const dir = e.detail && e.detail.direction;
if (!dir) return;
const cur = this.data.currentQuestionIndex;
const total = (this.data.questions || []).length;
let next = dir === 'left' ? cur + 1 : cur - 1;
next = Math.max(0, Math.min(total - 1, next));
if (next !== cur) {
this.updateQuestionMaterial(next);
}
},
showQuestionCard() {
this.setData({
showQuestionCard: true
});
},
hideQuestionCard() {
this.setData({
showQuestionCard: false
});
},
jumpToQuestion(e) {
const index = parseInt(e.currentTarget.dataset.index) || 0;
this.updateQuestionMaterial(index);
this.setData({ showQuestionCard: false });
},
onPanelDragStart(e) {
if (!e.touches || !e.touches.length) return;
this._panelDragStartY = e.touches[0].clientY;
this._panelDragStartPanelY = this.data.panelY;
},
onPanelDragMove(e) {
if (!e.touches || !e.touches.length || this._panelDragStartY == null) return;
const { panelYMin, panelYMax } = this.data;
const deltaY = e.touches[0].clientY - this._panelDragStartY;
let newY = this._panelDragStartPanelY + deltaY;
newY = Math.max(panelYMin, Math.min(panelYMax, newY));
this.setData({ panelY: newY });
},
onPanelDragEnd(e) {
this._panelDragStartY = null;
this._panelDragStartPanelY = null;
},
showAnswerCard: function () {
// 实现显示答题卡逻辑
},
nextQuestion: function () {
if (this.data.currentQuestionIndex < this.data.questions.length - 1) {
this.updateQuestionMaterial(this.data.currentQuestionIndex + 1);
}
},
onAnswersChange: function () {
console.log('Current answers state:', this.data.answers);
},
isOptionSelected: function (questionIndex, optionIndex) {
const answers = this.data.answers[questionIndex] || [];
return answers.indexOf(optionIndex) !== -1;
},
isQuestionAnswered: function (index) {
const answers = this.data.answers;
console.log('answers:', answers);
if (!answers || !answers[index]) return false;
return answers[index].some(answer => answer === true);
},
submitAnswers() {
// 先关闭自定义答题卡弹窗
this.setData({
// showQuestionCard: false,
isPaused: false
});
// 检查是否有未作答的题目
const unanswered = this.data.answers.some(arr => !arr || arr.every(v => !v));
// 只定义 doSubmit不直接调用
const doSubmit = () => {
if (this.data.pendingSubmit) return;
this.setData({ pendingSubmit: true });
// 获取用户ID
const userId = wx.getStorageSync('wxuserid');
if (!userId) {
wx.showToast({
title: '请先登录',
icon: 'none'
});
this.setData({ pendingSubmit: false });
return;
}
// 计算开始时间和结束时间Unix时间戳
const startTimeDate = new Date(this.data.startTime);
const endTimeDate = new Date();
const startTimeSeconds = Math.floor(startTimeDate.getTime() / 1000); // 开始答题的时间戳(秒)
const endTimeSeconds = Math.floor(endTimeDate.getTime() / 1000); // 结束答题的时间戳(秒)
// 格式化答案数组
const answers = this.data.questions.map((question, index) => {
const answerArray = this.data.answers[index] || [];
const userAnswer = answerArray.reduce((acc, curr, idx) => {
if (curr) {
acc.push(this.data.optionMarkers[idx]);
}
return acc;
}, []).join(',');
return {
question_id: String(question.id),
user_answer: userAnswer || ''
};
});
const submitData = {
user_id: String(userId),
paper_id: String(this.data.paperId),
task_id: String(this.data.taskId || ''),
answers: answers,
start_time: startTimeSeconds,
end_time: endTimeSeconds
};
if (!submitData.paper_id) {
wx.showToast({
title: '参数错误',
icon: 'none'
});
this.setData({ pendingSubmit: false });
return;
}
// 先提交答题记录
questionApi.createAnswerRecord(submitData)
.then(res => {
if (res.code === 200 || res.success === true) {
// 客观题完成只看正确率(正确数/试卷总题数),由后端判断是否达标并保留最高正确率
const totalQuestions = (this.data.questions && this.data.questions.length) || 0;
const correctCount = (res.correct_count != null) ? Number(res.correct_count) : 0;
const progressData = {
user_id: String(userId),
task_id: String(this.data.taskId),
is_completed: true,
completed_at: String(endTimeSeconds),
objective_correct_count: correctCount,
objective_total_count: totalQuestions
};
return campApi.updateCampProgress(progressData)
.then(progressRes => {
wx.showToast({
title: '提交成功',
icon: 'success'
});
console.log('提交成功', this.data);
// 跳转到结果页面
setTimeout(() => {
const query = [];
query.push('paper_id=' + encodeURIComponent(this.data.paperId));
query.push('task_id=' + encodeURIComponent(this.data.taskId));
query.push('camp_id=' + encodeURIComponent(this.data.campId || ''));
query.push('task_title=' + encodeURIComponent(this.data.taskTitle || '客观题'));
if (this.data.courseId) {
query.push('course_id=' + encodeURIComponent(this.data.courseId));
}
wx.redirectTo({
url: '/pages/camp_task_objective_questions_result/index?' + query.join('&')
});
}, 1500);
this.setData({ pendingSubmit: false });
})
.catch(progressErr => {
// 答题记录已提交,但进度更新失败,仍然提示成功
wx.showToast({
title: '提交成功',
icon: 'success'
});
console.warn('答题记录提交成功,但进度更新失败:', progressErr);
// 跳转到结果页面
setTimeout(() => {
const query = [];
query.push('paper_id=' + encodeURIComponent(this.data.paperId));
query.push('task_id=' + encodeURIComponent(this.data.taskId));
query.push('camp_id=' + encodeURIComponent(this.data.campId || ''));
query.push('task_title=' + encodeURIComponent(this.data.taskTitle || '客观题'));
if (this.data.courseId) {
query.push('course_id=' + encodeURIComponent(this.data.courseId));
}
wx.redirectTo({
url: '/pages/camp_task_objective_questions_result/index?' + query.join('&')
});
}, 1500);
this.setData({ pendingSubmit: false });
});
} else {
wx.showToast({
title: res.message || res.msg || '提交失败',
icon: 'none'
});
this.setData({ pendingSubmit: false });
}
})
.catch(err => {
wx.showToast({
title: '提交失败',
icon: 'none'
});
this.setData({ pendingSubmit: false });
console.error('提交答案失败:', err);
});
};
// 保存 doSubmit 到 this供自定义弹窗确认按钮调用
this.doSubmit = doSubmit;
if (unanswered) {
setTimeout(() => {
this.setData({ showCustomModal: true });
}, 200);
} else {
doSubmit();
}
},
// 自定义弹窗的确认
onCustomModalConfirm() {
this.setData({ showCustomModal: false });
setTimeout(() => {
if (typeof this.doSubmit === 'function') {
this.doSubmit();
}
}, 200);
},
// 自定义弹窗的取消
onCustomModalCancel() {
this.setData({ showCustomModal: false });
},
startResize(e) {
const touch = e.touches[0];
this.startY = touch.clientY;
this.startHeight = this.data.materialHeight;
this.setData({ isResizing: true });
},
onResize(e) {
if (!this.data.isResizing) return;
const touch = e.touches[0];
const deltaY = touch.clientY - this.startY;
let newHeight = this.startHeight + deltaY;
newHeight = Math.max(this.data.minHeight, Math.min(newHeight, this.data.maxHeight));
this.setData({ materialHeight: newHeight });
},
endResize() {
if (!this.data.isResizing) return;
const presetHeights = [200, 300, 400, 500, 600, 700, 800, 900, 1000];
const currentHeight = this.data.materialHeight;
const targetHeight = presetHeights.reduce((prev, curr) =>
Math.abs(curr - currentHeight) < Math.abs(prev - currentHeight) ? curr : prev
);
this.setData({
materialHeight: targetHeight,
isResizing: false
});
},
onUnload() {
timerService.stopTimer(this);
},
});