1871 lines
64 KiB
TypeScript
1871 lines
64 KiB
TypeScript
// assessment.ts
|
||
import apiManager from '../../utils/api'
|
||
const recorderManager = wx.getRecorderManager()
|
||
let recorderHandlersBound = false
|
||
|
||
interface IPageData {
|
||
imagePath: string
|
||
imageSmall: boolean
|
||
imageMode: 'widthFix' | 'aspectFit'
|
||
currentSentence: any
|
||
sentences: any[]
|
||
currentIndex: number
|
||
selectedSentenceIndex: number
|
||
totalScore: number
|
||
accuracyScore: number
|
||
completenessScore: number
|
||
fluencyScore: number
|
||
circleProgressStyle: string
|
||
accuracyCircleStyle: string
|
||
completenessCircleStyle: string
|
||
fluencyCircleStyle: string
|
||
isRecording: boolean
|
||
recordStartTime: number
|
||
recordDuration: number
|
||
remainingTime: number // 剩余录音时间
|
||
hasScoreInfo: boolean // 是否有评分信息
|
||
words: string[] // 句子内容的单词数组
|
||
ipas: string[] // 音标的单词数组
|
||
isScoreExpanded: boolean // 评分区域是否展开
|
||
wordScores: Array<{ // 单词评分数组
|
||
word: string
|
||
pronAccuracy: number
|
||
pronFluency: number
|
||
matchTag?: number // 0:匹配单词、1:新增单词、2:缺少单词、3:错读的词、4:未录入单词
|
||
phoneInfos?: Array<{ // 音标信息数组
|
||
phone: string // 国际音标
|
||
pronAccuracy: number // 音标评分
|
||
matchTag: number // 音标匹配度
|
||
}>
|
||
}>
|
||
matchTagLegend: Array<{ // MatchTag 说明
|
||
tag: number
|
||
description: string
|
||
color: string
|
||
}>
|
||
standardAudioMap: { [key: string]: string } // 标准语音文件ID映射
|
||
isPlaying: boolean // 是否正在播放音频
|
||
audioDuration: number // 音频总时长
|
||
currentTime: number // 当前播放时间
|
||
sliderValue: number // 进度条值(0-100)
|
||
playIconName: string // 播放图标名称
|
||
showDictPopup: boolean // 控制弹窗是否显示
|
||
showDictExtended: boolean // 控制扩展内容是否显示
|
||
wordAudioPlaying: boolean // 单词音频是否播放中
|
||
wordAudioIconName: string // sound/sound-low/sound-mute
|
||
activeWordAudioType: string // 'uk' | 'us' 当前播放的音频类型
|
||
wordDict: {
|
||
ee: any
|
||
ec: any
|
||
expandEc: any
|
||
etym: any
|
||
syno: {
|
||
word: string
|
||
synos: Array<{
|
||
syno: {
|
||
ws: Array<{ w: string }>
|
||
pos: string
|
||
tran: string
|
||
}
|
||
}>
|
||
}
|
||
simple: any
|
||
phrs: any
|
||
individual: any
|
||
collins: any
|
||
relWord: {
|
||
rels: Array<{
|
||
rel: {
|
||
pos: string
|
||
words: Array<{
|
||
tran: string
|
||
word: string
|
||
}>
|
||
}
|
||
}>
|
||
}
|
||
discriminate: {
|
||
data: Array<{
|
||
source: string
|
||
usages: Array<{
|
||
usage: string
|
||
headword: string
|
||
}>
|
||
}>
|
||
}
|
||
}
|
||
prototypeWord: string
|
||
// 添加处理后的单词和音标数组
|
||
processedSentences: Array<{
|
||
words: string[]
|
||
ipas: string[]
|
||
zh: string
|
||
}>
|
||
// 添加翻译显示模式状态
|
||
transDisplayMode: 'en' | 'en_ipa' | 'en_zh' // 翻译显示模式
|
||
isScoreModalOpen: boolean // 评分结果弹窗是否打开
|
||
scoreModalVisible: boolean
|
||
justSelectedByWord: boolean
|
||
isMoreMenuOpen: boolean // 更多菜单是否打开
|
||
isMoreMenuClosing: boolean // 更多菜单是否打开
|
||
displayTotalScore: number
|
||
displayAccuracyScore: number
|
||
displayCompletenessScore: number
|
||
displayFluencyScore: number
|
||
dictDefaultTabValue: string
|
||
dictLoading: boolean
|
||
// 添加返回功能相关字段
|
||
showBackIcon: boolean // 是否显示返回图标
|
||
previousWord: string // 上一个单词
|
||
isReturningFromPrevious: boolean // 是否正在从上一个单词返回
|
||
overlayVisible: boolean,
|
||
highlightWords: string[],
|
||
highlightShow: boolean,
|
||
analizing: boolean,
|
||
highlightZoom: boolean,
|
||
focusWordIndex: number,
|
||
focusTransform: string,
|
||
cachedHighlightWords: string[],
|
||
cachedSentenceIndex: number,
|
||
isWordEmptyResult: boolean,
|
||
recordPermissionGranted: boolean
|
||
loadingMaskVisible: boolean,
|
||
standardAudioLocalMap: { [key: string]: string }, // 标准语音文件ID映射
|
||
assessmentAudioLocalMap: { [key: string]: string } // 评估语音文件ID映射
|
||
}
|
||
|
||
type IPageMethods = {
|
||
updateCircleProgress: () => void
|
||
startRecording: () => void
|
||
stopRecording: () => void
|
||
handleRecordStart: () => void
|
||
handleRecordEnd: () => void
|
||
updateWordsAndIpas: (content: string, ipa: string) => void
|
||
toggleScoreSection: () => void
|
||
handleImagePreview: () => void
|
||
getStandardVoice: (sentenceId: string) => Promise<void>
|
||
playStandardVoice: () => void
|
||
playAssessmentVoice: () => void
|
||
resetAudioState: () => void
|
||
handleWordClick: (e: any) => void
|
||
handleSentenceSelect: (e: any) => void
|
||
handleDictMore: () => void
|
||
handleDictClose: () => void
|
||
handleBackToPreviousWord: () => void // 添加返回上一个单词的方法
|
||
playWordAudio: (e: any) => void
|
||
onTabsChange: (event: any) => void
|
||
onTabsClick: (event: any) => void
|
||
onScoreTap: () => void
|
||
onTransTap: () => void
|
||
onCloseScoreModal: () => void
|
||
processSentences: (sentences: any[]) => Array<{ words: string[], ipas: string[], zh: string }>
|
||
getTransIconName: () => string
|
||
onPageScroll: (e: any) => void
|
||
processCollinsData: (collinsData: any) => any // Add this line
|
||
computeHighlightLayout:() => void
|
||
onMicHighlight: () => void
|
||
ensureRecordPermission: () => void
|
||
onMoreTap: () => void
|
||
noop: () => void
|
||
}
|
||
|
||
interface IPageInstance extends IPageMethods {
|
||
recordTimer?: number
|
||
data: IPageData
|
||
audioContext?: WechatMiniprogram.InnerAudioContext
|
||
playIconTimer?: number
|
||
// 新增:单词音频上下文与图标轮播定时器
|
||
wordAudioContext?: WechatMiniprogram.InnerAudioContext
|
||
wordAudioIconTimer?: number
|
||
// 新增:录音时长(毫秒),用于避免 setData 异步导致 onStop 判断失败
|
||
lastRecordDurationMs?: number
|
||
circleAnimTimer?: number
|
||
}
|
||
|
||
Page<IPageData, IPageInstance>({
|
||
data: {
|
||
imagePath: '', // 图片路径
|
||
imageSmall: false, // 图片是否缩小
|
||
imageMode: 'widthFix', // 初始完整展示模式
|
||
currentSentence: '', // 当前显示的例句
|
||
sentences: [], // 例句列表
|
||
currentIndex: 0, // 当前例句索引
|
||
selectedSentenceIndex: 0, // 当前选中的例句索引
|
||
totalScore: 0, // 总分
|
||
accuracyScore: 0, // 准确性评分
|
||
completenessScore: 0, // 完整性评分
|
||
fluencyScore: 0, // 流利度评分
|
||
circleProgressStyle: '', // 圆形进度条样式
|
||
accuracyCircleStyle: '',
|
||
completenessCircleStyle: '',
|
||
fluencyCircleStyle: '',
|
||
isRecording: false, // 是否正在录音
|
||
recordStartTime: 0, // 录音开始时间
|
||
recordDuration: 0, // 录音持续时间
|
||
remainingTime: 30, // 剩余录音时间,默认30秒
|
||
hasScoreInfo: false, // 是否有评分信息
|
||
words: [], // 句子内容的单词数组
|
||
ipas: [], // 音标的单词数组
|
||
isScoreExpanded: false, // 评分区域是否展开
|
||
wordScores: [], // 单词评分数组
|
||
prototypeWord: '',
|
||
wordDict: {
|
||
ee: {},
|
||
ec: {},
|
||
expandEc: {},
|
||
etym: {},
|
||
syno: { word: '', synos: [] },
|
||
simple: {},
|
||
phrs: {},
|
||
individual: {},
|
||
collins: {},
|
||
relWord: { rels: [] },
|
||
discriminate: { data: [] }
|
||
},
|
||
matchTagLegend: [
|
||
{ tag: 0, description: '匹配', color: '#ffffff' },
|
||
{ tag: 1, description: '新增', color: '#ffebee' },
|
||
{ tag: 2, description: '缺少', color: '#e3f2fd' },
|
||
{ tag: 3, description: '错读', color: '#fff3e0' },
|
||
{ tag: 4, description: '未录入', color: '#f5f5f5' }
|
||
],
|
||
standardAudioMap: {}, // 标准语音文件ID映射
|
||
// 页面级音频本地缓存:避免重复网络获取
|
||
standardAudioLocalMap: {},
|
||
assessmentAudioLocalMap: {},
|
||
isPlaying: false, // 是否正在播放音频
|
||
audioDuration: 0, // 音频总时长
|
||
currentTime: 0, // 当前播放时间
|
||
sliderValue: 0, // 进度条值(0-100)
|
||
playIconName: 'sound-low', // 播放图标名称
|
||
showDictPopup: false, // 控制弹窗是否显示
|
||
showDictExtended: false, // 控制扩展内容是否显示
|
||
// 新增:单词音频播放与图标轮播状态
|
||
wordAudioPlaying: false,
|
||
wordAudioIconName: 'sound',
|
||
activeWordAudioType: '',
|
||
processedSentences: [], // 处理后的句子数据
|
||
transDisplayMode: 'en', // 默认显示模式为英文 only
|
||
isScoreModalOpen: false, // 评分结果弹窗是否打开
|
||
scoreModalVisible: false,
|
||
justSelectedByWord: false,
|
||
isMoreMenuOpen: false, // 更多菜单是否打开
|
||
displayTotalScore: 0,
|
||
displayAccuracyScore: 0,
|
||
displayCompletenessScore: 0,
|
||
displayFluencyScore: 0,
|
||
dictDefaultTabValue: '2',
|
||
dictLoading: false,
|
||
// 返回功能相关字段
|
||
showBackIcon: false, // 是否显示返回图标
|
||
previousWord: '', // 上一个单词
|
||
isReturningFromPrevious: false, // 是否正在从上一个单词返回
|
||
isWordEmptyResult: false,
|
||
overlayVisible: false,
|
||
highlightWords: [],
|
||
highlightShow: false,
|
||
analizing: false,
|
||
highlightZoom: false,
|
||
focusWordIndex: 0,
|
||
focusTransform: '',
|
||
cachedHighlightWords: [],
|
||
cachedSentenceIndex: -1,
|
||
isMoreMenuClosing: false,
|
||
recordPermissionGranted: false,
|
||
loadingMaskVisible: false,
|
||
},
|
||
|
||
onMicHighlight() {
|
||
// const pre = this.data.cachedHighlightWords || []
|
||
// const cachedIdx = typeof this.data.cachedSentenceIndex === 'number' ? this.data.cachedSentenceIndex : -1
|
||
// const currentIdx = typeof this.data.selectedSentenceIndex === 'number' ? this.data.selectedSentenceIndex : -1
|
||
// const needRecompute = (!pre || pre.length === 0 || cachedIdx !== currentIdx)
|
||
|
||
this.computeHighlightLayout()
|
||
|
||
const startAnim = () => {
|
||
const words = this.data.cachedHighlightWords || []
|
||
if (!words || words.length === 0) return
|
||
this.setData({
|
||
overlayVisible: true,
|
||
highlightWords: words.map((w: any) => ({ ...w, transform: 'translate(0px, 0px) scale(1)' })),
|
||
highlightShow: false,
|
||
highlightZoom: false
|
||
})
|
||
setTimeout(() => {
|
||
this.setData({ highlightShow: true })
|
||
setTimeout(() => {
|
||
this.setData({ highlightZoom: true })
|
||
try {
|
||
wx.nextTick(() => {
|
||
const updated = (this.data.highlightWords || []).map((w: any) => ({
|
||
...w,
|
||
transform: w.targetTransform
|
||
}))
|
||
this.setData({ highlightWords: updated })
|
||
})
|
||
} catch (e) {
|
||
setTimeout(() => {
|
||
const updated = (this.data.highlightWords || []).map((w: any) => ({
|
||
...w,
|
||
transform: w.targetTransform
|
||
}))
|
||
this.setData({ highlightWords: updated })
|
||
}, 0)
|
||
}
|
||
}, 500)
|
||
}, 50)
|
||
}
|
||
|
||
// if (needRecompute) {
|
||
try {
|
||
wx.nextTick(() => {
|
||
setTimeout(() => startAnim(), 80)
|
||
})
|
||
} catch (e) {
|
||
setTimeout(() => {
|
||
setTimeout(() => startAnim(), 80)
|
||
}, 0)
|
||
}
|
||
// } else {
|
||
// startAnim()
|
||
// }
|
||
},
|
||
|
||
// 切换评分区域展开状态
|
||
toggleScoreSection() {
|
||
this.setData({
|
||
isScoreExpanded: !this.data.isScoreExpanded
|
||
})
|
||
},
|
||
|
||
// 更新单词和音标数组
|
||
updateWordsAndIpas(content: string, ipa: string) {
|
||
const words = content ? content.split(' ') : []
|
||
const ipas = ipa ? ipa.split(' ') : []
|
||
this.setData({ words, ipas })
|
||
},
|
||
|
||
// 更新圆形进度条样式
|
||
updateCircleProgress() {
|
||
const { totalScore, accuracyScore, completenessScore, fluencyScore, hasScoreInfo } = this.data
|
||
|
||
const normalize = (v: number) => {
|
||
if (v < 0) return 0
|
||
if (v <= 1) return Math.round(v * 100)
|
||
if (v > 100) return 100
|
||
return Math.round(v)
|
||
}
|
||
|
||
const totalPct = normalize(totalScore)
|
||
const accPct = normalize(accuracyScore)
|
||
const compPct = normalize(completenessScore)
|
||
const fluPct = normalize(fluencyScore)
|
||
|
||
const pickColor = (pct: number) => {
|
||
if (pct >= 70) return 'rgb(39, 174, 96)'
|
||
if (pct >= 40) return 'rgb(241, 196, 15)'
|
||
return 'rgb(231, 76, 60)'
|
||
}
|
||
|
||
const totalColor = pickColor(totalPct)
|
||
const accColor = pickColor(accPct)
|
||
const compColor = pickColor(compPct)
|
||
const fluColor = pickColor(fluPct)
|
||
|
||
if (this.circleAnimTimer) {
|
||
clearInterval(this.circleAnimTimer)
|
||
this.circleAnimTimer = undefined
|
||
}
|
||
|
||
const duration = 800
|
||
const start = Date.now()
|
||
|
||
const compose = (pct: number, color: string) => {
|
||
if (!hasScoreInfo || pct <= 0) {
|
||
return 'background: conic-gradient(transparent 0)'
|
||
}
|
||
return `background: conic-gradient(${color} ${pct}%, transparent 0)`
|
||
}
|
||
|
||
const tick = () => {
|
||
const elapsed = Date.now() - start
|
||
const progressPct = Math.min(100, Math.round((elapsed / duration) * 100))
|
||
|
||
const curTotal = Math.min(progressPct, totalPct)
|
||
const curAcc = Math.min(progressPct, accPct)
|
||
const curComp = Math.min(progressPct, compPct)
|
||
const curFlu = Math.min(progressPct, fluPct)
|
||
|
||
const ratio = (target: number, current: number) => (target === 0 ? 0 : (current / target))
|
||
const round2 = (v: number) => Math.round(v * 100) / 100
|
||
|
||
this.setData({
|
||
circleProgressStyle: compose(curTotal, totalColor),
|
||
accuracyCircleStyle: compose(curAcc, accColor),
|
||
completenessCircleStyle: compose(curComp, compColor),
|
||
fluencyCircleStyle: compose(curFlu, fluColor),
|
||
displayTotalScore: round2(totalScore * ratio(totalPct, curTotal)),
|
||
displayAccuracyScore: round2(accuracyScore * ratio(accPct, curAcc)),
|
||
displayCompletenessScore: round2(completenessScore * ratio(compPct, curComp)),
|
||
displayFluencyScore: round2(fluencyScore * ratio(fluPct, curFlu)),
|
||
})
|
||
|
||
if (elapsed >= duration) {
|
||
if (this.circleAnimTimer) {
|
||
clearInterval(this.circleAnimTimer)
|
||
this.circleAnimTimer = undefined
|
||
}
|
||
}
|
||
}
|
||
|
||
this.setData({
|
||
circleProgressStyle: compose(0, totalColor),
|
||
accuracyCircleStyle: compose(0, accColor),
|
||
completenessCircleStyle: compose(0, compColor),
|
||
fluencyCircleStyle: compose(0, fluColor),
|
||
displayTotalScore: 0,
|
||
displayAccuracyScore: 0,
|
||
displayCompletenessScore: 0,
|
||
displayFluencyScore: 0,
|
||
})
|
||
|
||
this.circleAnimTimer = setInterval(tick, 16) as any
|
||
},
|
||
|
||
|
||
|
||
// 重置音频播放状态
|
||
resetAudioState() {
|
||
// 隐藏可能存在的加载提示
|
||
wx.hideLoading()
|
||
|
||
if (this.audioContext) {
|
||
// iOS 真机上,直接 stop 可能无法立即中断播放;需先 pause,再 seek(0),并关闭自动播放
|
||
try { (this.audioContext as any).autoplay = false } catch (e) {}
|
||
try { this.audioContext.pause() } catch (e) {}
|
||
try { this.audioContext.seek(0) } catch (e) {}
|
||
try { this.audioContext.stop() } catch (e) {}
|
||
|
||
// 解绑所有事件,避免旧回调在切换句子后再次触发导致误播上一个音频
|
||
try {
|
||
this.audioContext.offPlay()
|
||
this.audioContext.offTimeUpdate()
|
||
this.audioContext.offEnded()
|
||
this.audioContext.offError()
|
||
this.audioContext.offCanplay && this.audioContext.offCanplay()
|
||
} catch (e) {}
|
||
|
||
// 清空音源并销毁上下文,确保下次播放重新创建,避免残留状态
|
||
try { this.audioContext.src = '' } catch (e) {}
|
||
try { this.audioContext.destroy() } catch (e) {}
|
||
this.audioContext = undefined
|
||
}
|
||
|
||
// 重置界面状态
|
||
this.setData({
|
||
isPlaying: false,
|
||
currentTime: 0,
|
||
sliderValue: 0,
|
||
audioDuration: 0,
|
||
playIconName: 'sound-low'
|
||
})
|
||
},
|
||
|
||
// 开始录音
|
||
startRecording() {
|
||
const options: WechatMiniprogram.RecorderManagerStartOption = {
|
||
duration: 30000, // 最长录音时长,单位 ms
|
||
sampleRate: 16000, // 采样率
|
||
numberOfChannels: 1, // 录音通道数
|
||
encodeBitRate: 48000, // 编码码率
|
||
format: 'mp3' as 'mp3', // 音频格式
|
||
}
|
||
|
||
// 重置上次录音时长(避免 onStop 读取旧值)
|
||
this.lastRecordDurationMs = 0
|
||
|
||
recorderManager.start(options)
|
||
this.setData({
|
||
isRecording: true,
|
||
recordStartTime: Date.now(),
|
||
recordDuration: 0
|
||
})
|
||
|
||
// 设置定时器更新录音时长和倒计时
|
||
this.recordTimer = setInterval(() => {
|
||
const duration = Date.now() - this.data.recordStartTime
|
||
const remaining = Math.max(0, 30 - Math.floor(duration / 1000))
|
||
this.setData({
|
||
recordDuration: duration,
|
||
remainingTime: remaining
|
||
})
|
||
|
||
// 时间到自动停止录音
|
||
if (remaining === 0) {
|
||
this.stopRecording()
|
||
}
|
||
}, 100)
|
||
},
|
||
|
||
// 停止录音
|
||
stopRecording() {
|
||
// 若未处于录音中,直接返回,避免调用 stop 后不触发 onStop
|
||
if (!this.data.isRecording) {
|
||
return
|
||
}
|
||
|
||
const duration = Date.now() - this.data.recordStartTime
|
||
if (this.recordTimer) {
|
||
clearInterval(this.recordTimer)
|
||
}
|
||
|
||
// 写入最终录音时长,确保 onStop 回调中可准确判断
|
||
this.lastRecordDurationMs = duration
|
||
// 写入 data 仅用于 UI 展示
|
||
this.setData({ recordDuration: duration })
|
||
|
||
if (duration < 3000) { // 小于3秒
|
||
recorderManager.stop()
|
||
this.setData({
|
||
isRecording: false,
|
||
remainingTime: 30,
|
||
overlayVisible: false,
|
||
highlightShow: false,
|
||
highlightZoom: false,
|
||
focusTransform: '',
|
||
highlightWords: [],
|
||
cachedHighlightWords: [],
|
||
cachedSentenceIndex: -1
|
||
})
|
||
return
|
||
}
|
||
|
||
recorderManager.stop()
|
||
// this.setData({ isRecording: false })
|
||
},
|
||
|
||
// 长按开始录音
|
||
handleRecordStart() {
|
||
if (this.data.isRecording) return
|
||
this.startRecording()
|
||
try { this.onMicHighlight() } catch (e) {}
|
||
},
|
||
|
||
// 松开结束录音
|
||
handleRecordEnd() {
|
||
console.log('---lisa-handleRecordEnd')
|
||
this.stopRecording()
|
||
// 淡出高亮层
|
||
// this.setData({ overlayVisible: false })
|
||
// const timer = setTimeout(() => {
|
||
// this.setData({ highlightShow: false, highlightZoom: false, focusTransform: '', highlightWords: [] })
|
||
// clearTimeout(timer)
|
||
// }, 320)
|
||
},
|
||
|
||
// 点击图片预览
|
||
handleImagePreview() {
|
||
wx.previewImage({
|
||
urls: [this.data.imagePath],
|
||
current: this.data.imagePath
|
||
})
|
||
},
|
||
|
||
// 获取标准语音
|
||
async getStandardVoice(sentenceId: string) {
|
||
try {
|
||
// 如果已经获取过该例句的标准语音,直接返回
|
||
if (this.data.standardAudioMap[sentenceId]) {
|
||
// 预加载音频
|
||
if (this.audioContext && !this.audioContext.src) {
|
||
this.audioContext.src = this.data.standardAudioMap[sentenceId]
|
||
}
|
||
return
|
||
}
|
||
|
||
const {audio_id} = await apiManager.getStandardVoice(sentenceId)
|
||
if (audio_id) {
|
||
// const fileUrl = `${FILE_BASE_URL}/${audio_id}`
|
||
const fileUrl = audio_id
|
||
this.setData({
|
||
standardAudioMap: {
|
||
...this.data.standardAudioMap,
|
||
[sentenceId]: fileUrl.toString() // Convert to string to ensure type compatibility
|
||
}
|
||
})
|
||
// 预加载音频
|
||
if (this.audioContext) {
|
||
this.audioContext.src = fileUrl.toString()
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('获取标准语音失败:', err)
|
||
wx.showToast({
|
||
title: '获取语音失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
|
||
// 播放标准语音
|
||
playStandardVoice() {
|
||
if (this.data.isRecording) return
|
||
const { currentSentence, standardAudioMap, isPlaying } = this.data
|
||
const audioUrl = standardAudioMap[currentSentence.id]
|
||
|
||
if (!audioUrl) {
|
||
wx.showLoading({ title: '正在获取...' })
|
||
this.getStandardVoice(currentSentence.id)
|
||
.then(() => {
|
||
wx.hideLoading()
|
||
// 重新尝试播放
|
||
this.playStandardVoice()
|
||
})
|
||
.catch(() => {
|
||
wx.hideLoading()
|
||
})
|
||
return
|
||
}
|
||
|
||
// 优先使用页面本地缓存
|
||
const cachedLocal = this.data.standardAudioLocalMap[currentSentence.id]
|
||
const playWithPath = (filePath: string) => {
|
||
if (isPlaying && this.audioContext?.src === filePath) {
|
||
try { this.audioContext.pause() } catch (e) {}
|
||
try { this.audioContext.seek(0) } catch (e) {}
|
||
if (this.playIconTimer) {
|
||
clearInterval(this.playIconTimer)
|
||
this.playIconTimer = undefined
|
||
}
|
||
this.setData({ isPlaying: false, playIconName: 'sound-low', currentTime: 0, sliderValue: 0 })
|
||
return
|
||
}
|
||
|
||
// 如果还没有音频上下文,则创建并绑定事件
|
||
if (!this.audioContext) {
|
||
this.audioContext = wx.createInnerAudioContext()
|
||
try { (this.audioContext as any).obeyMuteSwitch = false } catch (e) {}
|
||
try { (this.audioContext as any).autoplay = false } catch (e) {}
|
||
try { (this.audioContext as any).obeyMuteSwitch = false } catch (e) {}
|
||
// this.audioContext.onCanplay(() => {
|
||
// setTimeout(() => {
|
||
// const duration = this.audioContext!.duration
|
||
// if (duration && duration > 0) {
|
||
// this.setData({
|
||
// audioDuration: duration,
|
||
// currentTime: this.audioContext!.currentTime
|
||
// })
|
||
// }
|
||
// }, 100)
|
||
// })
|
||
this.audioContext.onPlay(() => {
|
||
// const duration = this.audioContext!.duration
|
||
// 停止可能存在的动画计时器
|
||
if (this.playIconTimer) {
|
||
clearInterval(this.playIconTimer)
|
||
this.playIconTimer = undefined
|
||
}
|
||
// 切换为播放中的图标并启动动画
|
||
this.setData({
|
||
isPlaying: true,
|
||
// currentTime: this.audioContext!.currentTime,
|
||
// audioDuration: duration && duration > 0 ? duration : 0,
|
||
playIconName: 'sound-low'
|
||
})
|
||
// 每500ms在 sound-low、sound、sound-high 三个图标之间循环切换
|
||
const iconOrder = ['sound-low', 'sound', 'sound-high']
|
||
let i = 0
|
||
this.playIconTimer = setInterval(() => {
|
||
i = (i + 1) % iconOrder.length
|
||
this.setData({ playIconName: iconOrder[i] })
|
||
}, 500) as any
|
||
})
|
||
this.audioContext.onTimeUpdate(() => {
|
||
// const currentTime = this.audioContext!.currentTime
|
||
// const duration = this.audioContext!.duration
|
||
// if (duration && duration > 0) {
|
||
// this.setData({
|
||
// currentTime,
|
||
// audioDuration: duration,
|
||
// sliderValue: (currentTime / duration) * 100
|
||
// })
|
||
// } else {
|
||
// this.setData({
|
||
// currentTime: 0,
|
||
// audioDuration: 0,
|
||
// sliderValue: 0
|
||
// })
|
||
// }
|
||
})
|
||
this.audioContext.onEnded(() => {
|
||
// 结束时清除动画并复位图标
|
||
if (this.playIconTimer) {
|
||
clearInterval(this.playIconTimer)
|
||
this.playIconTimer = undefined
|
||
}
|
||
this.setData({
|
||
isPlaying: false,
|
||
// currentTime: 0,
|
||
// sliderValue: 0,
|
||
playIconName: 'sound-low'
|
||
})
|
||
})
|
||
this.audioContext.onError((err: any) => {
|
||
// 出错时清除动画并复位图标
|
||
if (this.playIconTimer) {
|
||
clearInterval(this.playIconTimer)
|
||
this.playIconTimer = undefined
|
||
}
|
||
this.setData({
|
||
isPlaying: false,
|
||
// currentTime: 0,
|
||
// sliderValue: 0,
|
||
playIconName: 'sound-low'
|
||
})
|
||
console.error('标准语音播放错误:', err)
|
||
wx.showToast({ title: '播放失败', icon: 'none' })
|
||
})
|
||
this.audioContext.onTimeUpdate(() => {
|
||
// const currentTime = this.audioContext!.currentTime
|
||
// const duration = this.audioContext!.duration
|
||
// const sliderValue = (currentTime / duration) * 100
|
||
// this.setData({
|
||
// currentTime,
|
||
// audioDuration: duration,
|
||
// sliderValue
|
||
// })
|
||
})
|
||
}
|
||
|
||
// 切换音源并播放
|
||
if (this.audioContext.src !== filePath) {
|
||
try { this.audioContext.pause() } catch (e) {}
|
||
try { this.audioContext.seek(0) } catch (e) {}
|
||
this.setData({ currentTime: 0, sliderValue: 0, audioDuration: 0 })
|
||
this.audioContext.src = filePath
|
||
} else {
|
||
try { this.audioContext.seek(0) } catch (e) {}
|
||
this.setData({ currentTime: 0, sliderValue: 0 })
|
||
}
|
||
|
||
try {
|
||
this.audioContext.play()
|
||
this.setData({ isPlaying: true })
|
||
} catch (error) {
|
||
wx.showToast({ title: '音频播放失败', icon: 'none' })
|
||
}
|
||
}
|
||
if (cachedLocal) {
|
||
playWithPath(cachedLocal)
|
||
} else {
|
||
apiManager.downloadFile(audioUrl).then((filePath) => {
|
||
this.setData({
|
||
standardAudioLocalMap: {
|
||
...this.data.standardAudioLocalMap,
|
||
[currentSentence.id]: filePath
|
||
}
|
||
})
|
||
playWithPath(filePath)
|
||
}).catch((error) => {
|
||
console.error('下载音频文件失败:', error)
|
||
wx.showToast({ title: '音频下载失败', icon: 'none' })
|
||
})
|
||
}
|
||
},
|
||
|
||
// 新增:播放评分结果音频(使用当前句子的 file_id)
|
||
playAssessmentVoice() {
|
||
if (this.data.isRecording) return
|
||
const { currentSentence, isPlaying } = this.data
|
||
const fileId = currentSentence?.file_id
|
||
|
||
if (!fileId) {
|
||
wx.showToast({ title: '暂无评分音频', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
const cachedLocal = this.data.assessmentAudioLocalMap[fileId]
|
||
const playWithPath = (filePath: string) => {
|
||
if (isPlaying && this.audioContext?.src === filePath) {
|
||
try { this.audioContext.pause() } catch (e) {}
|
||
try { this.audioContext.seek(0) } catch (e) {}
|
||
if (this.playIconTimer) {
|
||
clearInterval(this.playIconTimer)
|
||
this.playIconTimer = undefined
|
||
}
|
||
this.setData({ isPlaying: false, playIconName: 'sound-low', currentTime: 0, sliderValue: 0 })
|
||
return
|
||
}
|
||
|
||
// 如果还没有音频上下文,则创建并绑定事件
|
||
if (!this.audioContext) {
|
||
this.audioContext = wx.createInnerAudioContext()
|
||
try { (this.audioContext as any).obeyMuteSwitch = false } catch (e) {}
|
||
try { (this.audioContext as any).autoplay = false } catch (e) {}
|
||
try { (this.audioContext as any).obeyMuteSwitch = false } catch (e) {}
|
||
// this.audioContext.onCanplay(() => {
|
||
// setTimeout(() => {
|
||
// const duration = this.audioContext!.duration
|
||
// if (duration && duration > 0) {
|
||
// this.setData({
|
||
// audioDuration: duration,
|
||
// currentTime: this.audioContext!.currentTime
|
||
// })
|
||
// }
|
||
// }, 100)
|
||
// })
|
||
this.audioContext.onPlay(() => {
|
||
const duration = this.audioContext!.duration
|
||
// 停止可能存在的动画计时器
|
||
if (this.playIconTimer) {
|
||
clearInterval(this.playIconTimer)
|
||
this.playIconTimer = undefined
|
||
}
|
||
// 切换为播放中的图标并启动动画
|
||
this.setData({
|
||
isPlaying: true,
|
||
currentTime: this.audioContext!.currentTime,
|
||
audioDuration: duration && duration > 0 ? duration : 0,
|
||
playIconName: 'sound-low'
|
||
})
|
||
// 每500ms在 play、play-2、play-1 三个图标之间循环切换(顺序:play -> play-2 -> play-1)
|
||
const iconNames = ['sound-low', 'sound', 'sound-high']
|
||
let i = 0
|
||
this.playIconTimer = setInterval(() => {
|
||
i = (i + 1) % iconNames.length
|
||
this.setData({ playIconName: iconNames[i] })
|
||
}, 500) as any
|
||
})
|
||
this.audioContext.onTimeUpdate(() => {
|
||
const currentTime = this.audioContext!.currentTime
|
||
const duration = this.audioContext!.duration
|
||
if (duration && duration > 0) {
|
||
this.setData({
|
||
currentTime,
|
||
audioDuration: duration,
|
||
sliderValue: (currentTime / duration) * 100
|
||
})
|
||
} else {
|
||
this.setData({
|
||
currentTime: 0,
|
||
audioDuration: 0,
|
||
sliderValue: 0
|
||
})
|
||
}
|
||
})
|
||
this.audioContext.onEnded(() => {
|
||
// 结束时清除动画并复位图标
|
||
if (this.playIconTimer) {
|
||
clearInterval(this.playIconTimer)
|
||
this.playIconTimer = undefined
|
||
}
|
||
this.setData({
|
||
isPlaying: false,
|
||
currentTime: 0,
|
||
sliderValue: 0,
|
||
playIconName: 'sound-low'
|
||
})
|
||
})
|
||
this.audioContext.onError((err: any) => {
|
||
// 出错时清除动画并复位图标
|
||
if (this.playIconTimer) {
|
||
clearInterval(this.playIconTimer)
|
||
this.playIconTimer = undefined
|
||
}
|
||
this.setData({
|
||
isPlaying: false,
|
||
currentTime: 0,
|
||
sliderValue: 0,
|
||
playIconName: 'sound-low'
|
||
})
|
||
console.error('评分语音播放错误:', err)
|
||
wx.showToast({ title: '播放失败', icon: 'none' })
|
||
})
|
||
}
|
||
|
||
if (this.audioContext.src !== filePath) {
|
||
try { this.audioContext.pause() } catch (e) {}
|
||
try { this.audioContext.seek(0) } catch (e) {}
|
||
this.setData({ currentTime: 0, sliderValue: 0, audioDuration: 0 })
|
||
this.audioContext.src = filePath
|
||
} else {
|
||
try { this.audioContext.seek(0) } catch (e) {}
|
||
this.setData({ currentTime: 0, sliderValue: 0 })
|
||
}
|
||
|
||
try {
|
||
this.audioContext.play()
|
||
this.setData({ isPlaying: true })
|
||
} catch (error) {
|
||
wx.showToast({ title: '音频播放失败', icon: 'none' })
|
||
}
|
||
}
|
||
if (cachedLocal) {
|
||
playWithPath(cachedLocal)
|
||
} else {
|
||
apiManager.downloadFile(fileId).then((filePath) => {
|
||
this.setData({
|
||
assessmentAudioLocalMap: {
|
||
...this.data.assessmentAudioLocalMap,
|
||
[fileId]: filePath
|
||
}
|
||
})
|
||
playWithPath(filePath)
|
||
}).catch((error) => {
|
||
console.error('下载音频文件失败:', error)
|
||
wx.showToast({ title: '音频下载失败', icon: 'none' })
|
||
})
|
||
}
|
||
},
|
||
|
||
async handleWordClick(e: any) {
|
||
const { word, index } = e.currentTarget.dataset
|
||
// 若例句未被选中,优先选中例句并阻止单词选择
|
||
const { selectedSentenceIndex, justSelectedByWord } = this.data
|
||
if (typeof index === 'number' && index !== selectedSentenceIndex) {
|
||
// 选中目标例句,并更新相关状态(由点击单词触发)
|
||
const sentences = this.data.sentences
|
||
if (!sentences || !sentences[index]) return
|
||
|
||
const currentSentence = sentences[index]
|
||
const assessmentResult = currentSentence?.details?.assessment?.result
|
||
const suggestedScore = assessmentResult?.SuggestedScore ?? -1
|
||
const pronAccuracy = assessmentResult?.PronAccuracy ?? -1
|
||
const pronCompletion = assessmentResult?.PronCompletion ?? -1
|
||
const pronFluency = assessmentResult?.PronFluency ?? -1
|
||
const allNegative = suggestedScore < 0 && pronAccuracy < 0 && pronCompletion < 0 && pronFluency < 0
|
||
|
||
this.resetAudioState()
|
||
this.setData({
|
||
selectedSentenceIndex: index,
|
||
currentSentence,
|
||
hasScoreInfo: !allNegative && !!currentSentence.details,
|
||
totalScore: suggestedScore > 0 ? suggestedScore : 0,
|
||
accuracyScore: pronAccuracy > 0 ? pronAccuracy : 0,
|
||
completenessScore: pronCompletion > 0 ? pronCompletion : 0,
|
||
fluencyScore: pronFluency > 0 ? pronFluency : 0,
|
||
justSelectedByWord: true,
|
||
prototypeWord: ''
|
||
})
|
||
// this.updateCircleProgress()
|
||
return
|
||
}
|
||
|
||
// 若是刚通过点击单词选中句子,则本次点击不弹词典,重置标记位
|
||
if (justSelectedByWord) {
|
||
this.setData({ justSelectedByWord: false })
|
||
// 第二次点击,允许继续执行查询词典逻辑
|
||
}
|
||
|
||
// 清理单词,移除标点符号
|
||
const cleanedWord = word.replace(/[.,?!*;:'"()]/g, '').trim()
|
||
if (!cleanedWord) return
|
||
|
||
// 先清空 prototypeWord,避免新词没有原型时保留旧值
|
||
// 判断是否通过 word-source 标签点击(通过是否有 index 判断)
|
||
const isFromWordSource = typeof index === 'undefined';
|
||
const { isReturningFromPrevious } = this.data;
|
||
|
||
// 只有通过 word-source 标签点击且不是从返回功能调用时才保存当前单词作为上一个单词,并显示返回图标
|
||
let showBackIcon = false;
|
||
let previousWord = '';
|
||
|
||
if (isFromWordSource && !isReturningFromPrevious) {
|
||
// 保存当前单词作为上一个单词,并显示返回图标
|
||
previousWord = this.data.wordDict?.simple?.query || '';
|
||
showBackIcon = !!previousWord;
|
||
}
|
||
|
||
// 注意:这里不立即设置 showBackIcon 和 previousWord,而是在获取单词详情后再设置
|
||
// 重置返回标记位
|
||
this.setData({
|
||
prototypeWord: '',
|
||
isReturningFromPrevious: false
|
||
});
|
||
|
||
try {
|
||
// 调用API获取单词详情
|
||
this.setData({ dictLoading: true, showDictPopup: true })
|
||
const wordDetail: any = await apiManager.getWordDetail(cleanedWord);
|
||
console.log('获取到单词详情:', 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 } }
|
||
})
|
||
}
|
||
}
|
||
|
||
// 这里可以添加处理单词详情的逻辑,比如显示在页面上
|
||
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) {
|
||
console.error('获取单词详情失败:', error)
|
||
wx.showToast({
|
||
title: '获取单词详情失败',
|
||
icon: 'none'
|
||
})
|
||
this.setData({ dictLoading: false })
|
||
}
|
||
},
|
||
|
||
// 选中例句
|
||
handleSentenceSelect(e: any) {
|
||
const { index } = e.currentTarget.dataset
|
||
if (typeof index !== 'number') return
|
||
|
||
const { sentences } = this.data
|
||
if (!sentences || !sentences[index]) return
|
||
|
||
// 切换选中时,重置音频播放状态
|
||
this.resetAudioState()
|
||
|
||
const currentSentence = sentences[index]
|
||
const assessmentResult = currentSentence?.details?.assessment?.result
|
||
const suggestedScore = assessmentResult?.SuggestedScore ?? -1
|
||
const pronAccuracy = assessmentResult?.PronAccuracy ?? -1
|
||
const pronCompletion = assessmentResult?.PronCompletion ?? -1
|
||
const pronFluency = assessmentResult?.PronFluency ?? -1
|
||
|
||
const allNegative = [suggestedScore, pronAccuracy, pronCompletion, pronFluency].every((score: number) => score < 0)
|
||
|
||
const wordScores = assessmentResult?.Words?.map((word: any) => ({
|
||
word: word.Word,
|
||
pronAccuracy: Number(word.PronAccuracy.toFixed(2)),
|
||
pronFluency: Number(word.PronFluency.toFixed(2)),
|
||
matchTag: word.MatchTag || 0,
|
||
phoneInfos: word.PhoneInfos?.map((phone: any) => ({
|
||
phone: phone.Phone,
|
||
pronAccuracy: Number(phone.PronAccuracy.toFixed(2)),
|
||
matchTag: phone.MatchTag || 0
|
||
})) || []
|
||
})) || []
|
||
|
||
this.setData({
|
||
selectedSentenceIndex: index,
|
||
currentSentence,
|
||
currentIndex: index,
|
||
hasScoreInfo: !allNegative && !!currentSentence.details,
|
||
totalScore: suggestedScore >= 0 ? Number(suggestedScore.toFixed(2)) : 0,
|
||
accuracyScore: pronAccuracy >= 0 ? Number(pronAccuracy.toFixed(2)) : 0,
|
||
completenessScore: pronCompletion >= 0 ? Number((pronCompletion * 100).toFixed(2)) : 0,
|
||
fluencyScore: pronFluency >= 0 ? Number((pronFluency * 100).toFixed(2)) : 0,
|
||
wordScores,
|
||
justSelectedByWord: false,
|
||
prototypeWord: '',
|
||
overlayVisible: false,
|
||
highlightShow: false,
|
||
highlightZoom: false,
|
||
focusTransform: '',
|
||
highlightWords: [],
|
||
cachedHighlightWords: [],
|
||
cachedSentenceIndex: -1
|
||
})
|
||
|
||
// 预加载并绑定新的标准语音
|
||
if (currentSentence.id) {
|
||
this.getStandardVoice(currentSentence.id)
|
||
}
|
||
|
||
// try {
|
||
// wx.nextTick(() => {
|
||
// this.computeHighlightLayout()
|
||
// })
|
||
// } catch (e) {
|
||
// setTimeout(() => this.computeHighlightLayout(), 0)
|
||
// }
|
||
},
|
||
|
||
onLoad(options: Record<string, string>) {
|
||
this.ensureRecordPermission()
|
||
// 如果图片ID,调用接口获取文本和评分信息
|
||
if (options.index) {
|
||
this.setData({ currentIndex: Number(options.index) })
|
||
}
|
||
if (options.imageId) {
|
||
wx.showLoading({ title: '加载中...' })
|
||
this.setData({ loadingMaskVisible: true })
|
||
apiManager.getImageTextInit(options.imageId)
|
||
.then(res => {
|
||
// 更新图片路径(如果有)
|
||
if (res.image_file_id) {
|
||
// 使用安全的文件下载方式获取图片
|
||
apiManager.getFileDisplayUrl(res.image_file_id)
|
||
.then(imagePath => {
|
||
this.setData({ imagePath })
|
||
})
|
||
.catch(error => {
|
||
console.error('获取图片失败:', error)
|
||
})
|
||
.finally(() => {
|
||
this.setData({ loadingMaskVisible: false })
|
||
// try { wx.nextTick(() => { this.computeHighlightLayout() }) } catch (e) { setTimeout(() => this.computeHighlightLayout(), 0) }
|
||
})
|
||
}
|
||
// 更新例句和评分信息
|
||
if (res.assessments && res.assessments.length > 0) {
|
||
const sentences = res.assessments
|
||
// 处理句子数据
|
||
const processedSentences = this.processSentences(sentences)
|
||
const index = this.data.currentIndex || 0
|
||
const currentSentence = sentences[index]
|
||
const assessmentResult = currentSentence?.details?.assessment?.result
|
||
const suggestedScore = assessmentResult?.SuggestedScore ?? -1
|
||
const pronAccuracy = assessmentResult?.PronAccuracy ?? -1
|
||
const pronCompletion = assessmentResult?.PronCompletion ?? -1
|
||
const pronFluency = assessmentResult?.PronFluency ?? -1
|
||
|
||
// 检查是否所有评分都为负数
|
||
const allNegative = [suggestedScore, pronAccuracy, pronCompletion, pronFluency].every((score: number) => score < 0)
|
||
|
||
// 处理单词评分数据
|
||
const wordScores = assessmentResult?.Words?.map((word: any) => ({
|
||
word: word.Word,
|
||
pronAccuracy: Number(word.PronAccuracy.toFixed(2)),
|
||
pronFluency: Number(word.PronFluency.toFixed(2)),
|
||
matchTag: word.MatchTag || 0,
|
||
phoneInfos: word.PhoneInfos?.map((phone: any) => ({
|
||
phone: phone.Phone,
|
||
pronAccuracy: Number(phone.PronAccuracy.toFixed(2)),
|
||
matchTag: phone.MatchTag || 0
|
||
})) || []
|
||
})) || []
|
||
|
||
this.setData({
|
||
sentences,
|
||
processedSentences,
|
||
currentSentence,
|
||
currentIndex: index,
|
||
selectedSentenceIndex: index,
|
||
hasScoreInfo: !allNegative && !!currentSentence.details,
|
||
totalScore: suggestedScore >= 0 ? Number(suggestedScore.toFixed(2)) : 0,
|
||
accuracyScore: pronAccuracy >= 0 ? Number(pronAccuracy.toFixed(2)) : 0,
|
||
completenessScore: pronCompletion >= 0 ? Number((pronCompletion * 100).toFixed(2)) : 0,
|
||
fluencyScore: pronFluency >= 0 ? Number((pronFluency * 100).toFixed(2)) : 0,
|
||
wordScores
|
||
})
|
||
// this.updateWordsAndIpas(currentSentence.content, currentSentence.ipa)
|
||
// 获取当前例句的标准语音
|
||
if (currentSentence.id) {
|
||
this.getStandardVoice(currentSentence.id)
|
||
}
|
||
// try {
|
||
// wx.nextTick(() => {
|
||
// this.computeHighlightLayout()
|
||
// })
|
||
// } catch (e) {
|
||
// setTimeout(() => this.computeHighlightLayout(), 0)
|
||
// }
|
||
if (!res.image_file_id) {
|
||
this.setData({ loadingMaskVisible: false })
|
||
}
|
||
}
|
||
// 初始化圆形进度条
|
||
// this.updateCircleProgress()
|
||
})
|
||
.catch(err => {
|
||
console.error('获取图片文本和评分信息失败:', err)
|
||
wx.showToast({
|
||
title: '加载失败',
|
||
icon: 'none'
|
||
})
|
||
this.setData({ loadingMaskVisible: false })
|
||
// try { wx.nextTick(() => { this.computeHighlightLayout() }) } catch (e) { setTimeout(() => this.computeHighlightLayout(), 0) }
|
||
})
|
||
.finally(() => {
|
||
wx.hideLoading()
|
||
})
|
||
} else {
|
||
// 初始化圆形进度条
|
||
// this.updateCircleProgress()
|
||
}
|
||
|
||
// 录音事件监听(提前绑定,避免事件丢失)
|
||
if (!recorderHandlersBound) {
|
||
recorderManager.onStop((res) => {
|
||
const ms = Date.now() - this.data.recordStartTime
|
||
if (ms >= 3000) {
|
||
wx.showModal({
|
||
title: '提示',
|
||
content: '录音完成,是否确认提交?',
|
||
success: (result) => {
|
||
if (result.confirm) {
|
||
// wx.showLoading({ title: '正在解析...' })
|
||
console.log('录音文件路径:', res.tempFilePath)
|
||
this.setData({ analizing: true })
|
||
apiManager.uploadFile(res.tempFilePath).then((fileId) => {
|
||
apiManager.getAssessmentResult(fileId, this.data.currentSentence.id).then((result) => {
|
||
const assessmentResult = result.assessment_result.assessment.result
|
||
const suggestedScore = assessmentResult.SuggestedScore
|
||
const pronAccuracy = assessmentResult.PronAccuracy
|
||
const pronCompletion = assessmentResult.PronCompletion
|
||
const pronFluency = assessmentResult.PronFluency
|
||
|
||
const allNegative = [suggestedScore, pronAccuracy, pronCompletion, pronFluency].every(score => score < 0)
|
||
|
||
const { sentences, currentIndex } = this.data
|
||
sentences[currentIndex].details = {
|
||
assessment: { result: assessmentResult }
|
||
}
|
||
sentences[currentIndex].file_id = fileId
|
||
|
||
const wordScores = assessmentResult.Words?.map((word: any) => ({
|
||
word: word.Word,
|
||
pronAccuracy: Number(word.PronAccuracy.toFixed(2)),
|
||
pronFluency: Number(word.PronFluency.toFixed(2)),
|
||
matchTag: word.MatchTag || 0,
|
||
phoneInfos: word.PhoneInfos?.map((phone: any) => ({
|
||
phone: phone.Phone,
|
||
pronAccuracy: Number(phone.PronAccuracy.toFixed(2)),
|
||
matchTag: phone.MatchTag || 0
|
||
}))
|
||
})) || []
|
||
|
||
this.setData({
|
||
sentences,
|
||
currentSentence: sentences[currentIndex],
|
||
hasScoreInfo: !allNegative && !!assessmentResult,
|
||
totalScore: suggestedScore >= 0 ? Number(suggestedScore.toFixed(2)) : 0,
|
||
accuracyScore: pronAccuracy >= 0 ? Number(pronAccuracy.toFixed(2)) : 0,
|
||
completenessScore: pronCompletion >= 0 ? Number(pronCompletion.toFixed(2)) : 0,
|
||
fluencyScore: pronFluency >= 0 ? Number(pronFluency.toFixed(2)) : 0,
|
||
wordScores,
|
||
isRecording: false,
|
||
overlayVisible: false,
|
||
highlightShow: false,
|
||
highlightZoom: false,
|
||
focusTransform: '',
|
||
highlightWords: [],
|
||
analizing: false
|
||
})
|
||
|
||
this.onScoreTap()
|
||
// this.updateCircleProgress()
|
||
// wx.hideLoading()
|
||
}).catch(err => {
|
||
console.error('获取评估结果失败:', err)
|
||
this.setData({
|
||
isRecording: false,
|
||
overlayVisible: false,
|
||
highlightShow: false,
|
||
highlightZoom: false,
|
||
focusTransform: '',
|
||
highlightWords: [],
|
||
analizing: false
|
||
})
|
||
wx.showToast({ title: '评估失败', icon: 'none' })
|
||
wx.hideLoading()
|
||
})
|
||
}).catch(err => {
|
||
console.error('上传录音失败:', err)
|
||
this.setData({
|
||
isRecording: false,
|
||
overlayVisible: false,
|
||
highlightShow: false,
|
||
highlightZoom: false,
|
||
focusTransform: '',
|
||
highlightWords: [],
|
||
analizing: false
|
||
})
|
||
wx.showToast({ title: '上传失败', icon: 'none' })
|
||
wx.hideLoading()
|
||
})
|
||
}
|
||
},
|
||
fail: () => {
|
||
this.setData({
|
||
isRecording: false,
|
||
overlayVisible: false,
|
||
highlightShow: false,
|
||
highlightZoom: false,
|
||
focusTransform: '',
|
||
highlightWords: [],
|
||
analizing: false
|
||
})
|
||
}
|
||
})
|
||
} else {
|
||
this.setData({
|
||
isRecording: false,
|
||
overlayVisible: false,
|
||
highlightShow: false,
|
||
highlightZoom: false,
|
||
focusTransform: '',
|
||
highlightWords: [],
|
||
analizing: false
|
||
})
|
||
wx.showToast({
|
||
title: '说话时间太短',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
|
||
recorderManager.onError((res) => {
|
||
wx.showToast({ title: '录音失败', icon: 'none' })
|
||
// 录音出错时淡出高亮层
|
||
this.setData({ overlayVisible: false, isRecording: false })
|
||
const timer = setTimeout(() => {
|
||
this.setData({ highlightShow: false, highlightZoom: false, focusTransform: '', highlightWords: [] })
|
||
clearTimeout(timer)
|
||
}, 320)
|
||
})
|
||
|
||
recorderHandlersBound = true
|
||
}
|
||
},
|
||
|
||
noop() {},
|
||
|
||
onUnload() {
|
||
// 销毁音频实例
|
||
if (this.audioContext) {
|
||
this.audioContext.destroy()
|
||
}
|
||
if (this.circleAnimTimer) {
|
||
clearInterval(this.circleAnimTimer)
|
||
this.circleAnimTimer = undefined
|
||
}
|
||
// 销毁单词音频实例与清理定时器
|
||
try { this.wordAudioContext && this.wordAudioContext.stop() } catch (e) {}
|
||
try {
|
||
if (this.wordAudioContext) {
|
||
this.wordAudioContext.offPlay()
|
||
this.wordAudioContext.offEnded()
|
||
this.wordAudioContext.offError()
|
||
this.wordAudioContext.destroy()
|
||
this.wordAudioContext = undefined
|
||
}
|
||
} catch (e) {}
|
||
if (this.wordAudioIconTimer) {
|
||
clearInterval(this.wordAudioIconTimer)
|
||
this.wordAudioIconTimer = undefined
|
||
}
|
||
this.setData({
|
||
wordAudioPlaying: false,
|
||
wordAudioIconName: 'sound',
|
||
activeWordAudioType: '',
|
||
// 清空页面级音频缓存映射
|
||
standardAudioLocalMap: {},
|
||
assessmentAudioLocalMap: {}
|
||
})
|
||
},
|
||
|
||
onReady() {
|
||
// 录音事件监听已在 onLoad 提前绑定,避免未绑定时触发 stop 事件导致丢失
|
||
},
|
||
|
||
// 点击“more”按钮,展开/收起扩展内容
|
||
handleDictMore() {
|
||
this.setData({
|
||
showDictExtended: !this.data.showDictExtended
|
||
})
|
||
},
|
||
|
||
// 点击“x”按钮,关闭弹窗
|
||
handleDictClose() {
|
||
// 关闭弹窗
|
||
this.setData({
|
||
showDictExtended: false,
|
||
showDictPopup: false,
|
||
showBackIcon: false, // 清除返回图标标记位
|
||
previousWord: '', // 清除上一个单词
|
||
isReturningFromPrevious: false // 清除返回标记位
|
||
})
|
||
// 停止并销毁单词音频,重置轮播状态
|
||
try { this.wordAudioContext && this.wordAudioContext.stop() } catch (e) {}
|
||
try {
|
||
if (this.wordAudioContext) {
|
||
this.wordAudioContext.offPlay()
|
||
this.wordAudioContext.offEnded()
|
||
this.wordAudioContext.offError()
|
||
this.wordAudioContext.destroy()
|
||
this.wordAudioContext = undefined
|
||
}
|
||
} catch (e) {}
|
||
if (this.wordAudioIconTimer) {
|
||
clearInterval(this.wordAudioIconTimer)
|
||
this.wordAudioIconTimer = undefined
|
||
}
|
||
this.setData({
|
||
wordAudioPlaying: false,
|
||
wordAudioIconName: 'sound',
|
||
activeWordAudioType: ''
|
||
})
|
||
},
|
||
|
||
onTabsChange(event: any) {
|
||
console.log(`Change tab, tab-panel value is ${event.detail.value}.`);
|
||
},
|
||
|
||
onTabsClick(event: any) {
|
||
console.log(`Click tab, tab-panel value is ${event.detail.value}.`);
|
||
},
|
||
|
||
// 播放单词音频(带图标轮播)
|
||
playWordAudio(e: any) {
|
||
const { audio, type } = e.currentTarget.dataset
|
||
if (!audio) return
|
||
|
||
// 如果点击同一类型且正在播放,则切换为停止
|
||
if (this.data.wordAudioPlaying && this.data.activeWordAudioType === type) {
|
||
try { this.wordAudioContext && this.wordAudioContext.stop() } catch (err) {}
|
||
try {
|
||
if (this.wordAudioContext) {
|
||
this.wordAudioContext.offPlay()
|
||
this.wordAudioContext.offEnded()
|
||
this.wordAudioContext.offError()
|
||
this.wordAudioContext.destroy()
|
||
this.wordAudioContext = undefined
|
||
}
|
||
} catch (err) {}
|
||
if (this.wordAudioIconTimer) {
|
||
clearInterval(this.wordAudioIconTimer)
|
||
this.wordAudioIconTimer = undefined
|
||
}
|
||
this.setData({
|
||
wordAudioPlaying: false,
|
||
wordAudioIconName: 'sound',
|
||
activeWordAudioType: ''
|
||
})
|
||
return
|
||
}
|
||
|
||
// 先清理上一次的上下文与定时器
|
||
try { this.wordAudioContext && this.wordAudioContext.stop() } catch (err) {}
|
||
try {
|
||
if (this.wordAudioContext) {
|
||
this.wordAudioContext.offPlay()
|
||
this.wordAudioContext.offEnded()
|
||
this.wordAudioContext.offError()
|
||
this.wordAudioContext.destroy()
|
||
this.wordAudioContext = undefined
|
||
}
|
||
} catch (err) {}
|
||
if (this.wordAudioIconTimer) {
|
||
clearInterval(this.wordAudioIconTimer)
|
||
this.wordAudioIconTimer = undefined
|
||
}
|
||
|
||
// 构造音频URL
|
||
const audioUrl = `https://dict.youdao.com/dictvoice?audio=${audio}`
|
||
|
||
// 创建音频上下文
|
||
this.wordAudioContext = wx.createInnerAudioContext()
|
||
try { (this.wordAudioContext as any).autoplay = false } catch (err) {}
|
||
|
||
// 绑定事件
|
||
this.wordAudioContext.onPlay(() => {
|
||
// 开始轮播图标
|
||
const seq = ['sound', 'sound-low']
|
||
let i = 0
|
||
this.setData({ wordAudioPlaying: true, activeWordAudioType: type, wordAudioIconName: seq[0] })
|
||
this.wordAudioIconTimer = setInterval(() => {
|
||
i = (i + 1) % seq.length
|
||
this.setData({ wordAudioIconName: seq[i] })
|
||
}, 400) as any
|
||
})
|
||
|
||
const finalize = () => {
|
||
if (this.wordAudioIconTimer) {
|
||
clearInterval(this.wordAudioIconTimer)
|
||
this.wordAudioIconTimer = undefined
|
||
}
|
||
this.setData({
|
||
wordAudioPlaying: false,
|
||
wordAudioIconName: 'sound',
|
||
activeWordAudioType: ''
|
||
})
|
||
try {
|
||
if (this.wordAudioContext) {
|
||
this.wordAudioContext.offPlay()
|
||
this.wordAudioContext.offEnded()
|
||
this.wordAudioContext.offError()
|
||
this.wordAudioContext.destroy()
|
||
this.wordAudioContext = undefined
|
||
}
|
||
} catch (err) {}
|
||
}
|
||
|
||
this.wordAudioContext.onEnded(() => {
|
||
finalize()
|
||
})
|
||
|
||
this.wordAudioContext.onError((res) => {
|
||
console.error('播放单词音频失败:', res)
|
||
wx.showToast({ title: '播放失败', icon: 'none' })
|
||
finalize()
|
||
})
|
||
|
||
// 设置音频源并播放
|
||
this.wordAudioContext.src = audioUrl
|
||
this.wordAudioContext.play()
|
||
},
|
||
|
||
onScoreTap() {
|
||
if (this.data.isRecording) return
|
||
console.log('Score button tapped')
|
||
// 当当前选中例句无评分信息时,不响应点击
|
||
if (!this.data.hasScoreInfo) {
|
||
return
|
||
}
|
||
// 有评分信息时,打开评分结果弹窗
|
||
this.setData({
|
||
isScoreModalOpen: true,
|
||
scoreModalVisible: false
|
||
})
|
||
setTimeout(() => {
|
||
this.setData({ scoreModalVisible: true })
|
||
setTimeout(() => {
|
||
this.updateCircleProgress()
|
||
}, 100)
|
||
}, 0)
|
||
},
|
||
onCloseScoreModal() {
|
||
// 关闭评分弹窗时重置音频播放状态,避免残留播放
|
||
this.resetAudioState()
|
||
if (this.circleAnimTimer) {
|
||
clearInterval(this.circleAnimTimer)
|
||
this.circleAnimTimer = undefined
|
||
}
|
||
this.setData({ scoreModalVisible: false })
|
||
this.setData({
|
||
circleProgressStyle: 'background: conic-gradient(transparent 0)',
|
||
accuracyCircleStyle: 'background: conic-gradient(transparent 0)',
|
||
completenessCircleStyle: 'background: conic-gradient(transparent 0)',
|
||
fluencyCircleStyle: 'background: conic-gradient(transparent 0)',
|
||
displayTotalScore: 0,
|
||
displayAccuracyScore: 0,
|
||
displayCompletenessScore: 0,
|
||
displayFluencyScore: 0,
|
||
isScoreModalOpen: false
|
||
})
|
||
},
|
||
onTransTap() {
|
||
if (this.data.isRecording) return
|
||
console.log('User button tapped')
|
||
// Cycle through translation display modes
|
||
const currentMode = this.data.transDisplayMode
|
||
let nextMode: 'en' | 'en_ipa' | 'en_zh' = 'en'
|
||
|
||
switch (currentMode) {
|
||
case 'en':
|
||
nextMode = 'en_ipa'
|
||
break
|
||
case 'en_ipa':
|
||
nextMode = 'en_zh'
|
||
break
|
||
case 'en_zh':
|
||
nextMode = 'en'
|
||
break
|
||
default:
|
||
nextMode = 'en'
|
||
}
|
||
|
||
this.setData({
|
||
transDisplayMode: nextMode
|
||
})
|
||
},
|
||
|
||
// 获取翻译按钮图标名称
|
||
getTransIconName() {
|
||
switch (this.data.transDisplayMode) {
|
||
case 'en':
|
||
return 'file-teams'
|
||
case 'en_ipa':
|
||
return 'file-search'
|
||
case 'en_zh':
|
||
return 'translate'
|
||
default:
|
||
return 'file-teams'
|
||
}
|
||
},
|
||
|
||
onPageScroll(e: any) {
|
||
if (!e || typeof e.scrollTop !== 'number') {
|
||
// 忽略无效滚动事件,避免误判为 0 导致回到大图
|
||
return
|
||
}
|
||
const scrollTop = e.scrollTop
|
||
const shrinkThreshold = 120
|
||
// 一旦缩小,保持小图;不再在下滑时恢复为大图
|
||
if (!this.data.imageSmall && scrollTop >= shrinkThreshold) {
|
||
this.setData({
|
||
imageSmall: true,
|
||
imageMode: 'aspectFit'
|
||
})
|
||
}
|
||
|
||
// try { if ((this as any)._layoutDebounceTimer) clearTimeout((this as any)._layoutDebounceTimer) } catch (e) {}
|
||
// ;(this as any)._layoutDebounceTimer = setTimeout(() => {
|
||
// this.computeHighlightLayout()
|
||
// }, 100)
|
||
},
|
||
|
||
// 处理句子数据,分割单词和音标
|
||
processSentences(sentences: any[]) {
|
||
return sentences.map(sentence => {
|
||
// 分割内容和音标
|
||
const words = sentence.content ? sentence.content.split(' ') : []
|
||
const ipas = sentence.ipa ? sentence.ipa.split(' ') : []
|
||
|
||
// 处理标点符号,确保单词末尾的标点符号保留在正确位置
|
||
const processedWords = words.map((word: string, index: number) => {
|
||
// 如果是最后一个单词,保留可能的句号或逗号
|
||
if (index === words.length - 1) {
|
||
// 保留句末标点符号
|
||
return word
|
||
}
|
||
// 对于其他单词,移除常见的标点符号
|
||
return word.replace(/[.,;:!?]/g, '')
|
||
})
|
||
|
||
return {
|
||
words: processedWords,
|
||
ipas,
|
||
zh: sentence.zh || ''
|
||
}
|
||
})
|
||
},
|
||
|
||
// 处理 Collins 数据中的 HTML 标签
|
||
processCollinsData(collinsData: any) {
|
||
if (!collinsData || !collinsData.collins_entries) {
|
||
return collinsData;
|
||
}
|
||
|
||
// 深拷贝数据以避免修改原始数据
|
||
const processedData = JSON.parse(JSON.stringify(collinsData));
|
||
|
||
// 遍历 collins_entries
|
||
processedData.collins_entries.forEach((entry: any) => {
|
||
if (entry.entries && entry.entries.entry) {
|
||
// 遍历 entry
|
||
entry.entries.entry.forEach((entryItem: any) => {
|
||
if (entryItem.tran_entry) {
|
||
// 遍历 tran_entry
|
||
entryItem.tran_entry.forEach((tranEntry: any) => {
|
||
// 处理 tran 字段中的 HTML 标签
|
||
if (tranEntry.tran) {
|
||
// 将包含 <b> 标签的文本转换为数组格式便于 WXML 渲染
|
||
const parts = tranEntry.tran.split(/(<b>.*?<\/b>)/g).filter(Boolean);
|
||
const processedParts: Array<{text: string, bold: boolean}> = [];
|
||
|
||
parts.forEach((part: string) => {
|
||
if (part.startsWith('<b>') && part.endsWith('</b>')) {
|
||
// 提取 bold 文本内容
|
||
const text = part.substring(3, part.length - 4);
|
||
processedParts.push({text, bold: true});
|
||
} else {
|
||
// 普通文本
|
||
processedParts.push({text: part, bold: false});
|
||
}
|
||
});
|
||
|
||
// 保存处理后的部分
|
||
tranEntry.tranParts = processedParts;
|
||
// 保留原始文本用于其他用途
|
||
tranEntry.originalTran = tranEntry.tran;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
return processedData;
|
||
},
|
||
|
||
onMoreTap() {
|
||
if (this.data.isRecording) return
|
||
const { isMoreMenuOpen } = this.data
|
||
if (!isMoreMenuOpen) {
|
||
this.setData({ isMoreMenuOpen: true, isMoreMenuClosing: false })
|
||
return
|
||
}
|
||
this.setData({ isMoreMenuOpen: false, isMoreMenuClosing: true })
|
||
const timer = setTimeout(() => {
|
||
this.setData({ isMoreMenuClosing: false })
|
||
clearTimeout(timer)
|
||
}, 360)
|
||
},
|
||
|
||
computeHighlightLayout() {
|
||
const { processedSentences, selectedSentenceIndex } = this.data
|
||
if (!processedSentences || processedSentences.length === 0) return
|
||
const sentence = processedSentences[selectedSentenceIndex]
|
||
const words = sentence?.words || []
|
||
if (!words || words.length === 0) return
|
||
|
||
const sys = wx.getSystemInfoSync()
|
||
const windowWidth = sys.windowWidth || 375
|
||
const windowHeight = sys.windowHeight || 667
|
||
const bottomOffset = 120
|
||
const scale = 2.5
|
||
|
||
const centerX = windowWidth / 2
|
||
const centerY = (windowHeight - bottomOffset) / 2
|
||
|
||
const query = wx.createSelectorQuery().in(this as any)
|
||
query.selectAll('.sentence-wrapper.selected .sentence-text').boundingClientRect()
|
||
query.exec((res: any) => {
|
||
const rects = (res && res[0]) || []
|
||
if (!rects || rects.length === 0) return
|
||
|
||
const sidePadding = 24
|
||
const wordSpacing = 12
|
||
const rowSpacing = 16
|
||
const availWidth = Math.max(windowWidth - sidePadding * 2, 100)
|
||
const scaledHeights = rects.map((r: any) => r.height * scale)
|
||
const rowHeight = Math.max(...scaledHeights)
|
||
const scaledWidths = rects.map((r: any) => Math.max(r.width * scale, 10))
|
||
|
||
const rows: { idxs: number[], width: number }[] = []
|
||
let current: { idxs: number[], width: number } = { idxs: [], width: 0 }
|
||
scaledWidths.forEach((w, i) => {
|
||
const extra = current.idxs.length > 0 ? wordSpacing : 0
|
||
if (current.width + extra + w <= availWidth) {
|
||
current.idxs.push(i)
|
||
current.width += extra + w
|
||
} else {
|
||
if (current.idxs.length > 0) rows.push(current)
|
||
current = { idxs: [i], width: w }
|
||
}
|
||
})
|
||
if (current.idxs.length > 0) rows.push(current)
|
||
|
||
const totalHeight = rows.length * rowHeight + Math.max(rows.length - 1, 0) * rowSpacing
|
||
const firstRowCenterY = centerY - totalHeight / 2 + rowHeight / 2
|
||
|
||
const targetWords = rects.map((r: any, idx: number) => {
|
||
const rcx = r.left + r.width / 2
|
||
const rcy = r.top + r.height / 2
|
||
let rowIndex = 0
|
||
let y = firstRowCenterY
|
||
for (let ri = 0; ri < rows.length; ri++) {
|
||
if (rows[ri].idxs.includes(idx)) { rowIndex = ri; break }
|
||
y += rowHeight + rowSpacing
|
||
}
|
||
const row = rows[rowIndex]
|
||
const rowStartX = centerX - row.width / 2
|
||
let cumX = 0
|
||
for (const j of row.idxs) {
|
||
if (j === idx) break
|
||
cumX += scaledWidths[j] + wordSpacing
|
||
}
|
||
const targetCx = rowStartX + cumX + scaledWidths[idx] / 2
|
||
const targetCy = firstRowCenterY + rowIndex * (rowHeight + rowSpacing)
|
||
const dx = targetCx - rcx
|
||
const dy = targetCy - rcy
|
||
const transform = `translate(${dx}px, ${dy}px) scale(${scale})`
|
||
return {
|
||
text: words[idx] || '',
|
||
left: r.left,
|
||
top: r.top,
|
||
width: r.width,
|
||
height: r.height,
|
||
targetTransform: transform,
|
||
transform: 'translate(0px, 0px) scale(1)'
|
||
}
|
||
})
|
||
|
||
this.setData({ cachedHighlightWords: targetWords, cachedSentenceIndex: selectedSentenceIndex })
|
||
})
|
||
},
|
||
|
||
ensureRecordPermission() {
|
||
try {
|
||
wx.getSetting({
|
||
success: (res) => {
|
||
const granted = !!(res.authSetting && res.authSetting['scope.record'])
|
||
if (granted) {
|
||
this.setData({ recordPermissionGranted: true })
|
||
return
|
||
}
|
||
wx.authorize({
|
||
scope: 'scope.record',
|
||
success: () => {
|
||
this.setData({ recordPermissionGranted: true })
|
||
},
|
||
fail: () => {
|
||
wx.showModal({
|
||
title: '需要麦克风权限',
|
||
content: '录音功能需要麦克风权限,请在设置中开启',
|
||
confirmText: '去设置',
|
||
cancelText: '取消',
|
||
success: (r) => {
|
||
if (r.confirm) {
|
||
wx.openSetting({
|
||
success: (s) => {
|
||
const ok = !!(s.authSetting && s.authSetting['scope.record'])
|
||
this.setData({ recordPermissionGranted: ok })
|
||
}
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
})
|
||
}
|
||
})
|
||
} catch (e) {}
|
||
},
|
||
|
||
// 返回上一个单词的查询结果
|
||
handleBackToPreviousWord() {
|
||
const { previousWord } = this.data;
|
||
if (previousWord === '') return;
|
||
|
||
// 设置返回标记位
|
||
this.setData({
|
||
isReturningFromPrevious: true
|
||
});
|
||
|
||
// 调用 handleWordClick 方法查询上一个单词
|
||
const event = {
|
||
currentTarget: {
|
||
dataset: {
|
||
word: previousWord
|
||
}
|
||
}
|
||
};
|
||
|
||
// 查询上一个单词
|
||
this.handleWordClick(event);
|
||
}
|
||
})
|