Continuando con la construcción de la interfaz de lista de reproduccción de nuestro reproductor de música, esta segunda parte se enfoca en implementar la navegación a páginas de detalle al hacer clic en una canción o lista. Abordaremos la configuración de rutas dinámicas con Vue Router y la gestión de estado mediante Vuex.
- Configuración de Rutas Dinámicas
Utilizaremos Vue Router para gestionar la navegación entre diferentes vistas. Para la navegación a detalles específicos de una canción o lista, implementaremos rutas dinámicas. Esto nos permite pasar identificadores únicos a través de la URL.
En el componente padre (por ejemplo, 'recommend.vue'), al hacer clic en un elemento de la lista, navegaremos a una ruta que incluye el ID del elemento seleccionado:
this.$router.push({
path: `/recommend/${item.id}`
});
La definición de estas rutas anidadas se realiza en el archivo de configuración de Vue Router (router/index.js):
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld.vue'
import Recommend from '@/views/Recommend.vue'
// Importar el componente de detalle de la lista de música
import MusicListDetail from '@/views/MusicListDetail.vue'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
component: HelloWorld
},
{
path: '/recommend',
component: Recommend,
// Definición de rutas hijas para mostrar detalles
children: [
{
path: ':id', // Ruta dinámica para el ID de la lista
component: MusicListDetail
}
]
},
// ... otras rutas
]
})
- Gestión de Estado con Vuex
Para compartir datos entre componentes de manera eficiente, especialmente la información de la lista de reproducción seleccionada, utilizaremos Vuex.
Primero, instalamos Vuex:
npm install vuex --save
Luego, inicializamos Vuex en su archivo principal (store/index.js):
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const state = {
selectedMusicList: null // Estado para almacenar la lista de música seleccionada
};
const getters = {
// Getter para acceder a la lista de música seleccionada
getSelectedMusicList: (state) => state.selectedMusicList
};
const mutations = {
// Mutación para actualizar la lista de música seleccionada
setMusicList(state, listData) {
state.selectedMusicList = listData;
}
};
const actions = {
// Acción para llamar a la mutación setMusicList
updateSelectedMusicList({ commit }, listData) {
commit('setMusicList', listData);
}
};
export default new Vuex.Store({
state,
mutations,
getters,
actions
});
- Componente de Lista Principal (Recommend.vue)
Este componente renderiza varias listas de música. Al hacer clic en un elemento de lista, se dispara un evento que navega a la ruta detallada y actualiza el estado de Vuex.
<template>
<div class="recommend-container" ref="containerWrapper">
<div class="content-wrapper">
<MusicListSection
title="Listas Destacadas"
:showPlayCount="true"
:showSinger="false"
:limit="6"
:resources="featuredPlaylists"
@select="handleListItemSelect"></MusicListSection>
<MusicListSection
title="Novedades Musicales"
:showPlayCount="false"
:showSinger="true"
:limit="6"
:resources="newMusicLists"
@select="handleListItemSelect"></MusicListSection>
<MusicListSection
title="Radios de Artistas"
:showPlayCount="false"
:showSinger="true"
:limit="6"
:resources="artistRadioLists"
@select="handleListItemSelect"></MusicListSection>
</div>
<!-- El router-view renderizará el componente de detalle anidado -->
<router-view></router-view>
</div>
</template>
<script>
import MusicListSection from '@/components/MusicListSection.vue';
import { mapActions } from 'vuex';
export default {
name: "recommend",
components: {
MusicListSection,
},
data() {
return {
featuredPlaylists: [], // URL para listas destacadas
newMusicLists: [], // URL para novedades
artistRadioLists: [] // URL para radios de artistas
};
},
methods: {
// Maneja la selección de un ítem de lista
handleListItemSelect(item) {
// Navega a la ruta detallada pasando el ID
this.$router.push({
path: `/recommend/${item.id}`
});
// Actualiza el estado de Vuex con la lista seleccionada
this.updateSelectedMusicList(item);
},
// Mapea la acción de Vuex para actualizar el estado
...mapActions(['updateSelectedMusicList'])
},
created() {
// Simulación de obtención de datos de API
this.featuredPlaylists = 'http://localhost:3000/top/playlist/highquality';
this.newMusicLists = 'http://localhost:3000/top/playlist?order=hot';
this.artistRadioLists = 'http://localhost:3000/personalized/djprogram';
}
};
</script>
<style lang="scss">
// Estilos para el contenedor principal
.recommend-container {
position: relative;
.content-wrapper {
// Estilos para el contenido de las listas
}
}
</style>
- Componante de Detalle de Lista (MusicListDetail.vue)
Este componente se activa cuando la ruta dinámica con el ID coinncide. Recupera la información de la lista seleccionada del estado de Vuex y la utiliza para renderizar los detalles.
<template>
<transition name="fade">
<MusicListView :musicData="currentMusicList" :listId="currentListId"></MusicListView>
</transition>
</template>
<script>
import { mapGetters } from 'vuex';
import MusicListView from '@/components/MusicListView.vue'; // Componente que muestra la lista de canciones
export default {
name: "music-list-detail",
data() {
return {
currentMusicList: {}, // Datos de la lista de música actual
currentListId: '' // ID de la lista de música actual
};
},
created() {
this.fetchMusicListData();
},
methods: {
// Obtiene los datos de la lista de música del estado de Vuex
fetchMusicListData() {
// Si no hay datos en Vuex, redirige a la página principal de recomendaciones
if (!this.getSelectedMusicList || !this.getSelectedMusicList.id) {
this.$router.push('/recommend');
return;
}
this.currentMusicList = this.getSelectedMusicList;
this.currentListId = this.getSelectedMusicList.id;
}
},
computed: {
// Mapea el getter de Vuex para acceder a la lista de música seleccionada
...mapGetters(['getSelectedMusicList'])
},
components: {
MusicListView
}
};
</script>
<style lang="scss">
// Estilos para la transición de entrada/salida
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>
- Componente de Vista de Música (MusicListView.vue)
Este componente es responsable de mostrar los detalles de una lista de música específica, incluyendo la imagen, el nombre, la descripción y la lista de canciones.
<template>
<div class="music-list-view">
<div class="header-section">
<div class="header-image-container">
<img :src="musicData.coverImgUrl" alt="Cover">
<div class="play-count-overlay"><i class="icon iconfont icon-headset"></i>{{ musicData.playCount | formatNumber }}</div>
<div class="details-button"><i class="icon iconfont icon-detail"></i></div>
</div>
<div class="header-info">
<div class="list-name">{{ musicData.name }}</div>
<div class="tags-container" v-if="musicData.tags">
<span>Etiquetas: </span><span class="tag" v-for="(tag, index) in musicData.tags" :key="index">{{ tag }}</span>
</div>
<div class="creator-info" v-if="musicData.creator">
<span>By {{ musicData.creator.nickname }}</span><span class="creation-time">Creada el {{ musicData.createTime }}</span>
</div>
</div>
<div class="back-button" @click="navigateBack">
<i class="icon iconfont icon-close"></i>
</div>
<div class="background-image">
<img :src="musicData.coverImgUrl" alt="Background">
</div>
</div>
<div class="body-section">
<div class="action-bar">
<i class="icon iconfont icon-play"></i><span class="play-all-text">Reproducir todo<i class="song-count">(Total: {{ trackList.length }} canciones)</i></span>
<span class="collect-button"><i class="icon iconfont icon-add"></i>Coleccionar({{ subscribedCount | formatNumber }})</span>
</div>
<div class="track-list-wrapper" ref="trackListContainer">
<ul class="track-list">
<li class="track-item" v-for="(track, index) in trackList" :key="index" @click="playTrack(track, index, $event)">
<div class="track-number">
<span>{{ index + 1 }}</span>
</div>
<div class="track-info">
<div class="track-name">{{ track.name }}</div>
<div class="artist-album">{{ track.ar[0].name }} - {{ track.al.name }}</div>
</div>
<div class="track-tools">
<i class="icon iconfont icon-tool"></i>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import { formatNumber } from '@/common/utils/formatter'; // Utilidad para formatear números
import { mapActions } from 'vuex';
export default {
name: "music-list-view",
props: {
musicData: { // Datos de la lista de música recibidos del componente padre
type: Object,
required: true
},
listId: { // ID de la lista de música
type: [String, Number],
required: true
}
},
data() {
return {
trackList: [], // Lista de canciones
subscribedCount: '' // Contador de suscripciones
};
},
created() {
// Obtiene los detalles de la lista de música si se proporciona un ID
if (this.listId) {
const apiUrl = `http://localhost:3000/playlist/detail?id=${this.listId}`;
this.axios.get(apiUrl)
.then((response) => {
this.trackList = response.data.playlist.tracks;
this.subscribedCount = response.data.playlist.subscribedCount;
})
.catch((error) => {
console.error("Error al obtener detalles de la lista:", error);
});
}
},
methods: {
navigateBack() {
this.$router.back(); // Navega a la ruta anterior
},
playTrack(track, index, event) {
// Lógica para reproducir una canción específica
// Ejemplo: this.selectPlay({ list: this.trackList, index: index });
},
...mapActions(['selectPlay']) // Mapea acciones de Vuex si son necesarias
},
filters: {
// Filtro para formatear números grandes
formatNumber(num) {
return formatNumber(num);
}
}
};
</script>
<style lang="scss">
@function px2rem($px) {
@return $px / 30 + rem;
}
.music-list-view {
position: fixed;
z-index: 200;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: #fff;
.header-section {
position: relative;
display: flex;
width: 100%;
height: px2rem(400);
align-items: center;
overflow: hidden;
background-color: rgba(0, 0, 0, 0.2);
.header-image-container {
display: flex;
position: relative;
justify-content: center;
flex: 0 0 px2rem(300);
width: px2rem(300);
height: px2rem(250);
img {
width: px2rem(250);
height: px2rem(250);
}
.play-count-overlay {
position: absolute;
top: 0;
right: px2rem(40);
color: #fff;
font-size: px2rem(24);
font-weight: bold;
.icon-headset {
color: #fff;
font-size: px2rem(24);
font-weight: bold;
margin-right: px2rem(8);
}
}
.details-button {
position: absolute;
bottom: 0;
right: px2rem(42);
color: #fff;
.icon-detail {
font-size: px2rem(36);
font-weight: bold;
}
}
}
.header-info {
flex: 1;
height: px2rem(250);
padding-right: px2rem(25);
color: #fff;
.list-name {
margin: px2rem(20) 0;
height: px2rem(90);
font-size: px2rem(32);
font-weight: bold;
}
.tags-container {
margin-bottom: px2rem(24);
font-size: px2rem(26);
line-height: px2rem(26);
height: px2rem(26);
.tag {
padding: 0 px2rem(12);
border: 2px solid #fff;
border-radius: px2rem(8);
margin-left: px2rem(12);
}
}
.creator-info {
font-size: px2rem(26);
line-height: px2rem(26);
height: px2rem(26);
.creation-time {
margin-left: px2rem(10);
}
}
}
.back-button {
position: absolute;
top: 0;
right: px2rem(16);
padding: px2rem(20);
color: #fff;
font-size: px2rem(32);
font-weight: bold;
}
.background-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: px2rem(400);
z-index: -1;
filter: blur(px2rem(100));
overflow: hidden;
}
}
.body-section {
.action-bar {
display: flex;
line-height: px2rem(80);
height: px2rem(80);
border-bottom: 1px solid #eee;
justify-content: center;
text-align: center;
.icon-play {
width: px2rem(80);
font-size: px2rem(48);
}
.play-all-text {
flex: 1;
font-size: px2rem(26);
text-align: left;
.song-count {
font-size: px2rem(24);
font-style: normal;
color: #7e8c8d;
}
}
.collect-button {
padding: 0 px2rem(24);
background: #F93021;
color: #fff;
font-size: px2rem(26);
font-weight: bold;
text-align: center;
.icon-add {
font-size: px2rem(26);
color: #fff;
font-weight: bold;
}
}
}
.track-list-wrapper {
position: absolute;
top: px2rem(480);
bottom: 0;
left: 0;
width: 100%;
overflow: hidden;
.track-list {
background: #fff;
.track-item {
display: flex;
&.list-complete-enter-active, &.list-complete-leave-active {
transition: all 0.2s linear;
}
&.list-complete-enter, &.list-complete-leave-to {
opacity: 0;
transform: translateY(px2rem(30));
}
.track-number {
flex: 0 0 px2rem(80);
width: px2rem(80);
height: px2rem(80);
line-height: px2rem(80);
text-align: center;
font-size: px2rem(30);
color: #7e8c8d;
}
.track-info {
flex: 1;
border-bottom: 1px solid #eee;
width: 80%;
.track-name {
font-size: px2rem(28);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.artist-album {
font-size: px2rem(22);
color: #7e8c8d;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.track-tools {
flex: 0 0 px2rem(80);
width: px2rem(80);
line-height: px2rem(80);
border-bottom: 1px solid #eee;
text-align: center;
.icon-tool {
font-size: px2rem(30);
}
}
}
}
}
}
}
</style>