YwYMusic/src/views/DiscoverView.vue

416 lines
9.4 KiB
Vue

<template>
<div class="page">
<n-carousel autoplay draggable :interval="5000" class="banner-carousel">
<div v-for="banner in banners" :key="banner.title" class="banner" :style="{ background: banner.gradient }">
<div class="banner-text">
<h2>{{ banner.title }}</h2>
<p>{{ banner.sub }}</p>
</div>
<AppIcon name="music" :size="72" class="banner-mark" />
</div>
</n-carousel>
<section class="section">
<div class="section-head">
<h3>分类</h3>
<span class="section-sub">歌单分类</span>
</div>
<n-scrollbar x-scrollable class="category-scrollbar">
<n-radio-group v-model:value="activeCategory" name="playlist-category" @update:value="playList">
<n-radio-button v-for="category in categories" :key="category.id" :value="category.id" :label="category.name" />
</n-radio-group>
</n-scrollbar>
<div class="card-grid">
<MusicCard v-for="playlist in recommendedPlaylists" :key="playlist.id" :playlist="playlist" />
</div>
</section>
<section class="section">
<div class="section-head">
<h3>推荐歌单</h3>
<span class="section-sub">为你精选</span>
</div>
<!-- <div class="card-grid">
<MusicCard v-for="playlist in recommendedPlaylists" :key="playlist.id" :playlist="playlist" />
</div> -->
</section>
<section class="section">
<div class="section-head">
<h3>新歌速递</h3>
<span class="section-sub">每日 20:00 更新</span>
</div>
<div class="new-song-row">
<div
v-for="(song, i) in newSongs"
:key="song.id"
class="new-song"
tabindex="0"
role="button"
:aria-label="`播放 ${song.title}`"
@click="playNewSong(i)"
@keydown.enter="playNewSong(i)"
>
<div class="mini-cover" :style="{ background: song.cover }">
<AppIcon name="music" :size="18" class="mini-mark" />
<span class="mini-play"><AppIcon name="play" :size="11" /></span>
</div>
<p class="mini-title" :title="song.title">{{ song.title }}</p>
<p class="mini-artist">{{ song.artist }} · {{ formatTime(song.duration) }}</p>
</div>
</div>
</section>
<section class="section">
<div class="section-head">
<h3>排行榜精选</h3>
<span class="section-sub">总有一个榜单属于你</span>
</div>
<div class="rank-preview-grid">
<div
v-for="rank in rankLists.slice(0, 3)"
:key="rank.id"
class="rank-preview"
tabindex="0"
role="button"
:aria-label="`查看榜单 ${rank.name}`"
@click="router.push(`/playlist/${rank.id}`)"
@keydown.enter="router.push(`/playlist/${rank.id}`)"
>
<div class="rank-head" :style="{ background: rank.cover }">
<span class="rank-name">{{ rank.name }}</span>
<AppIcon name="chevron-right" :size="16" />
</div>
<ol class="rank-list">
<li v-for="(song, i) in rank.songs.slice(0, 5)" :key="song.id">
<span class="rank-no" :class="{ top: i < 3 }">{{ i + 1 }}</span>
<span class="rank-title" :title="song.title">{{ song.title }}</span>
<span class="rank-artist">{{ song.artist }}</span>
</li>
</ol>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { NCarousel, useMessage } from "naive-ui"
import { useRouter } from "vue-router"
import AppIcon from "@/components/AppIcon.vue"
import MusicCard from "@/components/MusicCard.vue"
import { newSongs, rankLists, type Playlist } from "@/mocks/music"
import { playSong } from "@/stores/player"
import { proxyCover } from "@/utils/cover"
import { formatPlayCount } from "@/utils/format"
import { formatTime } from "@/utils/time"
import PlayListApi from "@/api/playlist"
import Source from "@/types/global"
import { cachePlayListSummary, type PlayListCategory } from "@/api/playlist"
const message = useMessage()
const router = useRouter()
const categories = ref<PlayListCategory[]>([])
const activeCategory = ref<string>("")
const recommendedPlaylists = ref<Playlist[]>([])
const banners = [
{
title: "本周主打",
sub: "把耳朵交给这 12 首新单曲",
gradient: "linear-gradient(120deg, #4338ca 0%, #7c3aed 55%, #ec4899 100%)",
},
{
title: "私人雷达",
sub: "根据你的口味生成的专属歌单",
gradient: "linear-gradient(120deg, #0ea5e9 0%, #22d3ee 55%, #22c55e 100%)",
},
{
title: "热歌榜",
sub: "全站播放量最高的 100 首歌",
gradient: "linear-gradient(120deg, #f59e0b 0%, #ef4444 55%, #f97316 100%)",
},
]
function playNewSong(songIndex: number) {
const song = newSongs[songIndex]
playSong(song, newSongs)
message.success(`正在播放:${song.title}`)
}
const list = async () => {
const res = await PlayListApi.categoryList([Source.KuGou])
categories.value = res[0].categories
if (categories.value.length > 0) {
activeCategory.value = categories.value[1].id
await playList(activeCategory.value)
}
}
const playList = async (categoryId: string) => {
const res = await PlayListApi.categoryPlayList(categoryId, Source.KuGou, 1, 30)
recommendedPlaylists.value = res.playlists.map((p) => {
cachePlayListSummary(p)
return {
id: p.id,
name: p.name,
desc: p.description,
cover: proxyCover(p.cover),
tags: [],
playCount: formatPlayCount(p.play_count),
songs: [],
}
})
}
onMounted(() => {
list()
})
</script>
<style scoped>
.page {
max-width: 1200px;
margin: 0 auto;
}
.banner-carousel {
height: clamp(150px, 17vw, 200px);
border-radius: 14px;
overflow: hidden;
}
.banner {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 clamp(20px, 4vw, 44px);
overflow: hidden;
}
.banner-text h2 {
margin: 0 0 8px;
font-size: clamp(22px, 3.2vw, 30px);
font-weight: 700;
color: #fff;
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);
}
.banner-text p {
margin: 0;
font-size: 15px;
color: rgba(255, 255, 255, 0.85);
}
.banner-mark {
position: absolute;
right: clamp(16px, 4vw, 36px);
opacity: 0.28;
color: #fff;
transform: rotate(-8deg);
}
.section {
margin-top: clamp(24px, 3vw, 34px);
}
.section-head {
display: flex;
align-items: baseline;
gap: 12px;
margin-bottom: 16px;
}
.section-head h3 {
margin: 0;
font-size: clamp(17px, 1.8vw, 20px);
font-weight: 600;
}
.section-sub {
font-size: 12px;
color: var(--app-text-muted);
}
.category-scrollbar {
padding-bottom: 8px;
}
.card-grid {
display: grid;
margin-top: 20px;
grid-template-columns: repeat(auto-fill, minmax(clamp(140px, 14vw, 180px), 1fr));
gap: 20px 18px;
}
.new-song-row {
display: grid;
grid-auto-flow: column;
grid-auto-columns: clamp(150px, 16vw, 200px);
gap: 14px;
overflow-x: auto;
padding-bottom: 8px;
}
.new-song {
cursor: pointer;
min-width: 0;
}
.mini-cover {
position: relative;
aspect-ratio: 1;
border-radius: 10px;
overflow: hidden;
display: grid;
place-items: center;
}
.mini-mark {
color: rgba(255, 255, 255, 0.4);
}
.mini-play {
position: absolute;
right: 8px;
bottom: 8px;
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 50%;
background: var(--app-green);
color: #052e16;
opacity: 0;
transform: translateY(5px);
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.new-song:hover .mini-play,
.new-song:focus-visible .mini-play {
opacity: 1;
transform: translateY(0);
}
.mini-title {
margin: 8px 0 2px;
font-size: 13px;
color: var(--app-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mini-artist {
margin: 0;
font-size: 12px;
color: var(--app-text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rank-preview-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
}
.rank-preview {
border: 1px solid var(--app-border);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.rank-preview:hover,
.rank-preview:focus-visible {
transform: translateY(-2px);
border-color: rgba(109, 93, 246, 0.45);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.3);
}
.rank-head {
display: flex;
align-items: center;
justify-content: space-between;
height: 52px;
padding: 0 16px;
color: rgba(255, 255, 255, 0.9);
}
.rank-name {
font-size: 16px;
font-weight: 700;
}
.rank-list {
margin: 0;
padding: 8px 16px 12px;
list-style: none;
}
.rank-list li {
display: flex;
align-items: center;
gap: 10px;
height: 30px;
font-size: 13px;
}
.rank-no {
width: 18px;
color: var(--app-text-muted);
font-variant-numeric: tabular-nums;
}
.rank-no.top {
color: var(--app-green);
font-weight: 700;
}
.rank-title {
flex: 1;
min-width: 0;
color: var(--app-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rank-artist {
color: var(--app-text-muted);
font-size: 12px;
}
@media (max-width: 720px) {
.banner-mark {
width: 48px;
height: 48px;
}
.banner-text h2 {
font-size: 22px;
}
.banner-text p {
font-size: 13px;
}
}
@media (max-width: 900px) {
.rank-preview-grid {
grid-template-columns: 1fr;
}
}
</style>