更新:图书搜索功能;听书实时显示对应文字;书籍评论功能完善;
This commit is contained in:
@@ -4,21 +4,16 @@
|
||||
<nav-bar :title="currentChapter.chapter"></nav-bar>
|
||||
|
||||
<view class="player-content" :style="{ height: contentHeight + 'px' }">
|
||||
<!-- 封面 -->
|
||||
<view class="cover-section">
|
||||
<image
|
||||
:src="bookInfo.images"
|
||||
class="cover-image"
|
||||
:class="{ rotating: isPlaying }"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 章节信息 -->
|
||||
<view class="chapter-info">
|
||||
<text class="chapter-title">{{ currentChapter.chapter }}</text>
|
||||
<text v-if="currentChapter.content" class="chapter-subtitle">{{ currentChapter.content }}</text>
|
||||
</view>
|
||||
<!-- 文字内容区域 -->
|
||||
<scroll-view
|
||||
class="content-section"
|
||||
scroll-y
|
||||
:scroll-into-view="scrollIntoViewId"
|
||||
>
|
||||
<view id="content-top">
|
||||
<text class="content-text">{{ currentContent }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<view class="progress-section">
|
||||
@@ -126,6 +121,12 @@ const currentChapter = ref<IChapter>({
|
||||
voices: ''
|
||||
})
|
||||
|
||||
// 内容相关
|
||||
const currentContent = ref('') // 当前显示的文字内容
|
||||
const contentDataList = ref<any[]>([]) // 章节内容数据列表
|
||||
const voicesTimeList = ref<number[]>([]) // 音频时间点数组
|
||||
const scrollIntoViewId = ref('') // 滚动控制
|
||||
|
||||
// 音频状态
|
||||
const audioContext = ref<UniApp.InnerAudioContext | null>(null)
|
||||
const isPlaying = ref(false)
|
||||
@@ -200,6 +201,9 @@ function initAudioContext() {
|
||||
isPlaying.value = false
|
||||
})
|
||||
|
||||
let lastUpdateTime = -1
|
||||
let lastContentIndex = -1
|
||||
|
||||
audioContext.value.onTimeUpdate(() => {
|
||||
if (!isChanging.value && audioContext.value) {
|
||||
currentTime.value = audioContext.value.currentTime
|
||||
@@ -207,6 +211,21 @@ function initAudioContext() {
|
||||
if (duration.value > 0) {
|
||||
progress.value = (currentTime.value / duration.value) * 100
|
||||
}
|
||||
|
||||
// 根据音频时间更新文字内容
|
||||
const currentTimeFloor = Math.floor(currentTime.value)
|
||||
if (currentTimeFloor !== lastUpdateTime) {
|
||||
lastUpdateTime = currentTimeFloor
|
||||
|
||||
// 查找当前时间对应的内容索引
|
||||
const contentIndex = voicesTimeList.value.indexOf(currentTimeFloor)
|
||||
if (contentIndex !== -1 && contentIndex !== lastContentIndex) {
|
||||
lastContentIndex = contentIndex
|
||||
if (contentDataList.value[contentIndex]) {
|
||||
currentContent.value = contentDataList.value[contentIndex].content || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -239,25 +258,101 @@ async function loadBookInfo() {
|
||||
// 加载章节列表
|
||||
async function loadChapterList() {
|
||||
try {
|
||||
uni.showLoading({ title: t('common.loading') })
|
||||
|
||||
const res = await bookApi.getBookChapter({
|
||||
bookId: bookId.value
|
||||
})
|
||||
|
||||
console.log('章节列表响应:', res)
|
||||
|
||||
if (res.chapterList && res.chapterList.length > 0) {
|
||||
chapterList.value = res.chapterList
|
||||
|
||||
// 播放当前章节
|
||||
uni.hideLoading()
|
||||
|
||||
// 加载当前章节内容
|
||||
if (currentChapterIndex.value < chapterList.value.length) {
|
||||
playChapter(chapterList.value[currentChapterIndex.value])
|
||||
const currentChapter = chapterList.value[currentChapterIndex.value]
|
||||
console.log('当前章节:', currentChapter)
|
||||
|
||||
await loadChapterContent(currentChapter.id)
|
||||
playChapter(currentChapter)
|
||||
}
|
||||
} else {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '章节列表为空',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('Failed to load chapter list:', error)
|
||||
uni.showToast({
|
||||
title: '加载章节失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加载章节内容(带音频时间点)
|
||||
async function loadChapterContent(chapterId: number) {
|
||||
try {
|
||||
uni.showLoading({ title: t('common.loading') })
|
||||
|
||||
console.log('加载章节内容, chapterId:', chapterId)
|
||||
const res = await bookApi.getChapterContentListen(chapterId)
|
||||
console.log('章节内容响应:', res)
|
||||
|
||||
if (res.bookChapterContents && res.bookChapterContents.length > 0) {
|
||||
contentDataList.value = res.bookChapterContents
|
||||
|
||||
// 提取音频时间点数组
|
||||
voicesTimeList.value = res.bookChapterContents.map((item: any) =>
|
||||
Number(item.voicesStart || 0)
|
||||
)
|
||||
|
||||
console.log('音频时间点数组:', voicesTimeList.value)
|
||||
|
||||
// 显示第一段内容
|
||||
if (res.bookChapterContents[0]) {
|
||||
currentContent.value = res.bookChapterContents[0].content || ''
|
||||
console.log('第一段内容:', currentContent.value.substring(0, 50))
|
||||
}
|
||||
|
||||
// 滚动到顶部
|
||||
scrollToTop()
|
||||
} else {
|
||||
console.log('章节内容为空')
|
||||
currentContent.value = '暂无内容'
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
} catch (error) {
|
||||
console.error('Failed to load chapter content:', error)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '加载内容失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到顶部
|
||||
function scrollToTop() {
|
||||
scrollIntoViewId.value = 'content-top'
|
||||
setTimeout(() => {
|
||||
scrollIntoViewId.value = ''
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 播放章节
|
||||
function playChapter(chapter: IChapter) {
|
||||
console.log('播放章节:', chapter)
|
||||
|
||||
if (!chapter.voices) {
|
||||
console.log('章节没有音频文件')
|
||||
uni.showToast({
|
||||
title: t('book.voices_null'),
|
||||
icon: 'none'
|
||||
@@ -268,9 +363,21 @@ function playChapter(chapter: IChapter) {
|
||||
currentChapter.value = chapter
|
||||
|
||||
if (audioContext.value) {
|
||||
console.log('设置音频源:', chapter.voices)
|
||||
uni.showLoading({ title: t('common.readAudio') })
|
||||
|
||||
audioContext.value.src = chapter.voices
|
||||
audioContext.value.playbackRate = playbackRate.value
|
||||
|
||||
// 监听音频准备就绪
|
||||
audioContext.value.onCanplay(() => {
|
||||
console.log('音频准备就绪')
|
||||
uni.hideLoading()
|
||||
})
|
||||
|
||||
audioContext.value.play()
|
||||
} else {
|
||||
console.log('音频上下文未初始化')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,20 +393,21 @@ function togglePlay() {
|
||||
}
|
||||
|
||||
// 上一章
|
||||
function prevChapter() {
|
||||
async function prevChapter() {
|
||||
if (currentChapterIndex.value > 0) {
|
||||
currentChapterIndex.value--
|
||||
await loadChapterContent(chapterList.value[currentChapterIndex.value].id)
|
||||
playChapter(chapterList.value[currentChapterIndex.value])
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '已经是第一章了',
|
||||
title: t('listen.earlier'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 下一章
|
||||
function nextChapter() {
|
||||
async function nextChapter() {
|
||||
// 检查是否锁定
|
||||
if (isBuy.value === '1' && currentChapterIndex.value + 1 >= count.value) {
|
||||
uni.showModal({
|
||||
@@ -319,10 +427,11 @@ function nextChapter() {
|
||||
|
||||
if (currentChapterIndex.value < chapterList.value.length - 1) {
|
||||
currentChapterIndex.value++
|
||||
await loadChapterContent(chapterList.value[currentChapterIndex.value].id)
|
||||
playChapter(chapterList.value[currentChapterIndex.value])
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '已经是最后一章了',
|
||||
title: t('listen.behind'),
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
@@ -360,6 +469,20 @@ function onProgressChange(e: any) {
|
||||
if (audioContext.value && duration.value > 0) {
|
||||
const newTime = (value / 100) * duration.value
|
||||
audioContext.value.seek(newTime)
|
||||
|
||||
// 更新文字内容
|
||||
const seekTimeFloor = Math.floor(newTime)
|
||||
for (let i = voicesTimeList.value.length - 1; i >= 0; i--) {
|
||||
if (seekTimeFloor >= voicesTimeList.value[i]) {
|
||||
if (contentDataList.value[i]) {
|
||||
currentContent.value = contentDataList.value[i].content || ''
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到顶部
|
||||
scrollToTop()
|
||||
}
|
||||
setTimeout(() => {
|
||||
isChanging.value = false
|
||||
@@ -380,18 +503,30 @@ function changeChapter() {
|
||||
}
|
||||
|
||||
// 选择章节
|
||||
function selectChapter(index: number) {
|
||||
async function selectChapter(index: number) {
|
||||
if (index === currentChapterIndex.value) {
|
||||
showChapterSelect.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否锁定
|
||||
if (isBuy.value === '1' && index >= count.value) {
|
||||
uni.showToast({
|
||||
title: t('book.afterPurchase'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新当前章节索引
|
||||
currentChapterIndex.value = index
|
||||
|
||||
// 关闭弹窗
|
||||
showChapterSelect.value = false
|
||||
|
||||
// 加载章节内容
|
||||
await loadChapterContent(chapterList.value[index].id)
|
||||
|
||||
// 播放选中的章节
|
||||
playChapter(chapterList.value[index])
|
||||
}
|
||||
@@ -408,7 +543,7 @@ function formatTime(seconds: number): string {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.audio-player-page {
|
||||
background: linear-gradient(180deg, #f7faf9 0%, #fff 100%);
|
||||
background: linear-gradient(180deg, #fdfcfb 30%, #eef0cf 100%);
|
||||
min-height: 100vh;
|
||||
|
||||
.player-content {
|
||||
@@ -416,43 +551,20 @@ function formatTime(seconds: number): string {
|
||||
flex-direction: column;
|
||||
padding: 40rpx 40rpx;
|
||||
|
||||
.cover-section {
|
||||
.content-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 600rpx;
|
||||
margin-bottom: 60rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 20rpx;
|
||||
|
||||
.cover-image {
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
&.rotating {
|
||||
animation: rotate 20s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chapter-info {
|
||||
text-align: center;
|
||||
margin-bottom: 60rpx;
|
||||
|
||||
.chapter-title {
|
||||
.content-text {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
font-size: 36rpx;
|
||||
line-height: 60rpx;
|
||||
color: #333;
|
||||
line-height: 50rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.chapter-subtitle {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 36rpx;
|
||||
letter-spacing: 0.1rem;
|
||||
text-align: justify;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user