基于springboot+vue(thymeleaf)+mysql下的自创音乐网站平台--CrushMusic(开发日志十四)--audio控件重写音乐播放
本次花了很大精力去完成了播放界面,虽然歌词同步这里没完成,但后续还是可以完善的,这次我重写了audio控件,让audio是自己想要的样式,先看成果图。这个界面参考的是酷狗音乐网页版的布局,感觉自己还原的还不错。我看网上都没有详细介绍audio控件的重写,这次我花费了挺多经历去了解这个,所以我决定详细介绍一下。先看我的布局,播放控件布局如下:这个布局就不介绍了,自己用div嵌套或表格就可以完成这个效
本次花了很大精力去完成了播放界面,虽然歌词同步这里没完成,但后续还是可以完善的,这次我重写了audio控件,让audio是自己想要的样式,先看成果图。
这个界面参考的是酷狗音乐网页版的布局,感觉自己还原的还不错。
我看网上都没有详细介绍audio控件的重写,这次我花费了挺多经历去了解这个,所以我决定详细介绍一下。
先看我的布局,播放控件布局如下:
这个布局就不介绍了,自己用div嵌套或表格就可以完成这个效果。
先介绍audio的属性:
src属性
<!--用于告诉video标签需要播放的音频地址-->
<audio src = "音频地址"> </audio>
autoplay属性
<!--用于告诉video标签是否需要自动播放音频-->
<audio aotuplay = "aotuplay"> </audio>
controls属性
<!--用于告诉video标签是否需要显示控制条-->
<audio controls = "controls"> </audio>
loop属性
<!--用于告诉video标签音频播放完毕之后是否需要循环播放-->
<audio loop = "loop">
</audio>
preload属性
<!--预加载音频,但是需要注意preload和autoplay相冲,如果设置了autoplay属性,那么preload属性就会失效-->
<audio preload = "preload">
</audio>
muted属性
<!--让音频静音-->
<audio muted = "muted">
</audio>
以上是audio的常用属性,我们知道原生的audio标签是非常丑陋的,如下图:
那么他这些样式的点击,肯定是触发了一些audio的一些属性,所以我们只需要重新写点击按键时,相应的属性进行变化即可,但原生audio必须存在,只不过我们把它要先隐藏起来,让它只起作用而不被用户看到,用户看到的是自己设计的布局。
<audio id="aud" src="" controls autoplay ref="audio" ontimeupdate="update()" oncanplay="loginFinish()" style="display: none;"></audio>
我们先从上一首、暂停/播放、下一首这里入手:
首先我们要把前端设计出来,这里的图片我采用的是bootstrap的图形库:
<!--切换上一首键-->
<div style="width: 60px;font-size: 40px;padding: 20px 10px 20px 10px;color: white;float: left;">
<a>
<i class="bi bi-skip-start-circle"></i>
</a>
</div>
<!--播放与暂停键-->
<div style="width: 80px;font-size: 50px;padding: 15px;color: white;float: left;">
<a>
<i id="p" class="bi bi-play-circle" style="display: none;"></i>
<i id="s" class="bi bi-pause-circle" style="display:block;"></i>
</a>
</div>
<!--切换下一首键-->
<div style="width: 60px;font-size: 40px;padding: 20px 10px 20px 10px;color: white;float: left;">
<a>
<i class="bi bi-skip-end-circle"></i>
</a>
</div>
这里我直接用style进行div调整,为了可读性更强,也可以进行css的封装,我这里重点不在这里,可以看到我们已经绘制出了三个div,分别对应三个按钮,其中第二个播放与暂停是有两个键的,所以我们要进行点击切换,初始是播放状态,用第一个,点击暂停会切换第二个图,这三个键都有点击事件,我们写出相应的点击的点击事件:
<!--切换上一首键-->
<div style="width: 60px;font-size: 40px;padding: 20px 10px 20px 10px;color: white;float: left;">
<a onclick="preMusic()">
<i class="bi bi-skip-start-circle"></i>
</a>
</div>
<!--播放与暂停键-->
<div style="width: 80px;font-size: 50px;padding: 15px;color: white;float: left;">
<a onclick="playMusic()">
<i id="p" class="bi bi-play-circle" style="display: none;"></i>
<i id="s" class="bi bi-pause-circle" style="display:block;"></i>
</a>
</div>
<!--切换下一首键-->
<div style="width: 60px;font-size: 40px;padding: 20px 10px 20px 10px;color: white;float: left;">
<a onclick="nextMusic()">
<i class="bi bi-skip-end-circle"></i>
</a>
</div>
先不急着写js函数,先将图形绘制完毕,接下来写这个前端div:
左边是一个图片,右边是名称和时间以及进度条,这种排列方式我们可以用表格来实现:
<div style="width: 500px;padding: 10px;float: left;">
<table>
<tr>
<!--歌曲封面-->
<td rowspan="2" width="60px"><img id="song_imgUrl" src="" width="60px;" height="60px;"></td>
<!--歌曲名称-->
<td width="300px" id="song_name" style="padding-left: 10px;color: white;">
</td>
<!--歌曲 播放时间/总时间-->
<td width="110px" style="color: white;">
<div style="float: right"><span id="time"></span></div>
</td>
</tr>
<tr>
<!--音频进度条-->
<td colspan="2">
<input type="range" id="range" oninput="onChange()" onchange="onChange()" min="0" max="360" value="0" style="padding-left: 10px;margin-bottom: 10px;">
</td>
</tr>
</table>
</div>
将id设置好,因为之后要让js根据歌曲来进行赋值,音频进度条这里,HTML5新增的属性type="range",是专门针对进度条的,先设置好,oninput与onchage都是检测进度条变化触发函数的,这对于显示时间以及进度条变化非常重要,min=0,max=360,是将进度条分割成360份。
接下来设置右边四个按钮,分别为静音键、循环方式、下载键、歌曲列表键。
音量键:
<!--音量键-->
<div style="width: 75px;float: left;font-size: 20px;padding: 30px 25px 30px 25px;">
<a onclick="clickVoice()">
<i id="volumeUp" class="bi bi-volume-up" style="display: block;"></i>
<i id="volumeMute" class="bi bi-volume-mute" style="display: none;"></i>
</a>
</div>
音量键这里,我只重写了音量静音与不静音选择,所以有两个图标,一个为喇叭、一个为静音喇叭,也是通过点击来触发display来切换显示。
循环方式键:
<!--播放方式键-->
<div style="width: 75px;float: left;font-size: 20px;padding: 30px 25px 30px 25px;">
<a onclick="clickMode()">
<i id="playOrder" class="bi bi-arrow-left-right" style="display: block;" title="顺序播放"></i>
<i id="playRandom" class="bi bi-arrows-move" style="display: none;" title="随机播放"></i>
<i id="playSingle" class="bi bi-arrow-repeat" style="display: none;" title="单曲循环"></i>
</a>
</div>
我们有三种循环方式,一个为顺序循环、一个为随机循环、一个为单曲循环,也是点击切换这三个图标,从而触发不同的cllickMode函数功能。
下载键:
<!--下载键-->
<div style="width: 75px;float: left;font-size: 20px;padding: 30px 25px 30px 25px;">
<a id="download" style="color: white;">
<i class="bi bi-download"></i>
</a>
</div>
下载键没有触发相应的函数,因为a标签有下载功能,所以直接进行id接收js传值就可以进行下载相应的mp3了。
歌曲列表键:
点击歌曲列表会弹出一个歌曲列表框,这个框里装的就是从后台传来的歌曲列表的数据:
<!--歌曲播放列表键-->
<div style="width: 75px;float: left;font-size: 20px;padding: 30px 25px 30px 25px;position: relative;">
<a onclick="openList()"><i class="bi bi-music-note-list"></i>
<div id="list" style="position: absolute;width: 300px;height: 500px;bottom: 30px; left: 100px;border-radius: 3%;background-color: rgb(60,60,60);padding: 20px;overflow-x: scroll;display: none;color: white;z-index: 2">
<div style="position: absolute;top: 0;width: 100%;height: 25px;">
<span style="float: left;">歌曲列表</span>
<a style="color: dimgray;float: right;" th:href="@{/song/delAllPlaySong}">
<i class="bi bi-trash"></i>清空全部
</a>
</div>
<hr style="color: rgb(211,211,211);border-color: rgb(211,211,211)">
<ul id="ulList">
<li th:each="song,songStat:${session.songPlayList}" th:onclick="|playList(${songStat.index})|" th:value="${song.getSong_mp3Url()}" th:text="${song.getSong_name()}+' '+${song.getSong_singerName()}" th:text2="${song.getSong_name()}+'-'+${song.getSong_singerName()}" th:img="${song.getSong_albumImgUrl()}" th:lrc="${song.getSong_lyc()}"><hr></li>
</ul>
</div>
</a>
</div>
通过点击这个键,会触发openList()函数,在函数里会将<div id="list">的display属性设置为block,从而出现该歌曲栏的框,而里面的列表是后台传过来进行一个遍历生成的,在歌曲列表也有一个点击事件playList(),是根据点击列表里的歌就行播放相应的歌曲,然后就是先设置一些值在这里,比如img、text、text2、lrc等,都是后台传过来的数据,我们这样设置是为了js能够取到。
这样我们的播放框就设置完毕,那我们来实现js代码:
先将前端设置的id全都获取出来:
let list = document.getElementById("list");
let audio = document.getElementById("aud");
let li = document.getElementById("ulList").getElementsByTagName("li");
let range = document.getElementById("range");
let time = document.getElementById("time");
let playOrder = document.getElementById("playOrder");
let playRandom = document.getElementById("playRandom");
let playSingle = document.getElementById("playSingle");
let song_name = document.getElementById("song_name");
let song_imgUrl = document.getElementById("song_imgUrl");
let download = document.getElementById("download");
let background = document.getElementById("background");
let song_img = document.getElementById("song_img");
let song_title = document.getElementById("song_title");
let song_lrc = document.getElementById("song_lrc");
首先从逻辑上来分析,我们用户进入这个界面肯定是从前台点击了一首歌曲的,那么在点进来时就必须进行播放,这个时候我们就要初始化播放一首歌。
//页面加载自动播放第一首歌
window.onload = function () {
li[0].className = 'play';
audio.src = li[0].getAttribute('value');
setNameAndImg(0);
audio.play();
};
设置class为play,因为只有为play的才是播放的歌曲,后面我们要通过play来判断当前播放的是哪首歌,从刚才设置的列表属性里获取在0号位置歌曲的value,也就是mp3路径,赋值给audio.src就完成了对播放歌曲的重写,audio.play()就是启动播放的意思,setNameAndImg()是一个自定义的函数,因为我们页面上也要获取播放的是什么歌、封面等信息,还有歌词部分等信息。
//给前端页面设置歌曲名字与封面及各种信息、歌词等
function setNameAndImg(i) {
let name = li[i].getAttribute('text2');
let img = li[i].getAttribute('img');
song_name.innerText = name;
song_imgUrl.src = img;
download.href = img;
download.download = name + '.mp3';
}
直接对相应的id属性绑定的进行赋值即可,这样我们就可以获取值将值赋给前台显示。
接下来是获取正在播放的歌曲的函数:
//获取正在播放的音乐
function getPlay() {
for (let r = 0; r < li.length; r++) {
if (li[r].getAttribute('class') === 'play') {
return r;
}
}
}
可以看到通过遍历li标签下的每一层,检查是否有class等于play的属性,有就认为它正在播放,就可以返回相应的下标。
接下来是切换上一首按钮的函数:
//上一首歌曲(需要根据循环播放的方式来跳转上一首)
function preMusic() {
let a = getPlay();
if(playOrder.style.display === "block"||playSingle.style.display === "block"){
//单曲与顺序播放都采用顺序切换方式
a--;
if(a < 0){//判断是否为第一首
a = li.length - 1;
}
setNameAndImg(a);
}else if(playRandom.style.display === "block"){
//随机播放采用随机切换方式(取0-li.length之间的随机整数,向下取整,左闭右开)
a = Math.floor(Math.random() * (li.length));
setNameAndImg(a);
}
for(let i = 0;i<li.length;++i){
li[i].className = '';
}
audio.src = li[a].getAttribute('value');//切换到上一首
audio.play();
li[a].className = 'play';
}
先获取正在播放歌曲的位置,检查播放的方式,如果是顺序播放与单曲播放的图标display为block的话,那就是顺序切换即可,将下标进行减一,如果减一之后小于0,那么就证明到头了,直接切换成队尾即可,然后调用设置信息的函数,将信息更新成切换歌手的信息;同理如果是随机播放的话,那就是playRandom的display为block,那生成一个随机数,从0到列表长度-1之间的随机整数,将该歌曲下标作为下一首播放的歌曲。然后进行遍历循环,将所有li的class设置为空,将当前准备播放的li的class设置为play,然后进行对audio.src的重新赋mp3路径值,进行播放即可。
点击播放下一首也是同样道理,不用解释了:
//下一首歌曲(需要根据循环播放的方式来跳转下一首)
function nextMusic() {
let a = getPlay();
if(playOrder.style.display === "block"||playSingle.style.display === "block"){
//单曲与顺序播放都采用顺序切换方式
a++;
if(a > li.length - 1){//判断是否为最后一首,然后循环播放
a = 0;
}
setNameAndImg(a);
}else if(playRandom.style.display === "block"){
//随机播放采用随机切换方式(取0-li.length之间的随机整数,向下取整,左闭右开)
a = Math.floor(Math.random() * (li.length));
setNameAndImg(a);
}
for(let i = 0;i<li.length;i++){
li[i].className = '';
}
audio.src = li[a].getAttribute('value');//切换到下一首
audio.play();
li[a].className = 'play';
}
之后是点击播放或暂停的按钮:
//点击播放或暂停歌曲
function playMusic() {
let p = document.getElementById('p');
let s = document.getElementById('s');
if(audio.paused){
audio.play();
p.style.display = 'none';
s.style.display = 'block';
}else {
audio.pause();
p.style.display = 'block';
s.style.display = 'none';
}
}
获取两个按钮的id,进行图标切换显示即可,当audio.play时为播放,当audio.pause时,为暂停,这二者是audio自带的属性。
进行点击列表播放:
//点击列表播放
function playList(i){
let r = getPlay();
li[r].className = '';
li[i].className = 'play';
setNameAndImg(i);
audio.src = li[i].getAttribute('value');
audio.play();
}
前端点击列表触发playList函数并且携带了下标,获取正在播放歌曲的下标,将其class设置为空,将点击列表下标的class设置为play,同时调用设置信息的函数,并且给src赋值并播放该歌曲。
audio还有个属性就是:audio.onended,这个属性用来检测是否播放完毕,如果播放完毕那就会执行,因为我们设置了循环方式,所以必须要重写这个方法:
//当前播放结束后,根据循环播放方式选择下一首
audio.onended = function () {
let a = getPlay();
if(playOrder.style.display === "block"){
//顺序播放方式
a++;
if(a > li.length - 1){
a = 0;
}
setNameAndImg(a);
}else if(playRandom.style.display === "block"){
//随机播放方式(取0-li.length之间的随机整数,向下取整,左闭右开)
a = Math.floor(Math.random() * (li.length));
setNameAndImg(a);
}//单曲循环不用设置a,a不变则为单曲循环
for (let i = 0; i < li.length; i++){
li[i].className = '';
}
audio.src = li[a].getAttribute('value');
audio.play();
li[a].className = 'play';
};
获取循环方式,设置下一首自动播放的歌曲,原理与切换歌曲类似,不做介绍。
还有点击切换循环方式:
//点击切换播放方式,有顺序、随机、单曲,三种循环方式
function clickMode() {
//三个方式,每点击一次切换一次,切换顺序为:顺序、随机、单曲
if(playOrder.style.display === "block"){
playOrder.style.display = "none";
playRandom.style.display = "block";
}else if(playRandom.style.display === "block"){
playRandom.style.display = "none";
playSingle.style.display = "block";
}else{
playSingle.style.display = "none";
playOrder.style.display = "block";
}
}
就是一个图标的切换,方便前面的判断。
写到这里,我们切歌和播放歌曲都能成功执行了,接下来写进度条的方法,进度条会随着mp3播放而变化,时间轴显示的也会同时变化,我们来实现这个。
先将进度条的css列出来:
input[type='range'] {
outline: none;
-webkit-appearance: none; /*清除系统默认样式*/
width: 100% !important;
background: -webkit-linear-gradient(#10a9ff, #10a9ff) no-repeat, #dddddd; /*背景颜色,俩个颜色分别对应上下*/
background-size: 0% 100%; /*设置左右宽度比例,这里可以设置为拖动条属性*/
height: 2px; /*横条的高度,细的真的比较好看嗯*/
}
/*拖动块的样式*/
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none; /*清除系统默认样式*/
height: 10px; /*拖动块高度*/
width: 3px; /*拖动块宽度*/
background: white; /*拖动块背景*/
}
通过重写range属性即可实现对进度条的重写,上面是进度条样式,下面是拖动快样式。
先获取音频的长度
//当音频加载开始播放后,将获取audio控件里duration属性获取总的音频长度,并换名字为totalTime
function loginFinish() {
audio.totalTime = audio.duration;
}
audio.duration是audio标签内置的属性,用来获取mp3音频的总时长。
我们之前的html代码上在进度条标签那里设置了一个检测进度条改变的函数onchange,该触发的函数为:
之前的前端代码:
<!--音频进度条-->
<td colspan="2">
<input type="range" id="range" oninput="onChange()" onchange="onChange()" min="0" max="360" value="0" style="padding-left: 10px;margin-bottom: 10px;">
</td>
触发的函数:(可以自行百度oninput与onchange的作用)
// 拖动进度条得到的回调
function onChange() {
//获取当前拖动range的值,最大360,最小0
let value = range.value;
//设置进度条的面积百分比
range.style.backgroundSize = ((value / 360) * 100).toFixed(1) + '%';
//获取当前进度的时间并且赋值给audio控件
audio.currentTime = (value / 360) * audio.totalTime;
}
获取当前滑块的位置,最大为360,最小为0,通过range.value可以得到返回的位置,这样我们对range的进度条面积进行更改,得到百分比来进行更改,实现了拖动滑块时,range颜色面积也跟着改变,通过将audio.currentTime设置为百分比总的音频长度,从而实现当前播放的音频位置跟滑块的位置也对应起来了,可以通过滑块位置来变化音频播放时间。
那么我们在不拖动滑块时,音频是自动播放的,那么进度条和显示时间是如何进行改变的呢,那么先看之前的HTML代码:
<audio id="aud" src="" controls autoplay ref="audio" ontimeupdate="update()" oncanplay="loginFinish()" style="display: none;"></audio>
可以看到我们在audio标签处设置了一个ontimeupdate属性,这个属性就会根据时间变化不停地去调用uodate()函数,我们的update函数如下:
// 实时进度变化的时候的回调--实时改变文字以及进度条
function update() {
// 改变进度条的值
range.value = ((audio.currentTime / audio.totalTime) * 360).toFixed(1);
// 进度条的值改变的时候面积也跟着改变
range.style.backgroundSize = ((audio.currentTime / audio.totalTime) * 100).toFixed(1) + '%';
// 文字显示
time.innerHTML = formatTime(audio.currentTime)+'/'+formatTime(audio.totalTime);
}
通过当前audio的音频时间去获取占总音频时间的一个比值,这样对range.value进行设置即可改变滑块的位置,同时也用相同方法去设置进度条面积变化,最后再将处理好的时间传到前端页面id="time"的div里面去,格式为:当前音频时间/总音频时间
因为audio的音频时间是用秒计算的,所以我们要转格式,调用formatTime,自定义的转换器:
//将秒数转换为显示出来的分钟数 格式为 11:11
function formatTime(value) {
let second = 0;
let minute = 0;
minute = parseInt(value / 60);
second = parseInt(value % 60);
//小于10的数,十位补0
minute = minute < 10 ? '0' + minute : minute;
second = second < 10 ? '0' + second : second;
return minute + ':' + second;
}
将秒数转换为00:00/00:00这种格式,返回回去,最终效果就是:
最后一个播放栏地方就是音量的设置,音量是可以重写控制音量大小的,这里我觉得不太重要就没写,毕竟可以从外面控制音量大小,我只写了一个静音键:
audio.muted属性为true的话就是静音,为false就是取消静音,默认为false:
//点击静音与取消静音
function clickVoice() {
let volumeUp = document.getElementById("volumeUp");
let volumeMute = document.getElementById("volumeMute");
if(volumeUp.style.display === "block"){
//切换图像
volumeUp.style.display = "none";
volumeMute.style.display = "block";
//静音
audio.muted = true;
}else {
//切换图像
volumeUp.style.display = "block";
volumeMute.style.display = "none";
//取消静音
audio.muted = false;
}
}
也进行了图标的切换,不过多解释。
这样我们的播放栏模块就可以自由自在控制歌曲的播放了,audio重要标签也重写完毕。
接下来介绍另一个模块,就是歌词方面的。
可以看到我们有背景图的模糊效果以及歌曲封面和歌词部分。
html代码:
<div style="width: 50%;padding-top: 100px;z-index: 1;position: relative;margin: 0 auto;">
<table>
<tr>
<!--歌曲封面-->
<td style="width: 40%;padding-bottom: 100px;" rowspan="2"><img id="song_img" width="260px" height="260px" src=""></td>
<!--歌曲标题-->
<td id="song_title" style="text-align: center;width: 60%;color: white;font-family: '黑体';font-size: 25px;"></td>
</tr>
<tr>
<!--歌词-->
<td>
<textarea cols="50" rows="27" id="song_lrc" readonly
style="resize: none;white-space:pre-line;background-color:transparent;border: 0;color: white;font-size: 20px;">
</textarea>
</td>
</tr>
</table>
</div>
首先这些属性需要有js进行设置传值下来,我们对之前写的setNameAndImg进行添加属性:
//给前端页面设置歌曲名字与封面及各种信息、歌词等
function setNameAndImg(i) {
let name = li[i].getAttribute('text2');
let img = li[i].getAttribute('img');
let lrcFile = li[i].getAttribute('lrc');
let ajax = new XMLHttpRequest();
ajax.open("get",lrcFile,true);
ajax.onload = function(){
if(ajax.status === 200){
song_lrc.value = ajax.responseText;
}
};
ajax.send();
song_name.innerText = name;
song_imgUrl.src = img;
download.href = img;
download.download = name + '.mp3';
background.style.backgroundImage = 'url('+img+')';
song_img.src = img;
song_title.innerText = name;
}
我们获取了lrc文件路径,如何让前端能打开这个文件并显示呢,我尝试了很多遍,最终选用了ajax的XMLHttpRequest这个方法,用get的方式打开这个文件,如果状态码是正确的,那就将xhr对象响应的文本赋值给歌词显示的区域即可,以及封面和标题,直接获取即可。
背景图也是用的封面图,对background.style.backgroundImage进行设置即可。
这个背景图是脱离了浮动的,所以采用了absolute的方式定位,这样歌词等信息的div才能通过z-index在背景图的上面,对应的html代码:
<div id="background" class="bg" style="width: 100%;height: 800px;top: 90px;position: absolute;z-index: 0"></div>
模糊效果通过.bg实现:
.bg{
width: 100%;
background: url(/images/background/加载中.png) no-repeat;
background-size: 100% 1800px;
background-attachment:fixed;
/*进行模糊处理*/
-webkit-filter: blur(100px);
filter: blur(100px);
}
那么播放界面就完成了。
附上前端实现的完整代码:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<title>音乐播放</title>
<div th:replace="~{reception/common/common::cjbar}"/>
<style>
.bg{
width: 100%;
background: url(/images/background/加载中.png) no-repeat;
background-size: 100% 1800px;
background-attachment:fixed;
/*进行模糊处理*/
-webkit-filter: blur(100px);
filter: blur(100px);
}
</style>
</head>
<body>
<div th:replace="~{reception/common/common::topbar}"/>
<div th:replace="~{reception/common/common::topbar2}"/>
<div id="background" class="bg" style="width: 100%;height: 800px;top: 90px;position: absolute;z-index: 0"></div>
<div style="width: 50%;padding-top: 100px;z-index: 1;position: relative;margin: 0 auto;">
<table>
<tr>
<!--歌曲封面-->
<td style="width: 40%;padding-bottom: 100px;" rowspan="2"><img id="song_img" width="260px" height="260px" src=""></td>
<!--歌曲标题-->
<td id="song_title" style="text-align: center;width: 60%;color: white;font-family: '黑体';font-size: 25px;"></td>
</tr>
<tr>
<!--歌词-->
<td>
<textarea cols="50" rows="27" id="song_lrc" readonly
style="resize: none;white-space:pre-line;background-color:transparent;border: 0;color: white;font-size: 20px;">
</textarea>
</td>
</tr>
</table>
</div>
<div class="bottom-navigation">
<audio id="aud" src="" controls autoplay ref="audio" ontimeupdate="update()" oncanplay="loginFinish()" style="display: none;"></audio>
<div style="width: 1000px;margin: 0 auto;">
<div style="width: 200px;float: left;">
<!--切换上一首键-->
<div style="width: 60px;font-size: 40px;padding: 20px 10px 20px 10px;color: white;float: left;">
<a onclick="preMusic()">
<i class="bi bi-skip-start-circle"></i>
</a>
</div>
<!--播放与暂停键-->
<div style="width: 80px;font-size: 50px;padding: 15px;color: white;float: left;">
<a onclick="playMusic()">
<i id="p" class="bi bi-play-circle" style="display: none;"></i>
<i id="s" class="bi bi-pause-circle" style="display:block;"></i>
</a>
</div>
<!--切换下一首键-->
<div style="width: 60px;font-size: 40px;padding: 20px 10px 20px 10px;color: white;float: left;">
<a onclick="nextMusic()">
<i class="bi bi-skip-end-circle"></i>
</a>
</div>
</div>
<div style="width: 500px;padding: 10px;float: left;">
<table>
<tr>
<!--歌曲封面-->
<td rowspan="2" width="60px"><img id="song_imgUrl" src="" width="60px;" height="60px;"></td>
<!--歌曲名称-->
<td width="300px" id="song_name" style="padding-left: 10px;color: white;"></td>
<!--歌曲 播放时间/总时间-->
<td width="110px" style="color: white;">
<div style="float: right"><span id="time"></span></div>
</td>
</tr>
<tr>
<!--音频进度条-->
<td colspan="2">
<input type="range" id="range" oninput="onChange()" onchange="onChange()" min="0" max="360" value="0" style="padding-left: 10px;margin-bottom: 10px;">
</td>
</tr>
</table>
</div>
<div style="width: 300px;float: left;color: white;">
<!--音量键-->
<div style="width: 75px;float: left;font-size: 20px;padding: 30px 25px 30px 25px;">
<a onclick="clickVoice()">
<i id="volumeUp" class="bi bi-volume-up" style="display: block;"></i>
<i id="volumeMute" class="bi bi-volume-mute" style="display: none;"></i>
</a>
</div>
<!--播放方式键-->
<div style="width: 75px;float: left;font-size: 20px;padding: 30px 25px 30px 25px;">
<a onclick="clickMode()">
<i id="playOrder" class="bi bi-arrow-left-right" style="display: block;" title="顺序播放"></i>
<i id="playRandom" class="bi bi-arrows-move" style="display: none;" title="随机播放"></i>
<i id="playSingle" class="bi bi-arrow-repeat" style="display: none;" title="单曲循环"></i>
</a>
</div>
<!--下载键-->
<div style="width: 75px;float: left;font-size: 20px;padding: 30px 25px 30px 25px;">
<a id="download" style="color: white;">
<i class="bi bi-download"></i>
</a>
</div>
<!--歌曲播放列表键-->
<div style="width: 75px;float: left;font-size: 20px;padding: 30px 25px 30px 25px;position: relative;">
<a onclick="openList()"><i class="bi bi-music-note-list"></i>
<div id="list" style="position: absolute;width: 300px;height: 500px;bottom: 30px; left: 100px;border-radius: 3%;
background-color: rgb(60,60,60);padding: 20px;overflow-x: scroll;display: none;color: white;z-index: 2">
<div style="position: absolute;top: 0;width: 100%;height: 25px;">
<span style="float: left;">歌曲列表</span>
<a style="color: dimgray;float: right;" th:href="@{/song/delAllPlaySong}">
<i class="bi bi-trash"></i>清空全部
</a>
</div>
<hr style="color: rgb(211,211,211);border-color: rgb(211,211,211)">
<ul id="ulList">
<li th:each="song,songStat:${session.songPlayList}" th:onclick="|playList(${songStat.index})|"
th:value="${song.getSong_mp3Url()}" th:text="${song.getSong_name()}+' '+${song.getSong_singerName()}"
th:text2="${song.getSong_name()}+'-'+${song.getSong_singerName()}" th:img="${song.getSong_albumImgUrl()}"
th:lrc="${song.getSong_lyc()}"><hr></li>
</ul>
</div>
</a>
</div>
</div>
</div>
</div>
</body>
<script>
let list = document.getElementById("list");
let audio = document.getElementById("aud");
let li = document.getElementById("ulList").getElementsByTagName("li");
let range = document.getElementById("range");
let time = document.getElementById("time");
let playOrder = document.getElementById("playOrder");
let playRandom = document.getElementById("playRandom");
let playSingle = document.getElementById("playSingle");
let song_name = document.getElementById("song_name");
let song_imgUrl = document.getElementById("song_imgUrl");
let download = document.getElementById("download");
let background = document.getElementById("background");
let song_img = document.getElementById("song_img");
let song_title = document.getElementById("song_title");
let song_lrc = document.getElementById("song_lrc");
/*播放切歌模块*/
//给前端页面设置歌曲名字与封面及各种信息、歌词等
function setNameAndImg(i) {
let name = li[i].getAttribute('text2');
let img = li[i].getAttribute('img');
let lrcFile = li[i].getAttribute('lrc');
let ajax = new XMLHttpRequest();
ajax.open("get",lrcFile,true);
ajax.onload = function(){
if(ajax.status === 200){
song_lrc.value = ajax.responseText;
}
};
ajax.send();
song_name.innerText = name;
song_imgUrl.src = img;
download.href = img;
download.download = name + '.mp3';
background.style.backgroundImage = 'url('+img+')';
song_img.src = img;
song_title.innerText = name;
}
//页面加载自动播放第一首歌
window.onload = function () {
li[0].className = 'play';
audio.src = li[0].getAttribute('value');
setNameAndImg(0);
audio.play();
};
//获取正在播放的音乐
function getPlay() {
for (let r = 0; r < li.length; r++) {
if (li[r].getAttribute('class') === 'play') {
return r;
}
}
}
//点击列表播放
function playList(i){
let r = getPlay();
li[r].className = '';
li[i].className = 'play';
setNameAndImg(i);
audio.src = li[i].getAttribute('value');
audio.play();
}
//当前播放结束后,根据循环播放方式选择下一首
audio.onended = function () {
let a = getPlay();
if(playOrder.style.display === "block"){
//顺序播放方式
a++;
if(a > li.length - 1){
a = 0;
}
setNameAndImg(a);
}else if(playRandom.style.display === "block"){
//随机播放方式(取0-li.length之间的随机整数,向下取整,左闭右开)
a = Math.floor(Math.random() * (li.length));
setNameAndImg(a);
}//单曲循环不用设置a,a不变则为单曲循环
for (let i = 0; i < li.length; i++){
li[i].className = '';
}
audio.src = li[a].getAttribute('value');
audio.play();
li[a].className = 'play';
};
//点击播放或暂停歌曲
function playMusic() {
let p = document.getElementById('p');
let s = document.getElementById('s');
if(audio.paused){
audio.play();
p.style.display = 'none';
s.style.display = 'block';
}else {
audio.pause();
p.style.display = 'block';
s.style.display = 'none';
}
}
//上一首歌曲(需要根据循环播放的方式来跳转上一首)
function preMusic() {
let a = getPlay();
if(playOrder.style.display === "block"||playSingle.style.display === "block"){
//单曲与顺序播放都采用顺序切换方式
a--;
if(a < 0){//判断是否为第一首
a = li.length - 1;
}
setNameAndImg(a);
}else if(playRandom.style.display === "block"){
//随机播放采用随机切换方式(取0-li.length之间的随机整数,向下取整,左闭右开)
a = Math.floor(Math.random() * (li.length));
setNameAndImg(a);
}
for(let i = 0;i<li.length;++i){
li[i].className = '';
}
audio.src = li[a].getAttribute('value');//切换到上一首
audio.play();
li[a].className = 'play';
}
//下一首歌曲(需要根据循环播放的方式来跳转下一首)
function nextMusic() {
let a = getPlay();
if(playOrder.style.display === "block"||playSingle.style.display === "block"){
//单曲与顺序播放都采用顺序切换方式
a++;
if(a > li.length - 1){//判断是否为最后一首,然后循环播放
a = 0;
}
setNameAndImg(a);
}else if(playRandom.style.display === "block"){
//随机播放采用随机切换方式(取0-li.length之间的随机整数,向下取整,左闭右开)
a = Math.floor(Math.random() * (li.length));
setNameAndImg(a);
}
for(let i = 0;i<li.length;i++){
li[i].className = '';
}
audio.src = li[a].getAttribute('value');//切换到下一首
audio.play();
li[a].className = 'play';
}
//点击打开列表窗口,再次点击关闭
function openList(){
if (list.style.display === "none"){
list.style.display = "block";
}else {
list.style.display = "none";
}
}
/*进度条模块*/
//当音频加载开始播放后,将获取audio控件里duration属性获取总的音频长度,并换名字为totalTime
function loginFinish() {
audio.totalTime = audio.duration;
}
// 拖动进度条得到的回调
function onChange() {
//获取当前拖动range的值,最大360,最小0
let value = range.value;
//设置进度条的面积百分比
range.style.backgroundSize = ((value / 360) * 100).toFixed(1) + '%';
//获取当前进度的时间并且赋值给audio控件
audio.currentTime = (value / 360) * audio.totalTime;
}
// 实时进度变化的时候的回调--实时改变文字以及进度条
function update() {
// 改变进度条的值
range.value = ((audio.currentTime / audio.totalTime) * 360).toFixed(1);
// 进度条的值改变的时候面积也跟着改变
range.style.backgroundSize = ((audio.currentTime / audio.totalTime) * 100).toFixed(1) + '%';
// 文字显示
time.innerHTML = formatTime(audio.currentTime)+'/'+formatTime(audio.totalTime);
}
//将秒数转换为显示出来的分钟数 格式为 11:11
function formatTime(value) {
let second = 0;
let minute = 0;
minute = parseInt(value / 60);
second = parseInt(value % 60);
//小于10的数,十位补0
minute = minute < 10 ? '0' + minute : minute;
second = second < 10 ? '0' + second : second;
return minute + ':' + second;
}
/*控制音量模块*/
//点击静音与取消静音
function clickVoice() {
let volumeUp = document.getElementById("volumeUp");
let volumeMute = document.getElementById("volumeMute");
if(volumeUp.style.display === "block"){
//切换图像
volumeUp.style.display = "none";
volumeMute.style.display = "block";
//静音
audio.muted = true;
}else {
//切换图像
volumeUp.style.display = "block";
volumeMute.style.display = "none";
//取消静音
audio.muted = false;
}
}
/*切换播放方式*/
//点击切换播放方式,有顺序、随机、单曲,三种循环方式
function clickMode() {
//三个方式,每点击一次切换一次,切换顺序为:顺序、随机、单曲
if(playOrder.style.display === "block"){
playOrder.style.display = "none";
playRandom.style.display = "block";
}else if(playRandom.style.display === "block"){
playRandom.style.display = "none";
playSingle.style.display = "block";
}else{
playSingle.style.display = "none";
playOrder.style.display = "block";
}
}
</script>
</html>
剩下部分为后端部分,后端如何给前端传值呢,只需要一个播放歌曲列表即可,有几种传值方式,一种是在歌曲列表直接点播放,那就会播放这首歌曲,并且是加上该歌曲到歌曲列表的首部;另一种是添加歌曲,是不会立即播放该歌曲,即添加到播放列表的尾部;还有一种为播放全部,则为直接播放整个列表的歌曲,这部分分为当前是歌单的歌曲列表,或者是歌手的歌曲列表,或是专辑的歌曲列表,通过不同的方式来实现歌曲列表的更新。
直接附上后端添加歌曲列表的控制器:
/**
* 播放单首歌曲,添加到播放列表头部
* @param song_id 歌曲ID
*/
@GetMapping(path = "playMusic")
public String playMusic(@Param("song_id")Integer song_id,HttpServletRequest req){
HttpSession session = req.getSession();
User user = (User) session.getAttribute("LoginUser");
List<Song2> songPlayList = (List<Song2>) session.getAttribute("songPlayList");
Song2 song2 = songService.findSongById2(song_id,user.getUser_id());
songPlayList.add(0,song2);
session.setAttribute("songPlayList",songPlayList);
return "reception/playMusic/playMusic";
}
/**
* 添加单首歌曲,添加到播放列表尾部
* @param song_id 歌曲ID
*/
@GetMapping(path = "addPlayMusic")
public void addPlayMusic(@Param("song_id")Integer song_id,HttpServletRequest req){
HttpSession session = req.getSession();
User user = (User) session.getAttribute("LoginUser");
Song2 song2 = songService.findSongById2(song_id,user.getUser_id());
List<Song2> songPlayList = (List<Song2>) session.getAttribute("songPlayList");
songPlayList.add(song2);
session.setAttribute("songPlayList",songPlayList);
}
/**
* 播放当前歌单全部歌曲
* @param sheet_id 歌单ID
*/
@GetMapping(path = "playAllSongBySheet")
public String playAllSongBySheet(@Param("sheet_id")Integer sheet_id,HttpServletRequest req){
HttpSession session = req.getSession();
User user = (User) session.getAttribute("LoginUser");
List<Song2> songPlayList = new ArrayList<>();
if(sheet_id == 0){
//该歌单为我喜欢歌单列表,数据库并不存在,所以单独列出来
songPlayList = songService.findSongByUserAndSong(user.getUser_id());
}else {
songPlayList = songService.findSongBySheetId(sheet_id,user.getUser_id());
}
session.setAttribute("songPlayList",songPlayList);
return "reception/playMusic/playMusic";
}
/**
* 播放当前专辑全部歌曲
* @param album_id 专辑ID
*/
@GetMapping(path = "playAllSongByAlbum")
public String playAllSongByAlbum(@Param("album_id")Integer album_id,HttpServletRequest req){
HttpSession session = req.getSession();
User user = (User) session.getAttribute("LoginUser");
List<Song2> songPlayList = new ArrayList<>();
songPlayList = songService.findSongByAlbumId2(album_id,user.getUser_id());
session.setAttribute("songPlayList",songPlayList);
return "reception/playMusic/playMusic";
}
/**
* 播放当前歌手全部歌曲
* @param singer_id 歌手ID
*/
@GetMapping(path = "playAllSongBySinger")
public String playAllSongBySinger(@Param("singer_id")Integer singer_id,HttpServletRequest req){
HttpSession session = req.getSession();
List<Song2> songPlayList = new ArrayList<>();
songPlayList = songService.findSongBySingerId2(singer_id);
session.setAttribute("songPlayList",songPlayList);
return "reception/playMusic/playMusic";
}
/**
* 清空当前歌单列表
*/
@GetMapping(path = "delAllPlaySong")
public String delAllPlaySong(HttpServletRequest req){
HttpSession session = req.getSession();
session.removeAttribute("songPlayList");
return "reception/playMusic/playMusic";
}
后台逻辑以及mybatis就不展示了,都是之前展示过的东西,播放部分就此设计完毕,还是挺辛苦的,不过学到了很多东西,下次再设计此类项目时,会考虑异步处理,局部刷新这些东西。
更多推荐
所有评论(0)