yggdrasil/pages/admin/
preview.rs1#[cfg(target_arch = "wasm32")]
34use crate::api::posts::get_post_preview;
35#[cfg(target_arch = "wasm32")]
36use crate::api::posts::SinglePostResponse;
37use crate::components::post::post_content::PostContent;
38use crate::components::post::post_cover::PostCover;
39use crate::components::post::post_footer::PostFooter;
40use crate::components::post::post_header::PostHeader;
41use crate::components::post::post_toc::PostToc;
42use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
43use crate::components::skeletons::post_preview_skeleton::PostPreviewSkeleton;
44use crate::components::ui::{BTN_OUTLINE, BTN_PRIMARY};
45use crate::models::post::Post;
46use crate::router::Route;
47use dioxus::prelude::*;
48use dioxus::router::components::Link;
49
50#[component]
55pub fn PostPreview(slug: String) -> Element {
56 let router = dioxus::router::router();
57 let render_slug = slug.clone();
58 let requested_slug = use_memo(move || match router.current::<Route>() {
59 Route::PostPreview { slug } => slug,
60 _ => slug.clone(),
61 });
62
63 #[allow(unused_mut)]
66 let mut post = use_signal(|| None::<Option<Post>>);
67 #[allow(unused_mut)]
68 let mut loaded_slug = use_signal(String::new);
69
70 use_effect(move || {
72 let current_slug = requested_slug();
73 #[cfg(not(target_arch = "wasm32"))]
75 let _ = ¤t_slug;
76 #[cfg(target_arch = "wasm32")]
79 {
80 post.set(None);
81 spawn(async move {
82 let resp = get_post_preview(current_slug.clone()).await;
83 let still_here = matches!(router.current::<Route>(),
86 Route::PostPreview { slug: s } if s == current_slug);
87 if still_here {
88 loaded_slug.set(current_slug);
89 post.set(Some(resp.ok().and_then(|SinglePostResponse { post }| post)));
90 }
91 });
92 }
93 });
94
95 if loaded_slug() != render_slug {
97 return rsx! { DelayedSkeleton { PostPreviewSkeleton {} } };
98 }
99
100 let post = match post.read().as_ref() {
103 None => {
104 return rsx! {
105 DelayedSkeleton { PostPreviewSkeleton {} }
106 };
107 }
108 Some(None) => {
109 return rsx! {
110 div { class: "flex flex-col items-center justify-center text-center py-20 px-4 animate-page-enter",
111 p { class: "text-sm text-paper-secondary",
112 "未找到该文章(可能已被删除)。"
113 }
114 Link { class: "mt-6 {BTN_OUTLINE}", "data-vt-return": "true", to: Route::Posts {}, "返回文章列表" }
115 }
116 };
117 }
118 Some(Some(post)) => post.clone(),
119 };
120
121 rsx! {
122 article { class: "post-single animate-page-enter", key: "{post.slug}", "data-vt-detail": "{post.id}",
123 div { class: "flex flex-wrap items-center justify-between gap-3 mb-6 p-3 rounded-2xl bg-[var(--color-paper-entry)] border border-[var(--color-paper-border)]",
125 div { class: "flex items-center gap-2 text-sm text-paper-secondary",
126 span { "预览模式" }
127 span { class: "px-2 py-0.5 rounded-full text-xs font-medium {post.status.badge_class()}",
128 "{post.status.label()}"
129 }
130 }
131 div { class: "flex items-center gap-2",
132 Link {
133 class: "{BTN_PRIMARY}",
134 to: Route::WriteEdit { id: post.id },
135 "继续编辑"
136 }
137 Link { class: "{BTN_OUTLINE}", "data-vt-return": "true", to: Route::Posts {}, "返回列表" }
138 }
139 }
140
141 PostHeader { post: post.clone(), full_reload: true }
142
143 if let Some(cover) = &post.cover_image {
145 PostCover { src: cover.clone(), post_id: post.id }
146 }
147
148 if let Some(toc) = &post.toc_html {
150 for toc_slug in std::iter::once(post.slug.clone()) {
151 PostToc { key: "{toc_slug}", toc_html: toc.clone() }
152 }
153 }
154
155 for post_slug in std::iter::once(post.slug.clone()) {
159 PostContent {
160 key: "{post_slug}",
161 content_html: post.content_html.clone().unwrap_or_default(),
162 }
163 }
164 PostFooter { post: post.clone(), full_reload: true }
165 }
166 }
167}