feat(components): add post page components (header, toc, content, footer, nav)
This commit is contained in:
parent
a07f6ca51b
commit
6b1f2e27c9
@ -4,5 +4,6 @@ pub mod footer;
|
||||
pub mod header;
|
||||
pub mod nav;
|
||||
pub mod page_layout;
|
||||
pub mod post;
|
||||
pub mod post_card;
|
||||
pub mod write_skeleton;
|
||||
|
||||
34
src/components/post/breadcrumbs.rs
Normal file
34
src/components/post/breadcrumbs.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn Breadcrumbs(title: String) -> Element {
|
||||
rsx! {
|
||||
nav {
|
||||
class: "breadcrumbs",
|
||||
role: "navigation",
|
||||
aria_label: "Breadcrumb",
|
||||
a {
|
||||
href: "/",
|
||||
onclick: move |evt| {
|
||||
evt.prevent_default();
|
||||
dioxus::router::navigator().push("/");
|
||||
},
|
||||
"Home"
|
||||
}
|
||||
svg {
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
view_box: "0 0 24 24",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
stroke_width: "2",
|
||||
stroke_linecap: "round",
|
||||
stroke_linejoin: "round",
|
||||
class: "feather feather-chevron-right",
|
||||
width: "16",
|
||||
height: "16",
|
||||
polyline { points: "9 18 15 12 9 6" }
|
||||
}
|
||||
span { "{title}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/components/post/mod.rs
Normal file
8
src/components/post/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
pub mod breadcrumbs;
|
||||
pub mod post_content;
|
||||
pub mod post_cover;
|
||||
pub mod post_footer;
|
||||
pub mod post_header;
|
||||
pub mod post_meta;
|
||||
pub mod post_nav_links;
|
||||
pub mod post_toc;
|
||||
39
src/components/post/post_content.rs
Normal file
39
src/components/post/post_content.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn PostContent(content_html: String) -> Element {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use_effect(move || {
|
||||
if let Some(window) = web_sys::window() {
|
||||
if let Some(document) = window.document() {
|
||||
// Add copy buttons to all code blocks
|
||||
if let Ok(elements) = document.query_selector_all("pre > code") {
|
||||
for i in 0..elements.length() {
|
||||
if let Some(codeblock) = elements.item(i) {
|
||||
if let Some(parent) = codeblock.parent_element() {
|
||||
// Check if button already exists
|
||||
if parent.query_selector(".copy-code").ok().flatten().is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let copybutton = document.create_element("button").unwrap();
|
||||
copybutton.set_class_name("copy-code");
|
||||
copybutton.set_text_content(Some("copy"));
|
||||
copybutton.set_attribute("aria-label", "Copy code").unwrap();
|
||||
|
||||
let _ = parent.append_child(©button);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "post-content md-content",
|
||||
dangerous_inner_html: "{content_html}"
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/components/post/post_cover.rs
Normal file
14
src/components/post/post_cover.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn PostCover(src: String) -> Element {
|
||||
rsx! {
|
||||
figure { class: "entry-cover",
|
||||
img {
|
||||
loading: "eager",
|
||||
src: "{src}",
|
||||
alt: "Cover image"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/components/post/post_footer.rs
Normal file
46
src/components/post/post_footer.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::post::post_nav_links::PostNavLinks;
|
||||
use crate::models::post::Post;
|
||||
|
||||
#[component]
|
||||
pub fn PostFooter(post: Post) -> Element {
|
||||
let tags = post.tags.clone();
|
||||
|
||||
rsx! {
|
||||
footer { class: "post-footer",
|
||||
if !tags.is_empty() {
|
||||
ul { class: "post-tags",
|
||||
for tag in tags.into_iter() {
|
||||
li {
|
||||
a {
|
||||
href: "/tags/{tag}",
|
||||
onclick: move |evt| {
|
||||
evt.prevent_default();
|
||||
dioxus::router::navigator().push(format!("/tags/{}", tag));
|
||||
},
|
||||
"{tag}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if post.prev_post.is_some() || post.next_post.is_some() {
|
||||
PostNavLinks {
|
||||
prev: post.prev_post,
|
||||
next: post.next_post
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "back-to-home",
|
||||
button {
|
||||
onclick: move |_| {
|
||||
let _ = dioxus::router::navigator().push("/");
|
||||
},
|
||||
"← Back to Home"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/components/post/post_header.rs
Normal file
39
src/components/post/post_header.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::post::breadcrumbs::Breadcrumbs;
|
||||
use crate::components::post::post_meta::PostMeta;
|
||||
use crate::models::post::{Post, PostStatus};
|
||||
|
||||
#[component]
|
||||
pub fn PostHeader(post: Post) -> Element {
|
||||
rsx! {
|
||||
header { class: "post-header",
|
||||
Breadcrumbs { title: post.title.clone() }
|
||||
|
||||
h1 { class: "post-title",
|
||||
"{post.title}"
|
||||
if post.status == PostStatus::Draft {
|
||||
span {
|
||||
class: "entry-hint",
|
||||
title: "Draft",
|
||||
svg {
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
height: "35",
|
||||
view_box: "0 -960 960 960",
|
||||
fill: "currentColor",
|
||||
path {
|
||||
d: "M160-410v-60h300v60H160Zm0-165v-60h470v60H160Zm0-165v-60h470v60H160Zm360 580v-123l221-220q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q9 9 13 20t4 22q0 11-4.5 22.5T862.09-380L643-160H520Zm300-263-37-37 37 37ZM580-220h38l121-122-18-19-19-18-122 121v38Zm141-141-19-18 37 37-18-19Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(summary) = &post.summary {
|
||||
div { class: "post-description", "{summary}" }
|
||||
}
|
||||
|
||||
PostMeta { post: post.clone() }
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/components/post/post_meta.rs
Normal file
16
src/components/post/post_meta.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::models::post::Post;
|
||||
|
||||
#[component]
|
||||
pub fn PostMeta(post: Post) -> Element {
|
||||
rsx! {
|
||||
div { class: "post-meta",
|
||||
span { "{post.formatted_date()}" }
|
||||
span { "·" }
|
||||
span { "{post.reading_time} min read" }
|
||||
span { "·" }
|
||||
span { "{post.word_count} words" }
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/components/post/post_nav_links.rs
Normal file
40
src/components/post/post_nav_links.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::models::post::PostNav;
|
||||
|
||||
#[component]
|
||||
pub fn PostNavLinks(prev: Option<PostNav>, next: Option<PostNav>) -> Element {
|
||||
rsx! {
|
||||
nav { class: "paginav",
|
||||
if let Some(prev_post) = prev {
|
||||
a {
|
||||
class: "prev",
|
||||
href: "/post/{prev_post.slug}",
|
||||
onclick: move |evt| {
|
||||
evt.prevent_default();
|
||||
dioxus::router::navigator().push(format!("/post/{}", prev_post.slug));
|
||||
},
|
||||
span { class: "title", "« Prev" }
|
||||
span { class: "post-title-nav", "{prev_post.title}" }
|
||||
}
|
||||
} else {
|
||||
span { class: "prev" }
|
||||
}
|
||||
|
||||
if let Some(next_post) = next {
|
||||
a {
|
||||
class: "next",
|
||||
href: "/post/{next_post.slug}",
|
||||
onclick: move |evt| {
|
||||
evt.prevent_default();
|
||||
dioxus::router::navigator().push(format!("/post/{}", next_post.slug));
|
||||
},
|
||||
span { class: "title", "Next »" }
|
||||
span { class: "post-title-nav", "{next_post.title}" }
|
||||
}
|
||||
} else {
|
||||
span { class: "next" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/components/post/post_toc.rs
Normal file
18
src/components/post/post_toc.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn PostToc(toc_html: String) -> Element {
|
||||
rsx! {
|
||||
details { class: "toc",
|
||||
summary {
|
||||
accesskey: "c",
|
||||
title: "(Alt + C)",
|
||||
span { class: "title", "Table of Contents" }
|
||||
}
|
||||
div {
|
||||
class: "inner",
|
||||
dangerous_inner_html: "{toc_html}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,11 @@ use dioxus::prelude::*;
|
||||
use crate::api::posts::{get_post_by_slug, SinglePostResponse};
|
||||
use crate::components::nav::use_nav_items;
|
||||
use crate::components::page_layout::PageLayout;
|
||||
use crate::components::post::post_content::PostContent;
|
||||
use crate::components::post::post_cover::PostCover;
|
||||
use crate::components::post::post_footer::PostFooter;
|
||||
use crate::components::post::post_header::PostHeader;
|
||||
use crate::components::post::post_toc::PostToc;
|
||||
use crate::hooks::delayed_loading::use_delayed_loading;
|
||||
use crate::router::Route;
|
||||
|
||||
@ -18,59 +23,37 @@ pub fn PostDetail(slug: String) -> Element {
|
||||
PageLayout { nav_items,
|
||||
match &*post_res.read() {
|
||||
Some(Ok(SinglePostResponse { post: Some(post) })) => {
|
||||
let date_str = post.formatted_date();
|
||||
|
||||
rsx! {
|
||||
article { class: "py-6",
|
||||
header { class: "mb-8",
|
||||
h1 { class: "text-3xl md:text-4xl font-bold text-gray-900 dark:text-[#dadadb] leading-tight",
|
||||
"{post.title}"
|
||||
}
|
||||
div { class: "mt-4 flex items-center gap-3 text-sm text-gray-500 dark:text-[#9b9c9d]",
|
||||
span { "{date_str}" }
|
||||
if !post.tags.is_empty() {
|
||||
span { "·" }
|
||||
for tag in post.tags.clone().into_iter() {
|
||||
a {
|
||||
class: "hover:text-gray-700 dark:hover:text-[#dadadb] transition-colors",
|
||||
href: "/tags/{tag}",
|
||||
onclick: move |evt| {
|
||||
evt.prevent_default();
|
||||
dioxus::router::navigator().push(format!("/tags/{}", tag).as_str());
|
||||
},
|
||||
"{tag}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
article { class: "post-single",
|
||||
PostHeader { post: post.clone() }
|
||||
|
||||
if let Some(cover) = &post.cover_image {
|
||||
PostCover { src: cover.clone() }
|
||||
}
|
||||
div {
|
||||
class: "prose dark:prose-invert max-w-none text-gray-800 dark:text-[#c9cacc] leading-relaxed",
|
||||
dangerous_inner_html: "{post.content_html.as_deref().unwrap_or(\"\")}"
|
||||
|
||||
if let Some(toc) = &post.toc_html {
|
||||
PostToc { toc_html: toc.clone() }
|
||||
}
|
||||
div { class: "mt-12 pt-6 border-t border-gray-200 dark:border-[#333]",
|
||||
button {
|
||||
class: "text-sm text-gray-500 dark:text-[#9b9c9d] hover:text-gray-700 dark:hover:text-[#dadadb] transition-colors",
|
||||
onclick: move |_| {
|
||||
let _ = dioxus::router::navigator().push("/");
|
||||
},
|
||||
"← 返回首页"
|
||||
}
|
||||
|
||||
PostContent {
|
||||
content_html: post.content_html.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
PostFooter { post: post.clone() }
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Ok(SinglePostResponse { post: None })) => {
|
||||
rsx! {
|
||||
div { class: "text-center py-20",
|
||||
h2 { class: "text-2xl font-bold text-gray-900 dark:text-[#dadadb] mb-4",
|
||||
h2 { class: "text-2xl font-bold text-paper-primary mb-4",
|
||||
"文章不存在"
|
||||
}
|
||||
p { class: "text-gray-500 dark:text-[#9b9c9d] mb-6",
|
||||
p { class: "text-paper-secondary mb-6",
|
||||
"这篇文章可能已被删除或移动。"
|
||||
}
|
||||
button {
|
||||
class: "px-6 py-2 bg-gray-900 dark:bg-[#dadadb] text-white dark:text-gray-900 rounded-full font-medium hover:opacity-80 transition-opacity",
|
||||
class: "px-6 py-2 bg-paper-primary text-paper-theme rounded-full font-medium hover:opacity-80 transition-opacity",
|
||||
onclick: move |_| {
|
||||
let _ = dioxus::router::navigator().push("/");
|
||||
},
|
||||
@ -89,11 +72,11 @@ pub fn PostDetail(slug: String) -> Element {
|
||||
None => {
|
||||
rsx! {
|
||||
div { class: if show_skeleton() { "animate-pulse py-6 space-y-4" } else { "py-6 space-y-4 opacity-0" },
|
||||
div { class: "h-10 w-3/4 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
|
||||
div { class: "h-4 w-32 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
|
||||
div { class: "h-4 w-full bg-gray-200 dark:bg-[#2a2a2a] rounded mt-8" }
|
||||
div { class: "h-4 w-full bg-gray-200 dark:bg-[#2a2a2a] rounded" }
|
||||
div { class: "h-4 w-2/3 bg-gray-200 dark:bg-[#2a2a2a] rounded" }
|
||||
div { class: "h-10 w-3/4 bg-paper-tertiary rounded" }
|
||||
div { class: "h-4 w-32 bg-paper-tertiary rounded" }
|
||||
div { class: "h-4 w-full bg-paper-tertiary rounded mt-8" }
|
||||
div { class: "h-4 w-full bg-paper-tertiary rounded" }
|
||||
div { class: "h-4 w-2/3 bg-paper-tertiary rounded" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user