refactor(skeletons): 统一骨架屏延迟机制,200ms 内加载不显示

- DelayedSkeleton 从「延迟 pulse 动画」改为「延迟渲染」:
  前 200ms 完全不渲染,超时后才显示并带 pulse 动画
- admin_layout 从 use_delayed_loading hook 改为 DelayedSkeleton 包裹
- CommentListSkeleton 的两处裸用(section.rs / post_detail.rs)
  补上 DelayedSkeleton 包裹
- 移除不再使用的 use_delayed_loading hook 模块
This commit is contained in:
xfy 2026-07-08 14:18:08 +08:00
parent 5b9e65bf72
commit 61c64841f9
5 changed files with 35 additions and 29 deletions

View File

@ -8,9 +8,9 @@ use dioxus::router::components::Link;
use crate::api::auth::{get_current_user, logout};
use crate::components::admin_skeleton::AdminDashboardSkeleton;
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
use crate::components::write_skeleton::WriteSkeleton;
use crate::context::UserContext;
use crate::hooks::delayed_loading::use_delayed_loading;
use crate::router::Route;
use crate::theme::ThemeToggle;
@ -19,7 +19,7 @@ pub fn AdminLayout() -> Element {
let mut ctx: UserContext = use_context();
let navigator = dioxus::router::navigator();
let route = use_route::<Route>();
let show_skeleton = use_delayed_loading(move || !(ctx.checked)());
use_effect(move || {
if !(ctx.checked)() {
@ -145,11 +145,13 @@ pub fn AdminLayout() -> Element {
div { class: "flex-1 flex flex-col min-w-0 h-screen p-2 md:p-4",
div { class: "flex-1 bg-[var(--color-paper-theme)] rounded-[2rem] shadow-sm border border-[var(--color-paper-border)] overflow-hidden relative flex flex-col",
main { class: "{main_class}",
div { class: if show_skeleton() { "p-10" } else { "opacity-0" },
{
match route {
Route::Write {} => rsx! { WriteSkeleton {} },
_ => rsx! { AdminDashboardSkeleton {} },
DelayedSkeleton {
div { class: "p-10",
{
match route {
Route::Write {} => rsx! { WriteSkeleton {} },
_ => rsx! { AdminDashboardSkeleton {} },
}
}
}
}

View File

@ -9,6 +9,7 @@ use crate::api::comments::{check_pending_status, get_comments, CommentTreeRespon
use crate::components::comments::form::CommentForm;
use crate::components::comments::list::CommentList;
use crate::components::skeletons::comment_skeleton::CommentListSkeleton;
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
use crate::utils::comment_storage::{self, PendingComment};
/// 评论上下文,供评论相关组件共享状态。
@ -118,7 +119,7 @@ pub fn CommentSection(post_id: i32) -> Element {
}
}
None => rsx! {
CommentListSkeleton {}
DelayedSkeleton { CommentListSkeleton {} }
},
}
}

View File

@ -1,36 +1,39 @@
//! 延迟骨架屏包装组件
//!
//! 让骨架屏先以静态灰色块立即显示,延迟一段时间后再启动 pulse 动画,
//! 避免快速加载时动画一闪而过带来的视觉抖动
//! 200ms 内完全不渲染骨架屏,避免快速加载时骨架屏一闪而过;
//! 超过 200ms 后渲染骨架屏并带有 pulse 动画,提示正在加载
use crate::utils::time::sleep_ms;
use dioxus::prelude::*;
/// 骨架屏 pulse 动画延迟(毫秒)。
/// 骨架屏显示延迟(毫秒)。
///
/// 加载时间低于此值时骨架屏只显示静态灰色块,避免 pulse 动画一闪而过
const SKELETON_PULSE_DELAY_MS: u32 = 200;
/// 加载时间低于此值时完全不显示骨架屏,避免闪烁
const SKELETON_DELAY_MS: u32 = 200;
/// 延迟 pulse 动画的骨架屏包装组件。
/// 延迟渲染的骨架屏包装组件。
///
/// 骨架屏区域**立即显示**(灰色静态占位块),避免空白闪烁。
/// 延迟 `SKELETON_PULSE_DELAY_MS` 毫秒后,如果仍在加载,则启动 pulse 动画。
/// 前 `SKELETON_DELAY_MS` 毫秒内不渲染任何内容;
/// 超过延迟后渲染子元素并启动 pulse 动画。
///
/// 快加载(< 200ms用户只看到静态灰色块一闪而过,无动画感知
/// 慢加载:灰色块正常 pulse提示正在加载。
/// 快加载(< 200ms组件在渲染前就被卸载,用户完全看不到骨架屏
/// 慢加载:骨架屏正常显示并 pulse提示正在加载。
#[component]
pub fn DelayedSkeleton(children: Element) -> Element {
let mut pulsing = use_signal(|| false);
let mut visible = use_signal(|| false);
// 延迟启动 pulse 动画
use_effect(move || {
spawn(async move {
sleep_ms(SKELETON_PULSE_DELAY_MS).await;
pulsing.set(true);
sleep_ms(SKELETON_DELAY_MS).await;
visible.set(true);
});
});
rsx! {
div { class: if pulsing() { "animate-pulse" } else { "" }, {children} }
if visible() {
rsx! {
div { class: "animate-pulse", {children} }
}
} else {
rsx! {}
}
}

View File

@ -1,15 +1,13 @@
//! 共享的 Dioxus Hooks 模块。
//!
//! 该模块集中管理可在组件树中复用的自定义 Hook包括
//! - 骨架屏延迟加载状态
//! - 通用 DOM 事件监听(注册 + 自动卸载清理)
//! - 客户端分页数据加载
//!
//! 评论草稿持久化localStorage 读写)已迁至 `crate::utils::comment_storage`
//! 因为它是纯工具函数而非渲染期 Hook。
/// 延迟加载状态 Hook。
pub mod delayed_loading;
//!
//! 骨架屏延迟渲染已统一至 `crate::components::skeletons::delayed_skeleton::DelayedSkeleton` 组件。
/// 通用 DOM 事件监听 Hookadd/remove_event_listener 生命周期封装)。
pub mod event_listener;

View File

@ -63,7 +63,9 @@ pub fn PostDetail(slug: String) -> Element {
div { class: "mt-12 border-t border-gray-200 dark:border-gray-700 pt-8",
SuspenseBoundary {
fallback: move |_| rsx! {
crate::components::skeletons::comment_skeleton::CommentListSkeleton {}
DelayedSkeleton {
crate::components::skeletons::comment_skeleton::CommentListSkeleton {}
}
},
crate::components::comments::section::CommentSection { post_id: post.id }
}