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' }); } }, }));