|                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |  | <template>  <view class="richtext-container">    <!-- 状态栏安全区域 -->    <uv-status-bar></uv-status-bar>    
        <!-- 富文本内容区域 -->    <view class="content-wrapper">      <view class="content-container">        <!-- 使用 uv-parse 组件渲染富文本 -->        <uv-parse           v-if="htmlContent"          :content="htmlContent"          :tag-style="tagStyle"          :show-with-animation="true"          :animation-duration="300"        ></uv-parse>                <!-- 加载状态 -->        <view v-else class="loading-container">          <uv-loading-icon mode="circle"></uv-loading-icon>          <text class="loading-text">内容加载中...</text>        </view>      </view>    </view>        <!-- 音频播放悬浮按钮 -->    <view v-if="audioUrl" :class="['audio-float-btn', { 'playing': isPlaying }]" @click="toggleAudio">      <uv-icon         :name="isPlaying ? 'pause-circle-fill' : 'play-circle-fill'"         size="60"         :color="isPlaying ? '#ff6b6b' : '#4CAF50'"      ></uv-icon>    </view>  </view></template>
<script>export default {  data() {    return {      htmlContent: '',      articleDetail: null, // 文章详情数据
      audioUrl: '', // 音频URL
      isPlaying: false, // 音频播放状态
      audioContext: null, // 音频上下文
      // 富文本样式配置
      tagStyle: {        p: 'margin: 16rpx 0; line-height: 1.6; color: #333;',        img: 'max-width: 100%; height: auto; border-radius: 8rpx; margin: 16rpx 0;',        h1: 'font-size: 36rpx; font-weight: bold; margin: 24rpx 0 16rpx 0; color: #222;',        h2: 'font-size: 32rpx; font-weight: bold; margin: 20rpx 0 12rpx 0; color: #333;',        h3: 'font-size: 28rpx; font-weight: bold; margin: 16rpx 0 8rpx 0; color: #444;',        strong: 'font-weight: bold; color: #222;',        em: 'font-style: italic; color: #666;',        ul: 'margin: 16rpx 0; padding-left: 32rpx;',        ol: 'margin: 16rpx 0; padding-left: 32rpx;',        li: 'margin: 8rpx 0; line-height: 1.5;',        blockquote: 'margin: 16rpx 0; padding: 16rpx; background: #f5f5f5; border-left: 4rpx solid #ddd; border-radius: 4rpx;',        code: 'background: #f5f5f5; padding: 4rpx 8rpx; border-radius: 4rpx; font-family: monospace;',        pre: 'background: #f5f5f5; padding: 16rpx; border-radius: 8rpx; overflow-x: auto; margin: 16rpx 0;'      }    }  },    onLoad(options) {    // 获取传递的富文本内容
    if (options.content) {      this.htmlContent = decodeURIComponent(options.content)    } else if(options.articleId) {      // 从数据库获取文章内容
      this.getArticleContent(options.articleId)    } else {      uni.showToast({        title: '内容加载失败',        icon: 'error'      })      setTimeout(() => {        this.goBack()      }, 1500)    }  },    methods: {    // 返回上一页
    goBack() {      uni.navigateBack({        delta: 1      })    },        // 获取文章详情
    async getArticleContent(articleId) {      try {        const res = await this.$api.home.getArticleDetail({ id: articleId })        if (res.code === 200 && res.result) {          this.articleDetail = res.result          this.htmlContent = res.result.content                    // 获取第一个音频URL
          if (res.result.audios) {            const audioKeys = Object.keys(res.result.audios)            if (audioKeys.length > 0) {              this.audioUrl = res.result.audios[audioKeys[0]]            }          }        } else {          uni.showToast({            title: '文章加载失败',            icon: 'error'          })          setTimeout(() => {            this.goBack()          }, 1500)        }      } catch (error) {        console.error('获取文章详情失败:', error)        uni.showToast({          title: '网络错误',          icon: 'error'        })        setTimeout(() => {          this.goBack()        }, 1500)      }    },        // 创建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;    },
    // 切换音频播放状态
    toggleAudio() {      if (!this.audioUrl) {        uni.showToast({          title: '暂无音频',          icon: 'none'        })        return      }            if (this.isPlaying) {        this.pauseAudio()      } else {        this.playAudio()      }    },        // 播放音频
    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) {        this.audioContext.pause()      }    },        // 停止音频
    stopAudio() {      if (this.audioContext) {        try {          this.audioContext.stop();          this.audioContext.destroy();          this.isPlaying = false;          this.audioContext = null;        } catch (error) {          console.error('停止音频失败:', error);        }      }    }  },    // 页面卸载时清理音频资源
  onUnload() {    this.stopAudio()  }}</script>
<style lang="scss" scoped>.richtext-container {  min-height: 100vh;  background: #fff;}
.content-wrapper {  padding: 0 32rpx 40rpx;}
.content-container {  background: #fff;  border-radius: 16rpx;  overflow: hidden;}
.loading-container {  display: flex;  flex-direction: column;  align-items: center;  justify-content: center;  padding: 120rpx 0;    .loading-text {    margin-top: 24rpx;    font-size: 28rpx;    color: #999;  }}
// 富文本内容样式优化
:deep(.uv-parse) {  font-size: 28rpx;  line-height: 1.6;  color: #333;    // 图片样式
  img {    max-width: 100% !important;    height: auto !important;    border-radius: 8rpx;    margin: 16rpx 0;    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);  }    // 段落样式
  p {    margin: 16rpx 0;    text-align: justify;    word-break: break-word;  }    // 标题样式
  h1, h2, h3, h4, h5, h6 {    margin: 24rpx 0 16rpx 0;    font-weight: bold;    line-height: 1.4;  }    // 列表样式
  ul, ol {    margin: 16rpx 0;    padding-left: 32rpx;        li {      margin: 8rpx 0;      line-height: 1.5;    }  }    // 引用样式
  blockquote {    margin: 16rpx 0;    padding: 16rpx;    background: #f8f9fa;    border-left: 4rpx solid $primary-color;    border-radius: 4rpx;    font-style: italic;  }    // 代码样式
  code {    background: #f1f3f4;    padding: 4rpx 8rpx;    border-radius: 4rpx;    font-family: 'Courier New', monospace;    font-size: 24rpx;  }    pre {    background: #f1f3f4;    padding: 16rpx;    border-radius: 8rpx;    overflow-x: auto;    margin: 16rpx 0;        code {      background: none;      padding: 0;    }  }}
// 音频播放悬浮按钮样式
.audio-float-btn {  position: fixed;  right: 60rpx;  bottom: 120rpx;  width: 120rpx;  height: 120rpx;  background: rgba(255, 255, 255, 0.95);  border-radius: 50%;  display: flex;  align-items: center;  justify-content: center;  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);  backdrop-filter: blur(10rpx);  z-index: 999;  transition: all 0.3s ease;    &:active {    transform: scale(0.95);    box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);  }    // 添加呼吸动画效果
  &.playing {    animation: pulse 2s infinite;  }}
@keyframes pulse {  0% {    box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15), 0 0 0 0 rgba(76, 175, 80, 0.4);  }  70% {    box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15), 0 0 0 20rpx rgba(76, 175, 80, 0);  }  100% {    box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15), 0 0 0 0 rgba(76, 175, 80, 0);  }}</style>
 |