docs(pages-admin): 补充中文注释

This commit is contained in:
xfy 2026-06-12 19:21:46 +08:00
parent abfab19839
commit 18500c9496
5 changed files with 99 additions and 6 deletions

View File

@ -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<HashSet<i64>> = use_signal(HashSet::new);
let mut comments: Signal<Vec<AdminComment>> = use_signal(Vec::new);
let mut total: Signal<i64> = use_signal(|| 0);
@ -49,6 +62,7 @@ pub fn AdminCommentsPage(page: i32) -> Element {
#[allow(unused_mut)]
let mut error: Signal<Option<String>> = 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;

View File

@ -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::<PostStats>);
let mut recent_posts = use_signal(|| None::<Vec<Post>>);
let mut pending_count = use_signal(|| None::<i64>);
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();

View File

@ -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};

View File

@ -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::<String>::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<i32>) -> Element {
let date_str = post.formatted_date();

View File

@ -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<i32>) -> 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<i32>) -> 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::<String>);
@ -42,9 +65,10 @@ fn write_editor(post_id: Option<i32>) -> Element {
let mut has_backfilled = use_signal(|| false);
let mut load_error = use_signal(|| None::<String>);
// 编辑模式:加载文章数据CSR
// 编辑模式:用于暂存从服务端加载的文章数据。
let mut edit_post = use_signal(|| None::<Post>);
// 编辑模式:文章数据加载完成后,将字段回填到表单信号。
use_effect(move || {
if !is_edit || has_backfilled() {
return;
@ -61,6 +85,7 @@ fn write_editor(post_id: Option<i32>) -> Element {
}
});
// 编辑模式:仅在 WASM 前端通过 server function 加载文章详情。
use_effect(move || {
if is_edit {
#[cfg(target_arch = "wasm32")]
@ -82,6 +107,7 @@ fn write_editor(post_id: Option<i32>) -> Element {
}
});
// 组件卸载时清理 Tiptap 实例:销毁编辑器、删除实例映射、重置全局就绪标志与内容缓存。
#[cfg(target_arch = "wasm32")]
use_drop(move || {
let _ = js_sys::eval(
@ -101,14 +127,17 @@ fn write_editor(post_id: Option<i32>) -> 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<i32>) -> 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<i32>) -> 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<i32>) -> 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<i32>) -> 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<i32>) -> Element {
return;
}
// 将逗号分隔的标签字符串转换为列表。
let tags_list: Vec<String> = tags()
.split(',')
.map(|t| t.trim().to_string())