目录
备注(功能逻辑都写在了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>