From 265eb15887917f5959895f8d09d47bc28293b149 Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 10 Jun 2026 18:03:36 +0800 Subject: [PATCH] feat(admin): add pagination to posts management page - list_posts API now accepts page/per_page params with LIMIT/OFFSET - Add /admin/posts/:page route with PostsPage component - Pagination component with prev/next navigation (20 posts/page) - Update dashboard to use new list_posts(1, 5) signature --- src/api/posts/list.rs | 22 +++++++++--- src/pages/admin/dashboard.rs | 2 +- src/pages/admin/mod.rs | 2 +- src/pages/admin/posts.rs | 66 ++++++++++++++++++++++++++++++++++-- src/router.rs | 4 ++- 5 files changed, 86 insertions(+), 10 deletions(-) diff --git a/src/api/posts/list.rs b/src/api/posts/list.rs index 9a98479..e4c9259 100644 --- a/src/api/posts/list.rs +++ b/src/api/posts/list.rs @@ -73,13 +73,27 @@ pub async fn list_published_posts( } #[server(ListPosts, "/api")] -pub async fn list_posts() -> Result { +pub async fn list_posts( + page: i32, + per_page: i32, +) -> Result { let _user = get_current_admin_user().await?; #[cfg(feature = "server")] { let client = get_conn().await.map_err(AppError::db_conn)?; + let count_row = client + .query_one( + "SELECT COUNT(*) FROM posts WHERE deleted_at IS NULL", + &[], + ) + .await + .map_err(AppError::query)?; + let total: i64 = count_row.get(0); + + let offset = ((page - 1).max(0) as i64) * (per_page as i64); + let limit = per_page as i64; let rows = client .query( "SELECT @@ -91,8 +105,9 @@ pub async fn list_posts() -> Result { LEFT JOIN tags t ON pt.tag_id = t.id WHERE p.deleted_at IS NULL GROUP BY p.id - ORDER BY p.created_at DESC", - &[], + ORDER BY p.created_at DESC + LIMIT $1 OFFSET $2", + &[&limit, &offset], ) .await .map_err(AppError::query)?; @@ -102,7 +117,6 @@ pub async fn list_posts() -> Result { posts.push(row_to_post_list(&client, row).await); } - let total = posts.len() as i64; Ok(PostListResponse { posts, total }) } diff --git a/src/pages/admin/dashboard.rs b/src/pages/admin/dashboard.rs index 3649807..6ee0ada 100644 --- a/src/pages/admin/dashboard.rs +++ b/src/pages/admin/dashboard.rs @@ -9,7 +9,7 @@ use crate::router::Route; #[component] pub fn Admin() -> Element { let stats_res = use_resource(get_post_stats); - let posts_res = use_resource(list_posts); + let posts_res = use_resource(|| list_posts(1, 5)); let show_stats_skeleton = use_delayed_loading(move || stats_res.read().is_none()); let show_posts_skeleton = use_delayed_loading(move || posts_res.read().is_none()); diff --git a/src/pages/admin/mod.rs b/src/pages/admin/mod.rs index 805a081..64c8ae3 100644 --- a/src/pages/admin/mod.rs +++ b/src/pages/admin/mod.rs @@ -3,5 +3,5 @@ pub mod posts; pub mod write; pub use dashboard::Admin; -pub use posts::Posts; +pub use posts::{Posts, PostsPage}; pub use write::{Write, WriteEdit}; diff --git a/src/pages/admin/posts.rs b/src/pages/admin/posts.rs index 577216b..68f88ed 100644 --- a/src/pages/admin/posts.rs +++ b/src/pages/admin/posts.rs @@ -6,13 +6,27 @@ use crate::hooks::delayed_loading::use_delayed_loading; use crate::models::post::Post; use crate::router::Route; +const POSTS_PER_PAGE: i32 = 20; + #[component] #[allow(unused_variables)] pub fn Posts() -> Element { - let mut posts_res = use_resource(list_posts); + rsx! { PostsPage { page: 1 } } +} + +#[component] +#[allow(unused_variables)] +pub fn PostsPage(page: i32) -> Element { + let current_page = page.max(1); + let mut posts_res = use_server_future(move || list_posts(current_page, POSTS_PER_PAGE))?; let mut deleting = use_signal(|| None::); let show_skeleton = use_delayed_loading(move || posts_res.read().is_none()); + let posts_data = posts_res.read().as_ref().map(|r| match r { + Ok(PostListResponse { posts, total }) => Ok((posts.clone(), *total)), + Err(e) => Err(e.to_string()), + }); + rsx! { div { class: "space-y-6", div { class: "flex items-center justify-between", @@ -26,8 +40,8 @@ pub fn Posts() -> Element { } } - match &*posts_res.read() { - Some(Ok(PostListResponse { posts, total: _ })) => { + match posts_data { + Some(Ok((posts, total))) => { if posts.is_empty() { rsx! { div { class: "text-center py-20 text-gray-500 dark:text-[#9b9c9d]", @@ -75,6 +89,7 @@ pub fn Posts() -> Element { } } } + Pagination { current_page, total } } } } @@ -102,6 +117,51 @@ pub fn Posts() -> Element { } } +#[component] +fn Pagination(current_page: i32, total: i64) -> Element { + let has_prev = current_page > 1; + let total_pages = ((total + POSTS_PER_PAGE as i64 - 1) / POSTS_PER_PAGE as i64).max(1) as i32; + let has_next = current_page < total_pages; + + rsx! { + nav { class: "flex mt-6 justify-between", + if has_prev { + Link { + class: "inline-flex items-center px-4 py-2 text-sm text-white bg-gray-900 dark:bg-[#dadadb] dark:text-gray-900 rounded-full hover:opacity-80 transition-opacity cursor-pointer", + to: if current_page - 1 <= 1 { + Route::Posts {} + } else { + Route::PostsPage { page: current_page - 1 } + }, + span { class: "mr-1", "«" } + "上一页" + } + } else { + span { class: "inline-flex items-center px-4 py-2 text-sm text-gray-400 bg-gray-100 dark:bg-[#2a2a2a] rounded-full cursor-not-allowed", + span { class: "mr-1", "«" } + "上一页" + } + } + span { class: "text-sm text-gray-500 dark:text-[#9b9c9d] self-center", + "{current_page} / {total_pages} 页 (共 {total} 篇)" + } + if has_next { + Link { + class: "inline-flex items-center px-4 py-2 text-sm text-white bg-gray-900 dark:bg-[#dadadb] dark:text-gray-900 rounded-full hover:opacity-80 transition-opacity cursor-pointer", + to: Route::PostsPage { page: current_page + 1 }, + "下一页" + span { class: "ml-1", "»" } + } + } else { + span { class: "inline-flex items-center px-4 py-2 text-sm text-gray-400 bg-gray-100 dark:bg-[#2a2a2a] rounded-full cursor-not-allowed", + "下一页" + span { class: "ml-1", "»" } + } + } + } + } +} + #[component] fn PostRow(post: Post, deleting: bool, on_delete: EventHandler) -> Element { let date_str = post.formatted_date(); diff --git a/src/router.rs b/src/router.rs index 5b47fa4..5e96e78 100644 --- a/src/router.rs +++ b/src/router.rs @@ -5,7 +5,7 @@ use crate::components::admin_layout::AdminLayout; use crate::components::frontend_layout::FrontendLayout; use crate::context::UserContext; use crate::pages::about::About; -use crate::pages::admin::{Admin, Posts, Write, WriteEdit}; +use crate::pages::admin::{Admin, Posts, PostsPage, Write, WriteEdit}; use crate::pages::archives::Archives; use crate::pages::home::{Home, HomePage}; use crate::pages::login::Login; @@ -50,6 +50,8 @@ pub enum Route { WriteEdit { id: i32 }, #[route("/posts")] Posts {}, + #[route("/posts/:page")] + PostsPage { page: i32 }, #[end_layout] #[end_nest]