add variation
This commit is contained in:
7
miniprogram/components/cloud-image/cloud-image.json
Normal file
7
miniprogram/components/cloud-image/cloud-image.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"t-image": "tdesign-miniprogram/image/image",
|
||||
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton"
|
||||
}
|
||||
}
|
||||
98
miniprogram/components/cloud-image/cloud-image.ts
Normal file
98
miniprogram/components/cloud-image/cloud-image.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import apiManager from '../../utils/api';
|
||||
|
||||
// Global memory cache for image URLs
|
||||
const urlCache = new Map<string, string>();
|
||||
// Track pending requests to avoid duplicate fetches for the same fileId
|
||||
const pendingRequests = new Map<string, Promise<string>>();
|
||||
|
||||
Component({
|
||||
virtualHost: true,
|
||||
properties: {
|
||||
fileId: {
|
||||
type: String,
|
||||
observer: 'loadUrl'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
value: '100%'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
value: '100%'
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
value: 'widthFix'
|
||||
},
|
||||
radius: {
|
||||
type: String,
|
||||
value: '0'
|
||||
}
|
||||
},
|
||||
|
||||
data: {
|
||||
imageUrl: '',
|
||||
isLoading: true,
|
||||
isError: false
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadUrl(this: any, fileId: string) {
|
||||
if (!fileId) {
|
||||
this.setData({ imageUrl: '', isLoading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
if (urlCache.has(fileId)) {
|
||||
this.setData({
|
||||
imageUrl: urlCache.get(fileId),
|
||||
isLoading: false,
|
||||
isError: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setData({ isLoading: true, isError: false });
|
||||
|
||||
try {
|
||||
let promise = pendingRequests.get(fileId);
|
||||
if (!promise) {
|
||||
promise = apiManager.getFileDisplayUrl(fileId);
|
||||
pendingRequests.set(fileId, promise);
|
||||
}
|
||||
|
||||
const url = await promise;
|
||||
urlCache.set(fileId, url);
|
||||
pendingRequests.delete(fileId);
|
||||
|
||||
// Ensure the fileId hasn't changed while we were fetching
|
||||
if (this.data.fileId === fileId) {
|
||||
console.log('[CloudImage] Loaded url for', fileId, url)
|
||||
this.setData({
|
||||
imageUrl: url,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load image url for fileId:', fileId, e);
|
||||
if (this.data.fileId === fileId) {
|
||||
this.setData({
|
||||
isLoading: false,
|
||||
isError: true
|
||||
});
|
||||
}
|
||||
pendingRequests.delete(fileId);
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(this: any, e: any) {
|
||||
this.triggerEvent('load', e);
|
||||
},
|
||||
|
||||
onError(this: any, e: any) {
|
||||
this.setData({ isError: true, isLoading: false });
|
||||
this.triggerEvent('error', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
24
miniprogram/components/cloud-image/cloud-image.wxml
Normal file
24
miniprogram/components/cloud-image/cloud-image.wxml
Normal file
@@ -0,0 +1,24 @@
|
||||
<view class="cloud-image" style="width: {{width}}; height: {{height}}; border-radius: {{radius}}; overflow: hidden; position: relative;">
|
||||
<t-skeleton
|
||||
wx:if="{{isLoading}}"
|
||||
row-col="{{[{ width: '100%', height: '100%', borderRadius: radius }]}}"
|
||||
loading
|
||||
class="skeleton"
|
||||
style="width: 100%; height: 100%; display: block;"
|
||||
></t-skeleton>
|
||||
|
||||
<t-image
|
||||
wx:if="{{!isLoading && imageUrl}}"
|
||||
src="{{imageUrl}}"
|
||||
mode="{{mode}}"
|
||||
class="image"
|
||||
t-class="image"
|
||||
style="width: 100%; height: {{mode === 'widthFix' ? 'auto' : '100%'}}; display: block;"
|
||||
bind:error="onError"
|
||||
bind:load="onLoad"
|
||||
/>
|
||||
|
||||
<view wx:if="{{isError}}" class="error-placeholder" style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f5f5f5; color: #999;">
|
||||
<text style="font-size: 24rpx;">加载失败</text>
|
||||
</view>
|
||||
</view>
|
||||
34
miniprogram/components/cloud-image/cloud-image.wxss
Normal file
34
miniprogram/components/cloud-image/cloud-image.wxss
Normal file
@@ -0,0 +1,34 @@
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cloud-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ccc;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
@@ -21,7 +21,14 @@ Component({
|
||||
const raw = String(word || '')
|
||||
const cleaned = raw.replace(/[.,?!*;:'"()]/g, '').trim()
|
||||
if (!cleaned) return
|
||||
self.setData({ visible: true, loading: true })
|
||||
// Clear previous word data before showing loading state
|
||||
self.setData({
|
||||
visible: true,
|
||||
loading: true,
|
||||
wordDict: {},
|
||||
prototypeWord: '',
|
||||
isWordEmptyResult: false
|
||||
})
|
||||
try {
|
||||
const detail: any = await apiManager.getWordDetail(cleaned)
|
||||
const collins = detail['collins']
|
||||
|
||||
@@ -71,8 +71,7 @@
|
||||
<view class="bottom-more-area {{isMoreMenuOpen ? 'open' : (isMoreMenuClosing ? 'close' : '')}}" wx:if="{{isMoreMenuOpen || isMoreMenuClosing}}">
|
||||
<view class="more-items">
|
||||
<view class="more-item" bindtap="onSceneSentenceTap">场景句型</view>
|
||||
<view class="more-item" bindtap="onImageQaExerciseTap" data-type="choice">问答练习</view>
|
||||
<view class="more-item" bindtap="onImageQaExerciseTap" data-type="cloze">完形填空</view>
|
||||
<view class="more-item" bindtap="onImageQaExerciseTap" data-type="cloze">场景练习</view>
|
||||
<!-- <view class="more-item">功能3</view> -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton",
|
||||
"word-dictionary": "../../components/word-dictionary/word-dictionary",
|
||||
"vx-confetti": "/components/vx-confetti/vx-confetti"
|
||||
"vx-confetti": "/components/vx-confetti/vx-confetti",
|
||||
"cloud-image": "../../components/cloud-image/cloud-image"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,15 @@ import logger from '../../utils/logger'
|
||||
import { IQaExerciseItem, IQaExerciseSession, IAppOption } from '../../types/app'
|
||||
|
||||
export const QUESTION_MODES = {
|
||||
CHOICE: 'choice',
|
||||
CLOZE: 'cloze',
|
||||
FREE_TEXT: 'free_text'
|
||||
CHOICE: 'choice',
|
||||
FREE_TEXT: 'free_text',
|
||||
VARIATION: 'variation'
|
||||
} as const
|
||||
|
||||
export type QuestionMode = typeof QUESTION_MODES[keyof typeof QUESTION_MODES]
|
||||
|
||||
export const ENABLED_QUESTION_MODES: QuestionMode[] = [QUESTION_MODES.CHOICE, QUESTION_MODES.CLOZE]
|
||||
export const ENABLED_QUESTION_MODES: QuestionMode[] = [QUESTION_MODES.CHOICE, QUESTION_MODES.CLOZE, QUESTION_MODES.VARIATION]
|
||||
|
||||
interface IData {
|
||||
loadingMaskVisible: boolean
|
||||
@@ -40,6 +41,7 @@ interface IData {
|
||||
clozeOptions?: string[]
|
||||
selectedClozeIndex?: number
|
||||
clozeParts?: string[]
|
||||
clozeSentenceTokens?: Array<{ text: string; word?: string; isBlank?: boolean }>
|
||||
freeTextInput: string
|
||||
imageLocalUrl?: string
|
||||
imageLoaded?: boolean
|
||||
@@ -85,20 +87,35 @@ interface IData {
|
||||
confetti?: any
|
||||
nextButtonIcon?: string
|
||||
isAutoSwitching?: boolean
|
||||
variationQaList?: any[]
|
||||
mainQaList?: any[]
|
||||
variationTaskId?: string
|
||||
variationImages?: Record<string, string>
|
||||
variationImagesLoading?: Record<string, boolean>
|
||||
variationImageLoaded: Record<string, boolean>
|
||||
variationSelectedIndex?: number
|
||||
variationSubmitted?: boolean
|
||||
variationResultStatus?: 'correct' | 'incorrect'
|
||||
variationExerciseId?: string
|
||||
}
|
||||
|
||||
interface IPageInstance {
|
||||
pollTimer?: number
|
||||
variationPollTimer?: number
|
||||
audioCtx?: WechatMiniprogram.InnerAudioContext
|
||||
fetchQaExercises: (imageId: string, referrerId?: string) => Promise<void>
|
||||
fetchVariationExercises: (imageId: string) => Promise<void>
|
||||
startPolling: (taskId: string, imageId: string) => void
|
||||
startVariationPolling: (taskId: string, imageId: string) => void
|
||||
initExerciseContent: (exercise: any, session?: IQaExerciseSession) => void
|
||||
updateActionButtonsState: () => void
|
||||
shuffleArray: <T>(arr: T[]) => T[]
|
||||
buildClozeSentence: (sentenceRaw: any, correctWordRaw: any) => { blanked: string; parts: string[]; tokens: Array<{ text: string; word?: string; isBlank?: boolean }> }
|
||||
updateContentReady: () => void
|
||||
switchQuestion: (delta: number) => void
|
||||
switchMode: (mode: QuestionMode) => void
|
||||
switchMode: (arg: any) => void
|
||||
selectOption: (e: any) => void
|
||||
selectVariationOption: (e: any) => void
|
||||
onRetryTap: () => void
|
||||
ensureQuestionResultFetched: (questionId: string) => Promise<void>
|
||||
applyCachedResultForQuestion: (questionId: string) => void
|
||||
@@ -133,6 +150,8 @@ interface IPageInstance {
|
||||
handleShareAchievement: () => void
|
||||
fireConfetti: () => void
|
||||
resetConfetti: () => void
|
||||
loadVariationImages: (list: any[]) => void
|
||||
onVariationImageLoad: (e: any) => void
|
||||
}
|
||||
|
||||
Page<IData, IPageInstance>({
|
||||
@@ -156,6 +175,7 @@ Page<IData, IPageInstance>({
|
||||
imageLoaded: false,
|
||||
choiceSubmitted: false,
|
||||
evalClasses: [],
|
||||
clozeSentenceTokens: [],
|
||||
freeTextInput: '',
|
||||
modeAnim: '',
|
||||
isModeSwitching: false,
|
||||
@@ -196,7 +216,14 @@ Page<IData, IPageInstance>({
|
||||
canvasHeight: 0,
|
||||
confetti: null,
|
||||
nextButtonIcon: 'chevron-right',
|
||||
isAutoSwitching: false
|
||||
isAutoSwitching: false,
|
||||
variationQaList: [],
|
||||
variationTaskId: '',
|
||||
variationImages: {},
|
||||
variationImagesLoading: {},
|
||||
variationImageLoaded: {},
|
||||
variationSelectedIndex: -1,
|
||||
variationSubmitted: false
|
||||
},
|
||||
updateProcessDots() {
|
||||
const list = this.data.qaList || []
|
||||
@@ -292,6 +319,10 @@ Page<IData, IPageInstance>({
|
||||
} else if (mode === QUESTION_MODES.FREE_TEXT) {
|
||||
submitDisabled = false
|
||||
retryDisabled = true
|
||||
} else if (mode === QUESTION_MODES.VARIATION) {
|
||||
const hasSelection = typeof this.data.variationSelectedIndex === 'number' && this.data.variationSelectedIndex >= 0
|
||||
submitDisabled = hasResult || !hasSelection
|
||||
retryDisabled = !hasResult
|
||||
}
|
||||
this.setData({ submitDisabled, retryDisabled })
|
||||
},
|
||||
@@ -305,6 +336,29 @@ Page<IData, IPageInstance>({
|
||||
}
|
||||
return a
|
||||
},
|
||||
buildClozeSentence(sentenceRaw: any, correctWordRaw: any) {
|
||||
const sentence = typeof sentenceRaw === 'string' ? sentenceRaw : ''
|
||||
const correctWord = typeof correctWordRaw === 'string' ? correctWordRaw : ''
|
||||
const rawTokens = sentence.trim().length ? sentence.trim().split(/\s+/) : []
|
||||
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z-']/g, '')
|
||||
const target = normalize(correctWord)
|
||||
const blankIndex = target ? rawTokens.findIndex((t) => normalize(t) === target) : -1
|
||||
|
||||
const blankedTokens = rawTokens.slice()
|
||||
if (blankIndex >= 0) blankedTokens[blankIndex] = '___'
|
||||
const blanked = blankedTokens.join(' ')
|
||||
|
||||
let parts = blanked.split('___')
|
||||
if (parts.length < 2) parts = [blanked, '']
|
||||
if (parts.length > 2) parts = [parts[0], parts.slice(1).join('___')]
|
||||
|
||||
const tokens = rawTokens.map((t, idx) => {
|
||||
if (idx === blankIndex) return { text: '_____', word: '', isBlank: true }
|
||||
return { text: t, word: t, isBlank: false }
|
||||
})
|
||||
|
||||
return { blanked, parts, tokens }
|
||||
},
|
||||
updateContentReady() {
|
||||
const hasList = Array.isArray(this.data.qaList) && this.data.qaList.length > 0
|
||||
this.setData({ contentReady: hasList })
|
||||
@@ -410,7 +464,11 @@ Page<IData, IPageInstance>({
|
||||
const imageId = options?.id || options?.image_id || ''
|
||||
const thumbnailId = options?.thumbnail_id || '' // 兼容旧逻辑,如果是分享进来的可能需要通过 API 获取图片链接
|
||||
this.setData({ imageId, loadingMaskVisible: true, statusText: '加载中...' })
|
||||
await this.fetchQaExercises(imageId, referrerId)
|
||||
if (type === QUESTION_MODES.VARIATION) {
|
||||
await this.fetchVariationExercises(imageId)
|
||||
} else {
|
||||
await this.fetchQaExercises(imageId, referrerId)
|
||||
}
|
||||
|
||||
// 如果没有 thumbnailId,尝试通过 imageId 获取(或者 fetchQaExercises 内部处理了?)
|
||||
// 假设 fetchQaExercises 会处理内容,这里主要处理图片加载
|
||||
@@ -488,7 +546,7 @@ Page<IData, IPageInstance>({
|
||||
},
|
||||
async fetchQaExercises(imageId: string, referrerId?: string) {
|
||||
try {
|
||||
const res = await apiManager.listQaExercisesByImage(imageId)
|
||||
const res = await apiManager.listQaExercisesByImage(imageId, 'scene_basic')
|
||||
if (res && res.exercise && Array.isArray(res.questions) && res.questions.length > 0) {
|
||||
const exercise = res.exercise as any
|
||||
const qaList = (res.questions || []).map((q: any) => ({
|
||||
@@ -508,7 +566,10 @@ Page<IData, IPageInstance>({
|
||||
completed = (session.progress as any).answered
|
||||
}
|
||||
const progressText = `已完成 ${completed} / ${total}`
|
||||
this.setData({ session, progressText })
|
||||
|
||||
// Save to mainQaList as well
|
||||
this.setData({ session, progressText, mainQaList: qaList })
|
||||
|
||||
this.initExerciseContent(exerciseWithList, session)
|
||||
|
||||
// 尝试获取图片链接(如果是通过 image_id 进入且没有 thumbnail_id)
|
||||
@@ -526,7 +587,7 @@ Page<IData, IPageInstance>({
|
||||
|
||||
return
|
||||
}
|
||||
const { task_id } = await apiManager.createQaExerciseTask(imageId)
|
||||
const { task_id } = await apiManager.createQaExerciseTask(imageId, 'scene_basic')
|
||||
this.setData({ taskId: task_id, statusText: '解析中...' })
|
||||
this.startPolling(task_id, imageId)
|
||||
} catch (e) {
|
||||
@@ -536,6 +597,118 @@ Page<IData, IPageInstance>({
|
||||
this.setData({ loadingMaskVisible: false, statusText: '加载失败' })
|
||||
}
|
||||
},
|
||||
async loadVariationImages(list: any[]) {
|
||||
if (!list || list.length === 0) return
|
||||
const loadingMap: Record<string, boolean> = {}
|
||||
|
||||
list.forEach(item => {
|
||||
const fileId = (item.ext || {}).file_id
|
||||
if (fileId) {
|
||||
// If not already in variationImages, fetch url
|
||||
if (!this.data.variationImages || !this.data.variationImages[fileId]) {
|
||||
loadingMap[fileId] = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (Object.keys(loadingMap).length === 0) return
|
||||
|
||||
// Update loading state for new items
|
||||
this.setData({
|
||||
variationImagesLoading: {
|
||||
...this.data.variationImagesLoading,
|
||||
...loadingMap
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch URLs
|
||||
Object.keys(loadingMap).forEach(async (fileId) => {
|
||||
try {
|
||||
const url = await apiManager.getFileDisplayUrl(fileId)
|
||||
this.setData({
|
||||
variationImages: { ...this.data.variationImages, [fileId]: url }
|
||||
})
|
||||
} catch (err) {
|
||||
logger.error('Failed to get variation image url', err)
|
||||
this.setData({
|
||||
variationImagesLoading: { ...this.data.variationImagesLoading, [fileId]: false }
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
onVariationImageLoad(e: any) {
|
||||
const fileId = e.currentTarget.dataset.fileid
|
||||
if (fileId) {
|
||||
this.setData({
|
||||
variationImageLoaded: { ...this.data.variationImageLoaded, [fileId]: true },
|
||||
variationImagesLoading: { ...this.data.variationImagesLoading, [fileId]: false }
|
||||
})
|
||||
}
|
||||
},
|
||||
async fetchVariationExercises(imageId: string) {
|
||||
try {
|
||||
this.setData({ loadingMaskVisible: true, statusText: '加载练习...' })
|
||||
const res = await apiManager.listQaExercisesByImage(imageId, 'scene_variation')
|
||||
|
||||
if (res && Array.isArray(res.questions) && res.questions.length > 0) {
|
||||
const exercise = res.exercise as any
|
||||
const variationExerciseId = String(exercise?.id || '')
|
||||
const qaList = (res.questions || []).map((q: any) => ({
|
||||
id: q.id,
|
||||
exercise_id: q.exercise_id,
|
||||
image_id: q.image_id,
|
||||
question: q.question,
|
||||
...(q.ext || {})
|
||||
}))
|
||||
|
||||
logger.log('Variation exercises loaded:', qaList.length)
|
||||
this.setData({
|
||||
variationQaList: qaList,
|
||||
variationExerciseId,
|
||||
loadingMaskVisible: false,
|
||||
statusText: '加载完成',
|
||||
qaList: qaList,
|
||||
currentIndex: 0,
|
||||
questionMode: 'variation' as QuestionMode
|
||||
})
|
||||
this.switchQuestion(0)
|
||||
return
|
||||
}
|
||||
|
||||
const { task_id } = await apiManager.createQaExerciseTask(imageId, 'scene_variation')
|
||||
this.setData({ variationTaskId: task_id, statusText: '生成变化练习中...' })
|
||||
this.startVariationPolling(task_id, imageId)
|
||||
|
||||
} catch (e) {
|
||||
logger.error('获取变化练习失败', e)
|
||||
wx.showToast({ title: '加载失败', icon: 'none' })
|
||||
this.setData({ loadingMaskVisible: false, statusText: '加载失败' })
|
||||
}
|
||||
},
|
||||
startVariationPolling(taskId: string, imageId: string) {
|
||||
if (this.variationPollTimer) {
|
||||
clearInterval(this.variationPollTimer)
|
||||
this.variationPollTimer = undefined
|
||||
}
|
||||
this.setData({ loadingMaskVisible: true })
|
||||
this.variationPollTimer = setInterval(async () => {
|
||||
try {
|
||||
const res = await apiManager.getQaExerciseTaskStatus(taskId)
|
||||
if (res.status === 'completed') {
|
||||
clearInterval(this.variationPollTimer!)
|
||||
this.variationPollTimer = undefined
|
||||
await this.fetchVariationExercises(imageId)
|
||||
} else if (res.status === 'failed') {
|
||||
clearInterval(this.variationPollTimer!)
|
||||
this.variationPollTimer = undefined
|
||||
wx.showToast({ title: '任务失败', icon: 'none' })
|
||||
this.setData({ loadingMaskVisible: false, statusText: '任务失败' })
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('轮询变化任务状态失败', err)
|
||||
}
|
||||
}, 3000) as any
|
||||
},
|
||||
startPolling(taskId: string, imageId: string) {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer)
|
||||
@@ -579,12 +752,14 @@ Page<IData, IPageInstance>({
|
||||
}
|
||||
const q = qaList[idx] || {}
|
||||
const hasOptions = (Array.isArray(q?.correct_options) && q.correct_options.length > 0) || (Array.isArray(q?.incorrect_options) && q.incorrect_options.length > 0)
|
||||
const hasCloze = !!q?.cloze && !!q.cloze.sentence_with_blank
|
||||
const hasCloze = !!q?.cloze && !!q.cloze.sentence
|
||||
const preferredMode: QuestionMode = hasOptions ? QUESTION_MODES.CHOICE : (hasCloze ? QUESTION_MODES.CLOZE : QUESTION_MODES.FREE_TEXT)
|
||||
let mode: QuestionMode = ENABLED_QUESTION_MODES.includes(preferredMode) ? preferredMode : ENABLED_QUESTION_MODES[0]
|
||||
|
||||
if (this.data.fixedMode) {
|
||||
mode = this.data.fixedMode
|
||||
} else {
|
||||
mode = QUESTION_MODES.CHOICE
|
||||
}
|
||||
|
||||
let choiceOptions: Array<{ content: string; correct: boolean; type?: string }> = []
|
||||
@@ -594,11 +769,11 @@ Page<IData, IPageInstance>({
|
||||
choiceOptions = [...correct, ...incorrect]
|
||||
}
|
||||
const choiceRequiredCount = Array.isArray(q?.correct_options) ? q.correct_options.length : 1
|
||||
const clozeSentenceWithBlank = q?.cloze?.sentence_with_blank || ''
|
||||
const clozeCorrectWord = q?.cloze?.correct_word || ''
|
||||
const clozeSentence = q?.cloze?.sentence || ''
|
||||
const clozeDistractorWords = Array.isArray(q?.cloze?.distractor_words) ? q.cloze.distractor_words : []
|
||||
const clozeOptions = this.shuffleArray([clozeCorrectWord, ...clozeDistractorWords].filter((w) => !!w))
|
||||
const clozeParts = typeof clozeSentenceWithBlank === 'string' ? clozeSentenceWithBlank.split('___') : [clozeSentenceWithBlank || '', '']
|
||||
const { blanked: clozeSentenceWithBlank, parts: clozeParts, tokens: clozeSentenceTokens } = this.buildClozeSentence(clozeSentence, clozeCorrectWord)
|
||||
const questionText = String(q?.question || '')
|
||||
const questionWords = questionText.split(/\s+/).filter((w) => !!w)
|
||||
this.setData({
|
||||
@@ -621,6 +796,7 @@ Page<IData, IPageInstance>({
|
||||
clozeOptions,
|
||||
selectedClozeIndex: -1,
|
||||
clozeParts,
|
||||
clozeSentenceTokens,
|
||||
answers: q.answers || {},
|
||||
freeTextInput: '',
|
||||
questionWords,
|
||||
@@ -648,7 +824,6 @@ Page<IData, IPageInstance>({
|
||||
if (idx > total - 1) idx = total - 1
|
||||
const q = this.data.qaList[idx] || {}
|
||||
const hasOptions = (Array.isArray(q?.correct_options) && q.correct_options.length > 0) || (Array.isArray(q?.incorrect_options) && q.incorrect_options.length > 0)
|
||||
const hasCloze = !!q?.cloze && !!q.cloze.sentence_with_blank
|
||||
const mode: QuestionMode = this.data.questionMode
|
||||
let choiceOptions: Array<{ content: string; correct: boolean; type?: string }> = []
|
||||
if (hasOptions) {
|
||||
@@ -657,11 +832,11 @@ Page<IData, IPageInstance>({
|
||||
choiceOptions = [...correct, ...incorrect]
|
||||
}
|
||||
const choiceRequiredCount = Array.isArray(q?.correct_options) ? q.correct_options.length : 1
|
||||
const clozeSentenceWithBlank = q?.cloze?.sentence_with_blank || ''
|
||||
const clozeCorrectWord = q?.cloze?.correct_word || ''
|
||||
const clozeSentence = q?.cloze?.sentence || ''
|
||||
const clozeDistractorWords = Array.isArray(q?.cloze?.distractor_words) ? q.cloze.distractor_words : []
|
||||
const clozeOptions = this.shuffleArray([clozeCorrectWord, ...clozeDistractorWords].filter((w) => !!w))
|
||||
const clozeParts = typeof clozeSentenceWithBlank === 'string' ? clozeSentenceWithBlank.split('___') : [clozeSentenceWithBlank || '', '']
|
||||
const { blanked: clozeSentenceWithBlank, parts: clozeParts, tokens: clozeSentenceTokens } = this.buildClozeSentence(clozeSentence, clozeCorrectWord)
|
||||
const questionText = String(q?.question || '')
|
||||
const questionWords = questionText.split(/\s+/).filter((w) => !!w)
|
||||
this.setData({
|
||||
@@ -681,6 +856,7 @@ Page<IData, IPageInstance>({
|
||||
clozeOptions,
|
||||
selectedClozeIndex: -1,
|
||||
clozeParts,
|
||||
clozeSentenceTokens,
|
||||
answers: q.answers || {},
|
||||
freeTextInput: '',
|
||||
questionWords
|
||||
@@ -699,8 +875,17 @@ Page<IData, IPageInstance>({
|
||||
}
|
||||
this.updateProcessDots()
|
||||
},
|
||||
switchMode(mode: QuestionMode) {
|
||||
if (this.data.fixedMode) return
|
||||
async switchMode(arg: any) {
|
||||
if (!this.data.contentReady || this.data.loadingMaskVisible) return
|
||||
|
||||
let mode: QuestionMode | undefined
|
||||
if (arg && typeof arg === 'object' && arg.currentTarget && arg.currentTarget.dataset) {
|
||||
mode = arg.currentTarget.dataset.mode
|
||||
} else if (typeof arg === 'string') {
|
||||
mode = arg as QuestionMode
|
||||
}
|
||||
|
||||
if (!mode) return
|
||||
|
||||
let target: QuestionMode = mode
|
||||
if (!ENABLED_QUESTION_MODES.includes(target)) {
|
||||
@@ -709,10 +894,66 @@ Page<IData, IPageInstance>({
|
||||
if (this.data.isModeSwitching || target === this.data.questionMode) {
|
||||
return
|
||||
}
|
||||
|
||||
// Immediately update questionMode to reflect user selection visually
|
||||
// Save previous mode in case we need to revert
|
||||
const previousMode = this.data.questionMode
|
||||
this.setData({ questionMode: target })
|
||||
|
||||
// Special handling for Variation mode switching
|
||||
logger.log('Switching to mode:', target)
|
||||
if (target === QUESTION_MODES.VARIATION) {
|
||||
if (!this.data.variationQaList || this.data.variationQaList.length === 0) {
|
||||
try {
|
||||
await this.fetchVariationExercises(this.data.imageId)
|
||||
} catch (e) {
|
||||
// If fetch fails, revert mode
|
||||
this.setData({ questionMode: previousMode })
|
||||
return
|
||||
}
|
||||
}
|
||||
logger.log('Variation mode data:', this.data.variationQaList)
|
||||
if (this.data.variationQaList && this.data.variationQaList.length > 0) {
|
||||
this.setData({ isModeSwitching: true, modeAnim: 'fade-out' })
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
qaList: this.data.variationQaList,
|
||||
currentIndex: 0,
|
||||
modeAnim: 'fade-in',
|
||||
isModeSwitching: false
|
||||
})
|
||||
this.switchQuestion(0)
|
||||
}, 300)
|
||||
} else {
|
||||
// Should not happen if fetch succeeded, but as safeguard
|
||||
this.setData({ questionMode: previousMode })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Switching FROM Variation TO others
|
||||
if (previousMode === QUESTION_MODES.VARIATION) {
|
||||
if (this.data.mainQaList && this.data.mainQaList.length > 0) {
|
||||
this.setData({ isModeSwitching: true, modeAnim: 'fade-out' })
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
qaList: this.data.mainQaList,
|
||||
currentIndex: 0,
|
||||
modeAnim: 'fade-in',
|
||||
isModeSwitching: false
|
||||
})
|
||||
this.switchQuestion(0)
|
||||
}, 300)
|
||||
} else {
|
||||
// Fallback if mainQaList missing
|
||||
this.fetchQaExercises(this.data.imageId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ isModeSwitching: true, modeAnim: 'fade-out' })
|
||||
setTimeout(() => {
|
||||
const resetData: any = {
|
||||
questionMode: target,
|
||||
selectedOptionIndexes: [],
|
||||
selectedIndex: -1,
|
||||
freeTextInput: '',
|
||||
@@ -744,6 +985,7 @@ Page<IData, IPageInstance>({
|
||||
}
|
||||
this.updateActionButtonsState()
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.setData({ modeAnim: '', isModeSwitching: false })
|
||||
}, 300)
|
||||
@@ -755,6 +997,7 @@ Page<IData, IPageInstance>({
|
||||
if (mode === QUESTION_MODES.CHOICE) return (resultObj.choice || {}).evaluation || null
|
||||
if (mode === QUESTION_MODES.CLOZE) return (resultObj.cloze || {}).evaluation || null
|
||||
if (mode === QUESTION_MODES.FREE_TEXT) return (resultObj.free_text || {}).evaluation || null
|
||||
if (mode === QUESTION_MODES.VARIATION) return (resultObj.variation || {}).evaluation || null
|
||||
return null
|
||||
} catch (e) {
|
||||
return null
|
||||
@@ -828,6 +1071,28 @@ Page<IData, IPageInstance>({
|
||||
if (text) {
|
||||
this.setData({ freeTextInput: text })
|
||||
}
|
||||
} else if (this.data.questionMode === QUESTION_MODES.VARIATION) {
|
||||
const base = (r as any).variation || {}
|
||||
const fileId = String(base.file_id || '')
|
||||
const detail = String(evaluation?.detail || '').toLowerCase()
|
||||
|
||||
let status: 'correct' | 'incorrect' | undefined
|
||||
if (detail === 'correct' || detail === 'exact' || detail === '完全正确') {
|
||||
status = 'correct'
|
||||
} else if (detail === 'incorrect' || detail === '错误') {
|
||||
status = 'incorrect'
|
||||
}
|
||||
|
||||
// Find index of the selected fileId
|
||||
const list = this.data.variationQaList || []
|
||||
const idx = list.findIndex(item => String(item.file_id || (item.ext || {}).file_id || '') === fileId)
|
||||
|
||||
this.setData({
|
||||
variationSelectedIndex: idx >= 0 ? idx : -1,
|
||||
variationResultStatus: status,
|
||||
variationSubmitted: true,
|
||||
resultDisplayed: true
|
||||
})
|
||||
}
|
||||
},
|
||||
onSubmitTap() {
|
||||
@@ -971,6 +1236,11 @@ Page<IData, IPageInstance>({
|
||||
this.updateActionButtonsState()
|
||||
return
|
||||
}
|
||||
if (this.data.questionMode === QUESTION_MODES.VARIATION) {
|
||||
this.setData({ variationSelectedIndex: -1, variationSubmitted: false, variationResultStatus: null as any, resultDisplayed: false })
|
||||
this.updateActionButtonsState()
|
||||
return
|
||||
}
|
||||
},
|
||||
async onHintTap() {
|
||||
if (this.data.questionMode !== QUESTION_MODES.FREE_TEXT) {
|
||||
@@ -1133,8 +1403,12 @@ Page<IData, IPageInstance>({
|
||||
return
|
||||
}
|
||||
|
||||
const exerciseId = this.data.questionMode === QUESTION_MODES.VARIATION
|
||||
? (this.data.variationExerciseId || String(this.data.exercise?.id || ''))
|
||||
: String(this.data.exercise?.id || '')
|
||||
|
||||
const payload: any = {
|
||||
exercise_id: String(this.data.exercise?.id || ''),
|
||||
exercise_id: exerciseId,
|
||||
mode: this.data.questionMode
|
||||
}
|
||||
if (this.data.isTrialMode) {
|
||||
@@ -1154,6 +1428,16 @@ Page<IData, IPageInstance>({
|
||||
}
|
||||
} else if (this.data.questionMode === QUESTION_MODES.FREE_TEXT) {
|
||||
payload.input_text = this.data.freeTextInput
|
||||
} else if (this.data.questionMode === QUESTION_MODES.VARIATION) {
|
||||
const idx = typeof this.data.variationSelectedIndex === 'number' ? this.data.variationSelectedIndex : -1
|
||||
if (idx < 0) {
|
||||
wx.showToast({ title: '请先选择图片', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const item = (this.data.variationQaList || [])[idx] || {}
|
||||
const fileId = String(item?.file_id || (item?.ext || {}).file_id || '')
|
||||
// 复用选择题的 selected_options 结构提交
|
||||
payload.selected_options = fileId ? [fileId] : []
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '提交中' })
|
||||
@@ -1178,11 +1462,12 @@ Page<IData, IPageInstance>({
|
||||
this.setData({ qaResultFetched: fetched })
|
||||
|
||||
await this.ensureQuestionResultFetched(String(qid))
|
||||
|
||||
wx.hideLoading()
|
||||
this.setData({ choiceSubmitted: true, submitDisabled: true, retryDisabled: false })
|
||||
|
||||
if (this.data.questionMode !== QUESTION_MODES.VARIATION) {
|
||||
this.setData({ choiceSubmitted: true, submitDisabled: true, retryDisabled: false })
|
||||
}
|
||||
|
||||
// 检查自动跳转
|
||||
const cache = this.data.qaResultCache || {}
|
||||
const res = cache[k]
|
||||
const mode = this.data.questionMode
|
||||
@@ -1190,18 +1475,30 @@ Page<IData, IPageInstance>({
|
||||
if (mode === QUESTION_MODES.CHOICE) evaluation = res.choice?.evaluation
|
||||
else if (mode === QUESTION_MODES.CLOZE) evaluation = res.cloze?.evaluation
|
||||
else if (mode === QUESTION_MODES.FREE_TEXT) evaluation = res.free_text?.evaluation
|
||||
|
||||
else if (mode === QUESTION_MODES.VARIATION) evaluation = res.variation?.evaluation
|
||||
|
||||
if (evaluation) {
|
||||
this.triggerAutoNextIfCorrect(evaluation, String(qid))
|
||||
this.triggerAutoNextIfCorrect(evaluation, String(qid))
|
||||
}
|
||||
this.checkAllQuestionsAttempted()
|
||||
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
logger.error('提交失败', e)
|
||||
wx.showToast({ title: '提交失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
selectVariationOption(e: any) {
|
||||
if (this.data.resultDisplayed) return
|
||||
let i = -1
|
||||
if (typeof e === 'number') {
|
||||
i = e
|
||||
} else {
|
||||
i = Number(e?.currentTarget?.dataset?.index)
|
||||
if (isNaN(i)) i = -1
|
||||
}
|
||||
this.setData({ variationSelectedIndex: i, variationResultStatus: null as any })
|
||||
this.updateActionButtonsState()
|
||||
},
|
||||
async getQuestionAudio(questionId: string) {
|
||||
try {
|
||||
const map = this.data.audioUrlMap || {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<view class="qa-exercise-container">
|
||||
<view wx:if="{{!contentReady}}" class="page-loading-mask">
|
||||
|
||||
<view wx:if="{{!contentReady || loadingMaskVisible}}" class="page-loading-mask">
|
||||
<view class="loading-center">
|
||||
<view class="scanner scanner-visible">
|
||||
<view class="star star1"></view>
|
||||
@@ -10,13 +11,19 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="container {{contentVisible ? 'fade-in' : 'fade-out'}}" wx:if="{{contentReady}}">
|
||||
<view class="type-container" wx:if="{{fixedMode}}">
|
||||
<view class="type-item {{questionMode === 'cloze' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="cloze">填空</view>
|
||||
<view class="type-item {{questionMode === 'choice' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="choice">问答</view>
|
||||
<view class="type-item {{questionMode === 'variation' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="variation">扩展训练</view>
|
||||
</view>
|
||||
|
||||
<view class="container {{contentVisible ? 'fade-in' : 'fade-out'}}" wx:if="{{contentReady && !loadingMaskVisible}}">
|
||||
<view class="process-container" wx:if="{{qaList && qaList.length > 0}}">
|
||||
<block wx:for="{{qaList}}" wx:key="index">
|
||||
<view class="process-dot {{processDotClasses[index]}} {{index === currentIndex ? 'current' : ''}}"></view>
|
||||
</block>
|
||||
</view>
|
||||
<view class="image-card">
|
||||
<view class="image-card" wx:if="{{questionMode !== 'variation'}}">
|
||||
<image wx:if="{{imageLocalUrl}}" class="image" src="{{imageLocalUrl}}" mode="aspectFill" bindtap="previewImage" bindload="onImageLoad" binderror="onImageError"></image>
|
||||
<view class="view-full" wx:if="{{imageLocalUrl}}" bindtap="previewImage">
|
||||
<t-icon name="zoom-in" size="32rpx" />
|
||||
@@ -40,11 +47,9 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="question-content {{modeAnim}}" wx:if="{{questionMode === 'cloze'}}">
|
||||
<view class="choice-title">Select the correct word to complete the sentence:</view>
|
||||
<!-- <view class="choice-title">Select the correct word to complete the sentence:</view> -->
|
||||
<view class="cloze-sentence">
|
||||
<text class="cloze-text">{{clozeParts[0]}}</text>
|
||||
<text class="cloze-fill">_____</text>
|
||||
<text class="cloze-text">{{clozeParts[1]}}</text>
|
||||
<text wx:for="{{clozeSentenceTokens}}" wx:key="index" class="{{item.isBlank ? 'cloze-fill' : 'cloze-text'}}" data-word="{{item.word}}" bindtap="handleWordClick">{{item.text}}</text>
|
||||
</view>
|
||||
<view class="option-list">
|
||||
<view wx:for="{{clozeOptions}}" wx:key="index" class="option-item {{evalClasses[index]}} {{resultDisplayed ? 'disabled' : ''}}" data-index="{{index}}" data-word="{{item}}" bindtap="selectClozeOption" bindlongpress="onOptionLongPress">
|
||||
@@ -69,6 +74,29 @@
|
||||
<text class="input-type">English input</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="question-content {{modeAnim}}" wx:if="{{questionMode === 'variation'}}">
|
||||
<view class="variation-container">
|
||||
<view class="variation-grid">
|
||||
<view class="variation-item" wx:for="{{variationQaList}}" wx:key="index">
|
||||
<view class="variation-image-wrapper {{index === variationSelectedIndex ? 'selected' : ''}}" data-index="{{index}}" bindtap="selectVariationOption">
|
||||
<cloud-image
|
||||
file-id="{{item.file_id}}"
|
||||
mode="widthFix"
|
||||
height="auto"
|
||||
radius="24rpx"
|
||||
bind:load="onVariationImageLoad"
|
||||
data-fileid="{{item.file_id}}"
|
||||
/>
|
||||
<view wx:if="{{variationImageLoaded[item.file_id]}}" class="selection-badge {{variationResultStatus === 'incorrect' && index === variationSelectedIndex ? 'incorrect' : (index === variationSelectedIndex ? 'selected' : 'unselected')}}">
|
||||
<t-icon name="{{variationResultStatus === 'incorrect' && index === variationSelectedIndex ? 'close-circle' : 'check-circle'}}" size="36rpx" color="#fff" />
|
||||
</view>
|
||||
<view wx:if="{{variationResultStatus && index === variationSelectedIndex}}" class="variation-border {{variationResultStatus}}"></view>
|
||||
</view>
|
||||
<!-- <view class="variation-text">{{item.question}}</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="submit-row">
|
||||
<button class="submit-btn" bindtap="onSubmitTap" disabled="{{submitDisabled}}" wx:if="{{retryDisabled}}">提交</button>
|
||||
|
||||
@@ -348,6 +348,83 @@
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
/* Variation Mode Styles */
|
||||
.variation-container {
|
||||
width: 100%;
|
||||
}
|
||||
.variation-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24rpx;
|
||||
width: 100%;
|
||||
}
|
||||
.variation-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.variation-image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 200rpx;
|
||||
}
|
||||
|
||||
.variation-image-wrapper.selected {
|
||||
}
|
||||
|
||||
.variation-border {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 24rpx;
|
||||
border-width: 4rpx;
|
||||
border-style: solid;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
}
|
||||
.variation-border.correct {
|
||||
border-color: #21cc80;
|
||||
}
|
||||
.variation-border.incorrect {
|
||||
border-color: #e34d59;
|
||||
}
|
||||
|
||||
.selection-badge {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
right: 12rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.selection-badge.unselected {
|
||||
background-color: rgba(0,0,0,0.25);
|
||||
}
|
||||
.selection-badge.selected {
|
||||
background-color: #21cc80;
|
||||
box-shadow: 0 4rpx 12rpx rgba(33, 204, 128, 0.4);
|
||||
}
|
||||
.selection-badge.incorrect {
|
||||
background-color: #e34d59;
|
||||
box-shadow: 0 4rpx 12rpx rgba(227, 77, 89, 0.3);
|
||||
}
|
||||
|
||||
.variation-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.confetti.c1 { background: #FFD700; top: -10rpx; left: 10rpx; transform: rotate(-15deg); }
|
||||
.confetti.c2 { background: #4facfe; top: 20rpx; right: -10rpx; transform: rotate(25deg); }
|
||||
.confetti.c3 { background: #a18cd1; bottom: 0; left: -20rpx; transform: rotate(-45deg); }
|
||||
@@ -407,3 +484,37 @@
|
||||
z-index: 9999;
|
||||
pointer-events: none; /* 确保canvas不会阻挡点击事件 */
|
||||
}
|
||||
|
||||
.type-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 40rpx;
|
||||
padding: 8rpx;
|
||||
width: fit-content;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.type-item {
|
||||
padding: 12rpx 48rpx;
|
||||
border-radius: 32rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.type-item.active {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.type-item-hover {
|
||||
opacity: 0.7;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
@@ -92,8 +92,7 @@
|
||||
</view>
|
||||
<view wx:if="{{image.more_open || image.menu_closing}}" class="image-more-menu {{image.menu_closing ? 'closing' : 'opening'}}">
|
||||
<view class="more-item-mini" bindtap="onImageSceneSentenceTap" data-image-id="{{image.image_id}}">场景句型</view>
|
||||
<view class="more-item-mini" bindtap="onImageQaExerciseTap" data-image-id="{{image.image_id}}" data-thumbnail-id="{{image.thumbnail_file_id}}" data-type="choice">问答练习</view>
|
||||
<view class="more-item-mini" bindtap="onImageQaExerciseTap" data-image-id="{{image.image_id}}" data-thumbnail-id="{{image.thumbnail_file_id}}" data-type="cloze">完形填空</view>
|
||||
<view class="more-item-mini" bindtap="onImageQaExerciseTap" data-image-id="{{image.image_id}}" data-thumbnail-id="{{image.thumbnail_file_id}}" data-type="cloze">场景练习</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -115,8 +114,7 @@
|
||||
</view>
|
||||
<view wx:if="{{item.more_open || item.menu_closing}}" class="image-more-menu {{item.menu_closing ? 'closing' : 'opening'}}">
|
||||
<view class="more-item-mini" bindtap="onImageSceneSentenceTap" data-image-id="{{item.image_id}}">场景句型</view>
|
||||
<view class="more-item-mini" bindtap="onImageQaExerciseTap" data-image-id="{{item.image_id}}" data-thumbnail-id="{{item.thumbnail_file_id}}" data-type="choice">问答练习</view>
|
||||
<view class="more-item-mini" bindtap="onImageQaExerciseTap" data-image-id="{{item.image_id}}" data-thumbnail-id="{{item.thumbnail_file_id}}" data-type="cloze">完形填空</view>
|
||||
<view class="more-item-mini" bindtap="onImageQaExerciseTap" data-image-id="{{item.image_id}}" data-thumbnail-id="{{item.thumbnail_file_id}}" data-type="cloze">场景练习</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -135,8 +133,7 @@
|
||||
</view>
|
||||
<view wx:if="{{item.more_open || item.menu_closing}}" class="image-more-menu {{item.menu_closing ? 'closing' : 'opening'}}">
|
||||
<view class="more-item-mini" bindtap="onImageSceneSentenceTap" data-image-id="{{item.image_id}}">场景句型</view>
|
||||
<view class="more-item-mini" bindtap="onImageQaExerciseTap" data-image-id="{{item.image_id}}" data-thumbnail-id="{{item.thumbnail_file_id}}" data-type="choice">问答练习</view>
|
||||
<view class="more-item-mini" bindtap="onImageQaExerciseTap" data-image-id="{{item.image_id}}" data-thumbnail-id="{{item.thumbnail_file_id}}" data-type="cloze">完形填空</view>
|
||||
<view class="more-item-mini" bindtap="onImageQaExerciseTap" data-image-id="{{item.image_id}}" data-thumbnail-id="{{item.thumbnail_file_id}}" data-type="cloze">场景练习</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -2090,10 +2090,9 @@ class ApiManager {
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async createQaExerciseTask(imageId: number | string, title?: string, description?: string): Promise<IQaExerciseCreateAccepted> {
|
||||
async createQaExerciseTask(imageId: number | string, type?: string): Promise<IQaExerciseCreateAccepted> {
|
||||
const payload: Record<string, any> = { image_id: imageId }
|
||||
if (title !== undefined) payload.title = title
|
||||
if (description !== undefined) payload.description = description
|
||||
if (type !== undefined) payload.type = type
|
||||
const resp = await this.request<IQaExerciseCreateAccepted>(`/api/v1/qa/exercises/tasks`, 'POST', payload)
|
||||
return resp.data
|
||||
}
|
||||
@@ -2103,8 +2102,12 @@ class ApiManager {
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async listQaExercisesByImage(imageId: string): Promise<IQaExerciseQueryResponse | null> {
|
||||
const resp = await this.request<IQaExerciseQueryResponse | null>(`/api/v1/qa/${imageId}/exercises`, 'GET')
|
||||
async listQaExercisesByImage(imageId: string, type?: string): Promise<IQaExerciseQueryResponse | null> {
|
||||
let url = `/api/v1/qa/${imageId}/exercises`
|
||||
if (type) {
|
||||
url += `?type=${type}`
|
||||
}
|
||||
const resp = await this.request<IQaExerciseQueryResponse | null>(url, 'GET')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user