Desarrollo de un Reproductor de Música con Vue.js: Navegación a Páginas de Detalle

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.

  1. 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
 ]
})
 
  1. 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
});
 
  1. 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>
 
  1. 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>
 
  1. 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>
 

Etiquetas: vue.js Vue Router vuex componentes Navegación

Publicado el 6-18 00:16