diff --git a/subPages/home/AudioControls.vue b/subPages/home/AudioControls.vue index 1dd242d..7af9b55 100644 --- a/subPages/home/AudioControls.vue +++ b/subPages/home/AudioControls.vue @@ -321,7 +321,18 @@ export default { console.log(`🎵 自动播放缓存音频: ${firstAudioData.url}`); audioManager.playAudio(firstAudioData.url, 'sentence', { playbackRate: this.playSpeed }); this.isPlaying = true; - this.updateHighlightIndex(); + + // 页面切换时需要立即更新高亮和滚动,不受防抖机制影响 + const highlightIndex = firstAudioData.originalTextIndex !== undefined ? firstAudioData.originalTextIndex : 0; + this.currentHighlightIndex = highlightIndex; + + // 立即发送高亮变化事件 + this.emitHighlightChange(highlightIndex, firstAudioData); + + // 立即发送滚动事件,传入音频数据 + this.emitScrollToText(highlightIndex, firstAudioData); + + console.log(`🎵 页面切换自动播放: 高亮索引=${highlightIndex}, 页面=${this.currentPage}`); } }); } else { @@ -1560,27 +1571,27 @@ export default { }, // 发送高亮变化事件的辅助方法 - emitHighlightChange(highlightIndex = -1) { + emitHighlightChange(highlightIndex = -1, audioData = null) { if (highlightIndex === -1) { // 清除高亮 this.$emit('highlight-change', -1); return; } - // 获取当前播放的音频数据(使用currentAudioIndex而不是highlightIndex) - const audioData = this.currentPageAudios[this.currentAudioIndex]; - if (!audioData) { + // 获取当前播放的音频数据,优先使用传入的audioData + const currentAudioData = audioData || this.currentPageAudios[this.currentAudioIndex]; + if (!currentAudioData) { this.$emit('highlight-change', -1); return; } const highlightData = { - highlightIndex: audioData.originalTextIndex !== undefined ? audioData.originalTextIndex : highlightIndex, - isSegmented: audioData.isSegmented || false, - segmentIndex: audioData.segmentIndex || 0, - startIndex: audioData.startIndex || 0, - endIndex: audioData.endIndex || 0, - currentText: audioData.text || '' + highlightIndex: currentAudioData.originalTextIndex !== undefined ? currentAudioData.originalTextIndex : highlightIndex, + isSegmented: currentAudioData.isSegmented || false, + segmentIndex: currentAudioData.segmentIndex || 0, + startIndex: currentAudioData.startIndex || 0, + endIndex: currentAudioData.endIndex || 0, + currentText: currentAudioData.text || '' }; // 发送详细的高亮信息 @@ -1588,14 +1599,14 @@ export default { }, // 发送滚动到文本事件的辅助方法 - emitScrollToText(highlightIndex = -1) { + emitScrollToText(highlightIndex = -1, audioData = null) { if (highlightIndex === -1) { return; } - // 获取当前播放的音频数据 - const audioData = this.currentPageAudios[this.currentAudioIndex]; - if (!audioData) { + // 获取当前播放的音频数据,优先使用传入的audioData + const currentAudioData = audioData || this.currentPageAudios[this.currentAudioIndex]; + if (!currentAudioData) { return; } @@ -1604,16 +1615,16 @@ export default { const currentPageCache = this.audioCache[audioCacheKey]; // 如果当前音频数据不属于当前页面,则不发送滚动事件 - if (!currentPageCache || !currentPageCache.audios.includes(audioData)) { + if (!currentPageCache || !currentPageCache.audios.includes(currentAudioData)) { console.warn('🎵 emitScrollToText: 音频数据与当前页面不匹配,跳过滚动事件'); return; } const scrollData = { - highlightIndex: audioData.originalTextIndex !== undefined ? audioData.originalTextIndex : highlightIndex, - isSegmented: audioData.isSegmented || false, - segmentIndex: audioData.segmentIndex || 0, - currentText: audioData.text || '', + highlightIndex: currentAudioData.originalTextIndex !== undefined ? currentAudioData.originalTextIndex : highlightIndex, + isSegmented: currentAudioData.isSegmented || false, + segmentIndex: currentAudioData.segmentIndex || 0, + currentText: currentAudioData.text || '', currentPage: this.currentPage }; diff --git a/subPages/home/book.vue b/subPages/home/book.vue index f881e29..c84626a 100644 --- a/subPages/home/book.vue +++ b/subPages/home/book.vue @@ -191,6 +191,7 @@ export default { touchStartTime: 0, // 触摸开始时间 touchStartY: 0, // 触摸开始Y坐标 userScrollTimer: null, // 用户滚动检测定时器 + lastUserScrollTime: 0, // 最后一次用户滚动时间 courseIdList: [], bookTitle: '', courseList: [ @@ -322,6 +323,9 @@ export default { // 如果移动距离超过阈值,认为是滚动操作 if (deltaY > 10) { + // 记录用户滚动时间 + this.lastUserScrollTime = Date.now(); + // 如果当前正在自动滚动,立即停止 if (this.isScrolling) { console.log('🛑 检测到用户手动滚动,停止自动滚动'); @@ -345,14 +349,17 @@ export default { this.userScrollTimer = setTimeout(() => { console.log('✋ 用户滚动操作结束,允许自动滚动'); this.userScrollTimer = null; - }, 1000); // 1秒后允许自动滚动 + }, 500); // 减少到500ms,提高响应性 console.log('👆 用户停止触摸屏幕'); }, // 检查是否应该阻止自动滚动 shouldPreventAutoScroll() { - return this.isUserTouching || this.userScrollTimer !== null; + // 降低敏感度:只有在用户正在触摸且最近有滚动行为时才阻止 + const now = Date.now(); + const recentUserScroll = this.userScrollTimer !== null && (now - this.lastUserScrollTime) < 1000; + return this.isUserTouching && recentUserScroll; }, // 处理scroll-view滚动事件 @@ -368,9 +375,10 @@ export default { if (this.isScrolling) { // 如果滚动差异很大,可能是用户手动滚动,中断自动滚动状态 const scrollDifference = Math.abs(previousScrollTop - scrollTop); - if (scrollDifference > 100) { // 大幅度滚动,很可能是手动操作 + if (scrollDifference > 50) { // 调整阈值,避免误判 console.log('🖐️ 检测到手动滚动,中断自动滚动状态'); this.isScrolling = false; + this.lastUserScrollTime = Date.now(); // 记录手动滚动时间 } } @@ -728,6 +736,8 @@ export default { // 处理滚动到高亮文本 onScrollToText(scrollData) { + console.log('📍 收到滚动请求:', scrollData); + // 检查是否应该阻止自动滚动(用户正在手动操作) if (this.shouldPreventAutoScroll()) { console.log('🚫 用户正在手动滚动,跳过自动滚动到文本'); @@ -746,7 +756,7 @@ export default { return; } this.performScrollToText(scrollData); - }, 100); // 100ms防抖延迟 + }, 50); // 减少防抖延迟,提高响应性 }, // 执行滚动到高亮文本的具体逻辑 @@ -817,7 +827,52 @@ export default { this.isScrolling = true; // 等待DOM更新后再查找元素 - this.$nextTick(() => { + this.$nextTick(async () => { + try { + // 获取所有元素的真实位置信息 + const elementPositions = await this.getAllElementPositions(); + console.log('📏 获取到的元素位置信息:', elementPositions); + + // 计算精确的滚动位置 + const preciseScrollTop = await this.calculatePreciseScrollPosition(scrollData, elementPositions); + + if (preciseScrollTop !== null) { + // 检查是否需要滚动(避免不必要的滚动) + const currentScroll = this.scrollTops[this.currentPage - 1] || 0; + const scrollDifference = Math.abs(preciseScrollTop - currentScroll); + + if (scrollDifference > 20) { + this.$set(this.scrollTops, this.currentPage - 1, preciseScrollTop); + console.log('✅ 使用精确位置滚动:', { + selector, + preciseScrollTop, + currentPage: this.currentPage, + scrollDifference + }); + + // 滚动完成后重置状态 + setTimeout(() => { + resetScrollingState(); + }, 200); + } else { + resetScrollingState(); + console.log('📍 目标已在视野内,无需滚动'); + } + } else { + // 精确计算失败,使用原有的查询方法作为备用 + console.log('🔄 精确计算失败,使用备用查询方法'); + this.fallbackScrollToText(selector, resetScrollingState, safetyTimeout); + } + } catch (error) { + console.error('❌ 精确滚动计算失败:', error); + // 使用原有的查询方法作为备用 + this.fallbackScrollToText(selector, resetScrollingState, safetyTimeout); + } + }); + }, + + // 备用滚动方法(原有的查询方式) + fallbackScrollToText(selector, resetScrollingState, safetyTimeout) { // 使用uni.createSelectorQuery获取元素位置 const query = uni.createSelectorQuery().in(this); @@ -855,13 +910,13 @@ export default { const currentScroll = this.scrollTops[this.currentPage - 1] || 0; const scrollDifference = Math.abs(finalScrollTop - currentScroll); - if (scrollDifference > 30) { // 降低滚动阈值,提高响应性 + if (scrollDifference > 20) { // 进一步降低滚动阈值,提高响应性 this.$set(this.scrollTops, this.currentPage - 1, finalScrollTop); // 滚动完成后重置状态 setTimeout(() => { resetScrollingState(); - }, 300); // 稍微减少等待时间 + }, 200); // 减少等待时间 console.log('✅ 滚动到高亮文本:', { selector, @@ -887,18 +942,18 @@ export default { // 尝试备用方案:直接滚动到页面顶部附近 if (!targetRect) { console.log('🔄 尝试备用滚动方案'); - const fallbackScrollTop = scrollData.highlightIndex * 100; // 简单估算位置 - this.$set(this.scrollTops, this.currentPage - 1, fallbackScrollTop); + // 改进备用方案:基于highlightIndex计算更准确的位置 + const estimatedPosition = this.calculateEstimatedScrollPosition(scrollData.highlightIndex); + this.$set(this.scrollTops, this.currentPage - 1, estimatedPosition); setTimeout(() => { resetScrollingState(); - }, 300); + }, 200); } else { // 立即重置状态 resetScrollingState(); } } }); - }); }, // 查找文本元素在页面中的实际索引 @@ -922,6 +977,141 @@ export default { return -1; // 未找到 }, + // 获取所有页面元素的位置信息 + async getAllElementPositions() { + return new Promise((resolve) => { + const currentPageData = this.bookPages[this.currentPage - 1]; + if (!currentPageData || !Array.isArray(currentPageData)) { + resolve([]); + return; + } + + const query = uni.createSelectorQuery().in(this); + const elementPositions = []; + + // 获取scroll-container的位置作为基准 + query.select('.scroll-container').boundingClientRect(); + + // 为每个元素添加查询 + currentPageData.forEach((item, index) => { + if (item && (item.type === 'text' || item.type === 'image' || item.type === 'video')) { + if (item.type === 'text') { + query.select(`#text-${index}`).boundingClientRect(); + } else if (item.type === 'image') { + query.select(`.image-container`).boundingClientRect(); + } else if (item.type === 'video') { + query.select(`.video-content`).boundingClientRect(); + } + } + }); + + query.exec((res) => { + const containerRect = res[0]; + if (!containerRect) { + resolve([]); + return; + } + + // 处理查询结果 + let resultIndex = 1; // 跳过第一个容器结果 + currentPageData.forEach((item, index) => { + if (item && (item.type === 'text' || item.type === 'image' || item.type === 'video')) { + const elementRect = res[resultIndex]; + if (elementRect) { + elementPositions.push({ + index: index, + type: item.type, + top: elementRect.top - containerRect.top, + height: elementRect.height, + bottom: elementRect.top - containerRect.top + elementRect.height + }); + } + resultIndex++; + } + }); + + resolve(elementPositions); + }); + }); + }, + + // 计算精确的滚动位置 + async calculatePreciseScrollPosition(scrollData, elementPositions) { + if (!elementPositions || elementPositions.length === 0) { + return null; + } + + let targetElementIndex = -1; + + if (scrollData.segmentIndex !== undefined) { + // 分段音频情况 + targetElementIndex = scrollData.segmentIndex; + } else if (scrollData.highlightIndex !== undefined) { + // 普通音频情况,需要找到对应的文本元素 + targetElementIndex = this.findTextItemIndex(scrollData.highlightIndex); + } + + if (targetElementIndex === -1) { + return null; + } + + // 查找目标元素的位置信息 + const targetElement = elementPositions.find(pos => pos.index === targetElementIndex && pos.type === 'text'); + + if (!targetElement) { + console.warn('未找到目标元素位置信息:', targetElementIndex); + return null; + } + + // 计算滚动位置:让目标元素显示在屏幕上方1/4处 + const screenHeight = uni.getSystemInfoSync().windowHeight; + const offsetFromTop = screenHeight * 0.25; + + // 目标滚动位置 = 目标元素顶部位置 - 偏移量 + const targetScrollTop = Math.max(0, targetElement.top - offsetFromTop); + + console.log('🎯 精确滚动位置计算:', { + targetElementIndex, + targetElement, + screenHeight, + offsetFromTop, + targetScrollTop + }); + + return targetScrollTop; + }, + + // 计算估算的滚动位置(备用方案) + calculateEstimatedScrollPosition(highlightIndex) { + const currentPageData = this.bookPages[this.currentPage - 1]; + if (!currentPageData || !Array.isArray(currentPageData)) { + return highlightIndex * 80; // 基础估算 + } + + // 基于页面内容计算更准确的位置 + let estimatedHeight = 0; + let textCount = 0; + + for (let i = 0; i < currentPageData.length && textCount <= highlightIndex; i++) { + const item = currentPageData[i]; + if (item && item.type === 'text' && item.content) { + if (textCount === highlightIndex) { + break; + } + // 根据内容长度估算高度 + const contentLength = item.content.length; + estimatedHeight += Math.max(60, contentLength * 1.2); // 基础高度 + 内容长度因子 + textCount++; + } else if (item && item.type === 'image') { + estimatedHeight += 200; // 图片估算高度 + } else if (item && item.type === 'video') { + estimatedHeight += 300; // 视频估算高度 + } + } + + return Math.max(0, estimatedHeight - 100); // 留一些上边距 + }, + // 获取音色列表 拿第一个做默认的音色id async getVoiceList() { const voiceRes = await this.$api.music.list()