Compare commits
2 Commits
373d4f09ed
...
9cf6a7e4e6
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cf6a7e4e6 | |||
| 265eb15887 |
@ -73,13 +73,27 @@ pub async fn list_published_posts(
|
||||
}
|
||||
|
||||
#[server(ListPosts, "/api")]
|
||||
pub async fn list_posts() -> Result<PostListResponse, ServerFnError> {
|
||||
pub async fn list_posts(
|
||||
page: i32,
|
||||
per_page: i32,
|
||||
) -> Result<PostListResponse, ServerFnError> {
|
||||
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<PostListResponse, ServerFnError> {
|
||||
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<PostListResponse, ServerFnError> {
|
||||
posts.push(row_to_post_list(&client, row).await);
|
||||
}
|
||||
|
||||
let total = posts.len() as i64;
|
||||
Ok(PostListResponse { posts, total })
|
||||
}
|
||||
|
||||
|
||||
@ -4,5 +4,6 @@ pub mod delayed_skeleton;
|
||||
pub mod home_skeleton;
|
||||
pub mod post_card_skeleton;
|
||||
pub mod post_detail_skeleton;
|
||||
pub mod posts_skeleton;
|
||||
pub mod search_skeleton;
|
||||
pub mod tags_skeleton;
|
||||
|
||||
31
src/components/skeletons/posts_skeleton.rs
Normal file
31
src/components/skeletons/posts_skeleton.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::skeletons::atoms::SkeletonBox;
|
||||
|
||||
#[component]
|
||||
pub fn PostsSkeleton() -> Element {
|
||||
rsx! {
|
||||
div { class: "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] overflow-hidden",
|
||||
table { class: "w-full text-sm",
|
||||
thead {
|
||||
tr { class: "border-b border-gray-200 dark:border-[#333]",
|
||||
th { class: "px-4 py-3", SkeletonBox { class: "h-3 w-10" } }
|
||||
th { class: "px-4 py-3 w-24", SkeletonBox { class: "h-3 w-10 mx-auto" } }
|
||||
th { class: "px-4 py-3 w-32", SkeletonBox { class: "h-3 w-10" } }
|
||||
th { class: "px-4 py-3 w-24", SkeletonBox { class: "h-3 w-10 ml-auto" } }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
for _ in 0..10 {
|
||||
tr { class: "border-b border-gray-100 dark:border-[#333] last:border-0",
|
||||
td { class: "px-4 py-3", SkeletonBox { class: "h-4 w-1/3" } }
|
||||
td { class: "px-4 py-3", SkeletonBox { class: "h-5 w-14 mx-auto rounded" } }
|
||||
td { class: "px-4 py-3", SkeletonBox { class: "h-4 w-20" } }
|
||||
td { class: "px-4 py-3", SkeletonBox { class: "h-4 w-12 ml-auto" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -2,16 +2,30 @@ use dioxus::prelude::*;
|
||||
use dioxus::router::components::Link;
|
||||
|
||||
use crate::api::posts::{delete_post, list_posts, CreatePostResponse, PostListResponse};
|
||||
use crate::hooks::delayed_loading::use_delayed_loading;
|
||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||
use crate::components::skeletons::posts_skeleton::PostsSkeleton;
|
||||
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::<i32>);
|
||||
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",
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -87,14 +102,7 @@ pub fn Posts() -> Element {
|
||||
}
|
||||
None => {
|
||||
rsx! {
|
||||
div { class: if show_skeleton() { "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] animate-pulse" } else { "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] opacity-0" },
|
||||
for _ in 0..5 {
|
||||
div { class: "flex items-center px-4 py-3 border-b border-gray-100 dark:border-[#333] last:border-0",
|
||||
div { class: "h-4 w-1/3 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
|
||||
div { class: "ml-auto h-4 w-16 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
|
||||
}
|
||||
}
|
||||
}
|
||||
DelayedSkeleton { PostsSkeleton {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -102,6 +110,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<i32>) -> Element {
|
||||
let date_str = post.formatted_date();
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user