| <template> | |
|     <view class="article-detail"> | |
|         <!-- 导航栏 --> | |
|         <!-- #ifndef H5 --> | |
|         <uv-navbar :placeholder="true" left-icon="arrow-left" :title="articleData.title || '文章详情'" | |
|             @leftClick="goBack"></uv-navbar> | |
|         <!-- #endif --> | |
| 
 | |
|         <!-- 文章内容 --> | |
|         <view class="article-container" v-if="articleData.id"> | |
|             <!-- 文章标题 --> | |
|             <view class="article-title">{{ articleData.title }}</view> | |
| 
 | |
|             <!-- 文章信息 --> | |
|             <view class="article-info"> | |
|                 <text class="article-time">{{ formatTime(articleData.createTime) }}</text> | |
|                 <!-- <text class="article-author">{{ articleData.createBy }}</text> --> | |
|             </view> | |
| 
 | |
|             <!-- 文章内容 --> | |
|             <view class="article-content"> | |
|                 <uv-parse :content="articleData.content"/> | |
|             </view> | |
|         </view> | |
| 
 | |
|         <!-- 加载状态 --> | |
|         <view class="loading-container" v-else-if="loading"> | |
|             <uv-loading-icon mode="circle"></uv-loading-icon> | |
|             <text class="loading-text">加载中...</text> | |
|         </view> | |
| 
 | |
|         <!-- 音频播放悬浮按钮 --> | |
|         <view class="audio-float-button" v-if="hasAudio && articleData.id" @click="toggleAudio"> | |
|             <uv-icon :name="isPlaying ? 'pause-circle-fill' : 'play-circle-fill'" size="50" | |
|                 :color="isPlaying ? '#ff6b6b' : '#4CAF50'"></uv-icon> | |
|         </view> | |
| 
 | |
|     </view> | |
| </template> | |
| 
 | |
| <script> | |
| import audioManager from '@/utils/audioManager.js' | |
| 
 | |
| export default { | |
|     data() { | |
|         return { | |
|             articleId: '', | |
|             articleData: {}, | |
|             loading: true, | |
|             isPlaying: false, | |
|             audioUrl: '', | |
|             hasAudio: false | |
|         } | |
|     }, | |
| 
 | |
|     onLoad(options) { | |
|         if (options.id) { | |
|             this.articleId = options.id | |
|             this.getArticleDetail() | |
|         } | |
|          | |
|         // 绑定音频管理器事件监听 | |
|         this.bindAudioEvents() | |
|     }, | |
| 
 | |
|     onUnload() { | |
|         // 页面卸载时清理音频资源和事件监听 | |
|         this.stopAudio() | |
|         this.unbindAudioEvents() | |
|     }, | |
| 
 | |
|     methods: { | |
|         // 绑定音频管理器事件监听 | |
|         bindAudioEvents() { | |
|             audioManager.on('play', this.onAudioPlay) | |
|             audioManager.on('pause', this.onAudioPause) | |
|             audioManager.on('ended', this.onAudioEnded) | |
|             audioManager.on('error', this.onAudioError) | |
|         }, | |
| 
 | |
|         // 解绑音频管理器事件监听 | |
|         unbindAudioEvents() { | |
|             audioManager.off('play', this.onAudioPlay) | |
|             audioManager.off('pause', this.onAudioPause) | |
|             audioManager.off('ended', this.onAudioEnded) | |
|             audioManager.off('error', this.onAudioError) | |
|         }, | |
| 
 | |
|         // 音频播放开始事件 | |
|         onAudioPlay(data) { | |
|             if (data.audioType === 'article') { | |
|                 this.isPlaying = true | |
|                 console.log('文章音频开始播放') | |
|             } | |
|         }, | |
| 
 | |
|         // 音频暂停事件 | |
|         onAudioPause(data) { | |
|             if (data.audioType === 'article') { | |
|                 this.isPlaying = false | |
|                 console.log('文章音频暂停播放') | |
|             } | |
|         }, | |
| 
 | |
|         // 音频播放结束事件 | |
|         onAudioEnded(data) { | |
|             if (data.audioType === 'article') { | |
|                 this.isPlaying = false | |
|                 console.log('文章音频播放结束') | |
|             } | |
|         }, | |
| 
 | |
|         // 音频播放错误事件 | |
|         onAudioError(data) { | |
|             if (data.audioType === 'article') { | |
|                 this.isPlaying = false | |
|                 console.error('文章音频播放错误:', data.error) | |
|                 uni.showToast({ | |
|                     title: '音频播放失败', | |
|                     icon: 'none' | |
|                 }) | |
|             } | |
|         }, | |
| 
 | |
|         // 返回上一页 | |
|         goBack() { | |
|             uni.navigateBack() | |
|         }, | |
| 
 | |
|         // 获取文章详情 | |
|         async getArticleDetail() { | |
|             try { | |
|                 this.loading = true | |
|                 const res = await this.$api.home.getArticleDetail({ id: this.articleId }) | |
| 
 | |
|                 if (res.code === 200) { | |
|                     this.articleData = res.result | |
| 
 | |
|                     // #ifdef H5 | |
|                     window.document.title = this.articleData.title || '文章详情' | |
|                     // #endif | |
|  | |
|                     // 处理音频数据 | |
|                     if (res.result.audios && Object.keys(res.result.audios).length > 0) { | |
|                         // 获取第一个音频URL | |
|                         const audioKeys = Object.keys(res.result.audios) | |
|                         this.audioUrl = res.result.audios[audioKeys[0]] | |
|                         this.hasAudio = true | |
|                     } | |
|                 } else { | |
|                     uni.showToast({ | |
|                         title: res.message || '获取文章详情失败', | |
|                         icon: 'none' | |
|                     }) | |
|                 } | |
|             } catch (error) { | |
|                 console.error('获取文章详情失败:', error) | |
|                 uni.showToast({ | |
|                     title: '网络错误,请重试', | |
|                     icon: 'none' | |
|                 }) | |
|             } finally { | |
|                 this.loading = false | |
|             } | |
|         }, | |
| 
 | |
|         // 切换音频播放状态 | |
|         toggleAudio() { | |
|             if (!this.audioUrl) { | |
|                 console.error('音频URL为空') | |
|                 return | |
|             } | |
| 
 | |
|             try { | |
|                 if (this.isPlaying) { | |
|                     // 暂停音频 | |
|                     this.pauseAudio() | |
|                 } else { | |
|                     // 播放音频 | |
|                     this.playAudio() | |
|                 } | |
|             } catch (error) { | |
|                 console.error('音频播放切换异常:', error) | |
|                 this.isPlaying = false | |
|             } | |
|         }, | |
| 
 | |
|         // 播放音频 | |
|         async playAudio() { | |
|             if (!this.audioUrl) { | |
|                 console.error('音频URL为空') | |
|                 return | |
|             } | |
| 
 | |
|             try { | |
|                 // 使用audioManager播放音频,指定音频类型为'article' | |
|                 await audioManager.playAudio(this.audioUrl, 'article') | |
|                 console.log('开始播放文章音频:', this.audioUrl) | |
|             } catch (error) { | |
|                 console.error('音频播放异常:', error) | |
|                 this.isPlaying = false | |
|                 uni.showToast({ | |
|                     title: '音频播放失败', | |
|                     icon: 'none' | |
|                 }) | |
|             } | |
|         }, | |
| 
 | |
|         // 暂停音频 | |
|         pauseAudio() { | |
|             try { | |
|                 audioManager.pause() | |
|                 console.log('暂停文章音频') | |
|             } catch (error) { | |
|                 console.error('暂停音频失败:', error) | |
|             } | |
|         }, | |
| 
 | |
|         // 停止音频 | |
|         stopAudio() { | |
|             try { | |
|                 audioManager.stopCurrentAudio() | |
|                 this.isPlaying = false | |
|                 console.log('停止文章音频') | |
|             } catch (error) { | |
|                 console.error('停止音频失败:', error) | |
|             } | |
|         }, | |
| 
 | |
|         // 格式化时间 | |
|         formatTime(timeStr) { | |
|             if (!timeStr) return '' | |
| 
 | |
|             // 处理iOS兼容性问题,将 "2025-10-23 16:36:43" 格式转换为 "2025/10/23 16:36:43" | |
|             let formattedTimeStr = timeStr.replace(/-/g, '/') | |
| 
 | |
|             const date = new Date(formattedTimeStr) | |
| 
 | |
|             // 检查日期是否有效 | |
|             if (isNaN(date.getTime())) { | |
|                 console.warn('Invalid date format:', timeStr) | |
|                 return timeStr // 返回原始字符串 | |
|             } | |
| 
 | |
|             const year = date.getFullYear() | |
|             const month = String(date.getMonth() + 1).padStart(2, '0') | |
|             const day = String(date.getDate()).padStart(2, '0') | |
|             return `${year}-${month}-${day}` | |
|         } | |
|     } | |
| } | |
| </script> | |
| 
 | |
| <style lang="scss" scoped> | |
| .article-detail { | |
|     min-height: 100vh; | |
|     background: #fff; | |
| } | |
| 
 | |
| .article-container { | |
|     padding: 30rpx; | |
| } | |
| 
 | |
| .article-title { | |
|     font-size: 40rpx; | |
|     font-weight: 600; | |
|     color: #333; | |
|     line-height: 1.4; | |
|     margin-bottom: 30rpx; | |
| } | |
| 
 | |
| .article-info { | |
|     display: flex; | |
|     align-items: center; | |
|     gap: 30rpx; | |
|     margin-bottom: 40rpx; | |
|     padding-bottom: 30rpx; | |
|     border-bottom: 1px solid #f0f0f0; | |
| 
 | |
|     .article-time { | |
|         font-size: 26rpx; | |
|         color: #999; | |
|     } | |
| 
 | |
|     .article-author { | |
|         font-size: 26rpx; | |
|         color: #666; | |
| 
 | |
|         &::before { | |
|             content: '作者:'; | |
|         } | |
|     } | |
| } | |
| 
 | |
| .article-content { | |
|     font-size: 32rpx; | |
|     line-height: 1.8; | |
|     color: #333; | |
| 
 | |
|     // 富文本内容样式 | |
|     :deep(.rich-text) { | |
|         p { | |
|             margin-bottom: 20rpx; | |
|             line-height: 1.8; | |
|         } | |
| 
 | |
|         img { | |
|             max-width: 100%; | |
|             height: auto; | |
|             border-radius: 8rpx; | |
|             margin: 20rpx 0; | |
|         } | |
| 
 | |
|         section { | |
|             margin: 20rpx 0; | |
|         } | |
|     } | |
| } | |
| 
 | |
| .loading-container { | |
|     display: flex; | |
|     flex-direction: column; | |
|     align-items: center; | |
|     justify-content: center; | |
|     height: 400rpx; | |
| 
 | |
|     .loading-text { | |
|         margin-top: 20rpx; | |
|         font-size: 28rpx; | |
|         color: #999; | |
|     } | |
| } | |
| 
 | |
| // 音频播放悬浮按钮 | |
| .audio-float-button { | |
|     position: fixed; | |
|     right: 30rpx; | |
|     bottom: 100rpx; | |
|     width: 100rpx; | |
|     height: 100rpx; | |
|     border-radius: 50%; | |
|     background: rgba(255, 255, 255, 0.9); | |
|     box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); | |
|     display: flex; | |
|     align-items: center; | |
|     justify-content: center; | |
|     z-index: 999; | |
|     transition: all 0.3s ease; | |
| 
 | |
|     &:active { | |
|         transform: scale(0.95); | |
|     } | |
| 
 | |
|     // 添加呼吸动画效果 | |
|     &::before { | |
|         content: ''; | |
|         position: absolute; | |
|         top: -10rpx; | |
|         left: -10rpx; | |
|         right: -10rpx; | |
|         bottom: -10rpx; | |
|         border-radius: 50%; | |
|         background: rgba(76, 175, 80, 0.2); | |
|         animation: pulse 2s infinite; | |
|         z-index: -1; | |
|     } | |
| } | |
| 
 | |
| @keyframes pulse { | |
|     0% { | |
|         transform: scale(1); | |
|         opacity: 1; | |
|     } | |
| 
 | |
|     50% { | |
|         transform: scale(1.1); | |
|         opacity: 0.7; | |
|     } | |
| 
 | |
|     100% { | |
|         transform: scale(1); | |
|         opacity: 1; | |
|     } | |
| } | |
| </style> |