Browse Source

'音頻組件拆分出來了'

hfll
hflllll 1 month ago
parent
commit
520ce546a1
2 changed files with 1253 additions and 502 deletions
  1. +1182
    -0
      subPages/home/AudioControls.vue
  2. +71
    -502
      subPages/home/book.vue

+ 1182
- 0
subPages/home/AudioControls.vue
File diff suppressed because it is too large
View File


+ 71
- 502
subPages/home/book.vue View File

@ -17,7 +17,6 @@
<swiper
class="content-swiper"
:current="currentPage - 1"
:disable-touch="isAudioLoading"
@change="onSwiperChange"
>
<swiper-item
@ -91,70 +90,19 @@
<!-- 自定义底部控制栏 -->
<view class="custom-tabbar" :class="{ 'tabbar-hidden': !showNavbar }">
<!-- 音频控制栏 -->
<!-- 获取音频按钮 -->
<view v-if="!hasAudioData && !isAudioLoading && isTextPage" class="audio-get-button-container">
<view class="get-audio-btn" @click="handleGetAudio">
<uv-icon name="play-circle" size="24" color="#06DADC"></uv-icon>
<text class="get-audio-text">获取第{{currentPage}}页音频</text>
</view>
</view>
<!-- 音频加载状态 -->
<view v-else-if="isAudioLoading && isTextPage" class="audio-loading-container">
<uv-loading-icon mode="spinner" size="30" color="#06DADC"></uv-loading-icon>
<text class="loading-text">正在加载第{{currentPage}}页音频...</text>
</view>
<!-- 正常音频控制栏 -->
<view v-else-if="hasAudioData" class="audio-controls" :class="{ 'audio-hidden': !isTextPage }">
<view class="audio-time">
<text class="time-text">{{ formatTime(currentTime) }}</text>
<view class="progress-container">
<uv-slider
v-model="sliderValue"
:min="0"
:max="totalTime"
:step="0.1"
activeColor="#06DADC"
backgroundColor="#e0e0e0"
:blockSize="16"
blockColor="#ffffff"
:disabled="!hasAudioData || totalTime <= 0"
@input="onSliderInput"
@changing="onSliderChanging"
@change="onSliderChange"
:customStyle="{ flex: 1, margin: '0 10px' }"
/>
</view>
<text class="time-text">{{ formatTime(totalTime) }}</text>
</view>
<view class="audio-controls-row">
<view class="control-btn" @click="toggleLoop">
<uv-icon name="reload" size="20" :color="isLoop ? '#06DADC' : '#999'"></uv-icon>
<text class="control-text">循环</text>
</view>
<view class="control-btn" @click="previousPage">
<text class="control-text">上一页</text>
</view>
<view class="play-btn" @click="togglePlay">
<uv-icon :name="isPlaying ? 'pause-circle-fill' : 'play-circle-fill'" size="40" color="#666"></uv-icon>
</view>
<view class="control-btn" @click="nextPage">
<text class="control-text">下一页</text>
</view>
<view class="control-btn" @click="toggleSpeed" :class="{ 'disabled': !playbackRateSupported }">
<text class="control-text" :style="{ opacity: playbackRateSupported ? 1 : 0.5 }">
{{ playbackRateSupported ? playSpeed + 'x' : '不支持' }}
</text>
</view>
</view>
</view>
<!-- 音频控制栏组件 -->
<AudioControls
:current-page="currentPage"
:course-id="courseId"
:voice-id="voiceId"
:book-pages="bookPages"
:is-text-page="isTextPage"
@previous-page="previousPage"
@next-page="nextPage"
@audio-state-change="onAudioStateChange"
@highlight-change="onHighlightChange"
ref="audioControls"
/>
<view style="background-color: #fff;position: relative;z-index: 100" >
@ -280,7 +228,12 @@
</template>
<script>
import AudioControls from './AudioControls.vue'
export default {
components: {
AudioControls
},
data() {
return {
isMember: false,
@ -292,30 +245,10 @@ export default {
currentCourse: 1, //
currentWordMeaning: null, //
isReversed: false, //
//
isPlaying: false,
currentTime: 0,
totalTime: 0,
sliderValue: 0, //
isDragging: false, //
isLoop: false,
playSpeed: 1.0,
speedOptions: [0.5, 0.8, 1.0, 1.25, 1.5, 2.0], // uni-app
playbackRateSupported: true, //
//
currentPageAudios: [], //
currentAudioIndex: 0, //
audioContext: null, //
currentAudio: null, //
//
audioCache: {}, // {pageIndex: {audios: [], totalDuration: 0}}
wordAudioCache: {}, //
currentWordAudio: null, //
//
isAudioLoading: false, //
hasAudioData: false, //
//
currentHighlightIndex: -1, //
wordAudioCache: {}, //
currentWordAudio: null, //
courseIdList: [],
bookTitle: '',
courseList: [
@ -351,10 +284,7 @@ export default {
// currentPageData typetexttrue
return currentPageData && currentPageData.some(item => item.type === 'text');
},
//
progressPercent() {
return this.totalTime > 0 ? (this.currentTime / this.totalTime) * 100 : 0;
},
//
currentPageTitle() {
@ -381,170 +311,19 @@ export default {
}
},
//
async getCurrentPageAudio() {
//
const cacheKey = `${this.courseId}_${this.currentPage}`;
if (this.audioCache[cacheKey]) {
console.log('从缓存加载音频数据:', cacheKey);
//
this.currentPageAudios = this.audioCache[cacheKey].audios;
this.totalTime = this.audioCache[cacheKey].totalDuration;
this.currentAudioIndex = 0;
this.isPlaying = false;
this.currentTime = 0;
return;
}
//
this.isAudioLoading = true;
//
this.currentPageAudios = [];
this.currentAudioIndex = 0;
this.isPlaying = false;
this.currentTime = 0;
this.totalTime = 0;
// [] text
const currentPageData = this.bookPages[this.currentPage - 1];
if (currentPageData) {
// text
const textItems = currentPageData.filter(item => item.type === 'text');
if (textItems.length > 0) {
//
const audioPromises = textItems.map(async (item, index) => {
try {
// - 使contenttext
const radioRes = await this.$api.music.textToVoice({
text: item.content,
voiceType: this.voiceId,
});
console.log(`音频请求响应 ${index + 1}:`, radioRes);
if(radioRes.code === 200){
// APIURL
const audioUrl = radioRes.result.audio;
// API
const duration = radioRes.result.duration || 0;
//
item.audioUrl = audioUrl;
console.log(`音频URL设置成功 ${index + 1}:`, audioUrl, '时长:', duration);
return {
url: audioUrl,
text: item.content,
duration: duration, // 使API
index: index //
};
} else {
console.error(`音频请求失败 ${index + 1}:`, radioRes);
return null;
}
} catch (error) {
console.error(`音频请求异常 ${index + 1}:`, error);
return null;
}
});
//
const audioResults = await Promise.all(audioPromises);
//
this.currentPageAudios = audioResults
.filter(result => result !== null)
.sort((a, b) => a.index - b.index)
.map(result => ({
url: result.url,
text: result.text,
duration: result.duration
}));
console.log('所有音频请求完成,共获取', this.currentPageAudios.length, '个音频');
}
//
if (this.currentPageAudios.length > 0) {
await this.calculateTotalDuration();
//
const cacheKey = `${this.courseId}_${this.currentPage}`;
this.audioCache[cacheKey] = {
audios: [...this.currentPageAudios], //
totalDuration: this.totalTime
};
console.log('音频数据已缓存:', cacheKey, this.audioCache[cacheKey]);
//
this.limitCacheSize(10);
}
}
//
this.isAudioLoading = false;
//
this.hasAudioData = this.currentPageAudios.length > 0;
// AudioControls
onAudioStateChange(audioState) {
//
this.currentHighlightIndex = audioState.currentHighlightIndex;
},
//
resetAudioState() {
//
const pageKey = `${this.courseId}_${this.currentPage}`;
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 {
//
this.currentPageAudios = [];
this.totalTime = 0;
this.hasAudioData = false;
}
// 1.0
this.currentAudioIndex = 0;
this.isPlaying = false;
this.currentTime = 0;
this.isAudioLoading = false;
this.currentHighlightIndex = -1;
// 1.0
this.playSpeed = 1.0;
},
//
async handleGetAudio() {
// ID
if (!this.voiceId) {
uni.showToast({
title: '音色未加载,请稍后重试',
icon: 'none'
});
return;
}
//
if (!this.isTextPage) {
uni.showToast({
title: '当前页面没有文本内容',
icon: 'none'
});
return;
}
//
if (this.isAudioLoading) {
return;
}
//
await this.getCurrentPageAudio();
},
onHighlightChange(highlightIndex) {
//
this.currentHighlightIndex = highlightIndex;
},
// id
async getVoiceList() {
const voiceRes = await this.$api.music.list()
@ -1376,58 +1155,33 @@ export default {
},
async previousPage() {
//
if (this.isAudioLoading) {
return;
}
if (this.currentPage > 1) {
//
this.pauseAudio();
if (this.currentAudio) {
this.currentAudio.destroy();
this.currentAudio = null;
}
this.currentPage--;
//
if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) {
await this.getBookPages(this.courseIdList[this.currentPage - 1]);
}
//
this.resetAudioState();
//
if (this.$refs.audioControls) {
this.$refs.audioControls.resetAudioState();
}
}
},
async nextPage() {
//
if (this.isAudioLoading) {
return;
}
if (this.currentPage < this.bookPages.length) {
//
this.pauseAudio();
if (this.currentAudio) {
this.currentAudio.destroy();
this.currentAudio = null;
}
this.currentPage++;
//
if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) {
await this.getBookPages(this.courseIdList[this.currentPage - 1]);
}
//
this.resetAudioState();
//
if (this.$refs.audioControls) {
this.$refs.audioControls.resetAudioState();
}
}
},
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
},
toggleSound() {
console.log('音色切换')
uni.navigateTo({
@ -1442,18 +1196,6 @@ export default {
})
},
async goToPage(page) {
//
if (this.isAudioLoading) {
return;
}
//
this.pauseAudio();
if (this.currentAudio) {
this.currentAudio.destroy();
this.currentAudio = null;
}
this.currentPage = page
console.log('跳转到页面:', page)
//
@ -1461,30 +1203,22 @@ export default {
await this.getBookPages(this.courseIdList[this.currentPage - 1]);
}
//
this.resetAudioState();
//
if (this.$refs.audioControls) {
this.$refs.audioControls.resetAudioState();
}
},
async onSwiperChange(e) {
//
if (this.isAudioLoading) {
return;
}
//
this.pauseAudio();
if (this.currentAudio) {
this.currentAudio.destroy();
this.currentAudio = null;
}
this.currentPage = e.detail.current + 1
//
if (this.courseIdList[this.currentPage - 1] && this.bookPages[this.currentPage - 1].length === 0) {
await this.getBookPages(this.courseIdList[this.currentPage - 1]);
}
//
this.resetAudioState();
//
if (this.$refs.audioControls) {
this.$refs.audioControls.resetAudioState();
}
},
async getCourseList(id) {
const res = await this.$api.book.coursePage({
@ -1500,8 +1234,10 @@ export default {
this.pageTypes = this.courseIdList.map(() => '')
//
this.pageWords = this.courseIdList.map(() => [])
//
this.resetAudioState()
//
if (this.$refs.audioControls) {
this.$refs.audioControls.resetAudioState();
}
//
if (this.courseIdList.length > 0) {
await this.getBookPages(this.courseIdList[0])
@ -1600,81 +1336,58 @@ export default {
},
},
async onLoad(args) {
// AudioControls
uni.$on('selectVoice', (voiceId) => {
console.log('音色切換:', this.voiceId, '->', voiceId);
this.voiceId = voiceId;
//
this.clearAudioCache();
//
this.clearWordAudioCache();
//
if (this.currentAudio) {
this.currentAudio.destroy();
this.currentAudio = null;
}
if (this.currentWordAudio) {
this.currentWordAudio.destroy();
this.currentWordAudio = null;
}
//
this.currentPageAudios = [];
this.totalTime = 0;
this.hasAudioData = false;
this.isPlaying = false;
this.currentTime = 0;
this.currentAudioIndex = 0;
this.currentHighlightIndex = -1;
this.sliderValue = 0;
this.isDragging = false;
//
if (this.isTextPage) {
console.log('音色切換後重新獲取音頻數據');
this.getAudio();
}
})
this.courseId = args.courseId
this.memberId = args.memberId
//
this.resetAudioState()
//
this.checkInitialPlaybackRateSupport()
//
await Promise.all([this.getCourseList(this.courseId), this.getCoursePageList(args.bookId), this.getMemberInfo(), this.getVoiceList()])
//
await this.autoLoadAndPlayFirstPage();
// AudioControls
this.$nextTick(() => {
if (this.$refs.audioControls) {
this.$refs.audioControls.autoLoadAndPlayFirstPage();
}
});
},
//
//
onUnload() {
uni.$off('selectVoice')
if (this.currentAudio) {
this.currentAudio.destroy();
this.currentAudio = null;
}
//
if (this.currentWordAudio) {
this.currentWordAudio.destroy();
this.currentWordAudio = null;
}
this.isPlaying = false;
//
this.clearAudioCache();
//
this.clearWordAudioCache();
// AudioControls
if (this.$refs.audioControls) {
this.$refs.audioControls.destroyAudio();
}
},
//
onHide() {
this.pauseAudio();
// AudioControls
if (this.$refs.audioControls) {
this.$refs.audioControls.pauseOnHide();
}
}
}
</script>
@ -2247,149 +1960,5 @@ export default {
line-height: 48rpx;
}
/* 音频控制栏样式 */
.audio-controls {
background: #fff;
padding: 20rpx 40rpx;
border-bottom: 1rpx solid #eee;
transition: transform 0.3s ease;
position: relative;
z-index: 10;
&.audio-hidden {
transform: translateY(100%);
}
}
.audio-time {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.time-text {
font-size: 28rpx;
color: #999;
min-width: 80rpx;
}
.progress-container {
flex: 1;
margin: 0 20rpx;
}
.progress-bar {
position: relative;
height: 6rpx;
background: #f0f0f0;
border-radius: 3rpx;
}
.progress-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
background: #06DADC;
border-radius: 3rpx;
transition: width 0.1s ease;
}
.progress-thumb {
position: absolute;
top: 50%;
width: 16rpx;
height: 16rpx;
background: #06DADC;
border-radius: 50%;
transform: translate(-50%, -50%);
transition: left 0.1s ease;
}
.audio-controls-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.control-btn {
display: flex;
// flex-direction: column;
align-items: center;
padding: 10rpx;
gap: 8rpx;
}
.control-btn.disabled {
pointer-events: none;
opacity: 0.6;
}
.control-text {
font-size: 28rpx;
color: #4A4A4A;
// margin-top: 8rpx;
}
.play-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 10rpx;
}
/* 音频加载状态样式 */
.audio-loading-container {
background: #fff;
padding: 40rpx;
border-bottom: 1rpx solid #eee;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20rpx;
position: relative;
z-index: 10;
}
.loading-text {
font-size: 28rpx;
color: #999;
}
/* 获取音频按钮样式 */
.audio-get-button-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 30rpx;
border-radius: 20rpx;
border: 2rpx solid #E5E5E5;
transition: all 0.3s ease;
position: relative;
z-index: 10;
}
.get-audio-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
padding: 20rpx 40rpx;
background: linear-gradient(135deg, #06DADC 0%, #04B8BA 100%);
border-radius: 50rpx;
box-shadow: 0 8rpx 20rpx rgba(6, 218, 220, 0.3);
transition: all 0.3s ease;
}
.get-audio-btn:active {
transform: scale(0.95);
box-shadow: 0 4rpx 10rpx rgba(6, 218, 220, 0.2);
}
.get-audio-text {
font-size: 32rpx;
color: #FFFFFF;
font-weight: 500;
}
</style>

Loading…
Cancel
Save