mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-03 14:20:50 +08:00
style(ui): 桌面端 message 毛玻璃样式,本地音乐页面全页滚动优化
- message 提示适配项目设计:全圆角、backdrop-blur、半透明背景、深色/浅色模式 - 本地音乐页面:hero 缩小可滚出、action bar 吸顶、歌曲列表跟随全页滚动 - 顺序播放到最后一首:用户点下一首保持播放仅提示,自然播完才停止 - i18n 新增 playListEnded(5 种语言)
This commit is contained in:
@@ -17,6 +17,7 @@ export default {
|
||||
parseFailedPlayNext: 'Song parsing failed, playing next',
|
||||
consecutiveFailsError:
|
||||
'Playback error, possibly due to network issues or invalid source. Please switch playlist or try again later',
|
||||
playListEnded: 'Reached the end of the playlist',
|
||||
playMode: {
|
||||
sequence: 'Sequence',
|
||||
loop: 'Loop',
|
||||
|
||||
@@ -17,6 +17,7 @@ export default {
|
||||
parseFailedPlayNext: '楽曲の解析に失敗しました。次の曲を再生します',
|
||||
consecutiveFailsError:
|
||||
'再生エラーが発生しました。ネットワークの問題または無効な音源の可能性があります。プレイリストを切り替えるか、後でもう一度お試しください',
|
||||
playListEnded: 'プレイリストの最後に到達しました',
|
||||
playMode: {
|
||||
sequence: '順次再生',
|
||||
loop: 'リピート再生',
|
||||
|
||||
@@ -17,6 +17,7 @@ export default {
|
||||
parseFailedPlayNext: '곡 분석 실패, 다음 곡 재생',
|
||||
consecutiveFailsError:
|
||||
'재생 오류가 발생했습니다. 네트워크 문제 또는 유효하지 않은 음원일 수 있습니다. 재생 목록을 변경하거나 나중에 다시 시도하세요',
|
||||
playListEnded: '재생 목록의 마지막 곡에 도달했습니다',
|
||||
playMode: {
|
||||
sequence: '순차 재생',
|
||||
loop: '한 곡 반복',
|
||||
|
||||
@@ -16,6 +16,7 @@ export default {
|
||||
playFailed: '当前歌曲播放失败,播放下一首',
|
||||
parseFailedPlayNext: '歌曲解析失败,播放下一首',
|
||||
consecutiveFailsError: '播放遇到错误,可能是网络波动或解析源失效,请切换播放列表或稍后重试',
|
||||
playListEnded: '已播放到列表最后一首',
|
||||
playMode: {
|
||||
sequence: '顺序播放',
|
||||
loop: '单曲循环',
|
||||
|
||||
@@ -16,6 +16,7 @@ export default {
|
||||
playFailed: '目前歌曲播放失敗,播放下一首',
|
||||
parseFailedPlayNext: '歌曲解析失敗,播放下一首',
|
||||
consecutiveFailsError: '播放遇到錯誤,可能是網路波動或解析源失效,請切換播放清單或稍後重試',
|
||||
playListEnded: '已播放到列表最後一首',
|
||||
playMode: {
|
||||
sequence: '順序播放',
|
||||
loop: '單曲循環',
|
||||
|
||||
@@ -18,3 +18,115 @@ body {
|
||||
.settings-slider .n-slider-mark {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
/* ==================== 桌面端 Message 样式 ==================== */
|
||||
|
||||
.n-message {
|
||||
border-radius: 20px !important;
|
||||
padding: 10px 18px !important;
|
||||
font-size: 13px !important;
|
||||
backdrop-filter: blur(16px) saturate(1.8) !important;
|
||||
-webkit-backdrop-filter: blur(16px) saturate(1.8) !important;
|
||||
box-shadow:
|
||||
0 4px 24px rgba(0, 0, 0, 0.08),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05) !important;
|
||||
border: none !important;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
}
|
||||
|
||||
/* 浅色模式 */
|
||||
.n-message {
|
||||
background: rgba(255, 255, 255, 0.72) !important;
|
||||
color: #1a1a1a !important;
|
||||
}
|
||||
|
||||
/* 深色模式 */
|
||||
.dark .n-message {
|
||||
background: rgba(40, 40, 40, 0.75) !important;
|
||||
color: #e5e5e5 !important;
|
||||
box-shadow:
|
||||
0 4px 24px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06) !important;
|
||||
}
|
||||
|
||||
/* 成功 */
|
||||
.n-message--success-type {
|
||||
background: rgba(34, 197, 94, 0.15) !important;
|
||||
color: #16a34a !important;
|
||||
}
|
||||
.n-message--success-type .n-message__icon {
|
||||
color: #22c55e !important;
|
||||
}
|
||||
.dark .n-message--success-type {
|
||||
background: rgba(34, 197, 94, 0.18) !important;
|
||||
color: #4ade80 !important;
|
||||
}
|
||||
.dark .n-message--success-type .n-message__icon {
|
||||
color: #4ade80 !important;
|
||||
}
|
||||
|
||||
/* 错误 */
|
||||
.n-message--error-type {
|
||||
background: rgba(239, 68, 68, 0.12) !important;
|
||||
color: #dc2626 !important;
|
||||
}
|
||||
.n-message--error-type .n-message__icon {
|
||||
color: #ef4444 !important;
|
||||
}
|
||||
.dark .n-message--error-type {
|
||||
background: rgba(239, 68, 68, 0.18) !important;
|
||||
color: #f87171 !important;
|
||||
}
|
||||
.dark .n-message--error-type .n-message__icon {
|
||||
color: #f87171 !important;
|
||||
}
|
||||
|
||||
/* 警告 */
|
||||
.n-message--warning-type {
|
||||
background: rgba(245, 158, 11, 0.12) !important;
|
||||
color: #d97706 !important;
|
||||
}
|
||||
.n-message--warning-type .n-message__icon {
|
||||
color: #f59e0b !important;
|
||||
}
|
||||
.dark .n-message--warning-type {
|
||||
background: rgba(245, 158, 11, 0.18) !important;
|
||||
color: #fbbf24 !important;
|
||||
}
|
||||
.dark .n-message--warning-type .n-message__icon {
|
||||
color: #fbbf24 !important;
|
||||
}
|
||||
|
||||
/* 信息 */
|
||||
.n-message--info-type {
|
||||
background: rgba(59, 130, 246, 0.12) !important;
|
||||
color: #2563eb !important;
|
||||
}
|
||||
.n-message--info-type .n-message__icon {
|
||||
color: #3b82f6 !important;
|
||||
}
|
||||
.dark .n-message--info-type {
|
||||
background: rgba(59, 130, 246, 0.18) !important;
|
||||
color: #60a5fa !important;
|
||||
}
|
||||
.dark .n-message--info-type .n-message__icon {
|
||||
color: #60a5fa !important;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.n-message--loading-type {
|
||||
background: rgba(255, 255, 255, 0.72) !important;
|
||||
}
|
||||
.dark .n-message--loading-type {
|
||||
background: rgba(40, 40, 40, 0.75) !important;
|
||||
}
|
||||
|
||||
/* 图标统一大小 */
|
||||
.n-message__icon {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
/* 间距优化 */
|
||||
.n-message-wrapper {
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
@@ -1,171 +1,154 @@
|
||||
<template>
|
||||
<div
|
||||
class="local-music-page h-full w-full overflow-hidden bg-white dark:bg-black transition-colors duration-500"
|
||||
>
|
||||
<div class="local-music-content h-full flex flex-col">
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section relative overflow-hidden rounded-tl-2xl shrink-0">
|
||||
<!-- 背景模糊效果 -->
|
||||
<div class="hero-bg absolute inset-0 -top-20">
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-primary/20 via-transparent to-primary/10 blur-3xl opacity-50 dark:opacity-30"
|
||||
></div>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-b from-transparent via-white/80 to-white dark:via-black/80 dark:to-black"
|
||||
></div>
|
||||
</div>
|
||||
<div class="local-music-page h-full w-full bg-white dark:bg-black transition-colors duration-500">
|
||||
<n-scrollbar class="h-full">
|
||||
<div class="local-music-content pb-32">
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section relative overflow-hidden rounded-tl-2xl">
|
||||
<!-- 背景模糊效果 -->
|
||||
<div class="hero-bg absolute inset-0 -top-20">
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-br from-primary/20 via-transparent to-primary/10 blur-3xl opacity-50 dark:opacity-30"
|
||||
></div>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-b from-transparent via-white/80 to-white dark:via-black/80 dark:to-black"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Hero 内容 -->
|
||||
<div class="hero-content relative z-10 page-padding-x pt-10 pb-8">
|
||||
<div class="flex flex-col md:flex-row gap-8 items-center md:items-end">
|
||||
<div class="cover-wrapper relative group">
|
||||
<!-- Hero 内容 -->
|
||||
<div class="hero-content relative z-10 page-padding-x pt-6 pb-4">
|
||||
<div class="flex items-center gap-5">
|
||||
<div
|
||||
class="cover-container relative w-32 h-32 md:w-40 md:h-40 rounded-2xl bg-primary/10 flex items-center justify-center shadow-2xl ring-4 ring-white/50 dark:ring-neutral-800/50"
|
||||
class="cover-container relative w-20 h-20 rounded-2xl bg-primary/10 flex items-center justify-center shadow-lg ring-2 ring-white/50 dark:ring-neutral-800/50 shrink-0"
|
||||
>
|
||||
<i class="ri-folder-music-fill text-6xl text-primary opacity-80" />
|
||||
<i class="ri-folder-music-fill text-4xl text-primary opacity-80" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-content text-center md:text-left">
|
||||
<div class="badge mb-3">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-primary/10 dark:bg-primary/20 text-primary text-xs font-semibold uppercase tracking-wider"
|
||||
<div class="info-content min-w-0">
|
||||
<h1
|
||||
class="text-2xl md:text-3xl font-bold text-neutral-900 dark:text-white tracking-tight"
|
||||
>
|
||||
{{ t('localMusic.title') }}
|
||||
</span>
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{{ t('localMusic.songCount', { count: localMusicStore.musicList.length }) }}
|
||||
</p>
|
||||
</div>
|
||||
<h1
|
||||
class="text-3xl md:text-4xl lg:text-5xl font-bold text-neutral-900 dark:text-white tracking-tight"
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Action Bar (Sticky on scroll) -->
|
||||
<section
|
||||
class="action-bar sticky top-0 z-20 page-padding-x py-3 md:py-4 bg-white/80 dark:bg-black/80 backdrop-blur-xl border-b border-neutral-100 dark:border-neutral-800/50"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<!-- 左侧:搜索框 -->
|
||||
<div class="flex-1 max-w-xs">
|
||||
<n-input
|
||||
v-model:value="searchKeyword"
|
||||
:placeholder="t('localMusic.search')"
|
||||
clearable
|
||||
size="small"
|
||||
round
|
||||
>
|
||||
{{ t('localMusic.title') }}
|
||||
</h1>
|
||||
<p class="mt-4 text-sm md:text-base text-neutral-500 dark:text-neutral-400">
|
||||
{{ t('localMusic.songCount', { count: localMusicStore.musicList.length }) }}
|
||||
<template #prefix>
|
||||
<i class="ri-search-line text-neutral-400" />
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:操作按钮 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- 播放全部按钮 -->
|
||||
<button
|
||||
v-if="filteredList.length > 0"
|
||||
class="action-btn-pill flex items-center gap-2 px-4 py-2 rounded-full font-semibold text-sm transition-all bg-primary text-white hover:bg-primary/90"
|
||||
@click="handlePlayAll"
|
||||
>
|
||||
<i class="ri-play-fill text-lg" />
|
||||
<span class="hidden md:inline">{{ t('localMusic.playAll') }}</span>
|
||||
</button>
|
||||
|
||||
<!-- 扫描按钮 -->
|
||||
<button
|
||||
class="action-btn-icon w-10 h-10 rounded-full flex items-center justify-center bg-neutral-100 dark:bg-neutral-900 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-all"
|
||||
:disabled="localMusicStore.scanning"
|
||||
@click="handleScan"
|
||||
>
|
||||
<i
|
||||
class="ri-refresh-line text-lg"
|
||||
:class="{ 'animate-spin': localMusicStore.scanning }"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- 添加文件夹按钮 -->
|
||||
<button
|
||||
class="action-btn-icon w-10 h-10 rounded-full flex items-center justify-center bg-neutral-100 dark:bg-neutral-900 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-all"
|
||||
@click="handleAddFolder"
|
||||
>
|
||||
<i class="ri-folder-add-line text-lg" />
|
||||
</button>
|
||||
|
||||
<!-- 文件夹管理按钮 -->
|
||||
<button
|
||||
v-if="localMusicStore.folderPaths.length > 0"
|
||||
class="action-btn-icon w-10 h-10 rounded-full flex items-center justify-center bg-neutral-100 dark:bg-neutral-900 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-all"
|
||||
@click="showFolderManager = true"
|
||||
>
|
||||
<i class="ri-folder-settings-line text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 扫描进度提示 -->
|
||||
<section v-if="localMusicStore.scanning" class="page-padding-x mt-6">
|
||||
<div
|
||||
class="flex items-center gap-4 p-4 rounded-2xl bg-primary/5 dark:bg-primary/10 border border-primary/20"
|
||||
>
|
||||
<n-spin size="small" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-neutral-900 dark:text-white">
|
||||
{{ t('localMusic.scanning') }}
|
||||
</p>
|
||||
<p class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
|
||||
{{ t('localMusic.songCount', { count: localMusicStore.scanProgress }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Action Bar (Sticky) -->
|
||||
<section
|
||||
class="action-bar z-20 shrink-0 page-padding-x py-3 md:py-4 bg-white/80 dark:bg-black/80 backdrop-blur-xl border-b border-neutral-100 dark:border-neutral-800/50"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<!-- 左侧:搜索框 -->
|
||||
<div class="flex-1 max-w-xs">
|
||||
<n-input
|
||||
v-model:value="searchKeyword"
|
||||
:placeholder="t('localMusic.search')"
|
||||
clearable
|
||||
size="small"
|
||||
round
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="ri-search-line text-neutral-400" />
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:操作按钮 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- 播放全部按钮 -->
|
||||
<!-- 歌曲列表 -->
|
||||
<section class="list-section page-padding-x mt-6">
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="!localMusicStore.scanning && filteredList.length === 0"
|
||||
class="empty-state py-20 text-center"
|
||||
>
|
||||
<i class="ri-folder-music-fill text-5xl mb-4 text-neutral-200 dark:text-neutral-800" />
|
||||
<p class="text-neutral-400">{{ t('localMusic.emptyState') }}</p>
|
||||
<button
|
||||
v-if="filteredList.length > 0"
|
||||
class="action-btn-pill flex items-center gap-2 px-4 py-2 rounded-full font-semibold text-sm transition-all bg-primary text-white hover:bg-primary/90"
|
||||
@click="handlePlayAll"
|
||||
>
|
||||
<i class="ri-play-fill text-lg" />
|
||||
<span class="hidden md:inline">{{ t('localMusic.playAll') }}</span>
|
||||
</button>
|
||||
|
||||
<!-- 扫描按钮 -->
|
||||
<button
|
||||
class="action-btn-icon w-10 h-10 rounded-full flex items-center justify-center bg-neutral-100 dark:bg-neutral-900 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-all"
|
||||
:disabled="localMusicStore.scanning"
|
||||
@click="handleScan"
|
||||
>
|
||||
<i
|
||||
class="ri-refresh-line text-lg"
|
||||
:class="{ 'animate-spin': localMusicStore.scanning }"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- 添加文件夹按钮 -->
|
||||
<button
|
||||
class="action-btn-icon w-10 h-10 rounded-full flex items-center justify-center bg-neutral-100 dark:bg-neutral-900 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-all"
|
||||
class="mt-6 px-6 py-2 rounded-full bg-primary text-white text-sm font-medium hover:bg-primary/90 transition-all"
|
||||
@click="handleAddFolder"
|
||||
>
|
||||
<i class="ri-folder-add-line text-lg" />
|
||||
</button>
|
||||
|
||||
<!-- 文件夹管理按钮 -->
|
||||
<button
|
||||
v-if="localMusicStore.folderPaths.length > 0"
|
||||
class="action-btn-icon w-10 h-10 rounded-full flex items-center justify-center bg-neutral-100 dark:bg-neutral-900 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-all"
|
||||
@click="showFolderManager = true"
|
||||
>
|
||||
<i class="ri-folder-settings-line text-lg" />
|
||||
<i class="ri-folder-add-line mr-2" />
|
||||
{{ t('localMusic.scanFolder') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 扫描进度提示 -->
|
||||
<section v-if="localMusicStore.scanning" class="page-padding-x mt-6 shrink-0">
|
||||
<div
|
||||
class="flex items-center gap-4 p-4 rounded-2xl bg-primary/5 dark:bg-primary/10 border border-primary/20"
|
||||
>
|
||||
<n-spin size="small" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-neutral-900 dark:text-white">
|
||||
{{ t('localMusic.scanning') }}
|
||||
</p>
|
||||
<p class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
|
||||
{{ t('localMusic.songCount', { count: localMusicStore.scanProgress }) }}
|
||||
</p>
|
||||
<!-- 歌曲列表 -->
|
||||
<div v-else-if="filteredList.length > 0" class="song-list-container">
|
||||
<song-item
|
||||
v-for="(item, index) in filteredSongResults"
|
||||
:key="item.id"
|
||||
:index="index"
|
||||
:item="item"
|
||||
@play="handlePlaySong"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 歌曲列表 -->
|
||||
<section class="list-section page-padding-x mt-6 flex-1 min-h-0">
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="!localMusicStore.scanning && filteredList.length === 0"
|
||||
class="empty-state py-20 text-center"
|
||||
>
|
||||
<i class="ri-folder-music-fill text-5xl mb-4 text-neutral-200 dark:text-neutral-800" />
|
||||
<p class="text-neutral-400">{{ t('localMusic.emptyState') }}</p>
|
||||
<button
|
||||
class="mt-6 px-6 py-2 rounded-full bg-primary text-white text-sm font-medium hover:bg-primary/90 transition-all"
|
||||
@click="handleAddFolder"
|
||||
>
|
||||
<i class="ri-folder-add-line mr-2" />
|
||||
{{ t('localMusic.scanFolder') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 虚拟列表 -->
|
||||
<div v-else-if="filteredList.length > 0" class="song-list-container h-full">
|
||||
<n-virtual-list
|
||||
class="song-virtual-list h-full"
|
||||
:items="filteredSongResults"
|
||||
:item-size="70"
|
||||
item-resizable
|
||||
key-field="id"
|
||||
>
|
||||
<template #default="{ item, index }">
|
||||
<div>
|
||||
<song-item :index="index" :item="item" @play="handlePlaySong" />
|
||||
<!-- 列表末尾留白 -->
|
||||
<div v-if="index === filteredSongResults.length - 1" class="h-36"></div>
|
||||
</div>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
|
||||
<!-- 文件夹管理抽屉 -->
|
||||
<n-drawer v-model:show="showFolderManager" :width="400" placement="right">
|
||||
|
||||
Reference in New Issue
Block a user