From 18500c949644162fcf58a68c77cae9de4c1007c5 Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 12 Jun 2026 19:21:46 +0800 Subject: [PATCH] =?UTF-8?q?docs(pages-admin):=20=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/admin/comments.rs | 19 +++++++++++++++- src/pages/admin/dashboard.rs | 14 ++++++++++++ src/pages/admin/mod.rs | 12 ++++++++++ src/pages/admin/posts.rs | 17 ++++++++++++++ src/pages/admin/write.rs | 43 +++++++++++++++++++++++++++++++----- 5 files changed, 99 insertions(+), 6 deletions(-) diff --git a/src/pages/admin/comments.rs b/src/pages/admin/comments.rs index 57fcc21..78ab628 100644 --- a/src/pages/admin/comments.rs +++ b/src/pages/admin/comments.rs @@ -1,8 +1,14 @@ +//! 评论管理页面。 +//! +//! 提供评论列表、状态筛选(全部 / 待审核 / 已通过 / 垃圾箱)、批量操作与单条操作。 +//! 数据加载与状态变更仅在 WASM 前端通过 Dioxus server functions 交互。 + use std::collections::HashSet; use dioxus::prelude::*; use dioxus::router::components::Link; +// 仅在 WASM 前端使用的评论管理接口。 #[cfg(target_arch = "wasm32")] use crate::api::comments::trash_comment; use crate::api::comments::{approve_comment, batch_update_comment_status, spam_comment}; @@ -12,16 +18,22 @@ use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; use crate::models::comment::{AdminComment, CommentStatus}; use crate::router::Route; +/// 每页展示的评论数量。 const COMMENTS_PER_PAGE: i32 = 20; +/// 评论管理入口组件,默认展示第 1 页。 #[component] pub fn AdminComments() -> Element { rsx! { AdminCommentsPage { page: 1 } } } +/// 评论管理分页组件。 +/// +/// 支持按状态筛选、全选 / 单选、批量审批 / 标记垃圾 / 删除,以及单条评论状态操作。 #[component] pub fn AdminCommentsPage(page: i32) -> Element { let current_page = page.max(1); + // 当前筛选状态:优先从 URL 查询参数 `?status=` 读取(仅 WASM 前端)。 let mut active_filter = use_signal(|| { #[cfg(target_arch = "wasm32")] { @@ -41,6 +53,7 @@ pub fn AdminCommentsPage(page: i32) -> Element { #[cfg(not(target_arch = "wasm32"))] String::new() }); + // 已选中的评论 ID 集合、评论列表、总数、加载与错误状态。 let mut selected_ids: Signal> = use_signal(HashSet::new); let mut comments: Signal> = use_signal(Vec::new); let mut total: Signal = use_signal(|| 0); @@ -49,6 +62,7 @@ pub fn AdminCommentsPage(page: i32) -> Element { #[allow(unused_mut)] let mut error: Signal> = use_signal(|| None); + // 将当前筛选字符串转换为接口所需的 status 参数。 #[allow(unused_variables)] let filter_status = move || { let f = active_filter(); @@ -59,11 +73,12 @@ pub fn AdminCommentsPage(page: i32) -> Element { } }; - // 客户端(CSR)加载数据 + // 客户端(CSR)加载数据:筛选或页码变化时触发。 use_effect(move || { let _ = active_filter(); let _ = current_page; + // 仅在 WASM 前端发起评论列表请求。 #[cfg(target_arch = "wasm32")] { let page = current_page; @@ -305,6 +320,7 @@ pub fn AdminCommentsPage(page: i32) -> Element { } } +/// 评论表格行组件,展示单条评论的作者、内容、所属文章、状态与操作按钮。 #[component] fn CommentRow( comment: AdminComment, @@ -418,6 +434,7 @@ fn CommentRow( } } +/// 评论分页导航组件。 #[component] fn CommentsPagination(current_page: i32, total: i64) -> Element { let has_prev = current_page > 1; diff --git a/src/pages/admin/dashboard.rs b/src/pages/admin/dashboard.rs index de029a8..cf724c2 100644 --- a/src/pages/admin/dashboard.rs +++ b/src/pages/admin/dashboard.rs @@ -1,6 +1,11 @@ +//! 管理后台仪表盘页面。 +//! +//! 展示文章统计、待审核评论数量以及最近文章列表,仅在 WASM 前端通过 Dioxus server functions 加载数据。 + use dioxus::prelude::*; use dioxus::router::components::Link; +// 仅在 WASM 前端使用的 server function 导入。 #[cfg(target_arch = "wasm32")] use crate::api::comments::get_pending_count; #[cfg(target_arch = "wasm32")] @@ -10,18 +15,25 @@ use crate::api::posts::{PostListResponse, PostStatsResponse}; use crate::models::post::{Post, PostStats}; use crate::router::Route; +/// 后台仪表盘页面组件。 +/// +/// 展示文章总数、草稿数、已发布数的统计卡片,待审核评论入口,以及最近 5 篇文章列表。 +/// 所有数据仅在 WASM 前端通过 server functions 异步加载。 #[component] #[allow(unused_mut)] pub fn Admin() -> Element { + // 仪表盘状态:统计数据、最近文章、待审核评论数与首次加载标志。 let mut stats = use_signal(|| None::); let mut recent_posts = use_signal(|| None::>); let mut pending_count = use_signal(|| None::); let mut loaded = use_signal(|| false); + // 组件挂载后触发一次:仅在 WASM 前端异步请求仪表盘数据。 use_effect(move || { if !loaded() { loaded.set(true); + // 以下请求只在 WASM 前端执行,SSR 阶段不会访问浏览器 API。 #[cfg(target_arch = "wasm32")] { spawn(async move { @@ -135,6 +147,7 @@ pub fn Admin() -> Element { } } +/// 统计卡片组件,显示一个数值指标与标签。 #[component] fn StatCard(value: String, label: String) -> Element { rsx! { @@ -149,6 +162,7 @@ fn StatCard(value: String, label: String) -> Element { } } +/// 最近文章列表项,显示标题、状态标签与发布日期。 #[component] fn RecentPostItem(post: Post) -> Element { let date_str = post.formatted_date(); diff --git a/src/pages/admin/mod.rs b/src/pages/admin/mod.rs index 4ea2a15..75e89c8 100644 --- a/src/pages/admin/mod.rs +++ b/src/pages/admin/mod.rs @@ -1,9 +1,21 @@ +//! 管理后台页面模块。 +//! +//! 汇总并重新导出后台管理相关的页面组件,供路由与其他模块使用。 + +/// 评论管理页面模块。 pub mod comments; +/// 管理后台仪表盘页面模块。 pub mod dashboard; +/// 文章管理列表页面模块。 pub mod posts; +/// 文章编辑器页面模块(基于 Tiptap 富文本编辑器)。 pub mod write; +/// 评论管理入口组件(带默认分页)。 pub use comments::{AdminComments, AdminCommentsPage}; +/// 管理后台仪表盘组件。 pub use dashboard::Admin; +/// 文章管理列表组件(带默认分页)。 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 28de746..f9d9655 100644 --- a/src/pages/admin/posts.rs +++ b/src/pages/admin/posts.rs @@ -1,6 +1,12 @@ +//! 文章管理列表页面。 +//! +//! 提供文章分页列表、删除单篇文章、重建 content_html 缓存,以及跳转到编辑器的能力。 +//! 数据加载与写操作仅在 WASM 前端通过 Dioxus server functions 完成。 + use dioxus::prelude::*; use dioxus::router::components::Link; +// 仅在 WASM 前端使用的分页数据接口。 #[cfg(target_arch = "wasm32")] use crate::api::posts::list_posts; #[cfg(target_arch = "wasm32")] @@ -11,16 +17,22 @@ use crate::components::skeletons::posts_skeleton::PostsSkeleton; use crate::models::post::Post; use crate::router::Route; +/// 每页展示的文章数量。 const POSTS_PER_PAGE: i32 = 20; +/// 文章管理入口组件,默认展示第 1 页。 #[component] pub fn Posts() -> Element { rsx! { PostsPage { page: 1 } } } +/// 文章管理分页组件。 +/// +/// 根据 `page` 参数加载对应页的文章列表,支持删除单篇文章与重建文章 HTML 缓存。 #[component] pub fn PostsPage(page: i32) -> Element { let current_page = page.max(1); + // 文章列表、总数、加载状态、删除中 ID、重建缓存状态与结果。 let mut posts = use_signal(Vec::new); let mut total = use_signal(|| 0_i64); let mut loading = use_signal(|| true); @@ -28,10 +40,12 @@ pub fn PostsPage(page: i32) -> Element { let mut rebuilding = use_signal(|| false); let mut rebuild_result = use_signal(|| Option::::None); + // 页码变化时加载分页数据:WASM 前端请求接口,SSR 直接结束加载。 use_effect(move || { let _ = current_page; loading.set(true); + // 仅在 WASM 前端发起分页请求。 #[cfg(target_arch = "wasm32")] { let p = current_page; @@ -140,6 +154,7 @@ pub fn PostsPage(page: i32) -> Element { PostRow { post: post.clone(), deleting: deleting() == Some(post.id), + // 删除文章:先乐观更新本地列表,再调用 server function,失败时弹出浏览器提示。 on_delete: move |id| { deleting.set(Some(id)); let id_for_api = id; @@ -171,6 +186,7 @@ pub fn PostsPage(page: i32) -> Element { } } +/// 分页导航组件,根据当前页与总数量生成上一页 / 下一页链接。 #[component] fn Pagination(current_page: i32, total: i64) -> Element { let has_prev = current_page > 1; @@ -216,6 +232,7 @@ fn Pagination(current_page: i32, total: i64) -> Element { } } +/// 文章表格行组件,展示单篇文章的标题、状态、日期与操作按钮。 #[component] fn PostRow(post: Post, deleting: bool, on_delete: EventHandler) -> Element { let date_str = post.formatted_date(); diff --git a/src/pages/admin/write.rs b/src/pages/admin/write.rs index 87d263a..c2f8ce6 100644 --- a/src/pages/admin/write.rs +++ b/src/pages/admin/write.rs @@ -1,5 +1,12 @@ +//! 文章编辑器页面。 +//! +//! 提供新建文章与编辑文章两种模式,使用基于 Tiptap 的富文本编辑器。 +//! 编辑器通过 `js_sys::eval` 在 WASM 前端初始化,并与 `window.TiptapEditor` 实例交互, +//! 实现 Markdown 内容回填、图片上传与组件卸载时的清理。 + use dioxus::prelude::*; +// 仅在 WASM 前端使用的类型转换与文章 API。 #[cfg(target_arch = "wasm32")] use wasm_bindgen::JsCast; @@ -11,22 +18,37 @@ use crate::components::write_skeleton::WriteSkeleton; use crate::models::post::Post; use crate::router::Route; +/// 新建文章页面组件。 +/// +/// 内部委托给 `write_editor`,以 `None` 表示新建模式。 #[component] #[allow(unused_mut, unused_variables)] pub fn Write() -> Element { write_editor(None) } +/// 编辑文章页面组件。 +/// +/// `id` 为要编辑的文章 ID,内部委托给 `write_editor` 加载现有数据。 #[component] #[allow(unused_mut, unused_variables)] pub fn WriteEdit(id: i32) -> Element { write_editor(Some(id)) } +/// 文章编辑器核心组件,支持新建(`post_id == None`)与编辑模式。 +/// +/// 负责: +/// - 编辑模式下通过 server function 拉取文章数据; +/// - 在 WASM 前端初始化 Tiptap 富文本编辑器并轮询就绪状态; +/// - 编辑模式下将 Markdown 内容回填到编辑器; +/// - 提交时读取编辑器 Markdown、校验并调用 create_post / update_post; +/// - 组件卸载时销毁 Tiptap 实例并清理全局状态。 #[allow(unused_mut, unused_variables)] fn write_editor(post_id: Option) -> Element { let is_edit = post_id.is_some(); + // 文章元信息表单字段。 let mut title = use_signal(|| "".to_string()); let mut summary = use_signal(|| "".to_string()); let mut slug = use_signal(|| "".to_string()); @@ -34,6 +56,7 @@ fn write_editor(post_id: Option) -> Element { let mut cover_image = use_signal(|| "".to_string()); let mut status = use_signal(|| "draft".to_string()); let mut content = use_signal(|| "".to_string()); + // 页面与编辑器加载、保存、错误、成功等状态。 let mut loading = use_signal(|| true); let mut saving = use_signal(|| false); let mut error = use_signal(|| None::); @@ -42,9 +65,10 @@ fn write_editor(post_id: Option) -> Element { let mut has_backfilled = use_signal(|| false); let mut load_error = use_signal(|| None::); - // 编辑模式:加载文章数据(CSR) + // 编辑模式:用于暂存从服务端加载的文章数据。 let mut edit_post = use_signal(|| None::); + // 编辑模式:文章数据加载完成后,将字段回填到表单信号。 use_effect(move || { if !is_edit || has_backfilled() { return; @@ -61,6 +85,7 @@ fn write_editor(post_id: Option) -> Element { } }); + // 编辑模式:仅在 WASM 前端通过 server function 加载文章详情。 use_effect(move || { if is_edit { #[cfg(target_arch = "wasm32")] @@ -82,6 +107,7 @@ fn write_editor(post_id: Option) -> Element { } }); + // 组件卸载时清理 Tiptap 实例:销毁编辑器、删除实例映射、重置全局就绪标志与内容缓存。 #[cfg(target_arch = "wasm32")] use_drop(move || { let _ = js_sys::eval( @@ -101,14 +127,17 @@ fn write_editor(post_id: Option) -> Element { ); }); + // Tiptap 编辑器初始化:在 WASM 前端通过 js_sys::eval 调用 public/tiptap/ 构建产物暴露的 window.TiptapEditor。 + // 编辑模式需等待文章数据加载完成,避免空内容覆盖后续回填。 use_effect(move || { #[cfg(target_arch = "wasm32")] { - // 编辑模式:等数据加载完再初始化 + // 编辑模式:等数据加载完再初始化。 if is_edit && edit_post().is_none() { return; } + // 执行 JS 初始化脚本:查找 DOM 容器,调用 TiptapEditor.create,并注册 onUpdate / onImageUpload 回调。 let _ = js_sys::eval( r#" (function initEditor() { @@ -169,11 +198,11 @@ fn write_editor(post_id: Option) -> Element { } }); - // 轮询编辑器就绪状态 + // 轮询 Tiptap 编辑器就绪状态:最多等待约 10 秒,就绪后编辑模式回填 Markdown,最后结束加载。 use_effect(move || { #[cfg(target_arch = "wasm32")] { - // 编辑模式:等数据加载完再开始轮询 + // 编辑模式:等数据加载完再开始轮询。 if is_edit && edit_post().is_none() { return; } @@ -187,7 +216,7 @@ fn write_editor(post_id: Option) -> Element { } if let Ok(ready) = js_sys::eval("window.__tiptap_ready") { if ready.as_bool().unwrap_or(false) { - // 编辑模式:回填编辑器内容 + // 编辑模式:通过 window.TiptapEditor 实例的 setMarkdown 回填已有内容。 if is_edit && !editor_content_set() { let md = content(); if !md.is_empty() { @@ -214,6 +243,7 @@ fn write_editor(post_id: Option) -> Element { } }); + // 提交表单:校验标题与内容,读取 Tiptap 编辑器 Markdown,调用 create_post 或 update_post。 let on_submit = move |_| { if title().trim().is_empty() { error.set(Some("标题不能为空".to_string())); @@ -221,8 +251,10 @@ fn write_editor(post_id: Option) -> Element { return; } + // 仅在 WASM 前端读取编辑器内容并发起保存请求。 #[cfg(target_arch = "wasm32")] { + // 优先通过 window.TiptapEditor 实例读取 Markdown,否则退回到全局缓存。 let md = js_sys::eval(r#" (function() { var editor = window.TiptapEditor && window.TiptapEditor._instances && window.TiptapEditor._instances.get('tiptap-editor'); @@ -235,6 +267,7 @@ fn write_editor(post_id: Option) -> Element { return; } + // 将逗号分隔的标签字符串转换为列表。 let tags_list: Vec = tags() .split(',') .map(|t| t.trim().to_string())