audioList.vue 9.82 KB
<template>
  <div class="audio-list-page">
    <van-config-provider :theme-vars="themeVars">
      <van-floating-panel v-model:height="info_height" :anchors="anchors" @height-change="onHeightChange" style="box-shadow: #d6d6d6b8 -1px -5px 15px 5px;">
        <template #header>
          <div style="display: flex; align-items: center; justify-content: space-between; padding: 1.25rem 1rem 0.5rem 1rem;">
            <van-icon name="https://cdn.ipadbiz.cn/bieyuan/map/icon/Group%2054@3x.png" color="#DD7850" size="1.25rem" />
            <van-icon @click="onClose" name="cross" color="#DD7850" size="1.25rem" />
          </div>
        </template>
        <div class="audio-list van-hairline--top" style="padding: 1rem;">
          <div :id="index" v-for="(item, index) in audio_list" :key="index" class="van-hairline--bottom audio-item">
            <div :class="['point', audio_index === index ? 'checked' : '']"></div>
            <div :class="['text', audio_index === index ? 'checked' : '', 'van-ellipsis']" @click="toggleHandleAudio(item, index)">
              {{ index + 1 }}. {{ item.title }}<span v-if="item.play" class="text-center">正在播放</span>

            </div>
            <div class="progress-ring">
              <div class="circle">
                <div class="pause-icon">
                  <van-icon @click="handleAudioPause(item, index)" v-if="item.play" name="https://cdn.ipadbiz.cn/bieyuan/map/icon/pauseButton1@3x.png" size="2.2rem" />
                  <van-icon @click="handleAudioPlay(item, index)" v-else name="https://cdn.ipadbiz.cn/bieyuan/map/icon/play1@3x.png" size="2.2rem" />
                </div>
                <div :class="['progress', item.play ? 'checked' : '']"></div>
              </div>
            </div>
          </div>
        </div>
      </van-floating-panel>
    </van-config-provider>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router';
import $ from 'jquery';
import { storeToRefs } from 'pinia'
import { mainStore } from '@/store';
import { mapAudioAPI } from '@/api/map.js'

const store = mainStore();
const { audio_status, audio_entity, audio_list_status, audio_list_entity } = storeToRefs(store);

const props = defineProps({
  height: Number,
  status: String,
});

const themeVars = ref({
  floatingPanelHeaderHeight: 0,
  floatingPanelBorderRadius: '1.25rem'
})

const anchors = ref([0, (0.2 * window.innerHeight),  (0.5 * window.innerHeight)]);

const onHeightChange = ({ height }) => { // 监听高度变化
  if (!height) { // 关闭
    onClose();
  }
  // 打开列表高度需要删除底部滚动空间
  if (height > (0.2 * window.innerHeight)) {
    $('.audio-list').css('marginBottom', 0);
  }
  // 添加可滚动高度
  if (height === (0.2 * window.innerHeight)) {
    let str = $('.audio-item').outerHeight() * (audio_list.value.length + 1.25);
    $('.audio-list').css('marginBottom', `${str}px`);
  }
}

const info_height = ref(0);



watch(
  () => props.height,
  (v) => {
    info_height.value = v;
    anchors.value = [(0.2 * window.innerHeight),  (0.5 * window.innerHeight)];
    if (v) {
      // 自动打开第一个
      handleAudioPlay(audio_list.value[0], 0);
      nextTick(() => {
        // 添加可滚动高度
        let str = $('.audio-item').outerHeight() * (audio_list.value.length + 1.25);
        $('.audio-list').css('marginBottom', `${str}px`);
      })
    } else {
      onClose();
    }
  }
)
watch(
  () => props.status,
  (v) => {
    if (v) { // 监听详情页播放状态,关闭浮层音频播放
      console.warn(v);
    }
  }
)

const onClose = () => { // 关闭列表回调
  //
  anchors.value = [0, (0.2 * window.innerHeight),  (0.5 * window.innerHeight)];
  //
  audio.value.pause(); // 暂停音频播放
  pauseProgress();
  // 播放进度条
  $('.progress').css('background', 'conic-gradient(#f07142 0%, transparent 0%)');
  audio_list.value.forEach(item => item.play = false);
  audio_index.value = null;
  emit('close');
}

const emit = defineEmits(['close', 'status']);

const audio_index = ref();

const audio_list = ref([]);

onMounted(async () => {
  const { id, marker_id } = useRoute().query;
  const { data, code } = await mapAudioAPI({ mid: id, bid: marker_id });
  if (code) {
    if (data.length) {
      data.forEach((item) => {
        item.title = item.name;
        item.src = item.audio_val;
        item.play = false;
      });
      audio_list.value = data;
    } else {
      emit('status', 'none');
    }
  }
});

/**
 * 音频播放模块
 */

const audio = ref(new Audio());
const duration = ref(0);      // 音频总时长
// const currentTime = ref(0);   // 当前播放时间
let elapsedTime = 0;    // 已播放的时间
let intervalId = null;  // 保存setInterval的ID

/**
* 更新进度条
*/
const updateProgress = (audioDuration, index) => {
  let progressPercent = (elapsedTime / audioDuration) * 100;
  document.querySelector('.progress.checked').style.background =
    `conic-gradient(#f07142 ${progressPercent}%, transparent ${progressPercent}%)`;

  if (elapsedTime >= audioDuration) {
    pauseProgress(); // 音频播放完成,停止更新
    // 切换到下一个音频
    let idx = index + 1;
    if (idx >= audio_list.value.length) {
      idx = 0;
    }
    // 播放下一个音频
    handleAudioPlay(audio_list.value[idx], idx);
  }
}

/**
* 启动进度更新
*/
const startProgress = (audioDuration, index) => {
  intervalId = setInterval(() => {
    elapsedTime++;
    updateProgress(audioDuration, index);
  }, 1000);
}

/**
* 暂停进度更新
*/
const pauseProgress = () => {
  clearInterval(intervalId);
}

const toggleHandleAudio = (item, index) => { // 切换播放或者暂停操作
  if (item.play) {
    handleAudioPause(item, index);
  } else {
    handleAudioPlay(item, index);
  }
}

/**
 * 暂停播放回调
 * @param item
 * @param index
 */
const handleAudioPause = (item, index) => {
  item.play = false; // 更新播放状态
  audio.value.pause(); // 暂停音频播放
  pauseProgress(); // 暂停进度更新
}

const updateDuration = () => {
  duration.value = Math.floor(audio.value.duration); // 获取音频时长
};

// const updateTime = () => {
//   currentTime.value = Math.floor(audio.value.currentTime); // 获取当前播放时间
// };

// 监听 'loadedmetadata' 事件,确保在音频元数据加载后获取时长
audio.value.addEventListener('loadedmetadata', updateDuration);

// 监听 'timeupdate' 事件,实时更新当前播放时间
// audio.value.addEventListener('timeupdate', updateTime);

/*********************************** END *************************************/
/**
 * 播放音频回调
 * @param item
 * @param index
 */
const handleAudioPlay = (item, index) => {
  audio_list.value.forEach(item => item.play = false); // 清空播放状态
  if (audio_index.value !== index) {
    audio.value.src = item.src;
  }
  // 后台有播放器运行时,先暂停详情页播放器
  if (audio_status.value === 'play'){
    audio_entity.value.pause();
  }
  $('.progress').css('background', 'conic-gradient(#f07142 0%, transparent 0%)'); // 清空进度条
  pauseProgress(); // 暂停进度更新
  let play_status = audio.value.play() // 播放
  if (play_status) {
    play_status.then(() => {
      item.play = true;
      // 存放到pinia里面控制
      store.changeAudioList(audio.value);
      store.changeAudioListSrc(audio.value.src);
      store.changeAudioListStatus('play');
      //
      startProgress(duration.value, index); // 开始更新进度
      if (audio_index.value !== index) { // 点击非同一音频
        audio_index.value = index;
        elapsedTime = 0; // 重置已播放时间
      }
      emit('status', 'play');
    }).catch((e) => {
      // 失败
      console.log('Operation is too fast, audio play fails')
    })
  }
  // 指定滚动到播放位置
  scrollToId(index);
}

const scrollToId = (index) => { // 滚动到指定元素
  nextTick(() => {
    const element = document.getElementById(index);
    if (element) {
      element.scrollIntoView({ behavior: "smooth", block: "start" });
    }
  })
}

const voicePause = () => {
  audio.value.pause();
  pauseProgress(); // 暂停进度更新
  store.changeAudioListStatus('pause');
}

watch(
  () => audio_status.value,
  (v) => {
    if (v === 'play') {
      voicePause();
      audio_list.value.forEach(item => item.play = false);
    }
  },
  { immediate: true }
);

onUnmounted(() => { // 离开页面时关闭音频播放
  onClose();
  store.changeAudioStatus('pause');
})
</script>

<style lang="less" scoped>
.audio-list-page {
  .audio-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1rem 0;
    .point {
      padding: 0.25rem;
      width: 1rem;
      height: 1rem;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-right: 1rem;
      &.checked {
        border: 1px solid rgba(221, 120, 80, 0.5);
      }
      &::after {
        content: '';
        width: 1rem;
        height: 1rem;
        border-radius: 50%;
        background-color: #DD7850;
      }
    }
    .text {
      flex: 1;
      // display: flex;
      align-items: center;
      color: #47525F99;
      &.checked {
        color: #47525F;
      }
      .text-center {
        color: #DD7850;
        font-size: 0.85rem;
        margin-left: 1rem;
        line-height: 1.25;
      }
    }
  }

.progress-ring {
  position: relative;
  width: 2.5rem;
  height: 2.5rem;
}

.circle {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  // background-color: lightgray;
}

.pause-icon {
  z-index: 10;
  position: absolute;
  left:0.15rem;
  top: 0.15rem;
}

.progress {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  border-radius: 50%;
  background: conic-gradient(#f07142 0%, #f07142 0%, transparent 0%);
  z-index: 5;
  transform: rotate(0deg); /* 让进度条从顶部开始 */
}
}
</style>