一、迷你播放器
MiniPlayer.vue
<template>
<div class="mini-player">
<div class="cd-wrapper">
<div class="cd">
<img />
</div>
</div>
<div class="slider-wrapper">
<h2 class="name">歌曲名</h2>
<p class="desc">歌手名</p>
</div>
<!-- 进度圆圈 -->
<div class="control">进度圆占位</div>
<div class="control">
<i class="icon-playlist"></i>
</div>
<div>歌曲列表占位</div>
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.mini-player {
display: flex;
align-items: center;
position: fixed;
left: 0;
bottom: 0;
z-index: 180;
width: 100%;
height: 60px;
background: $color-highlight-background;
.cd-wrapper {
flex: 0 0 40px;
width: 40px;
height: 40px;
padding: 0 10px 0 20px;
.cd {
position: relative;
height: 100%;
width: 100%;
animation: rotate 10s linear infinite;
overflow: hidden;
border-radius: 50%;
img {
position: absolute;
width: 130%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
}
.slider-wrapper {
display: flex;
flex-direction: column;
justify-content: center;
flex: 1;
line-height: 20px;
overflow: hidden;
.name {
margin-bottom: 2px;
@include no-wrap();
font-size: $font-size-medium;
color: $color-text;
}
.desc {
@include no-wrap();
font-size: $font-size-small;
color: $color-text-d;
}
}
.control {
flex: 0 0 30px;
width: 30px;
padding: 0 10px;
.icon-mini {
position: absolute;
left: 0;
top: 0;
color: $color-theme-d;
font-size: 32px;
}
.icon-playlist {
position: relative;
top: -2px;
font-size: 28px;
color: $color-theme-d;
}
}
&.mini-enter-active,
&.mini-leave-active {
transition: all 0.6s cubic-bezier(0.45, 0, 0.55, 1);
}
&.mini-enter-from,
&.mini-leave-to {
opacity: 0;
transform: translate3d(0, 100%, 0);
}
}
</style>
在Player.vue中导入该组件
import MyMiniPlayer from "@/components/MiniPlayer.vue"
<my-miniPlayer></my-miniPlayer>
将父组件中函数传给子组件
<my-miniPlayer :handle="handle" :progress="progress" :cdStyle="cdStyle" :togglePlay="togglePlay"></my-miniPlayer>
子组件接收
const props = defineProps(["handle", "progress", "cdStyle", "togglePlay"]);
在mini播放组件中导入新建的进度圆圈组件
ProgressCircle.vue
<template>
<div class="progress-circle">
<svg></svg>
<slot></slot>
</div>
</template>
<my-progressCircle>
<i class="icon-mini" :class="miniBtn" @click="togglePlay"></i>
</my-progressCircle>
使用svg标签画两个圆
<template>
<div class="progress-circle">
<!-- svg图形容器 -->
<svg :width="radius" :height="radius" viewBox="0 0 100 100">
<circle
class="progress-background"
r="50"
cx="50"
cy="50"
fill="transparent"
/>
<circle
class="progress-bar"
r="50"
cx="50"
cy="50"
fill="transparent"
:stroke-dasharray="dashArray"
:stroke-dashoffset="dashOffset"
/>
</svg>
<slot></slot>
</div>
</template>
<script setup>
const { computed }=require("@vue/runtime-core");
const props = defineProps({
radius: {
type: Number,
default: 100,
},
progress: {
type: Number,
default: 0,
},
});
const dashArray = Math.PI * 2 * 50;
// progress 0-1
const dashOffset = computed(() => (1 - props.progress) * dashArray)
</script>
点击打开歌曲列表,点击按钮切换模式,渲染歌曲数据,添加喜欢功能,正在播放图标的添加,点击歌曲列表切歌并阻止事件冒泡,点击歌曲自动吸顶,点击删除从列表中删除歌曲,控制列表的显隐,
PlayList.vue
<template>
<div class="playlist" v-show="visible" @click.stop="hide">
<div class="list-wrapper" @click.stop>
<div class="list-header">
<h1 class="title">
<i class="icon" :class="modeIcon" @click="changeMode"></i>
<span class="text">{{ modeText }}</span>
<span class="clear">
<i class="icon-clear"></i>
</span>
</h1>
</div>
<my-scroll class="list-content" ref="scrollRef">
<transition-group ref="listRef" tag="ul" name="list">
<li
class="item"
v-for="song in playList"
:key="song.id"
@click="selectItem(song)"
>
<i class="current" :class="getCurrentIcon(song)"></i>
<span class="text">{{ song.name }}</span>
<span class="favorite" @click.stop="toggleFavorite(song)">
<i :class="getFavoriteIcon(song)"></i>
</span>
<span class="delete" @click.stop="removeSong(song)">
<i class="icon-delete"></i>
</span>
</li>
</transition-group>
</my-scroll>
<div class="list-footer" @click.stop="hide">
<span>关闭</span>
</div>
</div>
</div>
</template>
<script setup>
import MyScroll from "@/components/base/Scroll.vue";
import { computed, ref, watch } from "vue";
import { useStore } from "vuex";
import useMode from "@/assets/js/useMode";
import useFavorite from "@/assets/js/useFavorite";
// 显示和隐藏
const visible = ref(false);
// 获取滚动对象
const scrollRef = ref(null);
// 获取li
const listRef = ref(null);
// VueX
const store = useStore();
const playList = computed(() => store.state.playlist);
const sequenceList = computed(() => store.state.sequenceList);
const currentSong = computed(() => store.getters.currentSong);
// Hooks
const { modeIcon, changeMode, modeText } = useMode();
const { getFavoriteIcon, toggleFavorite } = useFavorite();
watch(currentSong, (newSong) => {
// 由playList渲染时才执行scroToCurrent
if (!newSong.id || !visible.value) return;
scrollToCurrent();
});
// 正在播放的Icon
function getCurrentIcon(song) {
if (currentSong.value.id === song.id) return "icon-play";
}
// 点击切歌
function selectItem(song) {
//找到被点击的歌曲在sequenceList中的下标
const index = sequenceList.value.findIndex((item) => item.id === song.id);
store.commit("setCurrentIndex", index);
store.commit("setPlayingState", true);
}
// 当前播放歌曲置顶
function scrollToCurrent() {
let song = currentSong.value;
let index = playList.value.findIndex((item) => item.id === song.id);
//找到对应的li元素
let targetEl = listRef.value.$el.children[index];
scrollRef.value.scroll.scrollToElement(targetEl, 300);
}
// 点击删除歌曲
function removeSong(song) {
store.dispatch("removeSong", song);
}
// 组件的显示和隐藏
function show() {
visible.value = true;
}
function hide() {
visible.value = false;
}
// 暴露出去
defineExpose({
show,
});
</script>
<style lang="scss" scoped>
.playlist {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 200;
background-color: $color-background-d;
&.list-fade-enter-active,
&.list-fade-leave-active {
transition: opacity 0.3s;
.list-wrapper {
transition: all 0.3s;
}
}
&.list-fade-enter-from,
&.list-fade-leave-to {
opacity: 0;
.list-wrapper {
transform: translate(0, 100%);
}
}
.list-wrapper {
position: fixed;
left: 0;
bottom: 0;
z-index: 210;
width: 100%;
background-color: $color-highlight-background;
.list-header {
position: relative;
padding: 20px 30px 10px 20px;
.title {
display: flex;
align-items: center;
.icon {
margin-right: 10px;
font-size: 24px;
color: $color-theme-d;
}
.text {
flex: 1;
font-size: $font-size-medium;
color: $color-text-l;
}
.clear {
@include extend-click();
.icon-clear {
font-size: $font-size-medium;
color: $color-text-d;
}
}
}
}
.list-content {
max-height: 280px;
overflow: hidden;
.item {
display: flex;
align-items: center;
height: 40px;
padding: 0 30px 0 20px;
overflow: hidden;
.current {
flex: 0 0 20px;
width: 20px;
font-size: $font-size-small;
color: $color-theme-d;
}
.text {
flex: 1;
@include no-wrap();
font-size: $font-size-medium;
color: $color-text-l;
}
.favorite {
@include extend-click();
margin-right: 15px;
font-size: $font-size-small;
color: $color-theme;
.icon-favorite {
color: $color-sub-theme;
}
}
.delete {
@include extend-click();
font-size: $font-size-small;
color: $color-theme;
&.disable {
color: $color-theme-d;
}
}
}
}
.list-add {
width: 140px;
margin: 20px auto 30px auto;
.add {
display: flex;
align-items: center;
padding: 8px 16px;
border: 1px solid $color-text-l;
border-radius: 100px;
color: $color-text-l;
.icon-add {
margin-right: 5px;
font-size: $font-size-small-s;
}
.text {
font-size: $font-size-small;
}
}
}
.list-footer {
text-align: center;
line-height: 60px;
background: $color-background;
font-size: $font-size-medium-x;
color: $color-text-l;
}
}
}
</style>
useMode.js
import { computed } from "vue";
import { useStore } from "vuex";
export default function useMode() {
const store = useStore()
const playMode = computed(() => store.state.playMode)
const modeIcon = computed(() => {
const playModeValue = playMode.value
switch (playModeValue) {
case 0://顺序播放
return 'icon-sequence'
break;
case 1://单曲播放
return 'icon-loop'
break;
case 2://随机播放
return 'icon-random'
break;
}
})
// 补充:模式文本
const modeText = computed(() => {
const playModeValue = playMode.value
switch (playModeValue) {
case 0://顺序播放
return '顺序播放'
break;
case 1://单曲播放
return '单曲播放'
break;
case 2://随机播放
return '随机播放'
break;
}
})
function changeMode() {
const mode = (playMode.value + 1) % 3
store.dispatch("changeMode", mode)
}
return {
modeIcon,
changeMode,
modeText
}
}
改进添加喜欢(useFavorite.js)
import storage from "@/assets/js/storage-api";
import { computed, onMounted } from "vue";
import { useStore} from "vuex";
export default function useFavorite() {
const store = useStore()
const favoriteList = computed(() => store.state.favoriteList)
onMounted(() => {
// 用上本地存储
if(!favoriteList.value.length) {
let list = storage.getLocal('__favoriteList__', [])
if(list.length) {
store.commit('setFavoriteList', list)
}
}
})
// 由外部传入song来判断
function getFavoriteIcon(song) {
return isFavorite(song) ? 'icon-favorite' : 'icon-not-favorite'
}
// 也由外部传入song来判断
function toggleFavorite(song) {
// 拿到当前的歌曲
// const song = currentSong.value
let list = favoriteList.value.slice()
// 点击按钮实现喜欢或取消喜欢功能
if(isFavorite(song)) {
// 在就删除
let index = list.findIndex(item => item.id === song.id)
list.splice(index,1)
}else {
// 不在添加
list.unshift(song)
}
store.commit('setFavoriteList', list)
storage.setLocal('__favoriteList__', list)
}
function isFavorite(song) {
// true 在喜欢列表,false不在喜欢列表
return favoriteList.value.findIndex(item => item.id === song.id) > -1
}
return {
getFavoriteIcon,
toggleFavorite
}
}
备注:做过渡动画时,若只有一个子元素就可以用transition标签包裹,若有多个子元素则使用transition-group标签包裹(使用tag属性改变标签)(transition是个组件封装了很多方法)
<transition-group ref="listRef" tag="ul" name="list">
<li
class="item"
v-for="song in playList"
:key="song.id"
@click="selectItem(song)"
>
<i class="current" :class="getCurrentIcon(song)"></i>
<span class="text">{{ song.name }}</span>
<span class="favorite" @click.stop="toggleFavorite(song)">
<i :class="getFavoriteIcon(song)"></i>
</span>
<span class="delete" @click.stop="removeSong(song)">
<i class="icon-delete"></i>
</span>
</li>
</transition-group>
点击列表时要阻止事件冒泡在@click加上stop
@click.stop=""