前言:
Vue3.0发布一年多了,然而我还没用过,想学,但是很懒,不知道从何开始,不能这么堕落下去了,突然想到以前用vue2写过一个音乐播放器,这次就以音乐播放器为起点,一边学习一边用3.0写一个音乐播放器。 基于vue2.x构建的音乐播放器样式以及歌词滚动
参考了大佬的github.com/maomao1996/…
本文项目地址(未全部完成):gitee.com/myDearDer/b…
写的不好的地方希望大佬指正
2022/1/12
vite创建项目
npm init @vitejs/app music-player
搭配路由,less,axios,element-plus。
先从简单的开始吧,图形界面。前期做的简单一些,实现播放,搜索。
2022/1/17
完成界面,支持点击播放音乐,歌词滚动,进度条。效果有点简陋,如下,为了方便看得清,调了一下背景色。
简单说说思路,首先布局分顶部,中间的内容,和底部的进度条三大部分。 中间四个切换我准备写成组件,说一下播放的实现。
先初始化第一条要播放的歌曲信息
先定义一些信息,暂时先用假数据代替。 代码都比较简单,基础,我都有写注释,就不细说了。
setup() {
const state = reactive({
backgroundUrl: "", // 模糊背景图
menuList: ["正在播放", "推荐", "搜索", "播放历史"],
activeIndex: 0, // 选中
playing: false, // 是否正在播放
songList: [
{
albumId: 122397809,
albumTitle: "一些古风歌【2021】",
artistsName: "平生不晚",
cover:
"http://localhost:8080/pOR45DW9BfLSQ2JDJJeUgQ==/109951165714496390.jpg",
id: 1820643403,
name: "伯虎说(纯戏腔段)",
url: "http://localhost:8080/song/media/outer/url?id=1820643403.mp3",
},
],
audioProgress: 0, // 进度
playType: 1, // 播放类型,单曲循环还是顺序播放
playIndex: 0, // 当前播放哪一首
thumbTranslateX: 0,
lyricIndex: 0, // 歌词到哪一行了
currentTime: 0, // 当前播放进度
progressL: 0, // 进度条总长度
songInfo: {}, // 歌曲详情
lyricInfo: [], // 歌词信息
audioTime: "00:00", // 歌曲时长
volume: 80, // 音量
});
let track = ref(null);
onMounted(() => {
const audio = document.getElementById("audio");
console.log(track.value); //取元素宽高等属性操作
state.progressL = track.value.offsetHeight;
Init(); // 初始化
});
const Init = () => {
GetSongInfo();
};
const GetSongInfo = () => {
let myList = state.songList;
state.songInfo = myList[0];
state.backgroundUrl = state.songInfo.cover
audioInit(); // 初始化audio
GetLyric(state.songInfo.id); // 获取歌词信息
};
const audioInit = () => {
let progressL = track.value.offsetWidth; // 进度条总长
audio.addEventListener("canplay", () => {
state.audioTime = TimeToString(audio.duration);
});
audio.addEventListener("timeupdate", () => {
// 当前播放时间
let compareTime = audio.currentTime;
for (let i = 0; i < state.lyricInfo.length; i++) {
if (compareTime > parseInt(state.lyricInfo[i].time)) {
const index = state.lyricInfo[i].index;
if (i === parseInt(index)) {
state.lyricIndex = i; // 获取当前播放歌词是哪一条
}
}
}
state.currentTime = TimeToString(audio.currentTime);
state.audioProgress = audio.currentTime / audio.duration;
state.thumbTranslateX = (state.audioProgress * progressL).toFixed(3);
});
audio.addEventListener("ended", () => {
switch (parseInt(state.playType)) {
case 1: // 列表循环
state.playIndex =
state.playIndex + 1 >= state.songList.length
? 0
: state.playIndex + 1;
break;
case 2: // 随机播放
state.playIndex = Math.floor(Math.random() * state.songList.length);
break;
case 3: // 单曲循环
break;
}
state.songInfo = state.songList[state.playIndex];
GetLyric(state.songInfo.id);
setTimeout(() => {
// rotate.style.animationPlayState = "running";
audio.play();
}, 100);
});
};
// 获取歌词信息生成歌词list
const GetLyric = (id) => {
liricApi
.GetLiricInfo({
id,
})
.then((res) => {
console.log("res", res);
let lrc = res.lrc.lyric;
GetLyricList(lrc);
});
};
const GetLyricList = (lrc) => {
let lyricsObjArr = [];
const regNewLine = /\n/;
const lineArr = lrc.split(regNewLine); // 每行歌词的数组
const regTime = /\[\d{2}:\d{2}.\d{2,3}\]/;
lineArr.forEach((item) => {
if (item === "") return;
const obj = {};
const time = item.match(regTime);
obj.lyric =
item.split("]")[1].trim() === "" ? "" : item.split("]")[1].trim();
obj.time = time
? TimeToSeconds(time[0].slice(1, time[0].length - 1))
: 0;
obj.uid = Math.random().toString().slice(-6);
if (obj.lyric === "") {
console.log("这一行没有歌词");
} else {
lyricsObjArr.push(obj);
}
});
state.lyricInfo = lyricsObjArr.map((item, index) => {
item.index = index;
return {
...item,
};
});
console.log("state.lyricInfo", state.lyricInfo);
};
const TimeToSeconds = (time) => {
// 格式化歌词的时间 转换成 sss:ms
const regMin = /.*:/;
const regSec = /:.*\./;
const regMs = /\./;
const min = parseInt(time.match(regMin)[0].slice(0, 2));
let sec = parseInt(time.match(regSec)[0].slice(1, 3));
const ms = time.slice(
time.match(regMs).index + 1,
time.match(regMs).index + 3
);
if (min !== 0) {
sec += min * 60;
}
return Number(sec + "." + ms);
};
const ChangeActive = (index) => {
state.activeIndex = index;
};
const TimeToString = (seconds) => {
let param = parseInt(seconds);
let hh = "",
mm = "",
ss = "";
if (param >= 0 && param < 60) {
param < 10 ? (ss = "0" + param) : (ss = param);
return "00:" + ss;
} else if (param >= 60 && param < 3600) {
mm = parseInt(param / 60);
mm < 10 ? (mm = "0" + mm) : mm;
param - parseInt(mm * 60) < 10
? (ss = "0" + String(param - parseInt(mm * 60)))
: (ss = param - parseInt(mm * 60));
return mm + ":" + ss;
}
};
const playMusic = () => {
if (state.playing) {
// 播放中,点击则为暂停
state.playing = false;
// rotate.style.animationPlayState = "paused";
audio.pause();
} else {
// 暂停中,点击则为播放
state.playing = true;
// rotate.style.animationPlayState = "running";
audio.play();
}
};
const audioProgressPercent = computed(() => {
return `${state.audioProgress * 100}%`;
});
return {
...toRefs(state),
ChangeActive,
playMusic,
track,
audioProgressPercent,
};
}
主要的HTML
<div class="play-icon-container">
<img class="play-icon" src="../assets/arrow_01.png" alt="上一曲" />
<img
v-show="!playing"
@click="playMusic"
class="play-icon"
src="../assets/play_01.png"
alt="播放"
/>
<img
v-show="playing"
@click="playMusic"
class="play-icon"
src="../assets/play_02.png"
alt="暂停"
/>
<img class="play-icon" src="../assets/arrow_02.png" alt="下一曲" />
</div>
组件
<div class="main-container">
<Playing :songInfo="songInfo" :lyricIndex="lyricIndex" :lyricInfo="lyricInfo"></Playing>
</div>
组件具体实现
<template>
<div class="song-cover-lyric">
<div class="disc-continer">
<div class="poster" ref="rotate">
<img :src="songInfo.cover" alt="" />
</div>
<div class="song-name">{{ songInfo.name }}</div>
<div class="song-artistsName">{{ songInfo.artistsName }}</div>
</div>
<div class="lyric">
<div
ref="musicLyric"
class="music-lyric"
:style="{ 'padding-top': paddingTop }"
>
<div class="music-lyric-items" :style="lyricTop">
<template v-if="lyricInfo.length > 0">
<p
v-for="(item, index) in lyricInfo"
:key="index"
:data-index="index"
ref="lyric"
:style="{
color: lyricIndex === index ? colorLight : color,
'font-size': fontSize,
}"
>
{{ item.lyric }}
</p>
</template>
<p style="color: #fff" v-else>文案加载失败!</p>
</div>
</div>
</div>
</div>
</template>
<script>
import { reactive, ref, computed, onMounted, toRefs } from "vue";
// import axios from "../api/request"
export default {
props: {
songInfo: {
type: Object,
default: () => [],
},
lyricInfo: {
type: Object,
default: () => [],
},
lyricIndex: {
type: Number,
default: 0,
},
},
setup(props, { emit }) {
console.log(props.lyricInfo);
const state = reactive({
lyricInfo: [],
audioProgress: 0,
thumbTranslateX: 0,
top: 0,
color: "#fff", //文案默认颜色
colorLight: "#40ce8f", //文案高亮色
fontSize: "16px", //文案字体大小
lineHeight: "42", //每段行高
paddingTop: "200px", //高亮文案部分居中
});
// let rotate = ref(null);
onMounted(() => {
// GetLyric(props.songInfo.id);
});
// 计算歌词滚动
const lyricTop = computed (()=> {
return `transform :translate3d(0, ${(0 - state.lineHeight) *
(props.lyricIndex - state.top)}px, 0);color: ${state.color};line-height: ${
state.lineHeight
}px`;
})
return {
...toRefs(state),
lyricTop
};
},
};
</script>
<style lang="less" scoped>
.song-cover-lyric {
position: relative;
width: 100%;
height: 100%;
padding-bottom: 72px;
box-sizing: border-box;
display: flex;
overflow: hidden;
.disc-continer {
width: 50%;
height: 100%;
position: relative;
.poster {
position: relative;
width: 280px;
height: 280px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
left: 50%;
top: 80px;
margin-left: -140px;
box-shadow: 0 0 0 12px rgba(255, 255, 255, 0.4);
animation: animations1 12s linear infinite forwards;
animation-play-state: paused;
overflow: hidden;
margin-bottom: 120px;
img {
width: 100%;
height: 100%;
}
}
.song-name {
width: 100%;
height: 40px;
text-align: center;
font-size: 32px;
font-weight: 600;
color: #fff;
line-height: 40px;
}
.song-artistsName {
width: 100%;
height: 40px;
text-align: center;
font-size: 28px;
font-weight: 600;
color: #fff;
line-height: 40px;
margin-top: 24px;
}
@keyframes animations1 {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
.lyric {
width: 50%;
height: 590px;
position: relative;
overflow: hidden;
padding-top: 84px;
box-sizing: border-box;
.music-lyric {
// position: absolute;
// top: 1.9rem;
// right: 0;
// bottom: 0;
// left: 0;
// padding-top: 300px;
box-sizing: border-box;
overflow: hidden;
text-align: center;
mask-image: linear-gradient(
to bottom,
rgba(255, 255, 255, 0) 0,
rgba(255, 255, 255, 0.6) 15%,
rgba(255, 255, 255, 1) 25%,
rgba(255, 255, 255, 1) 75%,
rgba(255, 255, 255, 0.6) 85%,
rgba(255, 255, 255, 0) 100%
);
.music-lyric-items {
text-align: center;
font-size: 16px;
color: #fff;
transform: translate3d(0, 0, 0);
transition: transform 0.6s ease-out;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
.on {
color: #40ce8f;
}
P {
margin: 0;
padding: 0;
}
}
}
}
}
</style>