refactor(ui): 优化骨架屏加载效果,修复用户页左侧黑色背景

- 关键布局组件(AppMenu/TitleBar/SearchBar)改为同步导入,消除加载闪烁
- 新增全局 skeleton-shimmer 流光动画替代 animate-pulse 闪烁效果
- 用户页 loading 骨架屏避免使用 .left scoped 样式导致的深色背景
- 全部 n-skeleton 组件替换为原生 div + shimmer,统一圆角风格
- 菜单容器添加背景色防止加载穿透
This commit is contained in:
alger
2026-03-11 23:02:04 +08:00
parent b5bac30258
commit 72fabc6d12
22 changed files with 133 additions and 110 deletions

View File

@@ -136,3 +136,43 @@
font-size: 2.25rem;
}
}
/* 骨架屏 shimmer 动画 */
.skeleton-shimmer {
position: relative;
overflow: hidden;
background: #e5e7eb;
}
.skeleton-shimmer::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.4) 50%,
transparent 100%
);
animation: shimmer 1.5s infinite;
transform: translateX(-100%);
}
:root.dark .skeleton-shimmer {
background: #262626;
}
:root.dark .skeleton-shimmer::after {
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.06) 50%,
transparent 100%
);
}
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}

View File

@@ -69,6 +69,10 @@ import { usePlayerStore } from '@/store/modules/player';
import { useSettingsStore } from '@/store/modules/settings';
import { isElectron } from '@/utils';
// 关键布局组件同步导入(始终可见,避免加载闪烁)
import AppMenu from './components/AppMenu.vue';
import SearchBar from './components/SearchBar.vue';
import TitleBar from './components/TitleBar.vue';
// 移动端专用布局
import MobileLayout from './MobileLayout.vue';
@@ -87,11 +91,9 @@ const keepAliveInclude = computed(() => {
.filter(Boolean);
});
const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));
// 非关键组件保持异步加载
const PlayBar = defineAsyncComponent(() => import('@/components/player/PlayBar.vue'));
const MobilePlayBar = defineAsyncComponent(() => import('@/components/player/MobilePlayBar.vue'));
const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'));
const TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue'));
const PlayingListDrawer = defineAsyncComponent(
() => import('@/components/player/PlayingListDrawer.vue')
);
@@ -151,7 +153,7 @@ provide('openPlaylistDrawer', openPlaylistDrawer);
}
.menu {
@apply h-full;
@apply h-full bg-light dark:bg-black;
}
.main {

View File

@@ -37,9 +37,8 @@ import otherRouter from '@/router/other';
import { useMenuStore } from '@/store/modules/menu';
import { usePlayerStore } from '@/store/modules/player';
import AppMenu from './components/AppMenu.vue';
import MobileHeader from './components/MobileHeader.vue';
const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));
const MobilePlayBar = defineAsyncComponent(() => import('@/components/player/MobilePlayBar.vue'));
const PlayingListDrawer = defineAsyncComponent(
() => import('@/components/player/PlayingListDrawer.vue')

View File

@@ -30,11 +30,9 @@
<!-- Loading State -->
<template v-if="loading && page === 0">
<div v-for="i in 15" :key="`loading-${i}`" class="space-y-3">
<div
class="aspect-square animate-pulse rounded-2xl bg-neutral-200 dark:bg-neutral-800"
/>
<div class="h-4 w-3/4 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="h-3 w-1/2 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="aspect-square skeleton-shimmer rounded-2xl" />
<div class="h-4 w-3/4 skeleton-shimmer rounded-lg" />
<div class="h-3 w-1/2 skeleton-shimmer rounded-lg" />
</div>
</template>

View File

@@ -9,17 +9,19 @@
<!-- Hero Skeleton -->
<div class="hero-section relative h-[400px] overflow-hidden rounded-tl-2xl">
<div class="hero-bg absolute inset-0 -top-20">
<div class="absolute inset-0 bg-neutral-200 dark:bg-neutral-800" />
<div class="absolute inset-0 skeleton-shimmer" />
</div>
<div class="hero-content relative z-10 px-4 pb-6 pt-4 md:px-8 md:pt-8">
<div class="flex flex-col items-center gap-6 md:flex-row md:items-end md:gap-10">
<n-skeleton class="h-36 w-36 rounded-full md:h-48 md:w-48" />
<div
class="h-36 w-36 md:h-48 md:w-48 skeleton-shimmer rounded-full flex-shrink-0"
/>
<div class="flex-1 space-y-4 text-center md:text-left">
<n-skeleton class="h-6 w-20 rounded-full" />
<n-skeleton class="h-10 w-1/2 md:h-12" />
<div class="h-6 w-20 skeleton-shimmer rounded-full" />
<div class="h-10 w-1/2 md:h-12 skeleton-shimmer rounded-xl" />
<div class="flex justify-center gap-4 md:justify-start">
<n-skeleton class="h-6 w-24" />
<n-skeleton class="h-6 w-24" />
<div class="h-6 w-24 skeleton-shimmer rounded-lg" />
<div class="h-6 w-24 skeleton-shimmer rounded-lg" />
</div>
</div>
</div>
@@ -29,12 +31,12 @@
<div class="mt-8 page-padding-x">
<div class="space-y-4">
<div v-for="i in 8" :key="i" class="flex items-center gap-4">
<n-skeleton class="h-12 w-12 rounded-xl" />
<div class="h-12 w-12 skeleton-shimmer rounded-xl flex-shrink-0" />
<div class="flex-1 space-y-2">
<n-skeleton text class="w-1/3" />
<n-skeleton text class="w-1/4" />
<div class="h-4 w-1/3 skeleton-shimmer rounded-lg" />
<div class="h-3 w-1/4 skeleton-shimmer rounded-lg" />
</div>
<n-skeleton class="h-8 w-8 rounded-full" />
<div class="h-8 w-8 skeleton-shimmer rounded-full flex-shrink-0" />
</div>
</div>
</div>

View File

@@ -20,9 +20,9 @@
<!-- Loading Skeleton -->
<div v-if="loading" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-6">
<div v-for="i in displayCount" :key="i" class="space-y-3">
<div class="aspect-square animate-pulse rounded-2xl bg-neutral-200 dark:bg-neutral-800" />
<div class="h-4 w-3/4 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="h-3 w-1/2 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="aspect-square skeleton-shimmer rounded-2xl" />
<div class="h-4 w-3/4 skeleton-shimmer rounded-lg" />
<div class="h-3 w-1/2 skeleton-shimmer rounded-lg" />
</div>
</div>

View File

@@ -2,11 +2,9 @@
<section class="artists-section">
<!-- Loading Skeleton -->
<div v-if="loading" class="artists-scroll flex gap-6 md:gap-8 overflow-x-hidden pb-4">
<div v-for="i in 8" :key="i" class="artist-skeleton flex flex-col items-center gap-3">
<div
class="h-20 w-20 md:h-24 md:w-24 lg:h-28 lg:w-28 animate-pulse rounded-full bg-neutral-200 dark:bg-neutral-800"
/>
<div class="h-3 w-16 animate-pulse rounded bg-neutral-100 dark:bg-neutral-800" />
<div v-for="i in 8" :key="i" class="flex flex-col items-center gap-3">
<div class="h-20 w-20 md:h-24 md:w-24 lg:h-28 lg:w-28 skeleton-shimmer rounded-full" />
<div class="h-3 w-16 skeleton-shimmer rounded-lg" />
</div>
</div>

View File

@@ -12,7 +12,7 @@
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
loading="lazy"
/>
<div v-else class="h-full w-full animate-pulse bg-neutral-200 dark:bg-neutral-800" />
<div v-else class="h-full w-full skeleton-shimmer" />
<!-- 播放按钮遮罩 (Apple Music 风格) -->
<div

View File

@@ -29,7 +29,7 @@
<div
v-for="i in 12"
:key="i"
class="skeleton-item aspect-square animate-pulse rounded-2xl md:rounded-3xl bg-neutral-100 dark:bg-neutral-800/50"
class="aspect-square skeleton-shimmer rounded-2xl md:rounded-3xl"
/>
</div>

View File

@@ -6,16 +6,12 @@
<div
v-for="i in 6"
:key="i"
class="skeleton-pill h-10 w-24 flex-shrink-0 animate-pulse rounded-full bg-neutral-200 dark:bg-neutral-800"
class="h-10 w-24 flex-shrink-0 skeleton-shimmer rounded-full"
/>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div
class="skeleton-card aspect-[2.2/1] animate-pulse rounded-2xl bg-neutral-200 dark:bg-neutral-800"
/>
<div
class="skeleton-card aspect-[2.2/1] animate-pulse rounded-2xl bg-neutral-200 dark:bg-neutral-800"
/>
<div class="aspect-[2.2/1] skeleton-shimmer rounded-2xl" />
<div class="aspect-[2.2/1] skeleton-shimmer rounded-2xl" />
</div>
</div>

View File

@@ -19,11 +19,7 @@
<!-- Loading Skeleton -->
<div v-if="loading" class="songs-grid grid gap-3" :class="gridClass">
<div
v-for="i in 10"
:key="i"
class="skeleton-item h-20 animate-pulse rounded-xl md:rounded-2xl bg-neutral-100 dark:bg-neutral-800/50"
/>
<div v-for="i in 10" :key="i" class="h-20 skeleton-shimmer rounded-xl md:rounded-2xl" />
</div>
<!-- Songs Grid (Even columns: 12345) -->

View File

@@ -20,9 +20,9 @@
<!-- Loading Skeleton -->
<div v-if="loading" class="grid gap-6" :style="gridStyle">
<div v-for="i in displayCount" :key="i" class="space-y-3">
<div class="aspect-square animate-pulse rounded-2xl bg-neutral-200 dark:bg-neutral-800" />
<div class="h-4 w-3/4 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="h-3 w-1/2 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="aspect-square skeleton-shimmer rounded-2xl" />
<div class="h-4 w-3/4 skeleton-shimmer rounded-lg" />
<div class="h-3 w-1/2 skeleton-shimmer rounded-lg" />
</div>
</div>

View File

@@ -18,7 +18,7 @@
<div
v-for="i in 3"
:key="i"
class="skeleton-item animate-pulse rounded-2xl md:rounded-3xl bg-neutral-100 dark:bg-neutral-800/50"
class="skeleton-shimmer rounded-2xl md:rounded-3xl"
style="aspect-ratio: 16/9"
/>
</div>

View File

@@ -30,10 +30,8 @@
<!-- Loading State -->
<template v-if="loading && page === 0">
<div v-for="i in 15" :key="`loading-${i}`" class="space-y-3">
<div
class="aspect-square animate-pulse rounded-2xl bg-neutral-200 dark:bg-neutral-800"
/>
<div class="h-4 w-3/4 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="aspect-square skeleton-shimmer rounded-2xl" />
<div class="h-4 w-3/4 skeleton-shimmer rounded-lg" />
</div>
</template>

View File

@@ -31,11 +31,9 @@
<!-- Loading State -->
<div v-if="initLoading" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<div v-for="i in 12" :key="i" class="space-y-3">
<div
class="aspect-video animate-pulse rounded-2xl bg-neutral-200 dark:bg-neutral-800"
/>
<div class="h-4 w-3/4 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="h-3 w-1/2 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="aspect-video skeleton-shimmer rounded-2xl" />
<div class="h-4 w-3/4 skeleton-shimmer rounded-lg" />
<div class="h-3 w-1/2 skeleton-shimmer rounded-lg" />
</div>
</div>

View File

@@ -193,11 +193,9 @@
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-6"
>
<div v-for="i in 10" :key="`skeleton-${i}`" class="space-y-3">
<div
class="aspect-square animate-pulse rounded-2xl bg-neutral-200 dark:bg-neutral-800"
/>
<div class="h-4 w-3/4 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="h-3 w-1/2 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="aspect-square skeleton-shimmer rounded-2xl" />
<div class="h-4 w-3/4 skeleton-shimmer rounded-lg" />
<div class="h-3 w-1/2 skeleton-shimmer rounded-lg" />
</div>
</div>
@@ -234,11 +232,9 @@
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-6">
<template v-if="categoryLoading && categoryPage === 0">
<div v-for="i in 15" :key="`loading-${i}`" class="space-y-3">
<div
class="aspect-square animate-pulse rounded-2xl bg-neutral-200 dark:bg-neutral-800"
/>
<div class="h-4 w-3/4 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="h-3 w-1/2 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="aspect-square skeleton-shimmer rounded-2xl" />
<div class="h-4 w-3/4 skeleton-shimmer rounded-lg" />
<div class="h-3 w-1/2 skeleton-shimmer rounded-lg" />
</div>
</template>

View File

@@ -117,10 +117,8 @@
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
>
<div v-for="i in 12" :key="i" class="space-y-3">
<div
class="aspect-square animate-pulse rounded-2xl bg-neutral-200 dark:bg-neutral-800"
/>
<div class="h-4 w-3/4 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="aspect-square skeleton-shimmer rounded-2xl" />
<div class="h-4 w-3/4 skeleton-shimmer rounded-lg" />
</div>
</div>

View File

@@ -19,11 +19,9 @@
<!-- Loading State -->
<div v-if="loading" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-6">
<div v-for="i in 15" :key="i" class="space-y-3">
<div
class="aspect-square animate-pulse rounded-2xl bg-neutral-200 dark:bg-neutral-800"
/>
<div class="h-4 w-3/4 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="h-3 w-1/2 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
<div class="aspect-square skeleton-shimmer rounded-2xl" />
<div class="h-4 w-3/4 skeleton-shimmer rounded-lg" />
<div class="h-3 w-1/2 skeleton-shimmer rounded-lg" />
</div>
</div>

View File

@@ -6,30 +6,32 @@
<div v-if="loading">
<!-- Hero Skeleton -->
<div class="relative h-[300px] overflow-hidden rounded-tl-2xl">
<div class="absolute inset-0 bg-neutral-200 dark:bg-neutral-800" />
<div class="absolute inset-0 skeleton-shimmer" />
<div class="relative z-10 page-padding-x pt-8 pb-6">
<div class="flex flex-col items-center gap-6 md:flex-row md:items-end md:gap-10">
<n-skeleton class="h-28 w-28 rounded-full md:h-40 md:w-40" />
<div
class="h-28 w-28 md:h-40 md:w-40 skeleton-shimmer rounded-full flex-shrink-0"
/>
<div class="flex-1 space-y-4 text-center md:text-left">
<n-skeleton class="h-8 w-40" />
<div class="h-8 w-40 skeleton-shimmer rounded-xl" />
<div class="flex justify-center gap-6 md:justify-start">
<n-skeleton class="h-12 w-16" />
<n-skeleton class="h-12 w-16" />
<n-skeleton class="h-12 w-16" />
<div class="h-12 w-16 skeleton-shimmer rounded-xl" />
<div class="h-12 w-16 skeleton-shimmer rounded-xl" />
<div class="h-12 w-16 skeleton-shimmer rounded-xl" />
</div>
<n-skeleton class="h-4 w-2/3" />
<div class="h-4 w-2/3 skeleton-shimmer rounded-lg" />
</div>
</div>
</div>
</div>
<!-- Content Skeleton -->
<div class="mt-8 page-padding-x">
<n-skeleton class="h-10 w-48 mb-6" />
<div class="h-10 w-48 mb-6 skeleton-shimmer rounded-xl" />
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
<div v-for="i in 10" :key="i" class="space-y-2">
<n-skeleton class="aspect-square w-full rounded-2xl" />
<n-skeleton text class="w-3/4" />
<n-skeleton text class="w-1/2" />
<div class="aspect-square w-full skeleton-shimmer rounded-2xl" />
<div class="h-4 w-3/4 skeleton-shimmer rounded-lg" />
<div class="h-3 w-1/2 skeleton-shimmer rounded-lg" />
</div>
</div>
</div>

View File

@@ -5,14 +5,14 @@
<!-- Loading State -->
<div v-if="followerListLoading && followerList.length === 0">
<div class="page-padding-x pt-8">
<n-skeleton class="h-8 w-48 mb-6" />
<div class="h-8 w-48 mb-6 skeleton-shimmer rounded-xl" />
<div
class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6"
>
<div v-for="i in 12" :key="i" class="flex flex-col items-center space-y-3">
<n-skeleton class="h-20 w-20 rounded-full" />
<n-skeleton text class="w-16" />
<n-skeleton text class="w-24" />
<div class="h-20 w-20 skeleton-shimmer rounded-full" />
<div class="h-4 w-16 skeleton-shimmer rounded-lg" />
<div class="h-3 w-24 skeleton-shimmer rounded-lg" />
</div>
</div>
</div>

View File

@@ -5,14 +5,14 @@
<!-- Loading State -->
<div v-if="followListLoading && followList.length === 0">
<div class="page-padding-x pt-8">
<n-skeleton class="h-8 w-48 mb-6" />
<div class="h-8 w-48 mb-6 skeleton-shimmer rounded-xl" />
<div
class="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6"
>
<div v-for="i in 12" :key="i" class="flex flex-col items-center space-y-3">
<n-skeleton class="h-20 w-20 rounded-full" />
<n-skeleton text class="w-16" />
<n-skeleton text class="w-24" />
<div class="h-20 w-20 skeleton-shimmer rounded-full" />
<div class="h-4 w-16 skeleton-shimmer rounded-lg" />
<div class="h-3 w-24 skeleton-shimmer rounded-lg" />
</div>
</div>
</div>

View File

@@ -1,30 +1,32 @@
<template>
<div class="user-page">
<template v-if="infoLoading">
<div class="left bg-gray-200 p-4 dark:bg-gray-800">
<div
class="left-skeleton flex-1 max-w-[600px] rounded-2xl overflow-hidden p-4 bg-light-200 dark:bg-dark-100"
>
<div class="flex flex-col gap-6">
<div class="flex justify-between">
<n-skeleton text class="h-8 w-32" />
<n-skeleton text class="h-6 w-20" />
<div class="h-8 w-32 skeleton-shimmer rounded-lg" />
<div class="h-6 w-20 skeleton-shimmer rounded-lg" />
</div>
<div class="flex items-center gap-4">
<n-skeleton class="h-[50px] w-[50px] rounded-full" />
<div class="h-[50px] w-[50px] skeleton-shimmer rounded-full" />
<div class="flex w-2/5 justify-around">
<div v-for="i in 3" :key="i" class="flex flex-col items-center gap-1">
<n-skeleton text class="h-5 w-8" />
<n-skeleton text class="h-4 w-12" />
<div class="h-5 w-8 skeleton-shimmer rounded-lg" />
<div class="h-4 w-12 skeleton-shimmer rounded-lg" />
</div>
</div>
</div>
<n-skeleton text class="h-4 w-3/4" />
<div class="h-4 w-3/4 skeleton-shimmer rounded-lg" />
<div class="mt-4 rounded-xl bg-light p-4 dark:bg-black">
<n-skeleton class="mb-4 h-8 w-full rounded-xl" />
<div class="mb-4 h-8 w-full skeleton-shimmer rounded-xl" />
<div class="space-y-4">
<div v-for="i in 5" :key="i" class="flex gap-3">
<n-skeleton class="h-[50px] w-[50px] rounded-xl" />
<div class="h-[50px] w-[50px] skeleton-shimmer rounded-xl flex-shrink-0" />
<div class="flex flex-1 flex-col justify-center gap-2">
<n-skeleton text class="h-4 w-1/2" />
<n-skeleton text class="h-3 w-1/3" />
<div class="h-4 w-1/2 skeleton-shimmer rounded-lg" />
<div class="h-3 w-1/3 skeleton-shimmer rounded-lg" />
</div>
</div>
</div>
@@ -32,7 +34,7 @@
</div>
</div>
<div v-if="!isMobile" class="right">
<div class="title"><n-skeleton text class="h-8 w-32" /></div>
<div class="title"><div class="h-8 w-32 skeleton-shimmer rounded-lg" /></div>
<div class="rounded-2xl bg-light p-4 dark:bg-black">
<div class="space-y-2">
<div
@@ -40,11 +42,11 @@
:key="i"
class="flex items-center gap-4 rounded-2xl bg-light-100 p-2 dark:bg-dark-100"
>
<n-skeleton class="h-10 w-10 rounded-full" />
<n-skeleton class="h-10 w-10 rounded-xl" />
<div class="h-10 w-10 skeleton-shimmer rounded-full flex-shrink-0" />
<div class="h-10 w-10 skeleton-shimmer rounded-xl flex-shrink-0" />
<div class="flex flex-1 flex-col gap-2">
<n-skeleton text class="h-4 w-1/3" />
<n-skeleton text class="h-3 w-1/4" />
<div class="h-4 w-1/3 skeleton-shimmer rounded-lg" />
<div class="h-3 w-1/4 skeleton-shimmer rounded-lg" />
</div>
</div>
</div>