docs(components): 补充中文注释

This commit is contained in:
xfy 2026-06-12 19:38:51 +08:00
parent c5d1eb117c
commit c43da3676f
38 changed files with 500 additions and 24 deletions

View File

@ -1,3 +1,8 @@
//! 后台管理布局组件
//!
//! 包裹所有后台路由,提供管理员专属导航、登录校验、主题切换与登出入口。
//! 在未完成身份校验前显示与真实布局结构一致的骨架屏,避免切换闪烁。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::api::auth::{get_current_user, logout}; 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::router::Route;
use crate::theme::ThemeToggle; use crate::theme::ThemeToggle;
/// 后台管理整体布局组件。
///
/// 负责:
/// - 通过 `get_current_user` 校验登录状态,未登录时跳转登录页
/// - 渲染顶部导航(仪表盘、写文章、管理文章)与主题切换/登出按钮
/// - 根据当前路由切换主区域样式Write 路由固定高度,其他路由可滚动)
/// - 校验完成前使用骨架屏保持布局稳定
#[component] #[component]
pub fn AdminLayout() -> Element { pub fn AdminLayout() -> Element {
let mut ctx: UserContext = use_context(); let mut ctx: UserContext = use_context();
@ -17,6 +29,7 @@ pub fn AdminLayout() -> Element {
let route = use_route::<Route>(); let route = use_route::<Route>();
let show_skeleton = use_delayed_loading(move || !(ctx.checked)()); let show_skeleton = use_delayed_loading(move || !(ctx.checked)());
// 仅在首次挂载时执行一次登录校验
use_effect(move || { use_effect(move || {
if !(ctx.checked)() { if !(ctx.checked)() {
(ctx.checked).set(true); (ctx.checked).set(true);
@ -37,6 +50,7 @@ pub fn AdminLayout() -> Element {
} }
}); });
// 后台导航项,当前路由高亮
let admin_nav_items = vec![ let admin_nav_items = vec![
NavItemConfig { NavItemConfig {
route: Route::Admin {}, route: Route::Admin {},
@ -55,6 +69,7 @@ pub fn AdminLayout() -> Element {
}, },
]; ];
// 右侧操作区:主题切换 + 登出按钮
let right_content = rsx! { let right_content = rsx! {
div { class: "flex items-center gap-3", div { class: "flex items-center gap-3",
ThemeToggle {} ThemeToggle {}
@ -88,6 +103,7 @@ pub fn AdminLayout() -> Element {
"min-h-screen flex flex-col bg-white dark:bg-[#1d1e20]" "min-h-screen flex flex-col bg-white dark:bg-[#1d1e20]"
}; };
// 根据校验状态与用户状态渲染真实布局、跳转提示或骨架屏
match ((ctx.checked)(), (ctx.user)()) { match ((ctx.checked)(), (ctx.user)()) {
(true, Some(_)) => { (true, Some(_)) => {
rsx! { rsx! {

View File

@ -1,8 +1,15 @@
//! 后台仪表盘骨架屏
//!
//! 仅在 AdminLayout 的内容区展示,不包含 Header 与 Footer
//! 用于校验登录状态期间保持视觉稳定。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::skeletons::atoms::SkeletonBox; use crate::components::skeletons::atoms::SkeletonBox;
/// 仅仪表盘内容区骨架(不含 header/footer /// 仪表盘内容区骨架屏组件(不含 header/footer
///
/// 包含统计卡片、快捷操作按钮与最近文章列表三组占位块。
#[component] #[component]
pub fn AdminDashboardSkeleton() -> Element { pub fn AdminDashboardSkeleton() -> Element {
rsx! { rsx! {

View File

@ -1,8 +1,21 @@
//! 评论管理操作组件
//!
//! 在后台管理文章评论时,提供通过、标记垃圾、删除(移入回收站)三种操作按钮。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::api::comments::{approve_comment, spam_comment, trash_comment}; use crate::api::comments::{approve_comment, spam_comment, trash_comment};
use crate::components::comments::section::CommentContext; use crate::components::comments::section::CommentContext;
/// 评论管理操作按钮组件。
///
/// Props
/// - `comment_id`:目标评论 ID
/// - `post_id`:所属文章 ID当前未使用保留用于未来扩展
///
/// 关键事件:
/// - 点击"通过"/"垃圾"/"删除"按钮后调用对应 API操作完成后触发评论列表刷新
/// - 操作期间禁用按钮,防止重复提交
#[component] #[component]
pub fn CommentActions(comment_id: i64, post_id: i32) -> Element { pub fn CommentActions(comment_id: i64, post_id: i32) -> Element {
let ctx: CommentContext = use_context(); let ctx: CommentContext = use_context();

View File

@ -1,3 +1,7 @@
//! 评论表单组件
//!
//! 提供发表评论与回复评论的表单,包含昵称、邮箱、网站、内容与反垃圾蜜罐字段。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::api::comments::create_comment; 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::components::forms::{AlertBox, BUTTON_PRIMARY_CLASS, INPUT_CLASS};
use crate::hooks::comment_storage::{self, PendingComment}; use crate::hooks::comment_storage::{self, PendingComment};
/// 评论表单组件,用于顶层评论或回复评论。
///
/// Props
/// - `post_id`:所属文章 ID
/// - `parent_id`:回复目标评论 ID`None` 表示顶层评论
///
/// 关键事件:
/// - 挂载时从本地存储恢复上次填写的作者信息
/// - 提交时校验必填项与蜜罐字段
/// - 提交成功后清空内容、保存作者信息、添加待审核评论并触发列表刷新
#[component] #[component]
pub fn CommentForm(post_id: i32, parent_id: Option<i64>) -> Element { pub fn CommentForm(post_id: i32, parent_id: Option<i64>) -> Element {
let ctx: CommentContext = use_context(); let ctx: CommentContext = use_context();
@ -21,6 +35,7 @@ pub fn CommentForm(post_id: i32, parent_id: Option<i64>) -> Element {
let mut message = use_signal(|| Option::<(String, &'static str)>::None); let mut message = use_signal(|| Option::<(String, &'static str)>::None);
let mut loaded = use_signal(|| false); let mut loaded = use_signal(|| false);
// 首次挂载时从本地存储加载作者信息
use_effect(move || { use_effect(move || {
if loaded() { if loaded() {
return; return;
@ -33,6 +48,7 @@ pub fn CommentForm(post_id: i32, parent_id: Option<i64>) -> Element {
} }
}); });
// 回复表单:当前未激活回复时隐藏
if let Some(pid) = parent_id { if let Some(pid) = parent_id {
if active_reply() != Some(pid) { if active_reply() != Some(pid) {
return rsx! {}; return rsx! {};
@ -107,6 +123,7 @@ pub fn CommentForm(post_id: i32, parent_id: Option<i64>) -> Element {
"支持 Markdown 语法" "支持 Markdown 语法"
} }
// 蜜罐字段:对普通用户隐藏,用于拦截简单机器人
textarea { textarea {
class: "hidden", class: "hidden",
aria_hidden: "true", aria_hidden: "true",
@ -131,6 +148,7 @@ pub fn CommentForm(post_id: i32, parent_id: Option<i64>) -> Element {
let content = content_md(); let content = content_md();
let hp = honeypot(); let hp = honeypot();
// 蜜罐被填充则直接丢弃
if !hp.is_empty() { if !hp.is_empty() {
return; return;
} }

View File

@ -1,15 +1,29 @@
//! 单条评论项组件
//!
//! 展示已审核通过的评论,支持展开/收起回复表单。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::comments::form::CommentForm; use crate::components::comments::form::CommentForm;
use crate::components::comments::section::CommentContext; use crate::components::comments::section::CommentContext;
use crate::models::comment::PublicComment; use crate::models::comment::PublicComment;
/// 单条已审核评论组件。
///
/// Props
/// - `comment`:已审核评论数据
/// - `post_id`:所属文章 ID
///
/// 关键行为:
/// - 点击"回复"按钮切换该评论下方的回复表单
/// - 最大递归深度限制为 20超过后隐藏回复按钮
#[component] #[component]
pub fn CommentItem(comment: PublicComment, post_id: i32) -> Element { pub fn CommentItem(comment: PublicComment, post_id: i32) -> Element {
let ctx: CommentContext = use_context(); let ctx: CommentContext = use_context();
let mut active_reply = ctx.active_reply; let mut active_reply = ctx.active_reply;
let refresh_trigger = ctx.refresh_trigger; let refresh_trigger = ctx.refresh_trigger;
// 孤儿评论按顶层展示
let depth = if comment.parent_id.is_none() && comment.depth > 0 { let depth = if comment.parent_id.is_none() && comment.depth > 0 {
0 0
} else { } else {
@ -23,6 +37,7 @@ pub fn CommentItem(comment: PublicComment, post_id: i32) -> Element {
let _ = refresh_trigger; let _ = refresh_trigger;
// 作者名展示为链接或普通文本
let author_element = match &comment.author_url { let author_element = match &comment.author_url {
Some(url) if !url.is_empty() => rsx! { Some(url) if !url.is_empty() => rsx! {
a { a {

View File

@ -1,3 +1,7 @@
//! 评论列表组件
//!
//! 将已审核评论与待审核评论合并成一棵树并按时间排序渲染。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::comments::item::CommentItem; 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::hooks::comment_storage::PendingComment;
use crate::models::comment::PublicComment; use crate::models::comment::PublicComment;
/// 合并后的评论节点,可能是已审核或待审核评论。
#[derive(Clone)] #[derive(Clone)]
enum MergedComment { enum MergedComment {
Approved(PublicComment), Approved(PublicComment),
Pending(PendingComment), Pending(PendingComment),
} }
/// 合并两类评论并构建成树形结构。
///
/// 处理逻辑:
/// - 将已审核与待审核评论统一为 `MergedComment`
/// - 若某条评论的 parent_id 不存在于当前集合中,则视为顶层评论
/// - 同一父节点下的子评论按时间排序
/// - 使用 DFS 前序遍历生成最终展示顺序
fn merge_and_treeify( fn merge_and_treeify(
approved: Vec<PublicComment>, approved: Vec<PublicComment>,
pending: Vec<PendingComment>, pending: Vec<PendingComment>,
@ -31,6 +43,7 @@ fn merge_and_treeify(
}) })
.collect(); .collect();
// 按 parent_id 分组,处理指向不存在父节点的 parent_id
let mut children_map: HashMap<Option<i64>, Vec<MergedComment>> = HashMap::new(); let mut children_map: HashMap<Option<i64>, Vec<MergedComment>> = HashMap::new();
for comment in all { for comment in all {
let parent_id = match &comment { let parent_id = match &comment {
@ -47,6 +60,7 @@ fn merge_and_treeify(
.push(comment); .push(comment);
} }
// 每个父节点下的子评论按创建时间排序
for children in children_map.values_mut() { for children in children_map.values_mut() {
children.sort_by(|a, b| { children.sort_by(|a, b| {
let time_a = match a { let time_a = match a {
@ -61,6 +75,7 @@ fn merge_and_treeify(
}); });
} }
// 深度优先遍历生成树形顺序
fn dfs( fn dfs(
parent_id: Option<i64>, parent_id: Option<i64>,
children_map: &HashMap<Option<i64>, Vec<MergedComment>>, children_map: &HashMap<Option<i64>, Vec<MergedComment>>,
@ -83,6 +98,14 @@ fn merge_and_treeify(
result result
} }
/// 评论列表组件。
///
/// Props
/// - `comments`:已审核评论列表
/// - `pending`:待审核评论列表
/// - `post_id`:所属文章 ID
///
/// 根据两类评论构建合并树,依次渲染为 `CommentItem` 或 `PendingCommentItem`。
#[component] #[component]
pub fn CommentList( pub fn CommentList(
comments: Vec<PublicComment>, comments: Vec<PublicComment>,

View File

@ -1,6 +1,16 @@
//! 评论组件模块
//!
//! 提供评论相关组件:评论列表、单条评论、待审核评论、评论表单、评论区段与管理操作。
/// 评论操作按钮组件。
pub mod actions; pub mod actions;
/// 评论表单组件。
pub mod form; pub mod form;
/// 单条评论组件。
pub mod item; pub mod item;
/// 评论列表组件。
pub mod list; pub mod list;
/// 待审核评论组件。
pub mod pending_item; pub mod pending_item;
/// 评论区段组件。
pub mod section; pub mod section;

View File

@ -1,10 +1,24 @@
//! 待审核评论项组件
//!
//! 展示用户刚提交、尚未通过审核的评论占位项,
//! 视觉上使用较低的透明度并标注"审核中"状态。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::hooks::comment_storage::{render_pending_content, PendingComment}; use crate::hooks::comment_storage::{render_pending_content, PendingComment};
/// 待审核评论项组件。
///
/// Props
/// - `comment`:待审核评论数据
/// - `post_id`:所属文章 ID当前未使用保留用于未来扩展
///
/// 展示内容包括:作者头像/链接、"刚刚"时间占位、审核中徽章、Markdown 渲染内容。
/// 深度最大展示 6 层缩进,孤儿评论深度会被修正为 0。
#[component] #[component]
#[allow(unused_variables)] #[allow(unused_variables)]
pub fn PendingCommentItem(comment: PendingComment, post_id: i32) -> Element { 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 { let depth = if comment.parent_id.is_none() && comment.depth > 0 {
0 0
} else { } else {
@ -14,6 +28,7 @@ pub fn PendingCommentItem(comment: PendingComment, post_id: i32) -> Element {
let indent = depth.min(6) * 24; let indent = depth.min(6) * 24;
let content_html = render_pending_content(&comment.content_md); let content_html = render_pending_content(&comment.content_md);
// 作者名展示为链接或普通文本
let author_element = match &comment.author_url { let author_element = match &comment.author_url {
Some(url) if !url.is_empty() => rsx! { Some(url) if !url.is_empty() => rsx! {
a { a {

View File

@ -1,3 +1,8 @@
//! 评论区段组件
//!
//! 管理单篇文章的评论上下文(回复目标、刷新触发器、待审核评论),
//! 负责加载评论列表、轮询待审核评论状态并渲染表单与列表。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::api::comments::{check_pending_status, get_comments, CommentTreeResponse}; 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::components::skeletons::comment_skeleton::CommentListSkeleton;
use crate::hooks::comment_storage::{self, PendingComment}; use crate::hooks::comment_storage::{self, PendingComment};
/// 评论上下文,供评论相关组件共享状态。
///
/// 字段:
/// - `active_reply`:当前正在回复的评论 ID
/// - `refresh_trigger`:刷新触发信号,切换时触发评论列表重新加载
/// - `pending_comments`:本地存储的待审核评论
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct CommentContext { pub struct CommentContext {
/// 当前正在回复的评论 ID。
pub active_reply: Signal<Option<i64>>, pub active_reply: Signal<Option<i64>>,
/// 刷新触发信号,切换时触发评论列表重新加载。
pub refresh_trigger: Signal<bool>, pub refresh_trigger: Signal<bool>,
/// 本地存储的待审核评论。
pub pending_comments: Signal<Vec<PendingComment>>, pub pending_comments: Signal<Vec<PendingComment>>,
} }
/// 评论区段组件。
///
/// Props
/// - `post_id`:所属文章 ID
///
/// 负责:
/// - 提供 `CommentContext` 上下文
/// - 加载本地待审核评论并定期轮询其审核状态
/// - 加载已审核评论列表并合并展示
/// - 空评论时展示提示文案
#[component] #[component]
pub fn CommentSection(post_id: i32) -> Element { pub fn CommentSection(post_id: i32) -> Element {
let ctx = use_context_provider(|| { let ctx = use_context_provider(|| {
@ -26,6 +50,7 @@ pub fn CommentSection(post_id: i32) -> Element {
} }
}); });
// 轮询待审核评论状态,已处理(非 pending的评论从本地移除
use_future(move || { use_future(move || {
let mut pending = ctx.pending_comments; let mut pending = ctx.pending_comments;
async move { async move {
@ -46,12 +71,13 @@ pub fn CommentSection(post_id: i32) -> Element {
} }
} }
Err(_e) => { Err(_e) => {
// Silently ignore on WASM; server-only logging not available // 在 WASM 环境下静默忽略,服务器端日志不可用
} }
} }
} }
}); });
// 评论数据资源refresh_trigger 变化时自动重新加载
let comments_resource = use_resource(move || async move { let comments_resource = use_resource(move || async move {
let _ = (ctx.refresh_trigger)(); let _ = (ctx.refresh_trigger)();
get_comments(post_id).await get_comments(post_id).await
@ -59,6 +85,7 @@ pub fn CommentSection(post_id: i32) -> Element {
let data = comments_resource.read(); let data = comments_resource.read();
// 根据加载结果渲染评论区、错误提示或骨架屏
match &*data { match &*data {
Some(Ok(CommentTreeResponse { comments, count })) => { Some(Ok(CommentTreeResponse { comments, count })) => {
let approved_count = *count; let approved_count = *count;

View File

@ -1,12 +1,25 @@
//! 页脚组件
//!
//! 提供站点版权信息,并在用户向下滚动超过一屏后显示"回到顶部"悬浮按钮。
//! 回到顶部的滚动监听与平滑滚动逻辑仅在 WASM 前端生效。
use dioxus::prelude::*; use dioxus::prelude::*;
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
/// 页脚与回到顶部按钮组件。
///
/// Props无。
/// 关键行为:
/// - 监听窗口滚动,超过一屏时显示回到顶部按钮
/// - 点击按钮平滑滚动到顶部,并清理 URL 中的 `#`
/// - 滚动监听与平滑滚动仅在 `target_arch = "wasm32"` 下执行
#[component] #[component]
#[allow(unused_mut)] #[allow(unused_mut)]
pub fn Footer() -> Element { pub fn Footer() -> Element {
let mut visible = use_signal(|| false); let mut visible = use_signal(|| false);
// WASM 下保存 scroll 事件闭包与 window用于后续清理
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
let listener_state = use_hook(|| { let listener_state = use_hook(|| {
Rc::new(RefCell::new( Rc::new(RefCell::new(
@ -14,12 +27,14 @@ pub fn Footer() -> Element {
)) ))
}); });
// 非 WASM 下保持类型一致,避免编译错误
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
let _listener_state = use_hook(|| Rc::new(RefCell::new(None::<()>))); let _listener_state = use_hook(|| Rc::new(RefCell::new(None::<()>)));
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
let listener_state_for_effect = listener_state.clone(); let listener_state_for_effect = listener_state.clone();
// 挂载时注册 scroll 监听器,并根据当前滚动位置初始化按钮可见性
use_effect(move || { use_effect(move || {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
@ -56,6 +71,7 @@ pub fn Footer() -> Element {
} }
}); });
// 卸载时移除 scroll 监听器,防止内存泄漏
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use_drop(move || { use_drop(move || {
if let Some((closure, window)) = listener_state.borrow_mut().take() { 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 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"; 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() { if visible() {
@ -105,6 +122,9 @@ pub fn Footer() -> Element {
} }
} }
/// 平滑滚动到页面顶部,并清理 history 中的 `#` 哈希。
///
/// 仅在 `target_arch = "wasm32"` 下执行实际滚动SSR 环境中为空操作。
fn scroll_to_top() { fn scroll_to_top() {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {

View File

@ -1,9 +1,24 @@
//! 表单控件组件
//!
//! 提供登录、注册、评论等页面共享的输入框、按钮与提示框样式常量与组件。
use dioxus::prelude::*; 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"; 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"; 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] #[component]
pub fn FormInput( pub fn FormInput(
r#type: &'static str, r#type: &'static str,
@ -35,6 +50,10 @@ pub fn FormInput(
} }
} }
/// 表单标签组件。
///
/// Props
/// - `label`:标签文本
#[component] #[component]
pub fn FormLabel(label: &'static str) -> Element { pub fn FormLabel(label: &'static str) -> Element {
rsx! { rsx! {
@ -44,6 +63,11 @@ pub fn FormLabel(label: &'static str) -> Element {
} }
} }
/// 提示框组件,用于显示成功、错误等状态消息。
///
/// Props
/// - `message`:提示文本
/// - `variant`:风格类型,支持 `"error"`、`"success"` 与其他默认类型
#[component] #[component]
pub fn AlertBox(message: String, variant: &'static str) -> Element { pub fn AlertBox(message: String, variant: &'static str) -> Element {
let (bg_class, text_class) = match variant { let (bg_class, text_class) = match variant {

View File

@ -1,3 +1,8 @@
//! 前台布局组件
//!
//! 包裹所有前台路由,提供统一的 Header、Footer 与主内容区容器,
//! 并为不同路由在 SuspenseBoundary 中展示对应的骨架屏。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::footer::Footer; use crate::components::footer::Footer;
@ -12,6 +17,7 @@ use crate::components::skeletons::tags_skeleton::TagsSkeleton;
use crate::router::Route; use crate::router::Route;
use crate::theme::ThemeToggle; use crate::theme::ThemeToggle;
/// 根据当前前台路由选择对应的骨架屏组件。
fn route_skeleton(route: &Route) -> Element { fn route_skeleton(route: &Route) -> Element {
match route { match route {
Route::Archives {} => rsx! { DelayedSkeleton { ArchiveSkeleton {} } }, Route::Archives {} => rsx! { DelayedSkeleton { ArchiveSkeleton {} } },
@ -23,6 +29,10 @@ fn route_skeleton(route: &Route) -> Element {
} }
} }
/// 前台整体布局组件。
///
/// 负责渲染 Header含前台导航与主题切换、主内容区与 Footer
/// 并在路由内容加载过程中显示与路由匹配的骨架屏。
#[component] #[component]
pub fn FrontendLayout() -> Element { pub fn FrontendLayout() -> Element {
let route = use_route::<Route>(); let route = use_route::<Route>();

View File

@ -1,15 +1,34 @@
//! 顶部导航栏组件
//!
//! 提供站点 Logo、响应式导航菜单项与右侧自定义内容区
//! 支持前台布局与后台布局复用。
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::router::components::Link; use dioxus::router::components::Link;
use crate::router::Route; use crate::router::Route;
/// 导航项配置,用于描述 Header 中的一个链接。
///
/// 字段:
/// - `route`:目标路由
/// - `label`:显示文本
/// - `is_active`:当前是否处于激活状态
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct NavItemConfig { pub struct NavItemConfig {
/// 目标路由。
pub route: Route, pub route: Route,
/// 显示文本。
pub label: &'static str, pub label: &'static str,
/// 当前是否处于激活状态。
pub is_active: bool, pub is_active: bool,
} }
/// 顶部导航栏组件。
///
/// Props
/// - `nav_items`:导航项列表
/// - `right_content`:右侧自定义内容(如主题切换、登出按钮)
#[component] #[component]
pub fn Header(nav_items: Vec<NavItemConfig>, right_content: Element) -> Element { pub fn Header(nav_items: Vec<NavItemConfig>, right_content: Element) -> Element {
rsx! { rsx! {
@ -37,6 +56,12 @@ pub fn Header(nav_items: Vec<NavItemConfig>, right_content: Element) -> Element
} }
} }
/// 单个导航项组件,根据 `is_active` 切换高亮样式。
///
/// Props
/// - `route`:目标路由
/// - `label`:显示文本
/// - `is_active`:是否高亮
#[component] #[component]
fn NavItem(route: Route, label: &'static str, is_active: bool) -> Element { 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"; let base_class = "px-3 py-1 text-base rounded-lg transition-all duration-200";

View File

@ -1,5 +1,22 @@
//! 图片查看器组件
//!
//! 提供缩略图展示与点击放大后的全屏灯箱lightbox查看
//! 支持自定义缩略图参数、alt 文本与懒加载。
use dioxus::prelude::*; use dioxus::prelude::*;
/// 图片查看器组件。
///
/// Props
/// - `src`:原图 URL
/// - `thumb_params`:缩略图 URL 参数,默认 `"?w=800"`
/// - `alt`:图片替代文本,默认 `"图片"`
/// - `lazy_load`:是否启用懒加载,默认 `false`
///
/// 关键事件:
/// - 点击缩略图:打开全屏灯箱
/// - 点击遮罩或关闭按钮:关闭灯箱
/// - 点击灯箱内容区:阻止事件冒泡,避免误关闭
#[component] #[component]
pub fn ImageViewer( pub fn ImageViewer(
src: String, src: String,
@ -9,6 +26,7 @@ pub fn ImageViewer(
) -> Element { ) -> Element {
let mut is_open = use_signal(|| false); let mut is_open = use_signal(|| false);
// 拼接缩略图 URL保留原 URL 的 query 参数并追加 thumb_params
let thumb_src = if src.contains('?') { let thumb_src = if src.contains('?') {
format!( format!(
"{}&{}", "{}&{}",
@ -20,7 +38,7 @@ pub fn ImageViewer(
}; };
rsx! { rsx! {
// Thumbnail // 缩略图
img { img {
class: "cursor-pointer transition-opacity hover:opacity-90", class: "cursor-pointer transition-opacity hover:opacity-90",
src: "{thumb_src}", src: "{thumb_src}",
@ -29,7 +47,7 @@ pub fn ImageViewer(
onclick: move |_| is_open.set(true), onclick: move |_| is_open.set(true),
} }
// Full-screen lightbox // 全屏灯箱
if is_open() { if is_open() {
div { div {
class: "image-viewer-overlay", class: "image-viewer-overlay",

View File

@ -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_layout;
/// 后台页面骨架屏组件。
pub mod admin_skeleton; pub mod admin_skeleton;
/// 评论相关组件。
pub mod comments; pub mod comments;
/// 页脚组件。
pub mod footer; pub mod footer;
/// 表单控件组件。
pub mod forms; pub mod forms;
/// 前台布局组件。
pub mod frontend_layout; pub mod frontend_layout;
/// 顶部导航栏组件。
pub mod header; pub mod header;
/// 图片查看器组件。
pub mod image_viewer; pub mod image_viewer;
/// 导航组件。
pub mod nav; pub mod nav;
/// 文章详情组件。
pub mod post; pub mod post;
/// 文章卡片组件。
pub mod post_card; pub mod post_card;
/// 骨架屏组件集合。
pub mod skeletons; pub mod skeletons;
/// 编辑器页面骨架屏组件。
pub mod write_skeleton; pub mod write_skeleton;

View File

@ -1,6 +1,16 @@
//! 前台导航项配置
//!
//! 根据当前路由生成前台 Header 所需的导航项列表。
use crate::components::header::NavItemConfig; use crate::components::header::NavItemConfig;
use crate::router::Route; use crate::router::Route;
/// 生成前台导航项列表,当前访问的路由会被标记为激活。
///
/// 参数:
/// - `route`:当前路由
///
/// 返回:包含首页、归档、标签、搜索、关于的导航配置数组。
pub fn use_nav_items(route: Route) -> Vec<NavItemConfig> { pub fn use_nav_items(route: Route) -> Vec<NavItemConfig> {
vec![ vec![
NavItemConfig { NavItemConfig {

View File

@ -1,8 +1,18 @@
//! 面包屑组件
//!
//! 在文章详情页展示从首页到当前文章标题的导航路径。
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::router::components::Link; use dioxus::router::components::Link;
use crate::router::Route; use crate::router::Route;
/// 面包屑导航组件。
///
/// Props
/// - `title`:当前文章标题
///
/// 渲染 `Home > 当前标题` 的面包屑路径。
#[component] #[component]
pub fn Breadcrumbs(title: String) -> Element { pub fn Breadcrumbs(title: String) -> Element {
rsx! { rsx! {

View File

@ -1,8 +1,21 @@
//! 文章详情组件模块
//!
//! 提供文章详情页所需的各个子组件:封面、内容、页眉、元信息、页脚、面包屑、
//! 上一篇/下一篇导航与目录。
/// 面包屑导航组件。
pub mod breadcrumbs; pub mod breadcrumbs;
/// 文章内容组件。
pub mod post_content; pub mod post_content;
/// 文章封面组件。
pub mod post_cover; pub mod post_cover;
/// 文章页脚组件。
pub mod post_footer; pub mod post_footer;
/// 文章页眉组件。
pub mod post_header; pub mod post_header;
/// 文章元信息组件。
pub mod post_meta; pub mod post_meta;
/// 上一篇/下一篇导航组件。
pub mod post_nav_links; pub mod post_nav_links;
/// 文章目录组件。
pub mod post_toc; pub mod post_toc;

View File

@ -1,5 +1,17 @@
//! 文章内容组件
//!
//! 渲染由服务端生成的文章 HTML 内容,并在 WASM 前端初始化交互脚本。
use dioxus::prelude::*; use dioxus::prelude::*;
/// 文章内容组件。
///
/// Props
/// - `content_html`:服务端渲染的文章 HTML 字符串
///
/// 关键行为:
/// - 在 `target_arch = "wasm32"` 环境下执行 `post-content.js` 并调用初始化函数,
/// 用于处理代码块、图片点击等前端交互
#[component] #[component]
pub fn PostContent(content_html: String) -> Element { pub fn PostContent(content_html: String) -> Element {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]

View File

@ -1,7 +1,17 @@
//! 文章封面组件
//!
//! 在文章详情页渲染封面大图,使用图片查看器支持点击放大。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::image_viewer::ImageViewer; use crate::components::image_viewer::ImageViewer;
/// 文章封面组件。
///
/// Props
/// - `src`:封面原图 URL
///
/// 使用 1200px 宽度的缩略图作为默认封面展示,点击可放大查看。
#[component] #[component]
pub fn PostCover(src: String) -> Element { pub fn PostCover(src: String) -> Element {
rsx! { rsx! {

View File

@ -1,3 +1,7 @@
//! 文章页脚组件
//!
//! 展示文章标签、上一篇/下一篇导航与返回首页链接。
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::router::components::Link; use dioxus::router::components::Link;
@ -5,6 +9,15 @@ use crate::components::post::post_nav_links::PostNavLinks;
use crate::models::post::Post; use crate::models::post::Post;
use crate::router::Route; use crate::router::Route;
/// 文章页脚组件。
///
/// Props
/// - `post`:文章数据模型
///
/// 展示内容包括:
/// - 文章标签云,链接到对应标签详情页
/// - 相邻文章导航(如有)
/// - 返回首页链接
#[component] #[component]
pub fn PostFooter(post: Post) -> Element { pub fn PostFooter(post: Post) -> Element {
let tags = post.tags.clone(); let tags = post.tags.clone();

View File

@ -1,9 +1,23 @@
//! 文章页眉组件
//!
//! 组合面包屑、标题、摘要与元信息,草稿状态会在标题旁显示草稿图标。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::post::breadcrumbs::Breadcrumbs; use crate::components::post::breadcrumbs::Breadcrumbs;
use crate::components::post::post_meta::PostMeta; use crate::components::post::post_meta::PostMeta;
use crate::models::post::{Post, PostStatus}; use crate::models::post::{Post, PostStatus};
/// 文章页眉组件。
///
/// Props
/// - `post`:文章数据模型
///
/// 展示内容包括:
/// - 面包屑导航Home → 文章标题)
/// - 文章标题,草稿状态附加草稿图标
/// - 文章摘要(如有)
/// - 文章元信息
#[component] #[component]
pub fn PostHeader(post: Post) -> Element { pub fn PostHeader(post: Post) -> Element {
rsx! { rsx! {

View File

@ -1,7 +1,17 @@
//! 文章元信息组件
//!
//! 展示文章发布日期、阅读时长与字数统计。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::models::post::Post; use crate::models::post::Post;
/// 文章元信息组件。
///
/// Props
/// - `post`:文章数据模型
///
/// 渲染格式:`日期 · min read · words`
#[component] #[component]
pub fn PostMeta(post: Post) -> Element { pub fn PostMeta(post: Post) -> Element {
rsx! { rsx! {

View File

@ -1,9 +1,20 @@
//! 文章上一篇/下一篇导航组件
//!
//! 在文章详情页底部提供相邻文章的快速跳转。
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::router::components::Link; use dioxus::router::components::Link;
use crate::models::post::PostNav; use crate::models::post::PostNav;
use crate::router::Route; use crate::router::Route;
/// 文章相邻导航组件。
///
/// Props
/// - `prev`:上一篇文章摘要
/// - `next`:下一篇文章摘要
///
/// 左右两侧分别渲染 Prev/Next 链接,若无相邻文章则占位空白。
#[component] #[component]
pub fn PostNavLinks(prev: Option<PostNav>, next: Option<PostNav>) -> Element { pub fn PostNavLinks(prev: Option<PostNav>, next: Option<PostNav>) -> Element {
rsx! { rsx! {

View File

@ -1,5 +1,15 @@
//! 文章目录组件
//!
//! 在文章详情页渲染由服务端生成的目录 HTML支持折叠展开。
use dioxus::prelude::*; use dioxus::prelude::*;
/// 文章目录Table of Contents组件。
///
/// Props
/// - `toc_html`:服务端生成的目录 HTML 字符串
///
/// 通过 `dangerous_inner_html` 注入目录结构,快捷键 `Alt + C` 可聚焦。
#[component] #[component]
pub fn PostToc(toc_html: String) -> Element { pub fn PostToc(toc_html: String) -> Element {
rsx! { rsx! {

View File

@ -1,3 +1,7 @@
//! 文章卡片组件
//!
//! 在首页、标签详情等列表中展示单篇文章的标题、摘要、封面、日期与标签。
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::router::components::Link; use dioxus::router::components::Link;
@ -5,6 +9,19 @@ use crate::components::image_viewer::ImageViewer;
use crate::models::post::Post; use crate::models::post::Post;
use crate::router::Route; use crate::router::Route;
/// 文章卡片组件。
///
/// Props
/// - `post`:文章数据模型
///
/// 展示内容包括:
/// - 封面图(如有,使用 400x300 缩略图)
/// - 文章标题
/// - 摘要(最多两行)
/// - 发布日期与标签
///
/// 关键事件:
/// - 点击标签时阻止事件冒泡,避免触发整卡跳转
#[component] #[component]
pub fn PostCard(post: Post) -> Element { pub fn PostCard(post: Post) -> Element {
let post_slug = post.slug.clone(); let post_slug = post.slug.clone();

View File

@ -1,9 +1,14 @@
//! 归档页骨架屏
//!
//! 在归档数据加载期间展示按年份/月份分组的文章列表占位。
use crate::components::skeletons::atoms::*; use crate::components::skeletons::atoms::*;
use dioxus::prelude::*; use dioxus::prelude::*;
/// 归档页骨架屏 /// 归档页骨架屏组件。
/// 结构:统计行("共 N 篇文章") + 年份标题 + 月份标题 + 文章条目列表 ///
/// 模拟 2 个年份,每个年份 2 个月,每个月 3 篇文章 /// 结构:统计行("共 N 篇文章"+ 年份标题 + 月份标题 + 文章条目列表。
/// 模拟 2 个年份,每个年份 2 个月,每个月 3 篇文章。
#[component] #[component]
pub fn ArchiveSkeleton() -> Element { pub fn ArchiveSkeleton() -> Element {
rsx! { rsx! {

View File

@ -1,5 +1,16 @@
//! 骨架屏原子组件
//!
//! 提供通用的脉冲动画占位块,供各页面骨架屏组合使用。
use dioxus::prelude::*; use dioxus::prelude::*;
/// 通用骨架占位块。
///
/// Props
/// - `class`Tailwind CSS 类,控制尺寸与形状
/// - `style`:可选的内联样式字符串
///
/// 默认带有 `animate-pulse` 动画与半透明的占位背景。
#[component] #[component]
pub fn SkeletonBox(class: &'static str, style: Option<&'static str>) -> Element { pub fn SkeletonBox(class: &'static str, style: Option<&'static str>) -> Element {
rsx! { rsx! {

View File

@ -1,6 +1,13 @@
//! 评论列表骨架屏
//!
//! 在评论数据加载期间展示评论条目占位。
use crate::components::skeletons::atoms::*; use crate::components::skeletons::atoms::*;
use dioxus::prelude::*; use dioxus::prelude::*;
/// 评论列表骨架屏组件。
///
/// 渲染一个标题占位与若干条评论条目占位,包含头像、作者名与内容行。
#[component] #[component]
pub fn CommentListSkeleton() -> Element { pub fn CommentListSkeleton() -> Element {
rsx! { rsx! {

View File

@ -1,21 +1,28 @@
//! 延迟骨架屏包装组件
//!
//! 让骨架屏先以静态灰色块立即显示,延迟一段时间后再启动 pulse 动画,
//! 避免快速加载时动画一闪而过带来的视觉抖动。
use crate::utils::time::sleep_ms; use crate::utils::time::sleep_ms;
use dioxus::prelude::*; use dioxus::prelude::*;
/// 骨架屏 pulse 动画延迟(毫秒) /// 骨架屏 pulse 动画延迟(毫秒)。
/// 加载时间低于此值时骨架屏只显示静态灰色块,避免 pulse 动画一闪而过 ///
/// 加载时间低于此值时骨架屏只显示静态灰色块,避免 pulse 动画一闪而过。
const SKELETON_PULSE_DELAY_MS: u32 = 200; const SKELETON_PULSE_DELAY_MS: u32 = 200;
/// 延迟 pulse 动画的骨架屏包装组件 /// 延迟 pulse 动画的骨架屏包装组件
/// ///
/// 骨架屏区域**立即显示**(灰色静态占位块),避免空白闪烁。 /// 骨架屏区域**立即显示**(灰色静态占位块),避免空白闪烁。
/// 延迟 SKELETON_PULSE_DELAY_MS 毫秒后,如果仍在加载,则启动 pulse 动画。 /// 延迟 `SKELETON_PULSE_DELAY_MS` 毫秒后,如果仍在加载,则启动 pulse 动画。
/// ///
/// 快加载(< 200ms用户只看到静态灰色块一闪而过无动画感知 /// 快加载(< 200ms用户只看到静态灰色块一闪而过无动画感知
/// 慢加载:灰色块正常 pulse提示正在加载 /// 慢加载:灰色块正常 pulse提示正在加载
#[component] #[component]
pub fn DelayedSkeleton(children: Element) -> Element { pub fn DelayedSkeleton(children: Element) -> Element {
let mut pulsing = use_signal(|| false); let mut pulsing = use_signal(|| false);
// 延迟启动 pulse 动画
use_effect(move || { use_effect(move || {
spawn(async move { spawn(async move {
sleep_ms(SKELETON_PULSE_DELAY_MS).await; sleep_ms(SKELETON_PULSE_DELAY_MS).await;

View File

@ -1,10 +1,15 @@
//! 首页骨架屏
//!
//! 模拟首页文章卡片列表与分页区域。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::skeletons::atoms::SkeletonBox; use crate::components::skeletons::atoms::SkeletonBox;
use crate::components::skeletons::post_card_skeleton::PostCardSkeleton; use crate::components::skeletons::post_card_skeleton::PostCardSkeleton;
/// 首页骨架屏 - 模拟文章卡片列表 + 分页区域 /// 首页骨架屏组件。
/// 显示 5 个文章卡片骨架 + 分页按钮占位 ///
/// 显示 5 个文章卡片骨架与分页按钮占位。
#[component] #[component]
pub fn HomeSkeleton() -> Element { pub fn HomeSkeleton() -> Element {
rsx! { rsx! {

View File

@ -1,10 +1,24 @@
//! 骨架屏组件模块
//!
//! 提供各页面在数据加载期间的占位组件,以及通用骨架原子组件。
/// 归档页面骨架屏组件。
pub mod archive_skeleton; pub mod archive_skeleton;
/// 通用骨架原子组件。
pub mod atoms; pub mod atoms;
/// 评论区骨架屏组件。
pub mod comment_skeleton; pub mod comment_skeleton;
/// 延迟显示骨架屏组件。
pub mod delayed_skeleton; pub mod delayed_skeleton;
/// 首页骨架屏组件。
pub mod home_skeleton; pub mod home_skeleton;
/// 文章卡片骨架屏组件。
pub mod post_card_skeleton; pub mod post_card_skeleton;
/// 文章详情页骨架屏组件。
pub mod post_detail_skeleton; pub mod post_detail_skeleton;
/// 文章列表页骨架屏组件。
pub mod posts_skeleton; pub mod posts_skeleton;
/// 搜索页骨架屏组件。
pub mod search_skeleton; pub mod search_skeleton;
/// 标签页骨架屏组件。
pub mod tags_skeleton; pub mod tags_skeleton;

View File

@ -1,9 +1,14 @@
//! 文章卡片骨架屏
//!
//! 模拟 PostCard 组件的视觉占位,用于列表页加载。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::skeletons::atoms::SkeletonBox; use crate::components::skeletons::atoms::SkeletonBox;
/// 文章卡片骨架屏 - 模拟 PostCard 的视觉结构 /// 文章卡片骨架屏组件。
/// 包含:标题行(24px bold) + 摘要2行 + 元信息行(日期+标签) ///
/// 包含:标题行(24px bold) + 摘要两行 + 元信息行(日期+标签)。
#[component] #[component]
pub fn PostCardSkeleton() -> Element { pub fn PostCardSkeleton() -> Element {
rsx! { rsx! {

View File

@ -1,8 +1,13 @@
//! 文章详情页骨架屏
//!
//! 在文章详情数据加载期间展示面包屑、标题、摘要、元信息、封面与正文占位。
use crate::components::skeletons::atoms::*; use crate::components::skeletons::atoms::*;
use dioxus::prelude::*; use dioxus::prelude::*;
/// 文章详情页骨架屏 /// 文章详情页骨架屏组件。
/// 结构:面包屑 + 标题(h1) + 摘要 + 元信息 + 封面图 + 正文(多段) + Footer ///
/// 结构:面包屑 + 标题(h1) + 摘要 + 元信息 + 封面图 + 正文(多段) + 页脚占位。
#[component] #[component]
pub fn PostDetailSkeleton() -> Element { pub fn PostDetailSkeleton() -> Element {
rsx! { rsx! {

View File

@ -1,7 +1,14 @@
//! 后台文章管理列表骨架屏
//!
//! 模拟后台 Posts 页面的表格结构。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::skeletons::atoms::SkeletonBox; use crate::components::skeletons::atoms::SkeletonBox;
/// 后台文章管理列表骨架屏组件。
///
/// 渲染带表头的表格占位,包含 10 行数据行。
#[component] #[component]
pub fn PostsSkeleton() -> Element { pub fn PostsSkeleton() -> Element {
rsx! { rsx! {

View File

@ -1,9 +1,14 @@
//! 搜索页骨架屏
//!
//! 在搜索结果加载期间展示搜索卡片列表占位。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::skeletons::post_card_skeleton::PostCardSkeleton; use crate::components::skeletons::post_card_skeleton::PostCardSkeleton;
/// 搜索页骨架屏 - 搜索结果卡片列表 /// 搜索页骨架屏组件。
/// 模拟 3 个搜索结果卡片(与搜索页现有内联骨架结构一致) ///
/// 模拟 3 个搜索结果卡片,与搜索页现有内联骨架结构一致。
#[component] #[component]
pub fn SearchSkeleton() -> Element { pub fn SearchSkeleton() -> Element {
rsx! { rsx! {

View File

@ -1,10 +1,15 @@
//! 标签相关骨架屏
//!
//! 提供标签列表页与标签详情页的加载占位组件。
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::skeletons::atoms::SkeletonBox; use crate::components::skeletons::atoms::SkeletonBox;
use crate::components::skeletons::post_card_skeleton::PostCardSkeleton; use crate::components::skeletons::post_card_skeleton::PostCardSkeleton;
/// 标签列表页骨架屏 /// 标签列表页骨架屏组件。
/// 结构:统计行("共 N 个标签M 篇文章") + 标签云flex wrap 的 pill 列表) ///
/// 结构:统计行("共 N 个标签M 篇文章"+ 标签云flex wrap 的 pill 列表)。
#[component] #[component]
pub fn TagsSkeleton() -> Element { pub fn TagsSkeleton() -> Element {
rsx! { rsx! {
@ -35,7 +40,9 @@ pub fn TagsSkeleton() -> Element {
} }
} }
/// 标签详情页骨架屏 - 与首页文章列表结构相同 /// 标签详情页骨架屏组件。
///
/// 结构与首页文章列表相同,包含统计行与文章卡片骨架。
#[component] #[component]
pub fn TagDetailSkeleton() -> Element { pub fn TagDetailSkeleton() -> Element {
rsx! { rsx! {

View File

@ -1,6 +1,13 @@
//! 文章编辑器页骨架屏
//!
//! 在写文章/编辑文章页面加载时展示,模拟标题、元信息、编辑器工具栏与正文区域。
use crate::components::skeletons::atoms::*; use crate::components::skeletons::atoms::*;
use dioxus::prelude::*; use dioxus::prelude::*;
/// 文章编辑器页骨架屏组件。
///
/// 模拟 Write 页面的整体结构:顶部标题与元信息、中间编辑器区域、底部操作按钮。
#[component] #[component]
pub fn WriteSkeleton() -> Element { pub fn WriteSkeleton() -> Element {
rsx! { rsx! {