Merge pull request '[lisa]fix: bugs' (#12) from lisa into main

Reviewed-on: http://gitea.xhzone.cn/felix/miniprogram-1/pulls/12
This commit is contained in:
felix
2025-11-09 06:40:52 +00:00
2 changed files with 181 additions and 67 deletions

View File

@@ -52,6 +52,9 @@ interface IPageData {
playIconSrc: string // 播放图标图片源
showDictPopup: boolean // 控制弹窗是否显示
showDictExtended: boolean // 控制扩展内容是否显示
wordAudioPlaying: boolean // 单词音频是否播放中
wordAudioIconName: string // sound/sound-low/sound-mute
activeWordAudioType: string // 'uk' | 'us' 当前播放的音频类型
wordDict: {
ee: any
ec: any
@@ -141,6 +144,9 @@ interface IPageInstance extends IPageMethods {
data: IPageData
audioContext?: WechatMiniprogram.InnerAudioContext
playIconTimer?: number
// 新增:单词音频上下文与图标轮播定时器
wordAudioContext?: WechatMiniprogram.InnerAudioContext
wordAudioIconTimer?: number
}
Page<IPageData, IPageInstance>({
@@ -198,6 +204,10 @@ Page<IPageData, IPageInstance>({
playIconSrc: '/static/play.png',
showDictPopup: false, // 控制弹窗是否显示
showDictExtended: false, // 控制扩展内容是否显示
// 新增:单词音频播放与图标轮播状态
wordAudioPlaying: false,
wordAudioIconName: 'sound',
activeWordAudioType: '',
processedSentences: [], // 处理后的句子数据
transDisplayMode: 'en', // 默认显示模式为英文 only
isScoreModalOpen: false, // 评分结果弹窗是否打开
@@ -292,22 +302,30 @@ Page<IPageData, IPageInstance>({
resetAudioState() {
// 隐藏可能存在的加载提示
wx.hideLoading()
if (this.audioContext) {
this.audioContext.stop()
// 清空音频源以取消当前加载
this.audioContext.src = ''
}
this.setData({
isPlaying: false,
currentTime: 0,
sliderValue: 0,
audioDuration: 0
})
if (this.playIconTimer) {
clearInterval(this.playIconTimer)
this.playIconTimer = undefined
// 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,
@@ -568,6 +586,7 @@ Page<IPageData, IPageInstance>({
if (!this.audioContext) {
this.audioContext = wx.createInnerAudioContext()
try { (this.audioContext as any).autoplay = false } catch (e) {}
this.audioContext.onCanplay(() => {
setTimeout(() => {
const duration = this.audioContext!.duration
@@ -626,33 +645,18 @@ Page<IPageData, IPageInstance>({
})
})
this.audioContext.onError(() => {
// 出错时清除动画并复位图标,不再自动重试,避免误播上一个音频
if (this.playIconTimer) {
clearInterval(this.playIconTimer)
this.playIconTimer = undefined
}
this.setData({
isPlaying: false,
currentTime: 0,
sliderValue: 0
})
// 播放失败时重新尝试
if (this.audioContext?.src) {
wx.showLoading({ title: '正在重试...' })
setTimeout(() => {
try {
this.audioContext?.play()
wx.hideLoading()
this.setData({ isPlaying: true })
} catch (error) {
wx.hideLoading()
wx.showToast({
title: '播放失败',
icon: 'none'
})
}
}, 1000)
return
}
wx.showToast({
title: '播放失败',
icon: 'none'
sliderValue: 0,
playIconSrc: '/static/play.png'
})
wx.showToast({ title: '播放失败', icon: 'none' })
})
this.audioContext.onTimeUpdate(() => {
const currentTime = this.audioContext!.currentTime
@@ -667,13 +671,10 @@ Page<IPageData, IPageInstance>({
}
if (this.audioContext.src !== audioUrl) {
// 重置状态
this.setData({
currentTime: 0,
sliderValue: 0,
audioDuration: 0
})
// 设置新的音频源
// 切换音源前,先暂停并复位进度,避免 iOS 上继续播放旧音源
try { this.audioContext.pause() } catch (e) {}
try { this.audioContext.seek(0) } catch (e) {}
this.setData({ currentTime: 0, sliderValue: 0, audioDuration: 0 })
this.audioContext.src = audioUrl
}
@@ -848,7 +849,8 @@ Page<IPageData, IPageInstance>({
accuracyScore: pronAccuracy > 0 ? pronAccuracy : 0,
completenessScore: pronCompletion > 0 ? pronCompletion : 0,
fluencyScore: pronFluency > 0 ? pronFluency : 0,
justSelectedByWord: true
justSelectedByWord: true,
prototypeWord: ''
})
this.updateCircleProgress()
return
@@ -863,6 +865,9 @@ Page<IPageData, IPageInstance>({
// 清理单词,移除标点符号
const cleanedWord = word.replace(/[.,?!*;:]/g, '').trim()
if (!cleanedWord) return
// 先清空 prototypeWord避免新词没有原型时保留旧值
this.setData({ prototypeWord: '' })
try {
// 调用API获取单词详情
@@ -882,7 +887,7 @@ Page<IPageData, IPageInstance>({
relWord: wordDetail['rel_word'], syno: wordDetail['syno'],
discriminate: wordDetail['discriminate']
},
prototypeWord: wordDetail['ec'].word[0]?.prototype
prototypeWord: (wordDetail?.ec?.word?.[0]?.prototype) || ''
})
} catch (error) {
console.error('获取单词详情失败:', error)
@@ -935,7 +940,8 @@ Page<IPageData, IPageInstance>({
completenessScore: pronCompletion >= 0 ? Number((pronCompletion * 100).toFixed(2)) : 0,
fluencyScore: pronFluency >= 0 ? Number((pronFluency * 100).toFixed(2)) : 0,
wordScores,
justSelectedByWord: false
justSelectedByWord: false,
prototypeWord: ''
})
// 预加载并绑定新的标准语音
@@ -1102,6 +1108,26 @@ Page<IPageData, IPageInstance>({
if (this.audioContext) {
this.audioContext.destroy()
}
// 销毁单词音频实例与清理定时器
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: ''
})
},
onReady() {
@@ -1117,10 +1143,31 @@ Page<IPageData, IPageInstance>({
// 点击“x”按钮关闭弹窗
handleDictClose() {
// 关闭弹窗
this.setData({
showDictExtended: false,
showDictPopup: 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) {
@@ -1131,37 +1178,104 @@ Page<IPageData, IPageInstance>({
console.log(`Click tab, tab-panel value is ${event.detail.value}.`);
},
// 播放单词音频
// 播放单词音频(带图标轮播)
playWordAudio(e: any) {
const { audio } = e.currentTarget.dataset
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}`
// 创建音频上下文
const wordAudioContext = wx.createInnerAudioContext()
wordAudioContext.onPlay(() => {
console.log('开始播放单词音频')
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
})
wordAudioContext.onError((res) => {
console.error('播放单词音频失败:', res)
wx.showToast({
title: '播放失败',
icon: 'none'
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()
})
wordAudioContext.onEnded(() => {
console.log('单词音频播放结束')
wordAudioContext.destroy()
this.wordAudioContext.onError((res) => {
console.error('播放单词音频失败:', res)
wx.showToast({ title: '播放失败', icon: 'none' })
finalize()
})
// 设置音频源并播放
wordAudioContext.src = audioUrl
wordAudioContext.play()
this.wordAudioContext.src = audioUrl
this.wordAudioContext.play()
},
onScoreTap() {

View File

@@ -156,13 +156,13 @@
<span class="pron-item-text" wx:if="{{wordDict.simple.word[0].ukphone}}">
UK:[{{wordDict.simple.word[0].ukphone}}]
</span>
<t-icon class="ipa-audio" wx:if="{{wordDict.simple.word[0].ukspeech}}" bind:click="playWordAudio" data-audio="{{wordDict.simple.word[0].ukspeech}}" name="sound" size="30rpx" />
<t-icon class="ipa-audio" wx:if="{{wordDict.simple.word[0].ukspeech}}" bind:click="playWordAudio" data-type="uk" data-audio="{{wordDict.simple.word[0].ukspeech}}" name="{{(activeWordAudioType === 'uk' && wordAudioPlaying) ? wordAudioIconName : 'sound'}}" size="30rpx" />
</view>
<view class="pron-item" wx:if="{{wordDict.simple && wordDict.simple.word && wordDict.simple.word.length > 0}}">
<span class="pron-item-text" wx:if="{{wordDict.simple.word[0].usphone}}">
US:[{{wordDict.simple.word[0].usphone}}]
</span>
<t-icon class="ipa-audio" wx:if="{{wordDict.simple.word[0].usspeech}}" bind:click="playWordAudio" data-audio="{{wordDict.simple.word[0].usspeech}}" name="sound" size="30rpx" />
<t-icon class="ipa-audio" wx:if="{{wordDict.simple.word[0].usspeech}}" bind:click="playWordAudio" data-type="us" data-audio="{{wordDict.simple.word[0].usspeech}}" name="{{(activeWordAudioType === 'us' && wordAudioPlaying) ? wordAudioIconName : 'sound'}}" size="30rpx" />
</view>
</view>
<!-- 基础词性释义 -->