打赏作者
感谢打赏,创作不易~
  • 微信
  • 支付宝

手写一个audio播放器,实现歌曲切换,列表歌曲循环,音量调节等 vue组件

  1 <template>
  2 <div class="wrapper">
  3 <svg
  4 t="1673833915638"
  5 class="icon rotateIcon"
  6 viewBox="0 0 1024 1024"
  7 version="1.1"
  8 xmlns="http://www.w3.org/2000/svg"
  9 p-id="2105"
 10 >
 11 <path
 12 d="M478.388706 45.236706L363.52 2.529882v668.431059a254.674824 254.674824 0 0 0-115.892706 18.010353c-96.015059 39.213176-145.889882 133.541647-118.362353 199.499294 27.587765 66.138353 122.217412 96.617412 218.112 57.404236 73.728-30.117647 124.867765-87.401412 131.553883-142.275765l-0.481883-591.329883c158.238118 48.067765 272.263529 100.773647 272.26353 258.048 0 55.838118 91.557647 109.808941 91.557647-109.146352-0.060235-186.247529-159.804235-298.887529-363.881412-315.934118z"
 13 fill="#17966f"
 14 p-id="2106"
 15 />
 16 </svg>
 17 <img
 18 class="musicIcon"
 19 :src="musicList[currentIndex].icon"
 20 :style="{ animationPlayState: `${playStatus ? 'running' : 'paused'}` }"
 21 />
 22 <div class="controls">
 23 <div class="controls-title">
 24 正在播放中:{{ musicList[currentIndex].singer }} -
 25 {{ musicList[currentIndex].song }}
 26 </div>
 27 <audio
 28 ref="audio"
 29 :src="musicList[currentIndex].url"
 30 @ended="ended"
 31 autoplay
 32 />
 33 <div class="controls-change">
 34 <svg
 35 t="1673834949112"
 36 class="icon"
 37 viewBox="0 0 1024 1024"
 38 version="1.1"
 39 xmlns="http://www.w3.org/2000/svg"
 40 p-id="1372"
 41 @click="preMusic"
 42 >
 43 <path
 44 d="M76.442058 472.123804l479.972785-344.375282c31.816917-17.080004 68.268577-21.123087 68.295184 44.618197l0 149.899008 271.113573-194.517205c31.815893-17.080004 69.179357-17.590634 68.295184 46.384423l0 676.471912c-0.91078 57.779961-39.127716 66.168002-68.295184 47.295166L624.711051 703.367469l0 148.120502c-0.026607 56.012711-39.127716 65.284889-68.295184 46.411029L76.442058 553.481763C54.339786 531.021216 54.339786 494.597655 76.442058 472.123804zM907.552193 196.359156 568.143437 433.700703c0 0 0-186.833199 0-237.341546L115.598428 512.809435l452.545009 316.465628c0-13.244652 0-237.328243 0-237.328243l339.408757 237.328243C907.552193 816.029388 907.552193 246.867504 907.552193 196.359156z"
 45 fill="#0fd7ce"
 46 p-id="1373"
 47 />
 48 </svg>
 49 <svg
 50 t="1673835088221"
 51 class="icon"
 52 viewBox="0 0 1024 1024"
 53 version="1.1"
 54 xmlns="http://www.w3.org/2000/svg"
 55 p-id="1230"
 56 @click="play"
 57 v-if="!playStatus"
 58 >
 59 <path
 60 d="M512 0C230.4 0 0 230.4 0 512s230.4 512 512 512 512-230.4 512-512S793.6 0 512 0z m0 981.333333C253.866667 981.333333 42.666667 770.133333 42.666667 512S253.866667 42.666667 512 42.666667s469.333333 211.2 469.333333 469.333333-211.2 469.333333-469.333333 469.333333z"
 61 fill="#0fd7ce"
 62 p-id="1231"
 63 />
 64 <path
 65 d="M672 441.6l-170.666667-113.066667c-57.6-38.4-106.666667-12.8-106.666666 57.6v256c0 70.4 46.933333 96 106.666666 57.6l170.666667-113.066666c57.6-42.666667 57.6-106.666667 0-145.066667z"
 66 fill="#0fd7ce"
 67 p-id="1232"
 68 />
 69 </svg>
 70 <svg
 71 t="1673835567808"
 72 class="icon"
 73 viewBox="0 0 1048 1024"
 74 version="1.1"
 75 xmlns="http://www.w3.org/2000/svg"
 76 p-id="1531"
 77 @click="pause"
 78 v-else
 79 >
 80 <path
 81 d="M524.272128 0.018022C241.513165 0.018022 12.288102 229.245952 12.288102 512.005018c0 112.734003 36.43904 216.957952 98.17897 301.537997l38.667981-40.258048C97.458176 699.230003 67.143168 609.158963 67.143168 512.005018 67.143168 259.540992 271.807078 54.872986 524.272128 54.872986c252.45696 0 457.120973 204.668006 457.120973 457.132032 0 252.460954-204.664013 457.118003-457.120973 457.118003-96.240026 0-185.530982-29.744026-259.189043-80.53504l-34.539008 42.797978c83.150029 58.344038 184.437965 92.596019 293.728973 92.596019 282.758963 0 511.984026-229.220966 511.984026-511.976038C1036.256154 229.245952 807.031091 0.018022 524.272128 0.018022zM615.693107 256.011981l0 511.987 54.855 0L670.548107 256.012 615.693128 256.012zM377.996083 256.011981l0 511.987 54.855 0L432.851083 256.012 377.996128 256.012z"
 82 fill="#0fd7ce"
 83 p-id="1532"
 84 />
 85 </svg>
 86 <svg
 87 t="1673835103937"
 88 class="icon"
 89 viewBox="0 0 1024 1024"
 90 version="1.1"
 91 xmlns="http://www.w3.org/2000/svg"
 92 p-id="1381"
 93 @click="nextMusic"
 94 >
 95 <path
 96 d="M947.557942 553.481763 467.585156 897.899c-29.166445 18.872836-68.267554 9.601682-68.295184-46.411029L399.289972 703.367469 128.175376 897.899c-29.166445 18.872836-67.384404 10.484795-68.295184-47.295166L59.880192 174.132946c-0.883149-63.975057 36.479291-63.464427 68.295184-46.384423l271.113573 194.517205L399.288949 172.366719c0.02763-65.741283 36.479291-61.698201 68.295184-44.618197l479.973809 344.375282C969.661238 494.597655 969.661238 531.021216 947.557942 553.481763zM116.44883 829.275064l339.408757-237.328243c0 0 0 224.082568 0 237.328243l452.545009-316.465628-452.545009-316.450279c0 50.508347 0 237.341546 0 237.341546L116.44883 196.359156C116.44883 246.867504 116.44883 816.029388 116.44883 829.275064z"
 97 fill="#0fd7ce"
 98 p-id="1382"
 99 />
100 </svg>
101 </div>
102 <div class="controls-progress">
103 <span class="controls-progress-duration"
104 >{{ Math.floor(currentTime / 60) }}:{{
105 Math.floor(currentTime % 60) < 10
106 ? `0${Math.floor(currentTime % 60)}`
107 : Math.floor(currentTime % 60)
108 }}
109 / {{ Math.floor(totalDuration / 60) }}:{{
110 Math.floor(totalDuration % 60) < 10
111 ? `0${Math.floor(totalDuration % 60)}`
112 : Math.floor(totalDuration % 60)
113 }}
114 </span>
115 <input
116 class="controls-progress-range"
117 type="range"
118 :max="totalDuration"
119 :value="currentTime"
120 :style="{
121 backgroundSize: `${(currentTime / totalDuration) * 100}% 100%`
122 }"
123 @change="changePlayProgress"
124 />
125 <svg
126 t="1673839083338"
127 class="icon volume-icon"
128 viewBox="0 0 1024 1024"
129 version="1.1"
130 xmlns="http://www.w3.org/2000/svg"
131 p-id="1290"
132 @click="volumeStatus = !volumeStatus"
133 v-if="!muteStatus"
134 >
135 <path
136 d="M448 938.666667a21.333333 21.333333 0 0 1-15.093333-6.246667L225.833333 725.333333H53.333333a53.393333 53.393333 0 0 1-53.333333-53.333333V352a53.393333 53.393333 0 0 1 53.333333-53.333333h172.5l207.08-207.086667A21.333333 21.333333 0 0 1 469.333333 106.666667v810.666666a21.333333 21.333333 0 0 1-21.333333 21.333334zM53.333333 341.333333a10.666667 10.666667 0 0 0-10.666666 10.666667v320a10.666667 10.666667 0 0 0 10.666666 10.666667h181.333334a21.333333 21.333333 0 0 1 15.086666 6.246666L426.666667 865.833333V158.166667L249.753333 335.086667A21.333333 21.333333 0 0 1 234.666667 341.333333z"
137 fill="#0fd7ce"
138 p-id="1291"
139 />
140 </svg>
141 <svg
142 t="1673845914200"
143 class="icon volume-icon"
144 viewBox="0 0 1024 1024"
145 version="1.1"
146 xmlns="http://www.w3.org/2000/svg"
147 p-id="1440"
148 @click="volumeStatus = !volumeStatus"
149 v-else
150 >
151 <path
152 d="M448 938.666667a21.333333 21.333333 0 0 1-15.093333-6.246667L225.833333 725.333333H53.333333a53.393333 53.393333 0 0 1-53.333333-53.333333V352a53.393333 53.393333 0 0 1 53.333333-53.333333h172.5l207.08-207.086667A21.333333 21.333333 0 0 1 469.333333 106.666667v810.666666a21.333333 21.333333 0 0 1-21.333333 21.333334zM53.333333 341.333333a10.666667 10.666667 0 0 0-10.666666 10.666667v320a10.666667 10.666667 0 0 0 10.666666 10.666667h181.333334a21.333333 21.333333 0 0 1 15.086666 6.246666L426.666667 865.833333V158.166667L249.753333 335.086667A21.333333 21.333333 0 0 1 234.666667 341.333333z m964.42 377.753334a21.333333 21.333333 0 0 0 0-30.173334L840.833333 512l176.92-176.913333a21.333333 21.333333 0 1 0-30.173333-30.173334L810.666667 481.833333 633.753333 304.913333a21.333333 21.333333 0 0 0-30.173333 30.173334L780.5 512l-176.92 176.913333a21.333333 21.333333 0 0 0 30.173333 30.173334L810.666667 542.166667l176.913333 176.92a21.333333 21.333333 0 0 0 30.173333 0z"
153 fill="#0fd7ce"
154 p-id="1441"
155 />
156 </svg>
157 <input
158 v-if="volumeStatus"
159 type="range"
160 class="volume-range"
161 :value="volume"
162 max="100"
163 :style="{ backgroundSize: `${(volume / 100) * 100}% 100%` }"
164 @change="changeVolume"
165 />
166 </div>
167 </div>
168 </div>
169 </template>
170 <script lang="ts" setup>
171 import { onMounted, ref } from "vue";
172 const musicList: any[] = [
173 {
174 id: 26445261,
175 url: "https://apis.jxcxin.cn/api/kuwo?id=26445261&type=mp3",
176 icon: "https://img4.kuwo.cn/star/albumcover/500/12/73/1352603132.jpg",
177 singer: "买辣椒也用券",
178 song: "起风了(旧版)"
179 },
180 {
181 id: 60262165,
182 url: "https://apis.jxcxin.cn/api/kuwo?id=60262165&type=mp3",
183 icon: "https://img4.kuwo.cn/star/albumcover/500/28/0/866943227.jpg",
184 singer: "尚士达",
185 song: "生而为人"
186 },
187 {
188 id: 5961360,
189 url: "https://apis.jxcxin.cn/api/kuwo?id=5961360&type=mp3",
190 icon: "https://img1.kuwo.cn/star/albumcover/500/59/77/2376077807.jpg",
191 singer: "胡彦斌",
192 song: "月光"
193 },
194 {
195 id: 19519910,
196 url: "https://apis.jxcxin.cn/api/kuwo?id=19519910&type=mp3",
197 icon: "https://img1.kuwo.cn/star/albumcover/500/26/56/3241621283.jpg",
198 singer: "胡彦斌",
199 song: "诀别诗"
200 },
201 {
202 id: 158104409,
203 url: "https://apis.jxcxin.cn/api/kuwo?id=158104409&type=mp3",
204 icon: "https://img1.kuwo.cn/star/albumcover/500/58/72/3795397878.jpg",
205 singer: "蓝心羽",
206 song: "阿拉斯加海湾"
207 },
208 {
209 id: 178937138,
210 url: "https://apis.jxcxin.cn/api/kuwo?id=178937138&type=mp3",
211 icon: "https://img2.kuwo.cn/star/albumcover/500/60/49/2041460549.jpg",
212 singer: "LKer林柯",
213 song: "满目星辰皆是你"
214 }
215 ];
216 // 播放器实例
217 const audio = ref(null);
218 const playStatus = ref(false);
219 const currentIndex = ref(0);
220 let timer = null;
221 // 播放
222 const play = () => {
223 clearInterval(timer);
224 audio.value.play();
225 playStatus.value = true;
226 getMusicDuration();
227 currentTime.value = Math.floor(audio.value.currentTime) || 0;
228 timer = setInterval(() => {
229 currentTime.value++;
230 if (currentTime.value >= totalDuration.value) clearInterval(timer);
231 }, 1000);
232 };
233 // 暂停
234 const pause = () => {
235 audio.value.pause();
236 playStatus.value = false;
237 clearInterval(timer);
238 };
239 
240 // 播放结束
241 const ended = () => {
242 if (currentIndex.value >= musicList.length - 1) {
243 currentIndex.value = 0;
244 return;
245 }
246 currentIndex.value++;
247 };
248 // 上一曲
249 const preMusic = () => {
250 currentTime.value = 0;
251 if (currentIndex.value === 0) {
252 currentIndex.value = musicList.length - 1;
253 play();
254 return;
255 }
256 currentIndex.value--;
257 play();
258 };
259 // 下一曲
260 const nextMusic = () => {
261 currentTime.value = 0;
262 if (currentIndex.value === musicList.length - 1) {
263 currentIndex.value = 0;
264 play();
265 return;
266 }
267 currentIndex.value++;
268 play();
269 };
270 // 拖动进度
271 const changePlayProgress = (event: any) => {
272 currentTime.value = event.target.value;
273 audio.value.currentTime = currentTime.value;
274 };
275 // 静音
276 const muteStatus = ref(false);
277 const volume = ref(10);
278 const volumeStatus = ref(false);
279 const changeVolume = (e: any) => {
280 volume.value = e.target.value;
281 volume.value == 0 ? (muteStatus.value = true) : (muteStatus.value = false);
282 audio.value.volume = volume.value / 100;
283 volumeStatus.value = false;
284 };
285 // duration
286 const currentTime = ref(0);
287 const totalDuration = ref(100);
288 const getMusicDuration = () => {
289 if (![2, 3, 4].includes(audio.value.readyState)) return;
290 setTimeout(() => {
291 totalDuration.value = Math.floor(audio.value.duration);
292 }, 2000);
293 };
294 // 初始化
295 onMounted(() => {
296 getMusicDuration();
297 audio.value.volume = volume.value / 100;
298 audio.value.addEventListener("play", () => {
299 play();
300 });
301 });
302 </script>
303 <style scoped>
304 .wrapper {
305 display: flex;
306 align-items: center;
307 border-radius: 10px;
308 width: 460px;
309 background-image: linear-gradient(to right, white, #63ff63);
310 margin-bottom: 300px;
311 }
312 .wrapper .rotateIcon {
313 width: 20px;
314 height: 40px;
315 transform: translateY(-40px) translateX(18px);
316 }
317 .icon {
318 width: 20px;
319 height: 40px;
320 }
321 .musicIcon {
322 width: 120px;
323 height: 120px;
324 border-radius: 50%;
325 border: 4px solid white;
326 animation: rotateIcon 6s linear infinite backwards;
327 }
328 @keyframes rotateIcon {
329 from {
330 transform: rotate(0deg);
331 }
332 to {
333 transform: rotate(360deg);
334 }
335 }
336 .controls {
337 display: flex;
338 flex: 1;
339 flex-direction: column;
340 align-items: center;
341 padding: 20px;
342 }
343 .controls .controls-title {
344 color: #348eb1;
345 font-size: 12px;
346 }
347 .controls .controls-change {
348 display: flex;
349 justify-content: space-between;
350 margin-top: 10px;
351 width: 100%;
352 }
353 .controls .controls-change .icon {
354 width: 30px;
355 cursor: pointer;
356 }
357 .controls .controls-progress {
358 display: flex;
359 width: 100%;
360 align-items: center;
361 font-size: 12px;
362 position: relative;
363 }
364 .controls .controls-progress .controls-progress-duration {
365 color: #0f9cd3;
366 }
367 .controls .controls-progress .controls-progress-range {
368 flex: 1;
369 margin: 0 20px;
370 outline: none;
371 -webkit-appearance: none;
372 background: -webkit-linear-gradient(#cc0ceb, #dc01ff) no-repeat, #ddd;
373 height: 3px;
374 }
375 .controls .controls-progress .controls-progress-range::-webkit-slider-thumb {
376 -webkit-appearance: none;
377 height: 16px;
378 width: 16px;
379 background: #f8f9fa;
380 border-radius: 50%;
381 border: solid 1px #ddd;
382 }
383 .controls .controls-progress .icon {
384 cursor: pointer;
385 }
386 .volume-range {
387 position: absolute;
388 right: 0;
389 transform: rotate(-90deg) translate(40px, 18px);
390 outline: none;
391 -webkit-appearance: none;
392 background: -webkit-linear-gradient(#d5601c, #ff6308) no-repeat, #ddd;
393 height: 3px;
394 width: 60px;
395 }
396 .controls .controls-progress .volume-range::-webkit-slider-thumb {
397 -webkit-appearance: none;
398 height: 14px;
399 width: 4px;
400 background: #f8f9fa;
401 border-radius: 50%;
402 border: solid 1px #ddd;
403 }
404 </style>
展开 

posted @ 2023-02-01 15:25  Lanny-Chung  阅读(126)  评论(0编辑  收藏  举报