From d72495c00262e2c5a67f24aa1a0b54e04bbe694a Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 8 Jul 2026 16:52:02 +0800 Subject: [PATCH] refactor(admin): merge trash into /admin/posts as a URL-driven tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回收站从独立路由 /admin/trash 合并为 /admin/posts 下的一个 tab, 消除「回收站」导航项,让文章管理(列表 + 回收站 + 自动清理配置)集中在 同一页面,符合「相关功能就近聚合」的组织原则。 路由(router.rs) - 删除 Route::Trash / Route::TrashPage - 新增 Route::PostsTrash (/posts/trash) 与 PostsTrashPage (/posts/trash/:page) - 静态段声明在 :page 之前::page 为 i32 类型,天然排斥 'trash' 文本, 路由匹配安全且遵循文件内 static-first 约定 导航(admin_layout.rs) - 移除「回收站」导航项 - 「管理文章」高亮覆盖其下所有子路由(列表/分页/回收站 tab), 顺带修掉原 PostsPage 分页不高亮的小 bug 页面(posts.rs / posts_trash.rs) - 新增 PostsTabs:URL 驱动的 tab 栏(Link 切换路由,非本地 signal), 回收站 tab 带 get_post_stats().trash 数量角标,>0 用主题强调色提醒 - trash.rs → posts_trash.rs:TrashPage 重命名为 PostsTrashPage, 分页路由改指向 PostsTrash/PostsTrashPage,内容(列表/批量操作/ 清空回收站/AutoPurgeSettings)原样迁移;include_str 测试同步更新 - posts.rs 的 PostsPage 渲染 PostsTabs,列表逻辑不变 无后端契约变更:所有 server function(list/delete/restore/purge/ batch/empty_trash/get_trash_settings)签名不变,Pagination 组件 接收具体 Route 变体,给 trash tab 传 PostsTrash 即可,无需改组件。 --- src/components/admin_layout.rs | 13 +++- src/pages/admin/mod.rs | 8 +-- src/pages/admin/posts.rs | 70 +++++++++++++++++++- src/pages/admin/{trash.rs => posts_trash.rs} | 24 ++++--- src/router.rs | 16 ++--- 5 files changed, 108 insertions(+), 23 deletions(-) rename src/pages/admin/{trash.rs => posts_trash.rs} (97%) diff --git a/src/components/admin_layout.rs b/src/components/admin_layout.rs index 75d05bd..4236e1f 100644 --- a/src/components/admin_layout.rs +++ b/src/components/admin_layout.rs @@ -45,13 +45,20 @@ pub fn AdminLayout() -> Element { (Route::Admin {}, "仪表盘"), (Route::Write {}, "写文章"), (Route::Posts {}, "管理文章"), - (Route::Trash {}, "回收站"), (Route::Runner {}, "试运行"), (Route::System {}, "系统"), ]; let is_write_route = matches!(route, Route::Write {}) || matches!(route, Route::WriteEdit { .. }); + // 「管理文章」高亮覆盖其下所有子路由:列表、分页、回收站 tab。 + let is_posts_route = matches!( + route, + Route::Posts {} + | Route::PostsPage { .. } + | Route::PostsTrash {} + | Route::PostsTrashPage { .. } + ); let main_class = if is_write_route { "flex-1 flex flex-col relative w-full h-full overflow-y-auto px-10 py-12 rounded-[2rem] bg-[var(--color-paper-theme)] shadow-sm border border-[var(--color-paper-border)]" @@ -75,7 +82,9 @@ pub fn AdminLayout() -> Element { nav { class: "flex-1 flex flex-col gap-2", for (dest, label) in admin_nav_items { { - let is_active = route == dest || (label == "写文章" && is_write_route) || (label == "回收站" && matches!(route, Route::TrashPage { .. })); + let is_active = route == dest + || (label == "写文章" && is_write_route) + || (label == "管理文章" && is_posts_route); let base_class = "flex items-center px-4 py-3 rounded-2xl text-sm font-medium transition-all"; let text_class = if is_active { "bg-[var(--color-paper-theme)] text-[var(--color-paper-primary)] shadow-sm border border-[var(--color-paper-border)]" diff --git a/src/pages/admin/mod.rs b/src/pages/admin/mod.rs index 7ba9d34..ecf067e 100644 --- a/src/pages/admin/mod.rs +++ b/src/pages/admin/mod.rs @@ -8,12 +8,12 @@ pub mod comments; pub mod dashboard; /// 文章管理列表页面模块。 pub mod posts; +/// 回收站页面模块(文章管理下的 tab)。 +pub mod posts_trash; /// 代码试运行沙箱页面模块。 pub mod runner; /// 系统管理页面模块(数据库 + 服务器状态 + SQL 控制台 + 导出 + 备份)。 pub mod system; -/// 回收站管理页面模块。 -pub mod trash; /// 文章编辑器页面模块(基于 Tiptap 富文本编辑器)。 pub mod write; @@ -23,11 +23,11 @@ pub use comments::{AdminComments, AdminCommentsPage}; pub use dashboard::Admin; /// 文章管理列表组件(带默认分页)。 pub use posts::{Posts, PostsPage}; +/// 回收站管理组件(带默认分页,文章管理下的 tab)。 +pub use posts_trash::{PostsTrash, PostsTrashPage}; /// 代码试运行沙箱组件。 pub use runner::Runner; /// 系统管理入口组件。 pub use system::System; -/// 回收站管理组件(带默认分页)。 -pub use trash::{Trash, TrashPage}; /// 文章编辑器组件(新建与编辑模式)。 pub use write::{Write, WriteEdit}; diff --git a/src/pages/admin/posts.rs b/src/pages/admin/posts.rs index 1fd3993..f64fc2a 100644 --- a/src/pages/admin/posts.rs +++ b/src/pages/admin/posts.rs @@ -9,8 +9,12 @@ use dioxus::router::components::Link; // 分页数据接口:list_posts 是 server function,两端都生成(wasm 端为 client stub, // server 端为真实实现),故无需 cfg。实际请求只在 use_paginated 的 wasm 分支发出。 use crate::api::posts::{list_posts, PostListResponse}; +// get_post_stats / PostStatsResponse 仅在 PostsTabs 的 wasm 加载路径使用, +// SSR 下对应 use_effect 分支被裁剪,故允许 unused imports。 +#[allow(unused_imports)] use crate::api::posts::{ - delete_post, rebuild_content_html, rebuild_post_content_html, CreatePostResponse, RebuildResult, + delete_post, get_post_stats, rebuild_content_html, rebuild_post_content_html, + CreatePostResponse, PostStatsResponse, RebuildResult, }; use crate::components::empty_state::{EmptyState, EmptyStateAction}; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; @@ -95,6 +99,9 @@ pub fn PostsPage(page: i32) -> Element { } } + // tab 栏:文章 / 回收站。URL 驱动(Link 切换路由),回收站带数量角标。 + PostsTabs {} + // 重建结果消息:独立成行,进入文档流,吃 space-y-6 的正常间距。 // 既不撑高 header(不在 header 内)触发 items-center 重排,也不脱离流 // 溢进表格(曾用 absolute top-full mt-2,因 28px > 24px 间距溢出 4px)。 @@ -364,3 +371,64 @@ fn PostRow( } } } + +/// 文章管理 tab 栏:「全部文章」与「回收站」。 +/// +/// tab 状态由 URL(当前路由)驱动而非本地 signal:点击即跳转路由,刷新/深链均可直达 +/// 对应 tab。回收站 tab 带 `get_post_stats().stats.trash` 数量角标,便于发现待清理文章。 +/// 本组件在 `PostsPage`(全部文章)与 `PostsTrashPage`(回收站)共用,故设为 pub。 +#[component] +#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))] +pub fn PostsTabs() -> Element { + let route = use_route::(); + // 当前 tab:true=回收站(PostsTrash/PostsTrashPage),false=全部文章(Posts/PostsPage)。 + let is_trash = matches!( + route, + Route::PostsTrash {} | Route::PostsTrashPage { .. } + ); + // 回收站数量:仅 WASM 异步拉取,供角标展示。 + let mut trash_count = use_signal(|| Option::::None); + + use_effect(move || { + #[cfg(target_arch = "wasm32")] + spawn(async move { + if let Ok(PostStatsResponse { stats }) = get_post_stats().await { + trash_count.set(Some(stats.trash)); + } + }); + }); + + rsx! { + div { class: "flex gap-4 border-b border-paper-border", + Link { + to: Route::Posts {}, + class: if !is_trash { + "px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-primary transition-colors cursor-pointer border-b-2 border-paper-primary -mb-px" + } else { + "px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-secondary hover:text-paper-primary transition-colors cursor-pointer border-b-2 border-transparent -mb-px" + }, + "全部文章" + } + Link { + to: Route::PostsTrash {}, + class: if is_trash { + "inline-flex items-center gap-1.5 px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-primary transition-colors cursor-pointer border-b-2 border-paper-primary -mb-px" + } else { + "inline-flex items-center gap-1.5 px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-secondary hover:text-paper-primary transition-colors cursor-pointer border-b-2 border-transparent -mb-px" + }, + "回收站" + // 数量角标:有数据才显示。0 显示中性灰,>0 用主题强调色提醒。 + if let Some(count) = trash_count() { + span { + class: if count > 0 { + "inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5 rounded-full text-[0.625rem] font-semibold normal-case tracking-normal bg-paper-accent-soft text-paper-accent" + } else { + "inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5 rounded-full text-[0.625rem] font-semibold normal-case tracking-normal bg-paper-tertiary text-paper-secondary" + }, + "{count}" + } + } + } + } + } +} diff --git a/src/pages/admin/trash.rs b/src/pages/admin/posts_trash.rs similarity index 97% rename from src/pages/admin/trash.rs rename to src/pages/admin/posts_trash.rs index 559a38b..416d12b 100644 --- a/src/pages/admin/trash.rs +++ b/src/pages/admin/posts_trash.rs @@ -1,8 +1,11 @@ -//! 回收站管理页面。 +//! 回收站(文章管理下的 tab)。 //! //! 展示已软删除文章,支持恢复、彻底删除、批量操作、一键清空, //! 以及自动清理配置(启用开关 + 保留天数)。 //! 数据加载与操作仅在 WASM 前端通过 Dioxus server functions 交互。 +//! +//! 回收站原为独立路由 `/admin/trash`,现合并为 `/admin/posts/trash` tab, +//! 与文章列表共享「管理文章」导航项与 tab 栏(见 `posts.rs::PostsTabs`)。 use std::collections::HashSet; @@ -29,15 +32,17 @@ use crate::hooks::query::use_paginated; use crate::models::post::PostListItem; use crate::models::settings::TrashSettings; use crate::router::Route; +// 文章管理共享的 tab 栏(同目录 posts 模块)。 +use super::posts::PostsTabs; /// 每页展示的回收站文章数量。 const TRASH_PER_PAGE: i32 = 20; /// 回收站入口组件,默认展示第 1 页。 #[component] -pub fn Trash() -> Element { +pub fn PostsTrash() -> Element { rsx! { - TrashPage { page: 1 } + PostsTrashPage { page: 1 } } } @@ -46,7 +51,7 @@ pub fn Trash() -> Element { /// 支持单条/批量恢复与彻底删除、一键清空,以及内联自动清理配置。 #[allow(unused_mut, unused_variables)] #[component] -pub fn TrashPage(page: i32) -> Element { +pub fn PostsTrashPage(page: i32) -> Element { let current_page = page.max(1); let mut selected_ids: Signal> = use_signal(HashSet::new); @@ -88,6 +93,9 @@ pub fn TrashPage(page: i32) -> Element { } } + // tab 栏:文章 / 回收站。URL 驱动,与全部文章页共用(见 posts::PostsTabs)。 + PostsTabs {} + // 自动清理配置卡片(抽取为子组件 AutoPurgeSettings,见文件末尾)。 AutoPurgeSettings { settings } @@ -294,10 +302,10 @@ pub fn TrashPage(page: i32) -> Element { current_page, total: total(), per_page: TRASH_PER_PAGE, - prev_route: if current_page - 1 <= 1 { Route::Trash {} } else { Route::TrashPage { + prev_route: if current_page - 1 <= 1 { Route::PostsTrash {} } else { Route::PostsTrashPage { page: current_page - 1, } }, - next_route: Route::TrashPage { + next_route: Route::PostsTrashPage { page: current_page + 1, }, unit: "篇", @@ -316,7 +324,7 @@ pub fn TrashPage(page: i32) -> Element { /// 内部完成。`settings`(已保存的服务端配置)由父组件传入双向绑定 signal: /// 本组件加载/保存时写入,父组件读取 `retention_days` 供 TrashRow 的「剩余天数」。 /// -/// 从 `TrashPage` 抽取以降低 god component 复杂度(见 dioxus-render-purity skill)。 +/// 从 `PostsTrashPage` 抽取以降低 god component 复杂度(见 dioxus-render-purity skill)。 #[component] #[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))] fn AutoPurgeSettings(settings: Signal) -> Element { @@ -644,7 +652,7 @@ fn TrashRow( mod tests { #[test] fn test_auto_purge_settings_has_transition_class() { - let full_code = include_str!("trash.rs"); + let full_code = include_str!("posts_trash.rs"); let code = full_code.split("#[cfg(test)]").next().unwrap_or(full_code); assert!(code.contains("grid transition-all duration-300 ease-in-out")); assert!(code.contains("grid-template-rows: 1fr; opacity: 1;")); diff --git a/src/router.rs b/src/router.rs index feda103..2efabec 100644 --- a/src/router.rs +++ b/src/router.rs @@ -12,8 +12,8 @@ use crate::components::frontend_layout::FrontendLayout; use crate::context::UserContext; use crate::pages::about::About; use crate::pages::admin::{ - Admin, AdminComments, AdminCommentsPage, Posts, PostsPage, Runner, System, Trash, TrashPage, - Write, WriteEdit, + Admin, AdminComments, AdminCommentsPage, Posts, PostsPage, PostsTrash, PostsTrashPage, Runner, + System, Write, WriteEdit, }; use crate::pages::archives::Archives; use crate::pages::home::{Home, HomePage}; @@ -76,6 +76,12 @@ pub enum Route { /// 文章管理列表 #[route("/posts")] Posts {}, + /// 回收站(文章管理下的 tab,静态段优先于 :page) + #[route("/posts/trash")] + PostsTrash {}, + /// 回收站分页 + #[route("/posts/trash/:page")] + PostsTrashPage { page: i32 }, /// 文章管理列表分页 #[route("/posts/:page")] PostsPage { page: i32 }, @@ -85,12 +91,6 @@ pub enum Route { /// 评论管理分页 #[route("/comments/:page")] AdminCommentsPage { page: i32 }, - /// 回收站 - #[route("/trash")] - Trash {}, - /// 回收站分页 - #[route("/trash/:page")] - TrashPage { page: i32 }, /// 系统管理(数据库 + 服务器状态 + SQL 控制台 + 导出 + 备份) #[route("/system")] System {},