|                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |  | <template>  <view class="article-detail">    <!-- 导航栏 -->    <uv-navbar       :placeholder="true"       left-icon="arrow-left"       :title="articleData.title || '文章详情'"      @leftClick="goBack"    ></uv-navbar>        <!-- 文章内容 -->    <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">        <rich-text :nodes="articleData.content"></rich-text>      </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>export default {  data() {    return {      articleId: '',      articleData: {},      loading: true,      isPlaying: false,      audioUrl: '',      hasAudio: false,      audioContext: null    }  },    onLoad(options) {    if (options.id) {      this.articleId = options.id      this.getArticleDetail()    }  },
  onUnload() {    // 页面卸载时清理音频资源
    this.stopAudio();  },    methods: {    // 创建HTML5 Audio实例并包装为uni-app兼容接口
    createHTML5Audio() {      const audio = new Audio();            // 包装为uni-app兼容的接口
      const wrappedAudio = {        // 原生HTML5 Audio实例
        _nativeAudio: audio,                // 基本属性
        get src() { return audio.src; },        set src(value) { audio.src = value; },                get duration() { return audio.duration || 0; },        get currentTime() { return audio.currentTime || 0; },                get paused() { return audio.paused; },                // 支持倍速的关键属性
        get playbackRate() { return audio.playbackRate; },        set playbackRate(value) {           try {            audio.playbackRate = value;          } catch (error) {            console.error('HTML5 Audio倍速设置失败:', error);          }        },                // 基本方法
        play() {          return audio.play().catch(error => {            console.error('HTML5 Audio播放失败:', error);          });        },                pause() {          audio.pause();        },                stop() {          audio.pause();          audio.currentTime = 0;        },                seek(time) {          audio.currentTime = time;        },                destroy() {          audio.pause();          audio.src = '';          audio.load();        },                // 事件绑定方法
        onCanplay(callback) {          audio.addEventListener('canplay', callback);        },                onPlay(callback) {          audio.addEventListener('play', callback);        },                onPause(callback) {          audio.addEventListener('pause', callback);        },                onEnded(callback) {          audio.addEventListener('ended', callback);        },                onTimeUpdate(callback) {          audio.addEventListener('timeupdate', callback);        },                onError(callback) {          // 包装错误事件,过滤掉非关键错误
          const wrappedCallback = (error) => {            // 只在有src且音频正在播放时才传递错误事件
            if (audio.src && audio.src.trim() !== '' && !audio.paused) {              callback(error);            } else {              console.log('HTML5 Audio错误(已忽略):', {                hasSrc: !!audio.src,                paused: audio.paused,                errorType: error.type || 'unknown'              });            }          };          audio.addEventListener('error', wrappedCallback);        },                // 移除事件监听
        offCanplay(callback) {          audio.removeEventListener('canplay', callback);        },                offPlay(callback) {          audio.removeEventListener('play', callback);        },                offPause(callback) {          audio.removeEventListener('pause', callback);        },                offEnded(callback) {          audio.removeEventListener('ended', callback);        },                offTimeUpdate(callback) {          audio.removeEventListener('timeupdate', callback);        },                offError(callback) {          audio.removeEventListener('error', callback);        }      };            return wrappedAudio;    },
    // 返回上一页
    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                    // 处理音频数据
          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;      }    },
    // 播放音频
    playAudio() {      if (!this.audioUrl) {        console.error('音频URL为空');        return;      }            try {        // 销毁旧的音频实例
        if (this.audioContext) {          this.audioContext.destroy();          this.audioContext = null;        }                // 平台判断:H5环境使用createHTML5Audio,其他环境使用uni.createInnerAudioContext
        if (uni.getSystemInfoSync().platform === 'devtools' || process.env.NODE_ENV === 'development' || typeof window !== 'undefined') {          // H5环境:使用包装的HTML5 Audio
          this.audioContext = this.createHTML5Audio();        } else {          // 非H5环境(小程序等)
          this.audioContext = uni.createInnerAudioContext();        }                // 设置音频源
        this.audioContext.src = this.audioUrl;                // 绑定事件监听
        this.audioContext.onPlay(() => {          this.isPlaying = true;        });                this.audioContext.onPause(() => {          this.isPlaying = false;        });                this.audioContext.onEnded(() => {          this.isPlaying = false;        });                this.audioContext.onError((error) => {          console.error('音频播放错误:', error);          this.isPlaying = false;        });                // 开始播放
        this.audioContext.play();      } catch (error) {        console.error('音频播放异常:', error);        this.isPlaying = false;      }    },
    // 暂停音频
    pauseAudio() {      if (this.audioContext) {        try {          this.audioContext.pause();          this.isPlaying = false;        } catch (error) {          console.error('暂停音频失败:', error);        }      }    },
    // 停止音频
    stopAudio() {      if (this.audioContext) {        try {          this.audioContext.stop();          this.audioContext.destroy();          this.isPlaying = false;          this.audioContext = null;        } 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>
 |