Vue3项目自学训练----音乐播放器(使用vueX管理项目)(十一)

一、迷你播放器

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=""

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr.MUXIAO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值