|
|
@ -40,9 +40,9 @@ |
|
|
<image :src="configParamContent('highlight_icon')" class="card-line-image" mode="aspectFill" /> |
|
|
<image :src="configParamContent('highlight_icon')" class="card-line-image" mode="aspectFill" /> |
|
|
<text class="card-line-text">划线重点</text> |
|
|
<text class="card-line-text">划线重点</text> |
|
|
</view> |
|
|
</view> |
|
|
<view v-for="(item, index) in page" :key="index"> |
|
|
|
|
|
<image class="card-image" v-if="item.type === 'image'" :src="item.imageUrl" mode="aspectFill"></image> |
|
|
|
|
|
<view class="english-text-container" v-else-if="item.type === 'text' && item.language === 'en'"> |
|
|
|
|
|
|
|
|
<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> |
|
|
|
|
|
<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)"> |
|
|
<text |
|
|
<text |
|
|
v-for="(token, tokenIndex) in splitEnglishSentence(item.content)" |
|
|
v-for="(token, tokenIndex) in splitEnglishSentence(item.content)" |
|
|
:key="tokenIndex" |
|
|
:key="tokenIndex" |
|
|
@ -52,17 +52,19 @@ |
|
|
user-select |
|
|
user-select |
|
|
>{{ token.text }}</text> |
|
|
>{{ token.text }}</text> |
|
|
</view> |
|
|
</view> |
|
|
<text class="chinese-text" v-else-if="item.type === 'text' && item.language === 'zh'" user-select>{{ item.content }}</text> |
|
|
|
|
|
|
|
|
<view v-else-if="item && item.type === 'text' && item.language === 'zh' && item.content" @click.stop="handleTextClick(item.content, item, index)"> |
|
|
|
|
|
<text class="chinese-text clickable-text" user-select>{{ item.content }}</text> |
|
|
|
|
|
</view> |
|
|
</view> |
|
|
</view> |
|
|
</view> |
|
|
</view> |
|
|
|
|
|
|
|
|
<view v-else> |
|
|
<view v-else> |
|
|
<view v-for="(item, index) in page" :key="index"> |
|
|
|
|
|
|
|
|
<view v-for="(item, itemIndex) in page" :key="itemIndex"> |
|
|
<!-- 文本页面 --> |
|
|
<!-- 文本页面 --> |
|
|
<view v-if="item.type === 'text'" class="text-content" > |
|
|
|
|
|
<view :class="{ 'text-highlight': isTextHighlighted(page, index) }"> |
|
|
|
|
|
|
|
|
<view v-if="item && item.type === 'text' && item.content" class="text-content" > |
|
|
|
|
|
<view :class="{ 'text-highlight': isTextHighlighted(page, itemIndex) }" @click.stop="handleTextClick(item.content, item, index)"> |
|
|
<text |
|
|
<text |
|
|
class="content-text" |
|
|
|
|
|
|
|
|
class="content-text clickable-text" |
|
|
user-select |
|
|
user-select |
|
|
> |
|
|
> |
|
|
{{ item.content }} |
|
|
{{ item.content }} |
|
|
@ -318,9 +320,103 @@ export default { |
|
|
this.currentHighlightIndex = audioState.currentHighlightIndex; |
|
|
this.currentHighlightIndex = audioState.currentHighlightIndex; |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
onHighlightChange(highlightIndex) { |
|
|
|
|
|
// 更新高亮索引 |
|
|
|
|
|
this.currentHighlightIndex = highlightIndex; |
|
|
|
|
|
|
|
|
// 处理文本点击事件 |
|
|
|
|
|
handleTextClick(textContent, item, pageIndex) { |
|
|
|
|
|
console.log('点击文本:', textContent); |
|
|
|
|
|
console.log('textContent类型:', typeof textContent); |
|
|
|
|
|
console.log('textContent是否为undefined:', textContent === undefined); |
|
|
|
|
|
console.log('完整item对象:', item); |
|
|
|
|
|
console.log('item.content:', item ? item.content : 'item为空'); |
|
|
|
|
|
console.log('当前页面索引:', this.currentPage); |
|
|
|
|
|
console.log('点击的页面索引:', pageIndex); |
|
|
|
|
|
console.log('当前页面数据:', this.bookPages[this.currentPage - 1]); |
|
|
|
|
|
console.log('页面数据长度:', this.bookPages[this.currentPage - 1] ? this.bookPages[this.currentPage - 1].length : '页面不存在'); |
|
|
|
|
|
|
|
|
|
|
|
// 检查是否点击的是当前页面 |
|
|
|
|
|
if (pageIndex !== undefined && pageIndex !== this.currentPage - 1) { |
|
|
|
|
|
console.log('点击的不是当前页面,忽略点击事件'); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 验证参数有效性 |
|
|
|
|
|
if (!item) { |
|
|
|
|
|
console.error('handleTextClick: item参数为空'); |
|
|
|
|
|
uni.showToast({ |
|
|
|
|
|
title: '数据错误,请刷新页面', |
|
|
|
|
|
icon: 'none' |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 如果textContent为undefined,尝试从item中获取 |
|
|
|
|
|
if (!textContent && item && item.content) { |
|
|
|
|
|
textContent = item.content; |
|
|
|
|
|
console.log('从item中获取到content:', textContent); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 最终验证textContent |
|
|
|
|
|
if (!textContent || typeof textContent !== 'string' || textContent.trim() === '') { |
|
|
|
|
|
console.error('handleTextClick: 无效的文本内容', textContent); |
|
|
|
|
|
uni.showToast({ |
|
|
|
|
|
title: '文本内容无效', |
|
|
|
|
|
icon: 'none' |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有音频控制组件的引用 |
|
|
|
|
|
if (!this.$refs.audioControls) { |
|
|
|
|
|
console.log('音频控制组件未找到'); |
|
|
|
|
|
uni.showToast({ |
|
|
|
|
|
title: '音频控制组件未准备好', |
|
|
|
|
|
icon: 'none' |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 检查当前页面是否为文本页面 |
|
|
|
|
|
if (!this.isTextPage) { |
|
|
|
|
|
console.log('当前页面不是文本页面'); |
|
|
|
|
|
uni.showToast({ |
|
|
|
|
|
title: '当前页面不支持音频播放', |
|
|
|
|
|
icon: 'none' |
|
|
|
|
|
}); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 调用AudioControls组件的播放指定音频方法 |
|
|
|
|
|
const success = this.$refs.audioControls.playSpecificAudio(textContent); |
|
|
|
|
|
|
|
|
|
|
|
if (success) { |
|
|
|
|
|
console.log('成功播放指定音频段落'); |
|
|
|
|
|
} else { |
|
|
|
|
|
console.log('播放指定音频段落失败'); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
onHighlightChange(highlightData) { |
|
|
|
|
|
// 兼容旧格式(直接传递索引)和新格式(传递对象) |
|
|
|
|
|
if (typeof highlightData === 'number') { |
|
|
|
|
|
// 旧格式:直接是索引 |
|
|
|
|
|
this.currentHighlightIndex = highlightData; |
|
|
|
|
|
} else if (typeof highlightData === 'object' && highlightData !== null) { |
|
|
|
|
|
// 新格式:包含详细信息的对象 |
|
|
|
|
|
this.currentHighlightIndex = highlightData.highlightIndex; |
|
|
|
|
|
|
|
|
|
|
|
// 可以在这里处理分段音频的额外信息 |
|
|
|
|
|
if (highlightData.isSegmented) { |
|
|
|
|
|
console.log('分段音频高亮:', { |
|
|
|
|
|
highlightIndex: highlightData.highlightIndex, |
|
|
|
|
|
segmentIndex: highlightData.segmentIndex, |
|
|
|
|
|
startIndex: highlightData.startIndex, |
|
|
|
|
|
endIndex: highlightData.endIndex, |
|
|
|
|
|
currentText: highlightData.currentText |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
// 清除高亮 |
|
|
|
|
|
this.currentHighlightIndex = -1; |
|
|
|
|
|
} |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -409,9 +505,9 @@ export default { |
|
|
console.log('單詞語音API響應:', audioRes); |
|
|
console.log('單詞語音API響應:', audioRes); |
|
|
|
|
|
|
|
|
// 檢查響應並播放音頻 |
|
|
// 檢查響應並播放音頻 |
|
|
if (audioRes && audioRes.result && audioRes.result.audio) { |
|
|
|
|
|
// 新格式:直接使用返回的音頻URL |
|
|
|
|
|
const audioUrl = audioRes.result.audio; |
|
|
|
|
|
|
|
|
if (audioRes && audioRes.result && audioRes.result.url) { |
|
|
|
|
|
// 新格式:使用返回的url字段 |
|
|
|
|
|
const audioUrl = audioRes.result.url; |
|
|
|
|
|
|
|
|
// 創建並播放音頻 |
|
|
// 創建並播放音頻 |
|
|
const audio = uni.createInnerAudioContext(); |
|
|
const audio = uni.createInnerAudioContext(); |
|
|
@ -1203,9 +1299,19 @@ export default { |
|
|
await this.getBookPages(this.courseIdList[this.currentPage - 1]); |
|
|
await this.getBookPages(this.courseIdList[this.currentPage - 1]); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 通知音频控制组件重置状态 |
|
|
|
|
|
|
|
|
// 通知音频控制组件处理页面切换 |
|
|
if (this.$refs.audioControls) { |
|
|
if (this.$refs.audioControls) { |
|
|
this.$refs.audioControls.resetAudioState(); |
|
|
|
|
|
|
|
|
// 检查当前页面是否有预加载的音频缓存 |
|
|
|
|
|
const hasPreloadedAudio = this.$refs.audioControls.checkAudioCache(this.currentPage); |
|
|
|
|
|
|
|
|
|
|
|
if (hasPreloadedAudio) { |
|
|
|
|
|
// 如果有预加载的音频,直接播放 |
|
|
|
|
|
console.log(`第${this.currentPage}页音频已预加载,自动播放`); |
|
|
|
|
|
this.$refs.audioControls.autoPlayCachedAudio(); |
|
|
|
|
|
} else { |
|
|
|
|
|
// 如果没有预加载的音频,重置状态 |
|
|
|
|
|
this.$refs.audioControls.resetAudioState(); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
async onSwiperChange(e) { |
|
|
async onSwiperChange(e) { |
|
|
@ -1215,9 +1321,19 @@ export default { |
|
|
await this.getBookPages(this.courseIdList[this.currentPage - 1]); |
|
|
await this.getBookPages(this.courseIdList[this.currentPage - 1]); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 通知音频控制组件重置状态 |
|
|
|
|
|
|
|
|
// 通知音频控制组件处理页面切换 |
|
|
if (this.$refs.audioControls) { |
|
|
if (this.$refs.audioControls) { |
|
|
this.$refs.audioControls.resetAudioState(); |
|
|
|
|
|
|
|
|
// 检查当前页面是否有预加载的音频缓存 |
|
|
|
|
|
const hasPreloadedAudio = this.$refs.audioControls.checkAudioCache(this.currentPage); |
|
|
|
|
|
|
|
|
|
|
|
if (hasPreloadedAudio) { |
|
|
|
|
|
// 如果有预加载的音频,直接播放 |
|
|
|
|
|
console.log(`第${this.currentPage}页音频已预加载,自动播放`); |
|
|
|
|
|
this.$refs.audioControls.autoPlayCachedAudio(); |
|
|
|
|
|
} else { |
|
|
|
|
|
// 如果没有预加载的音频,重置状态 |
|
|
|
|
|
this.$refs.audioControls.resetAudioState(); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
async getCourseList(id) { |
|
|
async getCourseList(id) { |
|
|
@ -1241,6 +1357,8 @@ export default { |
|
|
// 初始化第一页 |
|
|
// 初始化第一页 |
|
|
if (this.courseIdList.length > 0) { |
|
|
if (this.courseIdList.length > 0) { |
|
|
await this.getBookPages(this.courseIdList[0]) |
|
|
await this.getBookPages(this.courseIdList[0]) |
|
|
|
|
|
// 预加载后续几页的内容(异步执行,不阻塞当前页面显示) |
|
|
|
|
|
this.preloadNextPages() |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
@ -1250,10 +1368,18 @@ export default { |
|
|
}) |
|
|
}) |
|
|
if (res.code === 200) { |
|
|
if (res.code === 200) { |
|
|
// 使用$set确保响应式更新 |
|
|
// 使用$set确保响应式更新 |
|
|
console.log('获取到的页面数据:', JSON.parse(res.result.content)) |
|
|
|
|
|
|
|
|
const rawPageData = JSON.parse(res.result.content) |
|
|
|
|
|
console.log('获取到的原始页面数据:', rawPageData) |
|
|
|
|
|
|
|
|
|
|
|
// 过滤掉无效的数据项 |
|
|
|
|
|
const filteredPageData = rawPageData.filter(item => { |
|
|
|
|
|
return item && typeof item === 'object' && (item.type || item.content) |
|
|
|
|
|
}) |
|
|
|
|
|
console.log('过滤后的页面数据:', filteredPageData) |
|
|
|
|
|
|
|
|
// 确保当前页面存在 |
|
|
// 确保当前页面存在 |
|
|
if (this.currentPage - 1 < this.bookPages.length) { |
|
|
if (this.currentPage - 1 < this.bookPages.length) { |
|
|
this.$set(this.bookPages, this.currentPage - 1, JSON.parse(res.result.content)) |
|
|
|
|
|
|
|
|
this.$set(this.bookPages, this.currentPage - 1, filteredPageData) |
|
|
// 保存页面标题 |
|
|
// 保存页面标题 |
|
|
this.$set(this.pageTitles, this.currentPage - 1, res.result.title || '') |
|
|
this.$set(this.pageTitles, this.currentPage - 1, res.result.title || '') |
|
|
// 保存页面类型 |
|
|
// 保存页面类型 |
|
|
@ -1304,6 +1430,75 @@ export default { |
|
|
console.log('缓存大小已限制,删除了', keysToDelete.length, '个缓存项'); |
|
|
console.log('缓存大小已限制,删除了', keysToDelete.length, '个缓存项'); |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// 预加载后续页面内容 |
|
|
|
|
|
async preloadNextPages() { |
|
|
|
|
|
try { |
|
|
|
|
|
console.log('开始预加载后续页面内容'); |
|
|
|
|
|
|
|
|
|
|
|
// 优化策略:只预加载接下来的2-3页内容,避免过多请求 |
|
|
|
|
|
const preloadCount = Math.min(3, this.courseIdList.length - 1); // 预加载3页或剩余页数 |
|
|
|
|
|
|
|
|
|
|
|
// 串行预加载,避免并发请求过多 |
|
|
|
|
|
for (let i = 1; i <= preloadCount; i++) { |
|
|
|
|
|
if (i < this.courseIdList.length && this.bookPages[i].length === 0) { |
|
|
|
|
|
try { |
|
|
|
|
|
console.log(`预加载第${i + 1}页内容`); |
|
|
|
|
|
await this.preloadSinglePage(this.courseIdList[i], i); |
|
|
|
|
|
|
|
|
|
|
|
// 每页之间间隔800ms,给服务器更多缓冲时间 |
|
|
|
|
|
if (i < preloadCount) { |
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 800)); |
|
|
|
|
|
} |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
console.error(`预加载第${i + 1}页失败:`, error); |
|
|
|
|
|
// 继续预加载下一页 |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
console.log('页面内容预加载完成'); |
|
|
|
|
|
|
|
|
|
|
|
// 延迟1.5秒后再通知AudioControls组件开始预加载音频,避免接口冲突 |
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
if (this.$refs.audioControls) { |
|
|
|
|
|
this.$refs.audioControls.startPreloadAudio(); |
|
|
|
|
|
} |
|
|
|
|
|
}, 1500); |
|
|
|
|
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
console.error('预加载页面内容失败:', error); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// 预加载单个页面 |
|
|
|
|
|
async preloadSinglePage(courseId, pageIndex) { |
|
|
|
|
|
try { |
|
|
|
|
|
const res = await this.$api.book.coursesPageDetail({ |
|
|
|
|
|
id: courseId |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if (res.code === 200) { |
|
|
|
|
|
const rawPageData = JSON.parse(res.result.content); |
|
|
|
|
|
const filteredPageData = rawPageData.filter(item => { |
|
|
|
|
|
return item && typeof item === 'object' && (item.type || item.content); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// 使用$set确保响应式更新 |
|
|
|
|
|
this.$set(this.bookPages, pageIndex, filteredPageData); |
|
|
|
|
|
this.$set(this.pageTitles, pageIndex, res.result.title || ''); |
|
|
|
|
|
this.$set(this.pageTypes, pageIndex, res.result.type || ''); |
|
|
|
|
|
this.$set(this.pageWords, pageIndex, res.result.words || []); |
|
|
|
|
|
this.$set(this.pagePay, pageIndex, res.result.pay || 'N'); |
|
|
|
|
|
|
|
|
|
|
|
console.log(`第${pageIndex + 1}页内容预加载完成`); |
|
|
|
|
|
} |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
console.error(`预加载第${pageIndex + 1}页失败:`, error); |
|
|
|
|
|
throw error; |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
// 自動加載第一頁音頻並播放 |
|
|
// 自動加載第一頁音頻並播放 |
|
|
async autoLoadAndPlayFirstPage() { |
|
|
async autoLoadAndPlayFirstPage() { |
|
|
try { |
|
|
try { |
|
|
@ -1318,12 +1513,8 @@ export default { |
|
|
|
|
|
|
|
|
// 檢查是否成功加載音頻 |
|
|
// 檢查是否成功加載音頻 |
|
|
if (this.currentPageAudios && this.currentPageAudios.length > 0) { |
|
|
if (this.currentPageAudios && this.currentPageAudios.length > 0) { |
|
|
console.log('音頻加載成功,開始自動播放'); |
|
|
|
|
|
|
|
|
|
|
|
// 稍微延遲一下確保音頻完全準備好 |
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
this.playAudio(); |
|
|
|
|
|
}, 500); |
|
|
|
|
|
|
|
|
console.log('音頻加載成功,getCurrentPageAudio已經自動播放第一個音頻'); |
|
|
|
|
|
// getCurrentPageAudio方法已經處理了第一個音頻的播放,這裡不需要再次調用playAudio |
|
|
} else { |
|
|
} else { |
|
|
console.log('第一頁沒有音頻數據'); |
|
|
console.log('第一頁沒有音頻數據'); |
|
|
} |
|
|
} |
|
|
@ -1635,6 +1826,16 @@ export default { |
|
|
transition: all 0.3s ease; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.clickable-text { |
|
|
|
|
|
cursor: pointer; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&:active { |
|
|
|
|
|
background-color: rgba(6, 218, 220, 0.1); |
|
|
|
|
|
border-radius: 4rpx; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
.text-highlight { |
|
|
.text-highlight { |
|
|
background-color: $primary-color; |
|
|
background-color: $primary-color; |
|
|
// color: #fff; |
|
|
// color: #fff; |
|
|
|