|
|
@ -35,6 +35,9 @@ |
|
|
style="height: 100vh;" |
|
|
style="height: 100vh;" |
|
|
class="scroll-container" |
|
|
class="scroll-container" |
|
|
@scroll="onScroll" |
|
|
@scroll="onScroll" |
|
|
|
|
|
@touchstart="onTouchStart" |
|
|
|
|
|
@touchmove="onTouchMove" |
|
|
|
|
|
@touchend="onTouchEnd" |
|
|
> |
|
|
> |
|
|
<view class="content-area" @click="toggleNavbar"> |
|
|
<view class="content-area" @click="toggleNavbar"> |
|
|
<view class="title">{{ currentPageTitle }}</view> |
|
|
<view class="title">{{ currentPageTitle }}</view> |
|
|
@ -54,7 +57,7 @@ |
|
|
</view> |
|
|
</view> |
|
|
<view v-for="(item, itemIndex) in page" :key="itemIndex"> |
|
|
<view v-for="(item, itemIndex) in page" :key="itemIndex"> |
|
|
<image class="card-image" v-if="item && item.type === 'image'" :src="item.imageUrl" mode="aspectFill" ></image> |
|
|
<image class="card-image" v-if="item && item.type === 'image'" :src="item.imageUrl" mode="aspectFill" ></image> |
|
|
<view class="english-text-container clickable-text" v-else-if="item && item.type === 'text' && item.language === 'en' && item.content" @click.stop="handleTextClick(item.content, item, index)" > |
|
|
|
|
|
|
|
|
<view :class="['english-text-container', 'clickable-text', { 'lead-text': isCardTextHighlighted(page, itemIndex) }]" v-else-if="item && item.type === 'text' && item.language === 'en' && item.content" @click.stop="handleTextClick(item.content, item, index)" > |
|
|
<text |
|
|
<text |
|
|
v-for="(token, tokenIndex) in splitEnglishSentence(item.content)" |
|
|
v-for="(token, tokenIndex) in splitEnglishSentence(item.content)" |
|
|
:key="tokenIndex" |
|
|
:key="tokenIndex" |
|
|
@ -64,7 +67,7 @@ |
|
|
:style="item.style" |
|
|
:style="item.style" |
|
|
>{{ token.text }}</text> |
|
|
>{{ token.text }}</text> |
|
|
</view> |
|
|
</view> |
|
|
<view v-else-if="item && item.type === 'text' && item.language === 'zh' && item.content" @click.stop="handleTextClick(item.content, item, index)"> |
|
|
|
|
|
|
|
|
<view :class="{ 'lead-text': isCardTextHighlighted(page, itemIndex) }" v-else-if="item && item.type === 'text' && item.language === 'zh' && item.content" @click.stop="handleTextClick(item.content, item, index)"> |
|
|
<text |
|
|
<text |
|
|
v-for="(segment, segmentIndex) in processChineseText(item.content)" |
|
|
v-for="(segment, segmentIndex) in processChineseText(item.content)" |
|
|
:key="segmentIndex" |
|
|
:key="segmentIndex" |
|
|
@ -220,6 +223,12 @@ export default { |
|
|
scrollTops: [], // 每个页面的scroll-view滚动位置数组 |
|
|
scrollTops: [], // 每个页面的scroll-view滚动位置数组 |
|
|
scrollDebounceTimer: null, // 滚动防抖定时器 |
|
|
scrollDebounceTimer: null, // 滚动防抖定时器 |
|
|
isScrolling: false, // 是否正在滚动中 |
|
|
isScrolling: false, // 是否正在滚动中 |
|
|
|
|
|
|
|
|
|
|
|
// 手动滚动检测相关 |
|
|
|
|
|
isUserTouching: false, // 用户是否正在触摸屏幕 |
|
|
|
|
|
touchStartTime: 0, // 触摸开始时间 |
|
|
|
|
|
touchStartY: 0, // 触摸开始Y坐标 |
|
|
|
|
|
userScrollTimer: null, // 用户滚动检测定时器 |
|
|
courseIdList: [], |
|
|
courseIdList: [], |
|
|
bookTitle: '', |
|
|
bookTitle: '', |
|
|
courseList: [ |
|
|
courseList: [ |
|
|
@ -305,14 +314,82 @@ export default { |
|
|
// } |
|
|
// } |
|
|
// }, |
|
|
// }, |
|
|
methods: { |
|
|
methods: { |
|
|
|
|
|
// 触摸开始事件 - 检测用户开始触摸 |
|
|
|
|
|
onTouchStart(e) { |
|
|
|
|
|
this.isUserTouching = true; |
|
|
|
|
|
this.touchStartTime = Date.now(); |
|
|
|
|
|
this.touchStartY = e.touches[0].pageY; |
|
|
|
|
|
|
|
|
|
|
|
// 清除之前的用户滚动定时器 |
|
|
|
|
|
if (this.userScrollTimer) { |
|
|
|
|
|
clearTimeout(this.userScrollTimer); |
|
|
|
|
|
this.userScrollTimer = null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
console.log('👆 用户开始触摸屏幕'); |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// 触摸移动事件 - 检测用户滚动操作 |
|
|
|
|
|
onTouchMove(e) { |
|
|
|
|
|
if (!this.isUserTouching) return; |
|
|
|
|
|
|
|
|
|
|
|
const currentY = e.touches[0].pageY; |
|
|
|
|
|
const deltaY = Math.abs(currentY - this.touchStartY); |
|
|
|
|
|
|
|
|
|
|
|
// 如果移动距离超过阈值,认为是滚动操作 |
|
|
|
|
|
if (deltaY > 10) { |
|
|
|
|
|
// 如果当前正在自动滚动,立即停止 |
|
|
|
|
|
if (this.isScrolling) { |
|
|
|
|
|
console.log('🛑 检测到用户手动滚动,停止自动滚动'); |
|
|
|
|
|
this.isScrolling = false; |
|
|
|
|
|
|
|
|
|
|
|
// 清除滚动防抖定时器 |
|
|
|
|
|
if (this.scrollDebounceTimer) { |
|
|
|
|
|
clearTimeout(this.scrollDebounceTimer); |
|
|
|
|
|
this.scrollDebounceTimer = null; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// 触摸结束事件 - 用户停止触摸 |
|
|
|
|
|
onTouchEnd(e) { |
|
|
|
|
|
this.isUserTouching = false; |
|
|
|
|
|
|
|
|
|
|
|
// 设置一个短暂的延迟,在用户停止触摸后的一段时间内仍然阻止自动滚动 |
|
|
|
|
|
// 这样可以避免用户刚停止滚动就立即触发自动滚动 |
|
|
|
|
|
this.userScrollTimer = setTimeout(() => { |
|
|
|
|
|
console.log('✋ 用户滚动操作结束,允许自动滚动'); |
|
|
|
|
|
this.userScrollTimer = null; |
|
|
|
|
|
}, 1000); // 1秒后允许自动滚动 |
|
|
|
|
|
|
|
|
|
|
|
console.log('👆 用户停止触摸屏幕'); |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// 检查是否应该阻止自动滚动 |
|
|
|
|
|
shouldPreventAutoScroll() { |
|
|
|
|
|
return this.isUserTouching || this.userScrollTimer !== null; |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
// 处理scroll-view滚动事件 |
|
|
// 处理scroll-view滚动事件 |
|
|
onScroll(e) { |
|
|
onScroll(e) { |
|
|
// 更新当前页面的滚动位置 |
|
|
// 更新当前页面的滚动位置 |
|
|
const scrollTop = e.detail.scrollTop; |
|
|
const scrollTop = e.detail.scrollTop; |
|
|
const currentPageIndex = this.currentPage - 1; |
|
|
const currentPageIndex = this.currentPage - 1; |
|
|
|
|
|
const previousScrollTop = this.scrollTops[currentPageIndex] || 0; |
|
|
|
|
|
|
|
|
// 只有当滚动位置发生显著变化时才更新 |
|
|
// 只有当滚动位置发生显著变化时才更新 |
|
|
if (Math.abs((this.scrollTops[currentPageIndex] || 0) - scrollTop) > 5) { |
|
|
|
|
|
|
|
|
if (Math.abs(previousScrollTop - scrollTop) > 5) { |
|
|
|
|
|
// 检测是否为手动滚动(如果正在自动滚动中,但滚动位置与预期不符,则认为是手动滚动) |
|
|
|
|
|
if (this.isScrolling) { |
|
|
|
|
|
// 如果滚动差异很大,可能是用户手动滚动,中断自动滚动状态 |
|
|
|
|
|
const scrollDifference = Math.abs(previousScrollTop - scrollTop); |
|
|
|
|
|
if (scrollDifference > 100) { // 大幅度滚动,很可能是手动操作 |
|
|
|
|
|
console.log('🖐️ 检测到手动滚动,中断自动滚动状态'); |
|
|
|
|
|
this.isScrolling = false; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
this.$set(this.scrollTops, currentPageIndex, scrollTop); |
|
|
this.$set(this.scrollTops, currentPageIndex, scrollTop); |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
@ -677,20 +754,52 @@ export default { |
|
|
|
|
|
|
|
|
// 处理滚动到高亮文本 |
|
|
// 处理滚动到高亮文本 |
|
|
onScrollToText(scrollData) { |
|
|
onScrollToText(scrollData) { |
|
|
|
|
|
// 检查是否应该阻止自动滚动(用户正在手动操作) |
|
|
|
|
|
if (this.shouldPreventAutoScroll()) { |
|
|
|
|
|
console.log('🚫 用户正在手动滚动,跳过自动滚动到文本'); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 防抖处理:如果正在滚动中,清除之前的定时器 |
|
|
// 防抖处理:如果正在滚动中,清除之前的定时器 |
|
|
if (this.scrollDebounceTimer) { |
|
|
if (this.scrollDebounceTimer) { |
|
|
clearTimeout(this.scrollDebounceTimer); |
|
|
clearTimeout(this.scrollDebounceTimer); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this.scrollDebounceTimer = setTimeout(() => { |
|
|
this.scrollDebounceTimer = setTimeout(() => { |
|
|
|
|
|
// 再次检查是否应该阻止自动滚动 |
|
|
|
|
|
if (this.shouldPreventAutoScroll()) { |
|
|
|
|
|
console.log('🚫 防抖延迟后检测到用户手动滚动,跳过自动滚动到文本'); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
this.performScrollToText(scrollData); |
|
|
this.performScrollToText(scrollData); |
|
|
}, 100); // 100ms防抖延迟 |
|
|
}, 100); // 100ms防抖延迟 |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
// 执行滚动到高亮文本的具体逻辑 |
|
|
// 执行滚动到高亮文本的具体逻辑 |
|
|
performScrollToText(scrollData) { |
|
|
performScrollToText(scrollData) { |
|
|
|
|
|
// 最终检查:如果用户正在手动操作,直接返回 |
|
|
|
|
|
if (this.shouldPreventAutoScroll()) { |
|
|
|
|
|
console.log('🚫 执行滚动前检测到用户手动操作,取消自动滚动'); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 确保在任何情况下都能重置滚动状态 |
|
|
|
|
|
const resetScrollingState = () => { |
|
|
|
|
|
this.isScrolling = false; |
|
|
|
|
|
console.log('🔄 滚动状态已重置'); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 设置安全超时,确保状态不会永久卡住 |
|
|
|
|
|
const safetyTimeout = setTimeout(() => { |
|
|
|
|
|
if (this.isScrolling) { |
|
|
|
|
|
console.warn('⚠️ 滚动状态安全超时,强制重置'); |
|
|
|
|
|
resetScrollingState(); |
|
|
|
|
|
} |
|
|
|
|
|
}, 2000); // 2秒安全超时 |
|
|
|
|
|
|
|
|
if (!scrollData || typeof scrollData.highlightIndex !== 'number' || scrollData.highlightIndex < 0) { |
|
|
if (!scrollData || typeof scrollData.highlightIndex !== 'number' || scrollData.highlightIndex < 0) { |
|
|
console.warn('滚动数据无效:', scrollData); |
|
|
console.warn('滚动数据无效:', scrollData); |
|
|
|
|
|
clearTimeout(safetyTimeout); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -700,12 +809,14 @@ export default { |
|
|
scrollDataPage: scrollData.currentPage, |
|
|
scrollDataPage: scrollData.currentPage, |
|
|
currentPage: this.currentPage |
|
|
currentPage: this.currentPage |
|
|
}); |
|
|
}); |
|
|
|
|
|
clearTimeout(safetyTimeout); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 如果正在滚动中,跳过本次滚动 |
|
|
// 如果正在滚动中,跳过本次滚动 |
|
|
if (this.isScrolling) { |
|
|
if (this.isScrolling) { |
|
|
console.warn('正在滚动中,跳过本次滚动'); |
|
|
console.warn('正在滚动中,跳过本次滚动'); |
|
|
|
|
|
clearTimeout(safetyTimeout); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -742,6 +853,9 @@ export default { |
|
|
query.select(selector).boundingClientRect(); |
|
|
query.select(selector).boundingClientRect(); |
|
|
|
|
|
|
|
|
query.exec((res) => { |
|
|
query.exec((res) => { |
|
|
|
|
|
// 清除安全超时 |
|
|
|
|
|
clearTimeout(safetyTimeout); |
|
|
|
|
|
|
|
|
const scrollViewRect = res[0]; |
|
|
const scrollViewRect = res[0]; |
|
|
const targetRect = res[1]; |
|
|
const targetRect = res[1]; |
|
|
|
|
|
|
|
|
@ -772,7 +886,7 @@ export default { |
|
|
|
|
|
|
|
|
// 滚动完成后重置状态 |
|
|
// 滚动完成后重置状态 |
|
|
setTimeout(() => { |
|
|
setTimeout(() => { |
|
|
this.isScrolling = false; |
|
|
|
|
|
|
|
|
resetScrollingState(); |
|
|
}, 300); // 稍微减少等待时间 |
|
|
}, 300); // 稍微减少等待时间 |
|
|
|
|
|
|
|
|
console.log('✅ 滚动到高亮文本:', { |
|
|
console.log('✅ 滚动到高亮文本:', { |
|
|
@ -784,7 +898,7 @@ export default { |
|
|
}); |
|
|
}); |
|
|
} else { |
|
|
} else { |
|
|
// 不需要滚动,立即重置状态 |
|
|
// 不需要滚动,立即重置状态 |
|
|
this.isScrolling = false; |
|
|
|
|
|
|
|
|
resetScrollingState(); |
|
|
console.log('📍 目标已在视野内,无需滚动'); |
|
|
console.log('📍 目标已在视野内,无需滚动'); |
|
|
} |
|
|
} |
|
|
} else { |
|
|
} else { |
|
|
@ -795,7 +909,6 @@ export default { |
|
|
currentPage: this.currentPage, |
|
|
currentPage: this.currentPage, |
|
|
highlightIndex: scrollData.highlightIndex |
|
|
highlightIndex: scrollData.highlightIndex |
|
|
}); |
|
|
}); |
|
|
this.isScrolling = false; |
|
|
|
|
|
|
|
|
|
|
|
// 尝试备用方案:直接滚动到页面顶部附近 |
|
|
// 尝试备用方案:直接滚动到页面顶部附近 |
|
|
if (!targetRect) { |
|
|
if (!targetRect) { |
|
|
@ -803,8 +916,11 @@ export default { |
|
|
const fallbackScrollTop = scrollData.highlightIndex * 100; // 简单估算位置 |
|
|
const fallbackScrollTop = scrollData.highlightIndex * 100; // 简单估算位置 |
|
|
this.$set(this.scrollTops, this.currentPage - 1, fallbackScrollTop); |
|
|
this.$set(this.scrollTops, this.currentPage - 1, fallbackScrollTop); |
|
|
setTimeout(() => { |
|
|
setTimeout(() => { |
|
|
this.isScrolling = false; |
|
|
|
|
|
|
|
|
resetScrollingState(); |
|
|
}, 300); |
|
|
}, 300); |
|
|
|
|
|
} else { |
|
|
|
|
|
// 立即重置状态 |
|
|
|
|
|
resetScrollingState(); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
@ -1404,6 +1520,24 @@ export default { |
|
|
return false; |
|
|
return false; |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// 判断划线重点卡片中的文本是否应该高亮 |
|
|
|
|
|
isCardTextHighlighted(page, index) { |
|
|
|
|
|
// 只有当前页面且是文本类型才可能高亮 |
|
|
|
|
|
if (page !== this.bookPages[this.currentPage - 1]) return false; |
|
|
|
|
|
|
|
|
|
|
|
// 计算当前页面中text类型元素的索引 |
|
|
|
|
|
let textIndex = 0; |
|
|
|
|
|
for (let i = 0; i <= index; i++) { |
|
|
|
|
|
if (page[i].type === 'text') { |
|
|
|
|
|
if (i === index) { |
|
|
|
|
|
return textIndex === this.currentHighlightIndex; |
|
|
|
|
|
} |
|
|
|
|
|
textIndex++; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return false; |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
async previousPage() { |
|
|
async previousPage() { |
|
|
if (this.currentPage > 1) { |
|
|
if (this.currentPage > 1) { |
|
|
this.currentPage--; |
|
|
this.currentPage--; |
|
|
@ -2175,6 +2309,9 @@ export default { |
|
|
border: 1px solid #ffe58f; |
|
|
border: 1px solid #ffe58f; |
|
|
border-radius: 8px; |
|
|
border-radius: 8px; |
|
|
padding: 10rpx 20rpx; |
|
|
padding: 10rpx 20rpx; |
|
|
|
|
|
|
|
|
|
|
|
/* 添加平滑过渡动画 */ |
|
|
|
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
.text-highlight { |
|
|
.text-highlight { |
|
|
@ -2182,15 +2319,10 @@ export default { |
|
|
border-left: 4rpx solid #ffd700; /* 左侧金色边框作为朗读指示 */ |
|
|
border-left: 4rpx solid #ffd700; /* 左侧金色边框作为朗读指示 */ |
|
|
padding: 4rpx 8rpx; |
|
|
padding: 4rpx 8rpx; |
|
|
border-radius: 6rpx; |
|
|
border-radius: 6rpx; |
|
|
transition: all 0.3s ease; |
|
|
|
|
|
box-shadow: 0 2rpx 6rpx rgba(255, 215, 0, 0.15); /* 柔和的阴影 */ |
|
|
box-shadow: 0 2rpx 6rpx rgba(255, 215, 0, 0.15); /* 柔和的阴影 */ |
|
|
|
|
|
|
|
|
|
|
|
/* 添加平滑过渡动画 */ |
|
|
|
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</style> |
|
|
</style> |