|
|
|
@ -19,7 +19,7 @@ |
|
|
|
|
|
|
|
<!-- 文章内容 --> |
|
|
|
<view class="article-content"> |
|
|
|
<uv-parse :content="articleData.content"/> |
|
|
|
<uv-parse :content="content"/> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
@ -35,11 +35,17 @@ |
|
|
|
:color="isPlaying ? '#ff6b6b' : '#4CAF50'"></uv-icon> |
|
|
|
</view> |
|
|
|
|
|
|
|
<!-- 底部字幕展示 --> |
|
|
|
<!-- <view v-if="hasAudio && subtitlesJson.length > 0 && currentSubtitleText" class="subtitle-container"> |
|
|
|
<text class="subtitle-text">{{ currentSubtitleText }}</text> |
|
|
|
</view> --> |
|
|
|
|
|
|
|
</view> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script> |
|
|
|
import audioManager from '@/utils/audioManager.js' |
|
|
|
import { convertToSentences } from '@/utils/audioUtils.js' |
|
|
|
|
|
|
|
export default { |
|
|
|
data() { |
|
|
|
@ -49,10 +55,18 @@ export default { |
|
|
|
loading: true, |
|
|
|
isPlaying: false, |
|
|
|
audioUrl: '', |
|
|
|
hasAudio: false |
|
|
|
hasAudio: false, |
|
|
|
subtitlesJson: [], |
|
|
|
// 字幕同步相关 |
|
|
|
currentSentenceIndex: -1, |
|
|
|
currentSubtitleText: '', |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
computed: { |
|
|
|
content(){ |
|
|
|
return this.articleData.content |
|
|
|
}, |
|
|
|
}, |
|
|
|
onLoad(options) { |
|
|
|
if (options.id) { |
|
|
|
this.articleId = options.id |
|
|
|
@ -76,6 +90,7 @@ export default { |
|
|
|
audioManager.on('pause', this.onAudioPause) |
|
|
|
audioManager.on('ended', this.onAudioEnded) |
|
|
|
audioManager.on('error', this.onAudioError) |
|
|
|
audioManager.on('timeupdate', this.onAudioTimeUpdate) |
|
|
|
}, |
|
|
|
|
|
|
|
// 解绑音频管理器事件监听 |
|
|
|
@ -84,6 +99,7 @@ export default { |
|
|
|
audioManager.off('pause', this.onAudioPause) |
|
|
|
audioManager.off('ended', this.onAudioEnded) |
|
|
|
audioManager.off('error', this.onAudioError) |
|
|
|
audioManager.off('timeupdate', this.onAudioTimeUpdate) |
|
|
|
}, |
|
|
|
|
|
|
|
// 音频播放开始事件 |
|
|
|
@ -107,6 +123,9 @@ export default { |
|
|
|
if (data.audioType === 'article') { |
|
|
|
this.isPlaying = false |
|
|
|
console.log('文章音频播放结束') |
|
|
|
// 清空字幕状态 |
|
|
|
this.currentSentenceIndex = -1 |
|
|
|
this.currentSubtitleText = '' |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
@ -115,6 +134,9 @@ export default { |
|
|
|
if (data.audioType === 'article') { |
|
|
|
this.isPlaying = false |
|
|
|
console.error('文章音频播放错误:', data.error) |
|
|
|
// 清空字幕状态 |
|
|
|
this.currentSentenceIndex = -1 |
|
|
|
this.currentSubtitleText = '' |
|
|
|
uni.showToast({ |
|
|
|
title: '音频播放失败', |
|
|
|
icon: 'none' |
|
|
|
@ -122,11 +144,78 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 音频时间更新事件(字幕同步) |
|
|
|
onAudioTimeUpdate({ audioType, currentTime }) { |
|
|
|
if (audioType !== 'article') return |
|
|
|
|
|
|
|
if (!this.subtitlesJson || this.subtitlesJson.length === 0) return |
|
|
|
|
|
|
|
// audioManager.currentTime 为秒,字幕时间为毫秒,这里统一为毫秒比较 |
|
|
|
const curMs = Math.floor(currentTime * 1000) |
|
|
|
const sentences = this.subtitlesJson |
|
|
|
|
|
|
|
// 如果当前索引有效且还在范围内,直接使用当前句子 |
|
|
|
if (this.currentSentenceIndex >= 0) { |
|
|
|
const cur = sentences[this.currentSentenceIndex] |
|
|
|
// 容忍度 80ms,避免边界抖动 |
|
|
|
if (curMs >= cur.beginTime && curMs <= cur.endTime + 80) { |
|
|
|
this.currentSubtitleText = cur.text |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 否则扫描找到当前时间对应的句子 |
|
|
|
let idx = -1 |
|
|
|
for (let i = 0; i < sentences.length; i++) { |
|
|
|
const s = sentences[i] |
|
|
|
if (curMs >= s.beginTime && curMs <= s.endTime + 80) { |
|
|
|
idx = i |
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
this.currentSentenceIndex = idx |
|
|
|
this.currentSubtitleText = idx >= 0 ? sentences[idx].text : '' |
|
|
|
}, |
|
|
|
|
|
|
|
// 返回上一页 |
|
|
|
goBack() { |
|
|
|
uni.navigateBack() |
|
|
|
}, |
|
|
|
|
|
|
|
changeSubtitlesJson(articleData) { |
|
|
|
if (articleData.subtitlesJson) { |
|
|
|
try { |
|
|
|
//[{"EndTime":480,"BeginTime":250,"Text":"语","BeginIndex":0,"EndIndex":1,"Phoneme":"yv3"}] |
|
|
|
let json = JSON.parse(articleData.subtitlesJson) |
|
|
|
|
|
|
|
if (!Array.isArray(json) || json.length === 0) { |
|
|
|
this.subtitlesJson = [] |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// 将字级别数据转换为句子级别 |
|
|
|
const sentences = convertToSentences(json) |
|
|
|
this.subtitlesJson = sentences |
|
|
|
|
|
|
|
console.log('转换后的句子数据:', sentences) |
|
|
|
} catch (error) { |
|
|
|
console.error('解析字幕数据失败:', error) |
|
|
|
this.subtitlesJson = [] |
|
|
|
} |
|
|
|
} else { |
|
|
|
this.subtitlesJson = [] |
|
|
|
} |
|
|
|
}, |
|
|
|
chooseAudio(key = '501008') { |
|
|
|
let articleData = this.articleData?.audios?.[key] |
|
|
|
if (articleData) { |
|
|
|
// 获取第一个音频URL |
|
|
|
this.audioUrl = articleData.audioId |
|
|
|
// 处理字幕数据 |
|
|
|
this.changeSubtitlesJson(articleData) |
|
|
|
this.hasAudio = true |
|
|
|
} |
|
|
|
}, |
|
|
|
// 获取文章详情 |
|
|
|
async getArticleDetail() { |
|
|
|
try { |
|
|
|
@ -141,12 +230,7 @@ export default { |
|
|
|
// #endif |
|
|
|
|
|
|
|
// 处理音频数据 |
|
|
|
if (res.result.audios && Object.keys(res.result.audios).length > 0) { |
|
|
|
// 获取第一个音频URL |
|
|
|
const audioKeys = Object.keys(res.result.audios) |
|
|
|
this.audioUrl = res.result.audios['501008'] || res.result.audios[audioKeys[0]] |
|
|
|
this.hasAudio = true |
|
|
|
} |
|
|
|
this.chooseAudio() |
|
|
|
} else { |
|
|
|
uni.showToast({ |
|
|
|
title: res.message || '获取文章详情失败', |
|
|
|
@ -222,6 +306,9 @@ export default { |
|
|
|
audioManager.stopCurrentAudio() |
|
|
|
this.isPlaying = false |
|
|
|
console.log('停止文章音频') |
|
|
|
// 清空字幕状态 |
|
|
|
this.currentSentenceIndex = -1 |
|
|
|
this.currentSubtitleText = '' |
|
|
|
} catch (error) { |
|
|
|
console.error('停止音频失败:', error) |
|
|
|
} |
|
|
|
@ -252,6 +339,16 @@ export default { |
|
|
|
</script> |
|
|
|
|
|
|
|
<style lang="scss" scoped> |
|
|
|
.subtitle-container{ |
|
|
|
position: fixed; |
|
|
|
bottom: 220rpx; |
|
|
|
left: 5%; |
|
|
|
width: 90%; |
|
|
|
text-align: center; |
|
|
|
background-color: bisque; |
|
|
|
border-radius: 10rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.article-detail { |
|
|
|
min-height: 100vh; |
|
|
|
background: #fff; |
|
|
|
|