docs(pages-admin): 补充中文注释
This commit is contained in:
parent
abfab19839
commit
18500c9496
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user