330 lines
9.9 KiB
JavaScript
330 lines
9.9 KiB
JavaScript
// pages/test/mbti.js - MBTI测试页面逻辑
|
||
const { loadQuestions } = require('../../utils/questionBank')
|
||
const { mbtiDescriptions } = require('../../utils/descriptions')
|
||
const payment = require('../../utils/payment')
|
||
const { afterTestSubmitNavigate } = require('../../utils/phoneAuth')
|
||
const app = getApp()
|
||
|
||
const MBTI_TIME_SEC = 30 * 60 // 30 分钟
|
||
|
||
Page({
|
||
data: {
|
||
loading: true,
|
||
loadError: false,
|
||
loadErrorMsg: '',
|
||
usingLocalFallback: false,
|
||
questions: [],
|
||
currentIndex: 0,
|
||
currentQuestion: null,
|
||
answers: {},
|
||
selectedAnswer: null,
|
||
total: 0,
|
||
answeredCount: 0,
|
||
progress: 0,
|
||
timeRemaining: MBTI_TIME_SEC,
|
||
_initialSeconds: MBTI_TIME_SEC,
|
||
formatTime: '30:00',
|
||
isSubmitting: false,
|
||
canAccess: false
|
||
},
|
||
|
||
timer: null,
|
||
|
||
onLoad(options) {
|
||
try {
|
||
require('../../utils/thirdPartyContext.js').ingestThirdPartyOnPageLoad(options || {}, app)
|
||
} catch (e) {}
|
||
this._loadOptions = options || {}
|
||
this._startLoadQuestions()
|
||
},
|
||
|
||
retryLoad() {
|
||
this._startLoadQuestions()
|
||
},
|
||
|
||
/**
|
||
* 登录与拉题解耦:即使登录失败,仍尝试拉题(线上失败会走 401/network 降级本地)
|
||
*/
|
||
_startLoadQuestions() {
|
||
this.setData({ loading: true, loadError: false, loadErrorMsg: '', usingLocalFallback: false })
|
||
// 先尝试登录,但不阻塞题库;拉题失败会降级本地 fallback
|
||
const ensure = (typeof app.ensureLogin === 'function')
|
||
? app.ensureLogin().catch(() => false)
|
||
: Promise.resolve(false)
|
||
ensure
|
||
.then(() => loadQuestions('mbti', { allowLocalFallback: true }))
|
||
.then((questions) => {
|
||
if (!questions || !questions.length) {
|
||
this.setData({ loading: false, loadError: true, loadErrorMsg: '暂无题目,请稍后再试' })
|
||
return
|
||
}
|
||
const total = questions.length
|
||
// 判断是否为本地题(id 以 'L-' 前缀)
|
||
const isLocal = !!(questions[0] && typeof questions[0].id === 'string' && questions[0].id.indexOf('L-') === 0)
|
||
this.setData({
|
||
loading: false,
|
||
loadError: false,
|
||
usingLocalFallback: isLocal,
|
||
questions,
|
||
currentQuestion: questions[0],
|
||
canAccess: true,
|
||
total,
|
||
progress: Math.round((1 / total) * 100),
|
||
timeRemaining: MBTI_TIME_SEC,
|
||
_initialSeconds: MBTI_TIME_SEC,
|
||
formatTime: '30:00'
|
||
})
|
||
try {
|
||
require('../../utils/analytics').track('test_start', { type: 'mbti', total, source: isLocal ? 'local' : 'remote' })
|
||
} catch (e) {}
|
||
try {
|
||
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
|
||
} catch (e) {}
|
||
this.startTimer()
|
||
})
|
||
.catch((err) => {
|
||
const msg = (err && err.message) || '加载失败,请检查网络后重试'
|
||
this.setData({ loading: false, loadError: true, loadErrorMsg: msg })
|
||
})
|
||
},
|
||
|
||
// 检查访问权限
|
||
checkAccess() {
|
||
// 当前策略:所有测试免费开放,直接允许访问
|
||
// 若后续恢复收费,可重新启用 payment.canTakeTest 等校验逻辑
|
||
return true
|
||
},
|
||
|
||
onUnload() {
|
||
if (this._advanceTimer) {
|
||
clearTimeout(this._advanceTimer)
|
||
this._advanceTimer = null
|
||
}
|
||
if (this.timer) {
|
||
clearInterval(this.timer)
|
||
}
|
||
},
|
||
|
||
// 启动计时器
|
||
startTimer() {
|
||
this.timer = setInterval(() => {
|
||
let time = this.data.timeRemaining - 1
|
||
if (time <= 0) {
|
||
clearInterval(this.timer)
|
||
this.submitTest({ allowIncomplete: true })
|
||
return
|
||
}
|
||
const minutes = Math.floor(time / 60)
|
||
const seconds = time % 60
|
||
this.setData({
|
||
timeRemaining: time,
|
||
formatTime: `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
||
})
|
||
}, 1000)
|
||
},
|
||
|
||
// 选择答案:防抖 + 过期回调校验,避免连点导致重复 next 漏题
|
||
selectAnswer(e) {
|
||
if (this._advanceTimer) {
|
||
clearTimeout(this._advanceTimer)
|
||
this._advanceTimer = null
|
||
}
|
||
const value = e.currentTarget.dataset.value
|
||
const cq = this.data.currentQuestion
|
||
if (value == null || !cq || cq.id == null) return
|
||
|
||
const questionId = cq.id
|
||
const idx = this.data.currentIndex
|
||
const tot = this.data.total
|
||
|
||
const answers = { ...this.data.answers }
|
||
answers[questionId] = value
|
||
const answeredCount = Object.keys(answers).length
|
||
const progress = tot ? Math.round(((idx + 1) / tot) * 100) : 0
|
||
|
||
this.setData(
|
||
{
|
||
selectedAnswer: value,
|
||
answers,
|
||
answeredCount,
|
||
progress
|
||
},
|
||
() => {
|
||
this._advanceTimer = setTimeout(() => {
|
||
this._advanceTimer = null
|
||
const d = this.data
|
||
if (d.currentIndex !== idx || !d.currentQuestion || d.currentQuestion.id !== questionId) return
|
||
if (idx < tot - 1) {
|
||
this.nextQuestion()
|
||
} else {
|
||
this.submitTest()
|
||
}
|
||
}, 320)
|
||
}
|
||
)
|
||
},
|
||
|
||
// 上一题
|
||
prevQuestion() {
|
||
if (this._advanceTimer) {
|
||
clearTimeout(this._advanceTimer)
|
||
this._advanceTimer = null
|
||
}
|
||
if (this.data.currentIndex > 0) {
|
||
const newIndex = this.data.currentIndex - 1
|
||
const newQuestion = this.data.questions[newIndex]
|
||
const tot = this.data.total
|
||
this.setData({
|
||
currentIndex: newIndex,
|
||
currentQuestion: newQuestion,
|
||
selectedAnswer: this.data.answers[newQuestion.id] || null,
|
||
progress: tot ? Math.round(((newIndex + 1) / tot) * 100) : 0
|
||
})
|
||
}
|
||
},
|
||
|
||
// 下一题(跳过:不记录答案)
|
||
nextQuestion() {
|
||
if (this.data.currentIndex < this.data.total - 1) {
|
||
const newIndex = this.data.currentIndex + 1
|
||
const newQuestion = this.data.questions[newIndex]
|
||
const tot = this.data.total
|
||
this.setData({
|
||
currentIndex: newIndex,
|
||
currentQuestion: newQuestion,
|
||
selectedAnswer: this.data.answers[newQuestion.id] || null,
|
||
progress: tot ? Math.round(((newIndex + 1) / tot) * 100) : 0
|
||
})
|
||
}
|
||
},
|
||
|
||
/** 最后一题手动提交(自动提交失败时点这里) */
|
||
finishTest() {
|
||
const q = this.data.currentQuestion
|
||
if (!q) return
|
||
if (this.data.answers[q.id] == null) {
|
||
wx.showToast({ title: '请先选择一项', icon: 'none' })
|
||
return
|
||
}
|
||
const tot = this.data.total
|
||
if (Object.keys(this.data.answers).length < tot) {
|
||
wx.showToast({ title: '还有题目未作答,请返回补答', icon: 'none' })
|
||
return
|
||
}
|
||
this.submitTest()
|
||
},
|
||
|
||
/**
|
||
* @param {{ allowIncomplete?: boolean }} opt 计时结束允许未答完也出结果
|
||
*/
|
||
submitTest(opt = {}) {
|
||
if (this.data.isSubmitting) return
|
||
const allowIncomplete = !!opt.allowIncomplete
|
||
if (this.timer) {
|
||
clearInterval(this.timer)
|
||
this.timer = null
|
||
}
|
||
|
||
const tot = this.data.total
|
||
const n = Object.keys(this.data.answers).length
|
||
if (!allowIncomplete && n < tot) {
|
||
wx.showToast({ title: `还有 ${tot - n} 题未作答`, icon: 'none' })
|
||
this.startTimer()
|
||
return
|
||
}
|
||
|
||
this.setData({ isSubmitting: true })
|
||
|
||
let result
|
||
try {
|
||
result = this.calculateResult()
|
||
} catch (err) {
|
||
console.error('calculateResult', err)
|
||
wx.showToast({ title: '计算结果失败,请重试', icon: 'none' })
|
||
this.setData({ isSubmitting: false })
|
||
this.startTimer()
|
||
return
|
||
}
|
||
|
||
const resultData = {
|
||
...result,
|
||
answers: this.data.answers,
|
||
testDuration: (this.data._initialSeconds || MBTI_TIME_SEC) - this.data.timeRemaining,
|
||
completedAt: new Date().toISOString(),
|
||
timestamp: new Date().toISOString()
|
||
}
|
||
wx.setStorageSync('mbtiResult', resultData)
|
||
try { require('../../utils/analytics').track('test_complete', { type: 'mbti', result: result.mbtiType, confidence: result.confidence, duration: resultData.testDuration }) } catch (e) {}
|
||
|
||
app.saveTestResult('mbti', resultData).then((extra) => {
|
||
const rid = extra && extra.id
|
||
const target = rid ? `/pages/result/mbti?id=${rid}&type=mbti` : '/pages/result/mbti'
|
||
afterTestSubmitNavigate(target)
|
||
})
|
||
},
|
||
|
||
// 计算MBTI结果
|
||
calculateResult() {
|
||
const answers = this.data.answers
|
||
const scores = { E: 0, I: 0, S: 0, N: 0, T: 0, F: 0, J: 0, P: 0 }
|
||
|
||
// 统计各维度得分
|
||
Object.values(answers).forEach(value => {
|
||
if (scores.hasOwnProperty(value)) {
|
||
scores[value]++
|
||
}
|
||
})
|
||
|
||
// 确定MBTI类型
|
||
const mbtiType = [
|
||
scores.E >= scores.I ? 'E' : 'I',
|
||
scores.S >= scores.N ? 'S' : 'N',
|
||
scores.T >= scores.F ? 'T' : 'F',
|
||
scores.J >= scores.P ? 'J' : 'P'
|
||
].join('')
|
||
|
||
const pct = (a, b) => {
|
||
const s = a + b
|
||
if (!s) return 50
|
||
return Math.round((Math.max(a, b) / s) * 100)
|
||
}
|
||
|
||
const dimensionScores = {
|
||
EI: { E: scores.E, I: scores.I, dominant: scores.E >= scores.I ? 'E' : 'I', percentage: pct(scores.E, scores.I) },
|
||
SN: { S: scores.S, N: scores.N, dominant: scores.S >= scores.N ? 'S' : 'N', percentage: pct(scores.S, scores.N) },
|
||
TF: { T: scores.T, F: scores.F, dominant: scores.T >= scores.F ? 'T' : 'F', percentage: pct(scores.T, scores.F) },
|
||
JP: { J: scores.J, P: scores.P, dominant: scores.J >= scores.P ? 'J' : 'P', percentage: pct(scores.J, scores.P) }
|
||
}
|
||
|
||
let confidence = Math.round(
|
||
(dimensionScores.EI.percentage + dimensionScores.SN.percentage +
|
||
dimensionScores.TF.percentage + dimensionScores.JP.percentage) / 4
|
||
)
|
||
if (!Number.isFinite(confidence)) confidence = 0
|
||
|
||
return {
|
||
mbtiType,
|
||
scores,
|
||
dimensionScores,
|
||
confidence,
|
||
description: mbtiDescriptions[mbtiType] || {}
|
||
}
|
||
},
|
||
|
||
onShareAppMessage() {
|
||
const { getSharePath } = require('../../utils/share')
|
||
return {
|
||
title: '来测测你的 MBTI 性格类型',
|
||
path: getSharePath('/pages/test/mbti')
|
||
}
|
||
},
|
||
|
||
onShareTimeline() {
|
||
const { buildShareQuery } = require('../../utils/share')
|
||
return {
|
||
title: '来测测你的 MBTI 性格类型',
|
||
query: buildShareQuery()
|
||
}
|
||
}
|
||
})
|