Vue3项目自学训练------音乐播放器(使用vueX管理项目)(八)

目录

备注(功能逻辑都写在了actions.js中)

功能实现一:切歌

功能实现二:播放模式

功能实现三:添加喜欢

功能实现四:播放时间显示

功能实现五:播放进度条

功能实现六: 进度条拖动


备注(功能逻辑都写在了actions.js中)

// 有所需要的功能
const actions = {
    // ----------添加一首歌到列表----------
    addOnePlay({ commit, state }, list) {
        // const playlist = [...state.playlist]//源播放列表(浅拷贝获取)
        const playlist = state.playlist.slice()
        const sequenceList = state.sequenceList.slice() //当前播放列表
        let currentIndex = state.currentIndex //当前播放的索引值

        // 新增歌曲放到当前播放歌曲的后面
        sequenceList.splice(currentIndex + 1, 0, list[0])
        // 判断加入的歌曲是否在sequenceList中存在的
        let sequenceIndex = findIndex(sequenceList, list[0])
        if (sequenceIndex > 0) {
            // 歌曲存在
            if (currentIndex < sequenceIndex) {
                // 原本存在的歌曲在当前播放歌曲的后面,删除原来的
                sequenceList.splice(sequenceIndex + 1, 1)
                // 当前播放的索引值加一
                currentIndex++
            } else {
                // 原本存在的歌曲在当前播放歌曲的前面,删除原来的
                sequenceList.splice(sequenceIndex, 1)
            }
        } else {
            // 歌曲不存在
            // 当前播放的索引值加一
            currentIndex++
            // 添加到源歌曲列表
            playlist.unshift(list[0])
        }

        // 边界情况
        if (sequenceList.length === 1) {
            // 还原
            currentIndex = 0 //其他下标根本没有值
        }

        // 设置当前播放歌曲列表
        commit('setSequenceList', sequenceList)
        // 设置源歌曲列表
        commit('setPlaylist', playlist)
        // 设置播放状态
        commit('setPlayingState', true)
        // 设置全屏
        commit('setFullScreen', true)
        // 设置当前歌曲下标
        commit('setCurrentIndex', currentIndex)
    },

    // ----------设置全部歌曲到当前歌曲列表----------
    addAllPlay({ commit }, list) {
        // 设置当前播放歌曲列表
        commit('setSequenceList', list)
        // 设置源歌曲列表
        commit('setPlaylist', list)
        // 设置播放状态
        commit('setPlayingState', true)
        // 设置全屏
        commit('setFullScreen', true)
        // 设置当前歌曲下标
        commit('setCurrentIndex', 0)
        // 设置播放模式为顺序播放
        commit('setPlayMode', 0)
    },
    // ----------修改播放模式----------
    changeMode({ commit, state, getters }, mode) {
        const currentSong = getters.currentSong
        // 修改为随机播放
        if (mode === 2) {
            commit('setSequenceList', shuffle(state.playlist))
        } else {
            commit('setSequenceList', state.playlist)
        }
        // 确保当前播放歌曲不变
        const index = findIndex(state.sequenceList, currentSong)
        // 设置当前歌曲下标
        commit('setCurrentIndex', index)
        // 设置播放模式
        commit('setPlayMode', mode)
    },

    // ----------删除歌曲----------
    removeSong({ commit, state }, song) {
        const playlist = state.playlist.slice()//源播放列表
        const sequenceList = state.sequenceList.slice() //当前播放列表
        let currentIndex = state.currentIndex //当前播放的索引值

        // 找到需要被删除的歌曲对应的index
        const sequenceIndex = findIndex(sequenceList, song)
        const playlistIndex = findIndex(playlist, song)
        // 找不到
        if (sequenceIndex < 0 || playlist < 0) return
        // 找到删掉
        sequenceList.splice(sequenceIndex, 1)
        playlist.splice(playlistIndex, 1)
        // 被删除项在当前播放歌曲前面
        if (sequenceIndex < currentIndex) {
            currentIndex--
        }
        // 被删除的是当前播放歌曲,且在sequenceList最后一项
        if (sequenceList.length === currentIndex) {
            currentIndex = 0
        }
        // 设置当前播放歌曲列表
        commit('setSequenceList', sequenceList)
        // 设置源歌曲列表
        commit('setPlaylist', playlist)
        // 源歌曲列表为空
        if (!playlist.length) {
            // 设置播放状态
            commit('setPlayingState', false)
        }
        // 设置当前歌曲下标
        commit('setCurrentIndex', currentIndex)

    },

    // ----------全部清空----------
    clearSongList({ commit }) {
        // 设置当前播放歌曲列表
        commit('setSequenceList', [])
        // 设置源歌曲列表
        commit('setPlaylist', [])
        // 设置播放状态
        commit('setPlayingState', false)
        // 设置当前歌曲下标
        commit('setCurrentIndex', 0)
    }
}

// 封装findIndex
function findIndex(list, song) {
    // 为true时是index下标,不满足返回-1
    return list.findIndex(item => item.id === song.id)
}

// 封装随机打乱函数
function shuffle(list) {
    const arr = list.slice()
    arr.sort((a, b) => {
        return Math.random() - 0.5
    })
    return arr
}

export default actions

功能实现一:切歌

下一首:

先绑定点击事件

<div class="icon i-right">
   <i class="icon-next" @click="next"></i>
</div>

获取vuex中的歌曲索引值和歌曲列表

const sequenceList = computed(() => store.state.sequenceList);
const currentIndex = computed(() => store.state.currentIndex);

// 下一首
function next() {
  const list = sequenceList.value
  // console.log(list);
  // 如果没有歌
  if(!list.length) return;
  if(list.length === 1) return loop();
  let index = currentIndex.value + 1;
  if(index === list.length) {
    // 说明当前歌曲是列表最后一项
    index = 0;
  }
  // 提交状态
  store.commit("setCurrentIndex", index);
}
// 封装单曲循环
function loop() {
  const audio = audioRef.value;
  // currentTime:当前播放时间
  audio.currentTime = 0;
  // 重新播放
  audio.play()
  store.commit("setPlayingState", true);
}

上一首:

<div class="icon i-left">
   <i class="icon-prev" @click="prev"></i>
</div>

同理使用上面封装的函数并提交状态

// 上一首
function prev() {
  const list = sequenceList.value;
  // console.log(list);
  // 如果没有歌
  if (!list.length) return;
  if (list.length === 1) return loop();
  let index = currentIndex.value - 1;
  if (index === -1) {
    // 说明当前歌曲是列表第一项
    index = list.length -1;
  }
  // 提交状态
  store.commit("setCurrentIndex", index);
}

功能实现二:播放模式

抽离封装组件useMode.js

import { computed } from "vue";
import { useStore } from "vuex";

export default function useMode() {
    const store = useStore()
    const playMode = computed(() => store.state.playMode)

    const modeIcon = computed(() => {
        const playModeValue = playMode.value
        switch (playModeValue) {
            case 0://顺序播放
                return 'icon-sequence'
                break;
            case 1://单曲播放
                return 'icon-loop'
                break;
            case 2://随机播放
                return 'icon-random'
                break;
        }
    })

    function changeMode() {
        const mode = (playMode.value + 1) % 3
        store.dispatch("changeMode", mode)
    }

    return {
        modeIcon,
        changeMode
    }
}

导入使用

import useMode from "@/assets/js/useMode";
const { modeIcon,changeMode } = useMode();

拿到之后渲染上去

<div class="icon i-left">
  <i :class="modeIcon" @click="changeMode"></i>
</div>

 点击切换到随机播放时报错了:发现是action封装的函数出错

// 封装随机打乱函数
function shuffle(list) {
    const arr = list.splice()
    arr.sort((a, b) => {
        return Math.random() - 0.5
    })
    return arr
}

应该更正为

// 封装随机打乱函数
function shuffle(list) {
    const arr = list.slice()
    arr.sort((a, b) => {
        return Math.random() - 0.5
    })
    return arr
}

(要牢记slice和splice的区别)

功能实现三:添加喜欢

在之前的state.js中定义了存储位置

    // 最爱的歌曲列表
    favoriteList: [],
    // 历史播放记录
    playHistory: []

新建useFavorite.js实现永久存储(进入页面之后在判断一次是否在喜欢列表)

import storage from "@/assets/js/storage-api";
import { computed, onMounted } from "vue";
import { useStore} from "vuex";

export default function useFavorite() {
    const store = useStore()
    const favoriteList = computed(() => store.state.favoriteList)
    const currentSong = computed(() => store.getters.currentSong)

    const favoriteIcon = computed(() => {
        return isFavorite(currentSong.value) ? 'icon-favorite' : 'icon-not-favorite'
    })

    onMounted(() => {
        // 用上本地存储
        if(!favoriteList.value.length) {
            let list = storage.getLocal('__favoriteList__', [])
            if(list.length) {
                store.commit('setFavoriteList', list)
            }
        }
    })

    function toggleFavorite() {
        // 拿到当前的歌曲
        const song = currentSong.value
        let list = favoriteList.value.slice()
        // 点击按钮实现喜欢或取消喜欢功能
        if(isFavorite(song)) {
            // 在就删除
            let index = list.findIndex(item => item.id === song.id)
            list.splice(index,1)
        }else {
            // 不在添加
            list.unshift(song)
        }
        store.commit('setFavoriteList', list)
        storage.setLocal('__favoriteList__', list)
    }

    function isFavorite(song) {
        // true 在喜欢列表,false不在喜欢列表
        return favoriteList.value.findIndex(item => item.id === song.id) > -1
    }


    return {
        favoriteIcon,
        toggleFavorite
    }
}

导入使用

import useFavorite from "@/assets/js/useFavorite";
const { favoriteIcon, toggleFavorite } = useFavorite();
<div class="icon i-right">
    <i :class="favoriteIcon" @click="toggleFavorite"></i>
</div>

功能实现四:播放时间显示

const currentTime = ref(0); //当前时长
const duration = ref(0); //总时长
<audio ref="audioRef" @timeupdate="updateTime" @canplay="ready"></audio>
// 当前时长
function updateTime() {
  // console.log(audioRef.value.currentTime);//记录实时的时间
  currentTime.value = audioRef.value.currentTime
}
// 总时长
function ready() {
  // console.log(audioRef.value.duration);
  duration.value = audioRef.value.duration;
}

创建utils.js封装时间函数

export function formatTime(interval) {
    // mm:ss
    interval = interval | 0 //取整
    // 分钟
    const minute = ((interval / 60 | 0) + '').padStart(2, '0')
    // 秒钟
    const second = (interval % 60 + '').padStart(2, '0')
    return `${minute}:${second}`
}

导入使用

import { formatTime } from "@/assets/js/utils";
        <div class="progress-wrapper">
          <span class="time time-l">{{ formatTime(currentTime) }}</span>
          <div class="progress-bar-wrapper">进度条占位</div>
          <span class="time time-r">{{ formatTime(duration) }}</span>
        </div>

功能实现五:播放进度条

新建ProgressBar.vue组件并导入使用

import { MyProgressBar } from "@/components/base/ProgressBar";
           <my-progressBar
              :progress="progress"
            ></my-progressBar>
<template>
  <div class="progress-bar">
    <!-- 播放进度条容器 -->
    <div class="bar-inner" ref="progressWrapperRef">
      <!-- 实际播放进度条 -->
      <div class="progress" :style="progressStyle"></div>
      <div class="progress-btn-wrapper" :style="btnStyle">
        <div class="progress-btn"></div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed, onMounted, ref, watch } from "vue";

const props = defineProps({
  progress: {
    type: Number,
    default: 0,
  },
});
const btnWidth = 16;
// 记录偏移量
const offset = ref(0);
// 宽度和偏移量的最大范围
let maxNum = ref(0);
const progressWrapperRef = ref(null);

watch(
  () => props.progress,
  (newProgress) => {
    offset.value = newProgress * maxNum.value;
  }
);

const progressStyle = computed(() => `width: ${offset.value}px`);
const btnStyle = computed(() => `transform: translate(${offset.value}px,0)`);

//播放时间 / 总时间 = 当前progress的宽度 / 总progress宽度
// 总progress宽度 = inner宽度 - btn宽度
onMounted(() => {
  maxNum.value = progressWrapperRef.value.clientWidth - btnWidth;
});
</script>

功能实现六: 进度条拖动

<div class="progress-bar">
    <!-- 播放进度条容器 -->
    <div
      class="bar-inner"
      ref="progressWrapperRef"
      @touchstart.prevent="onTouchStart"
      @touchmove.prevent="onTouchMove"
      @touchend.prevent="onTouchEnd"
    >
      <!-- 实际播放进度条 -->
      <div class="progress" :style="progressStyle" ref="progressRef"></div>
      <div class="progress-btn-wrapper" :style="btnStyle">
        <div class="progress-btn"></div>
      </div>
    </div>
  </div>
// touch事件的位置信息
const touch = {}

const progressRef = ref(null);

<my-progressBar
 :progress="progress"
 @progressChanging="onProgressChanging"
 @progressChanged="onProgressChanged"
></my-progressBar>
let progressChanging = false; //记录是否在进度拖动中

// 当前时长
function updateTime() {
  // 当进度拖动时不触发此currentTIme更新
  if (progressChanging) return;
  // console.log(audioRef.value.currentTime);//记录实时的时间
  currentTime.value = audioRef.value.currentTime;
}

// 进度条变化中
function onProgressChanging(progress) {
  progressChanging = true;
  currentTime.value = progress * duration.value;
}
// 进度条变化后(拖动后松手)
function onProgressChanged(progress) {
  progressChanging = false;
  // 设置给audio真正去修改位置
  audioRef.value.currentTime = progress * duration.value;
  //修改播放状态
  if(!playing.value) {
    store.commit("setPlayingState",true);
  }
}

ProgressBar.vue

<template>
  <div class="progress-bar">
    <!-- 播放进度条容器 -->
    <div
      class="bar-inner"
      ref="progressWrapperRef"
      @touchstart.prevent="onTouchStart"
      @touchmove.prevent="onTouchMove"
      @touchend.prevent="onTouchEnd"
    >
      <!-- 实际播放进度条 -->
      <div class="progress" :style="progressStyle" ref="progressRef"></div>
      <div class="progress-btn-wrapper" :style="btnStyle">
        <div class="progress-btn"></div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed, onMounted, ref, watch } from "vue";

const props = defineProps({
  progress: {
    type: Number,
    default: 0,
  },
});
const emit = defineEmits(["progressChanging", "progressChanged"]);
const btnWidth = 16;
// touch事件的位置信息
const touch = {};

// 记录偏移量
const offset = ref(0);
// 宽度和偏移量的最大范围
let maxNum = ref(0);
const progressWrapperRef = ref(null);
const progressRef = ref(null);

watch(
  () => props.progress,
  (newProgress) => {
    offset.value = newProgress * maxNum.value;
  }
);

const progressStyle = computed(() => `width: ${offset.value}px`);
const btnStyle = computed(() => `transform: translate(${offset.value}px,0)`);

// 手指落下
function onTouchStart(e) {
  touch.x1 = e.touches[0].pageX; //记录手指落下初识x坐标
  touch.beginWidth = progressRef.value.clientWidth; //记录当前播放条的初始宽度
}
// 手指滑动
function onTouchMove(e) {
  // 计算和初始位置的差值,是手指移动触发宽度变化,触发progress变化
  const delta = e.touches[0].pageX - touch.x1;
  const tempWidth = touch.beginWidth + delta;
  // 显示范围在0-maxNum之间
  offset.value = Math.max(0, Math.min(tempWidth, maxNum.value));
  // 限制progress 的范围在0-1之间
  const progress = offset.value / maxNum.value;
  // 传给Player进行currentTime的计算
  emit("progressChanging", progress);
}
// 手指离开
function onTouchEnd(e) {
  // 得到最终的进度值
  const progress = offset.value / maxNum.value;
  // 传给Player进行currentTime的计算
  emit("progressChanged", progress);
}

//播放时间 / 总时间 = 当前progress的宽度 / 总progress宽度
// 总progress宽度 = inner宽度 - btn宽度
onMounted(() => {
  maxNum.value = progressWrapperRef.value.clientWidth - btnWidth;
});
</script>

<style lang="scss" scoped>
.progress-bar {
  height: 30px;
  .bar-inner {
    position: relative;
    top: 13px;
    height: 4px;
    background: rgba(0, 0, 0, 0.3);
    .progress {
      position: absolute;
      height: 100%;
      background: $color-theme;
    }
    .progress-btn-wrapper {
      position: absolute;
      left: -8px;
      top: -13px;
      width: 30px;
      height: 30px;
      .progress-btn {
        position: relative;
        top: 7px;
        left: 7px;
        box-sizing: border-box;
        width: 16px;
        height: 16px;
        border: 3px solid $color-text;
        border-radius: 50%;
        background: $color-theme;
      }
    }
  }
}
</style>

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr.MUXIAO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值