|
|
|
@ -52,12 +52,18 @@ |
|
|
|
v-for="(token, tokenIndex) in splitEnglishSentence(item.content)" |
|
|
|
:key="tokenIndex" |
|
|
|
:class="['english-token', { 'clickable-word': token.isWord && findWordDefinition(token.text) }]" |
|
|
|
@tap="token.isWord && findWordDefinition(token.text) ? handleWordClick(token.text) : null" |
|
|
|
@click.stop="token.isWord && findWordDefinition(token.text) ? handleWordClick(token.text) : null" |
|
|
|
user-select |
|
|
|
>{{ token.text }}</text> |
|
|
|
</view> |
|
|
|
<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> |
|
|
|
<text |
|
|
|
v-for="(segment, segmentIndex) in processChineseText(item.content)" |
|
|
|
:key="segmentIndex" |
|
|
|
:class="['chinese-segment', { 'clickable-keyword': segment.isKeyword }]" |
|
|
|
@click.stop="segment.isKeyword ? handleChineseKeywordClick(segment.keywordData) : null" |
|
|
|
user-select |
|
|
|
>{{ segment.text }}</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
@ -68,11 +74,14 @@ |
|
|
|
<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 |
|
|
|
v-if="!item.isLead" |
|
|
|
class="content-text clickable-text" |
|
|
|
:style="item.style" |
|
|
|
user-select |
|
|
|
> |
|
|
|
{{ item.content }} |
|
|
|
</text> |
|
|
|
<text v-else-if="item.isLead" class="content-text clickable-text lead-text" :style="item.style" user-select>{{ item.content }}</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
@ -97,7 +106,6 @@ |
|
|
|
:poster="item.coverUrl" |
|
|
|
@loadstart="onVideoLoadStart" |
|
|
|
@loadeddata="onVideoLoadStart" |
|
|
|
|
|
|
|
@error="onVideoError" |
|
|
|
></video> |
|
|
|
</view> |
|
|
|
@ -109,155 +117,64 @@ |
|
|
|
</swiper> |
|
|
|
|
|
|
|
<!-- 自定义底部控制栏 --> |
|
|
|
<view class="custom-tabbar" :class="{ 'tabbar-hidden': !showNavbar }"> |
|
|
|
<!-- 音频控制栏组件 --> |
|
|
|
<AudioControls |
|
|
|
:current-page="currentPage" |
|
|
|
:course-id="courseId" |
|
|
|
:voice-id="voiceId" |
|
|
|
:book-pages="bookPages" |
|
|
|
:is-text-page="isTextPage" |
|
|
|
:is-member="isMember" |
|
|
|
:current-page-requires-member="currentPageRequiresMember" |
|
|
|
:page-pay="pagePay" |
|
|
|
@previous-page="previousPage" |
|
|
|
@next-page="nextPage" |
|
|
|
@audio-state-change="onAudioStateChange" |
|
|
|
@highlight-change="onHighlightChange" |
|
|
|
@voice-change-complete="onVoiceChangeComplete" |
|
|
|
@voice-change-error="onVoiceChangeError" |
|
|
|
ref="audioControls" |
|
|
|
/> |
|
|
|
|
|
|
|
|
|
|
|
<view style="background-color: #fff;position: relative;z-index: 100" > |
|
|
|
<view class="tabbar-content"> |
|
|
|
<view class="tabbar-left"> |
|
|
|
<view class="tab-button" @click="toggleCoursePopup"> |
|
|
|
<image src="/static/课程图标.png" class="tab-icon" /> |
|
|
|
<text class="tab-text">课程</text> |
|
|
|
</view> |
|
|
|
<view class="tab-button" @click="toggleSound"> |
|
|
|
<image src="/static/音色切换图标.png" class="tab-icon" /> |
|
|
|
<text class="tab-text">音色切换</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
<view class="tabbar-right"> |
|
|
|
<view class="page-controls"> |
|
|
|
<view class="page-numbers"> |
|
|
|
<view |
|
|
|
v-for="(page, index) in bookPages" |
|
|
|
:key="index" |
|
|
|
class="page-number" |
|
|
|
:class="{ 'active': (index + 1) === currentPage }" |
|
|
|
@click="goToPage(index + 1)" |
|
|
|
> |
|
|
|
{{ index + 1 }} |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
<uv-safe-bottom></uv-safe-bottom> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
<CustomTabbar |
|
|
|
:show-navbar="showNavbar" |
|
|
|
:current-page="currentPage" |
|
|
|
:course-id="courseId" |
|
|
|
:voice-id="voiceId" |
|
|
|
:book-pages="bookPages" |
|
|
|
:is-text-page="isTextPage" |
|
|
|
:should-load-audio="shouldLoadAudio" |
|
|
|
:is-member="isMember" |
|
|
|
:current-page-requires-member="currentPageRequiresMember" |
|
|
|
:page-pay="pagePay" |
|
|
|
@toggle-course-popup="toggleCoursePopup" |
|
|
|
@toggle-sound="toggleSound" |
|
|
|
@go-to-page="goToPage" |
|
|
|
@previous-page="previousPage" |
|
|
|
@next-page="nextPage" |
|
|
|
@audio-state-change="onAudioStateChange" |
|
|
|
@highlight-change="onHighlightChange" |
|
|
|
@voice-change-complete="onVoiceChangeComplete" |
|
|
|
@voice-change-error="onVoiceChangeError" |
|
|
|
ref="customTabbar" |
|
|
|
/> |
|
|
|
|
|
|
|
<!-- 课程选择弹出窗 --> |
|
|
|
<uv-popup |
|
|
|
mode="bottom" |
|
|
|
<CoursePopup |
|
|
|
:style="{zIndex: 10000}" |
|
|
|
:course-list="courseList" |
|
|
|
:current-course="currentCourse" |
|
|
|
:is-reversed="isReversed" |
|
|
|
@toggle-sort="toggleSort" |
|
|
|
@select-course="selectCourse" |
|
|
|
ref="coursePopup" |
|
|
|
round="32rpx" |
|
|
|
bg-color="#f8f8f8" |
|
|
|
> |
|
|
|
<view class="course-popup"> |
|
|
|
<view class="popup-header"> |
|
|
|
<view> |
|
|
|
<uv-icon name="arrow-down" color="black" size="20"></uv-icon> |
|
|
|
</view> |
|
|
|
<view class="popup-title">课程</view> |
|
|
|
<view class="popup-title" @click="toggleSort"> |
|
|
|
倒序 |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
<view class="course-list"> |
|
|
|
<view |
|
|
|
v-for="(course, index) in displayCourseList" |
|
|
|
:key="course.id" |
|
|
|
class="course-item" |
|
|
|
:class="{ 'active': course.id === currentCourse }" |
|
|
|
@click="selectCourse(course.id)" |
|
|
|
> |
|
|
|
<view class="course-number " :class="{ 'highlight': course.id === currentCourse }">{{ String(course.index).padStart(2, '0') }}</view> |
|
|
|
<view class="course-content"> |
|
|
|
<view class="course-english" :class="{ 'highlight': course.id === currentCourse }">{{ course.english }}</view> |
|
|
|
<view class="course-chinese" :class="{ 'highlight': course.id === currentCourse }">{{ course.chinese }}</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</uv-popup> |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
<!-- 释义弹出窗 --> |
|
|
|
<uv-popup |
|
|
|
mode="bottom" |
|
|
|
|
|
|
|
<MeaningPopup |
|
|
|
:style="{zIndex: 10000}" |
|
|
|
:current-word-meaning="currentWordMeaning" |
|
|
|
@close-meaning-popup="closeMeaningPopup" |
|
|
|
@repeat-word-audio="repeatWordAudio" |
|
|
|
ref="meaningPopup" |
|
|
|
round="32rpx" |
|
|
|
bg-color="#FFFFFF" |
|
|
|
:overlay="true" |
|
|
|
> |
|
|
|
<view class="meaning-popup" v-if="currentWordMeaning"> |
|
|
|
<view class="meaning-header"> |
|
|
|
<view class="close-btn" @click="closeMeaningPopup"> |
|
|
|
<text class="close-text">关闭</text> |
|
|
|
</view> |
|
|
|
<view class="meaning-title">释义</view> |
|
|
|
<view style="width: 80rpx;"></view> |
|
|
|
</view> |
|
|
|
|
|
|
|
<view class="meaning-content"> |
|
|
|
<image v-if="currentWordMeaning.imag" class="meaning-image" :src="currentWordMeaning.image" mode="aspectFill"></image> |
|
|
|
|
|
|
|
<view class="word-info"> |
|
|
|
<view class="word-main"> |
|
|
|
<text class="word-text">{{ currentWordMeaning.word }}</text> |
|
|
|
</view> |
|
|
|
<view class="phonetic-container"> |
|
|
|
<uv-icon |
|
|
|
name="volume-fill" |
|
|
|
size="16" |
|
|
|
color="#007AFF" |
|
|
|
class="speaker-icon" |
|
|
|
@click="repeatWordAudio" |
|
|
|
></uv-icon> |
|
|
|
<text class="phonetic-text">{{ currentWordMeaning.phonetic }}</text> |
|
|
|
</view> |
|
|
|
<view class="word-meaning"> |
|
|
|
<text class="part-of-speech">{{ currentWordMeaning.partOfSpeech }}</text> |
|
|
|
<text class="meaning-text">{{ currentWordMeaning.meaning }}</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
<view class="knowledge-gain"> |
|
|
|
<view class="knowledge-header"> |
|
|
|
<image src="/static/知识收获图标.png" class="knowledge-icon" mode="aspectFill" /> |
|
|
|
<text class="knowledge-title">知识收获</text> |
|
|
|
</view> |
|
|
|
<text class="knowledge-content">{{ currentWordMeaning.knowledgeGain }}</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</uv-popup> |
|
|
|
/> |
|
|
|
</view> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script> |
|
|
|
import AudioControls from './AudioControls.vue' |
|
|
|
import CustomTabbar from './components/CustomTabbar.vue' |
|
|
|
import CoursePopup from './components/CoursePopup.vue' |
|
|
|
import MeaningPopup from './components/MeaningPopup.vue' |
|
|
|
|
|
|
|
export default { |
|
|
|
components: { |
|
|
|
AudioControls |
|
|
|
AudioControls, |
|
|
|
CustomTabbar, |
|
|
|
CoursePopup, |
|
|
|
MeaningPopup |
|
|
|
}, |
|
|
|
data() { |
|
|
|
return { |
|
|
|
@ -318,6 +235,21 @@ export default { |
|
|
|
return currentPageData && currentPageData.some(item => item.type === 'text'); |
|
|
|
}, |
|
|
|
|
|
|
|
// 判断当前页面是否需要加载音频(包括文本页面和卡片页面) |
|
|
|
shouldLoadAudio() { |
|
|
|
// 文本页面需要加载音频 |
|
|
|
if (this.isTextPage) { |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
// 卡片页面(type为'1')也需要加载音频以支持点击播放 |
|
|
|
if (this.currentPageType === '1') { |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
return false; |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
// 动态页面标题 |
|
|
|
currentPageTitle() { |
|
|
|
@ -429,15 +361,15 @@ export default { |
|
|
|
|
|
|
|
// 处理文本点击事件 |
|
|
|
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 : '页面不存在'); |
|
|
|
// 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) { |
|
|
|
@ -472,7 +404,7 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
// 检查是否有音频控制组件的引用 |
|
|
|
if (!this.$refs.audioControls) { |
|
|
|
if (!this.$refs.customTabbar || !this.$refs.customTabbar.$refs.audioControls) { |
|
|
|
console.log('音频控制组件未找到'); |
|
|
|
uni.showToast({ |
|
|
|
title: '音频控制组件未准备好', |
|
|
|
@ -481,16 +413,10 @@ export default { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 检查当前页面是否为文本页面 |
|
|
|
// 对于卡片页面(type为'1'),不显示错误提示,因为单词点击会单独处理 |
|
|
|
if (!this.isTextPage) { |
|
|
|
// 如果是卡片页面,静默返回,不显示错误提示 |
|
|
|
if (this.currentPageType === '1') { |
|
|
|
console.log('卡片页面的文本点击,单词播放由handleWordClick处理'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
console.log('当前页面不是文本页面'); |
|
|
|
// 检查当前页面是否为文本页面或卡片页面 |
|
|
|
// 卡片页面(type为'1')现在也支持整句音频播放 |
|
|
|
if (!this.isTextPage && this.currentPageType !== '1') { |
|
|
|
console.log('当前页面不是文本页面或卡片页面'); |
|
|
|
uni.showToast({ |
|
|
|
title: '当前页面不支持音频播放', |
|
|
|
icon: 'none' |
|
|
|
@ -499,7 +425,7 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
// 调用AudioControls组件的播放指定音频方法 |
|
|
|
const success = this.$refs.audioControls.playSpecificAudio(textContent); |
|
|
|
const success = this.$refs.customTabbar.$refs.audioControls.playSpecificAudio(textContent); |
|
|
|
|
|
|
|
if (success) { |
|
|
|
console.log('成功播放指定音频段落'); |
|
|
|
@ -562,9 +488,6 @@ export default { |
|
|
|
}, |
|
|
|
selectCourse(courseId) { |
|
|
|
this.currentCourse = courseId |
|
|
|
if (this.$refs.coursePopup) { |
|
|
|
this.$refs.coursePopup.close() |
|
|
|
} |
|
|
|
// 这里可以添加切换课程的逻辑 |
|
|
|
// console.log('选择课程:', courseId) |
|
|
|
this.getCourseList(courseId) |
|
|
|
@ -575,9 +498,6 @@ export default { |
|
|
|
} |
|
|
|
}, |
|
|
|
closeMeaningPopup() { |
|
|
|
if (this.$refs.meaningPopup) { |
|
|
|
this.$refs.meaningPopup.close() |
|
|
|
} |
|
|
|
this.currentWordMeaning = null |
|
|
|
}, |
|
|
|
|
|
|
|
@ -602,6 +522,82 @@ export default { |
|
|
|
); |
|
|
|
}, |
|
|
|
|
|
|
|
// 处理中文文本,标记重点词汇 |
|
|
|
processChineseText(text) { |
|
|
|
const currentPageWords = this.pageWords[this.currentPage - 1] || []; |
|
|
|
|
|
|
|
if (!text || currentPageWords.length === 0) { |
|
|
|
return [{ text: text, isKeyword: false, keywordData: null }]; |
|
|
|
} |
|
|
|
|
|
|
|
// 创建一个数组来存储处理后的文本片段 |
|
|
|
const segments = []; |
|
|
|
let currentIndex = 0; |
|
|
|
|
|
|
|
// 按照重点词汇的长度排序,优先匹配较长的词汇 |
|
|
|
const sortedWords = [...currentPageWords].sort((a, b) => b.word.length - a.word.length); |
|
|
|
|
|
|
|
while (currentIndex < text.length) { |
|
|
|
let matched = false; |
|
|
|
|
|
|
|
// 尝试匹配重点词汇 |
|
|
|
for (const wordData of sortedWords) { |
|
|
|
const keyword = wordData.word; |
|
|
|
if (text.substr(currentIndex, keyword.length) === keyword) { |
|
|
|
// 找到匹配的重点词汇 |
|
|
|
segments.push({ |
|
|
|
text: keyword, |
|
|
|
isKeyword: true, |
|
|
|
keywordData: wordData |
|
|
|
}); |
|
|
|
currentIndex += keyword.length; |
|
|
|
matched = true; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (!matched) { |
|
|
|
// 没有匹配到重点词汇,添加单个字符 |
|
|
|
segments.push({ |
|
|
|
text: text[currentIndex], |
|
|
|
isKeyword: false, |
|
|
|
keywordData: null |
|
|
|
}); |
|
|
|
currentIndex++; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 合并相邻的非重点词汇片段 |
|
|
|
const mergedSegments = []; |
|
|
|
let currentSegment = null; |
|
|
|
|
|
|
|
for (const segment of segments) { |
|
|
|
if (segment.isKeyword) { |
|
|
|
// 如果当前有未完成的非重点词汇片段,先添加它 |
|
|
|
if (currentSegment) { |
|
|
|
mergedSegments.push(currentSegment); |
|
|
|
currentSegment = null; |
|
|
|
} |
|
|
|
// 添加重点词汇 |
|
|
|
mergedSegments.push(segment); |
|
|
|
} else { |
|
|
|
// 非重点词汇,合并到当前片段 |
|
|
|
if (currentSegment) { |
|
|
|
currentSegment.text += segment.text; |
|
|
|
} else { |
|
|
|
currentSegment = { ...segment }; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 添加最后的非重点词汇片段 |
|
|
|
if (currentSegment) { |
|
|
|
mergedSegments.push(currentSegment); |
|
|
|
} |
|
|
|
|
|
|
|
return mergedSegments; |
|
|
|
}, |
|
|
|
|
|
|
|
async playWordAudio(word) { |
|
|
|
try { |
|
|
|
console.log('開始播放單詞語音:', word); |
|
|
|
@ -678,8 +674,10 @@ export default { |
|
|
|
// 重複播放單詞語音(用於釋義彈窗中的揚聲器圖標) |
|
|
|
repeatWordAudio() { |
|
|
|
if (this.currentWordMeaning && this.currentWordMeaning.word) { |
|
|
|
console.log('重複播放單詞語音:', this.currentWordMeaning.word); |
|
|
|
this.playWordAudio(this.currentWordMeaning.word); |
|
|
|
// 将单词和解释合并后播放音频 |
|
|
|
const combinedText = `${this.currentWordMeaning.word}。${this.currentWordMeaning.meaning || ''}`; |
|
|
|
console.log('重複播放合併文本:', combinedText); |
|
|
|
this.playWordAudio(combinedText); |
|
|
|
} else { |
|
|
|
console.warn('沒有當前單詞可以播放'); |
|
|
|
} |
|
|
|
@ -690,11 +688,6 @@ export default { |
|
|
|
const definition = this.findWordDefinition(word); |
|
|
|
console.log('查找单词:', word, '释义:', definition); |
|
|
|
|
|
|
|
// 獲取單詞的讀音 |
|
|
|
if (word) { |
|
|
|
this.playWordAudio(word); |
|
|
|
} |
|
|
|
|
|
|
|
if (definition) { |
|
|
|
this.currentWordMeaning = { |
|
|
|
word: definition.word, |
|
|
|
@ -703,9 +696,43 @@ export default { |
|
|
|
meaning: definition.paraphrase || '', |
|
|
|
knowledgeGain: definition.knowledge || '' |
|
|
|
}; |
|
|
|
|
|
|
|
// 将单词和解释合并后播放音频 |
|
|
|
const combinedText = `${word}。${definition.paraphrase || ''}`; |
|
|
|
console.log('播放合并文本:', combinedText); |
|
|
|
this.playWordAudio(combinedText); |
|
|
|
|
|
|
|
this.showWordMeaning(); |
|
|
|
} else { |
|
|
|
console.log('未找到单词释义:', word); |
|
|
|
// 如果没有释义,只播放单词 |
|
|
|
if (word) { |
|
|
|
this.playWordAudio(word); |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 处理中文重点词汇点击事件 |
|
|
|
handleChineseKeywordClick(keywordData) { |
|
|
|
console.log('点击中文重点词汇:', keywordData.word, '释义:', keywordData); |
|
|
|
|
|
|
|
if (keywordData) { |
|
|
|
this.currentWordMeaning = { |
|
|
|
word: keywordData.word, |
|
|
|
phonetic: keywordData.soundmark || '', |
|
|
|
partOfSpeech: '', // 可以根据需要添加词性 |
|
|
|
meaning: keywordData.paraphrase || '', |
|
|
|
knowledgeGain: keywordData.knowledge || '' |
|
|
|
}; |
|
|
|
|
|
|
|
// 将词汇和解释合并后播放音频 |
|
|
|
const combinedText = `${keywordData.word}。${keywordData.paraphrase || ''}`; |
|
|
|
console.log('播放中文合并文本:', combinedText); |
|
|
|
this.playWordAudio(combinedText); |
|
|
|
|
|
|
|
this.showWordMeaning(); |
|
|
|
} else { |
|
|
|
console.log('未找到中文词汇释义'); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
@ -870,6 +897,8 @@ export default { |
|
|
|
createAudioInstance() { |
|
|
|
// 销毁之前的音频实例 |
|
|
|
if (this.currentAudio) { |
|
|
|
console.log('销毁原来的音频'); |
|
|
|
|
|
|
|
this.currentAudio.destroy(); |
|
|
|
} |
|
|
|
|
|
|
|
@ -1397,7 +1426,7 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
// 检查AudioControls组件是否正在加载音频 |
|
|
|
if (this.$refs.audioControls && this.$refs.audioControls.isAudioLoading) { |
|
|
|
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls && this.$refs.customTabbar.$refs.audioControls.isAudioLoading) { |
|
|
|
uni.showToast({ |
|
|
|
title: '音频加载中,请稍后再试', |
|
|
|
icon: 'none', |
|
|
|
@ -1439,8 +1468,8 @@ export default { |
|
|
|
}) |
|
|
|
if (res.code === 200) { |
|
|
|
// 课程切换时,先清理音频控制组件的所有数据 |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
this.$refs.audioControls.resetForCourseChange(); |
|
|
|
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) { |
|
|
|
this.$refs.customTabbar.$refs.audioControls.resetForCourseChange(); |
|
|
|
} |
|
|
|
|
|
|
|
// 清空当前页面相关数据 |
|
|
|
@ -1475,11 +1504,11 @@ export default { |
|
|
|
|
|
|
|
// 使用$nextTick确保DOM和数据都已更新 |
|
|
|
this.$nextTick(async () => { |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) { |
|
|
|
console.log('开始自动加载课程音频'); |
|
|
|
try { |
|
|
|
// 直接调用getCurrentPageAudio方法,更可靠 |
|
|
|
await this.$refs.audioControls.getCurrentPageAudio(); |
|
|
|
await this.$refs.customTabbar.$refs.audioControls.getCurrentPageAudio(); |
|
|
|
console.log('课程切换后音频加载完成'); |
|
|
|
} catch (error) { |
|
|
|
console.error('课程切换后音频加载失败:', error); |
|
|
|
@ -1591,8 +1620,8 @@ export default { |
|
|
|
|
|
|
|
// 延迟1.5秒后再通知AudioControls组件开始预加载音频,避免接口冲突 |
|
|
|
setTimeout(() => { |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
this.$refs.audioControls.startPreloadAudio(); |
|
|
|
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) { |
|
|
|
this.$refs.customTabbar.$refs.audioControls.startPreloadAudio(); |
|
|
|
} |
|
|
|
}, 1500); |
|
|
|
|
|
|
|
@ -1634,9 +1663,9 @@ export default { |
|
|
|
try { |
|
|
|
console.log('開始自動加載第一頁音頻'); |
|
|
|
|
|
|
|
// 確保當前是第一頁且是文字頁面 |
|
|
|
if (this.currentPage === 1 && this.isTextPage) { |
|
|
|
console.log('當前是第一頁文字頁面,開始加載音頻'); |
|
|
|
// 確保當前是第一頁且需要加載音頻 |
|
|
|
if (this.currentPage === 1 && this.shouldLoadAudio) { |
|
|
|
console.log('當前是第一頁且需要音頻,開始加載音頻'); |
|
|
|
|
|
|
|
// 加載音頻 |
|
|
|
await this.getCurrentPageAudio(); |
|
|
|
@ -1665,7 +1694,7 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
// 检查是否正在加载音频,如果是则阻止音色切换 |
|
|
|
if (this.isAudioLoading || (this.$refs.audioControls && this.$refs.audioControls.isAudioLoading)) { |
|
|
|
if (this.isAudioLoading || (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls && this.$refs.customTabbar.$refs.audioControls.isAudioLoading)) { |
|
|
|
console.log('音频正在加载中,阻止音色切换'); |
|
|
|
uni.showToast({ |
|
|
|
title: '音频加载中,请稍后再试', |
|
|
|
@ -1686,11 +1715,11 @@ export default { |
|
|
|
} |
|
|
|
|
|
|
|
// 通知AudioControls组件处理音色切换 |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) { |
|
|
|
try { |
|
|
|
console.log('开始处理音色切换,清理所有音频缓存并重新获取所有页面音频'); |
|
|
|
// 传入选项:preloadAllPages: true 表示要预加载所有页面的音频 |
|
|
|
await this.$refs.audioControls.handleVoiceChange(voiceId, { |
|
|
|
await this.$refs.customTabbar.$refs.audioControls.handleVoiceChange(voiceId, { |
|
|
|
preloadAllPages: true |
|
|
|
}); |
|
|
|
console.log('音色切换处理完成'); |
|
|
|
@ -1708,8 +1737,8 @@ export default { |
|
|
|
|
|
|
|
// 页面加载完成后,通知AudioControls组件自动加载第一页音频 |
|
|
|
this.$nextTick(() => { |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
this.$refs.audioControls.autoLoadAndPlayFirstPage(); |
|
|
|
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) { |
|
|
|
this.$refs.customTabbar.$refs.audioControls.autoLoadAndPlayFirstPage(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
@ -1726,16 +1755,16 @@ export default { |
|
|
|
this.clearWordAudioCache(); |
|
|
|
|
|
|
|
// 通知AudioControls组件清理资源 |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
this.$refs.audioControls.destroyAudio(); |
|
|
|
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) { |
|
|
|
this.$refs.customTabbar.$refs.audioControls.destroyAudio(); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 页面隐藏时暂停音频 |
|
|
|
onHide() { |
|
|
|
// 通知AudioControls组件暂停音频 |
|
|
|
if (this.$refs.audioControls) { |
|
|
|
this.$refs.audioControls.pauseOnHide(); |
|
|
|
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) { |
|
|
|
this.$refs.customTabbar.$refs.audioControls.pauseOnHide(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -1855,11 +1884,12 @@ export default { |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
gap: 32rpx; |
|
|
|
height: 1172rpx; |
|
|
|
min-height: 1172rpx; |
|
|
|
margin-top: 20rpx; |
|
|
|
border-radius: 32rpx; |
|
|
|
// height: 100%; |
|
|
|
padding: 40rpx; |
|
|
|
padding-bottom: 100rpx; |
|
|
|
// margin: 0 |
|
|
|
border: 1px solid #FFFFFF; |
|
|
|
box-sizing: border-box; |
|
|
|
@ -1926,6 +1956,27 @@ export default { |
|
|
|
background-color: rgba(0, 122, 255, 0.1); |
|
|
|
border-radius: 4rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.chinese-segment { |
|
|
|
font-family: PingFang SC; |
|
|
|
font-weight: 400; |
|
|
|
font-size: 28rpx; |
|
|
|
line-height: 48rpx; |
|
|
|
color: #3B3D3D; |
|
|
|
} |
|
|
|
|
|
|
|
.clickable-keyword { |
|
|
|
background: $primary-color; |
|
|
|
text-decoration: underline; |
|
|
|
cursor: pointer; |
|
|
|
transition: all 0.2s ease; |
|
|
|
border-radius: 4rpx; |
|
|
|
padding: 2rpx 4rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.clickable-keyword:hover { |
|
|
|
background-color: rgba(0, 122, 255, 0.1); |
|
|
|
} |
|
|
|
|
|
|
|
.chinese-text { |
|
|
|
display: block; |
|
|
|
@ -2027,331 +2078,26 @@ export default { |
|
|
|
border-radius: 4rpx; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.text-highlight { |
|
|
|
background-color: $primary-color; |
|
|
|
// color: #fff; |
|
|
|
// padding: 8rpx 16rpx; |
|
|
|
// border-radius: 8rpx; |
|
|
|
// box-shadow: 0 2rpx 8rpx rgba(6, 218, 220, 0.3); |
|
|
|
} |
|
|
|
|
|
|
|
.custom-tabbar { |
|
|
|
position: fixed; |
|
|
|
bottom: 0; |
|
|
|
left: 0; |
|
|
|
right: 0; |
|
|
|
// background-color: #fff; |
|
|
|
border-top: 1rpx solid #EEEEEE; |
|
|
|
z-index: 1000; |
|
|
|
transition: transform 0.3s ease; |
|
|
|
|
|
|
|
&.tabbar-hidden { |
|
|
|
transform: translateY(100%); |
|
|
|
} |
|
|
|
.lead-text { |
|
|
|
background: #fffbe6; /* 柔和的提示背景 */ |
|
|
|
border: 1px solid #ffe58f; |
|
|
|
border-radius: 8px; |
|
|
|
} |
|
|
|
|
|
|
|
.tabbar-content { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: space-between; |
|
|
|
padding: 24rpx 62rpx; |
|
|
|
// z-index: 100; |
|
|
|
// position: relative; |
|
|
|
// background-color: #fff; |
|
|
|
// padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); |
|
|
|
height: 88rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.tabbar-left { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
gap: 35rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.tab-button { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
flex-direction: column; |
|
|
|
gap: 8rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.tab-icon { |
|
|
|
width: 52rpx; |
|
|
|
height: 52rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.tab-text { |
|
|
|
font-family: PingFang SC; |
|
|
|
// font-weight: 400; |
|
|
|
font-size: 22rpx; |
|
|
|
color: #999; |
|
|
|
line-height: 24rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.tabbar-right { |
|
|
|
flex: 1; |
|
|
|
display: flex; |
|
|
|
justify-content: flex-end; |
|
|
|
} |
|
|
|
|
|
|
|
.page-controls { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
} |
|
|
|
|
|
|
|
.page-numbers { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
gap: 8rpx; |
|
|
|
overflow-x: auto; |
|
|
|
max-width: 400rpx; |
|
|
|
|
|
|
|
&::-webkit-scrollbar { |
|
|
|
display: none; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.page-number { |
|
|
|
min-width: 84rpx; |
|
|
|
height: 58rpx; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
border-radius: 100rpx; |
|
|
|
font-family: PingFang SC; |
|
|
|
// font-weight: 400; |
|
|
|
font-size: 30rpx; |
|
|
|
color: #3B3D3D; |
|
|
|
background-color: transparent; |
|
|
|
border: 1px solid #3B3D3D; |
|
|
|
.text-highlight { |
|
|
|
background-color: rgba(255, 248, 220, 0.8); /* 温暖的米黄色,对眼睛友好 */ |
|
|
|
border-left: 4rpx solid #ffd700; /* 左侧金色边框作为朗读指示 */ |
|
|
|
padding: 4rpx 8rpx; |
|
|
|
border-radius: 6rpx; |
|
|
|
transition: all 0.3s ease; |
|
|
|
|
|
|
|
&.active { |
|
|
|
border: 1px solid $primary-color; |
|
|
|
color: $primary-color; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/* 课程弹出窗样式 */ |
|
|
|
.course-popup { |
|
|
|
padding: 0 32rpx; |
|
|
|
max-height: 80vh; |
|
|
|
} |
|
|
|
|
|
|
|
.popup-header { |
|
|
|
display: flex; |
|
|
|
justify-content: space-between; |
|
|
|
align-items: center; |
|
|
|
padding: 20rpx 0; |
|
|
|
border-bottom: 2rpx solid #EEEEEE |
|
|
|
// margin-bottom: 40rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.popup-title { |
|
|
|
font-family: PingFang SC; |
|
|
|
font-weight: 500; |
|
|
|
font-size: 34rpx; |
|
|
|
color: #181818; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.course-list { |
|
|
|
max-height: 60vh; |
|
|
|
overflow-y: auto; |
|
|
|
} |
|
|
|
|
|
|
|
.course-item { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
gap: 24rpx; |
|
|
|
padding-top: 24rpx; |
|
|
|
padding-right: 8rpx; |
|
|
|
padding-bottom: 24rpx; |
|
|
|
padding-left: 8rpx; |
|
|
|
|
|
|
|
border-bottom: 1px solid #EEEEEE; |
|
|
|
cursor: pointer; |
|
|
|
|
|
|
|
&:last-child { |
|
|
|
border-bottom: none; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.course-number { |
|
|
|
width: 80rpx; |
|
|
|
font-family: PingFang SC; |
|
|
|
// font-weight: 400; |
|
|
|
font-size: 36rpx; |
|
|
|
color: #999; |
|
|
|
&.highlight { |
|
|
|
color: $primary-color; |
|
|
|
} |
|
|
|
// margin-right: 24rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.course-content { |
|
|
|
flex: 1; |
|
|
|
} |
|
|
|
|
|
|
|
.course-english { |
|
|
|
font-family: PingFang SC; |
|
|
|
font-weight: 600; |
|
|
|
font-size: 36rpx; |
|
|
|
line-height: 44rpx; |
|
|
|
color: #252545; |
|
|
|
margin-bottom: 8rpx; |
|
|
|
|
|
|
|
&.highlight { |
|
|
|
color: $primary-color; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.course-chinese { |
|
|
|
font-size: 28rpx; |
|
|
|
line-height: 48rpx; |
|
|
|
color: #3B3D3D; |
|
|
|
&.highlight { |
|
|
|
color: $primary-color; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/* 释义弹窗样式 */ |
|
|
|
.meaning-popup { |
|
|
|
// width: 670rpx; |
|
|
|
background-color: #FFFFFF; |
|
|
|
// border-radius: 32rpx; |
|
|
|
overflow: hidden; |
|
|
|
} |
|
|
|
|
|
|
|
.meaning-header { |
|
|
|
display: flex; |
|
|
|
justify-content: space-between; |
|
|
|
align-items: center; |
|
|
|
padding: 32rpx; |
|
|
|
border-bottom: 2rpx solid #EEEEEE; |
|
|
|
} |
|
|
|
|
|
|
|
.close-btn { |
|
|
|
width: 80rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.close-text { |
|
|
|
font-family: PingFang SC; |
|
|
|
font-size: 32rpx; |
|
|
|
color: #8b8b8b; |
|
|
|
} |
|
|
|
|
|
|
|
.meaning-title { |
|
|
|
font-family: PingFang SC; |
|
|
|
font-weight: 500; |
|
|
|
font-size: 34rpx; |
|
|
|
color: #181818; |
|
|
|
box-shadow: 0 2rpx 6rpx rgba(255, 215, 0, 0.15); /* 柔和的阴影 */ |
|
|
|
} |
|
|
|
|
|
|
|
.meaning-content { |
|
|
|
padding: 32rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.meaning-image { |
|
|
|
width: 670rpx; |
|
|
|
height: 268rpx; |
|
|
|
border-radius: 24rpx; |
|
|
|
margin-bottom: 32rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.word-info { |
|
|
|
margin-bottom: 32rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.word-main { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
gap: 16rpx; |
|
|
|
margin-bottom: 8rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.word-text { |
|
|
|
font-family: PingFang SC; |
|
|
|
font-weight: 500; |
|
|
|
font-size: 40rpx; |
|
|
|
color: #181818; |
|
|
|
} |
|
|
|
|
|
|
|
.phonetic-container{ |
|
|
|
display: flex; |
|
|
|
gap: 24rpx; |
|
|
|
align-items: center; |
|
|
|
|
|
|
|
.speaker-icon { |
|
|
|
cursor: pointer; |
|
|
|
transition: opacity 0.2s ease; |
|
|
|
padding: 8rpx; |
|
|
|
border-radius: 8rpx; |
|
|
|
|
|
|
|
&:hover { |
|
|
|
opacity: 0.7; |
|
|
|
background-color: rgba(0, 122, 255, 0.1); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.phonetic-text { |
|
|
|
font-family: PingFang SC; |
|
|
|
font-size: 28rpx; |
|
|
|
color: #262626; |
|
|
|
margin-bottom: 16rpx; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.word-meaning { |
|
|
|
display: flex; |
|
|
|
gap: 16rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.part-of-speech { |
|
|
|
font-family: PingFang SC; |
|
|
|
font-size: 28rpx; |
|
|
|
color: #262626; |
|
|
|
} |
|
|
|
|
|
|
|
.meaning-text { |
|
|
|
font-family: PingFang SC; |
|
|
|
font-size: 28rpx; |
|
|
|
color: #181818; |
|
|
|
flex: 1; |
|
|
|
} |
|
|
|
|
|
|
|
.knowledge-gain { |
|
|
|
background: linear-gradient(180deg, #DEFFFF 0%, #FBFEFF 22.65%, #F0FBFF 100%); |
|
|
|
border-radius: 24rpx; |
|
|
|
padding: 32rpx 40rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.knowledge-header { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
gap: 16rpx; |
|
|
|
margin-bottom: 20rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.knowledge-icon { |
|
|
|
width: 48rpx; |
|
|
|
height: 48rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.knowledge-title { |
|
|
|
font-family: PingFang SC; |
|
|
|
font-weight: 600; |
|
|
|
font-size: 30rpx; |
|
|
|
color: #3B3D3D; |
|
|
|
} |
|
|
|
|
|
|
|
.knowledge-content { |
|
|
|
font-family: PingFang SC; |
|
|
|
font-size: 28rpx; |
|
|
|
color: #4f4f4f; |
|
|
|
line-height: 48rpx; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
</style> |