Browse Source

feat(音频控制): 实现音频播放时自动滚动到高亮文本功能

添加scroll-to-text事件处理逻辑,在音频播放时自动滚动到当前高亮的文本位置。包括防抖处理、元素定位计算和滚动优化,提升用户体验。同时完善音频数据有效性检查和错误处理,确保功能稳定性。
hfll
hflllll 1 week ago
parent
commit
fbbbb69471
3 changed files with 312 additions and 41 deletions
  1. +141
    -37
      subPages/home/AudioControls.vue
  2. +167
    -1
      subPages/home/book.vue
  3. +4
    -3
      subPages/home/components/CustomTabbar.vue

+ 141
- 37
subPages/home/AudioControls.vue View File

@ -401,6 +401,18 @@ export default {
checkAndLoadPreloadedAudio() {
//
if (!this.shouldLoadAudio) {
//
this.currentPageAudios = [];
this.totalTime = 0;
this.hasAudioData = false;
this.isAudioLoading = false;
//
this.$emit('audio-state-change', {
hasAudioData: false,
isLoading: false,
currentHighlightIndex: -1
});
return;
}
@ -414,10 +426,21 @@ export default {
this.totalTime = cachedAudio.totalDuration || 0;
this.hasAudioData = true;
this.isAudioLoading = false;
this.currentAudioIndex = 0;
this.currentTime = 0;
this.currentHighlightIndex = -1;
console.log(`🎵 checkAndLoadPreloadedAudio: 从缓存加载音频,页面=${this.currentPage}, 音频数量=${this.currentPageAudios.length}`);
//
this.$emit('audio-state-change', {
hasAudioData: true,
isLoading: false,
currentHighlightIndex: -1
});
} else {
//
console.log(`🎵 checkAndLoadPreloadedAudio: 无缓存,开始加载音频,页面=${this.currentPage}`);
this.getCurrentPageAudio();
}
},
@ -955,37 +978,17 @@ export default {
this.currentHighlightIndex = -1;
this.playSpeed = 1.0;
//
if (!this.shouldLoadAudio) {
this.currentPageAudios = [];
this.totalTime = 0;
this.hasAudioData = false;
} else {
//
const pageKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const cachedAudio = this.audioCache[pageKey];
if (cachedAudio && cachedAudio.audios && cachedAudio.audios.length > 0) {
//
this.currentPageAudios = cachedAudio.audios;
this.totalTime = cachedAudio.totalDuration || 0;
this.hasAudioData = true;
} else {
// checkAndLoadPreloadedAudio
this.currentPageAudios = [];
this.totalTime = 0;
this.hasAudioData = false;
}
}
//
// checkAndLoadPreloadedAudio
this.currentPageAudios = [];
this.totalTime = 0;
this.hasAudioData = false;
//
this.$emit('audio-state-change', {
hasAudioData: this.hasAudioData,
isLoading: this.isAudioLoading,
currentHighlightIndex: this.currentHighlightIndex
hasAudioData: false,
isLoading: false,
currentHighlightIndex: -1
});
},
@ -1206,13 +1209,37 @@ export default {
return;
}
if (this.currentPageAudios.length === 0) return;
//
if (!this.currentPageAudios || this.currentPageAudios.length === 0) {
console.warn('🎵 playAudio: 没有音频数据');
return;
}
if (this.currentAudioIndex < 0 || this.currentAudioIndex >= this.currentPageAudios.length) {
console.error('🎵 playAudio: 音频索引无效', this.currentAudioIndex);
return;
}
const currentAudioData = this.currentPageAudios[this.currentAudioIndex];
if (!currentAudioData || !currentAudioData.url) {
console.error('🎵 playAudio: 音频数据无效', currentAudioData);
return;
}
//
const audioCacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const currentPageCache = this.audioCache[audioCacheKey];
if (!currentPageCache || !currentPageCache.audios.includes(currentAudioData)) {
console.error('🎵 playAudio: 音频数据与当前页面不匹配,停止播放');
return;
}
// 🎯
this.stopWordAudio();
//
if (!this.currentAudio || this.currentAudio.src !== this.currentPageAudios[this.currentAudioIndex].url) {
if (!this.currentAudio || this.currentAudio.src !== currentAudioData.url) {
this.createAudioInstance();
//
setTimeout(() => {
@ -1632,6 +1659,32 @@ export default {
//
createAudioInstance() {
//
if (!this.currentPageAudios || this.currentPageAudios.length === 0) {
console.error('🎵 createAudioInstance: 没有音频数据');
return;
}
if (this.currentAudioIndex < 0 || this.currentAudioIndex >= this.currentPageAudios.length) {
console.error('🎵 createAudioInstance: 音频索引无效', this.currentAudioIndex);
return;
}
const currentAudioData = this.currentPageAudios[this.currentAudioIndex];
if (!currentAudioData || !currentAudioData.url) {
console.error('🎵 createAudioInstance: 音频数据无效', currentAudioData);
return;
}
//
const audioCacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const currentPageCache = this.audioCache[audioCacheKey];
if (!currentPageCache || !currentPageCache.audios.includes(currentAudioData)) {
console.error('🎵 createAudioInstance: 音频数据与当前页面不匹配');
return;
}
//
if (this.currentAudio) {
this.currentAudio.pause();
@ -1657,13 +1710,12 @@ export default {
// audio = uni.createInnerAudioContext();
// }
// }
const audioUrl = this.currentPageAudios[this.currentAudioIndex].url;
const audioUrl = currentAudioData.url;
audio.src = audioUrl;
console.log('🎵 创建音频实例 - 音频文本:', this.currentPageAudios[this.currentAudioIndex].text?.substring(0, 50) + '...');
console.log('🎵 创建音频实例 - 页面:', this.currentPage, '音频索引:', this.currentAudioIndex);
console.log('🎵 创建音频实例 - 音频URL:', audioUrl);
console.log('🎵 创建音频实例 - 音频文本:', currentAudioData.text?.substring(0, 50) + '...');
// playbackRate
audio.onCanplay(() => {
@ -1763,6 +1815,11 @@ export default {
return;
}
//
if (this.isPageChanging) {
return;
}
//
const currentAudio = this.currentPageAudios[this.currentAudioIndex];
if (!currentAudio) {
@ -1771,6 +1828,16 @@ export default {
return;
}
//
const audioCacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const currentPageCache = this.audioCache[audioCacheKey];
//
if (!currentPageCache || !currentPageCache.audios.includes(currentAudio)) {
console.warn('🎵 updateHighlightIndex: 音频数据与当前页面不匹配,跳过高亮更新');
return;
}
//
if (currentAudio.isSegmented && typeof currentAudio.originalTextIndex !== 'undefined') {
// 使
@ -1784,6 +1851,9 @@ export default {
// 使
this.emitHighlightChange(this.currentHighlightIndex);
//
this.emitScrollToText(this.currentHighlightIndex);
},
//
@ -1814,6 +1884,40 @@ export default {
this.$emit('highlight-change', highlightData);
},
//
emitScrollToText(highlightIndex = -1) {
if (highlightIndex === -1) {
return;
}
//
const audioData = this.currentPageAudios[this.currentAudioIndex];
if (!audioData) {
return;
}
//
const audioCacheKey = `${this.courseId}_${this.currentPage}_${this.localVoiceId}`;
const currentPageCache = this.audioCache[audioCacheKey];
//
if (!currentPageCache || !currentPageCache.audios.includes(audioData)) {
console.warn('🎵 emitScrollToText: 音频数据与当前页面不匹配,跳过滚动事件');
return;
}
const scrollData = {
highlightIndex: audioData.originalTextIndex !== undefined ? audioData.originalTextIndex : highlightIndex,
isSegmented: audioData.isSegmented || false,
segmentIndex: audioData.segmentIndex || 0,
currentText: audioData.text || '',
currentPage: this.currentPage
};
//
this.$emit('scroll-to-text', scrollData);
},
//
onAudioEnded() {
if (this.currentAudioIndex < this.currentPageAudios.length - 1) {


+ 167
- 1
subPages/home/book.vue View File

@ -72,6 +72,7 @@
@click.stop="segment.isKeyword ? handleChineseKeywordClick(segment.keywordData) : null"
user-select
:style="item.style"
:id="`text-segment-${segmentIndex}`"
>{{ segment.text }}</text>
</view>
</view>
@ -81,7 +82,7 @@
<view v-for="(item, itemIndex) in page" :key="itemIndex">
<!-- 文本页面 -->
<view v-if="item && item.type === 'text' && item.content" class="text-content" >
<view :class="{ 'lead-text': isTextHighlighted(page, itemIndex) }" @click.stop="handleTextClick(item.content, item, index)" :ref="`textRef_${index}_${itemIndex}`">
<view :class="{ 'lead-text': isTextHighlighted(page, itemIndex) }" @click.stop="handleTextClick(item.content, item, index)" :ref="`textRef_${index}_${itemIndex}`" :id="`text-${itemIndex}`">
<text
v-if="!item.isLead"
class="content-text clickable-text"
@ -145,6 +146,7 @@
@next-page="nextPage"
@audio-state-change="onAudioStateChange"
@highlight-change="onHighlightChange"
@scroll-to-text="onScrollToText"
@voice-change-complete="onVoiceChangeComplete"
@voice-change-error="onVoiceChangeError"
@page-data-needed="onPageDataNeeded"
@ -216,6 +218,8 @@ export default {
//
scrollTops: [], // scroll-view
scrollDebounceTimer: null, //
isScrolling: false, //
courseIdList: [],
bookTitle: '',
courseList: [
@ -671,6 +675,162 @@ export default {
}
},
//
onScrollToText(scrollData) {
//
if (this.scrollDebounceTimer) {
clearTimeout(this.scrollDebounceTimer);
}
this.scrollDebounceTimer = setTimeout(() => {
this.performScrollToText(scrollData);
}, 100); // 100ms
},
//
performScrollToText(scrollData) {
if (!scrollData || typeof scrollData.highlightIndex !== 'number' || scrollData.highlightIndex < 0) {
console.warn('滚动数据无效:', scrollData);
return;
}
//
if (scrollData.currentPage && scrollData.currentPage !== this.currentPage) {
console.warn('页面不匹配,跳过滚动:', {
scrollDataPage: scrollData.currentPage,
currentPage: this.currentPage
});
return;
}
//
if (this.isScrolling) {
console.warn('正在滚动中,跳过本次滚动');
return;
}
//
let selector = '';
if (scrollData.isSegmented && typeof scrollData.segmentIndex === 'number') {
// 使
selector = `#text-segment-${scrollData.segmentIndex}`;
} else {
//
// originalTextIndexDOM
const targetItemIndex = this.findTextItemIndex(scrollData.highlightIndex);
if (targetItemIndex !== -1) {
selector = `#text-${targetItemIndex}`;
} else {
console.warn('无法找到对应的文本元素索引:', scrollData.highlightIndex);
selector = `#text-${scrollData.highlightIndex}`; //
}
}
console.log('开始滚动到文本:', { selector, scrollData });
//
this.isScrolling = true;
// DOM
this.$nextTick(() => {
// 使uni.createSelectorQuery
const query = uni.createSelectorQuery().in(this);
// scroll-view
query.select('.scroll-container').boundingClientRect();
//
query.select(selector).boundingClientRect();
query.exec((res) => {
const scrollViewRect = res[0];
const targetRect = res[1];
console.log('查询结果:', {
scrollViewRect: scrollViewRect ? '找到' : '未找到',
targetRect: targetRect ? '找到' : '未找到',
selector
});
if (scrollViewRect && targetRect) {
// scroll-view
const currentScrollTop = this.scrollTops[this.currentPage - 1] || 0;
const targetOffsetTop = targetRect.top - scrollViewRect.top + currentScrollTop;
// 1/4
const screenHeight = uni.getSystemInfoSync().windowHeight;
const targetScrollTop = targetOffsetTop - screenHeight / 4;
// scroll-view
const finalScrollTop = Math.max(0, targetScrollTop);
//
const currentScroll = this.scrollTops[this.currentPage - 1] || 0;
const scrollDifference = Math.abs(finalScrollTop - currentScroll);
if (scrollDifference > 30) { //
this.$set(this.scrollTops, this.currentPage - 1, finalScrollTop);
//
setTimeout(() => {
this.isScrolling = false;
}, 300); //
console.log('✅ 滚动到高亮文本:', {
selector,
targetOffsetTop,
finalScrollTop,
currentPage: this.currentPage,
scrollDifference
});
} else {
//
this.isScrolling = false;
console.log('📍 目标已在视野内,无需滚动');
}
} else {
console.error('❌ 未找到目标元素或scroll-view:', {
selector,
scrollViewFound: !!scrollViewRect,
targetFound: !!targetRect,
currentPage: this.currentPage,
highlightIndex: scrollData.highlightIndex
});
this.isScrolling = false;
//
if (!targetRect) {
console.log('🔄 尝试备用滚动方案');
const fallbackScrollTop = scrollData.highlightIndex * 100; //
this.$set(this.scrollTops, this.currentPage - 1, fallbackScrollTop);
setTimeout(() => {
this.isScrolling = false;
}, 300);
}
}
});
});
},
//
findTextItemIndex(originalTextIndex) {
const currentPageData = this.bookPages[this.currentPage - 1];
if (!currentPageData || !Array.isArray(currentPageData)) {
return -1;
}
let textCount = 0;
for (let i = 0; i < currentPageData.length; i++) {
const item = currentPageData[i];
if (item && item.type === 'text' && item.content) {
if (textCount === originalTextIndex) {
return i; //
}
textCount++;
}
}
return -1; //
},
// id
async getVoiceList() {
@ -1600,6 +1760,12 @@ export default {
uni.$off('selectVoice')
// 0.
if (this.scrollDebounceTimer) {
clearTimeout(this.scrollDebounceTimer);
this.scrollDebounceTimer = null;
}
// 1.
if (this.currentWordAudio) {


+ 4
- 3
subPages/home/components/CustomTabbar.vue View File

@ -16,6 +16,7 @@
@next-page="nextPage"
@audio-state-change="onAudioStateChange"
@highlight-change="onHighlightChange"
@scroll-to-text="onScrollToText"
@voice-change-complete="onVoiceChangeComplete"
@voice-change-error="onVoiceChangeError"
@page-data-needed="onPageDataNeeded"
@ -141,9 +142,9 @@ export default {
onPageDataNeeded(pageNumber) {
this.$emit('page-data-needed', pageNumber)
},
// onScrollToText(refName) {
// this.$emit('scroll-to-text', refName)
// }
onScrollToText(scrollData) {
this.$emit('scroll-to-text', scrollData)
}
}
}
</script>


Loading…
Cancel
Save