docs(components): 补充中文注释
This commit is contained in:
parent
c5d1eb117c
commit
c43da3676f
@ -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! {
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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")]
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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>();
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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")]
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
@ -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! {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user