style(ui): 桌面端 message 毛玻璃样式,本地音乐页面全页滚动优化

- message 提示适配项目设计:全圆角、backdrop-blur、半透明背景、深色/浅色模式
- 本地音乐页面:hero 缩小可滚出、action bar 吸顶、歌曲列表跟随全页滚动
- 顺序播放到最后一首:用户点下一首保持播放仅提示,自然播完才停止
- i18n 新增 playListEnded(5 种语言)
This commit is contained in:
alger
2026-03-29 13:18:56 +08:00
parent 0cfec3dd82
commit eb801cfbfd
7 changed files with 247 additions and 147 deletions
+1
View File
@@ -17,6 +17,7 @@ export default {
parseFailedPlayNext: 'Song parsing failed, playing next', parseFailedPlayNext: 'Song parsing failed, playing next',
consecutiveFailsError: consecutiveFailsError:
'Playback error, possibly due to network issues or invalid source. Please switch playlist or try again later', '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: { playMode: {
sequence: 'Sequence', sequence: 'Sequence',
loop: 'Loop', loop: 'Loop',
+1
View File
@@ -17,6 +17,7 @@ export default {
parseFailedPlayNext: '楽曲の解析に失敗しました。次の曲を再生します', parseFailedPlayNext: '楽曲の解析に失敗しました。次の曲を再生します',
consecutiveFailsError: consecutiveFailsError:
'再生エラーが発生しました。ネットワークの問題または無効な音源の可能性があります。プレイリストを切り替えるか、後でもう一度お試しください', '再生エラーが発生しました。ネットワークの問題または無効な音源の可能性があります。プレイリストを切り替えるか、後でもう一度お試しください',
playListEnded: 'プレイリストの最後に到達しました',
playMode: { playMode: {
sequence: '順次再生', sequence: '順次再生',
loop: 'リピート再生', loop: 'リピート再生',
+1
View File
@@ -17,6 +17,7 @@ export default {
parseFailedPlayNext: '곡 분석 실패, 다음 곡 재생', parseFailedPlayNext: '곡 분석 실패, 다음 곡 재생',
consecutiveFailsError: consecutiveFailsError:
'재생 오류가 발생했습니다. 네트워크 문제 또는 유효하지 않은 음원일 수 있습니다. 재생 목록을 변경하거나 나중에 다시 시도하세요', '재생 오류가 발생했습니다. 네트워크 문제 또는 유효하지 않은 음원일 수 있습니다. 재생 목록을 변경하거나 나중에 다시 시도하세요',
playListEnded: '재생 목록의 마지막 곡에 도달했습니다',
playMode: { playMode: {
sequence: '순차 재생', sequence: '순차 재생',
loop: '한 곡 반복', loop: '한 곡 반복',
+1
View File
@@ -16,6 +16,7 @@ export default {
playFailed: '当前歌曲播放失败,播放下一首', playFailed: '当前歌曲播放失败,播放下一首',
parseFailedPlayNext: '歌曲解析失败,播放下一首', parseFailedPlayNext: '歌曲解析失败,播放下一首',
consecutiveFailsError: '播放遇到错误,可能是网络波动或解析源失效,请切换播放列表或稍后重试', consecutiveFailsError: '播放遇到错误,可能是网络波动或解析源失效,请切换播放列表或稍后重试',
playListEnded: '已播放到列表最后一首',
playMode: { playMode: {
sequence: '顺序播放', sequence: '顺序播放',
loop: '单曲循环', loop: '单曲循环',
+1
View File
@@ -16,6 +16,7 @@ export default {
playFailed: '目前歌曲播放失敗,播放下一首', playFailed: '目前歌曲播放失敗,播放下一首',
parseFailedPlayNext: '歌曲解析失敗,播放下一首', parseFailedPlayNext: '歌曲解析失敗,播放下一首',
consecutiveFailsError: '播放遇到錯誤,可能是網路波動或解析源失效,請切換播放清單或稍後重試', consecutiveFailsError: '播放遇到錯誤,可能是網路波動或解析源失效,請切換播放清單或稍後重試',
playListEnded: '已播放到列表最後一首',
playMode: { playMode: {
sequence: '順序播放', sequence: '順序播放',
loop: '單曲循環', loop: '單曲循環',
+112
View File
@@ -18,3 +18,115 @@ body {
.settings-slider .n-slider-mark { .settings-slider .n-slider-mark {
font-size: 10px !important; 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;
}
+130 -147
View File
@@ -1,171 +1,154 @@
<template> <template>
<div <div class="local-music-page h-full w-full bg-white dark:bg-black transition-colors duration-500">
class="local-music-page h-full w-full overflow-hidden bg-white dark:bg-black transition-colors duration-500" <n-scrollbar class="h-full">
> <div class="local-music-content pb-32">
<div class="local-music-content h-full flex flex-col"> <!-- Hero Section -->
<!-- Hero Section --> <section class="hero-section relative overflow-hidden rounded-tl-2xl">
<section class="hero-section relative overflow-hidden rounded-tl-2xl shrink-0"> <!-- 背景模糊效果 -->
<!-- 背景模糊效果 --> <div class="hero-bg absolute inset-0 -top-20">
<div class="hero-bg absolute inset-0 -top-20"> <div
<div class="absolute inset-0 bg-gradient-to-br from-primary/20 via-transparent to-primary/10 blur-3xl opacity-50 dark:opacity-30"
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> <div
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-white/80 to-white dark:via-black/80 dark:to-black"
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>
</div>
<!-- Hero 内容 --> <!-- Hero 内容 -->
<div class="hero-content relative z-10 page-padding-x pt-10 pb-8"> <div class="hero-content relative z-10 page-padding-x pt-6 pb-4">
<div class="flex flex-col md:flex-row gap-8 items-center md:items-end"> <div class="flex items-center gap-5">
<div class="cover-wrapper relative group">
<div <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>
<div class="info-content text-center md:text-left"> <div class="info-content min-w-0">
<div class="badge mb-3"> <h1
<span class="text-2xl md:text-3xl font-bold text-neutral-900 dark:text-white tracking-tight"
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"
> >
{{ t('localMusic.title') }} {{ 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> </div>
<h1 </div>
class="text-3xl md:text-4xl lg:text-5xl font-bold text-neutral-900 dark:text-white tracking-tight" </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') }} <template #prefix>
</h1> <i class="ri-search-line text-neutral-400" />
<p class="mt-4 text-sm md:text-base text-neutral-500 dark:text-neutral-400"> </template>
{{ t('localMusic.songCount', { count: localMusicStore.musicList.length }) }} </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> </p>
</div> </div>
</div> </div>
</div> </section>
</section>
<!-- Action Bar (Sticky) --> <!-- 歌曲列表 -->
<section <section class="list-section page-padding-x mt-6">
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
<div class="flex items-center justify-between gap-4"> v-if="!localMusicStore.scanning && filteredList.length === 0"
<!-- 左侧搜索框 --> class="empty-state py-20 text-center"
<div class="flex-1 max-w-xs"> >
<n-input <i class="ri-folder-music-fill text-5xl mb-4 text-neutral-200 dark:text-neutral-800" />
v-model:value="searchKeyword" <p class="text-neutral-400">{{ t('localMusic.emptyState') }}</p>
: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">
<!-- 播放全部按钮 -->
<button <button
v-if="filteredList.length > 0" class="mt-6 px-6 py-2 rounded-full bg-primary text-white text-sm font-medium hover:bg-primary/90 transition-all"
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" @click="handleAddFolder"
> >
<i class="ri-folder-add-line text-lg" /> <i class="ri-folder-add-line mr-2" />
</button> {{ t('localMusic.scanFolder') }}
<!-- 文件夹管理按钮 -->
<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> </button>
</div> </div>
</div>
</section>
<!-- 扫描进度提示 --> <!-- 歌曲列表 -->
<section v-if="localMusicStore.scanning" class="page-padding-x mt-6 shrink-0"> <div v-else-if="filteredList.length > 0" class="song-list-container">
<div <song-item
class="flex items-center gap-4 p-4 rounded-2xl bg-primary/5 dark:bg-primary/10 border border-primary/20" v-for="(item, index) in filteredSongResults"
> :key="item.id"
<n-spin size="small" /> :index="index"
<div> :item="item"
<p class="text-sm font-medium text-neutral-900 dark:text-white"> @play="handlePlaySong"
{{ 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> </div>
</n-scrollbar>
<!-- 歌曲列表 -->
<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>
<!-- 文件夹管理抽屉 --> <!-- 文件夹管理抽屉 -->
<n-drawer v-model:show="showFolderManager" :width="400" placement="right"> <n-drawer v-model:show="showFolderManager" :width="400" placement="right">