本次花了很大精力去完成了播放界面,虽然歌词同步这里没完成,但后续还是可以完善的,这次我重写了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>清空全部&nbsp;&nbsp;&nbsp;&nbsp;
                   </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>清空全部&nbsp;&nbsp;&nbsp;&nbsp;
                            </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就不展示了,都是之前展示过的东西,播放部分就此设计完毕,还是挺辛苦的,不过学到了很多东西,下次再设计此类项目时,会考虑异步处理,局部刷新这些东西。

Logo

前往低代码交流专区

更多推荐