四零语境前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

384 lines
10 KiB

  1. <template>
  2. <view class="article-detail">
  3. <!-- 导航栏 -->
  4. <!-- #ifndef H5 -->
  5. <uv-navbar :placeholder="true" left-icon="arrow-left" :title="articleData.title || '文章详情'"
  6. @leftClick="goBack"></uv-navbar>
  7. <!-- #endif -->
  8. <!-- 文章内容 -->
  9. <view class="article-container" v-if="articleData.id">
  10. <!-- 文章标题 -->
  11. <view class="article-title">{{ articleData.title }}</view>
  12. <!-- 文章信息 -->
  13. <view class="article-info">
  14. <text class="article-time">{{ formatTime(articleData.createTime) }}</text>
  15. <!-- <text class="article-author">{{ articleData.createBy }}</text> -->
  16. </view>
  17. <!-- 文章内容 -->
  18. <view class="article-content">
  19. <uv-parse :content="articleData.content"/>
  20. </view>
  21. </view>
  22. <!-- 加载状态 -->
  23. <view class="loading-container" v-else-if="loading">
  24. <uv-loading-icon mode="circle"></uv-loading-icon>
  25. <text class="loading-text">加载中...</text>
  26. </view>
  27. <!-- 音频播放悬浮按钮 -->
  28. <view class="audio-float-button" v-if="hasAudio && articleData.id" @click="toggleAudio">
  29. <uv-icon :name="isPlaying ? 'pause-circle-fill' : 'play-circle-fill'" size="50"
  30. :color="isPlaying ? '#ff6b6b' : '#4CAF50'"></uv-icon>
  31. </view>
  32. </view>
  33. </template>
  34. <script>
  35. import audioManager from '@/utils/audioManager.js'
  36. export default {
  37. data() {
  38. return {
  39. articleId: '',
  40. articleData: {},
  41. loading: true,
  42. isPlaying: false,
  43. audioUrl: '',
  44. hasAudio: false
  45. }
  46. },
  47. onLoad(options) {
  48. if (options.id) {
  49. this.articleId = options.id
  50. this.getArticleDetail()
  51. }
  52. // 绑定音频管理器事件监听
  53. this.bindAudioEvents()
  54. },
  55. onUnload() {
  56. // 页面卸载时清理音频资源和事件监听
  57. this.stopAudio()
  58. this.unbindAudioEvents()
  59. },
  60. methods: {
  61. // 绑定音频管理器事件监听
  62. bindAudioEvents() {
  63. audioManager.on('play', this.onAudioPlay)
  64. audioManager.on('pause', this.onAudioPause)
  65. audioManager.on('ended', this.onAudioEnded)
  66. audioManager.on('error', this.onAudioError)
  67. },
  68. // 解绑音频管理器事件监听
  69. unbindAudioEvents() {
  70. audioManager.off('play', this.onAudioPlay)
  71. audioManager.off('pause', this.onAudioPause)
  72. audioManager.off('ended', this.onAudioEnded)
  73. audioManager.off('error', this.onAudioError)
  74. },
  75. // 音频播放开始事件
  76. onAudioPlay(data) {
  77. if (data.audioType === 'article') {
  78. this.isPlaying = true
  79. console.log('文章音频开始播放')
  80. }
  81. },
  82. // 音频暂停事件
  83. onAudioPause(data) {
  84. if (data.audioType === 'article') {
  85. this.isPlaying = false
  86. console.log('文章音频暂停播放')
  87. }
  88. },
  89. // 音频播放结束事件
  90. onAudioEnded(data) {
  91. if (data.audioType === 'article') {
  92. this.isPlaying = false
  93. console.log('文章音频播放结束')
  94. }
  95. },
  96. // 音频播放错误事件
  97. onAudioError(data) {
  98. if (data.audioType === 'article') {
  99. this.isPlaying = false
  100. console.error('文章音频播放错误:', data.error)
  101. uni.showToast({
  102. title: '音频播放失败',
  103. icon: 'none'
  104. })
  105. }
  106. },
  107. // 返回上一页
  108. goBack() {
  109. uni.navigateBack()
  110. },
  111. // 获取文章详情
  112. async getArticleDetail() {
  113. try {
  114. this.loading = true
  115. const res = await this.$api.home.getArticleDetail({ id: this.articleId })
  116. if (res.code === 200) {
  117. this.articleData = res.result
  118. // #ifdef H5
  119. window.document.title = this.articleData.title || '文章详情'
  120. // #endif
  121. // 处理音频数据
  122. if (res.result.audios && Object.keys(res.result.audios).length > 0) {
  123. // 获取第一个音频URL
  124. const audioKeys = Object.keys(res.result.audios)
  125. this.audioUrl = res.result.audios[audioKeys[0]]
  126. this.hasAudio = true
  127. }
  128. } else {
  129. uni.showToast({
  130. title: res.message || '获取文章详情失败',
  131. icon: 'none'
  132. })
  133. }
  134. } catch (error) {
  135. console.error('获取文章详情失败:', error)
  136. uni.showToast({
  137. title: '网络错误,请重试',
  138. icon: 'none'
  139. })
  140. } finally {
  141. this.loading = false
  142. }
  143. },
  144. // 切换音频播放状态
  145. toggleAudio() {
  146. if (!this.audioUrl) {
  147. console.error('音频URL为空')
  148. return
  149. }
  150. try {
  151. if (this.isPlaying) {
  152. // 暂停音频
  153. this.pauseAudio()
  154. } else {
  155. // 播放音频
  156. this.playAudio()
  157. }
  158. } catch (error) {
  159. console.error('音频播放切换异常:', error)
  160. this.isPlaying = false
  161. }
  162. },
  163. // 播放音频
  164. async playAudio() {
  165. if (!this.audioUrl) {
  166. console.error('音频URL为空')
  167. return
  168. }
  169. try {
  170. // 使用audioManager播放音频,指定音频类型为'article'
  171. await audioManager.playAudio(this.audioUrl, 'article')
  172. console.log('开始播放文章音频:', this.audioUrl)
  173. } catch (error) {
  174. console.error('音频播放异常:', error)
  175. this.isPlaying = false
  176. uni.showToast({
  177. title: '音频播放失败',
  178. icon: 'none'
  179. })
  180. }
  181. },
  182. // 暂停音频
  183. pauseAudio() {
  184. try {
  185. audioManager.pause()
  186. console.log('暂停文章音频')
  187. } catch (error) {
  188. console.error('暂停音频失败:', error)
  189. }
  190. },
  191. // 停止音频
  192. stopAudio() {
  193. try {
  194. audioManager.stopCurrentAudio()
  195. this.isPlaying = false
  196. console.log('停止文章音频')
  197. } catch (error) {
  198. console.error('停止音频失败:', error)
  199. }
  200. },
  201. // 格式化时间
  202. formatTime(timeStr) {
  203. if (!timeStr) return ''
  204. // 处理iOS兼容性问题,将 "2025-10-23 16:36:43" 格式转换为 "2025/10/23 16:36:43"
  205. let formattedTimeStr = timeStr.replace(/-/g, '/')
  206. const date = new Date(formattedTimeStr)
  207. // 检查日期是否有效
  208. if (isNaN(date.getTime())) {
  209. console.warn('Invalid date format:', timeStr)
  210. return timeStr // 返回原始字符串
  211. }
  212. const year = date.getFullYear()
  213. const month = String(date.getMonth() + 1).padStart(2, '0')
  214. const day = String(date.getDate()).padStart(2, '0')
  215. return `${year}-${month}-${day}`
  216. }
  217. }
  218. }
  219. </script>
  220. <style lang="scss" scoped>
  221. .article-detail {
  222. min-height: 100vh;
  223. background: #fff;
  224. }
  225. .article-container {
  226. padding: 30rpx;
  227. }
  228. .article-title {
  229. font-size: 40rpx;
  230. font-weight: 600;
  231. color: #333;
  232. line-height: 1.4;
  233. margin-bottom: 30rpx;
  234. }
  235. .article-info {
  236. display: flex;
  237. align-items: center;
  238. gap: 30rpx;
  239. margin-bottom: 40rpx;
  240. padding-bottom: 30rpx;
  241. border-bottom: 1px solid #f0f0f0;
  242. .article-time {
  243. font-size: 26rpx;
  244. color: #999;
  245. }
  246. .article-author {
  247. font-size: 26rpx;
  248. color: #666;
  249. &::before {
  250. content: '作者:';
  251. }
  252. }
  253. }
  254. .article-content {
  255. font-size: 32rpx;
  256. line-height: 1.8;
  257. color: #333;
  258. // 富文本内容样式
  259. :deep(.rich-text) {
  260. p {
  261. margin-bottom: 20rpx;
  262. line-height: 1.8;
  263. }
  264. img {
  265. max-width: 100%;
  266. height: auto;
  267. border-radius: 8rpx;
  268. margin: 20rpx 0;
  269. }
  270. section {
  271. margin: 20rpx 0;
  272. }
  273. }
  274. }
  275. .loading-container {
  276. display: flex;
  277. flex-direction: column;
  278. align-items: center;
  279. justify-content: center;
  280. height: 400rpx;
  281. .loading-text {
  282. margin-top: 20rpx;
  283. font-size: 28rpx;
  284. color: #999;
  285. }
  286. }
  287. // 音频播放悬浮按钮
  288. .audio-float-button {
  289. position: fixed;
  290. right: 30rpx;
  291. bottom: 100rpx;
  292. width: 100rpx;
  293. height: 100rpx;
  294. border-radius: 50%;
  295. background: rgba(255, 255, 255, 0.9);
  296. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
  297. display: flex;
  298. align-items: center;
  299. justify-content: center;
  300. z-index: 999;
  301. transition: all 0.3s ease;
  302. &:active {
  303. transform: scale(0.95);
  304. }
  305. // 添加呼吸动画效果
  306. &::before {
  307. content: '';
  308. position: absolute;
  309. top: -10rpx;
  310. left: -10rpx;
  311. right: -10rpx;
  312. bottom: -10rpx;
  313. border-radius: 50%;
  314. background: rgba(76, 175, 80, 0.2);
  315. animation: pulse 2s infinite;
  316. z-index: -1;
  317. }
  318. }
  319. @keyframes pulse {
  320. 0% {
  321. transform: scale(1);
  322. opacity: 1;
  323. }
  324. 50% {
  325. transform: scale(1.1);
  326. opacity: 0.7;
  327. }
  328. 100% {
  329. transform: scale(1);
  330. opacity: 1;
  331. }
  332. }
  333. </style>