更新:图书搜索功能;听书实时显示对应文字;书籍评论功能完善;
This commit is contained in:
@@ -180,7 +180,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { homeApi } from '@/api/modules/home'
|
||||
import { homeApi } from '@/api/modules/book_home'
|
||||
import type {
|
||||
IBook,
|
||||
IBookWithStats,
|
||||
@@ -488,7 +488,6 @@ onShow(() => {
|
||||
background-size: cover;
|
||||
padding: 30rpx;
|
||||
position: relative;
|
||||
height: 340rpx;
|
||||
|
||||
.icon-hua {
|
||||
width: 100%;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<wd-popup v-model="commentVisible" position="bottom">
|
||||
<view class="comment-dialog">
|
||||
<text class="dialog-title">
|
||||
{{ replyTarget ? $t('bookDetails.reply') + replyTarget.name + $t('bookDetails.dpl') : $t('bookDetails.makeComment') }}
|
||||
{{ replyTarget ? $t('bookDetails.reply') + '"' + replyTarget.name + '"' + $t('bookDetails.dpl') : $t('bookDetails.makeComment') }}
|
||||
</text>
|
||||
|
||||
<!-- 富文本编辑器 -->
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
<!-- 这里需要一个Emoji选择器组件,暂时简化处理 -->
|
||||
<view v-if="showEmoji" class="emoji-picker">
|
||||
<text class="emoji-tip">Emoji功能待集成</text>
|
||||
<EmotionPicker @emotion="handleEmj" :height="220"></EmotionPicker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -95,8 +95,8 @@ import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { bookApi } from '@/api/modules/book'
|
||||
import type { IBookDetail, IComment } from '@/types/book'
|
||||
import CustomNavbar from '@/components/book/CustomNavbar.vue'
|
||||
import CommentList from '@/components/book/CommentList.vue'
|
||||
import EmotionPicker from '@/components/bkhumor-emojiplus/index.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -220,7 +220,7 @@ function showCommentDialog(comment?: IComment) {
|
||||
if (comment) {
|
||||
const userName = comment.userEntity.nickname || comment.userEntity.name || 'TA'
|
||||
replyTarget.value = { id: comment.id, name: userName }
|
||||
placeholder.value = t('bookDetails.replyText') + userName + t('bookDetails.dpl')
|
||||
placeholder.value = t('bookDetails.replyText') + '"' + userName + '"' + t('bookDetails.dpl')
|
||||
} else {
|
||||
replyTarget.value = null
|
||||
placeholder.value = t('bookDetails.enterText')
|
||||
@@ -266,6 +266,16 @@ function getEditorContent(): Promise<string> {
|
||||
})
|
||||
}
|
||||
|
||||
//获得输入的表情数组
|
||||
function handleEmj(i: any) {
|
||||
editorCtx.value.insertImage({
|
||||
src: i.emotion,
|
||||
alt: "emoji",
|
||||
className: 'emoji_image',
|
||||
success: function() {},
|
||||
});
|
||||
}
|
||||
|
||||
// 提交评论
|
||||
async function submitComment() {
|
||||
try {
|
||||
@@ -465,9 +475,17 @@ function toggleEmoji() {
|
||||
border: 1rpx solid #ddd;
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
height: 200rpx;
|
||||
padding: 10rpx;
|
||||
border-radius: 10rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
:deep() .ql-editor {
|
||||
img {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-section {
|
||||
@@ -482,8 +500,8 @@ function toggleEmoji() {
|
||||
padding-top: 20rpx;
|
||||
|
||||
image {
|
||||
width: 45rpx;
|
||||
height: 45rpx;
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
display: block;
|
||||
margin: 0 auto 5rpx;
|
||||
}
|
||||
@@ -498,10 +516,7 @@ function toggleEmoji() {
|
||||
|
||||
.emoji-picker {
|
||||
margin-top: 20rpx;
|
||||
padding: 20rpx;
|
||||
background: #f4f5f7;
|
||||
border-radius: 10rpx;
|
||||
|
||||
|
||||
.emoji-tip {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { homeApi } from '@/api/modules/home'
|
||||
import { homeApi } from '@/api/modules/book_home'
|
||||
import type { IBookWithStats, IVipInfo } from '@/types/home'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -93,7 +93,7 @@ const handleSearch = async () => {
|
||||
|
||||
try {
|
||||
const res = await homeApi.searchBooks({
|
||||
key: keyword.value.trim(),
|
||||
title: keyword.value.trim(),
|
||||
page: 1,
|
||||
limit: 10,
|
||||
})
|
||||
@@ -196,6 +196,7 @@ onMounted(async () => {
|
||||
:deep() {
|
||||
.wd-search {
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user