add qa
This commit is contained in:
@@ -11,7 +11,8 @@
|
||||
"pages/analyze/analyze",
|
||||
"pages/coupon/coupon",
|
||||
"pages/order/order",
|
||||
"pages/scene_sentence/scene_sentence"
|
||||
"pages/scene_sentence/scene_sentence",
|
||||
"pages/qa_exercise/qa_exercise"
|
||||
],
|
||||
"window": {
|
||||
"navigationBarTextStyle": "black",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import apiManager from '../../utils/api'
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
visible: { type: Boolean, value: false },
|
||||
@@ -6,6 +8,7 @@ Component({
|
||||
wordDict: { type: Object, value: {} },
|
||||
showBackIcon: { type: Boolean, value: false },
|
||||
prototypeWord: { type: String, value: '' },
|
||||
forceHidePrototype: { type: Boolean, value: false },
|
||||
isWordEmptyResult: { type: Boolean, value: false },
|
||||
dictDefaultTabValue: { type: String, value: '0' },
|
||||
activeWordAudioType: { type: String, value: '' },
|
||||
@@ -13,6 +16,85 @@ Component({
|
||||
wordAudioIconName: { type: String, value: 'sound' }
|
||||
},
|
||||
methods: {
|
||||
async queryWord(word: string) {
|
||||
const self = this as any
|
||||
const raw = String(word || '')
|
||||
const cleaned = raw.replace(/[.,?!*;:'"()]/g, '').trim()
|
||||
if (!cleaned) return
|
||||
self.setData({ visible: true, loading: true })
|
||||
try {
|
||||
const detail: any = await apiManager.getWordDetail(cleaned)
|
||||
const collins = detail['collins']
|
||||
let processedCollins = collins
|
||||
if (collins && collins.collins_entries) {
|
||||
const cloned = JSON.parse(JSON.stringify(collins))
|
||||
cloned.collins_entries.forEach((entry: any) => {
|
||||
if (entry.entries && entry.entries.entry) {
|
||||
entry.entries.entry.forEach((entryItem: any) => {
|
||||
if (entryItem.tran_entry) {
|
||||
entryItem.tran_entry.forEach((tranEntry: any) => {
|
||||
if (tranEntry.tran) {
|
||||
const parts = tranEntry.tran.split(/(<b>.*?<\/b>)/g).filter(Boolean)
|
||||
const processed: Array<{ text: string; bold: boolean }> = []
|
||||
parts.forEach((part: string) => {
|
||||
if (part.startsWith('<b>') && part.endsWith('</b>')) {
|
||||
const text = part.substring(3, part.length - 4)
|
||||
processed.push({ text, bold: true })
|
||||
} else {
|
||||
processed.push({ text: part, bold: false })
|
||||
}
|
||||
})
|
||||
tranEntry.tranParts = processed
|
||||
tranEntry.originalTran = tranEntry.tran
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
processedCollins = cloned
|
||||
}
|
||||
const rawRelWord = detail['rel_word']
|
||||
let sanitizedRelWord = rawRelWord
|
||||
if (rawRelWord && rawRelWord.rels && Array.isArray(rawRelWord.rels)) {
|
||||
sanitizedRelWord = {
|
||||
...rawRelWord,
|
||||
rels: rawRelWord.rels.map((entry: any) => {
|
||||
const rel = entry.rel || {}
|
||||
const pos = rel.pos
|
||||
const cleanPos = typeof pos === 'string' ? pos.replace(/\[|\]/g, '') : pos
|
||||
return { ...entry, rel: { ...rel, pos: cleanPos } }
|
||||
})
|
||||
}
|
||||
}
|
||||
const hasEE = !!detail['ee']
|
||||
const hasEC = !!(detail['ec'] && detail['ec'].word && detail['ec'].word.length > 0)
|
||||
const hasCollins = !!(processedCollins && processedCollins.collins_entries && processedCollins.collins_entries.length > 0)
|
||||
const isEmpty = !(hasEE || hasEC || hasCollins)
|
||||
const hasPhrs = !!(detail['phrs'] && detail['phrs'].phrs && detail['phrs'].phrs.length > 0)
|
||||
const hasPastExam = !!(detail['individual'] && detail['individual'].pastExamSents && detail['individual'].pastExamSents.length > 0)
|
||||
const defaultTab = hasCollins ? '0' : (hasPhrs ? '1' : (hasPastExam ? '2' : '3'))
|
||||
const proto = (detail.ee?.word?.['return-phrase']?.['l']?.['i']) || (detail?.ec?.word?.[0]?.prototype)
|
||||
const hideProto = !!self.data.forceHidePrototype
|
||||
self.setData({
|
||||
wordDict: {
|
||||
ee: detail['ee'], ec: detail['ec'], expandEc: detail['expand_ec'],
|
||||
simple: detail['simple'], phrs: detail['phrs'], etym: detail['etym'],
|
||||
individual: detail['individual'], collins: processedCollins,
|
||||
relWord: sanitizedRelWord, syno: detail['syno'],
|
||||
discriminate: detail['discriminate']
|
||||
},
|
||||
dictDefaultTabValue: defaultTab,
|
||||
isWordEmptyResult: isEmpty,
|
||||
prototypeWord: hideProto ? '' : proto,
|
||||
loading: false,
|
||||
visible: true
|
||||
})
|
||||
} catch (err) {
|
||||
self.setData({ loading: false })
|
||||
self.triggerEvent('error', { message: 'queryFailed' })
|
||||
}
|
||||
},
|
||||
onClose() {
|
||||
const self = this as any
|
||||
try { self.wordAudioContext && self.wordAudioContext.stop() } catch (e) {}
|
||||
@@ -128,7 +210,8 @@ Component({
|
||||
const propWord = (self.data && self.data.prototypeWord) ? self.data.prototypeWord : ''
|
||||
const word = dsWord || propWord || ''
|
||||
if (!word) return
|
||||
self.triggerEvent('wordTap', { word })
|
||||
const prev = (self.data && self.data.wordDict && self.data.wordDict.simple && self.data.wordDict.simple.query) ? self.data.wordDict.simple.query : ''
|
||||
self.triggerEvent('wordTap', { word, previousWord: prev })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<t-icon wx:if="{{showBackIcon}}" name="chevron-left" size="40rpx" bindtap="onBack"></t-icon>
|
||||
{{wordDict.simple.query}}
|
||||
</view>
|
||||
<view class="word-source" wx:if="{{prototypeWord}}">词源: <t-tag variant="light" theme="primary" data-word="{{prototypeWord}}" bindtap="onWordTap">{{prototypeWord}}</t-tag></view>
|
||||
<view class="word-source" wx:if="{{prototypeWord && !prototypeWord !== ''}}">词源: <t-tag variant="light" theme="primary" data-word="{{prototypeWord}}" bindtap="onWordTap">{{prototypeWord}}</t-tag></view>
|
||||
<view class="more-btn" wx:if="{{!isWordEmptyResult}}" bindtap="onMore">
|
||||
<text wx:if="{{!expanded}}">More</text>
|
||||
<t-icon wx:if="{{!expanded}}" name="chevron-up" size="48rpx"></t-icon>
|
||||
|
||||
@@ -287,6 +287,7 @@ Page<IPageData, IPageInstance>({
|
||||
showBackIcon: false, // 是否显示返回图标
|
||||
previousWord: '', // 上一个单词
|
||||
isReturningFromPrevious: false, // 是否正在从上一个单词返回
|
||||
forceHidePrototype: false,
|
||||
isWordEmptyResult: false,
|
||||
overlayVisible: false,
|
||||
highlightWords: [],
|
||||
@@ -967,7 +968,10 @@ Page<IPageData, IPageInstance>({
|
||||
},
|
||||
|
||||
async handleWordClick(e: any) {
|
||||
const { word, index } = e.currentTarget.dataset
|
||||
const ds = (e && e.currentTarget && e.currentTarget.dataset) ? e.currentTarget.dataset : {}
|
||||
const dt = (e && e.detail) ? e.detail : {}
|
||||
const word: string = String(ds.word || dt.word || '')
|
||||
const index = typeof ds.index === 'number' ? ds.index : (typeof dt.index === 'number' ? dt.index : undefined)
|
||||
// 若例句未被选中,优先选中例句并阻止单词选择
|
||||
const { selectedSentenceIndex, justSelectedByWord } = this.data
|
||||
if (typeof index === 'number' && index !== selectedSentenceIndex) {
|
||||
@@ -1017,76 +1021,33 @@ Page<IPageData, IPageInstance>({
|
||||
// 只有通过 word-source 标签点击且不是从返回功能调用时才保存当前单词作为上一个单词,并显示返回图标
|
||||
let showBackIcon = false;
|
||||
let previousWord = '';
|
||||
|
||||
if (isFromWordSource && !isReturningFromPrevious) {
|
||||
// 保存当前单词作为上一个单词,并显示返回图标
|
||||
previousWord = this.data.wordDict?.simple?.query || '';
|
||||
previousWord = String(dt.previousWord || '');
|
||||
showBackIcon = !!previousWord;
|
||||
} else if (isReturningFromPrevious) {
|
||||
showBackIcon = false;
|
||||
previousWord = '';
|
||||
}
|
||||
|
||||
// 注意:这里不立即设置 showBackIcon 和 previousWord,而是在获取单词详情后再设置
|
||||
// 重置返回标记位
|
||||
this.setData({
|
||||
prototypeWord: '',
|
||||
isReturningFromPrevious: false
|
||||
isReturningFromPrevious: false,
|
||||
forceHidePrototype: (isFromWordSource && !isReturningFromPrevious)
|
||||
});
|
||||
this.setData({ showBackIcon, previousWord })
|
||||
|
||||
try {
|
||||
// 调用API获取单词详情
|
||||
this.setData({ dictLoading: true, showDictPopup: true })
|
||||
const wordDetail: any = await apiManager.getWordDetail(cleanedWord);
|
||||
logger.info('获取到单词详情:', wordDetail);
|
||||
|
||||
// 处理 Collins 数据中的 HTML 标签
|
||||
const processedCollins = wordDetail['collins'] ? this.processCollinsData(wordDetail['collins']) : wordDetail['collins'];
|
||||
const rawRelWord = wordDetail['rel_word']
|
||||
let sanitizedRelWord = rawRelWord
|
||||
if (rawRelWord && rawRelWord.rels && Array.isArray(rawRelWord.rels)) {
|
||||
sanitizedRelWord = {
|
||||
...rawRelWord,
|
||||
rels: rawRelWord.rels.map((entry: any) => {
|
||||
const rel = entry.rel || {}
|
||||
const pos = rel.pos
|
||||
const cleanPos = typeof pos === 'string' ? pos.replace(/\[|\]/g, '') : pos
|
||||
return { ...entry, rel: { ...rel, pos: cleanPos } }
|
||||
})
|
||||
}
|
||||
this.setData({ showDictPopup: true })
|
||||
const comp = this.selectComponent('#wordDict') as any
|
||||
if (comp && typeof comp.queryWord === 'function') {
|
||||
try {
|
||||
wx.nextTick(() => {
|
||||
comp.queryWord(cleanedWord)
|
||||
})
|
||||
} catch (e) {
|
||||
setTimeout(() => comp.queryWord(cleanedWord), 0)
|
||||
}
|
||||
|
||||
// 这里可以添加处理单词详情的逻辑,比如显示在页面上
|
||||
const hasEE = !!wordDetail['ee']
|
||||
const hasEC = !!(wordDetail['ec'] && wordDetail['ec'].word && wordDetail['ec'].word.length > 0)
|
||||
const hasCollins = !!(processedCollins && processedCollins.collins_entries && processedCollins.collins_entries.length > 0)
|
||||
const isWordEmptyResult = !(hasEE || hasEC || hasCollins)
|
||||
|
||||
const hasPhrs = !!(wordDetail['phrs'] && wordDetail['phrs'].phrs && wordDetail['phrs'].phrs.length > 0)
|
||||
const hasPastExam = !!(wordDetail['individual'] && wordDetail['individual'].pastExamSents && wordDetail['individual'].pastExamSents.length > 0)
|
||||
const hasRelated = !!((wordDetail['syno'] && wordDetail['syno'].synos && wordDetail['syno'].synos.length > 0) || (sanitizedRelWord && sanitizedRelWord.rels && sanitizedRelWord.rels.length > 0) || (wordDetail['discriminate'] && wordDetail['discriminate'].data && wordDetail['discriminate'].data.length > 0))
|
||||
const dictDefaultTabValue = hasCollins ? '0' : (hasPhrs ? '1' : (hasPastExam ? '2' : '3'))
|
||||
|
||||
this.setData({
|
||||
showDictPopup: true,
|
||||
wordDict: {
|
||||
ee:wordDetail['ee'], ec:wordDetail['ec'], expandEc:wordDetail['expand_ec'],
|
||||
simple:wordDetail['simple'], phrs:wordDetail['phrs'], etym:wordDetail['etym'],
|
||||
individual:wordDetail['individual'], collins: processedCollins,
|
||||
relWord: sanitizedRelWord, syno: wordDetail['syno'],
|
||||
discriminate: wordDetail['discriminate']
|
||||
},
|
||||
prototypeWord: (wordDetail?.ec?.word?.[0]?.prototype) || '',
|
||||
showBackIcon: showBackIcon, // 只有通过 word-source 点击且不是从返回功能调用时才显示返回图标
|
||||
previousWord: previousWord, // 只有通过 word-source 点击且不是从返回功能调用时才保存上一个单词
|
||||
isWordEmptyResult,
|
||||
dictDefaultTabValue
|
||||
});
|
||||
this.setData({ dictLoading: false })
|
||||
} catch (error) {
|
||||
logger.error('获取单词详情失败:', error)
|
||||
wx.showToast({
|
||||
title: '获取单词详情失败',
|
||||
icon: 'none'
|
||||
})
|
||||
this.setData({ dictLoading: false })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2016,7 +1977,8 @@ Page<IPageData, IPageInstance>({
|
||||
|
||||
// 设置返回标记位
|
||||
this.setData({
|
||||
isReturningFromPrevious: true
|
||||
isReturningFromPrevious: true,
|
||||
forceHidePrototype: false
|
||||
});
|
||||
|
||||
// 调用 handleWordClick 方法查询上一个单词
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
bind:play="playAssessmentVoice"
|
||||
/>
|
||||
<word-dictionary
|
||||
id="wordDict"
|
||||
visible="{{showDictPopup}}"
|
||||
expanded="{{showDictExtended}}"
|
||||
loading="{{dictLoading}}"
|
||||
@@ -103,6 +104,7 @@
|
||||
showBackIcon="{{showBackIcon}}"
|
||||
prototypeWord="{{prototypeWord}}"
|
||||
isWordEmptyResult="{{isWordEmptyResult}}"
|
||||
forceHidePrototype="{{forceHidePrototype}}"
|
||||
dictDefaultTabValue="{{dictDefaultTabValue}}"
|
||||
activeWordAudioType="{{activeWordAudioType}}"
|
||||
wordAudioPlaying="{{wordAudioPlaying}}"
|
||||
|
||||
13
miniprogram/pages/qa_exercise/qa_exercise.json
Normal file
13
miniprogram/pages/qa_exercise/qa_exercise.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"navigationBarTitleText": "问答练习",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"backgroundColor": "#ffffff",
|
||||
"backgroundTextStyle": "light",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton",
|
||||
"word-dictionary": "../../components/word-dictionary/word-dictionary"
|
||||
}
|
||||
}
|
||||
1071
miniprogram/pages/qa_exercise/qa_exercise.ts
Normal file
1071
miniprogram/pages/qa_exercise/qa_exercise.ts
Normal file
File diff suppressed because it is too large
Load Diff
177
miniprogram/pages/qa_exercise/qa_exercise.wxml
Normal file
177
miniprogram/pages/qa_exercise/qa_exercise.wxml
Normal file
@@ -0,0 +1,177 @@
|
||||
<view class="qa-exercise-container">
|
||||
<view wx:if="{{!contentReady}}" class="page-loading-mask">
|
||||
<view class="loading-center">
|
||||
<view class="scanner scanner-visible">
|
||||
<view class="star star1"></view>
|
||||
<view class="star star2"></view>
|
||||
<view class="star star3"></view>
|
||||
</view>
|
||||
<view class="status-text">{{statusText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="container {{contentVisible ? 'fade-in' : 'fade-out'}}" wx:if="{{contentReady}}">
|
||||
<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">
|
||||
<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" />
|
||||
<!-- <text>View Full</text> -->
|
||||
</view>
|
||||
</view>
|
||||
<view class="question-title">
|
||||
<text wx:for="{{questionWords}}" wx:key="index" class="word-item" data-word="{{item}}" bindtap="handleWordClick">{{item}}</text>
|
||||
</view>
|
||||
<!-- <view class="progress-text">{{progressText}}</view> -->
|
||||
<scroll-view class="question-scroll" scroll-y="true">
|
||||
<view class="question-content {{modeAnim}}" wx:if="{{questionMode === 'choice'}}">
|
||||
<view class="choice-title">Select the correct answer ({{selectedCount}}/{{choiceRequiredCount}})</view>
|
||||
<view class="option-list">
|
||||
<view wx:for="{{choiceOptions}}" wx:key="index" class="option-item {{evalClasses[index]}} {{(!choiceSubmitted && !selectedFlags[index] && selectedCount >= choiceRequiredCount) ? 'disabled' : ''}}" data-index="{{index}}" data-word="{{item.content}}" bindtap="selectOption" bindlongpress="onOptionLongPress">
|
||||
<view class="option-radio">
|
||||
<view class="radio-dot {{selectedFlags[index] ? 'on' : ''}} {{(evalClasses[index] === 'opt-incorrect' && selectedFlags[index]) ? 'red' : ''}}"></view>
|
||||
</view>
|
||||
<text class="option-text">{{item.content}}</text>
|
||||
</view>
|
||||
</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="cloze-sentence">
|
||||
<text class="cloze-text">{{clozeParts[0]}}</text>
|
||||
<text class="cloze-fill">_____</text>
|
||||
<text class="cloze-text">{{clozeParts[1]}}</text>
|
||||
</view>
|
||||
<view class="option-list">
|
||||
<view wx:for="{{clozeOptions}}" wx:key="index" class="option-item {{evalClasses[index]}}" data-index="{{index}}" data-word="{{item}}" bindtap="selectClozeOption" bindlongpress="onOptionLongPress">
|
||||
<view class="option-radio">
|
||||
<view class="radio-dot {{selectedClozeIndex === index ? 'on' : ''}} {{(evalClasses[index] === 'opt-incorrect' && selectedClozeIndex === index) ? 'red' : ''}}"></view>
|
||||
</view>
|
||||
<text class="option-text">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="question-content {{modeAnim}}" wx:if="{{questionMode === 'free_text'}}">
|
||||
<view class="tip-row">
|
||||
<t-icon name="info-circle" size="32rpx" />
|
||||
<text class="tip-text">Tip: Be specific about the object type.</text>
|
||||
</view>
|
||||
<view class="answer-row">
|
||||
<text class="answer-label">Your Answer</text>
|
||||
<text class="answer-hint" bindtap="onHintTap">Need a hint?</text>
|
||||
</view>
|
||||
<view class="input-card">
|
||||
<textarea class="answer-input" placeholder="Type your answer here..." maxlength="200" bindinput="inputChange" value="{{freeTextInput}}" />
|
||||
<text class="input-type">English input</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="submit-row">
|
||||
<button class="submit-btn" bindtap="onSubmitTap" disabled="{{submitDisabled}}" wx:if="{{retryDisabled}}">提交</button>
|
||||
<button class="submit-btn" bindtap="onRetryTap" disabled="{{retryDisabled}}" wx:else>重试</button>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-bar {{contentVisible ? 'show' : ''}}">
|
||||
<t-icon name="chevron-left" class="bottom-btn {{currentIndex <= 0 ? 'disabled' : ''}}" size="48rpx" bind:tap="onPrevTap" />
|
||||
<t-icon name="{{isPlaying ? 'pause' : 'play'}}" class="bottom-btn" size="48rpx" bind:tap="playStandardVoice" />
|
||||
<t-icon name="swap" class="bottom-btn" size="48rpx" bind:tap="toggleMode" />
|
||||
<t-icon name="fact-check" class="bottom-btn {{resultDisplayed ? '' : 'disabled'}}" size="48rpx" bind:tap="onScoreTap" />
|
||||
<t-icon name="chevron-right" class="bottom-btn {{(qaList && (currentIndex >= qaList.length - 1)) ? 'disabled' : ''}}" size="48rpx" bind:tap="onNextTap" />
|
||||
</view>
|
||||
|
||||
<word-dictionary
|
||||
id="wordDict"
|
||||
visible="{{showDictPopup}}"
|
||||
expanded="{{showDictExtended}}"
|
||||
loading="{{dictLoading}}"
|
||||
wordDict="{{wordDict}}"
|
||||
showBackIcon="{{showBackIcon}}"
|
||||
prototypeWord="{{prototypeWord}}"
|
||||
forceHidePrototype="{{forceHidePrototype}}"
|
||||
isWordEmptyResult="{{isWordEmptyResult}}"
|
||||
dictDefaultTabValue="{{dictDefaultTabValue}}"
|
||||
activeWordAudioType="{{activeWordAudioType}}"
|
||||
wordAudioPlaying="{{wordAudioPlaying}}"
|
||||
wordAudioIconName="{{wordAudioIconName}}"
|
||||
bind:close="handleDictClose"
|
||||
bind:more="handleDictMore"
|
||||
bind:tabsChange="onTabsChange"
|
||||
bind:tabsClick="onTabsClick"
|
||||
bind:back="handleBackToPreviousWord"
|
||||
bind:wordTap="handleWordClick"
|
||||
/>
|
||||
<view class="word-popup-mask" wx:if="{{showDictPopup}}" bindtap="handleDictClose"></view>
|
||||
<view class="qa-detail-overlay" wx:if="{{qaDetailVisible}}" bindtap="onCloseDetailModal"></view>
|
||||
<view class="qa-detail-modal {{qaDetailVisible ? 'show' : ''}}" wx:if="{{qaDetailVisible}}">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">题目解析</text>
|
||||
<t-icon name="close" class="modal-close" size="40rpx" bind:tap="onCloseDetailModal" />
|
||||
</view>
|
||||
<scroll-view class="detail-body" scroll-y="true">
|
||||
<view class="section">
|
||||
<text class="question-text">{{qaDetailQuestionText}}</text>
|
||||
</view>
|
||||
<view class="section">
|
||||
<view class="overview-card {{qaDetailResultStatus}}">
|
||||
<view class="overview-icon">
|
||||
<t-icon name="{{qaDetailIconName}}" size="40rpx" color="#fff" />
|
||||
</view>
|
||||
<view class="overview-content">
|
||||
<text class="overview-label">评估详情 (EVALUATION DETAIL)</text>
|
||||
<text class="overview-text">{{qaDetailOverviewText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<block wx:for="{{qaDetailBlocks}}" wx:key="index">
|
||||
<view class="section" wx:if="{{item.items && item.items.length}}">
|
||||
<!-- <text class="section-title">{{item.title}}</text> -->
|
||||
<view wx:if="{{item.variant === 'incorrect'}}" class="detail-row incorrect">
|
||||
<view class="detail-icon">
|
||||
<t-icon name="{{item.iconName || 'data-error' }}" size="40rpx" color="#fff" />
|
||||
</view>
|
||||
<view class="detail-content">
|
||||
<text class="detail-label">错误选项</text>
|
||||
<view class="detail-items incorrect-list">
|
||||
<view class="incorrect-item" wx:for="{{item.items}}" wx:key="index">
|
||||
<view class="incorrect-head">
|
||||
<view class="chip incorrect">{{item.content}}</view>
|
||||
<text class="error-type">{{item.error_type}}</text>
|
||||
</view>
|
||||
<text class="error-reason">{{item.error_reason}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{item.variant === 'info'}}" class="detail-row your-choice {{qaDetailResultStatus}}">
|
||||
<view class="detail-icon">
|
||||
<t-icon name="{{item.iconName || 'info-circle' }}" size="40rpx" color="#fff" />
|
||||
</view>
|
||||
<view class="detail-content">
|
||||
<text class="detail-label">{{item.title}}</text>
|
||||
<view class="detail-items">
|
||||
<text class="detail-item" wx:for="{{item.items}}" wx:key="index">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{item.variant !== 'incorrect' && item.variant !== 'info'}}" class="detail-row {{item.variant}}">
|
||||
<view class="detail-icon">
|
||||
<t-icon name="{{item.iconName || 'info-circle' }}" size="40rpx" color="#fff" />
|
||||
</view>
|
||||
<view class="detail-content">
|
||||
<text class="detail-label">{{item.title}}</text>
|
||||
<view class="detail-items">
|
||||
<text class="detail-item" wx:for="{{item.items}}" wx:key="index">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
292
miniprogram/pages/qa_exercise/qa_exercise.wxss
Normal file
292
miniprogram/pages/qa_exercise/qa_exercise.wxss
Normal file
@@ -0,0 +1,292 @@
|
||||
.qa-exercise-container { min-height: 100vh; background: #ffffff; }
|
||||
.status-text { font-size: 28rpx; color: #666666; }
|
||||
.page-loading-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: transparent; z-index: 1000; }
|
||||
.loading-center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -100%); display: flex; flex-direction: column; align-items: center; gap: 16rpx; }
|
||||
.container { width: 100%; height: 100%; padding: 32rpx; box-sizing: border-box; margin: 0 auto; display: flex; flex-direction: column; gap: 24rpx; }
|
||||
.fade-in { animation: fadeInUp 260ms ease forwards; }
|
||||
.fade-out { animation: fadeOutDown 200ms ease forwards; }
|
||||
@keyframes fadeInUp { from { opacity: 0; transform: translate3d(0, 12rpx, 0) } to { opacity: 1; transform: translate3d(0, 0, 0) } }
|
||||
@keyframes fadeOutDown { from { opacity: 1; transform: translate3d(0, 0, 0) } to { opacity: 0; transform: translate3d(0, 12rpx, 0) } }
|
||||
|
||||
.question-scroll { padding-bottom: calc(110rpx + env(safe-area-inset-bottom));}
|
||||
.loading-container { padding: 32rpx; }
|
||||
|
||||
.process-container { width: 100%; display: flex; gap: 16rpx; align-items: center; padding: 8rpx 0; }
|
||||
.process-dot { flex: 1; height: 10rpx; border-radius: 8rpx; background: #e6eef9; transition: all 0.3s ease; border: 3rpx solid transparent; box-sizing: border-box; }
|
||||
.process-dot.dot-0 { background: #d9e6f2; }
|
||||
.process-dot.dot-1 { background: #c9f3e4; }
|
||||
.process-dot.dot-2 { background: #7be6b2; }
|
||||
.process-dot.dot-3 { background: #21cc80; }
|
||||
.process-dot.current { border-color: #21cc80; transform: scaleY(1.2); }
|
||||
|
||||
.bottom-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 32rpx calc(16rpx + env(safe-area-inset-bottom)); display: flex; align-items: center; justify-content: space-between; background: #fff; box-shadow: 0 -8rpx 24rpx rgba(0,0,0,0.08); opacity: 0; transition: opacity 300ms ease; }
|
||||
.bottom-bar.show { opacity: 1; }
|
||||
.bottom-btn { padding: 20rpx; border-radius: 50%; background: #f5f5f5; color: #666; }
|
||||
.bottom-btn.disabled { opacity: 0.4; pointer-events: none; }
|
||||
|
||||
.scanner { position: relative; width: 220rpx; height: 220rpx; pointer-events: none; border-radius: 12rpx; background-color: rgba(255, 255, 255, 0.35); display: flex; align-items: center; justify-content: center; opacity: 0; }
|
||||
.scanner-visible { animation: scannerFadeIn 1s 1000ms ease forwards; }
|
||||
@keyframes scannerFadeIn { from { opacity: 0 } to { opacity: 1 } }
|
||||
.star { position: relative; opacity: 0; transform-origin: center center; will-change: transform, opacity; }
|
||||
.star::after { content:""; position:absolute; width: 36rpx; height: 40rpx; border-radius: 8rpx; background: linear-gradient(337deg, #DE9EFC 7.88%, #31DAFF 107.03%); transform:rotate(45deg) skewX(22.5deg) skewY(22.5deg); }
|
||||
.star::before{ content:""; position:absolute; width: 36rpx; height: 40rpx; border-radius: 8rpx; background: linear-gradient(78.35deg, #DE9EFC 7.88%, #31DAFF 107.03%); transform:rotate(-45deg) skewX(22.5deg) skewY(22.5deg); }
|
||||
.scanner-visible .star { animation: starCycle 3000ms ease-in-out infinite; }
|
||||
.scanner-visible .star1 { animation-delay: 1000ms; }
|
||||
.scanner-visible .star2 { animation-delay: 2000ms; }
|
||||
.scanner-visible .star3 { animation-delay: 3000ms; }
|
||||
@keyframes starCycle { 0% { opacity: 0; transform: translate(-24rpx, -24rpx) scale(0.5) } 34% { opacity: 1; transform: translate(-24rpx, -24rpx) scale(0.5) } 67% { opacity: 1; transform: translate(0rpx, 0rpx) scale(1) } 100% { opacity: 0; transform: translate(36rpx, 36rpx) scale(0.5) } }
|
||||
|
||||
.word-popup-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.image-card { position: relative; border-radius: 24rpx; overflow: hidden; height: 360rpx; }
|
||||
.image { width: 100%; height: 360rpx; border-radius: 24rpx; background: #f5f5f5; }
|
||||
.view-full { position: absolute; right: 16rpx; bottom: 16rpx; display: flex; align-items: center; gap: 8rpx; padding: 10rpx 16rpx; border-radius: 24rpx; background: rgba(0,0,0,0.4); color: #fff; }
|
||||
.question-title { font-size: 40rpx; font-weight: 700; color: #001858; line-height: 56rpx; width: 100%; display: flex; flex-wrap: wrap; justify-content: flex-start;}
|
||||
.progress-text { font-size: 26rpx; color: #666; }
|
||||
.word-item { display: inline; font-size: 40rpx; font-weight: 700; color: #001858; line-height: 56rpx; margin-right: 12rpx; }
|
||||
.choice-title { font-size: 28rpx; color: #666; margin: 12rpx 0 16rpx; }
|
||||
.option-list { display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.option-item { display: flex; align-items: center; gap: 16rpx; padding: 24rpx; border-radius: 20rpx; background: #fff; border: 2rpx solid #e6eef9; }
|
||||
.option-item.selected { border-color: #21cc80; background: #eafaf2; }
|
||||
.option-item.opt-correct { border-color: #21cc80; background: #eafaf2; }
|
||||
.option-item.opt-incorrect { border-color: #e74c3c; background: #fdecea; }
|
||||
.option-item.opt-missing { border-color: #21cc80; background: #eafaf2; }
|
||||
.option-item.disabled { opacity: 0.6; }
|
||||
.option-radio { width: 36rpx; height: 36rpx; border-radius: 50%; border: 2rpx solid #cfd8e3; display: flex; align-items: center; justify-content: center; }
|
||||
.radio-dot { width: 16rpx; height: 16rpx; border-radius: 50%; background: transparent; }
|
||||
.radio-dot.on { background: #21cc80; }
|
||||
.radio-dot.red { background: #e74c3c; }
|
||||
.option-text { font-size: 30rpx; color: #001858; line-height: 44rpx; }
|
||||
.cloze-sentence { display: flex; flex-wrap: wrap; gap: 8rpx; align-items: baseline; }
|
||||
.cloze-text { font-size: 40rpx; font-weight: 700; color: #001858; line-height: 56rpx; }
|
||||
.cloze-fill { font-size: 40rpx; font-weight: 700; color: #001858; line-height: 56rpx; }
|
||||
.cloze-fill.selected { color: #21cc80; border-bottom-color: #21cc80; }
|
||||
|
||||
.tip-row { display: flex; align-items: center; gap: 8rpx; color: #21cc80; margin: 8rpx 0 16rpx; }
|
||||
.tip-text { font-size: 24rpx; color: #21cc80; }
|
||||
.answer-row { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8rpx; }
|
||||
.answer-label { font-size: 28rpx; color: #001858; font-weight: 600; }
|
||||
.answer-hint { font-size: 26rpx; color: #21cc80; }
|
||||
.input-card { position: relative; border-radius: 24rpx; border: 4rpx solid #21cc80; background: #ffffff; padding: 12rpx; }
|
||||
.answer-input { width: 100%; min-height: 200rpx; font-size: 30rpx; color: #001858; line-height: 44rpx; }
|
||||
.input-type { position: absolute; right: 16rpx; bottom: 12rpx; font-size: 22rpx; color: #999; }
|
||||
|
||||
.submit-row { display: flex; justify-content: center; width: 100%; margin-top: 48rpx; padding-bottom: 24rpx; }
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #21cc80;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.submit-btn::after { border: none; }
|
||||
.submit-btn:active { transform: translateY(8rpx); box-shadow: none; }
|
||||
.submit-btn[disabled] {
|
||||
background: #e5e5e5;
|
||||
color: #afafaf;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 结果详情弹窗样式 */
|
||||
.qa-detail-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.45);
|
||||
z-index: 999;
|
||||
}
|
||||
.qa-detail-modal {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
background: #fff;
|
||||
border-top-left-radius: 24rpx;
|
||||
border-top-right-radius: 24rpx;
|
||||
box-shadow: 0 -12rpx 36rpx rgba(0,0,0,0.12);
|
||||
z-index: 1000;
|
||||
transform: translateY(100%);
|
||||
transition: transform 250ms ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
.qa-detail-modal.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx;
|
||||
border-bottom: 2rpx solid #f0f4f8;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
color: #001858;
|
||||
font-weight: 700;
|
||||
}
|
||||
.modal-close {
|
||||
color: #666;
|
||||
}
|
||||
.detail-body {
|
||||
padding: 12rpx 24rpx 24rpx;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.section { margin: 12rpx 0; width: 100%; box-sizing: border-box; }
|
||||
.section-title { font-size: 28rpx; color: #001858; font-weight: 600; margin-bottom: 8rpx; }
|
||||
.question-text { font-size: 30rpx; color: #001858; line-height: 44rpx; }
|
||||
/* New Overview Card Styles */
|
||||
.overview-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 12rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
.overview-card.partial { background: #fff5e6; }
|
||||
.overview-card.correct { background: #eafaf2; }
|
||||
.overview-card.incorrect { background: #fff7f6; }
|
||||
|
||||
.overview-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background: #ddd;
|
||||
}
|
||||
.overview-card.partial .overview-icon { background: #ff6b00; }
|
||||
.overview-card.correct .overview-icon { background: #21cc80; }
|
||||
.overview-card.incorrect .overview-icon { background: #e74c3c; }
|
||||
|
||||
|
||||
.overview-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.overview-label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1rpx;
|
||||
color: #666;
|
||||
}
|
||||
.overview-card.partial .overview-label { color: #ff6b00; }
|
||||
.overview-card.correct .overview-label { color: #21cc80; }
|
||||
.overview-card.incorrect .overview-label { color: #e74c3c; }
|
||||
|
||||
.overview-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
color: #333;
|
||||
}
|
||||
.overview-card.partial .overview-text { color: #4a2b0f; }
|
||||
.overview-card.correct .overview-text { color: #004d2c; }
|
||||
.overview-card.incorrect .overview-text { color: #5a120c; }
|
||||
|
||||
.chip-list { display: flex; flex-wrap: wrap; gap: 8rpx; }
|
||||
.incorrect-list { display: flex; flex-direction: column; gap: 8rpx; }
|
||||
.incorrect-title { font-size: 26rpx; color: #e74c3c; font-weight: 700; margin-bottom: 4rpx; }
|
||||
.incorrect-item { display: flex; flex-direction: column; gap: 4rpx; padding: 12rpx; }
|
||||
.incorrect-head { display: flex; align-items: center; gap: 12rpx; flex-wrap: wrap; }
|
||||
.chip { display: inline-flex; align-items: center; padding: 8rpx 16rpx; border-radius: 20rpx; font-size: 26rpx; }
|
||||
.chip.correct { background: #eafaf2; color: #0b9e62; border: 2rpx solid #21cc80; }
|
||||
.chip.incorrect { background: #fdecea; color: #e74c3c; }
|
||||
.chip.missing { background: #eaf2ff; color: #2f80ed; border: 2rpx solid #2f80ed; }
|
||||
.error-type { font-size: 24rpx; color: #e74c3c; }
|
||||
.error-reason { font-size: 24rpx; color: #666; }
|
||||
.info-list { display: flex; flex-direction: column; gap: 8rpx; }
|
||||
.info-item { font-size: 28rpx; color: #001858; line-height: 40rpx; }
|
||||
/* 你的选择高亮(参考概览卡配色) */
|
||||
.info-list.your-choice.correct .info-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #eafaf2;
|
||||
color: #0b9e62;
|
||||
border: 2rpx solid #21cc80;
|
||||
}
|
||||
.info-list.your-choice.incorrect .info-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #fdecea;
|
||||
color: #e74c3c;
|
||||
border: 2rpx solid #e74c3c;
|
||||
}
|
||||
|
||||
/* 统一的结果行样式,参考概览卡结构 */
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 16rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #f8f9fb;
|
||||
}
|
||||
.detail-row.correct { background: #eafaf2; }
|
||||
.detail-row.incorrect { background: #fff7f6; }
|
||||
.detail-row.missing { background: #eaf2ff; }
|
||||
.detail-row.your-choice.correct { background: #eafaf2; }
|
||||
.detail-row.your-choice.incorrect { background: #fff7f6; }
|
||||
.detail-row.your-choice.partial { background: #eafaf2; }
|
||||
|
||||
.detail-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background: #ccc;
|
||||
}
|
||||
.detail-row.correct .detail-icon { background: #21cc80; }
|
||||
.detail-row.incorrect .detail-icon { background: #e74c3c; }
|
||||
.detail-row.missing .detail-icon { background: #2f80ed; }
|
||||
.detail-row.your-choice.correct .detail-icon { background: #21cc80; }
|
||||
.detail-row.your-choice.incorrect .detail-icon { background: #e74c3c; }
|
||||
.detail-row.your-choice.partial .detail-icon { background: #21cc80; }
|
||||
|
||||
.detail-content { display: flex; flex-direction: column; gap: 6rpx; flex: 1; }
|
||||
.detail-label { font-size: 26rpx; font-weight: 700; color: #001858; }
|
||||
.detail-items { display: flex; flex-wrap: wrap; gap: 12rpx; }
|
||||
.detail-item { font-size: 28rpx; color: #001858; }
|
||||
.detail-row.correct .detail-item { color: #0b9e62; }
|
||||
.detail-row.incorrect .detail-item { color: #e74c3c; }
|
||||
.detail-row.missing .detail-item { color: #2f80ed; }
|
||||
.detail-row.your-choice.partial .detail-item { color: #0b9e62; }
|
||||
@@ -90,6 +90,11 @@ interface IData {
|
||||
cachedSentenceIndex?: number
|
||||
loadingDots?: string
|
||||
loadingLabel?: string
|
||||
showBackIcon?: boolean
|
||||
previousWord?: string
|
||||
isReturningFromPrevious?: boolean
|
||||
prototypeWord?: string
|
||||
forceHidePrototype?: boolean
|
||||
}
|
||||
|
||||
interface IPageInstance {
|
||||
@@ -127,6 +132,7 @@ interface IPageInstance {
|
||||
fetchRecordResultForSentence: (textId: string) => Promise<void>
|
||||
startLoadingDots: () => void
|
||||
stopLoadingDots: () => void
|
||||
handleBackToPreviousWord: () => void
|
||||
}
|
||||
|
||||
Page<IData, IPageInstance>({
|
||||
@@ -186,7 +192,12 @@ Page<IData, IPageInstance>({
|
||||
cachedHighlightWords: [],
|
||||
cachedSentenceIndex: -1,
|
||||
loadingDots: '',
|
||||
loadingLabel: '场景分析中'
|
||||
loadingLabel: '场景分析中',
|
||||
showBackIcon: false,
|
||||
previousWord: '',
|
||||
isReturningFromPrevious: false,
|
||||
prototypeWord: '',
|
||||
forceHidePrototype: false
|
||||
},
|
||||
noop() {},
|
||||
|
||||
@@ -801,56 +812,43 @@ Page<IData, IPageInstance>({
|
||||
},
|
||||
|
||||
async onWordTap(e: any) {
|
||||
const rawWord = (e && e.currentTarget && e.currentTarget.dataset && e.currentTarget.dataset.word)
|
||||
|| (e && e.target && e.target.dataset && e.target.dataset.word)
|
||||
|| (e && e.detail && e.detail.word)
|
||||
|| ''
|
||||
console.log('rawWord:', rawWord)
|
||||
const dsWord = (e && e.currentTarget && e.currentTarget.dataset && e.currentTarget.dataset.word) || (e && e.target && e.target.dataset && e.target.dataset.word) || ''
|
||||
const dtWord = (e && e.detail && e.detail.word) || ''
|
||||
const rawWord = dtWord || dsWord || ''
|
||||
if (!rawWord) return
|
||||
const cleanedWord = String(rawWord).replace(/[.,?!*;:'"()]/g, '').trim()
|
||||
if (!cleanedWord) return
|
||||
try {
|
||||
this.setData({ dictLoading: true, showDictPopup: true })
|
||||
const wordDetail: any = await apiManager.getWordDetail(cleanedWord)
|
||||
const processedCollins = wordDetail['collins'] ? this.processCollinsData(wordDetail['collins']) : wordDetail['collins']
|
||||
const rawRelWord = wordDetail['rel_word']
|
||||
let sanitizedRelWord = rawRelWord
|
||||
if (rawRelWord && rawRelWord.rels && Array.isArray(rawRelWord.rels)) {
|
||||
sanitizedRelWord = {
|
||||
...rawRelWord,
|
||||
rels: rawRelWord.rels.map((entry: any) => {
|
||||
const rel = entry.rel || {}
|
||||
const pos = rel.pos
|
||||
const cleanPos = typeof pos === 'string' ? pos.replace(/\[|\]/g, '') : pos
|
||||
return { ...entry, rel: { ...rel, pos: cleanPos } }
|
||||
})
|
||||
}
|
||||
}
|
||||
const hasEE = !!wordDetail['ee']
|
||||
const hasEC = !!(wordDetail['ec'] && wordDetail['ec'].word && wordDetail['ec'].word.length > 0)
|
||||
const hasCollins = !!(processedCollins && processedCollins.collins_entries && processedCollins.collins_entries.length > 0)
|
||||
const isWordEmptyResult = !(hasEE || hasEC || hasCollins)
|
||||
const hasPhrs = !!(wordDetail['phrs'] && wordDetail['phrs'].phrs && wordDetail['phrs'].phrs.length > 0)
|
||||
const hasPastExam = !!(wordDetail['individual'] && wordDetail['individual'].pastExamSents && wordDetail['individual'].pastExamSents.length > 0)
|
||||
const dictDefaultTabValue = hasCollins ? '0' : (hasPhrs ? '1' : (hasPastExam ? '2' : '3'))
|
||||
this.setData({
|
||||
showDictPopup: true,
|
||||
wordDict: {
|
||||
ee: wordDetail['ee'], ec: wordDetail['ec'], expandEc: wordDetail['expand_ec'],
|
||||
simple: wordDetail['simple'], phrs: wordDetail['phrs'], etym: wordDetail['etym'],
|
||||
individual: wordDetail['individual'], collins: processedCollins,
|
||||
relWord: sanitizedRelWord, syno: wordDetail['syno'],
|
||||
discriminate: wordDetail['discriminate']
|
||||
},
|
||||
isWordEmptyResult,
|
||||
dictDefaultTabValue
|
||||
})
|
||||
this.setData({ dictLoading: false })
|
||||
} catch (err) {
|
||||
logger.error('获取单词详情失败:', err)
|
||||
wx.showToast({ title: '获取单词详情失败', icon: 'none' })
|
||||
this.setData({ dictLoading: false })
|
||||
const isFromWordSource = !!dtWord
|
||||
const { isReturningFromPrevious } = this.data
|
||||
let showBackIcon = false
|
||||
let previousWord = ''
|
||||
if (isFromWordSource && !isReturningFromPrevious) {
|
||||
previousWord = String((e && e.detail && e.detail.previousWord) || '')
|
||||
showBackIcon = !!previousWord
|
||||
} else if (isReturningFromPrevious) {
|
||||
showBackIcon = false
|
||||
previousWord = ''
|
||||
}
|
||||
this.setData({ prototypeWord: '', isReturningFromPrevious: false, forceHidePrototype: (isFromWordSource && !isReturningFromPrevious) })
|
||||
this.setData({ showBackIcon, previousWord })
|
||||
this.setData({ showDictPopup: true })
|
||||
const comp = this.selectComponent('#wordDict') as any
|
||||
if (comp && typeof comp.queryWord === 'function') {
|
||||
try {
|
||||
wx.nextTick(() => {
|
||||
comp.queryWord(cleanedWord)
|
||||
})
|
||||
} catch (e) {
|
||||
setTimeout(() => comp.queryWord(cleanedWord), 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
handleBackToPreviousWord() {
|
||||
const w = String(this.data.previousWord || '')
|
||||
if (!w) return
|
||||
this.setData({ isReturningFromPrevious: true, forceHidePrototype: false })
|
||||
const event = { currentTarget: { dataset: { word: w } } }
|
||||
this.onWordTap(event as any)
|
||||
},
|
||||
updateCircleProgress() {
|
||||
const { totalScore, accuracyScore, completenessScore, fluencyScore, hasScoreInfo } = this.data
|
||||
|
||||
@@ -144,10 +144,15 @@
|
||||
expanded="{{showDictExtended}}"
|
||||
loading="{{dictLoading}}"
|
||||
wordDict="{{wordDict}}"
|
||||
showBackIcon="{{showBackIcon}}"
|
||||
prototypeWord="{{prototypeWord}}"
|
||||
forceHidePrototype="{{forceHidePrototype}}"
|
||||
isWordEmptyResult="{{isWordEmptyResult}}"
|
||||
id="wordDict"
|
||||
dictDefaultTabValue="{{dictDefaultTabValue}}"
|
||||
bind:close="handleDictClose"
|
||||
bind:more="handleDictMore"
|
||||
bind:back="handleBackToPreviousWord"
|
||||
bind:wordTap="onWordTap"
|
||||
/>
|
||||
<view class="word-popup-mask" wx:if="{{showDictPopup}}" bindtap="handleDictClose"></view>
|
||||
|
||||
@@ -833,40 +833,40 @@ Page({
|
||||
const deltaRpx = (expandBottomRpx - expandTopRpx - (photoBottomRpx - photoTopRpx)) / 2
|
||||
const pxPerRpx = (win.windowWidth || vw) / 750
|
||||
dy += deltaRpx * pxPerRpx
|
||||
const startAtPhoto = `transform: translate(${dx}px, ${dy}px) scale(${scale});`
|
||||
const targetHeight = Math.round(rect.height / scale)
|
||||
this.setData({
|
||||
photoExpandTransform: startAtPhoto,
|
||||
photoExpandTransition: 'transition: transform 0ms',
|
||||
photoExpandCurrentWidth: targetWidth,
|
||||
photoExpandCurrentHeight: targetHeight,
|
||||
showExpandLayer: true,
|
||||
expandBorderStyle: 'opacity: 0;'
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.setData({ photoExpandTransition: 'transition: transform 900ms ease-in-out' })
|
||||
this.setData({ photoExpandTransform: 'transform: translate(0px, 0px) scale(1);', expandBorderStyle: 'opacity: 1; transition: opacity 600ms ease-out;' })
|
||||
setTimeout(() => { this.setData({ scannerVisible: true }) }, 100)
|
||||
}, 50)
|
||||
const startAtPhoto = `transform: translate(${dx}px, ${dy}px) scale(${scale});`
|
||||
const targetHeight = Math.round(rect.height / scale)
|
||||
this.setData({
|
||||
photoExpandTransform: startAtPhoto,
|
||||
photoExpandTransition: 'transition: transform 0ms',
|
||||
photoExpandCurrentWidth: targetWidth,
|
||||
photoExpandCurrentHeight: targetHeight,
|
||||
showExpandLayer: true,
|
||||
expandBorderStyle: 'opacity: 0;'
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.setData({ photoExpandTransition: 'transition: transform 900ms ease-in-out' })
|
||||
this.setData({ photoExpandTransform: 'transform: translate(0px, 0px) scale(1);', expandBorderStyle: 'opacity: 1; transition: opacity 600ms ease-out;' })
|
||||
setTimeout(() => { this.setData({ scannerVisible: true }) }, 100)
|
||||
}, 50)
|
||||
})
|
||||
}, waitMs)
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
resetPageState() {
|
||||
this.setData({
|
||||
takePhoto: false,
|
||||
photoPath: '',
|
||||
photoImageLoaded: false,
|
||||
photoExpandLoaded: false,
|
||||
showExpandLayer: false,
|
||||
photoExpandTransform: '',
|
||||
photoExpandTransition: '',
|
||||
photoExpandCurrentWidth: 0,
|
||||
photoExpandCurrentHeight: 0,
|
||||
expandBorderStyle: ''
|
||||
})
|
||||
},
|
||||
resetPageState() {
|
||||
this.setData({
|
||||
takePhoto: false,
|
||||
photoPath: '',
|
||||
photoImageLoaded: false,
|
||||
photoExpandLoaded: false,
|
||||
showExpandLayer: false,
|
||||
photoExpandTransform: '',
|
||||
photoExpandTransition: '',
|
||||
photoExpandCurrentWidth: 0,
|
||||
photoExpandCurrentHeight: 0,
|
||||
expandBorderStyle: ''
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到结果页面
|
||||
async navigateToResult() {
|
||||
@@ -1190,6 +1190,23 @@ Page({
|
||||
} catch (err) {}
|
||||
},
|
||||
|
||||
onImageQaExerciseTap(e: any) {
|
||||
try {
|
||||
const imageId = e?.currentTarget?.dataset?.imageId;
|
||||
const thumbnailId = e?.currentTarget?.dataset?.thumbnailId;
|
||||
if (!imageId) return;
|
||||
const list = (this.data.selectedDateImages || []).map((img: any) => ({ ...img, more_open: false, menu_closing: false }));
|
||||
this.setData({ selectedDateImages: list }, () => {
|
||||
if (this.data.useWaterfall) {
|
||||
this.balanceWaterfall(this.data.selectedDateImages || []);
|
||||
}
|
||||
});
|
||||
wx.navigateTo({
|
||||
url: `/pages/qa_exercise/qa_exercise?image_id=${encodeURIComponent(imageId)}${thumbnailId ? ('&thumbnail_id=' + encodeURIComponent(thumbnailId)) : ''}`
|
||||
});
|
||||
} catch (err) {}
|
||||
},
|
||||
|
||||
async ensureThumbSize(img: any): Promise<{ w: number; h: number } | null> {
|
||||
if (img && typeof img._thumbW === 'number' && typeof img._thumbH === 'number') {
|
||||
return { w: img._thumbW, h: img._thumbH };
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
</view>
|
||||
<view wx:if="{{takePhoto && showExpandLayer }}" class="photo-expand-layer" style="{{photoExpandTransform}} {{photoExpandTransition}}">
|
||||
<!-- <view class="photo-expand-inner" style="{{photoExpandCurrentWidth ? ('width: ' + photoExpandCurrentWidth + 'px;') : ''}} {{photoExpandCurrentHeight ? ('height: ' + photoExpandCurrentHeight + 'px;') : ''}}"> -->
|
||||
<view class="photo-expand-inner" style="{{photoExpandCurrentWidth ? ('width: ' + photoExpandCurrentWidth + 'px;') : ''}}">
|
||||
<image class="photo-expand-image" src="{{takePhoto ? photoExpandSrc : photoSvgData}}" mode="widthFix" bindload="onPhotoExpandLoaded" binderror="onPhotoExpandError"></image>
|
||||
<view class="photo-expand-inner" style="{{photoExpandCurrentWidth ? ('width: ' + photoExpandCurrentWidth + 'px;') : ''}} {{photoExpandCurrentHeight ? ('height: ' + photoExpandCurrentHeight + 'px;') : ''}}">
|
||||
<image class="photo-expand-image {{photoExpandLoaded ? 'visible' : 'hidden'}}" src="{{takePhoto ? photoExpandSrc : photoSvgData}}" mode="widthFix" bindload="onPhotoExpandLoaded" binderror="onPhotoExpandError"></image>
|
||||
<view class="scanner {{scannerVisible ? 'scanner-visible' : 'scanner-hidden'}}">
|
||||
<view class="star star1"></view>
|
||||
<view class="star star2"></view>
|
||||
@@ -53,10 +53,6 @@
|
||||
</view>
|
||||
<t-action-sheet id="t-action-sheet" bind:selected="handleSelected" />
|
||||
</view>
|
||||
<!-- <view class='button-wrapper'>
|
||||
<view class='button'>123</view>
|
||||
<view class='button-bg'></view>
|
||||
</view> -->
|
||||
</view>
|
||||
<view wx:if="{{!takePhoto && dateStripItems && dateStripItems.length > 0}}" class="date-section">
|
||||
<view class="date-strip-header">
|
||||
@@ -96,6 +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}}">场景练习</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -117,6 +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}}">场景练习</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -135,6 +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}}">场景练习</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -183,13 +183,10 @@
|
||||
position: absolute;
|
||||
right: 0rpx;
|
||||
bottom: 70rpx;
|
||||
background: #fff;
|
||||
color: #666;
|
||||
border-radius: 12rpx;
|
||||
padding: 8rpx 12rpx;
|
||||
box-shadow: 0 4rpx 8rpx rgba(0,0,0,0.08);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 12rpx;
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
@@ -204,6 +201,9 @@
|
||||
font-size: 26rpx;
|
||||
white-space: nowrap;
|
||||
line-height: 32rpx;
|
||||
padding: 10rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
@keyframes menuFadeIn {
|
||||
@@ -1196,44 +1196,47 @@
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.photo-expand-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 99;
|
||||
transition: transform 900ms ease-in-out;
|
||||
will-change: transform;
|
||||
transform-origin: center center;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.photo-expand-inner {
|
||||
display: inline-block;
|
||||
/* 初始与小卡片重叠,无额外外边距 */
|
||||
background-color: #ffffff;
|
||||
border: 24rpx solid #ffffff;
|
||||
border-bottom: 48rpx solid #fff;
|
||||
box-shadow: 0 12rpx 32rpx rgba(0,0,0,0.35);
|
||||
border-radius: 12rpx;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
transition: width 900ms ease-in-out;
|
||||
will-change: width, transform;
|
||||
transform-origin: center center;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.photo-expand-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
border-radius: 12rpx;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.photo-expand-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 99;
|
||||
transition: transform 900ms ease-in-out;
|
||||
will-change: transform;
|
||||
transform-origin: center center;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.photo-expand-inner {
|
||||
display: inline-block;
|
||||
/* 初始与小卡片重叠,无额外外边距 */
|
||||
background-color: #ffffff;
|
||||
border: 24rpx solid #ffffff;
|
||||
border-bottom: 48rpx solid #fff;
|
||||
box-shadow: 0 12rpx 32rpx rgba(0,0,0,0.35);
|
||||
border-radius: 12rpx;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
transition: width 900ms ease-in-out;
|
||||
will-change: width, transform;
|
||||
transform-origin: center center;
|
||||
backface-visibility: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.photo-expand-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
border-radius: 12rpx;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
.photo-expand-image.hidden { opacity: 0; }
|
||||
.photo-expand-image.visible { opacity: 1; transition: opacity 180ms ease; }
|
||||
|
||||
.scanner {
|
||||
position: absolute;
|
||||
|
||||
@@ -48,6 +48,100 @@ export interface IRecognitionResponseResult {
|
||||
level3: IRecognitionResponseResultLevel,
|
||||
}
|
||||
|
||||
export interface IQaExerciseCreateAccepted {
|
||||
task_id: string
|
||||
status: 'accepted'
|
||||
}
|
||||
|
||||
export interface IQaExerciseTaskStatus {
|
||||
task_id: string
|
||||
image_id: string
|
||||
ref_type: 'qa_exercise'
|
||||
ref_id: string
|
||||
status: string
|
||||
error_message?: string
|
||||
}
|
||||
|
||||
export interface IQaExerciseItem {
|
||||
id: number
|
||||
image_id: number
|
||||
title?: string
|
||||
description?: string
|
||||
status: string
|
||||
question_count: number
|
||||
created_by: number
|
||||
created_time?: string
|
||||
}
|
||||
|
||||
export type IQaExerciseListResponse = IQaExerciseItem[]
|
||||
|
||||
export type QaAttemptMode = 'choice' | 'free_text' | 'audio'
|
||||
|
||||
export interface IQaExerciseQuestion {
|
||||
id: number
|
||||
exercise_id: number
|
||||
image_id: number
|
||||
question: string
|
||||
user_id: number
|
||||
ext: any
|
||||
created_time?: string
|
||||
}
|
||||
|
||||
export interface IQaExerciseQueryResponse {
|
||||
exercise: IQaExerciseItem
|
||||
session: IQaExerciseSession
|
||||
questions: IQaExerciseQuestion[]
|
||||
}
|
||||
|
||||
export interface IQaQuestionAttemptAccepted {
|
||||
attempt_id: string
|
||||
task_id: string
|
||||
status: 'accepted'
|
||||
}
|
||||
|
||||
export interface IQaQuestionTaskStatus {
|
||||
task_id: string
|
||||
ref_type: 'qa_question_attempt'
|
||||
ref_id: string
|
||||
status: string
|
||||
error_message?: string
|
||||
}
|
||||
|
||||
export type QaCorrectness = 'correct' | 'partial' | 'incorrect'
|
||||
|
||||
export interface IIncorrectSelectionItem {
|
||||
content: string
|
||||
error_type?: string
|
||||
error_reason?: string
|
||||
}
|
||||
|
||||
export interface ISelectedDetail {
|
||||
correct: string[]
|
||||
incorrect: IIncorrectSelectionItem[]
|
||||
}
|
||||
|
||||
export interface IEvaluationSchema {
|
||||
type?: string
|
||||
result?: string
|
||||
detail?: string
|
||||
selected?: ISelectedDetail
|
||||
missing_correct?: string[]
|
||||
feedback?: string
|
||||
}
|
||||
|
||||
export interface IQaResult {
|
||||
attempt_id: string
|
||||
status: string
|
||||
is_correct?: QaCorrectness
|
||||
evaluation?: IEvaluationSchema
|
||||
mode: QaAttemptMode
|
||||
selected_option_ids?: number[]
|
||||
input_text?: string
|
||||
recording_id?: string
|
||||
stt_text?: string
|
||||
updated_time?: string
|
||||
}
|
||||
|
||||
// 单词详情接口
|
||||
export interface ExtendedWordDetail {
|
||||
word: string
|
||||
@@ -75,6 +169,18 @@ export interface IPointsData {
|
||||
expired_time: string
|
||||
}
|
||||
|
||||
export interface IQaExerciseSession {
|
||||
id: number
|
||||
exercise_id: number
|
||||
starter_user_id: number
|
||||
status: string
|
||||
started_at?: string
|
||||
completed_at?: string
|
||||
progress: number
|
||||
score?: number
|
||||
ext?: any
|
||||
}
|
||||
|
||||
export interface YdWordDetail {
|
||||
ee: {}
|
||||
ec: {}
|
||||
|
||||
@@ -7,7 +7,13 @@ import {
|
||||
IAuditHistoryResponse,
|
||||
IUserInfo,
|
||||
IDailySummaryResponse,
|
||||
YdWordDetail
|
||||
YdWordDetail,
|
||||
IQaExerciseCreateAccepted,
|
||||
IQaExerciseTaskStatus,
|
||||
IQaExerciseQueryResponse,
|
||||
IQaQuestionAttemptAccepted,
|
||||
IQaQuestionTaskStatus,
|
||||
IQaResult
|
||||
} from '../types/app';
|
||||
import { BASE_URL, USE_CLOUD } from './config';
|
||||
import { cloudConfig } from './cloud.config';
|
||||
@@ -2055,6 +2061,52 @@ class ApiManager {
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async createQaExerciseTask(imageId: number | string, title?: string, description?: string): Promise<IQaExerciseCreateAccepted> {
|
||||
const payload: Record<string, any> = { image_id: imageId }
|
||||
if (title !== undefined) payload.title = title
|
||||
if (description !== undefined) payload.description = description
|
||||
const resp = await this.request<IQaExerciseCreateAccepted>(`/api/v1/qa/exercises/tasks`, 'POST', payload)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async getQaExerciseTaskStatus(taskId: string): Promise<IQaExerciseTaskStatus> {
|
||||
const resp = await this.request<IQaExerciseTaskStatus>(`/api/v1/qa/exercises/tasks/${taskId}/status`, 'GET')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async listQaExercisesByImage(imageId: string): Promise<IQaExerciseQueryResponse | null> {
|
||||
const resp = await this.request<IQaExerciseQueryResponse | null>(`/api/v1/qa/${imageId}/exercises`, 'GET')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async createQaQuestionAttempt(
|
||||
questionId: string,
|
||||
payload: {
|
||||
exercise_id: string
|
||||
mode: 'choice' | 'free_text' | 'cloze'
|
||||
selected_options?: string[]
|
||||
input_text?: string
|
||||
file_id?: string
|
||||
}
|
||||
): Promise<IQaQuestionAttemptAccepted> {
|
||||
const resp = await this.request<IQaQuestionAttemptAccepted>(`/api/v1/qa/questions/${questionId}/attempts`, 'POST', payload)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async getQaQuestionTaskStatus(taskId: string): Promise<IQaQuestionTaskStatus> {
|
||||
const resp = await this.request<IQaQuestionTaskStatus>(`/api/v1/qa/question-tasks/${taskId}/status`, 'GET')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async getQaResult(questionId: string): Promise<IQaResult> {
|
||||
const resp = await this.request<IQaResult>(`/api/v1/qa/questions/${questionId}/result`, 'GET')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async getQaQuestionAudio(questionId: string | number): Promise<{ file_id: string | number }> {
|
||||
const resp = await this.request<{ file_id: string | number }>(`/api/v1/qa/questions/${questionId}/audio`, 'GET')
|
||||
return resp.data
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
|
||||
Reference in New Issue
Block a user