最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于Vue3打造一個(gè)高級(jí)的音樂(lè)播放器

 更新時(shí)間:2025年10月14日 10:09:02   作者:劉大華  
這篇文章將分享一下如何使用Vue3打造一個(gè)優(yōu)雅的音樂(lè)播放器,這個(gè)播放器擁有精美的視覺(jué)效果,包括旋轉(zhuǎn)專(zhuān)輯封面、動(dòng)態(tài)進(jìn)度條和音頻可視化效果,感興趣的小伙伴可以參考本文動(dòng)手試一下

效果預(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)文章

  • Vue.js 2.5新特性介紹(推薦)

    Vue.js 2.5新特性介紹(推薦)

    Vue 2.5 Level E 已發(fā)布,在原來(lái)2.x的基礎(chǔ)上做了很多相應(yīng)改進(jìn)和對(duì) bug 的修復(fù),目前 2.5 系列最新的版本為 2.5.2,本文給大家介紹vue 2.5 新特征,需要的朋友參考下吧
    2017-10-10
  • VUE3基礎(chǔ)學(xué)習(xí)之click事件詳解

    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文件方式

    這篇文章主要介紹了vue項(xiàng)目配置sass及引入外部scss文件方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-04-04
  • Vue路由配置項(xiàng)和參數(shù)使用及說(shuō)明

    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的解決方案

    這篇文章主要介紹了apache和nginx下vue頁(yè)面刷新404的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-12-12
  • Vue組件間的通信pubsub-js實(shí)現(xiàn)步驟解析

    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

    這篇文章主要介紹了詳解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ù)組

    這篇文章主要介紹了關(guān)于vue二進(jìn)制轉(zhuǎn)圖片顯示問(wèn)題 后端返回的是byte[]數(shù)組,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-06-06
  • vue自定義表單生成器form-create使用詳解

    vue自定義表單生成器form-create使用詳解

    這篇文章主要介紹了vue自定義表單生成器,可根據(jù)json參數(shù)動(dòng)態(tài)生成表單,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-07-07
  • vue組件封裝實(shí)現(xiàn)抽獎(jiǎng)隨機(jī)數(shù)

    vue組件封裝實(shí)現(xiàn)抽獎(jiǎng)隨機(jī)數(shù)

    這篇文章主要為大家詳細(xì)介紹了vue組件封裝實(shí)現(xiàn)抽獎(jiǎng)隨機(jī)數(shù),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-03-03

最新評(píng)論

苍梧县| 鄱阳县| 偃师市| 府谷县| 崇仁县| 永胜县| 大厂| 桐乡市| 县级市| 石阡县| 博罗县| 全州县| 耿马| 镇安县| 安国市| 长丰县| 漠河县| 黄大仙区| 五家渠市| 名山县| 栖霞市| 鄄城县| 霸州市| 黑水县| 庆元县| 白山市| 油尖旺区| 犍为县| 肥西县| 香格里拉县| 汉寿县| 尤溪县| 苍南县| 北辰区| 米易县| 呼伦贝尔市| 贺兰县| 左贡县| 乌兰浩特市| 阿克陶县| 靖西县|