diff --git a/src/pages/post_detail.rs b/src/pages/post_detail.rs index 8c65c05..9ee226a 100644 --- a/src/pages/post_detail.rs +++ b/src/pages/post_detail.rs @@ -20,7 +20,6 @@ //! 实际的数据库访问逻辑仅在 `feature = "server"` 启用时运行。 use dioxus::prelude::*; -use dioxus::router::components::Link; use crate::api::posts::{get_post_by_slug, SinglePostResponse}; use crate::components::post::post_content::PostContent; @@ -55,92 +54,80 @@ pub fn PostDetail(slug: String) -> Element { get_post_by_slug(current_slug) })?; - // 将结果映射为更直观的 Ok(post) / Err("not_found") / Err("error") 三种状态。 + // 将结果映射为 Ok(post) 或 Err(ServerFnError) 用于错误边界捕获 let post_data = post.read().as_ref().map(|r| match r { Ok(SinglePostResponse { post: Some(post) }) => Ok(post.clone()), - Ok(SinglePostResponse { post: None }) => Err("not_found"), - Err(_) => Err("error"), + Ok(SinglePostResponse { post: None }) => Err(ServerFnError::ServerError { + message: "not_found".to_string(), + code: 404, + details: None, + }), + Err(e) => Err(e.clone()), }); - match post_data { - Some(Ok(post)) => { - rsx! { - article { class: "post-single animate-page-enter", key: "{post.slug}", - PostHeader { post: post.clone() } - - // 如果文章设置了封面图,则渲染封面组件。 - if let Some(cover) = &post.cover_image { - PostCover { src: cover.clone() } - } - - // 如果文章生成了目录 HTML,则渲染目录组件。 - if let Some(toc) = &post.toc_html { - PostToc { toc_html: toc.clone() } - } - - // 用单元素 keyed 列表包裹 PostContent,key 绑定 slug。 - // - // 为什么不能直接给 PostContent 加 key:Dioxus 的 key diff(diff_keyed_children) - // 只在「兄弟节点列表」里生效,对单个非列表元素的 key 变化会走 diff_non_keyed - // 路径、按位置复用,不触发 remount。把组件放进单元素 for 循环并带 key, - // 才会进入 keyed diff——slug 变化时旧 PostContent 被移除、新的被创建。 - // - // 为什么需要 remount:上下篇切换时 PostContent 若被复用(仅重渲染), - // (1) 内部 CodeRunner 用片段索引作 key,两篇文章索引可能相同(如 1/3/5), - // keyed diff 按相同 key 复用 CodeRunner 实例——其挂载 use_effect 的 - // 「防重复 init」守卫阻止 CodeMirror 挂载到新的(已替换的)DOM 容器, - // 表现为翻页后代码块消失; - // (2) PostContent 自身的 use_effect(__initPostContent 复制按钮 / 灯箱初始化) - // 也不会重跑,新文章的交互脚本不初始化。 - // 强制 remount 让编辑器与脚本随文章切换全部重新初始化。 - for post_slug in std::iter::once(post.slug.clone()) { - PostContent { - key: "{post_slug}", - content_html: post.content_html.clone().unwrap_or_default(), - } - } - - PostFooter { post: post.clone() } - - // 仅对已发布文章展示评论区域,使用 SuspenseBoundary 处理加载状态。 - if post.status == crate::models::post::PostStatus::Published { - div { class: "mt-12 border-t border-gray-200 dark:border-gray-700 pt-8", - SuspenseBoundary { - fallback: move |_| rsx! { - DelayedSkeleton { - crate::components::skeletons::comment_skeleton::CommentListSkeleton {} - } - }, - crate::components::comments::section::CommentSection { post_id: post.id } - } - } - } - } - } + let post = match post_data { + Some(Ok(post)) => post, + Some(Err(err)) => { + // Bubble the error up to the ErrorBoundary + return Err(err.into()); } - Some(Err("not_found")) => { - rsx! { - div { class: "text-center py-20", - h2 { class: "text-2xl font-bold text-paper-primary mb-4", "文章不存在" } - p { class: "text-paper-secondary mb-6", - "这篇文章可能已被删除或移动。" - } - Link { - class: "px-6 py-2 bg-paper-primary text-paper-theme rounded-full font-medium hover:opacity-80 transition-opacity", - to: Route::Home {}, - "返回首页" - } - } - } - } - Some(Err("error")) => { - rsx! { - div { class: "text-center text-red-500 dark:text-red-400 py-20", "加载失败" } - } - } - _ => { - rsx! { + None => { + return rsx! { DelayedSkeleton { PostDetailSkeleton {} } + }; + } + }; + + rsx! { + article { class: "post-single animate-page-enter", key: "{post.slug}", + PostHeader { post: post.clone() } + + // 如果文章设置了封面图,则渲染封面组件。 + if let Some(cover) = &post.cover_image { + PostCover { src: cover.clone() } + } + + // 如果文章生成了目录 HTML,则渲染目录组件。 + if let Some(toc) = &post.toc_html { + PostToc { toc_html: toc.clone() } + } + + // 用单元素 keyed 列表包裹 PostContent,key 绑定 slug。 + // + // 为什么不能直接给 PostContent 加 key:Dioxus 的 key diff(diff_keyed_children) + // 只在「兄弟节点列表」里生效,对单个非列表元素的 key 变化会走 diff_non_keyed + // 路径、按位置复用,不触发 remount。把组件放进单元素 for 循环并带 key, + // 才会进入 keyed diff——slug 变化时旧 PostContent 被移除、新的被创建。 + // + // 为什么需要 remount:上下篇切换时 PostContent 若被复用(仅重渲染), + // (1) 内部 CodeRunner 用片段索引作 key,两篇文章索引可能相同(如 1/3/5), + // keyed diff 按相同 key 复用 CodeRunner 实例——其挂载 use_effect 的 + // 「防重复 init」守卫阻止 CodeMirror 挂载到新的(已替换的)DOM 容器, + // 表现为翻页后代码块消失; + // (2) PostContent 自身的 use_effect(__initPostContent 复制按钮 / 灯箱初始化) + // 也不会重跑,新文章的交互脚本不初始化。 + // 强制 remount 让编辑器与脚本随文章切换全部重新初始化。 + for post_slug in std::iter::once(post.slug.clone()) { + PostContent { + key: "{post_slug}", + content_html: post.content_html.clone().unwrap_or_default(), + } + } + + PostFooter { post: post.clone() } + + // 仅对已发布文章展示评论区域,使用 SuspenseBoundary 处理加载状态。 + if post.status == crate::models::post::PostStatus::Published { + div { class: "mt-12 border-t border-gray-200 dark:border-gray-700 pt-8", + SuspenseBoundary { + fallback: move |_| rsx! { + DelayedSkeleton { + crate::components::skeletons::comment_skeleton::CommentListSkeleton {} + } + }, + crate::components::comments::section::CommentSection { post_id: post.id } + } + } } } }