feat: v0.6.0 - 亮色主题、封面主色、发现页重做、漫游页重做、减少推荐、列表风格统一

新功能:
- 亮色主题:新增浅色外观模式,7种主题色各有对应亮色变体
- 封面主色背景:漫游抽屉自动提取封面主色,PlayerBar跟随继承
- 发现页重做:多类型搜索(歌曲/歌手/专辑)+搜索建议+搜索历史
- 漫游页重做:进入即播放,布局改为封面+歌名+播放/下一首/减少推荐
- 减少推荐:FM模式下可标记不推荐歌曲或歌手
- 列表风格统一:播放指示器跳动动画+hover播放图标+图标统一使用Lucide

修复:
- 专辑页艺术家过多时窗口缩小竖排,改为自动换行
- FM播放时退出登录后首页仍可点击下一首
- 本地音乐播放时缓冲进度条未重置
- 亮色主题下多处文字不可见
- 退出FM模式时状态未正确清理
- 暗色模式下关闭抽屉时PlayerBar闪烁亮色(改用opacity过渡)
- player.ts tickInterval双变量状态不同步,统一为clearTick/setTick

变更:
- 移除播放列表按钮数字角标
- 主页卡片标题固定白色不随主题变化
- 全项目空catch块格式统一
- 清理冗余注释和代码
This commit is contained in:
2026-05-28 22:50:22 +08:00
parent 6da544cffb
commit c275461015
36 changed files with 1079 additions and 475 deletions

View File

@ -1,13 +1,25 @@
<template>
<div :class="['flex items-center gap-4 p-3 rounded-xl cursor-pointer transition group', containerClass]">
<slot name="index" :index="index" :is-current="isCurrent">
<span v-if="showIndex" class="text-xs text-content-3 w-6 text-right flex-shrink-0">{{ index + 1 }}</span>
<div v-if="showIndex" class="w-6 text-right flex-shrink-0 flex items-center justify-end h-5">
<div v-if="isCurrent && showPlayingOverlay" class="flex items-center justify-end">
<div class="flex items-center gap-[3px] h-4">
<span class="w-[3px] bg-accent-text rounded-full animate-bounce" style="height: 50%; animation-delay: 0ms"></span>
<span class="w-[3px] bg-accent-text rounded-full animate-bounce" style="height: 100%; animation-delay: 150ms"></span>
<span class="w-[3px] bg-accent-text rounded-full animate-bounce" style="height: 35%; animation-delay: 300ms"></span>
</div>
</div>
<template v-else>
<span class="text-xs text-content-3 group-hover:hidden">{{ index + 1 }}</span>
<IconPlay class="hidden group-hover:block text-content" style="font-size: 14px" />
</template>
</div>
</slot>
<div :class="['rounded-md overflow-hidden flex-shrink-0 relative', coverClass]">
<img v-if="coverSrc" :src="coverSrc" class="w-full h-full object-cover" loading="lazy" />
<div v-else class="w-full h-full bg-muted flex items-center justify-center">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-content-4"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
<IconMusic style="font-size: 14px" class="text-content-4" />
</div>
<div v-if="isCurrent && showPlayingOverlay"
class="absolute inset-0 bg-black/30 flex items-center justify-center">
@ -37,13 +49,13 @@
<slot name="actions">
<button v-if="showLike" @click.stop="player.toggleLike(song.id)" class="text-content-3 hover:text-danger transition flex-shrink-0">
<svg v-if="player.isLiked(song.id)" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" class="text-danger"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>
<IconHeart v-if="player.isLiked(song.id)" class="w-4 h-4 text-danger [&>path]:fill-current [&>path]:stroke-0" />
<IconHeart v-else class="w-4 h-4" />
</button>
<button v-if="showDownload" @click.stop="download.downloadSong(song)" class="text-content-3 hover:text-accent-text transition flex-shrink-0" :title="download.isDownloaded(song.id) ? '已下载' : '下载'">
<svg v-if="download.isDownloading(song.id)" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin"><path d="M21 12a9 9 0 11-6.22-8.56"/></svg>
<svg v-else-if="download.isDownloaded(song.id)" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-accent-text"><polyline points="20 6 9 17 4 12"/></svg>
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
<IconLoader2 v-if="download.isDownloading(song.id)" class="w-4 h-4 animate-spin" />
<IconCheck v-else-if="download.isDownloaded(song.id)" class="w-4 h-4 text-accent-text" />
<IconDownload v-else class="w-4 h-4" />
</button>
<SongItemMenu v-if="showMenu" :song-id="song.id" />
</slot>
@ -60,6 +72,12 @@ import { useDownload } from '../composables/useDownload';
import { getCoverUrl, type Song } from '../utils/song';
import { formatDuration } from '../utils/format';
import SongItemMenu from './SongItemMenu.vue';
import IconPlay from '~icons/lucide/play';
import IconMusic from '~icons/lucide/music';
import IconHeart from '~icons/lucide/heart';
import IconLoader2 from '~icons/lucide/loader-2';
import IconCheck from '~icons/lucide/check';
import IconDownload from '~icons/lucide/download';
const router = useRouter();
const player = usePlayerStore();