目录
二、设置列表打开关闭动画(一个同级元素用transition包裹)
一、封装对话弹窗组件(封装组件要把所有的功能封装在组件内)
1、绑定点击事件
<span class="clear" @click.stop="showConfirm">
// 清空列表唤起弹窗;
function showConfirm() {
console.log(1);
}
2、先新建弹窗组件Confirm.vue完成样式结构
<template>
<teleport to="body">
<!-- 定位到body的位置 -->
<transition name="confirm-fade">
<div class="confirm" v-show="visible">
<div class="confirm-wrapper">
<div class="confirm-content">
<p class="text">外部传入提示信息</p>
<div class="operate">
<div class="operate-btn left">外部传入选项内容A</div>
<div class="operate-btn">外部传入选项内容B</div>
</div>
</div>
</div>
</div>
</transition>
</teleport>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.confirm {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 998;
background-color: $color-background-d;
&.confirm-fade-enter-active {
animation: confirm-fadein 0.3s;
.confirm-content {
animation: confirm-zoom-in 0.3s;
}
}
&.confirm-fade-leave-active {
animation: confirm-fadeout 0.3s;
.confirm-content {
animation: confirm-zoom-out 0.3s;
}
}
.confirm-wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 999;
.confirm-content {
width: 270px;
border-radius: 13px;
background: $color-highlight-background;
.text {
padding: 19px 15px;
line-height: 22px;
text-align: center;
font-size: $font-size-large;
color: $color-text-l;
}
.operate {
display: flex;
align-items: center;
text-align: center;
font-size: $font-size-large;
.operate-btn {
flex: 1;
line-height: 22px;
padding: 10px 0;
border-top: 1px solid $color-background-d;
color: $color-text-l;
&.left {
border-right: 1px solid $color-background-d;
color: $color-text;
}
}
}
}
}
}
@keyframes confirm-fadein {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes confirm-fadeout {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes confirm-zoom-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
@keyframes confirm-zoom-out {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
</style>
3、操作组件本身的功能封装
<template>
<teleport to="body">
<!-- 定位到body的位置 -->
<transition name="confirm-fade">
<div class="confirm" v-show="visible" @click="hide()">
<div class="confirm-wrapper" @click.stop>
<div class="confirm-content">
<p class="text">{{ text }}</p>
<div class="operate">
<div class="operate-btn left" @click="confirm">{{ confirmBtnText }}</div>
<div class="operate-btn" @click="cancel">{{ cancelBtnText }}</div>
</div>
</div>
</div>
</div>
</transition>
</teleport>
</template>
<script setup>
import { ref } from "vue";
const props = defineProps({
// 弹窗提示信息
text: {
type: String,
default: "是否删除",
},
confirmBtnText: {
type: String,
default: "确定",
},
cancelBtnText: {
type: String,
default: "取消",
},
});
// 创建自定义事件
const emit = defineEmits(["confirm", "cancel"])
const visible = ref(false);
// 显示
function show() {
visible.value = true;
}
// 隐藏
function hide() {
visible.value = false
}
// 点击确定时
function confirm() {
emit("confirm");
hide();
}
// 点击取消时
function cancel() {
emit("cancel");
hide();
}
// 把组件内部的函数暴露出去给其他组件
defineExpose({
show,
})
</script>
4、使用封装组件
<my-confirm
text="是否清空播放列表"
confirmBtnText="清空"
ref="confirmRef"
@confirm="clearAll"
></my-confirm>
<script setup>
import MyConfirm from "@/components/base/Confirm.vue";
// 定义ref
const confirmRef = ref(null);
// 清空列表唤起弹窗;
function showConfirm() {
confirmRef.value.show();
}
function clearAll() {
store.dispatch("clearSongList");
}
// 暴露出去
defineExpose({
show,
});
</script>
二、设置列表打开关闭动画(一个同级元素用transition包裹)
<transition name="list-fade">
<div class="playlist" v-show="visible" @click.stop="hide">
</div>
</transition>
<style scoped lang="scss">
&.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%);
}
}
<style>
三、全屏播放器和mini播放器的显示切换
// 拿到vuex的fullScreen
const fullScreen = computed(() => store.state.fullScreen);
<!-- mini:true / 全屏:false -->
<div class="mini-player" v-show="!fullScreen"></div>
<div class="normal-player" v-show="fullScreen"></div>
四、点击全屏播放器的缩小按钮变成mini播放器
<div class="back" @click="showMiniPlayer"></div>
<script setup>
// 打开mini播放器
function showMiniPlayer() {
store.commit('setFullscreen', false);
}
<script>
五、mini播放器切换成全屏播放器
<div class="mini-player" v-show="!fullScreen" @click="showFullScreenPlayer"></div>
<script setup>
// 打开全屏播放器
function showFullScreenPlayer() {
store.commit("setFullScreen", true);
}
<script>
六、打开全屏播放器的动画效果
<transition name="normal">
<div class="normal-player" v-show="fullScreen"></div>
</transition>
<style scoped>
&.normal-enter-active,
&.normal-leave-active {
transition: all 0.6s;
.middle-l,
.top,
.bottom {
transition: all 0.6s cubic-bezier(0.45, 0, 0.55, 1);
}
}
&.normal-enter-from,
&.normal-leave-to {
opacity: 0;
.top {
transform: translate3d(0, -100px, 0);
}
.bottom {
transform: translate3d(0, 100px, 0);
}
.middle-l {
transform: scale(0);
}
}
</style>
七、优化更新列表滚动置顶
// 组件的显示和隐藏
async function show() {
visible.value = true;
// 更新一下滚动效果,等DOM更新之后在更新滚动组件
await nextTick()
scrollRef.value.scroll.refresh();
scrollToCurrent();
}
八、推荐歌单首页点击进入的数据渲染
在视图View文件夹下创建AlumDetail.vue,配置路由
<ul>
<li
@click="selectItem(item)"
class="recommend-item"
v-for="(item, index) in albums"
:key="item.id"
>
</li>
</ul>
<scipt>
function selectItem(item) {
//拿到数据的id
console.log(item.id);
}
</script>
获取数据接口(axios)
//获取歌单详情
export function getAlbum(album) {
return get('/playlist/detail', {
id: album.id
})
}
import { getAlbum } from "@/service/recommend";
九、vue3封装方法并使用
封装到一个js文件中
import { computed, onMounted, ref } from "@vue/runtime-core";
import { useRoute, useRouter } from "vue-router";
import myStorage from "@/assets/js/storage-api";
export default function createDetail(props,sessionKey, getApi) {
const route = useRoute();
const router = useRouter();
// 歌曲列表
const songs = ref([]);
const loading = ref(true);
const computedData = computed(() => {
let result = null;
const data = props.detailObj;
if (data.id) {
// props获取成功
result = data;
} else {
//props获取失败
const cached = myStorage.getLocal("sessionKey");
// 存储的值cached存在的话,并且cached的id和路由上的id保持一致
if (cached && cached.id === route.params.id / 1) {
result = cached;
}
}
return result;
});
// 背景图片
const picUrl = computed(() => {
return computedData.value ? computedData.value.picUrl : "";
});
// 标题
const listTitle = computed(() => {
return computedData.value ? computedData.value.name : "";
});
onMounted(async () => {
const data = computedData.value;
//props内和本地存储内都没有数据
if (!data) {
// 跳转回当前路由的上一级路由
router.push({ path: route.matched[0].path });
return;
}
const result = await getApi(data);
songs.value = result.hotSongs;
loading.value = false;
});
return {
picUrl,
listTitle,
songs,
loading
}
}
然后导入使用
<template>
<div class="singer-detail">
<my-musicList
:picUrl="picUrl"
:listTitle="listTitle"
:songs="songs"
:loading="loading"
></my-musicList>
</div>
</template>
<script setup>
import MyMusicList from "@/components/MusicList";
import { getSingerDetail } from "@/service/singer";
import createDetail from "@/assets/js/create-detail";
// 接收数据
const props = defineProps(["detailObj"]);
const { picUrl, listTitle, songs, loading } = createDetail(
props,
"__singerDetail__",
getSingerDetail
);
</script>