493 lines
18 KiB
JavaScript
493 lines
18 KiB
JavaScript
const appContext = require('../../utils/app-context.js')
|
|
|
|
const getReviewSettings = () => {
|
|
const app = appContext.withApp()
|
|
const settings = app.globalData.userSettings || {}
|
|
return {
|
|
autoNextQuestion: Number(settings.auto_next_question || 0) === 1,
|
|
showAnswerAfterSubmit: Number(settings.show_answer_after_submit || 0) === 1,
|
|
blankCount: Number(settings.blank_count || 2),
|
|
blankDifficulty: settings.blank_difficulty || 'normal',
|
|
}
|
|
}
|
|
|
|
Page({
|
|
data: {
|
|
progress: 0,
|
|
current: 1,
|
|
total: 0,
|
|
word: '',
|
|
meaning: '',
|
|
answer: '',
|
|
answers: [],
|
|
clozeAnswers: [],
|
|
feedback: '',
|
|
revealAnswer: '',
|
|
feedbackType: '',
|
|
showFeedback: false,
|
|
completed: false,
|
|
masterHint: false,
|
|
questions: [],
|
|
loading: false,
|
|
error: '',
|
|
settings: getReviewSettings(),
|
|
questionBadge: '',
|
|
entryTypeHint: '',
|
|
isCloze: false,
|
|
currentClozeQuestion: null,
|
|
currentClozeResults: [],
|
|
showClozeFeedback: false,
|
|
clozeSegments: [],
|
|
clozeFeedbackSummary: '',
|
|
activeClozeBlankIndex: 0,
|
|
},
|
|
submitting: false,
|
|
judged: false,
|
|
onLoad() {
|
|
this.loadSettingsAndQuestions()
|
|
},
|
|
getReviewSessionState() {
|
|
const app = appContext.withApp()
|
|
return app.globalData.reviewSession
|
|
},
|
|
loadSettingsAndQuestions() {
|
|
const app = appContext.withApp()
|
|
if (!app.globalData.authToken) {
|
|
this.setData({ error: '请先登录' })
|
|
return
|
|
}
|
|
|
|
wx.request({
|
|
url: `${app.globalData.baseUrl}/api/v1/settings`,
|
|
header: { 'X-Auth-Token': app.globalData.authToken },
|
|
success: (res) => {
|
|
if (appContext.isUnauthorizedResponse(res)) {
|
|
appContext.handleUnauthorized()
|
|
return
|
|
}
|
|
|
|
if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.code === 0) {
|
|
app.globalData.userSettings = res.data?.data || {}
|
|
this.setData({ settings: getReviewSettings() })
|
|
}
|
|
this.loadQuestions()
|
|
},
|
|
fail: () => {
|
|
this.setData({ error: '设置加载失败' })
|
|
this.loadQuestions()
|
|
},
|
|
})
|
|
},
|
|
buildClozeSegments(clozeQuestion) {
|
|
if (!clozeQuestion || !Array.isArray(clozeQuestion.blanks)) return []
|
|
const prompt = clozeQuestion.prompt_text || ''
|
|
const blanks = clozeQuestion.blanks || []
|
|
const segments = []
|
|
const useUnderscorePrompt = prompt.includes('____')
|
|
const workingPrompt = useUnderscorePrompt ? prompt : (clozeQuestion.original_text || prompt)
|
|
|
|
const pushWordBlank = (blankIndex, blank) => {
|
|
const answerText = String(blank?.answer || '')
|
|
const answerLength = Math.max(1, answerText.length || Number(blank?.hint?.length || 1))
|
|
const blankWidth = Math.max(88, Math.min(320, 32 + answerLength * 28))
|
|
segments.push({
|
|
type: 'word-blank',
|
|
blankIndex,
|
|
index: `blank-${blankIndex}`,
|
|
width: `${blankWidth}rpx`,
|
|
minWidth: `${blankWidth}rpx`,
|
|
maxLength: Math.max(1, answerLength),
|
|
})
|
|
}
|
|
|
|
if (useUnderscorePrompt) {
|
|
let blankIndex = 0
|
|
workingPrompt.split('____').forEach((text, index, parts) => {
|
|
if (text) segments.push({ type: 'text', text, index: `text-${index}` })
|
|
if (index < parts.length - 1) {
|
|
pushWordBlank(blankIndex, blanks[blankIndex])
|
|
blankIndex += 1
|
|
}
|
|
})
|
|
return segments
|
|
}
|
|
|
|
let cursor = 0
|
|
blanks.forEach((blank, idx) => {
|
|
const start = Math.max(0, Number(blank.start || 0))
|
|
const end = Math.max(start, Number(blank.end || start + 1))
|
|
if (start > cursor) {
|
|
segments.push({ type: 'text', text: workingPrompt.slice(cursor, start), index: `text-${idx}` })
|
|
}
|
|
pushWordBlank(idx, blank)
|
|
cursor = end
|
|
})
|
|
if (cursor < workingPrompt.length) {
|
|
segments.push({ type: 'text', text: workingPrompt.slice(cursor), index: 'text-tail' })
|
|
}
|
|
return segments
|
|
},
|
|
loadQuestions() {
|
|
const app = appContext.withApp()
|
|
const session = app.globalData.reviewSessionMeta || wx.getStorageSync('reviewSessionMeta')
|
|
if (!session?.id) {
|
|
this.setData({ error: '暂无复习会话' })
|
|
return
|
|
}
|
|
|
|
app.globalData.reviewSessionMeta = session
|
|
this.setData({ loading: true, error: '' })
|
|
wx.request({
|
|
url: `${app.globalData.baseUrl}/api/v1/review/questions?session_id=${session.id}`,
|
|
header: { 'X-Auth-Token': app.globalData.authToken || '' },
|
|
success: (res) => {
|
|
if (appContext.isUnauthorizedResponse(res)) {
|
|
appContext.handleUnauthorized()
|
|
return
|
|
}
|
|
|
|
if (res.statusCode < 200 || res.statusCode >= 300 || res.data?.code !== 0) {
|
|
const message = res.data?.detail || res.data?.message || '题目加载失败'
|
|
this.setData({ error: message, questions: [] })
|
|
return
|
|
}
|
|
|
|
const questions = (res.data?.data || []).map((item) => {
|
|
if (item.question_mode === 'C') {
|
|
return {
|
|
...item,
|
|
blank_prompt: item.blank_prompt || item.cloze_question?.prompt_text || item.prompt,
|
|
cloze_question: item.cloze_question || null,
|
|
}
|
|
}
|
|
return {
|
|
...item,
|
|
entry_type: item.entry_type || 'word',
|
|
}
|
|
})
|
|
|
|
if (!questions.length) {
|
|
this.setData({ error: '当前复习会话没有题目,请返回重新选择范围', questions: [] })
|
|
return
|
|
}
|
|
|
|
app.globalData.reviewSession = {
|
|
deck: questions,
|
|
currentIndex: 0,
|
|
correctCount: 0,
|
|
wrongCount: 0,
|
|
completed: false,
|
|
currentAnswer: '',
|
|
currentAnswers: [],
|
|
lastClozeResults: [],
|
|
startedAt: Date.now(),
|
|
}
|
|
this.setData({ questions })
|
|
this.syncSession()
|
|
},
|
|
fail: () => this.setData({ error: '题目加载失败' }),
|
|
complete: () => this.setData({ loading: false }),
|
|
})
|
|
},
|
|
syncSession() {
|
|
const session = this.getReviewSessionState()
|
|
const current = session.deck[session.currentIndex]
|
|
if (!current) return
|
|
|
|
const total = session.deck.length || 1
|
|
const isCloze = current?.question_mode === 'C'
|
|
const clozeQuestion = isCloze ? (current?.cloze_question || null) : null
|
|
this.setData({
|
|
progress: Math.round((session.currentIndex / total) * 100),
|
|
current: session.currentIndex + 1,
|
|
total,
|
|
word: isCloze ? (current?.blank_prompt || clozeQuestion?.prompt_text || current?.prompt || '') : (current?.prompt || ''),
|
|
meaning: isCloze ? (current?.answer_hint || '') : '',
|
|
answer: session.currentAnswer || '',
|
|
clozeAnswers: isCloze ? (Array.isArray(session.currentAnswers) ? session.currentAnswers : Array.from({ length: clozeQuestion?.blanks?.length || 0 }, () => '')) : [],
|
|
activeClozeBlankIndex: 0,
|
|
feedback: '',
|
|
revealAnswer: '',
|
|
feedbackType: '',
|
|
showFeedback: false,
|
|
completed: session.completed,
|
|
masterHint: false,
|
|
questionBadge: isCloze ? `填空 · ${current?.entry_type === 'sentence' ? '句子' : '单词'}` : `${current?.entry_type === 'sentence' ? '句子' : '单词'} · ${current?.question_mode || 'A'}`,
|
|
entryTypeHint: isCloze ? '请逐空作答,提交后会显示每个空的结果。' : (current?.entry_type === 'sentence' ? '当前题目来自句子库,请注意上下文表达。' : ''),
|
|
isCloze,
|
|
currentClozeQuestion: clozeQuestion,
|
|
currentClozeResults: session.lastClozeResults || [],
|
|
showClozeFeedback: false,
|
|
clozeSegments: isCloze ? this.buildClozeSegments(clozeQuestion) : [],
|
|
clozeFeedbackSummary: '',
|
|
})
|
|
},
|
|
onAnswerInput(e) {
|
|
const session = this.getReviewSessionState()
|
|
const answer = e.detail.value
|
|
session.currentAnswer = answer
|
|
this.setData({ answer, showFeedback: false, revealAnswer: '', feedback: '', feedbackType: '', judged: false, showClozeFeedback: false })
|
|
},
|
|
onClozeLetterInput(e) {
|
|
const session = this.getReviewSessionState()
|
|
const { index, letterIndex } = e.currentTarget.dataset
|
|
const rawValue = e.detail.value || ''
|
|
const cleaned = rawValue.slice(-1)
|
|
const answers = [...(session.currentAnswers || [])]
|
|
const current = answers[index] || ''
|
|
const chars = current.split('')
|
|
chars[letterIndex] = cleaned
|
|
answers[index] = chars.join('').slice(0, Math.max(chars.length, letterIndex + 1))
|
|
session.currentAnswers = answers
|
|
this.setData({
|
|
[`clozeAnswers[${index}]`]: answers[index],
|
|
showFeedback: false,
|
|
revealAnswer: '',
|
|
feedback: '',
|
|
feedbackType: '',
|
|
judged: false,
|
|
showClozeFeedback: false,
|
|
}, () => {
|
|
if (cleaned) this.focusNextLetterCell(Number(index), Number(letterIndex) + 1)
|
|
})
|
|
},
|
|
focusNextLetterCell(blankIndex, letterIndex) {
|
|
const selector = `#cloze-input-${blankIndex}-${letterIndex}`
|
|
wx.nextTick(() => {
|
|
const query = wx.createSelectorQuery().in(this)
|
|
query.select(selector).fields({ node: true }).exec((res) => {
|
|
if (!res || !res[0]) return
|
|
const node = res[0].node
|
|
if (node && typeof node.focus === 'function') node.focus()
|
|
})
|
|
})
|
|
},
|
|
handleLetterKeydown(e) {
|
|
const session = this.getReviewSessionState()
|
|
const key = e?.detail?.key || e?.detail?.value || ''
|
|
const { index, letterIndex } = e.currentTarget.dataset
|
|
if (key === 'Backspace' || key === 'Delete') {
|
|
const answers = [...(session.currentAnswers || [])]
|
|
const current = answers[index] || ''
|
|
const chars = current.split('')
|
|
const pos = Number(letterIndex)
|
|
if (chars[pos]) {
|
|
chars[pos] = ''
|
|
answers[index] = chars.join('')
|
|
session.currentAnswers = answers
|
|
this.setData({ [`clozeAnswers[${index}]`]: answers[index] })
|
|
return
|
|
}
|
|
const prevIndex = Number(letterIndex) - 1
|
|
if (prevIndex >= 0) this.focusNextLetterCell(Number(index), prevIndex)
|
|
return
|
|
}
|
|
if (key === 'ArrowLeft') {
|
|
const prevIndex = Number(letterIndex) - 1
|
|
if (prevIndex >= 0) this.focusNextLetterCell(Number(index), prevIndex)
|
|
return
|
|
}
|
|
if (key === 'ArrowRight' || key === ' ' || key === 'Space') {
|
|
this.focusNextLetterCell(Number(index), Number(letterIndex) + 1)
|
|
}
|
|
},
|
|
onClozeAnswerInput(e) {
|
|
const session = this.getReviewSessionState()
|
|
const { index } = e.currentTarget.dataset
|
|
const rawValue = e.detail.value || ''
|
|
const clozeQuestion = this.data.currentClozeQuestion
|
|
const blank = clozeQuestion?.blanks?.[index]
|
|
const maxLength = Math.max(1, Number(blank?.answer?.length || 1))
|
|
const normalizedValue = rawValue.slice(-maxLength).slice(0, maxLength)
|
|
const answers = [...(session.currentAnswers || [])]
|
|
answers[index] = normalizedValue
|
|
session.currentAnswers = answers
|
|
const nextIndex = Number(index) + (normalizedValue.length >= maxLength ? 1 : 0)
|
|
const prevIndex = Number(index) - 1
|
|
this.setData({
|
|
[`clozeAnswers[${index}]`]: normalizedValue,
|
|
activeClozeBlankIndex: nextIndex,
|
|
showFeedback: false,
|
|
revealAnswer: '',
|
|
feedback: '',
|
|
feedbackType: '',
|
|
judged: false,
|
|
showClozeFeedback: false,
|
|
}, () => {
|
|
if (!normalizedValue && this.data.isCloze && rawValue === '') {
|
|
this.focusNextClozeInput(Math.max(0, prevIndex))
|
|
return
|
|
}
|
|
if (normalizedValue.length >= maxLength && this.data.isCloze) this.focusNextClozeInput(nextIndex)
|
|
})
|
|
},
|
|
focusNextClozeInput(nextIndex) {
|
|
const selector = `#cloze-input-${nextIndex}`
|
|
wx.nextTick(() => {
|
|
const query = wx.createSelectorQuery().in(this)
|
|
query.select(selector).fields({ node: true, size: true }).exec((res) => {
|
|
if (!res || !res[0]) return
|
|
const node = res[0].node
|
|
if (node && typeof node.focus === 'function') node.focus()
|
|
})
|
|
})
|
|
},
|
|
submitAnswer(showExplanation = false, advanceAfterSubmit = false) {
|
|
const app = appContext.withApp()
|
|
const session = this.getReviewSessionState()
|
|
const current = session.deck[session.currentIndex]
|
|
if (!current || this.submitting || this.judged) return
|
|
|
|
const answer = (this.data.answer || '').trim()
|
|
const clozeQuestion = current.cloze_question
|
|
const payload = {
|
|
session_id: app.globalData.reviewSessionMeta.id,
|
|
record_id: current.record_id,
|
|
user_answer: answer,
|
|
}
|
|
|
|
if (current.question_mode === 'C' && clozeQuestion?.blanks?.length) {
|
|
const answers = (this.data.clozeAnswers || []).map((item) => (item || '').trim())
|
|
while (answers.length < clozeQuestion.blanks.length) answers.push('')
|
|
if (!answers.some((item) => item)) {
|
|
wx.showToast({ title: '请先输入答案', icon: 'none' })
|
|
return
|
|
}
|
|
payload.user_answers = answers
|
|
} else if (!answer) {
|
|
wx.showToast({ title: '请先输入答案', icon: 'none' })
|
|
return
|
|
}
|
|
|
|
this.submitting = true
|
|
wx.request({
|
|
url: `${app.globalData.baseUrl}/api/v1/review/submit`,
|
|
method: 'POST',
|
|
header: { 'Content-Type': 'application/json', 'X-Auth-Token': app.globalData.authToken || '' },
|
|
data: payload,
|
|
success: (res) => {
|
|
if (appContext.isUnauthorizedResponse(res)) {
|
|
appContext.handleUnauthorized()
|
|
return
|
|
}
|
|
|
|
const ok = res.statusCode >= 200 && res.statusCode < 300 && res.data?.code === 0
|
|
if (!ok) {
|
|
wx.showToast({ title: res.data?.detail || res.data?.message || '提交答案失败', icon: 'none' })
|
|
return
|
|
}
|
|
|
|
const result = res.data?.data || {}
|
|
session.correctCount += result.is_correct ? 1 : 0
|
|
session.wrongCount += result.is_correct ? 0 : 1
|
|
this.judged = true
|
|
|
|
const shouldShowAnswer = showExplanation || this.data.settings.showAnswerAfterSubmit
|
|
const correctAnswerText = result.correct_answer_text || ''
|
|
const clozeResults = Array.isArray(result.cloze_results) ? result.cloze_results : []
|
|
const fallbackClozeResults = clozeResults.length
|
|
? clozeResults
|
|
: (this.data.currentClozeQuestion?.blanks || []).map((blank, idx) => ({
|
|
index: typeof blank?.index === 'number' ? blank.index : idx,
|
|
user_answer: '',
|
|
correct_answers: [blank?.answer, ...(Array.isArray(blank?.accepted_answers) ? blank.accepted_answers : [])].filter(Boolean),
|
|
correct_answer_text: blank?.answer || '',
|
|
is_correct: false,
|
|
}))
|
|
const normalizedClozeResults = fallbackClozeResults.map((item, idx) => {
|
|
const correctAnswers = Array.isArray(item.correct_answers)
|
|
? item.correct_answers.filter(Boolean)
|
|
: [item.correct_answer_text, ...(Array.isArray(item.accepted_answers) ? item.accepted_answers : [])].filter(Boolean)
|
|
const standardAnswerText = correctAnswers.join(' / ') || item.correct_answer_text || ''
|
|
return {
|
|
index: item.index ?? idx,
|
|
user_answer: item.user_answer ?? '',
|
|
correct_answers: correctAnswers,
|
|
correct_answer_text: item.correct_answer_text || standardAnswerText,
|
|
standard_answer_text: standardAnswerText,
|
|
is_correct: Boolean(item.is_correct),
|
|
}
|
|
})
|
|
|
|
const derivedClozeResults = normalizedClozeResults.length
|
|
? normalizedClozeResults
|
|
: (correctAnswerText
|
|
? [{ index: 0, user_answer: '', correct_answers: [correctAnswerText], correct_answer_text: correctAnswerText, standard_answer_text: correctAnswerText, is_correct: false }]
|
|
: [])
|
|
|
|
if (derivedClozeResults.length) {
|
|
session.lastClozeResults = derivedClozeResults
|
|
}
|
|
|
|
const clozeFeedbackSummary = result.is_correct ? '回答正确' : '回答错误'
|
|
this.setData({
|
|
feedback: clozeFeedbackSummary,
|
|
feedbackType: result.is_correct ? 'correct' : 'wrong',
|
|
revealAnswer: '',
|
|
showFeedback: true,
|
|
showClozeFeedback: derivedClozeResults.length > 0,
|
|
currentClozeResults: derivedClozeResults,
|
|
clozeFeedbackSummary,
|
|
})
|
|
|
|
if (this.data.isCloze) {
|
|
session.currentAnswers = []
|
|
this.setData({ clozeAnswers: [] })
|
|
}
|
|
|
|
if (this.data.settings.autoNextQuestion || advanceAfterSubmit) {
|
|
const delay = shouldShowAnswer ? 2500 : 0
|
|
setTimeout(() => this.nextQuestion(), delay)
|
|
}
|
|
},
|
|
complete: () => {
|
|
this.submitting = false
|
|
},
|
|
})
|
|
},
|
|
checkAnswer() {
|
|
this.submitAnswer(true)
|
|
},
|
|
nextQuestion() {
|
|
const app = appContext.withApp()
|
|
const session = this.getReviewSessionState()
|
|
if (!session.deck.length) return
|
|
if (!this.judged) {
|
|
this.submitAnswer(true, true)
|
|
return
|
|
}
|
|
|
|
this.judged = false
|
|
if (session.currentIndex >= session.deck.length - 1) {
|
|
session.completed = true
|
|
session.endedAt = Date.now()
|
|
wx.request({
|
|
url: `${app.globalData.baseUrl}/api/v1/review/finish?session_id=${app.globalData.reviewSessionMeta.id}`,
|
|
method: 'POST',
|
|
header: { 'X-Auth-Token': app.globalData.authToken || '' },
|
|
success: (res) => {
|
|
if (appContext.isUnauthorizedResponse(res)) {
|
|
appContext.handleUnauthorized()
|
|
return
|
|
}
|
|
wx.navigateTo({ url: '/pages/review-result/review-result' })
|
|
},
|
|
fail: () => {
|
|
wx.showToast({ title: '提交结果失败,已按本地完成处理', icon: 'none' })
|
|
wx.navigateTo({ url: '/pages/review-result/review-result' })
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
session.currentIndex += 1
|
|
session.currentAnswer = ''
|
|
session.currentAnswers = []
|
|
this.syncSession()
|
|
},
|
|
markMastered() {
|
|
this.setData({ masterHint: true })
|
|
wx.showToast({ title: '已标记', icon: 'success' })
|
|
},
|
|
})
|