| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566 |
- <template>
- <view class="container">
- <!-- 顶部导航栏 -->
- <view class="header">
- <view class="back-btn" @click="goBack">
- <text class="back-icon">↓</text>
- </view>
- <view class="share-btn" @click="handleShare">
- <text class="share-icon">↗</text>
- </view>
- </view>
-
- <!-- 书籍信息 -->
- <view class="book-info-section">
- <image class="book-cover" :src="bookInfo.image" mode="aspectFill"></image>
- <text class="book-title">{{ bookInfo.title }}</text>
- <text class="chapter-title" v-if="chapterInfo.title">{{ chapterInfo.title }}</text>
- <text class="book-author">{{ bookInfo.author }}</text>
- <button class="add-to-shelf-btn" @click="handleAddToShelf">加入书架</button>
- </view>
-
- <!-- 播放控制区域 -->
- <view class="player-section">
- <!-- 进度条 -->
- <view class="progress-section">
- <view class="progress-bar-wrapper" @touchstart="handleProgressTouchStart" @touchmove="handleProgressTouchMove" @touchend="handleProgressTouchEnd">
- <view class="progress-bar">
- <view class="progress-filled" :style="{ width: progressPercent + '%' }"></view>
- <view class="progress-dot" :style="{ left: progressPercent + '%' }" v-if="!isDragging"></view>
- </view>
- </view>
- <view class="time-label">
- <text class="time-text">{{ currentTime }}/{{ totalTime }}</text>
- </view>
- </view>
-
- <!-- 控制按钮 -->
- <view class="controls-section">
- <view class="control-btn" @click="rewind15s">
- <text class="control-text">-15s</text>
- </view>
- <view class="control-btn prev-btn" @click="playPrevious">
- <text class="control-icon">⏮</text>
- </view>
- <view class="control-btn play-btn" @click="togglePlay">
- <text class="play-icon">{{ isPlaying ? '⏸' : '▶' }}</text>
- </view>
- <view class="control-btn next-btn" @click="playNext">
- <text class="control-icon">⏭</text>
- </view>
- <view class="control-btn" @click="forward15s">
- <text class="control-text">+15s</text>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script>
- import { getChapterDetail, playChapter, saveListeningProgress, getListeningProgress, recordListeningHistory } from '../../utils/api.js'
-
- export default {
- data() {
- return {
- audiobookId: null,
- chapterId: null,
- bookInfo: {
- id: null,
- title: '',
- author: '',
- image: ''
- },
- chapterInfo: {
- id: null,
- title: '',
- audioUrl: '',
- duration: 0
- },
- isPlaying: false,
- currentTime: '00:00',
- currentSeconds: 0,
- totalTime: '00:00',
- totalSeconds: 0,
- progressPercent: 0,
- audioContext: null,
- playTimer: null,
- progressSaveTimer: null,
- userInfo: null,
- isDragging: false
- }
- },
- onLoad(options) {
- // 从路由参数获取信息
- if (options.audiobookId) {
- this.audiobookId = parseInt(options.audiobookId)
- } else if (options.bookId) {
- this.audiobookId = parseInt(options.bookId)
- }
-
- if (options.title) {
- this.bookInfo.title = decodeURIComponent(options.title)
- }
- if (options.image) {
- this.bookInfo.image = decodeURIComponent(options.image)
- }
- if (options.author) {
- this.bookInfo.author = decodeURIComponent(options.author)
- }
- if (options.chapterId) {
- this.chapterId = parseInt(options.chapterId)
- }
- if (options.audioUrl) {
- this.chapterInfo.audioUrl = decodeURIComponent(options.audioUrl)
- }
-
- // 加载用户信息
- this.loadUserInfo()
-
- // 初始化播放器
- if (this.chapterId) {
- this.initPlayer()
- }
- },
- onUnload() {
- // 页面卸载时保存进度并停止播放
- this.saveProgress()
- if (this.audioContext) {
- this.audioContext.destroy()
- }
- if (this.playTimer) {
- clearInterval(this.playTimer)
- }
- if (this.progressSaveTimer) {
- clearInterval(this.progressSaveTimer)
- }
- },
- methods: {
- loadUserInfo() {
- try {
- const userInfo = uni.getStorageSync('userInfo')
- const isLogin = uni.getStorageSync('isLogin')
- if (userInfo && userInfo.id && isLogin) {
- this.userInfo = userInfo
- }
- } catch (e) {
- console.error('获取用户信息失败:', e)
- }
- },
- async initPlayer() {
- try {
- // 加载章节详情
- if (this.chapterId) {
- const res = await getChapterDetail(this.chapterId)
- if (res && res.code === 200 && res.data) {
- this.chapterInfo = {
- id: res.data.id,
- title: res.data.title || '',
- audioUrl: res.data.audioUrl || this.chapterInfo.audioUrl,
- duration: res.data.duration || 0
- }
- this.totalSeconds = this.chapterInfo.duration
- this.totalTime = this.formatTime(this.totalSeconds)
- }
- }
-
- // 加载播放进度
- if (this.userInfo && this.userInfo.id && this.chapterId) {
- const progressRes = await getListeningProgress(this.userInfo.id, this.chapterId)
- if (progressRes && progressRes.code === 200 && progressRes.data) {
- const progress = progressRes.data
- this.currentSeconds = progress.currentPosition || 0
- this.currentTime = this.formatTime(this.currentSeconds)
- this.progressPercent = this.totalSeconds > 0 ? (this.currentSeconds / this.totalSeconds) * 100 : 0
- }
- }
-
- // 创建音频上下文
- this.audioContext = uni.createInnerAudioContext()
- this.audioContext.src = this.chapterInfo.audioUrl
- this.audioContext.startTime = this.currentSeconds
-
- // 监听播放事件
- this.audioContext.onPlay(() => {
- this.isPlaying = true
- this.startProgressTimer()
- })
-
- this.audioContext.onPause(() => {
- this.isPlaying = false
- this.stopProgressTimer()
- })
-
- this.audioContext.onEnded(() => {
- this.isPlaying = false
- this.stopProgressTimer()
- // 播放结束,自动播放下一章
- // this.playNext()
- })
-
- this.audioContext.onTimeUpdate(() => {
- if (!this.isDragging) {
- this.currentSeconds = Math.floor(this.audioContext.currentTime)
- this.currentTime = this.formatTime(this.currentSeconds)
- if (this.totalSeconds > 0) {
- this.progressPercent = (this.currentSeconds / this.totalSeconds) * 100
- }
- }
- })
-
- // 记录播放(增加播放次数)
- if (this.audiobookId && this.chapterId) {
- try {
- await playChapter(this.audiobookId, this.chapterId)
- } catch (e) {
- console.error('记录播放失败:', e)
- }
- }
-
- // 记录听书历史
- if (this.userInfo && this.userInfo.id && this.audiobookId && this.chapterId) {
- try {
- await recordListeningHistory(this.userInfo.id, this.audiobookId, this.chapterId)
- } catch (e) {
- console.error('记录听书历史失败:', e)
- }
- }
-
- // 启动进度保存定时器(每30秒保存一次)
- this.progressSaveTimer = setInterval(() => {
- this.saveProgress()
- }, 30000)
-
- } catch (e) {
- console.error('初始化播放器失败:', e)
- uni.showToast({
- title: '加载失败,请重试',
- icon: 'none'
- })
- }
- },
- goBack() {
- this.saveProgress()
- uni.navigateBack()
- },
- handleShare() {
- uni.showToast({
- title: '分享',
- icon: 'none'
- })
- },
- handleAddToShelf() {
- uni.showToast({
- title: '已加入书架',
- icon: 'success'
- })
- },
- togglePlay() {
- if (!this.audioContext) {
- uni.showToast({
- title: '播放器未初始化',
- icon: 'none'
- })
- return
- }
-
- if (this.isPlaying) {
- this.audioContext.pause()
- } else {
- this.audioContext.play()
- }
- },
- startProgressTimer() {
- // 进度更新已通过audioContext.onTimeUpdate处理
- },
- stopProgressTimer() {
- // 进度更新已通过audioContext.onTimeUpdate处理
- },
- playPrevious() {
- uni.showToast({
- title: '上一章功能待实现',
- icon: 'none'
- })
- },
- playNext() {
- uni.showToast({
- title: '下一章功能待实现',
- icon: 'none'
- })
- },
- rewind15s() {
- if (this.audioContext) {
- const newTime = Math.max(0, this.currentSeconds - 15)
- this.audioContext.seek(newTime)
- this.currentSeconds = newTime
- this.currentTime = this.formatTime(newTime)
- if (this.totalSeconds > 0) {
- this.progressPercent = (newTime / this.totalSeconds) * 100
- }
- }
- },
- forward15s() {
- if (this.audioContext) {
- const newTime = Math.min(this.totalSeconds, this.currentSeconds + 15)
- this.audioContext.seek(newTime)
- this.currentSeconds = newTime
- this.currentTime = this.formatTime(newTime)
- if (this.totalSeconds > 0) {
- this.progressPercent = (newTime / this.totalSeconds) * 100
- }
- }
- },
- handleProgressTouchStart(e) {
- this.isDragging = true
- },
- handleProgressTouchMove(e) {
- // 拖动进度条的处理
- // 这里需要获取进度条的宽度和触摸位置
- },
- async handleProgressTouchEnd(e) {
- this.isDragging = false
- // 结束拖动,跳转到指定位置
- // 这里需要计算拖动的百分比,然后设置播放位置
- if (this.audioContext && this.totalSeconds > 0) {
- // 这里应该根据触摸位置计算新的播放时间
- // 暂时使用progressPercent
- const newTime = Math.floor((this.progressPercent / 100) * this.totalSeconds)
- this.audioContext.seek(newTime)
- this.currentSeconds = newTime
- this.currentTime = this.formatTime(newTime)
- await this.saveProgress()
- }
- },
- async saveProgress() {
- if (this.userInfo && this.userInfo.id && this.audiobookId && this.chapterId) {
- try {
- await saveListeningProgress(
- this.userInfo.id,
- this.audiobookId,
- this.chapterId,
- this.currentSeconds,
- this.totalSeconds
- )
- } catch (e) {
- console.error('保存进度失败:', e)
- }
- }
- },
- formatTime(seconds) {
- if (!seconds || seconds < 0) {
- return '00:00'
- }
- const hours = Math.floor(seconds / 3600)
- const mins = Math.floor((seconds % 3600) / 60)
- const secs = seconds % 60
-
- if (hours > 0) {
- return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
- } else {
- return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
- }
- }
- }
- }
- </script>
- <style scoped>
- .container {
- width: 100%;
- height: 100vh;
- background-color: #F5F5F5;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: space-between;
- padding: 40rpx 30rpx 80rpx;
- padding-top: calc(30px + 40rpx);
- box-sizing: border-box;
- box-sizing: border-box;
- }
-
- .header {
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 20rpx 0;
- }
-
- .back-btn {
- width: 60rpx;
- height: 60rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .back-icon {
- font-size: 36rpx;
- color: #666666;
- }
-
- .share-btn {
- width: 60rpx;
- height: 60rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .share-icon {
- font-size: 36rpx;
- color: #666666;
- }
-
- .book-info-section {
- display: flex;
- flex-direction: column;
- align-items: center;
- flex: 1;
- justify-content: center;
- }
-
- .book-cover {
- width: 400rpx;
- height: 560rpx;
- border-radius: 8rpx;
- margin-bottom: 40rpx;
- box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.15);
- background-color: #F5F5F5;
- }
-
- .book-title {
- font-size: 40rpx;
- font-weight: bold;
- color: #333333;
- margin-bottom: 20rpx;
- text-align: center;
- }
-
- .book-author {
- font-size: 28rpx;
- color: #999999;
- margin-bottom: 20rpx;
- text-align: center;
- }
-
- .chapter-title {
- font-size: 26rpx;
- color: #666666;
- margin-bottom: 20rpx;
- text-align: center;
- }
-
- .add-to-shelf-btn {
- width: 300rpx;
- height: 80rpx;
- background-color: #F5F5F5;
- color: #333333;
- font-size: 28rpx;
- border: none;
- border-radius: 40rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .add-to-shelf-btn::after {
- border: none;
- }
-
- .player-section {
- width: 100%;
- padding-bottom: env(safe-area-inset-bottom);
- }
-
- .progress-section {
- margin-bottom: 60rpx;
- width: 100%;
- }
-
- .progress-bar-wrapper {
- width: 100%;
- height: 60rpx;
- display: flex;
- align-items: center;
- position: relative;
- margin-bottom: 20rpx;
- }
-
- .time-label {
- display: flex;
- justify-content: center;
- }
-
- .time-text {
- background-color: #4FC3F7;
- color: #FFFFFF;
- font-size: 24rpx;
- padding: 8rpx 20rpx;
- border-radius: 20rpx;
- }
-
- .progress-bar {
- width: 100%;
- height: 4rpx;
- background-color: #E0E0E0;
- border-radius: 2rpx;
- position: relative;
- }
-
- .progress-filled {
- height: 100%;
- background-color: #4FC3F7;
- border-radius: 2rpx;
- transition: width 0.3s ease;
- }
-
- .progress-dot {
- position: absolute;
- top: 50%;
- transform: translate(-50%, -50%);
- width: 24rpx;
- height: 24rpx;
- background-color: #4FC3F7;
- border-radius: 50%;
- box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.2);
- }
-
- .controls-section {
- display: flex;
- justify-content: space-around;
- align-items: center;
- padding: 0 40rpx;
- }
-
- .control-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .control-text {
- font-size: 28rpx;
- color: #999999;
- }
-
- .control-icon {
- font-size: 40rpx;
- color: #333333;
- }
-
- .play-btn {
- width: 120rpx;
- height: 120rpx;
- background-color: #4FC3F7;
- border-radius: 50%;
- box-shadow: 0 4rpx 12rpx rgba(79, 195, 247, 0.4);
- }
-
- .play-icon {
- font-size: 60rpx;
- color: #FFFFFF;
- margin-left: 6rpx;
- }
- </style>
|