284 lines
8.3 KiB
JavaScript
284 lines
8.3 KiB
JavaScript
import { createPage, request } from '../../utils/base-page.js';
|
||
import { refreshDashouMembership } from '../../utils/dashou-profile.js';
|
||
import { isApiSuccess, getApiMsg } from '../../utils/api-helper.js';
|
||
|
||
const LABELS = ['A', 'B', 'C', 'D', 'E', 'F'];
|
||
|
||
function toSlotNum(slot) {
|
||
const n = Number(slot);
|
||
return Number.isFinite(n) ? n : slot;
|
||
}
|
||
|
||
function normalizeSlots(slots) {
|
||
return (slots || []).map(toSlotNum);
|
||
}
|
||
|
||
function slotsEqual(a, b) {
|
||
const sa = normalizeSlots(a).sort((x, y) => x - y);
|
||
const sb = normalizeSlots(b).sort((x, y) => x - y);
|
||
return sa.length === sb.length && sa.every((v, i) => v == sb[i]);
|
||
}
|
||
|
||
function shuffle(arr) {
|
||
const a = [...arr];
|
||
for (let i = a.length - 1; i > 0; i--) {
|
||
const j = Math.floor(Math.random() * (i + 1));
|
||
[a[i], a[j]] = [a[j], a[i]];
|
||
}
|
||
return a;
|
||
}
|
||
|
||
Page(createPage({
|
||
data: {
|
||
loading: true,
|
||
alreadyPassed: false,
|
||
finished: false,
|
||
blocked: false,
|
||
blockTitle: '',
|
||
blockReason: '',
|
||
needRecharge: false,
|
||
questions: [],
|
||
currentIndex: 0,
|
||
currentQuestion: null,
|
||
questionType: 1,
|
||
total: 0,
|
||
wrongCount: 0,
|
||
maxWrong: 0,
|
||
correctCount: 0,
|
||
selectedSlots: [],
|
||
showExplanation: false,
|
||
},
|
||
|
||
onLoad() {
|
||
this.initExam();
|
||
},
|
||
|
||
showBlocked(title, reason, needRecharge = false) {
|
||
this.setData({
|
||
loading: false,
|
||
blocked: true,
|
||
blockTitle: title,
|
||
blockReason: reason,
|
||
needRecharge: !!needRecharge,
|
||
});
|
||
},
|
||
|
||
/** 进入页先查 status:不满足条件留在本页提示,不自动退出 */
|
||
async initExam() {
|
||
this.setData({ loading: true, blocked: false, needRecharge: false });
|
||
try {
|
||
await refreshDashouMembership(getApp());
|
||
const res = await request({ url: '/jituan/dashou-exam/status', method: 'POST' });
|
||
const body = res?.data;
|
||
if (!isApiSuccess(body)) {
|
||
this.showBlocked('无法开始考试', getApiMsg(body) || '无法获取考试状态,请稍后重试');
|
||
return;
|
||
}
|
||
const d = body.data || {};
|
||
if (!d.exam_enabled) {
|
||
this.showBlocked(
|
||
'未开启考试',
|
||
'本俱乐部尚未开启接单考试。若后台已开启,请确认配置的是当前小程序对应俱乐部。'
|
||
);
|
||
return;
|
||
}
|
||
if (d.exam_passed) {
|
||
this.setData({ loading: false, alreadyPassed: true });
|
||
return;
|
||
}
|
||
const cfg = d.config || {};
|
||
if (cfg.require_member && !d.has_member) {
|
||
this.showBlocked('须先开通会员', '本俱乐部要求开通会员后才能参加考试', true);
|
||
return;
|
||
}
|
||
if ((d.pool_count || 0) < 1) {
|
||
this.showBlocked(
|
||
'暂无可用考题',
|
||
'考试已开启,但题库没有「已上架」的题目。请在后台确认题目已上架,且每题至少两个选项并标记正确答案。'
|
||
);
|
||
return;
|
||
}
|
||
await this.loadBundle();
|
||
} catch (e) {
|
||
this.showBlocked('网络错误', '请检查网络后下拉重新进入本页');
|
||
}
|
||
},
|
||
|
||
async loadBundle() {
|
||
this.setData({ loading: true, finished: false, showExplanation: false, blocked: false });
|
||
try {
|
||
const res = await request({ url: '/jituan/dashou-exam/bundle', method: 'POST' });
|
||
const body = res?.data;
|
||
if (!isApiSuccess(body)) {
|
||
this.showBlocked('加载考题失败', getApiMsg(body) || '请稍后重试或联系管理员');
|
||
return;
|
||
}
|
||
const d = body.data || {};
|
||
if (d.already_passed) {
|
||
this.setData({ alreadyPassed: true, loading: false });
|
||
return;
|
||
}
|
||
const cfg = d.config || {};
|
||
const prepared = (d.questions || []).map((q) => this.prepareQuestion(q)).filter((q) => (q.displayOptions || []).length > 0);
|
||
if (!prepared.length) {
|
||
this.showBlocked(
|
||
'题目配置不完整',
|
||
'已抽到的题目缺少有效选项,请在后台检查题目是否至少两个选项并标记正确答案。'
|
||
);
|
||
return;
|
||
}
|
||
this.setData({
|
||
loading: false,
|
||
questions: prepared,
|
||
total: prepared.length,
|
||
currentIndex: 0,
|
||
wrongCount: 0,
|
||
correctCount: 0,
|
||
maxWrong: cfg.max_wrong || 0,
|
||
currentQuestion: prepared[0],
|
||
questionType: prepared[0].question_type || 1,
|
||
selectedSlots: [],
|
||
});
|
||
} catch (e) {
|
||
this.showBlocked('网络错误', '拉取考题失败,请稍后重试');
|
||
}
|
||
},
|
||
|
||
prepareQuestion(q) {
|
||
const oss = getApp().globalData.ossImageUrl || '';
|
||
const displayImages = (q.images || []).map((u) => (u.startsWith('http') ? u : oss + u));
|
||
const shuffled = shuffle(q.options || []).map((opt, idx) => ({
|
||
slot: toSlotNum(opt.slot),
|
||
text: opt.text,
|
||
label: LABELS[idx] || String(idx + 1),
|
||
selected: false,
|
||
}));
|
||
return {
|
||
...q,
|
||
question_type: Number(q.question_type) || 1,
|
||
displayImages,
|
||
displayOptions: shuffled,
|
||
correctSlots: normalizeSlots(q.correct_slots || []),
|
||
};
|
||
},
|
||
|
||
applySelectedToOptions(selectedSlots) {
|
||
const q = this.data.currentQuestion;
|
||
if (!q) return;
|
||
const set = normalizeSlots(selectedSlots);
|
||
const displayOptions = (q.displayOptions || []).map((opt) => ({
|
||
...opt,
|
||
selected: set.some((s) => s == opt.slot),
|
||
}));
|
||
this.setData({
|
||
selectedSlots: set,
|
||
currentQuestion: { ...q, displayOptions },
|
||
});
|
||
},
|
||
|
||
onSelectOption(e) {
|
||
const slot = toSlotNum(e.currentTarget.dataset.slot);
|
||
const q = this.data.currentQuestion;
|
||
if (!q || slot === undefined || slot === null || slot === '') return;
|
||
if (q.question_type === 2) {
|
||
const set = [...this.data.selectedSlots];
|
||
const i = set.findIndex((s) => s == slot);
|
||
if (i >= 0) set.splice(i, 1);
|
||
else set.push(slot);
|
||
if (this.data.showExplanation) {
|
||
this.setData({ showExplanation: false });
|
||
}
|
||
this.applySelectedToOptions(set);
|
||
return;
|
||
}
|
||
this.applySelectedToOptions([slot]);
|
||
if (this.data.showExplanation) {
|
||
this.setData({ showExplanation: false });
|
||
}
|
||
setTimeout(() => this.gradeAnswer([slot]), 120);
|
||
},
|
||
|
||
confirmMulti() {
|
||
if (this.data.showExplanation) {
|
||
this.setData({ showExplanation: false });
|
||
this.applySelectedToOptions([]);
|
||
return;
|
||
}
|
||
if (!this.data.selectedSlots.length) {
|
||
wx.showToast({ title: '请选择答案', icon: 'none' });
|
||
return;
|
||
}
|
||
this.gradeAnswer([...this.data.selectedSlots]);
|
||
},
|
||
|
||
/** 前端本地判分(bundle 已含正确答案与解析) */
|
||
gradeAnswer(selected) {
|
||
const q = this.data.currentQuestion;
|
||
const correct = q.correctSlots || [];
|
||
const isRight = slotsEqual(selected, correct);
|
||
|
||
if (!isRight) {
|
||
const wrongCount = this.data.wrongCount + 1;
|
||
this.setData({ wrongCount, showExplanation: true });
|
||
if (wrongCount > this.data.maxWrong) {
|
||
wx.showModal({
|
||
title: '答题失败',
|
||
content: `${q.explanation || '本题答错了'}。错题过多,将重新开始本轮考试。`,
|
||
showCancel: false,
|
||
success: () => this.loadBundle(),
|
||
});
|
||
} else {
|
||
wx.showToast({ title: '答错了,请查看解析', icon: 'none' });
|
||
}
|
||
return;
|
||
}
|
||
|
||
const correctCount = this.data.correctCount + 1;
|
||
const next = this.data.currentIndex + 1;
|
||
if (next >= this.data.total) {
|
||
this.submitPassed(correctCount);
|
||
return;
|
||
}
|
||
const nextQ = this.data.questions[next];
|
||
this.setData({
|
||
correctCount,
|
||
currentIndex: next,
|
||
currentQuestion: nextQ,
|
||
questionType: nextQ.question_type,
|
||
selectedSlots: [],
|
||
showExplanation: false,
|
||
});
|
||
},
|
||
|
||
async submitPassed(correctCount) {
|
||
try {
|
||
const res = await request({ url: '/jituan/dashou-exam/mark-passed', method: 'POST' });
|
||
const body = res?.data;
|
||
if (isApiSuccess(body)) {
|
||
this.setData({ finished: true, correctCount });
|
||
} else {
|
||
wx.showToast({ title: getApiMsg(body) || '提交失败', icon: 'none' });
|
||
}
|
||
} catch (e) {
|
||
wx.showToast({ title: '提交失败', icon: 'none' });
|
||
}
|
||
},
|
||
|
||
goRecharge() {
|
||
wx.navigateTo({ url: '/pages/fighter-recharge/fighter-recharge' });
|
||
},
|
||
|
||
retryInit() {
|
||
this.initExam();
|
||
},
|
||
|
||
goBack() {
|
||
const pages = getCurrentPages();
|
||
if (pages.length > 1) {
|
||
wx.navigateBack();
|
||
} else {
|
||
wx.switchTab({ url: '/pages/accept-order/accept-order' });
|
||
}
|
||
},
|
||
}));
|