fix: delay skeleton pulse animation instead of hiding skeleton, prevent blank flash

This commit is contained in:
xfy 2026-06-03 17:47:51 +08:00
parent 754c1f5b86
commit 326108ab68
7 changed files with 20 additions and 22 deletions

View File

@ -6,7 +6,7 @@ use dioxus::prelude::*;
#[component]
pub fn ArchiveSkeleton() -> Element {
rsx! {
div { class: "animate-pulse",
div {
// 统计行占位
div { class: "mt-2 mb-6",
div { class: "h-5 w-32 bg-gray-200 dark:bg-[#2a2a2a] rounded" }

View File

@ -1,8 +1,8 @@
use dioxus::prelude::*;
/// 骨架屏显示前的最小延迟(毫秒)
/// 加载时间低于此值时骨架屏不会显示,避免闪烁
const SKELETON_DELAY_MS: u32 = 200;
/// 骨架屏 pulse 动画延迟(毫秒)
/// 加载时间低于此值时骨架屏只显示静态灰色块,避免 pulse 动画一闪而过
const SKELETON_PULSE_DELAY_MS: u32 = 200;
#[cfg(target_arch = "wasm32")]
async fn sleep_ms(ms: u32) {
@ -20,29 +20,27 @@ async fn sleep_ms(ms: u32) {
tokio::time::sleep(std::time::Duration::from_millis(ms as u64)).await;
}
/// 延迟显示的骨架屏包装组件
/// 延迟 pulse 动画的骨架屏包装组件
///
/// 骨架屏区域始终占位但初始时不可见opacity-0
/// 延迟 SKELETON_DELAY_MS 毫秒后,如果仍在加载,则淡入显示。
/// 如果加载很快(< 200ms用户完全看不到骨架屏。
/// 骨架屏区域**立即显示**(灰色静态占位块),避免空白闪烁。
/// 延迟 SKELETON_PULSE_DELAY_MS 毫秒后,如果仍在加载,则启动 pulse 动画。
///
/// 快加载(< 200ms用户只看到静态灰色块一闪而过无动画感知
/// 慢加载:灰色块正常 pulse提示正在加载
#[component]
pub fn DelayedSkeleton(children: Element) -> Element {
let mut visible = use_signal(|| false);
let mut pulsing = use_signal(|| false);
use_effect(move || {
spawn(async move {
sleep_ms(SKELETON_DELAY_MS).await;
visible.set(true);
sleep_ms(SKELETON_PULSE_DELAY_MS).await;
pulsing.set(true);
});
});
rsx! {
div {
class: if visible() {
"opacity-100 transition-opacity duration-150"
} else {
"opacity-0"
},
class: if pulsing() { "animate-pulse" } else { "" },
{children}
}
}

View File

@ -7,7 +7,7 @@ use crate::components::skeletons::post_card_skeleton::PostCardSkeleton;
#[component]
pub fn HomeSkeleton() -> Element {
rsx! {
div { class: "animate-pulse",
div {
// 5 个文章卡片骨架
for _ in 0..5 {
PostCardSkeleton {}

View File

@ -6,7 +6,7 @@ use dioxus::prelude::*;
pub fn PostCardSkeleton() -> Element {
rsx! {
article {
class: "mb-6 p-6 bg-white dark:bg-[#2e2e33] rounded-lg border border-gray-200 dark:border-[#333] animate-pulse",
class: "mb-6 p-6 bg-white dark:bg-[#2e2e33] rounded-lg border border-gray-200 dark:border-[#333]",
// 标题占位 (模拟 h2 text-2xl font-bold)
div { class: "h-7 w-3/4 bg-gray-200 dark:bg-[#2a2a2a] rounded mb-3" }
// 摘要第一行

View File

@ -5,7 +5,7 @@ use dioxus::prelude::*;
#[component]
pub fn PostDetailSkeleton() -> Element {
rsx! {
article { class: "post-single animate-pulse",
article { class: "post-single",
// 面包屑占位
div { class: "h-4 w-48 bg-gray-200 dark:bg-[#2a2a2a] rounded mb-6" }

View File

@ -7,7 +7,7 @@ use crate::components::skeletons::post_card_skeleton::PostCardSkeleton;
#[component]
pub fn SearchSkeleton() -> Element {
rsx! {
div { class: "space-y-6 py-4 animate-pulse",
div { class: "space-y-6 py-4",
// 3 个结果卡片骨架
for _ in 0..3 {
PostCardSkeleton {}

View File

@ -7,7 +7,7 @@ use crate::components::skeletons::post_card_skeleton::PostCardSkeleton;
#[component]
pub fn TagsSkeleton() -> Element {
rsx! {
div { class: "animate-pulse",
div {
// 统计行占位
div { class: "mt-2 mb-6",
div { class: "h-5 w-48 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
@ -39,7 +39,7 @@ pub fn TagsSkeleton() -> Element {
#[component]
pub fn TagDetailSkeleton() -> Element {
rsx! {
div { class: "animate-pulse",
div {
// 统计行占位
div { class: "mt-2 mb-6",
div { class: "h-5 w-32 bg-gray-200 dark:bg-[#2a2a2a] rounded" }