Skip to main content

yggdrasil/components/skeletons/
delayed_skeleton.rs

1//! 延迟骨架屏包装组件
2//!
3//! 200ms 内完全不渲染骨架屏,避免快速加载时骨架屏一闪而过;
4//! 超过 200ms 后渲染骨架屏;占位块自带扫光(`.skeleton`),提示正在加载。
5//! 导航挂起(`is-route-pending`)期间挂载时立即显示,接上路由骨架。
6
7use leptos::prelude::*;
8use std::time::Duration;
9
10/// 骨架屏显示延迟(毫秒)。
11///
12/// 加载时间低于此值时完全不显示骨架屏,避免闪烁。
13const SKELETON_DELAY_MS: u64 = 200;
14
15/// 延迟渲染的骨架屏包装组件。
16///
17/// 前 `SKELETON_DELAY_MS` 毫秒内不渲染任何内容;
18/// 超过延迟后渲染子元素。计时只在浏览器端(`Effect` 内)启动,
19/// SSR 与 hydration 首帧都输出空内容,DOM 一致。
20///
21/// 快加载(< 200ms):组件在渲染前就被卸载,用户完全看不到骨架屏。
22/// 慢加载:骨架屏正常显示,占位块自带扫光。
23/// 导航挂起期间挂载(分包迟到、用户已看过路由骨架):立即显示,不再留 200ms 空白。
24/// `pulse = true` 额外给整体加呼吸动画,仅用于没有使用 `SkeletonBox` 的旧骨架。
25#[component]
26pub fn DelayedSkeleton(children: ChildrenFn, #[prop(optional)] pulse: bool) -> impl IntoView {
27    let visible = RwSignal::new(false);
28
29    Effect::new(move |_| {
30        // 分包迟到的导航刚提交:用户已等过 300ms 并看着路由骨架,直接显示,不再留 200ms 空白。
31        if crate::bridges::navigation::route_pending() {
32            visible.set(true);
33            return;
34        }
35        set_timeout(
36            move || {
37                // 计时到期前组件可能已卸载(信号已释放),用 try_set 静默跳过。
38                visible.try_set(true);
39            },
40            Duration::from_millis(SKELETON_DELAY_MS),
41        );
42    });
43
44    view! {
45        <Show when=move || visible.get()>
46            <div class=if pulse { "animate-pulse" } else { "" }>{children()}</div>
47        </Show>
48    }
49}