基于Vue3打造一個(gè)高級(jí)的音樂(lè)播放器
效果預(yù)覽
我們最終實(shí)現(xiàn)的音樂(lè)播放器將包含以下特性:
- 專(zhuān)輯封面旋轉(zhuǎn)動(dòng)畫(huà)
- 歌曲信息展示
- 可交互的進(jìn)度條
- 播放控制按鈕
- 音頻可視化效果
效果圖:

實(shí)現(xiàn)步驟
1. 播放器布局設(shè)計(jì)
播放器主要分為以下幾個(gè)區(qū)域:
<div class="music-player">
<!-- 專(zhuān)輯區(qū)域 -->
<div class="album-container">
<!-- 專(zhuān)輯封面和底座 -->
</div>
<!-- 歌曲信息 -->
<div class="song-info">
<!-- 歌曲標(biāo)題和藝術(shù)家 -->
</div>
<!-- 進(jìn)度條 -->
<div class="progress-container">
<!-- 可交互進(jìn)度條和時(shí)間顯示 -->
</div>
<!-- 控制按鈕 -->
<div class="controls">
<!-- 播放/暫停、上一曲、下一曲按鈕 -->
</div>
<!-- 音頻可視化 -->
<div class="visualizer">
<!-- 動(dòng)態(tài)音頻條 -->
</div>
</div>
2. 響應(yīng)式數(shù)據(jù)與計(jì)算屬性
使用Vue 3的ref和computed來(lái)管理播放器狀態(tài):
import { ref, computed, onMounted } from 'vue';
// 響應(yīng)式數(shù)據(jù)
const isPlaying = ref(false);
const currentTime = ref(0);
const duration = ref(240); // 4分鐘
const currentSongIndex = ref(0);
// 歌曲列表
const songs = ref([
{
title: '夜空中最亮的星',
artist: '逃跑計(jì)劃',
cover: 'https://picsum.photos/400/400?random=1',
},
// 更多歌曲...
]);
// 計(jì)算當(dāng)前歌曲
const currentSong = computed(() => songs.value[currentSongIndex.value]);
// 計(jì)算進(jìn)度百分比
const progressPercent = computed(() => {
return (currentTime.value / duration.value) * 100 + '%';
});
3. 核心功能實(shí)現(xiàn)
播放控制
// 播放/暫停切換
const togglePlay = () => {
isPlaying.value = !isPlaying.value;
};
// 上一曲
const prevSong = () => {
currentSongIndex.value =
currentSongIndex.value > 0 ? currentSongIndex.value - 1 : songs.value.length - 1;
resetPlayback();
};
// 下一曲
const nextSong = () => {
currentSongIndex.value =
currentSongIndex.value < songs.value.length - 1 ? currentSongIndex.value + 1 : 0;
resetPlayback();
};
// 重置播放狀態(tài)
const resetPlayback = () => {
currentTime.value = 0;
if (!isPlaying.value) isPlaying.value = true;
};
進(jìn)度條交互
// 設(shè)置播放進(jìn)度
const setProgress = (e) => {
const progressBar = e.currentTarget;
const clickX = e.offsetX;
const width = progressBar.offsetWidth;
const percent = clickX / width;
currentTime.value = percent * duration.value;
};
// 時(shí)間格式化
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
};
4. 音頻可視化效果
雖然我們無(wú)法直接獲取音頻數(shù)據(jù),但可以模擬可視化效果:
// 可視化數(shù)據(jù)
const visualizerData = ref([]);
// 初始化可視化
const initVisualizer = () => {
visualizerData.value = Array.from({ length: 30 }, () => Math.random() * 80 + 10);
};
// 更新可視化效果
const updateVisualizer = () => {
if (!isPlaying.value) return;
visualizerData.value = visualizerData.value.map((value, index) => {
const intensity = Math.sin(Date.now() * 0.01 + index) * 30 + 30;
const change = (Math.random() - 0.5) * 40;
let newValue = intensity + change;
return Math.max(5, Math.min(95, newValue));
});
};
5. 播放模擬與可視化更新
在組件掛載后,設(shè)置定時(shí)器模擬播放進(jìn)度和更新可視化:
onMounted(() => {
initVisualizer();
setInterval(() => {
if (isPlaying.value) {
if (currentTime.value < duration.value) {
currentTime.value += 0.1;
} else {
nextSong();
}
updateVisualizer();
}
}, 100);
});
6. 樣式設(shè)計(jì)要點(diǎn)
專(zhuān)輯封面旋轉(zhuǎn)動(dòng)畫(huà)
.album-cover.playing {
animation: rotate 20s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
進(jìn)度條樣式
.progress {
height: 100%;
background: linear-gradient(90deg, #e94560, #ff7aa8);
border-radius: 3px;
position: relative;
transition: width 0.1s linear;
}
控制按鈕交互效果
.control-btn {
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.control-btn:active {
transform: scale(0.95);
}
完整代碼
<template>
<div class="music-player">
<!-- 專(zhuān)輯區(qū)域 -->
<div class="album-container">
<div class="album-cover" :class="{ playing: isPlaying }">
<img :src="currentSong.cover" alt="專(zhuān)輯封面" />
<div class="album-center"></div>
</div>
<div class="album-base"></div>
</div>
<!-- 歌曲信息 -->
<div class="song-info">
<h2 class="song-title">{{ currentSong.title }}</h2>
<p class="song-artist">{{ currentSong.artist }}</p>
</div>
<!-- 進(jìn)度條 -->
<div class="progress-container">
<div class="progress-bar" @click="setProgress">
<div class="progress" :style="{ width: progressPercent }">
<div class="progress-handle"></div>
</div>
</div>
<div class="time-display">
<span>{{ formatTime(currentTime) }}</span>
<span>{{ formatTime(duration) }}</span>
</div>
</div>
<!-- 控制按鈕 -->
<div class="controls">
<button class="control-btn" @click="prevSong">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 6H8V18H6V6ZM9.5 12L18 18V6L9.5 12Z" fill="currentColor" />
</svg>
</button>
<button class="control-btn play-btn" @click="togglePlay">
<svg v-if="!isPlaying" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5V19L19 12L8 5Z" fill="currentColor" />
</svg>
<svg v-else width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 19H10V5H6V19ZM14 5V19H18V5H14Z" fill="currentColor" />
</svg>
</button>
<button class="control-btn" @click="nextSong">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 18L14.5 12L6 6V18ZM16 6V18H18V6H16Z" fill="currentColor" />
</svg>
</button>
</div>
<!-- 音頻可視化 -->
<div class="visualizer">
<div
v-for="(height, index) in visualizerData"
:key="index"
class="bar"
:style="{ height: height + '%' }"
></div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
// 響應(yīng)式數(shù)據(jù)
const isPlaying = ref(false);
const currentTime = ref(0);
const duration = ref(240); // 4分鐘(秒)
const currentSongIndex = ref(0);
// 歌曲列表
const songs = ref([
{
title: '夜空中最亮的星',
artist: '逃跑計(jì)劃',
cover: 'https://picsum.photos/400/400?random=1',
},
{
title: '平凡之路',
artist: '樸樹(shù)',
cover: 'https://picsum.photos/400/400?random=2',
},
{
title: '光年之外',
artist: 'G.E.M.鄧紫棋',
cover: 'https://picsum.photos/400/400?random=3',
},
]);
// 當(dāng)前歌曲
const currentSong = computed(() => songs.value[currentSongIndex.value]);
// 進(jìn)度條百分比
const progressPercent = computed(() => {
return (currentTime.value / duration.value) * 100 + '%';
});
// 時(shí)間格式化
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
};
// 播放/暫停
const togglePlay = () => {
isPlaying.value = !isPlaying.value;
};
// 上一曲
const prevSong = () => {
currentSongIndex.value =
currentSongIndex.value > 0 ? currentSongIndex.value - 1 : songs.value.length - 1;
currentTime.value = 0;
if (!isPlaying.value) isPlaying.value = true;
};
// 下一曲
const nextSong = () => {
currentSongIndex.value =
currentSongIndex.value < songs.value.length - 1 ? currentSongIndex.value + 1 : 0;
currentTime.value = 0;
if (!isPlaying.value) isPlaying.value = true;
};
// 設(shè)置進(jìn)度
const setProgress = (e) => {
const progressBar = e.currentTarget;
const clickX = e.offsetX;
const width = progressBar.offsetWidth;
const percent = clickX / width;
currentTime.value = percent * duration.value;
};
// 可視化數(shù)據(jù)
const visualizerData = ref([]);
// 初始化可視化
const initVisualizer = () => {
visualizerData.value = Array.from({ length: 30 }, () => Math.random() * 80 + 10);
};
// 更新可視化
const updateVisualizer = () => {
if (!isPlaying.value) return;
visualizerData.value = visualizerData.value.map((value, index) => {
const intensity = Math.sin(Date.now() * 0.01 + index) * 30 + 30;
const change = (Math.random() - 0.5) * 40;
let newValue = intensity + change;
return Math.max(5, Math.min(95, newValue));
});
};
// 模擬播放進(jìn)度和可視化更新
onMounted(() => {
initVisualizer();
setInterval(() => {
if (isPlaying.value) {
if (currentTime.value < duration.value) {
currentTime.value += 0.1;
} else {
nextSong();
}
updateVisualizer();
}
}, 100);
});
</script>
<style scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
.music-player {
width: 100%;
max-width: 350px;
background: rgba(30, 30, 46, 0.8);
border-radius: 24px;
padding: 30px 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
position: relative;
overflow: hidden;
font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
color: #fff;
margin: 0 auto;
}
.music-player::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
z-index: -1;
}
.album-container {
position: relative;
width: 280px;
height: 280px;
margin: 0 auto 30px;
perspective: 1000px;
}
.album-base {
position: absolute;
bottom: -15px;
left: 50%;
transform: translateX(-50%);
width: 200px;
height: 30px;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
filter: blur(10px);
z-index: -1;
}
.album-cover {
width: 100%;
height: 100%;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.5);
position: relative;
transition: transform 0.3s ease;
}
.album-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.album-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50px;
height: 50px;
background: #1a1a2e;
border-radius: 50%;
border: 5px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
.album-cover.playing {
animation: rotate 20s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.song-info {
text-align: center;
margin-bottom: 30px;
}
.song-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.song-artist {
font-size: 16px;
color: rgba(255, 255, 255, 0.7);
}
.progress-container {
margin-bottom: 30px;
}
.progress-bar {
width: 100%;
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
position: relative;
margin-bottom: 10px;
cursor: pointer;
}
.progress {
height: 100%;
background: linear-gradient(90deg, #e94560, #ff7aa8);
border-radius: 3px;
width: 30%;
position: relative;
transition: width 0.1s linear;
}
.progress-handle {
position: absolute;
right: -8px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
background: #fff;
border-radius: 50%;
box-shadow: 0 0 10px rgba(233, 69, 96, 0.8);
}
.time-display {
display: flex;
justify-content: space-between;
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
}
.controls {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 30px;
}
.control-btn {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background: rgba(255, 255, 255, 0.1);
border: none;
color: white;
font-size: 20px;
margin: 0 15px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.control-btn:active {
transform: scale(0.95);
}
.play-btn {
width: 60px;
height: 60px;
background: linear-gradient(135deg, #e94560, #ff7aa8);
box-shadow: 0 10px 20px rgba(233, 69, 96, 0.4);
}
.visualizer {
height: 100px;
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4px;
margin-top: 20px;
}
.bar {
width: 6px;
background: linear-gradient(to top, #e94560, #ff7aa8);
border-radius: 3px 3px 0 0;
transition: height 0.2s ease;
}
</style>
擴(kuò)展思路
1.真實(shí)音頻集成:使用Web Audio API替換模擬音頻,實(shí)現(xiàn)真實(shí)的可視化效果
2.播放列表:添加播放列表功能,支持歌曲選擇
3.主題切換:實(shí)現(xiàn)明暗主題切換功能
4.響應(yīng)式優(yōu)化:針對(duì)不同屏幕尺寸進(jìn)行優(yōu)化
5.離線功能:添加PWA支持,實(shí)現(xiàn)離線播放
你可以在此基礎(chǔ)上繼續(xù)擴(kuò)展功能,打造屬于自己的獨(dú)特音樂(lè)播放體驗(yàn)。
以上就是基于Vue3打造一個(gè)高級(jí)的音樂(lè)播放器的詳細(xì)內(nèi)容,更多關(guān)于Vue3音樂(lè)播放器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
VUE3基礎(chǔ)學(xué)習(xí)之click事件詳解
由于vue是一個(gè)雙向數(shù)據(jù)綁定的框架,它的點(diǎn)擊事件與以前常用的還是有很大的差別的,下面這篇文章主要給大家介紹了關(guān)于VUE3基礎(chǔ)學(xué)習(xí)之click事件的相關(guān)資料,需要的朋友可以參考下2022-01-01
vue項(xiàng)目配置sass及引入外部scss文件方式
這篇文章主要介紹了vue項(xiàng)目配置sass及引入外部scss文件方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04
Vue路由配置項(xiàng)和參數(shù)使用及說(shuō)明
文章介紹了Vue路由中query和params參數(shù)的傳遞與接收、命名路由簡(jiǎn)化跳轉(zhuǎn)、props配置參數(shù)、router-link的replace屬性、編程式導(dǎo)航、路由組件緩存及activated和deactivated兩個(gè)生命周期鉤子的用法2025-10-10
apache和nginx下vue頁(yè)面刷新404的解決方案
這篇文章主要介紹了apache和nginx下vue頁(yè)面刷新404的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12
Vue組件間的通信pubsub-js實(shí)現(xiàn)步驟解析
這篇文章主要介紹了Vue組件間的通信pubsub-js實(shí)現(xiàn)原理解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03
詳解vue-router 2.0 常用基礎(chǔ)知識(shí)點(diǎn)之router-link
這篇文章主要介紹了詳解vue-router 2.0 常用基礎(chǔ)知識(shí)點(diǎn)之router-link,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05
關(guān)于vue二進(jìn)制轉(zhuǎn)圖片顯示問(wèn)題 后端返回的是byte[]數(shù)組
這篇文章主要介紹了關(guān)于vue二進(jìn)制轉(zhuǎn)圖片顯示問(wèn)題 后端返回的是byte[]數(shù)組,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06
vue組件封裝實(shí)現(xiàn)抽獎(jiǎng)隨機(jī)數(shù)
這篇文章主要為大家詳細(xì)介紹了vue組件封裝實(shí)現(xiàn)抽獎(jiǎng)隨機(jī)數(shù),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03

