refactor(admin): merge trash into /admin/posts as a URL-driven tab
回收站从独立路由 /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 即可,无需改组件。
This commit is contained in:
parent
9c3f2242ba
commit
d72495c002
@ -45,13 +45,20 @@ pub fn AdminLayout() -> Element {
|
|||||||
(Route::Admin {}, "仪表盘"),
|
(Route::Admin {}, "仪表盘"),
|
||||||
(Route::Write {}, "写文章"),
|
(Route::Write {}, "写文章"),
|
||||||
(Route::Posts {}, "管理文章"),
|
(Route::Posts {}, "管理文章"),
|
||||||
(Route::Trash {}, "回收站"),
|
|
||||||
(Route::Runner {}, "试运行"),
|
(Route::Runner {}, "试运行"),
|
||||||
(Route::System {}, "系统"),
|
(Route::System {}, "系统"),
|
||||||
];
|
];
|
||||||
|
|
||||||
let is_write_route =
|
let is_write_route =
|
||||||
matches!(route, Route::Write {}) || matches!(route, Route::WriteEdit { .. });
|
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 {
|
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)]"
|
"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",
|
nav { class: "flex-1 flex flex-col gap-2",
|
||||||
for (dest, label) in admin_nav_items {
|
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 base_class = "flex items-center px-4 py-3 rounded-2xl text-sm font-medium transition-all";
|
||||||
let text_class = if is_active {
|
let text_class = if is_active {
|
||||||
"bg-[var(--color-paper-theme)] text-[var(--color-paper-primary)] shadow-sm border border-[var(--color-paper-border)]"
|
"bg-[var(--color-paper-theme)] text-[var(--color-paper-primary)] shadow-sm border border-[var(--color-paper-border)]"
|
||||||
|
|||||||
@ -8,12 +8,12 @@ pub mod comments;
|
|||||||
pub mod dashboard;
|
pub mod dashboard;
|
||||||
/// 文章管理列表页面模块。
|
/// 文章管理列表页面模块。
|
||||||
pub mod posts;
|
pub mod posts;
|
||||||
|
/// 回收站页面模块(文章管理下的 tab)。
|
||||||
|
pub mod posts_trash;
|
||||||
/// 代码试运行沙箱页面模块。
|
/// 代码试运行沙箱页面模块。
|
||||||
pub mod runner;
|
pub mod runner;
|
||||||
/// 系统管理页面模块(数据库 + 服务器状态 + SQL 控制台 + 导出 + 备份)。
|
/// 系统管理页面模块(数据库 + 服务器状态 + SQL 控制台 + 导出 + 备份)。
|
||||||
pub mod system;
|
pub mod system;
|
||||||
/// 回收站管理页面模块。
|
|
||||||
pub mod trash;
|
|
||||||
/// 文章编辑器页面模块(基于 Tiptap 富文本编辑器)。
|
/// 文章编辑器页面模块(基于 Tiptap 富文本编辑器)。
|
||||||
pub mod write;
|
pub mod write;
|
||||||
|
|
||||||
@ -23,11 +23,11 @@ pub use comments::{AdminComments, AdminCommentsPage};
|
|||||||
pub use dashboard::Admin;
|
pub use dashboard::Admin;
|
||||||
/// 文章管理列表组件(带默认分页)。
|
/// 文章管理列表组件(带默认分页)。
|
||||||
pub use posts::{Posts, PostsPage};
|
pub use posts::{Posts, PostsPage};
|
||||||
|
/// 回收站管理组件(带默认分页,文章管理下的 tab)。
|
||||||
|
pub use posts_trash::{PostsTrash, PostsTrashPage};
|
||||||
/// 代码试运行沙箱组件。
|
/// 代码试运行沙箱组件。
|
||||||
pub use runner::Runner;
|
pub use runner::Runner;
|
||||||
/// 系统管理入口组件。
|
/// 系统管理入口组件。
|
||||||
pub use system::System;
|
pub use system::System;
|
||||||
/// 回收站管理组件(带默认分页)。
|
|
||||||
pub use trash::{Trash, TrashPage};
|
|
||||||
/// 文章编辑器组件(新建与编辑模式)。
|
/// 文章编辑器组件(新建与编辑模式)。
|
||||||
pub use write::{Write, WriteEdit};
|
pub use write::{Write, WriteEdit};
|
||||||
|
|||||||
@ -9,8 +9,12 @@ use dioxus::router::components::Link;
|
|||||||
// 分页数据接口:list_posts 是 server function,两端都生成(wasm 端为 client stub,
|
// 分页数据接口:list_posts 是 server function,两端都生成(wasm 端为 client stub,
|
||||||
// server 端为真实实现),故无需 cfg。实际请求只在 use_paginated 的 wasm 分支发出。
|
// server 端为真实实现),故无需 cfg。实际请求只在 use_paginated 的 wasm 分支发出。
|
||||||
use crate::api::posts::{list_posts, PostListResponse};
|
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::{
|
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::empty_state::{EmptyState, EmptyStateAction};
|
||||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||||
@ -95,6 +99,9 @@ pub fn PostsPage(page: i32) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tab 栏:文章 / 回收站。URL 驱动(Link 切换路由),回收站带数量角标。
|
||||||
|
PostsTabs {}
|
||||||
|
|
||||||
// 重建结果消息:独立成行,进入文档流,吃 space-y-6 的正常间距。
|
// 重建结果消息:独立成行,进入文档流,吃 space-y-6 的正常间距。
|
||||||
// 既不撑高 header(不在 header 内)触发 items-center 重排,也不脱离流
|
// 既不撑高 header(不在 header 内)触发 items-center 重排,也不脱离流
|
||||||
// 溢进表格(曾用 absolute top-full mt-2,因 28px > 24px 间距溢出 4px)。
|
// 溢进表格(曾用 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::<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::<i64>::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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
//! 回收站管理页面。
|
//! 回收站(文章管理下的 tab)。
|
||||||
//!
|
//!
|
||||||
//! 展示已软删除文章,支持恢复、彻底删除、批量操作、一键清空,
|
//! 展示已软删除文章,支持恢复、彻底删除、批量操作、一键清空,
|
||||||
//! 以及自动清理配置(启用开关 + 保留天数)。
|
//! 以及自动清理配置(启用开关 + 保留天数)。
|
||||||
//! 数据加载与操作仅在 WASM 前端通过 Dioxus server functions 交互。
|
//! 数据加载与操作仅在 WASM 前端通过 Dioxus server functions 交互。
|
||||||
|
//!
|
||||||
|
//! 回收站原为独立路由 `/admin/trash`,现合并为 `/admin/posts/trash` tab,
|
||||||
|
//! 与文章列表共享「管理文章」导航项与 tab 栏(见 `posts.rs::PostsTabs`)。
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
@ -29,15 +32,17 @@ 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;
|
||||||
|
// 文章管理共享的 tab 栏(同目录 posts 模块)。
|
||||||
|
use super::posts::PostsTabs;
|
||||||
|
|
||||||
/// 每页展示的回收站文章数量。
|
/// 每页展示的回收站文章数量。
|
||||||
const TRASH_PER_PAGE: i32 = 20;
|
const TRASH_PER_PAGE: i32 = 20;
|
||||||
|
|
||||||
/// 回收站入口组件,默认展示第 1 页。
|
/// 回收站入口组件,默认展示第 1 页。
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Trash() -> Element {
|
pub fn PostsTrash() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
TrashPage { page: 1 }
|
PostsTrashPage { page: 1 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +51,7 @@ pub fn Trash() -> Element {
|
|||||||
/// 支持单条/批量恢复与彻底删除、一键清空,以及内联自动清理配置。
|
/// 支持单条/批量恢复与彻底删除、一键清空,以及内联自动清理配置。
|
||||||
#[allow(unused_mut, unused_variables)]
|
#[allow(unused_mut, unused_variables)]
|
||||||
#[component]
|
#[component]
|
||||||
pub fn TrashPage(page: i32) -> Element {
|
pub fn PostsTrashPage(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);
|
||||||
|
|
||||||
@ -88,6 +93,9 @@ pub fn TrashPage(page: i32) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tab 栏:文章 / 回收站。URL 驱动,与全部文章页共用(见 posts::PostsTabs)。
|
||||||
|
PostsTabs {}
|
||||||
|
|
||||||
// 自动清理配置卡片(抽取为子组件 AutoPurgeSettings,见文件末尾)。
|
// 自动清理配置卡片(抽取为子组件 AutoPurgeSettings,见文件末尾)。
|
||||||
AutoPurgeSettings { settings }
|
AutoPurgeSettings { settings }
|
||||||
|
|
||||||
@ -294,10 +302,10 @@ pub fn TrashPage(page: i32) -> Element {
|
|||||||
current_page,
|
current_page,
|
||||||
total: total(),
|
total: total(),
|
||||||
per_page: TRASH_PER_PAGE,
|
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,
|
page: current_page - 1,
|
||||||
} },
|
} },
|
||||||
next_route: Route::TrashPage {
|
next_route: Route::PostsTrashPage {
|
||||||
page: current_page + 1,
|
page: current_page + 1,
|
||||||
},
|
},
|
||||||
unit: "篇",
|
unit: "篇",
|
||||||
@ -316,7 +324,7 @@ pub fn TrashPage(page: i32) -> Element {
|
|||||||
/// 内部完成。`settings`(已保存的服务端配置)由父组件传入双向绑定 signal:
|
/// 内部完成。`settings`(已保存的服务端配置)由父组件传入双向绑定 signal:
|
||||||
/// 本组件加载/保存时写入,父组件读取 `retention_days` 供 TrashRow 的「剩余天数」。
|
/// 本组件加载/保存时写入,父组件读取 `retention_days` 供 TrashRow 的「剩余天数」。
|
||||||
///
|
///
|
||||||
/// 从 `TrashPage` 抽取以降低 god component 复杂度(见 dioxus-render-purity skill)。
|
/// 从 `PostsTrashPage` 抽取以降低 god component 复杂度(见 dioxus-render-purity skill)。
|
||||||
#[component]
|
#[component]
|
||||||
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))]
|
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))]
|
||||||
fn AutoPurgeSettings(settings: Signal<TrashSettings>) -> Element {
|
fn AutoPurgeSettings(settings: Signal<TrashSettings>) -> Element {
|
||||||
@ -644,7 +652,7 @@ fn TrashRow(
|
|||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_auto_purge_settings_has_transition_class() {
|
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);
|
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 transition-all duration-300 ease-in-out"));
|
||||||
assert!(code.contains("grid-template-rows: 1fr; opacity: 1;"));
|
assert!(code.contains("grid-template-rows: 1fr; opacity: 1;"));
|
||||||
@ -12,8 +12,8 @@ use crate::components::frontend_layout::FrontendLayout;
|
|||||||
use crate::context::UserContext;
|
use crate::context::UserContext;
|
||||||
use crate::pages::about::About;
|
use crate::pages::about::About;
|
||||||
use crate::pages::admin::{
|
use crate::pages::admin::{
|
||||||
Admin, AdminComments, AdminCommentsPage, Posts, PostsPage, Runner, System, Trash, TrashPage,
|
Admin, AdminComments, AdminCommentsPage, Posts, PostsPage, PostsTrash, PostsTrashPage, Runner,
|
||||||
Write, WriteEdit,
|
System, Write, WriteEdit,
|
||||||
};
|
};
|
||||||
use crate::pages::archives::Archives;
|
use crate::pages::archives::Archives;
|
||||||
use crate::pages::home::{Home, HomePage};
|
use crate::pages::home::{Home, HomePage};
|
||||||
@ -76,6 +76,12 @@ pub enum Route {
|
|||||||
/// 文章管理列表
|
/// 文章管理列表
|
||||||
#[route("/posts")]
|
#[route("/posts")]
|
||||||
Posts {},
|
Posts {},
|
||||||
|
/// 回收站(文章管理下的 tab,静态段优先于 :page)
|
||||||
|
#[route("/posts/trash")]
|
||||||
|
PostsTrash {},
|
||||||
|
/// 回收站分页
|
||||||
|
#[route("/posts/trash/:page")]
|
||||||
|
PostsTrashPage { page: i32 },
|
||||||
/// 文章管理列表分页
|
/// 文章管理列表分页
|
||||||
#[route("/posts/:page")]
|
#[route("/posts/:page")]
|
||||||
PostsPage { page: i32 },
|
PostsPage { page: i32 },
|
||||||
@ -85,12 +91,6 @@ pub enum Route {
|
|||||||
/// 评论管理分页
|
/// 评论管理分页
|
||||||
#[route("/comments/:page")]
|
#[route("/comments/:page")]
|
||||||
AdminCommentsPage { page: i32 },
|
AdminCommentsPage { page: i32 },
|
||||||
/// 回收站
|
|
||||||
#[route("/trash")]
|
|
||||||
Trash {},
|
|
||||||
/// 回收站分页
|
|
||||||
#[route("/trash/:page")]
|
|
||||||
TrashPage { page: i32 },
|
|
||||||
/// 系统管理(数据库 + 服务器状态 + SQL 控制台 + 导出 + 备份)
|
/// 系统管理(数据库 + 服务器状态 + SQL 控制台 + 导出 + 备份)
|
||||||
#[route("/system")]
|
#[route("/system")]
|
||||||
System {},
|
System {},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user