670 lines
27 KiB
JavaScript
670 lines
27 KiB
JavaScript
import * as resultService from './modules/result-service.js';
|
||
import { questionApi } from '../../config/question_api.js';
|
||
import { campApi } from '../../config/camp_api.js';
|
||
|
||
// 引入 TWEEN(已注释,改用简单动画)
|
||
// const TWEEN = require('tween.js');
|
||
|
||
Page({
|
||
data: {
|
||
paperId: null,
|
||
taskId: null,
|
||
paperInfo: {
|
||
title: '',
|
||
description: '',
|
||
duration_minutes: 0
|
||
},
|
||
questions: [],
|
||
currentQuestionIndex: 0,
|
||
loading: true,
|
||
answers: [],
|
||
// 材料相关(与答题页保持一致的双层布局)
|
||
materials: [],
|
||
currentQuestionMaterial: null,
|
||
materialScrollHeight: 400,
|
||
currentMaterialIndex: 0,
|
||
// 面板拖拽相关
|
||
swiperHeight: 0,
|
||
panelY: 0,
|
||
panelHeight: 400,
|
||
panelYMin: 0,
|
||
panelYMax: 400,
|
||
movableAreaHeight: 600,
|
||
showFullMaterial: false,
|
||
materialHeight: 400,
|
||
isResizing: false,
|
||
startY: 0,
|
||
startHeight: 500,
|
||
lastHeight: 500,
|
||
pauseStartTime: null,
|
||
currentAnswers: [],
|
||
isMultiple: false,
|
||
optionMarkers: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'],
|
||
optionsClasses: [],
|
||
remainingTime: 0,
|
||
pausedTime: null,
|
||
elapsedSeconds: 0,
|
||
tweenHeight: 500,
|
||
materialStyle: 'transition: none;',
|
||
tagStyle: {
|
||
img: 'max-width: 100% !important; width: auto !important; height: auto !important; display: block !important; margin: 20rpx auto !important;',
|
||
'img.inline': 'display: inline-block !important; vertical-align: middle !important; max-height: 1.5em !important; width: auto !important; margin: 0 4rpx !important;',
|
||
p: 'margin: 0 !important; padding: 0 !important; max-width: 100% !important; box-sizing: border-box !important;',
|
||
table: 'border-collapse: collapse !important; margin: 0 !important; max-width: 100% !important; box-sizing: border-box !important;',
|
||
th: 'border: 1px solid #ccc !important; padding: 4px 8px !important; max-width: 100% !important; box-sizing: border-box !important;',
|
||
td: 'border: 1px solid #ccc !important; padding: 4px 8px !important; max-width: 100% !important; box-sizing: border-box !important;',
|
||
},
|
||
totalPausedTime: 0,
|
||
questionAnsweredStatus: [],
|
||
courseId: null,
|
||
showExplanation: false,
|
||
currentExplanation: '',
|
||
showDetail: false,
|
||
correctCount: 0,
|
||
totalCount: 0,
|
||
correctRate: 0,
|
||
usedTime: '00:00',
|
||
stats: {},
|
||
domain: 'https://your-domain.com',
|
||
},
|
||
|
||
onLoad(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;
|
||
}
|
||
|
||
// 计算主内容区高度、问题面板高度、拖动边界(与答题页一致)
|
||
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);
|
||
const panelYMax = Math.max(panelYMin, h - 80);
|
||
const movableAreaHeight = panelYMax + panelHeight;
|
||
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({
|
||
campId: camp_id,
|
||
paperId: paper_id,
|
||
taskId: task_id,
|
||
taskTitle: decodedTitle || '答题结果',
|
||
courseId: course_id,
|
||
startTime: new Date().getTime(),
|
||
totalPausedTime: 0,
|
||
pauseStartTime: null,
|
||
timerText: '00:00:00',
|
||
loading: true
|
||
});
|
||
|
||
// 添加错误处理
|
||
this.fetchQuestionPaper(paper_id)
|
||
.then(() => {
|
||
// 延迟设置 loading 为 false,确保加载动画有足够时间显示
|
||
setTimeout(() => {
|
||
this.setData({ loading: false });
|
||
}, 500);
|
||
})
|
||
.catch(error => {
|
||
console.error('加载试卷失败:', error);
|
||
wx.showToast({
|
||
title: '加载试卷失败',
|
||
icon: 'none'
|
||
});
|
||
this.setData({ loading: false });
|
||
});
|
||
},
|
||
|
||
// 获取试卷及问题数据
|
||
fetchQuestionPaper(paperId) {
|
||
this.setData({ loading: true });
|
||
|
||
const userId = wx.getStorageSync('wxuserid');
|
||
if (!userId) {
|
||
wx.showToast({
|
||
title: '请先登录',
|
||
icon: 'none'
|
||
});
|
||
return Promise.reject(new Error('用户未登录'));
|
||
}
|
||
|
||
return resultService.fetchQuestionPaper(paperId, this.data.taskId, userId).then(resultData => {
|
||
const isQualified = resultData.stats.correctRate >= 60;
|
||
|
||
// 若无答题记录(如刚重置),应进入答题页而非结果页
|
||
const answeredCount = resultData.stats.answeredCount != null ? resultData.stats.answeredCount : resultData.stats.totalCount;
|
||
if (answeredCount === 0) {
|
||
const q = [
|
||
'paper_id=' + encodeURIComponent(this.data.paperId || paperId),
|
||
'task_id=' + encodeURIComponent(this.data.taskId || ''),
|
||
'camp_id=' + encodeURIComponent(this.data.campId || ''),
|
||
'task_title=' + encodeURIComponent(this.data.taskTitle || ''),
|
||
'course_id=' + encodeURIComponent(this.data.courseId || '')
|
||
].filter(Boolean).join('&');
|
||
wx.redirectTo({
|
||
url: `/pages/camp_task_objective_questions/index?${q}`,
|
||
fail: () => {
|
||
this.setData({
|
||
paperInfo: resultData.paperInfo,
|
||
questions: resultData.questions,
|
||
currentQuestionIndex: 0,
|
||
correctCount: 0,
|
||
totalCount: 0,
|
||
correctRate: '0',
|
||
usedTime: '00:00',
|
||
isQualified: false,
|
||
loading: false
|
||
});
|
||
}
|
||
});
|
||
return resultData;
|
||
}
|
||
|
||
// 处理 paper 级别的材料(与答题页保持一致)
|
||
const materials = resultData.materials || [];
|
||
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)));
|
||
// 根据第一道题目的 material_id 查找对应材料
|
||
const firstQuestion = resultData.questions[0] || null;
|
||
const currentQuestionMaterial = this.findMaterialForQuestion(firstQuestion, materials);
|
||
const actualPanelY = currentQuestionMaterial ? defaultPanelY : 0;
|
||
|
||
this.setData({
|
||
paperInfo: resultData.paperInfo,
|
||
questions: resultData.questions,
|
||
currentQuestionIndex: 0,
|
||
correctCount: resultData.stats.correctCount,
|
||
totalCount: resultData.stats.totalCount,
|
||
correctRate: resultData.stats.correctRate.toFixed(2),
|
||
usedTime: resultData.stats.usedTime,
|
||
isQualified: isQualified,
|
||
materials,
|
||
materialScrollHeight,
|
||
currentQuestionMaterial,
|
||
panelHeight,
|
||
panelY: actualPanelY,
|
||
panelYMin,
|
||
panelYMax,
|
||
movableAreaHeight,
|
||
loading: false
|
||
});
|
||
return resultData;
|
||
}).catch(err => {
|
||
console.error('API调用失败:', err);
|
||
this.setData({ loading: false });
|
||
throw err;
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 根据题目的 material_id 查找对应的材料(与答题页一致)
|
||
*/
|
||
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;
|
||
},
|
||
|
||
/**
|
||
* 切换题目时更新当前题目的材料和面板位置
|
||
*/
|
||
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,
|
||
currentQuestionMaterial,
|
||
panelY: newPanelY
|
||
});
|
||
},
|
||
|
||
// 面板拖拽相关方法(与答题页一致)
|
||
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;
|
||
},
|
||
|
||
// 移除HTML标签的辅助方法
|
||
stripHtmlTags(html) {
|
||
if (!html) return '';
|
||
return html.replace(/<\/?[^>]+(>|$)/g, "");
|
||
},
|
||
|
||
// 材料切换事件
|
||
onMaterialChange(e) {
|
||
this.setData({
|
||
currentMaterialIndex: e.detail.current
|
||
});
|
||
},
|
||
|
||
// 更新选项样式
|
||
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
|
||
});
|
||
},
|
||
|
||
|
||
// 获取选项样式
|
||
getOptionClass(optionIndex) {
|
||
const { currentQuestionIndex, questions, answers } = this.data;
|
||
if (!answers[currentQuestionIndex]) return '';
|
||
|
||
const currentQuestion = questions[currentQuestionIndex];
|
||
const currentAnswers = answers[currentQuestionIndex];
|
||
|
||
if (currentAnswers.includes(optionIndex)) {
|
||
return currentQuestion.type === 'single' ? 'option-selected-single' : 'option-selected-multiple';
|
||
}
|
||
return '';
|
||
},
|
||
|
||
// 题目切换动画开始时
|
||
onQuestionTransition() {
|
||
// 保持为空
|
||
},
|
||
|
||
// 题目切换动画完成时
|
||
onQuestionAnimationFinish(e) {
|
||
const index = e.detail.current;
|
||
this.updateQuestionMaterial(index);
|
||
},
|
||
|
||
|
||
// 修改拖动相关的方法
|
||
startResize(e) {
|
||
const currentQuestion = this.data.questions[this.data.currentQuestionIndex];
|
||
if (!currentQuestion?.materials?.length) return;
|
||
|
||
// 停止之前的动画(已注释 TWEEN,改用简单清理)
|
||
// if (this.tween) {
|
||
// this.tween.stop();
|
||
// this.tween = null;
|
||
// }
|
||
if (this.animationTimer) {
|
||
clearTimeout(this.animationTimer);
|
||
this.animationTimer = null;
|
||
}
|
||
|
||
this.initialY = e.touches[0].clientY;
|
||
this.initialHeight = this.data.materialHeight;
|
||
|
||
if (!this.systemInfo) {
|
||
this.systemInfo = wx.getSystemInfoSync();
|
||
this.rpxRatio = 750 / this.systemInfo.windowWidth;
|
||
}
|
||
|
||
this.setData({
|
||
isResizing: true,
|
||
materialStyle: 'transition: none;'
|
||
});
|
||
},
|
||
|
||
onResize(e) {
|
||
if (!this.data.isResizing) return;
|
||
|
||
if (!this.resizeAnimationFrame) {
|
||
this.resizeAnimationFrame = setTimeout(() => {
|
||
const deltaY = e.touches[0].clientY - this.initialY;
|
||
const newHeight = this.initialHeight + deltaY * this.rpxRatio;
|
||
|
||
// 限制高度范围
|
||
const minHeight = 200;
|
||
const maxHeight = 800;
|
||
const clampedHeight = Math.min(Math.max(newHeight, minHeight), maxHeight);
|
||
|
||
this.setData({
|
||
materialHeight: clampedHeight,
|
||
materialStyle: 'transition: none;'
|
||
});
|
||
|
||
this.resizeAnimationFrame = null;
|
||
}, 16);
|
||
}
|
||
},
|
||
|
||
endResize() {
|
||
if (!this.data.isResizing) return;
|
||
|
||
// 清理 requestAnimationFrame
|
||
if (this.resizeAnimationFrame) {
|
||
clearTimeout(this.resizeAnimationFrame);
|
||
this.resizeAnimationFrame = null;
|
||
}
|
||
|
||
const currentHeight = this.data.materialHeight;
|
||
const targetHeight = Math.round(currentHeight / 50) * 50;
|
||
|
||
// 使用简单动画替代 TWEEN
|
||
this.setData({
|
||
materialHeight: targetHeight,
|
||
materialStyle: 'transition: height 0.3s ease-out;',
|
||
isResizing: false
|
||
});
|
||
|
||
// 动画完成后清理
|
||
setTimeout(() => {
|
||
this.setData({
|
||
materialStyle: '' // 恢复默认样式
|
||
});
|
||
this.initialY = null;
|
||
this.initialHeight = null;
|
||
this.tween = null;
|
||
}, 300);
|
||
},
|
||
|
||
// 在页面卸载时清理所有定时器和状态
|
||
onUnload() {
|
||
// if (this.tween) {
|
||
// this.tween.stop();
|
||
// this.tween = null;
|
||
// }
|
||
if (this.animationTimer) {
|
||
clearTimeout(this.animationTimer);
|
||
this.animationTimer = null;
|
||
}
|
||
if (this.resizeAnimationFrame) {
|
||
clearTimeout(this.resizeAnimationFrame);
|
||
this.resizeAnimationFrame = null;
|
||
}
|
||
this.initialY = null;
|
||
this.initialHeight = null;
|
||
|
||
// if (this.timerInterval) {
|
||
// clearInterval(this.timerInterval);
|
||
// this.timerInterval = null;
|
||
// }
|
||
},
|
||
|
||
// 修改富文本点击处理方法
|
||
onRichTextTap(e) {
|
||
const nodes = e.currentTarget.dataset.content;
|
||
if (!nodes || !Array.isArray(nodes)) return;
|
||
|
||
// 收集所有图片URL
|
||
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 '';
|
||
|
||
// 为行内图片添加 class
|
||
return content.replace(/<img(.*?)>/g, (match, attrs) => {
|
||
// 检查图片是否在段落中且没有换行符
|
||
const isInline = match.includes('style="display: inline') ||
|
||
match.includes('vertical-align: middle');
|
||
|
||
if (isInline) {
|
||
// 如果是行内图片,添加 inline class
|
||
return match.replace('<img', '<img class="inline"');
|
||
}
|
||
return match;
|
||
});
|
||
},
|
||
|
||
// 图片点击事件处理
|
||
onImgTap(e) {
|
||
const { src } = e.detail;
|
||
wx.previewImage({
|
||
current: src,
|
||
urls: [src]
|
||
});
|
||
},
|
||
|
||
onQuestionChange(e) {
|
||
const index = e.detail.current;
|
||
this.updateQuestionMaterial(index);
|
||
},
|
||
// 从答题卡跳转到指定题目
|
||
jumpToQuestion(e) {
|
||
const index = parseInt(e.currentTarget.dataset.index) || 0;
|
||
this.setData({ showDetail: true }, () => {
|
||
this.updateQuestionMaterial(index);
|
||
});
|
||
},
|
||
|
||
|
||
|
||
// 添加 nextQuestion 方法(如果还没有的话)
|
||
nextQuestion: function () {
|
||
if (this.data.currentQuestionIndex < this.data.questions.length - 1) {
|
||
this.setData({
|
||
currentQuestionIndex: 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;
|
||
|
||
// 直接遍历答案数组,检查是否有 true 值
|
||
return answers[index].some(answer => answer === true);
|
||
},
|
||
|
||
// 监听材料区域高度变化
|
||
onMaterialResize(height) {
|
||
// height 参数是rpx值,需要转换为px
|
||
const systemInfo = wx.getSystemInfoSync();
|
||
const pxHeight = height * systemInfo.windowWidth / 750;
|
||
|
||
this.setData({
|
||
materialHeight: pxHeight
|
||
});
|
||
},
|
||
|
||
// 在拖动手柄时实时更新高度
|
||
onResizeMove(e) {
|
||
// ... 现有的拖动逻辑 ...
|
||
const newHeight = this.calculateNewHeight(e);
|
||
this.onMaterialResize(newHeight);
|
||
},
|
||
|
||
// 拖动结束时保存最终高度
|
||
onResizeEnd(e) {
|
||
// ... 现有的结束拖动逻辑 ...
|
||
const finalHeight = this.calculateNewHeight(e);
|
||
this.onMaterialResize(finalHeight);
|
||
},
|
||
|
||
// 重新答题
|
||
retakeQuiz() {
|
||
wx.showModal({
|
||
title: '提示',
|
||
content: '重新答题会清空当前答题记录,是否继续?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
// 获取用户ID
|
||
const userId = wx.getStorageSync('wxuserid');
|
||
if (!userId) {
|
||
wx.showToast({
|
||
title: '请先登录',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 显示加载提示
|
||
wx.showLoading({
|
||
title: '处理中...',
|
||
mask: true
|
||
});
|
||
|
||
// 调用重置任务进度接口
|
||
campApi.resetTaskProgress({
|
||
user_id: String(userId),
|
||
task_id: String(this.data.taskId)
|
||
})
|
||
.then(resetRes => {
|
||
console.log('重置任务进度接口返回:', resetRes);
|
||
|
||
// 判断重置是否成功(兼容多种返回格式)
|
||
const isSuccess = resetRes.success === true ||
|
||
resetRes.code === 200 ||
|
||
(resetRes.data && resetRes.data.success === true);
|
||
|
||
if (isSuccess) {
|
||
wx.hideLoading();
|
||
wx.showToast({
|
||
title: '已清空答题记录',
|
||
icon: 'success',
|
||
duration: 1500
|
||
});
|
||
|
||
// 延迟跳转,确保提示显示
|
||
setTimeout(() => {
|
||
// 构建跳转参数
|
||
const params = {
|
||
paper_id: this.data.paperId,
|
||
task_id: this.data.taskId,
|
||
course_id: this.data.courseId,
|
||
task_title: this.data.taskTitle,
|
||
camp_id: this.data.campId
|
||
};
|
||
const queryString = Object.keys(params)
|
||
.map(key => `${key}=${encodeURIComponent(params[key] || '')}`)
|
||
.join('&');
|
||
|
||
// 跳转到答题页面
|
||
wx.redirectTo({
|
||
url: `/pages/camp_task_objective_questions/index?${queryString}`
|
||
});
|
||
}, 1500);
|
||
} else {
|
||
wx.hideLoading();
|
||
wx.showToast({
|
||
title: resetRes.message || resetRes.msg || '重置失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
})
|
||
.catch(err => {
|
||
console.error('重置任务进度失败:', err);
|
||
wx.hideLoading();
|
||
wx.showToast({
|
||
title: '重置失败,请重试',
|
||
icon: 'none'
|
||
});
|
||
});
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 查看结算
|
||
showSummary() {
|
||
this.setData({
|
||
showDetail: false
|
||
});
|
||
},
|
||
|
||
// 查看全部解析
|
||
showAnalysis() {
|
||
this.setData({ showDetail: true }, () => {
|
||
this.updateQuestionMaterial(0);
|
||
});
|
||
}
|
||
}); |