diff --git a/src/components/admin_layout.rs b/src/components/admin_layout.rs index 6d11584..5663a6a 100644 --- a/src/components/admin_layout.rs +++ b/src/components/admin_layout.rs @@ -1,3 +1,8 @@ +//! 后台管理布局组件 +//! +//! 包裹所有后台路由,提供管理员专属导航、登录校验、主题切换与登出入口。 +//! 在未完成身份校验前显示与真实布局结构一致的骨架屏,避免切换闪烁。 + use dioxus::prelude::*; use crate::api::auth::{get_current_user, logout}; @@ -10,6 +15,13 @@ use crate::hooks::delayed_loading::use_delayed_loading; use crate::router::Route; use crate::theme::ThemeToggle; +/// 后台管理整体布局组件。 +/// +/// 负责: +/// - 通过 `get_current_user` 校验登录状态,未登录时跳转登录页 +/// - 渲染顶部导航(仪表盘、写文章、管理文章)与主题切换/登出按钮 +/// - 根据当前路由切换主区域样式(Write 路由固定高度,其他路由可滚动) +/// - 校验完成前使用骨架屏保持布局稳定 #[component] pub fn AdminLayout() -> Element { let mut ctx: UserContext = use_context(); @@ -17,6 +29,7 @@ pub fn AdminLayout() -> Element { let route = use_route::(); let show_skeleton = use_delayed_loading(move || !(ctx.checked)()); + // 仅在首次挂载时执行一次登录校验 use_effect(move || { if !(ctx.checked)() { (ctx.checked).set(true); @@ -37,6 +50,7 @@ pub fn AdminLayout() -> Element { } }); + // 后台导航项,当前路由高亮 let admin_nav_items = vec![ NavItemConfig { route: Route::Admin {}, @@ -55,6 +69,7 @@ pub fn AdminLayout() -> Element { }, ]; + // 右侧操作区:主题切换 + 登出按钮 let right_content = rsx! { div { class: "flex items-center gap-3", ThemeToggle {} @@ -88,6 +103,7 @@ pub fn AdminLayout() -> Element { "min-h-screen flex flex-col bg-white dark:bg-[#1d1e20]" }; + // 根据校验状态与用户状态渲染真实布局、跳转提示或骨架屏 match ((ctx.checked)(), (ctx.user)()) { (true, Some(_)) => { rsx! { diff --git a/src/components/admin_skeleton.rs b/src/components/admin_skeleton.rs index a3ebf84..0a13d23 100644 --- a/src/components/admin_skeleton.rs +++ b/src/components/admin_skeleton.rs @@ -1,8 +1,15 @@ +//! 后台仪表盘骨架屏 +//! +//! 仅在 AdminLayout 的内容区展示,不包含 Header 与 Footer, +//! 用于校验登录状态期间保持视觉稳定。 + use dioxus::prelude::*; use crate::components::skeletons::atoms::SkeletonBox; -/// 仅仪表盘内容区骨架(不含 header/footer) +/// 仪表盘内容区骨架屏组件(不含 header/footer)。 +/// +/// 包含统计卡片、快捷操作按钮与最近文章列表三组占位块。 #[component] pub fn AdminDashboardSkeleton() -> Element { rsx! { diff --git a/src/components/comments/actions.rs b/src/components/comments/actions.rs index 93529d9..938a545 100644 --- a/src/components/comments/actions.rs +++ b/src/components/comments/actions.rs @@ -1,8 +1,21 @@ +//! 评论管理操作组件 +//! +//! 在后台管理文章评论时,提供通过、标记垃圾、删除(移入回收站)三种操作按钮。 + use dioxus::prelude::*; use crate::api::comments::{approve_comment, spam_comment, trash_comment}; use crate::components::comments::section::CommentContext; +/// 评论管理操作按钮组件。 +/// +/// Props: +/// - `comment_id`:目标评论 ID +/// - `post_id`:所属文章 ID(当前未使用,保留用于未来扩展) +/// +/// 关键事件: +/// - 点击"通过"/"垃圾"/"删除"按钮后调用对应 API,操作完成后触发评论列表刷新 +/// - 操作期间禁用按钮,防止重复提交 #[component] pub fn CommentActions(comment_id: i64, post_id: i32) -> Element { let ctx: CommentContext = use_context(); diff --git a/src/components/comments/form.rs b/src/components/comments/form.rs index d9093e6..2011a90 100644 --- a/src/components/comments/form.rs +++ b/src/components/comments/form.rs @@ -1,3 +1,7 @@ +//! 评论表单组件 +//! +//! 提供发表评论与回复评论的表单,包含昵称、邮箱、网站、内容与反垃圾蜜罐字段。 + use dioxus::prelude::*; use crate::api::comments::create_comment; @@ -5,6 +9,16 @@ use crate::components::comments::section::CommentContext; use crate::components::forms::{AlertBox, BUTTON_PRIMARY_CLASS, INPUT_CLASS}; use crate::hooks::comment_storage::{self, PendingComment}; +/// 评论表单组件,用于顶层评论或回复评论。 +/// +/// Props: +/// - `post_id`:所属文章 ID +/// - `parent_id`:回复目标评论 ID,`None` 表示顶层评论 +/// +/// 关键事件: +/// - 挂载时从本地存储恢复上次填写的作者信息 +/// - 提交时校验必填项与蜜罐字段 +/// - 提交成功后清空内容、保存作者信息、添加待审核评论并触发列表刷新 #[component] pub fn CommentForm(post_id: i32, parent_id: Option) -> Element { let ctx: CommentContext = use_context(); @@ -21,6 +35,7 @@ pub fn CommentForm(post_id: i32, parent_id: Option) -> Element { let mut message = use_signal(|| Option::<(String, &'static str)>::None); let mut loaded = use_signal(|| false); + // 首次挂载时从本地存储加载作者信息 use_effect(move || { if loaded() { return; @@ -33,6 +48,7 @@ pub fn CommentForm(post_id: i32, parent_id: Option) -> Element { } }); + // 回复表单:当前未激活回复时隐藏 if let Some(pid) = parent_id { if active_reply() != Some(pid) { return rsx! {}; @@ -107,6 +123,7 @@ pub fn CommentForm(post_id: i32, parent_id: Option) -> Element { "支持 Markdown 语法" } + // 蜜罐字段:对普通用户隐藏,用于拦截简单机器人 textarea { class: "hidden", aria_hidden: "true", @@ -131,6 +148,7 @@ pub fn CommentForm(post_id: i32, parent_id: Option) -> Element { let content = content_md(); let hp = honeypot(); + // 蜜罐被填充则直接丢弃 if !hp.is_empty() { return; } diff --git a/src/components/comments/item.rs b/src/components/comments/item.rs index 6032b6e..a2ea1aa 100644 --- a/src/components/comments/item.rs +++ b/src/components/comments/item.rs @@ -1,15 +1,29 @@ +//! 单条评论项组件 +//! +//! 展示已审核通过的评论,支持展开/收起回复表单。 + use dioxus::prelude::*; use crate::components::comments::form::CommentForm; use crate::components::comments::section::CommentContext; use crate::models::comment::PublicComment; +/// 单条已审核评论组件。 +/// +/// Props: +/// - `comment`:已审核评论数据 +/// - `post_id`:所属文章 ID +/// +/// 关键行为: +/// - 点击"回复"按钮切换该评论下方的回复表单 +/// - 最大递归深度限制为 20,超过后隐藏回复按钮 #[component] pub fn CommentItem(comment: PublicComment, post_id: i32) -> Element { let ctx: CommentContext = use_context(); let mut active_reply = ctx.active_reply; let refresh_trigger = ctx.refresh_trigger; + // 孤儿评论按顶层展示 let depth = if comment.parent_id.is_none() && comment.depth > 0 { 0 } else { @@ -23,6 +37,7 @@ pub fn CommentItem(comment: PublicComment, post_id: i32) -> Element { let _ = refresh_trigger; + // 作者名展示为链接或普通文本 let author_element = match &comment.author_url { Some(url) if !url.is_empty() => rsx! { a { diff --git a/src/components/comments/list.rs b/src/components/comments/list.rs index 3358dad..ce6cdb3 100644 --- a/src/components/comments/list.rs +++ b/src/components/comments/list.rs @@ -1,3 +1,7 @@ +//! 评论列表组件 +//! +//! 将已审核评论与待审核评论合并成一棵树并按时间排序渲染。 + use dioxus::prelude::*; use crate::components::comments::item::CommentItem; @@ -5,12 +9,20 @@ use crate::components::comments::pending_item::PendingCommentItem; use crate::hooks::comment_storage::PendingComment; use crate::models::comment::PublicComment; +/// 合并后的评论节点,可能是已审核或待审核评论。 #[derive(Clone)] enum MergedComment { Approved(PublicComment), Pending(PendingComment), } +/// 合并两类评论并构建成树形结构。 +/// +/// 处理逻辑: +/// - 将已审核与待审核评论统一为 `MergedComment` +/// - 若某条评论的 parent_id 不存在于当前集合中,则视为顶层评论 +/// - 同一父节点下的子评论按时间排序 +/// - 使用 DFS 前序遍历生成最终展示顺序 fn merge_and_treeify( approved: Vec, pending: Vec, @@ -31,6 +43,7 @@ fn merge_and_treeify( }) .collect(); + // 按 parent_id 分组,处理指向不存在父节点的 parent_id let mut children_map: HashMap, Vec> = HashMap::new(); for comment in all { let parent_id = match &comment { @@ -47,6 +60,7 @@ fn merge_and_treeify( .push(comment); } + // 每个父节点下的子评论按创建时间排序 for children in children_map.values_mut() { children.sort_by(|a, b| { let time_a = match a { @@ -61,6 +75,7 @@ fn merge_and_treeify( }); } + // 深度优先遍历生成树形顺序 fn dfs( parent_id: Option, children_map: &HashMap, Vec>, @@ -83,6 +98,14 @@ fn merge_and_treeify( result } +/// 评论列表组件。 +/// +/// Props: +/// - `comments`:已审核评论列表 +/// - `pending`:待审核评论列表 +/// - `post_id`:所属文章 ID +/// +/// 根据两类评论构建合并树,依次渲染为 `CommentItem` 或 `PendingCommentItem`。 #[component] pub fn CommentList( comments: Vec, diff --git a/src/components/comments/mod.rs b/src/components/comments/mod.rs index f2fd5e3..5c1af08 100644 --- a/src/components/comments/mod.rs +++ b/src/components/comments/mod.rs @@ -1,6 +1,16 @@ +//! 评论组件模块 +//! +//! 提供评论相关组件:评论列表、单条评论、待审核评论、评论表单、评论区段与管理操作。 + +/// 评论操作按钮组件。 pub mod actions; +/// 评论表单组件。 pub mod form; +/// 单条评论组件。 pub mod item; +/// 评论列表组件。 pub mod list; +/// 待审核评论组件。 pub mod pending_item; +/// 评论区段组件。 pub mod section; diff --git a/src/components/comments/pending_item.rs b/src/components/comments/pending_item.rs index 4b711e6..3b09724 100644 --- a/src/components/comments/pending_item.rs +++ b/src/components/comments/pending_item.rs @@ -1,10 +1,24 @@ +//! 待审核评论项组件 +//! +//! 展示用户刚提交、尚未通过审核的评论占位项, +//! 视觉上使用较低的透明度并标注"审核中"状态。 + use dioxus::prelude::*; use crate::hooks::comment_storage::{render_pending_content, PendingComment}; +/// 待审核评论项组件。 +/// +/// Props: +/// - `comment`:待审核评论数据 +/// - `post_id`:所属文章 ID(当前未使用,保留用于未来扩展) +/// +/// 展示内容包括:作者头像/链接、"刚刚"时间占位、审核中徽章、Markdown 渲染内容。 +/// 深度最大展示 6 层缩进,孤儿评论深度会被修正为 0。 #[component] #[allow(unused_variables)] pub fn PendingCommentItem(comment: PendingComment, post_id: i32) -> Element { + // 孤儿评论(parent_id 为 None 但 depth > 0)按顶层展示 let depth = if comment.parent_id.is_none() && comment.depth > 0 { 0 } else { @@ -14,6 +28,7 @@ pub fn PendingCommentItem(comment: PendingComment, post_id: i32) -> Element { let indent = depth.min(6) * 24; let content_html = render_pending_content(&comment.content_md); + // 作者名展示为链接或普通文本 let author_element = match &comment.author_url { Some(url) if !url.is_empty() => rsx! { a { diff --git a/src/components/comments/section.rs b/src/components/comments/section.rs index 07b0c80..a300047 100644 --- a/src/components/comments/section.rs +++ b/src/components/comments/section.rs @@ -1,3 +1,8 @@ +//! 评论区段组件 +//! +//! 管理单篇文章的评论上下文(回复目标、刷新触发器、待审核评论), +//! 负责加载评论列表、轮询待审核评论状态并渲染表单与列表。 + use dioxus::prelude::*; use crate::api::comments::{check_pending_status, get_comments, CommentTreeResponse}; @@ -6,13 +11,32 @@ use crate::components::comments::list::CommentList; use crate::components::skeletons::comment_skeleton::CommentListSkeleton; use crate::hooks::comment_storage::{self, PendingComment}; +/// 评论上下文,供评论相关组件共享状态。 +/// +/// 字段: +/// - `active_reply`:当前正在回复的评论 ID +/// - `refresh_trigger`:刷新触发信号,切换时触发评论列表重新加载 +/// - `pending_comments`:本地存储的待审核评论 #[derive(Clone, Copy)] pub struct CommentContext { + /// 当前正在回复的评论 ID。 pub active_reply: Signal>, + /// 刷新触发信号,切换时触发评论列表重新加载。 pub refresh_trigger: Signal, + /// 本地存储的待审核评论。 pub pending_comments: Signal>, } +/// 评论区段组件。 +/// +/// Props: +/// - `post_id`:所属文章 ID +/// +/// 负责: +/// - 提供 `CommentContext` 上下文 +/// - 加载本地待审核评论并定期轮询其审核状态 +/// - 加载已审核评论列表并合并展示 +/// - 空评论时展示提示文案 #[component] pub fn CommentSection(post_id: i32) -> Element { let ctx = use_context_provider(|| { @@ -26,6 +50,7 @@ pub fn CommentSection(post_id: i32) -> Element { } }); + // 轮询待审核评论状态,已处理(非 pending)的评论从本地移除 use_future(move || { let mut pending = ctx.pending_comments; async move { @@ -46,12 +71,13 @@ pub fn CommentSection(post_id: i32) -> Element { } } Err(_e) => { - // Silently ignore on WASM; server-only logging not available + // 在 WASM 环境下静默忽略,服务器端日志不可用 } } } }); + // 评论数据资源,refresh_trigger 变化时自动重新加载 let comments_resource = use_resource(move || async move { let _ = (ctx.refresh_trigger)(); get_comments(post_id).await @@ -59,6 +85,7 @@ pub fn CommentSection(post_id: i32) -> Element { let data = comments_resource.read(); + // 根据加载结果渲染评论区、错误提示或骨架屏 match &*data { Some(Ok(CommentTreeResponse { comments, count })) => { let approved_count = *count; diff --git a/src/components/footer.rs b/src/components/footer.rs index c722d1e..3e3e9c8 100644 --- a/src/components/footer.rs +++ b/src/components/footer.rs @@ -1,12 +1,25 @@ +//! 页脚组件 +//! +//! 提供站点版权信息,并在用户向下滚动超过一屏后显示"回到顶部"悬浮按钮。 +//! 回到顶部的滚动监听与平滑滚动逻辑仅在 WASM 前端生效。 + use dioxus::prelude::*; use std::cell::RefCell; use std::rc::Rc; +/// 页脚与回到顶部按钮组件。 +/// +/// Props:无。 +/// 关键行为: +/// - 监听窗口滚动,超过一屏时显示回到顶部按钮 +/// - 点击按钮平滑滚动到顶部,并清理 URL 中的 `#` +/// - 滚动监听与平滑滚动仅在 `target_arch = "wasm32"` 下执行 #[component] #[allow(unused_mut)] pub fn Footer() -> Element { let mut visible = use_signal(|| false); + // WASM 下保存 scroll 事件闭包与 window,用于后续清理 #[cfg(target_arch = "wasm32")] let listener_state = use_hook(|| { Rc::new(RefCell::new( @@ -14,12 +27,14 @@ pub fn Footer() -> Element { )) }); + // 非 WASM 下保持类型一致,避免编译错误 #[cfg(not(target_arch = "wasm32"))] let _listener_state = use_hook(|| Rc::new(RefCell::new(None::<()>))); #[cfg(target_arch = "wasm32")] let listener_state_for_effect = listener_state.clone(); + // 挂载时注册 scroll 监听器,并根据当前滚动位置初始化按钮可见性 use_effect(move || { #[cfg(target_arch = "wasm32")] { @@ -56,6 +71,7 @@ pub fn Footer() -> Element { } }); + // 卸载时移除 scroll 监听器,防止内存泄漏 #[cfg(target_arch = "wasm32")] use_drop(move || { if let Some((closure, window)) = listener_state.borrow_mut().take() { @@ -66,6 +82,7 @@ pub fn Footer() -> Element { } }); + // 根据 visible 动态切换按钮显示/隐藏样式 let btn_class = use_memo(move || { let base = "fixed bottom-16 right-8 z-50 w-10 h-10 rounded-full bg-paper-entry border border-paper-border shadow-sm flex items-center justify-center cursor-pointer transition-all duration-300 text-paper-secondary hover:text-paper-accent"; if visible() { @@ -105,6 +122,9 @@ pub fn Footer() -> Element { } } +/// 平滑滚动到页面顶部,并清理 history 中的 `#` 哈希。 +/// +/// 仅在 `target_arch = "wasm32"` 下执行实际滚动,SSR 环境中为空操作。 fn scroll_to_top() { #[cfg(target_arch = "wasm32")] { diff --git a/src/components/forms.rs b/src/components/forms.rs index cc11c26..0dbc6ba 100644 --- a/src/components/forms.rs +++ b/src/components/forms.rs @@ -1,9 +1,24 @@ +//! 表单控件组件 +//! +//! 提供登录、注册、评论等页面共享的输入框、按钮与提示框样式常量与组件。 + use dioxus::prelude::*; +/// 输入框基础 CSS 类,统一文本框、邮箱框、URL 框等样式。 pub const INPUT_CLASS: &str = "w-full px-4 py-2 border border-paper-border rounded-lg bg-paper-entry text-paper-primary placeholder:text-paper-tertiary focus:outline-none focus:border-paper-accent focus:ring-1 focus:ring-paper-accent/30 transition-colors duration-200"; +/// 主按钮 CSS 类,用于表单提交等主操作按钮。 pub const BUTTON_PRIMARY_CLASS: &str = "w-full py-2.5 px-4 bg-paper-accent text-white font-medium rounded-full hover:brightness-110 active:scale-[0.98] transition-all duration-200 cursor-pointer"; +/// 表单输入框组件。 +/// +/// Props: +/// - `r#type`:input 类型(如 `"text"`、`"email"`、`"password"`) +/// - `placeholder`:占位提示文本 +/// - `value`:当前值 +/// - `disabled`:是否禁用 +/// - `oninput`:输入事件回调,返回新的字符串值 +/// - `onkeydown`:可选的键盘事件回调 #[component] pub fn FormInput( r#type: &'static str, @@ -35,6 +50,10 @@ pub fn FormInput( } } +/// 表单标签组件。 +/// +/// Props: +/// - `label`:标签文本 #[component] pub fn FormLabel(label: &'static str) -> Element { rsx! { @@ -44,6 +63,11 @@ pub fn FormLabel(label: &'static str) -> Element { } } +/// 提示框组件,用于显示成功、错误等状态消息。 +/// +/// Props: +/// - `message`:提示文本 +/// - `variant`:风格类型,支持 `"error"`、`"success"` 与其他默认类型 #[component] pub fn AlertBox(message: String, variant: &'static str) -> Element { let (bg_class, text_class) = match variant { diff --git a/src/components/frontend_layout.rs b/src/components/frontend_layout.rs index 77b0de8..bb4d7e3 100644 --- a/src/components/frontend_layout.rs +++ b/src/components/frontend_layout.rs @@ -1,3 +1,8 @@ +//! 前台布局组件 +//! +//! 包裹所有前台路由,提供统一的 Header、Footer 与主内容区容器, +//! 并为不同路由在 SuspenseBoundary 中展示对应的骨架屏。 + use dioxus::prelude::*; use crate::components::footer::Footer; @@ -12,6 +17,7 @@ use crate::components::skeletons::tags_skeleton::TagsSkeleton; use crate::router::Route; use crate::theme::ThemeToggle; +/// 根据当前前台路由选择对应的骨架屏组件。 fn route_skeleton(route: &Route) -> Element { match route { Route::Archives {} => rsx! { DelayedSkeleton { ArchiveSkeleton {} } }, @@ -23,6 +29,10 @@ fn route_skeleton(route: &Route) -> Element { } } +/// 前台整体布局组件。 +/// +/// 负责渲染 Header(含前台导航与主题切换)、主内容区与 Footer, +/// 并在路由内容加载过程中显示与路由匹配的骨架屏。 #[component] pub fn FrontendLayout() -> Element { let route = use_route::(); diff --git a/src/components/header.rs b/src/components/header.rs index 6459254..cff58c7 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -1,15 +1,34 @@ +//! 顶部导航栏组件 +//! +//! 提供站点 Logo、响应式导航菜单项与右侧自定义内容区, +//! 支持前台布局与后台布局复用。 + use dioxus::prelude::*; use dioxus::router::components::Link; use crate::router::Route; +/// 导航项配置,用于描述 Header 中的一个链接。 +/// +/// 字段: +/// - `route`:目标路由 +/// - `label`:显示文本 +/// - `is_active`:当前是否处于激活状态 #[derive(Clone, PartialEq)] pub struct NavItemConfig { + /// 目标路由。 pub route: Route, + /// 显示文本。 pub label: &'static str, + /// 当前是否处于激活状态。 pub is_active: bool, } +/// 顶部导航栏组件。 +/// +/// Props: +/// - `nav_items`:导航项列表 +/// - `right_content`:右侧自定义内容(如主题切换、登出按钮) #[component] pub fn Header(nav_items: Vec, right_content: Element) -> Element { rsx! { @@ -37,6 +56,12 @@ pub fn Header(nav_items: Vec, right_content: Element) -> Element } } +/// 单个导航项组件,根据 `is_active` 切换高亮样式。 +/// +/// Props: +/// - `route`:目标路由 +/// - `label`:显示文本 +/// - `is_active`:是否高亮 #[component] fn NavItem(route: Route, label: &'static str, is_active: bool) -> Element { let base_class = "px-3 py-1 text-base rounded-lg transition-all duration-200"; diff --git a/src/components/image_viewer.rs b/src/components/image_viewer.rs index c7875e5..ad03c1a 100644 --- a/src/components/image_viewer.rs +++ b/src/components/image_viewer.rs @@ -1,5 +1,22 @@ +//! 图片查看器组件 +//! +//! 提供缩略图展示与点击放大后的全屏灯箱(lightbox)查看, +//! 支持自定义缩略图参数、alt 文本与懒加载。 + use dioxus::prelude::*; +/// 图片查看器组件。 +/// +/// Props: +/// - `src`:原图 URL +/// - `thumb_params`:缩略图 URL 参数,默认 `"?w=800"` +/// - `alt`:图片替代文本,默认 `"图片"` +/// - `lazy_load`:是否启用懒加载,默认 `false` +/// +/// 关键事件: +/// - 点击缩略图:打开全屏灯箱 +/// - 点击遮罩或关闭按钮:关闭灯箱 +/// - 点击灯箱内容区:阻止事件冒泡,避免误关闭 #[component] pub fn ImageViewer( src: String, @@ -9,6 +26,7 @@ pub fn ImageViewer( ) -> Element { let mut is_open = use_signal(|| false); + // 拼接缩略图 URL:保留原 URL 的 query 参数并追加 thumb_params let thumb_src = if src.contains('?') { format!( "{}&{}", @@ -20,7 +38,7 @@ pub fn ImageViewer( }; rsx! { - // Thumbnail + // 缩略图 img { class: "cursor-pointer transition-opacity hover:opacity-90", src: "{thumb_src}", @@ -29,7 +47,7 @@ pub fn ImageViewer( onclick: move |_| is_open.set(true), } - // Full-screen lightbox + // 全屏灯箱 if is_open() { div { class: "image-viewer-overlay", diff --git a/src/components/mod.rs b/src/components/mod.rs index c7b7cc2..7a8704a 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,13 +1,33 @@ +//! 组件模块 +//! +//! 提供 Dioxus UI 组件,供前端页面(`src/pages/`)使用。 +//! 包括布局(`frontend_layout`、`admin_layout`)、导航(`header`、`nav`、`footer`)、 +//! 文章展示(`post`、`post_card`)、评论(`comments`)、骨架屏(`skeletons`)、 +//! 表单控件(`forms`)以及图片查看器(`image_viewer`)等共享组件。 + +/// 后台布局组件。 pub mod admin_layout; +/// 后台页面骨架屏组件。 pub mod admin_skeleton; +/// 评论相关组件。 pub mod comments; +/// 页脚组件。 pub mod footer; +/// 表单控件组件。 pub mod forms; +/// 前台布局组件。 pub mod frontend_layout; +/// 顶部导航栏组件。 pub mod header; +/// 图片查看器组件。 pub mod image_viewer; +/// 导航组件。 pub mod nav; +/// 文章详情组件。 pub mod post; +/// 文章卡片组件。 pub mod post_card; +/// 骨架屏组件集合。 pub mod skeletons; +/// 编辑器页面骨架屏组件。 pub mod write_skeleton; diff --git a/src/components/nav.rs b/src/components/nav.rs index 90fbf18..68e2a35 100644 --- a/src/components/nav.rs +++ b/src/components/nav.rs @@ -1,6 +1,16 @@ +//! 前台导航项配置 +//! +//! 根据当前路由生成前台 Header 所需的导航项列表。 + use crate::components::header::NavItemConfig; use crate::router::Route; +/// 生成前台导航项列表,当前访问的路由会被标记为激活。 +/// +/// 参数: +/// - `route`:当前路由 +/// +/// 返回:包含首页、归档、标签、搜索、关于的导航配置数组。 pub fn use_nav_items(route: Route) -> Vec { vec![ NavItemConfig { diff --git a/src/components/post/breadcrumbs.rs b/src/components/post/breadcrumbs.rs index 9cddaea..b04a4ea 100644 --- a/src/components/post/breadcrumbs.rs +++ b/src/components/post/breadcrumbs.rs @@ -1,8 +1,18 @@ +//! 面包屑组件 +//! +//! 在文章详情页展示从首页到当前文章标题的导航路径。 + use dioxus::prelude::*; use dioxus::router::components::Link; use crate::router::Route; +/// 面包屑导航组件。 +/// +/// Props: +/// - `title`:当前文章标题 +/// +/// 渲染 `Home > 当前标题` 的面包屑路径。 #[component] pub fn Breadcrumbs(title: String) -> Element { rsx! { diff --git a/src/components/post/mod.rs b/src/components/post/mod.rs index c12d412..a1ac6a6 100644 --- a/src/components/post/mod.rs +++ b/src/components/post/mod.rs @@ -1,8 +1,21 @@ +//! 文章详情组件模块 +//! +//! 提供文章详情页所需的各个子组件:封面、内容、页眉、元信息、页脚、面包屑、 +//! 上一篇/下一篇导航与目录。 + +/// 面包屑导航组件。 pub mod breadcrumbs; +/// 文章内容组件。 pub mod post_content; +/// 文章封面组件。 pub mod post_cover; +/// 文章页脚组件。 pub mod post_footer; +/// 文章页眉组件。 pub mod post_header; +/// 文章元信息组件。 pub mod post_meta; +/// 上一篇/下一篇导航组件。 pub mod post_nav_links; +/// 文章目录组件。 pub mod post_toc; diff --git a/src/components/post/post_content.rs b/src/components/post/post_content.rs index 70dc2a5..f19d4d8 100644 --- a/src/components/post/post_content.rs +++ b/src/components/post/post_content.rs @@ -1,5 +1,17 @@ +//! 文章内容组件 +//! +//! 渲染由服务端生成的文章 HTML 内容,并在 WASM 前端初始化交互脚本。 + use dioxus::prelude::*; +/// 文章内容组件。 +/// +/// Props: +/// - `content_html`:服务端渲染的文章 HTML 字符串 +/// +/// 关键行为: +/// - 在 `target_arch = "wasm32"` 环境下执行 `post-content.js` 并调用初始化函数, +/// 用于处理代码块、图片点击等前端交互 #[component] pub fn PostContent(content_html: String) -> Element { #[cfg(target_arch = "wasm32")] diff --git a/src/components/post/post_cover.rs b/src/components/post/post_cover.rs index 49e393f..988de85 100644 --- a/src/components/post/post_cover.rs +++ b/src/components/post/post_cover.rs @@ -1,7 +1,17 @@ +//! 文章封面组件 +//! +//! 在文章详情页渲染封面大图,使用图片查看器支持点击放大。 + use dioxus::prelude::*; use crate::components::image_viewer::ImageViewer; +/// 文章封面组件。 +/// +/// Props: +/// - `src`:封面原图 URL +/// +/// 使用 1200px 宽度的缩略图作为默认封面展示,点击可放大查看。 #[component] pub fn PostCover(src: String) -> Element { rsx! { diff --git a/src/components/post/post_footer.rs b/src/components/post/post_footer.rs index 43bb27f..f9f92ca 100644 --- a/src/components/post/post_footer.rs +++ b/src/components/post/post_footer.rs @@ -1,3 +1,7 @@ +//! 文章页脚组件 +//! +//! 展示文章标签、上一篇/下一篇导航与返回首页链接。 + use dioxus::prelude::*; use dioxus::router::components::Link; @@ -5,6 +9,15 @@ use crate::components::post::post_nav_links::PostNavLinks; use crate::models::post::Post; use crate::router::Route; +/// 文章页脚组件。 +/// +/// Props: +/// - `post`:文章数据模型 +/// +/// 展示内容包括: +/// - 文章标签云,链接到对应标签详情页 +/// - 相邻文章导航(如有) +/// - 返回首页链接 #[component] pub fn PostFooter(post: Post) -> Element { let tags = post.tags.clone(); diff --git a/src/components/post/post_header.rs b/src/components/post/post_header.rs index 47a2831..4d342da 100644 --- a/src/components/post/post_header.rs +++ b/src/components/post/post_header.rs @@ -1,9 +1,23 @@ +//! 文章页眉组件 +//! +//! 组合面包屑、标题、摘要与元信息,草稿状态会在标题旁显示草稿图标。 + use dioxus::prelude::*; use crate::components::post::breadcrumbs::Breadcrumbs; use crate::components::post::post_meta::PostMeta; use crate::models::post::{Post, PostStatus}; +/// 文章页眉组件。 +/// +/// Props: +/// - `post`:文章数据模型 +/// +/// 展示内容包括: +/// - 面包屑导航(Home → 文章标题) +/// - 文章标题,草稿状态附加草稿图标 +/// - 文章摘要(如有) +/// - 文章元信息 #[component] pub fn PostHeader(post: Post) -> Element { rsx! { diff --git a/src/components/post/post_meta.rs b/src/components/post/post_meta.rs index d579b61..f53bd21 100644 --- a/src/components/post/post_meta.rs +++ b/src/components/post/post_meta.rs @@ -1,7 +1,17 @@ +//! 文章元信息组件 +//! +//! 展示文章发布日期、阅读时长与字数统计。 + use dioxus::prelude::*; use crate::models::post::Post; +/// 文章元信息组件。 +/// +/// Props: +/// - `post`:文章数据模型 +/// +/// 渲染格式:`日期 · min read · words` #[component] pub fn PostMeta(post: Post) -> Element { rsx! { diff --git a/src/components/post/post_nav_links.rs b/src/components/post/post_nav_links.rs index 4023d14..eb70824 100644 --- a/src/components/post/post_nav_links.rs +++ b/src/components/post/post_nav_links.rs @@ -1,9 +1,20 @@ +//! 文章上一篇/下一篇导航组件 +//! +//! 在文章详情页底部提供相邻文章的快速跳转。 + use dioxus::prelude::*; use dioxus::router::components::Link; use crate::models::post::PostNav; use crate::router::Route; +/// 文章相邻导航组件。 +/// +/// Props: +/// - `prev`:上一篇文章摘要 +/// - `next`:下一篇文章摘要 +/// +/// 左右两侧分别渲染 Prev/Next 链接,若无相邻文章则占位空白。 #[component] pub fn PostNavLinks(prev: Option, next: Option) -> Element { rsx! { diff --git a/src/components/post/post_toc.rs b/src/components/post/post_toc.rs index ffb0fb3..8db37e0 100644 --- a/src/components/post/post_toc.rs +++ b/src/components/post/post_toc.rs @@ -1,5 +1,15 @@ +//! 文章目录组件 +//! +//! 在文章详情页渲染由服务端生成的目录 HTML,支持折叠展开。 + use dioxus::prelude::*; +/// 文章目录(Table of Contents)组件。 +/// +/// Props: +/// - `toc_html`:服务端生成的目录 HTML 字符串 +/// +/// 通过 `dangerous_inner_html` 注入目录结构,快捷键 `Alt + C` 可聚焦。 #[component] pub fn PostToc(toc_html: String) -> Element { rsx! { diff --git a/src/components/post_card.rs b/src/components/post_card.rs index d3588dd..452d204 100644 --- a/src/components/post_card.rs +++ b/src/components/post_card.rs @@ -1,3 +1,7 @@ +//! 文章卡片组件 +//! +//! 在首页、标签详情等列表中展示单篇文章的标题、摘要、封面、日期与标签。 + use dioxus::prelude::*; use dioxus::router::components::Link; @@ -5,6 +9,19 @@ use crate::components::image_viewer::ImageViewer; use crate::models::post::Post; use crate::router::Route; +/// 文章卡片组件。 +/// +/// Props: +/// - `post`:文章数据模型 +/// +/// 展示内容包括: +/// - 封面图(如有,使用 400x300 缩略图) +/// - 文章标题 +/// - 摘要(最多两行) +/// - 发布日期与标签 +/// +/// 关键事件: +/// - 点击标签时阻止事件冒泡,避免触发整卡跳转 #[component] pub fn PostCard(post: Post) -> Element { let post_slug = post.slug.clone(); diff --git a/src/components/skeletons/archive_skeleton.rs b/src/components/skeletons/archive_skeleton.rs index f50badd..17e2dd6 100644 --- a/src/components/skeletons/archive_skeleton.rs +++ b/src/components/skeletons/archive_skeleton.rs @@ -1,9 +1,14 @@ +//! 归档页骨架屏 +//! +//! 在归档数据加载期间展示按年份/月份分组的文章列表占位。 + use crate::components::skeletons::atoms::*; use dioxus::prelude::*; -/// 归档页骨架屏 -/// 结构:统计行("共 N 篇文章") + 年份标题 + 月份标题 + 文章条目列表 -/// 模拟 2 个年份,每个年份 2 个月,每个月 3 篇文章 +/// 归档页骨架屏组件。 +/// +/// 结构:统计行("共 N 篇文章")+ 年份标题 + 月份标题 + 文章条目列表。 +/// 模拟 2 个年份,每个年份 2 个月,每个月 3 篇文章。 #[component] pub fn ArchiveSkeleton() -> Element { rsx! { diff --git a/src/components/skeletons/atoms.rs b/src/components/skeletons/atoms.rs index 21db56e..1d640a3 100644 --- a/src/components/skeletons/atoms.rs +++ b/src/components/skeletons/atoms.rs @@ -1,5 +1,16 @@ +//! 骨架屏原子组件 +//! +//! 提供通用的脉冲动画占位块,供各页面骨架屏组合使用。 + use dioxus::prelude::*; +/// 通用骨架占位块。 +/// +/// Props: +/// - `class`:Tailwind CSS 类,控制尺寸与形状 +/// - `style`:可选的内联样式字符串 +/// +/// 默认带有 `animate-pulse` 动画与半透明的占位背景。 #[component] pub fn SkeletonBox(class: &'static str, style: Option<&'static str>) -> Element { rsx! { diff --git a/src/components/skeletons/comment_skeleton.rs b/src/components/skeletons/comment_skeleton.rs index 38fab18..08109ac 100644 --- a/src/components/skeletons/comment_skeleton.rs +++ b/src/components/skeletons/comment_skeleton.rs @@ -1,6 +1,13 @@ +//! 评论列表骨架屏 +//! +//! 在评论数据加载期间展示评论条目占位。 + use crate::components::skeletons::atoms::*; use dioxus::prelude::*; +/// 评论列表骨架屏组件。 +/// +/// 渲染一个标题占位与若干条评论条目占位,包含头像、作者名与内容行。 #[component] pub fn CommentListSkeleton() -> Element { rsx! { diff --git a/src/components/skeletons/delayed_skeleton.rs b/src/components/skeletons/delayed_skeleton.rs index 5b23455..4d42958 100644 --- a/src/components/skeletons/delayed_skeleton.rs +++ b/src/components/skeletons/delayed_skeleton.rs @@ -1,21 +1,28 @@ +//! 延迟骨架屏包装组件 +//! +//! 让骨架屏先以静态灰色块立即显示,延迟一段时间后再启动 pulse 动画, +//! 避免快速加载时动画一闪而过带来的视觉抖动。 + use crate::utils::time::sleep_ms; use dioxus::prelude::*; -/// 骨架屏 pulse 动画延迟(毫秒) -/// 加载时间低于此值时骨架屏只显示静态灰色块,避免 pulse 动画一闪而过 +/// 骨架屏 pulse 动画延迟(毫秒)。 +/// +/// 加载时间低于此值时骨架屏只显示静态灰色块,避免 pulse 动画一闪而过。 const SKELETON_PULSE_DELAY_MS: u32 = 200; -/// 延迟 pulse 动画的骨架屏包装组件 +/// 延迟 pulse 动画的骨架屏包装组件。 /// /// 骨架屏区域**立即显示**(灰色静态占位块),避免空白闪烁。 -/// 延迟 SKELETON_PULSE_DELAY_MS 毫秒后,如果仍在加载,则启动 pulse 动画。 +/// 延迟 `SKELETON_PULSE_DELAY_MS` 毫秒后,如果仍在加载,则启动 pulse 动画。 /// -/// 快加载(< 200ms):用户只看到静态灰色块一闪而过,无动画感知 -/// 慢加载:灰色块正常 pulse,提示正在加载 +/// 快加载(< 200ms):用户只看到静态灰色块一闪而过,无动画感知。 +/// 慢加载:灰色块正常 pulse,提示正在加载。 #[component] pub fn DelayedSkeleton(children: Element) -> Element { let mut pulsing = use_signal(|| false); + // 延迟启动 pulse 动画 use_effect(move || { spawn(async move { sleep_ms(SKELETON_PULSE_DELAY_MS).await; diff --git a/src/components/skeletons/home_skeleton.rs b/src/components/skeletons/home_skeleton.rs index c0f6f8f..b21e3f5 100644 --- a/src/components/skeletons/home_skeleton.rs +++ b/src/components/skeletons/home_skeleton.rs @@ -1,10 +1,15 @@ +//! 首页骨架屏 +//! +//! 模拟首页文章卡片列表与分页区域。 + use dioxus::prelude::*; use crate::components::skeletons::atoms::SkeletonBox; use crate::components::skeletons::post_card_skeleton::PostCardSkeleton; -/// 首页骨架屏 - 模拟文章卡片列表 + 分页区域 -/// 显示 5 个文章卡片骨架 + 分页按钮占位 +/// 首页骨架屏组件。 +/// +/// 显示 5 个文章卡片骨架与分页按钮占位。 #[component] pub fn HomeSkeleton() -> Element { rsx! { diff --git a/src/components/skeletons/mod.rs b/src/components/skeletons/mod.rs index 2b73c58..43d322a 100644 --- a/src/components/skeletons/mod.rs +++ b/src/components/skeletons/mod.rs @@ -1,10 +1,24 @@ +//! 骨架屏组件模块 +//! +//! 提供各页面在数据加载期间的占位组件,以及通用骨架原子组件。 + +/// 归档页面骨架屏组件。 pub mod archive_skeleton; +/// 通用骨架原子组件。 pub mod atoms; +/// 评论区骨架屏组件。 pub mod comment_skeleton; +/// 延迟显示骨架屏组件。 pub mod delayed_skeleton; +/// 首页骨架屏组件。 pub mod home_skeleton; +/// 文章卡片骨架屏组件。 pub mod post_card_skeleton; +/// 文章详情页骨架屏组件。 pub mod post_detail_skeleton; +/// 文章列表页骨架屏组件。 pub mod posts_skeleton; +/// 搜索页骨架屏组件。 pub mod search_skeleton; +/// 标签页骨架屏组件。 pub mod tags_skeleton; diff --git a/src/components/skeletons/post_card_skeleton.rs b/src/components/skeletons/post_card_skeleton.rs index 9bba741..4438581 100644 --- a/src/components/skeletons/post_card_skeleton.rs +++ b/src/components/skeletons/post_card_skeleton.rs @@ -1,9 +1,14 @@ +//! 文章卡片骨架屏 +//! +//! 模拟 PostCard 组件的视觉占位,用于列表页加载。 + use dioxus::prelude::*; use crate::components::skeletons::atoms::SkeletonBox; -/// 文章卡片骨架屏 - 模拟 PostCard 的视觉结构 -/// 包含:标题行(24px bold) + 摘要2行 + 元信息行(日期+标签) +/// 文章卡片骨架屏组件。 +/// +/// 包含:标题行(24px bold) + 摘要两行 + 元信息行(日期+标签)。 #[component] pub fn PostCardSkeleton() -> Element { rsx! { diff --git a/src/components/skeletons/post_detail_skeleton.rs b/src/components/skeletons/post_detail_skeleton.rs index 3b13a6c..d03fc8a 100644 --- a/src/components/skeletons/post_detail_skeleton.rs +++ b/src/components/skeletons/post_detail_skeleton.rs @@ -1,8 +1,13 @@ +//! 文章详情页骨架屏 +//! +//! 在文章详情数据加载期间展示面包屑、标题、摘要、元信息、封面与正文占位。 + use crate::components::skeletons::atoms::*; use dioxus::prelude::*; -/// 文章详情页骨架屏 -/// 结构:面包屑 + 标题(h1) + 摘要 + 元信息 + 封面图 + 正文(多段) + Footer +/// 文章详情页骨架屏组件。 +/// +/// 结构:面包屑 + 标题(h1) + 摘要 + 元信息 + 封面图 + 正文(多段) + 页脚占位。 #[component] pub fn PostDetailSkeleton() -> Element { rsx! { diff --git a/src/components/skeletons/posts_skeleton.rs b/src/components/skeletons/posts_skeleton.rs index 9faa395..d8920b8 100644 --- a/src/components/skeletons/posts_skeleton.rs +++ b/src/components/skeletons/posts_skeleton.rs @@ -1,7 +1,14 @@ +//! 后台文章管理列表骨架屏 +//! +//! 模拟后台 Posts 页面的表格结构。 + use dioxus::prelude::*; use crate::components::skeletons::atoms::SkeletonBox; +/// 后台文章管理列表骨架屏组件。 +/// +/// 渲染带表头的表格占位,包含 10 行数据行。 #[component] pub fn PostsSkeleton() -> Element { rsx! { diff --git a/src/components/skeletons/search_skeleton.rs b/src/components/skeletons/search_skeleton.rs index 2809de8..3bd8fa1 100644 --- a/src/components/skeletons/search_skeleton.rs +++ b/src/components/skeletons/search_skeleton.rs @@ -1,9 +1,14 @@ +//! 搜索页骨架屏 +//! +//! 在搜索结果加载期间展示搜索卡片列表占位。 + use dioxus::prelude::*; use crate::components::skeletons::post_card_skeleton::PostCardSkeleton; -/// 搜索页骨架屏 - 搜索结果卡片列表 -/// 模拟 3 个搜索结果卡片(与搜索页现有内联骨架结构一致) +/// 搜索页骨架屏组件。 +/// +/// 模拟 3 个搜索结果卡片,与搜索页现有内联骨架结构一致。 #[component] pub fn SearchSkeleton() -> Element { rsx! { diff --git a/src/components/skeletons/tags_skeleton.rs b/src/components/skeletons/tags_skeleton.rs index 439705e..dd572ba 100644 --- a/src/components/skeletons/tags_skeleton.rs +++ b/src/components/skeletons/tags_skeleton.rs @@ -1,10 +1,15 @@ +//! 标签相关骨架屏 +//! +//! 提供标签列表页与标签详情页的加载占位组件。 + use dioxus::prelude::*; use crate::components::skeletons::atoms::SkeletonBox; use crate::components::skeletons::post_card_skeleton::PostCardSkeleton; -/// 标签列表页骨架屏 -/// 结构:统计行("共 N 个标签,M 篇文章") + 标签云(flex wrap 的 pill 列表) +/// 标签列表页骨架屏组件。 +/// +/// 结构:统计行("共 N 个标签,M 篇文章")+ 标签云(flex wrap 的 pill 列表)。 #[component] pub fn TagsSkeleton() -> Element { rsx! { @@ -35,7 +40,9 @@ pub fn TagsSkeleton() -> Element { } } -/// 标签详情页骨架屏 - 与首页文章列表结构相同 +/// 标签详情页骨架屏组件。 +/// +/// 结构与首页文章列表相同,包含统计行与文章卡片骨架。 #[component] pub fn TagDetailSkeleton() -> Element { rsx! { diff --git a/src/components/write_skeleton.rs b/src/components/write_skeleton.rs index 018fa63..119b653 100644 --- a/src/components/write_skeleton.rs +++ b/src/components/write_skeleton.rs @@ -1,6 +1,13 @@ +//! 文章编辑器页骨架屏 +//! +//! 在写文章/编辑文章页面加载时展示,模拟标题、元信息、编辑器工具栏与正文区域。 + use crate::components::skeletons::atoms::*; use dioxus::prelude::*; +/// 文章编辑器页骨架屏组件。 +/// +/// 模拟 Write 页面的整体结构:顶部标题与元信息、中间编辑器区域、底部操作按钮。 #[component] pub fn WriteSkeleton() -> Element { rsx! {