更新:图书搜索功能;听书实时显示对应文字;书籍评论功能完善;

This commit is contained in:
2025-11-12 09:02:30 +08:00
parent 9fcc1b8549
commit 1da75a59f2
21 changed files with 484 additions and 828 deletions

View File

@@ -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;
}
}