From 59359b18ce82e7563908a689fcfac96cb8473221 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 2 Jul 2026 16:30:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor(hooks):=20=E6=96=B0=E5=A2=9E=20use=5Fp?= =?UTF-8?q?aginated=20=E5=B9=B6=E8=BF=81=E7=A7=BB=20posts/trash=20?= =?UTF-8?q?=E5=88=86=E9=A1=B5=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 Dioxus 0.7 custom hooks 范式,把后台分页列表页反复出现的 use_effect + spawn + (loading/items/total/error) 三件套收口到 use_paginated 组合式 hook。统一非 wasm cfg 处理(SSR 不发请求、 loading 立即置 false),调用方只关心 fetch 闭包与页码依赖。 迁移范围(按降级预案,只做最规整的两处): - pages/admin/posts.rs:顺手补 error signal(原先吞掉 Err),向 trash 看齐,消除一致性债。 - pages/admin/trash.rs:直接替换,error 已有。 保留手写的形态(强行套 hook 反而更复杂): - comments.rs:依赖 (page, status) 双 signal,status 是额外维度, 套不进 (page, per_page) 模型;改 hook 会为单一调用点变复杂。 - system.rs 三个 tab:load_once + use_future 轮询形态,与 hook 的 单次加载模型不契合。 - dashboard.rs:多源聚合(3 个并发 spawn 分别 set 不同 signal)。 list_posts / list_deleted_posts 是 server function,两端都生成 (wasm 端 client stub、server 端真实实现),故去掉原 cfg import。 --- src/hooks/mod.rs | 4 ++ src/hooks/query.rs | 99 ++++++++++++++++++++++++++++++++++++++++ src/pages/admin/posts.rs | 60 +++++++++--------------- src/pages/admin/trash.rs | 56 ++++++++--------------- 4 files changed, 144 insertions(+), 75 deletions(-) create mode 100644 src/hooks/query.rs diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index 8ef138f..b8c6d9a 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -3,6 +3,7 @@ //! 该模块集中管理可在组件树中复用的自定义 Hook,包括: //! - 骨架屏延迟加载状态 //! - 通用 DOM 事件监听(注册 + 自动卸载清理) +//! - 客户端分页数据加载 //! //! 评论草稿持久化(localStorage 读写)已迁至 `crate::utils::comment_storage`, //! 因为它是纯工具函数而非渲染期 Hook。 @@ -12,3 +13,6 @@ pub mod delayed_loading; /// 通用 DOM 事件监听 Hook(add/remove_event_listener 生命周期封装)。 pub mod event_listener; + +/// 客户端数据加载 Hook(use_paginated)。 +pub mod query; diff --git a/src/hooks/query.rs b/src/hooks/query.rs new file mode 100644 index 0000000..7e33c06 --- /dev/null +++ b/src/hooks/query.rs @@ -0,0 +1,99 @@ +//! 通用客户端数据加载 hook。 +//! +//! 封装 `use_effect + spawn + 多个 signal` 的三件套,统一 loading / data / error +//! 三态与非 wasm cfg 处理。后台 CSR 页面用这套;公共 SSR 页面仍用 +//! `use_server_future`(带 suspend 的真 SSR 预取),二者职责区分明确。 +//! +//! 当前只提供 `use_paginated`(分页列表)。system tab 的"load_once + use_future +//! 轮询"形态、dashboard 的多源聚合形态保持手写——强行套 hook 反而更复杂。 + +use dioxus::prelude::*; + +/// 分页列表加载状态。 +/// +/// 各字段是独立 `Signal`,调用方可按名读取、传给子组件,或对 `items` 做 +/// `with_mut` 乐观更新。 +pub struct PaginatedState { + /// 是否正在加载。 + pub loading: Signal, + /// 当前页数据。 + pub items: Signal>, + /// 总条数(用于分页器)。 + pub total: Signal, + /// 最近一次加载错误(成功时为 None)。 + pub error: Signal>, +} + +/// 分页列表加载 hook。 +/// +/// - `page`:返回当前页码的信号读取闭包(返回值变化时自动重新加载)。 +/// - `per_page`:每页条数。 +/// - `fetch`:接收 `(page, per_page)`,返回 `Future, i64), E>>`。 +/// `E: Display` 用于把错误写入 `error` signal。 +/// +/// 非 wasm32 下 `loading` 立即置 false(SSR 不发起请求)。 +/// +/// # 例 +/// +/// ```ignore +/// let state = use_paginated( +/// || current_page, +/// POSTS_PER_PAGE, +/// |p, pp| async move { +/// list_posts(p, pp).await +/// .map(|r| (r.posts, r.total)) +/// .map_err(|e| e.to_string()) +/// }, +/// ); +/// ``` +#[allow(unused_variables)] +pub fn use_paginated( + page: P, + per_page: i32, + fetch: F, +) -> PaginatedState +where + T: PartialEq + Clone + 'static, + P: Fn() -> i32 + Copy + 'static, + F: Fn(i32, i32) -> Fut + Copy + 'static, + Fut: std::future::Future, i64), E>> + 'static, + E: std::fmt::Display + 'static, +{ + let mut loading = use_signal(|| true); + let items = use_signal(Vec::new); + let total = use_signal(|| 0_i64); + let mut error: Signal> = use_signal(|| None); + + use_effect(move || { + let p = page(); + loading.set(true); + error.set(None); + #[cfg(target_arch = "wasm32")] + { + let f = fetch; + spawn(async move { + match f(p, per_page).await { + Ok((list, t)) => { + items.set(list); + total.set(t); + } + Err(e) => error.set(Some(e.to_string())), + } + loading.set(false); + }); + } + #[cfg(not(target_arch = "wasm32"))] + { + loading.set(false); + // 抚平非 wasm 下未使用的变量(p / per_page / fetch)。 + let _ = (p, per_page, fetch); + } + }); + + PaginatedState { + loading, + items, + total, + error, + } +} diff --git a/src/pages/admin/posts.rs b/src/pages/admin/posts.rs index 12ceb9a..3b104bf 100644 --- a/src/pages/admin/posts.rs +++ b/src/pages/admin/posts.rs @@ -6,11 +6,9 @@ use dioxus::prelude::*; use dioxus::router::components::Link; -// 仅在 WASM 前端使用的分页数据接口。 -#[cfg(target_arch = "wasm32")] -use crate::api::posts::list_posts; -#[cfg(target_arch = "wasm32")] -use crate::api::posts::PostListResponse; +// 分页数据接口:list_posts 是 server function,两端都生成(wasm 端为 client stub, +// server 端为真实实现),故无需 cfg。实际请求只在 use_paginated 的 wasm 分支发出。 +use crate::api::posts::{list_posts, PostListResponse}; use crate::api::posts::{delete_post, rebuild_content_html, CreatePostResponse, RebuildResult}; use crate::components::empty_state::{EmptyState, EmptyStateAction}; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; @@ -18,6 +16,7 @@ use crate::components::skeletons::posts_skeleton::PostsSkeleton; use crate::components::ui::{ Pagination, StatusBadge, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_TEXT_RED, }; +use crate::hooks::query::use_paginated; use crate::models::post::PostListItem; use crate::router::Route; @@ -38,10 +37,24 @@ pub fn Posts() -> Element { #[component] pub fn PostsPage(page: i32) -> Element { let current_page = page.max(1); - // 文章列表、总数、加载状态、删除中 ID、重建缓存状态与结果。 - let mut posts = use_signal(Vec::::new); - let mut total = use_signal(|| 0_i64); - let mut loading = use_signal(|| true); + + // 分页列表加载(loading / posts / total / error)由 use_paginated 统一管理; + // 原先吞掉 Err,现向 trash 看齐暴露 error signal(保持一致性)。 + let paginated = use_paginated( + move || current_page, + POSTS_PER_PAGE, + |p, pp| async move { + list_posts(p, pp) + .await + .map(|PostListResponse { posts, total }| (posts, total)) + }, + ); + let mut posts = paginated.items; + let mut total = paginated.total; + let loading = paginated.loading; + let _error = paginated.error; + + // 删除中 ID、重建缓存状态与结果仍由本组件持有(业务逻辑不归 hook 管)。 let mut deleting = use_signal(|| None::); // 重建缓存的状态由本组件持有并下发给 RebuildCacheBar:结果消息也在本组件 // 渲染(header 与表格之间的独立行),既不撑高 header 触发 items-center 重排, @@ -51,35 +64,6 @@ pub fn PostsPage(page: i32) -> Element { let rebuilding = use_signal(|| false); let rebuild_result = use_signal(|| Option::::None); - // 页码变化时加载分页数据:WASM 前端请求接口,SSR 直接结束加载。 - use_effect(move || { - let _ = current_page; - - loading.set(true); - // 仅在 WASM 前端发起分页请求。 - #[cfg(target_arch = "wasm32")] - { - let p = current_page; - spawn(async move { - match list_posts(p, POSTS_PER_PAGE).await { - Ok(PostListResponse { - posts: list, - total: t, - }) => { - posts.set(list); - total.set(t); - } - Err(_) => {} - } - loading.set(false); - }); - } - #[cfg(not(target_arch = "wasm32"))] - { - loading.set(false); - } - }); - let get_posts = move || -> Vec { posts() }; rsx! { diff --git a/src/pages/admin/trash.rs b/src/pages/admin/trash.rs index ae4a609..bc30e79 100644 --- a/src/pages/admin/trash.rs +++ b/src/pages/admin/trash.rs @@ -12,10 +12,9 @@ use dioxus::prelude::*; // 但部分仅用于 WASM 代码路径,SSR 下触发 unused imports,按项目惯例放行。 #[allow(unused_imports)] use crate::api::posts::{ - batch_purge_posts, batch_restore_posts, empty_trash, purge_post, restore_post, + batch_purge_posts, batch_restore_posts, empty_trash, list_deleted_posts, purge_post, + restore_post, PostListResponse, }; -#[cfg(target_arch = "wasm32")] -use crate::api::posts::{list_deleted_posts, PostListResponse}; #[allow(unused_imports)] use crate::api::settings::{get_trash_settings, update_trash_settings}; use crate::components::empty_state::EmptyState; @@ -25,6 +24,7 @@ use crate::components::ui::{ Pagination, StatusBadge, ADMIN_CARD_CLASS, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_SOLID_GREEN, BTN_SOLID_RED, BTN_TEXT_ACCENT, BTN_TEXT_RED, CHECKBOX_CLASS, }; +use crate::hooks::query::use_paginated; use crate::models::post::PostListItem; use crate::models::settings::TrashSettings; use crate::router::Route; @@ -48,44 +48,26 @@ pub fn Trash() -> Element { pub fn TrashPage(page: i32) -> Element { let current_page = page.max(1); let mut selected_ids: Signal> = use_signal(HashSet::new); - let mut posts: Signal> = use_signal(Vec::new); - let mut total: Signal = use_signal(|| 0); - #[allow(unused_mut)] - let mut loading: Signal = use_signal(|| false); - #[allow(unused_mut)] - let mut error: Signal> = use_signal(|| None); + + // 分页列表加载(loading / posts / total / error)由 use_paginated 统一管理。 + let paginated = use_paginated( + move || current_page, + TRASH_PER_PAGE, + |p, pp| async move { + list_deleted_posts(p, pp) + .await + .map(|PostListResponse { posts, total }| (posts, total)) + }, + ); + let mut posts = paginated.items; + let mut total = paginated.total; + let loading = paginated.loading; + let mut error = paginated.error; + // 自动清理配置:由子组件 AutoPurgeSettings 写入(加载/保存),本组件读取 // retention_days 供 TrashRow 的「剩余天数」展示。 let mut settings: Signal = use_signal(TrashSettings::default); - // 加载回收站列表(自动清理配置由子组件 AutoPurgeSettings 自行加载)。 - use_effect(move || { - let _ = current_page; - loading.set(true); - error.set(None); - #[cfg(target_arch = "wasm32")] - { - let page = current_page; - spawn(async move { - match list_deleted_posts(page, TRASH_PER_PAGE).await { - Ok(PostListResponse { - posts: list, - total: t, - }) => { - posts.set(list); - total.set(t); - } - Err(e) => error.set(Some(e.to_string())), - } - loading.set(false); - }); - } - #[cfg(not(target_arch = "wasm32"))] - { - loading.set(false); - } - }); - // 本地移除一篇文章(乐观更新)。 let mut remove_post = move |id: i32| { posts.with_mut(|list| list.retain(|p| p.id != id));