refactor(hooks): 新增 use_paginated 并迁移 posts/trash 分页加载
按 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。
This commit is contained in:
parent
759bbab7ee
commit
59359b18ce
@ -3,6 +3,7 @@
|
|||||||
//! 该模块集中管理可在组件树中复用的自定义 Hook,包括:
|
//! 该模块集中管理可在组件树中复用的自定义 Hook,包括:
|
||||||
//! - 骨架屏延迟加载状态
|
//! - 骨架屏延迟加载状态
|
||||||
//! - 通用 DOM 事件监听(注册 + 自动卸载清理)
|
//! - 通用 DOM 事件监听(注册 + 自动卸载清理)
|
||||||
|
//! - 客户端分页数据加载
|
||||||
//!
|
//!
|
||||||
//! 评论草稿持久化(localStorage 读写)已迁至 `crate::utils::comment_storage`,
|
//! 评论草稿持久化(localStorage 读写)已迁至 `crate::utils::comment_storage`,
|
||||||
//! 因为它是纯工具函数而非渲染期 Hook。
|
//! 因为它是纯工具函数而非渲染期 Hook。
|
||||||
@ -12,3 +13,6 @@ pub mod delayed_loading;
|
|||||||
|
|
||||||
/// 通用 DOM 事件监听 Hook(add/remove_event_listener 生命周期封装)。
|
/// 通用 DOM 事件监听 Hook(add/remove_event_listener 生命周期封装)。
|
||||||
pub mod event_listener;
|
pub mod event_listener;
|
||||||
|
|
||||||
|
/// 客户端数据加载 Hook(use_paginated)。
|
||||||
|
pub mod query;
|
||||||
|
|||||||
99
src/hooks/query.rs
Normal file
99
src/hooks/query.rs
Normal file
@ -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<T> {
|
||||||
|
/// 是否正在加载。
|
||||||
|
pub loading: Signal<bool>,
|
||||||
|
/// 当前页数据。
|
||||||
|
pub items: Signal<Vec<T>>,
|
||||||
|
/// 总条数(用于分页器)。
|
||||||
|
pub total: Signal<i64>,
|
||||||
|
/// 最近一次加载错误(成功时为 None)。
|
||||||
|
pub error: Signal<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 分页列表加载 hook。
|
||||||
|
///
|
||||||
|
/// - `page`:返回当前页码的信号读取闭包(返回值变化时自动重新加载)。
|
||||||
|
/// - `per_page`:每页条数。
|
||||||
|
/// - `fetch`:接收 `(page, per_page)`,返回 `Future<Output = Result<(Vec<T>, 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<T, P, F, Fut, E>(
|
||||||
|
page: P,
|
||||||
|
per_page: i32,
|
||||||
|
fetch: F,
|
||||||
|
) -> PaginatedState<T>
|
||||||
|
where
|
||||||
|
T: PartialEq + Clone + 'static,
|
||||||
|
P: Fn() -> i32 + Copy + 'static,
|
||||||
|
F: Fn(i32, i32) -> Fut + Copy + 'static,
|
||||||
|
Fut: std::future::Future<Output = Result<(Vec<T>, 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<Option<String>> = 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,11 +6,9 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus::router::components::Link;
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
// 仅在 WASM 前端使用的分页数据接口。
|
// 分页数据接口:list_posts 是 server function,两端都生成(wasm 端为 client stub,
|
||||||
#[cfg(target_arch = "wasm32")]
|
// server 端为真实实现),故无需 cfg。实际请求只在 use_paginated 的 wasm 分支发出。
|
||||||
use crate::api::posts::list_posts;
|
use crate::api::posts::{list_posts, PostListResponse};
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
use crate::api::posts::PostListResponse;
|
|
||||||
use crate::api::posts::{delete_post, rebuild_content_html, CreatePostResponse, RebuildResult};
|
use crate::api::posts::{delete_post, rebuild_content_html, CreatePostResponse, RebuildResult};
|
||||||
use crate::components::empty_state::{EmptyState, EmptyStateAction};
|
use crate::components::empty_state::{EmptyState, EmptyStateAction};
|
||||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||||
@ -18,6 +16,7 @@ use crate::components::skeletons::posts_skeleton::PostsSkeleton;
|
|||||||
use crate::components::ui::{
|
use crate::components::ui::{
|
||||||
Pagination, StatusBadge, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_TEXT_RED,
|
Pagination, StatusBadge, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_TEXT_RED,
|
||||||
};
|
};
|
||||||
|
use crate::hooks::query::use_paginated;
|
||||||
use crate::models::post::PostListItem;
|
use crate::models::post::PostListItem;
|
||||||
use crate::router::Route;
|
use crate::router::Route;
|
||||||
|
|
||||||
@ -38,10 +37,24 @@ pub fn Posts() -> Element {
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn PostsPage(page: i32) -> Element {
|
pub fn PostsPage(page: i32) -> Element {
|
||||||
let current_page = page.max(1);
|
let current_page = page.max(1);
|
||||||
// 文章列表、总数、加载状态、删除中 ID、重建缓存状态与结果。
|
|
||||||
let mut posts = use_signal(Vec::<PostListItem>::new);
|
// 分页列表加载(loading / posts / total / error)由 use_paginated 统一管理;
|
||||||
let mut total = use_signal(|| 0_i64);
|
// 原先吞掉 Err,现向 trash 看齐暴露 error signal(保持一致性)。
|
||||||
let mut loading = use_signal(|| true);
|
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::<i32>);
|
let mut deleting = use_signal(|| None::<i32>);
|
||||||
// 重建缓存的状态由本组件持有并下发给 RebuildCacheBar:结果消息也在本组件
|
// 重建缓存的状态由本组件持有并下发给 RebuildCacheBar:结果消息也在本组件
|
||||||
// 渲染(header 与表格之间的独立行),既不撑高 header 触发 items-center 重排,
|
// 渲染(header 与表格之间的独立行),既不撑高 header 触发 items-center 重排,
|
||||||
@ -51,35 +64,6 @@ pub fn PostsPage(page: i32) -> Element {
|
|||||||
let rebuilding = use_signal(|| false);
|
let rebuilding = use_signal(|| false);
|
||||||
let rebuild_result = use_signal(|| Option::<String>::None);
|
let rebuild_result = use_signal(|| Option::<String>::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<PostListItem> { posts() };
|
let get_posts = move || -> Vec<PostListItem> { posts() };
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
|
|||||||
@ -12,10 +12,9 @@ use dioxus::prelude::*;
|
|||||||
// 但部分仅用于 WASM 代码路径,SSR 下触发 unused imports,按项目惯例放行。
|
// 但部分仅用于 WASM 代码路径,SSR 下触发 unused imports,按项目惯例放行。
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use crate::api::posts::{
|
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)]
|
#[allow(unused_imports)]
|
||||||
use crate::api::settings::{get_trash_settings, update_trash_settings};
|
use crate::api::settings::{get_trash_settings, update_trash_settings};
|
||||||
use crate::components::empty_state::EmptyState;
|
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,
|
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,
|
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::post::PostListItem;
|
||||||
use crate::models::settings::TrashSettings;
|
use crate::models::settings::TrashSettings;
|
||||||
use crate::router::Route;
|
use crate::router::Route;
|
||||||
@ -48,44 +48,26 @@ pub fn Trash() -> Element {
|
|||||||
pub fn TrashPage(page: i32) -> Element {
|
pub fn TrashPage(page: i32) -> Element {
|
||||||
let current_page = page.max(1);
|
let current_page = page.max(1);
|
||||||
let mut selected_ids: Signal<HashSet<i32>> = use_signal(HashSet::new);
|
let mut selected_ids: Signal<HashSet<i32>> = use_signal(HashSet::new);
|
||||||
let mut posts: Signal<Vec<PostListItem>> = use_signal(Vec::new);
|
|
||||||
let mut total: Signal<i64> = use_signal(|| 0);
|
// 分页列表加载(loading / posts / total / error)由 use_paginated 统一管理。
|
||||||
#[allow(unused_mut)]
|
let paginated = use_paginated(
|
||||||
let mut loading: Signal<bool> = use_signal(|| false);
|
move || current_page,
|
||||||
#[allow(unused_mut)]
|
TRASH_PER_PAGE,
|
||||||
let mut error: Signal<Option<String>> = use_signal(|| None);
|
|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 写入(加载/保存),本组件读取
|
// 自动清理配置:由子组件 AutoPurgeSettings 写入(加载/保存),本组件读取
|
||||||
// retention_days 供 TrashRow 的「剩余天数」展示。
|
// retention_days 供 TrashRow 的「剩余天数」展示。
|
||||||
let mut settings: Signal<TrashSettings> = use_signal(TrashSettings::default);
|
let mut settings: Signal<TrashSettings> = 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| {
|
let mut remove_post = move |id: i32| {
|
||||||
posts.with_mut(|list| list.retain(|p| p.id != id));
|
posts.with_mut(|list| list.retain(|p| p.id != id));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user