diff --git a/src/components/admin_layout.rs b/src/components/admin_layout.rs index 6ecea00..e5efac4 100644 --- a/src/components/admin_layout.rs +++ b/src/components/admin_layout.rs @@ -26,11 +26,11 @@ pub fn AdminLayout() -> Element { if let Some(user) = response.user { ctx.user.set(Some(std::sync::Arc::new(user))); } else { - let _ = navigator.push("/login"); + let _ = navigator.push(Route::Login {}); } } Err(_) => { - let _ = navigator.push("/login"); + let _ = navigator.push(Route::Login {}); } } }); @@ -39,22 +39,22 @@ pub fn AdminLayout() -> Element { let admin_nav_items = vec![ NavItemConfig { - href: "/admin", + route: Route::Admin {}, label: "仪表盘", is_active: matches!(route, Route::Admin {}), }, NavItemConfig { - href: "/admin/write", + route: Route::Write {}, label: "写文章", is_active: matches!(route, Route::Write {}), }, NavItemConfig { - href: "/admin/posts", + route: Route::Posts {}, label: "管理文章", is_active: matches!(route, Route::Posts {}), }, NavItemConfig { - href: "/", + route: Route::Home {}, label: "前台", is_active: false, }, @@ -66,7 +66,7 @@ pub fn AdminLayout() -> Element { onclick: move |_| { spawn(async move { let _ = logout().await; - let _ = navigator.push("/login"); + let _ = navigator.push(Route::Login {}); }); }, "登出" diff --git a/src/components/header.rs b/src/components/header.rs index 5aee608..7feaf84 100644 --- a/src/components/header.rs +++ b/src/components/header.rs @@ -1,8 +1,11 @@ use dioxus::prelude::*; +use dioxus::router::components::Link; + +use crate::router::Route; #[derive(Clone, PartialEq)] pub struct NavItemConfig { - pub href: &'static str, + pub route: Route, pub label: &'static str, pub is_active: bool, } @@ -12,20 +15,16 @@ pub fn Header(nav_items: Vec, right_content: Element) -> Element rsx! { header { class: "sticky top-0 z-40 w-full border-b border-gray-200 dark:border-[#333] bg-white/80 dark:bg-[#1d1e20]/80 backdrop-blur-sm", nav { class: "max-w-3xl mx-auto px-6 h-[60px] flex items-center justify-between", - a { + Link { class: "text-2xl font-bold text-gray-900 dark:text-[#dadadb] hover:opacity-80 transition-opacity", - href: "/", - onclick: move |evt| { - evt.prevent_default(); - dioxus::router::navigator().push("/"); - }, + to: Route::Home {}, "Yggdrasil" } div { class: "flex items-center gap-2", ul { class: "hidden md:flex items-center gap-1", for item in nav_items.iter().cloned() { NavItem { - href: item.href, + route: item.route, label: item.label, is_active: item.is_active, } @@ -39,7 +38,7 @@ pub fn Header(nav_items: Vec, right_content: Element) -> Element } #[component] -fn NavItem(href: &'static str, 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-colors"; let class_str = if is_active { format!("{} font-medium text-gray-900 dark:text-[#dadadb] underline underline-offset-[0.3rem] decoration-2 decoration-gray-900 dark:decoration-[#dadadb]", base_class) @@ -50,16 +49,11 @@ fn NavItem(href: &'static str, label: &'static str, is_active: bool) -> Element ) }; - let href = href; rsx! { li { - a { + Link { class: "{class_str}", - href: "{href}", - onclick: move |evt| { - evt.prevent_default(); - dioxus::router::navigator().push(href); - }, + to: route, "{label}" } } diff --git a/src/components/nav.rs b/src/components/nav.rs index c00cfe1..90fbf18 100644 --- a/src/components/nav.rs +++ b/src/components/nav.rs @@ -4,27 +4,27 @@ use crate::router::Route; pub fn use_nav_items(route: Route) -> Vec { vec![ NavItemConfig { - href: "/", + route: Route::Home {}, label: "首页", is_active: matches!(route, Route::Home {}), }, NavItemConfig { - href: "/archives", + route: Route::Archives {}, label: "归档", is_active: matches!(route, Route::Archives {}), }, NavItemConfig { - href: "/tags", + route: Route::Tags {}, label: "标签", is_active: matches!(route, Route::Tags {}) || matches!(route, Route::TagDetail { .. }), }, NavItemConfig { - href: "/search", + route: Route::Search {}, label: "搜索", is_active: matches!(route, Route::Search {}), }, NavItemConfig { - href: "/about", + route: Route::About {}, label: "关于", is_active: matches!(route, Route::About {}), }, diff --git a/src/components/post/breadcrumbs.rs b/src/components/post/breadcrumbs.rs index 648eced..9cddaea 100644 --- a/src/components/post/breadcrumbs.rs +++ b/src/components/post/breadcrumbs.rs @@ -1,4 +1,7 @@ use dioxus::prelude::*; +use dioxus::router::components::Link; + +use crate::router::Route; #[component] pub fn Breadcrumbs(title: String) -> Element { @@ -7,12 +10,8 @@ pub fn Breadcrumbs(title: String) -> Element { class: "breadcrumbs", role: "navigation", aria_label: "Breadcrumb", - a { - href: "/", - onclick: move |evt| { - evt.prevent_default(); - dioxus::router::navigator().push("/"); - }, + Link { + to: Route::Home {}, "Home" } svg { diff --git a/src/components/post/post_footer.rs b/src/components/post/post_footer.rs index aa53484..b5d5b23 100644 --- a/src/components/post/post_footer.rs +++ b/src/components/post/post_footer.rs @@ -1,7 +1,9 @@ use dioxus::prelude::*; +use dioxus::router::components::Link; use crate::components::post::post_nav_links::PostNavLinks; use crate::models::post::Post; +use crate::router::Route; #[component] pub fn PostFooter(post: Post) -> Element { @@ -13,12 +15,8 @@ pub fn PostFooter(post: Post) -> Element { 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)); - }, + Link { + to: Route::TagDetail { tag: tag.clone() }, "{tag}" } } @@ -34,10 +32,8 @@ pub fn PostFooter(post: Post) -> Element { } div { class: "back-to-home", - button { - onclick: move |_| { - let _ = dioxus::router::navigator().push("/"); - }, + Link { + to: Route::Home {}, "← Back to Home" } } diff --git a/src/components/post/post_nav_links.rs b/src/components/post/post_nav_links.rs index 99b7a1f..3a2b75d 100644 --- a/src/components/post/post_nav_links.rs +++ b/src/components/post/post_nav_links.rs @@ -1,18 +1,25 @@ use dioxus::prelude::*; +use dioxus::router::components::Link; use crate::models::post::PostNav; +use crate::router::Route; #[component] pub fn PostNavLinks(prev: Option, next: Option) -> Element { + if let Some(ref p) = prev { + println!("[PostNavLinks] prev={} {}", p.slug, p.title); + } + if let Some(ref n) = next { + println!("[PostNavLinks] next={} {}", n.slug, n.title); + } rsx! { nav { class: "paginav", if let Some(prev_post) = prev { - a { + Link { class: "prev", - href: "/post/{prev_post.slug}", - onclick: move |evt| { - evt.prevent_default(); - dioxus::router::navigator().push(format!("/post/{}", prev_post.slug)); + to: Route::PostDetail { slug: prev_post.slug.clone() }, + onclick: move |evt: dioxus::events::MouseEvent| { + println!("[PostNavLinks] clicked prev: {}", prev_post.slug); }, span { class: "title", "« Prev" } span { class: "post-title-nav", "{prev_post.title}" } @@ -22,12 +29,11 @@ pub fn PostNavLinks(prev: Option, next: Option) -> Element { } if let Some(next_post) = next { - a { + Link { class: "next", - href: "/post/{next_post.slug}", - onclick: move |evt| { - evt.prevent_default(); - dioxus::router::navigator().push(format!("/post/{}", next_post.slug)); + to: Route::PostDetail { slug: next_post.slug.clone() }, + onclick: move |evt: dioxus::events::MouseEvent| { + println!("[PostNavLinks] clicked next: {}", next_post.slug); }, span { class: "title", "Next »" } span { class: "post-title-nav", "{next_post.title}" } diff --git a/src/components/post_card.rs b/src/components/post_card.rs index 5a83d87..5c936da 100644 --- a/src/components/post_card.rs +++ b/src/components/post_card.rs @@ -1,6 +1,8 @@ use dioxus::prelude::*; +use dioxus::router::components::Link; use crate::models::post::Post; +use crate::router::Route; #[component] pub fn PostCard(post: Post) -> Element { @@ -10,20 +12,16 @@ pub fn PostCard(post: Post) -> Element { rsx! { article { class: "relative mb-6 p-6 bg-white dark:bg-[#2e2e33] rounded-lg border border-gray-200 dark:border-[#333] hover:-translate-y-0.5 hover:border-gray-300 dark:hover:border-gray-600 transition-all duration-250", - a { + Link { class: "block group", - href: "/post/{post_slug}", - onclick: move |evt| { - evt.prevent_default(); - dioxus::router::navigator().push(format!("/post/{}", post_slug).as_str()); - }, + to: Route::PostDetail { slug: post_slug }, h2 { class: "text-2xl font-bold leading-tight text-gray-900 dark:text-[#dadadb] group-hover:opacity-80 transition-opacity", "{post.title}" } div { class: "mt-2 text-sm text-gray-500 dark:text-[#9b9c9d] leading-relaxed line-clamp-2", - "{post.summary.as_deref().unwrap_or(\"\")}" + "{post.summary.as_deref().unwrap_or_default()}" } div { class: "mt-3 flex items-center gap-3 text-[13px] text-gray-400 dark:text-[#9b9c9d]", @@ -32,14 +30,10 @@ pub fn PostCard(post: Post) -> Element { span { "·" } for tag in post.tags.clone().into_iter() { span { - a { + Link { class: "hover:text-gray-600 dark:hover:text-[#dadadb] transition-colors", - href: "/tags/{tag}", - onclick: move |evt| { - evt.prevent_default(); - evt.stop_propagation(); - dioxus::router::navigator().push(format!("/tags/{}", tag).as_str()); - }, + to: Route::TagDetail { tag: tag.clone() }, + onclick: move |evt: dioxus::events::MouseEvent| evt.stop_propagation(), "{tag}" } } diff --git a/src/pages/admin/dashboard.rs b/src/pages/admin/dashboard.rs index 59404f0..ee21a5f 100644 --- a/src/pages/admin/dashboard.rs +++ b/src/pages/admin/dashboard.rs @@ -1,8 +1,10 @@ use dioxus::prelude::*; +use dioxus::router::components::Link; use crate::api::posts::{get_post_stats, list_posts, PostListResponse, PostStatsResponse}; use crate::hooks::delayed_loading::use_delayed_loading; use crate::models::post::Post; +use crate::router::Route; #[component] pub fn Admin() -> Element { @@ -36,18 +38,14 @@ pub fn Admin() -> Element { } div { class: "grid grid-cols-1 md:grid-cols-2 gap-4", - button { + Link { class: "bg-gray-900 dark:bg-[#dadadb] text-white dark:text-gray-900 rounded-full px-6 py-3 text-center font-medium hover:opacity-80 transition-opacity cursor-pointer", - onclick: move |_| { - dioxus::router::navigator().push("/admin/write"); - }, + to: Route::Write {}, "写文章" } - button { + Link { class: "bg-gray-200 dark:bg-[#333] text-gray-700 dark:text-[#dadadb] rounded-full px-6 py-3 text-center font-medium hover:opacity-80 transition-opacity cursor-pointer", - onclick: move |_| { - dioxus::router::navigator().push("/admin/posts"); - }, + to: Route::Posts {}, "管理文章" } } diff --git a/src/pages/admin/posts.rs b/src/pages/admin/posts.rs index 28b70d4..c227d1f 100644 --- a/src/pages/admin/posts.rs +++ b/src/pages/admin/posts.rs @@ -1,8 +1,10 @@ use dioxus::prelude::*; +use dioxus::router::components::Link; use crate::api::posts::{delete_post, list_posts, CreatePostResponse, PostListResponse}; use crate::hooks::delayed_loading::use_delayed_loading; use crate::models::post::{Post, PostStatus}; +use crate::router::Route; #[component] pub fn Posts() -> Element { @@ -16,11 +18,9 @@ pub fn Posts() -> Element { h1 { class: "text-2xl font-bold text-gray-900 dark:text-[#dadadb]", "文章管理" } - button { + Link { class: "px-4 py-2 bg-gray-900 dark:bg-[#dadadb] text-white dark:text-gray-900 rounded-full text-sm font-medium hover:opacity-80 transition-opacity cursor-pointer", - onclick: move |_| { - dioxus::router::navigator().push("/admin/write"); - }, + to: Route::Write {}, "+ 写文章" } } @@ -123,13 +123,9 @@ fn PostRow(post: Post, deleting: bool, on_delete: EventHandler) -> Element rsx! { tr { class: "border-b border-gray-100 dark:border-[#333] last:border-0 hover:bg-gray-50 dark:hover:bg-[#2a2a2a] transition-colors", td { class: "px-4 py-3", - a { + Link { class: "text-gray-900 dark:text-[#dadadb] hover:opacity-80 transition-opacity", - href: "/post/{post.slug}", - onclick: move |evt| { - evt.prevent_default(); - dioxus::router::navigator().push(format!("/post/{}", post.slug).as_str()); - }, + to: Route::PostDetail { slug: post.slug.clone() }, "{post.title}" } } diff --git a/src/pages/admin/write.rs b/src/pages/admin/write.rs index c841615..c8f8103 100644 --- a/src/pages/admin/write.rs +++ b/src/pages/admin/write.rs @@ -5,6 +5,7 @@ use wasm_bindgen::JsCast; use crate::api::posts::{create_post, CreatePostResponse}; use crate::components::write_skeleton::WriteSkeleton; +use crate::router::Route; #[component] #[allow(unused_mut, unused_variables)] @@ -147,7 +148,7 @@ pub fn Write() -> Element { { let _ = js_sys::eval("new Promise(r => setTimeout(r, 800))"); } - let _ = dioxus::router::navigator().push("/admin"); + let _ = dioxus::router::navigator().push(Route::Admin {}); } Ok(CreatePostResponse { success: false, @@ -262,7 +263,7 @@ pub fn Write() -> Element { button { class: "px-6 py-2 bg-gray-200 dark:bg-[#333] text-gray-700 dark:text-[#dadadb] rounded-full font-medium hover:opacity-80 transition-opacity cursor-pointer", onclick: move |_| { - let _ = dioxus::router::navigator().push("/admin"); + let _ = dioxus::router::navigator().push(Route::Admin {}); }, "取消" } diff --git a/src/pages/archives.rs b/src/pages/archives.rs index 79ad30d..f5017f4 100644 --- a/src/pages/archives.rs +++ b/src/pages/archives.rs @@ -1,9 +1,11 @@ use dioxus::prelude::*; +use dioxus::router::components::Link; use crate::api::posts::{list_published_posts, PostListResponse}; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; use crate::components::skeletons::archive_skeleton::ArchiveSkeleton; use crate::models::post::Post; +use crate::router::Route; #[derive(Clone, PartialEq)] struct YearGroup { @@ -185,14 +187,10 @@ fn ArchiveEntry(post: Post) -> Element { div { class: "archive-meta text-sm text-gray-400 dark:text-[#9b9c9d] mt-1", "{date_str}" } - a { + Link { class: "entry-link absolute inset-0 z-10", aria_label: "post link to {post.title}", - href: "/post/{post.slug}", - onclick: move |evt| { - evt.prevent_default(); - dioxus::router::navigator().push(format!("/post/{}", post.slug).as_str()); - }, + to: Route::PostDetail { slug: post.slug.clone() }, } } } diff --git a/src/pages/home.rs b/src/pages/home.rs index cdca908..b4367f9 100644 --- a/src/pages/home.rs +++ b/src/pages/home.rs @@ -1,9 +1,11 @@ use dioxus::prelude::*; +use dioxus::router::components::Link; use crate::api::posts::{list_published_posts, PostListResponse}; use crate::components::post_card::PostCard; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; use crate::components::skeletons::home_skeleton::HomeSkeleton; +use crate::router::Route; const POSTS_PER_PAGE: i32 = 10; @@ -78,30 +80,27 @@ fn HomeInfo() -> Element { fn Pagination(current_page: i32, posts: Vec) -> Element { let has_prev = current_page > 1; let has_next = posts.len() >= POSTS_PER_PAGE as usize; + let prev = current_page - 1; + let prev_route = if prev <= 1 { + Route::Home {} + } else { + Route::HomePage { page: prev } + }; rsx! { nav { class: "flex mt-10 mb-6 justify-between", if has_prev { - button { + Link { class: "inline-flex items-center px-4 py-2 text-sm text-white bg-gray-900 dark:bg-[#dadadb] dark:text-gray-900 rounded-full hover:opacity-80 transition-opacity cursor-pointer", - onclick: move |_| { - let prev = current_page - 1; - if prev <= 1 { - dioxus::router::navigator().push("/"); - } else { - dioxus::router::navigator().push(format!("/page/{}", prev).as_str()); - } - }, + to: prev_route, span { class: "mr-1", "«" } "上一页" } } if has_next { - button { + Link { class: "ml-auto inline-flex items-center px-4 py-2 text-sm text-white bg-gray-900 dark:bg-[#dadadb] dark:text-gray-900 rounded-full hover:opacity-80 transition-opacity cursor-pointer", - onclick: move |_| { - dioxus::router::navigator().push(format!("/page/{}", current_page + 1).as_str()); - }, + to: Route::HomePage { page: current_page + 1 }, "下一页" span { class: "ml-1", "»" } } diff --git a/src/pages/login.rs b/src/pages/login.rs index e7526b8..81df2c6 100644 --- a/src/pages/login.rs +++ b/src/pages/login.rs @@ -1,6 +1,8 @@ use dioxus::prelude::*; +use dioxus::router::components::Link; use crate::api::auth::{login, AuthResponse}; +use crate::router::Route; #[component] pub fn Login() -> Element { @@ -21,7 +23,7 @@ pub fn Login() -> Element { token: Some(_token), .. }) => { - let _ = dioxus::router::navigator().push("/admin"); + let _ = dioxus::router::navigator().push(Route::Admin {}); } Ok(AuthResponse { success: false, @@ -89,9 +91,9 @@ pub fn Login() -> Element { onclick: move |_| on_submit(()), "登录" } - button { + Link { class: "block w-full py-2 px-4 text-center text-gray-500 dark:text-[#9b9c9d] hover:text-gray-700 dark:hover:text-[#dadadb] font-medium rounded-lg transition-colors cursor-pointer", - onclick: move |_| { dioxus::router::navigator().push("/register"); }, + to: Route::Register {}, "还没有账号?去注册" } } diff --git a/src/pages/post_detail.rs b/src/pages/post_detail.rs index b55f94f..1632fa6 100644 --- a/src/pages/post_detail.rs +++ b/src/pages/post_detail.rs @@ -1,4 +1,5 @@ use dioxus::prelude::*; +use dioxus::router::components::Link; use crate::api::posts::{get_post_by_slug, SinglePostResponse}; use crate::components::post::post_content::PostContent; @@ -8,19 +9,21 @@ use crate::components::post::post_header::PostHeader; use crate::components::post::post_toc::PostToc; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; use crate::components::skeletons::post_detail_skeleton::PostDetailSkeleton; +use crate::router::Route; #[component] pub fn PostDetail(slug: String) -> Element { - rsx! { - PostDetailContent { slug: slug.clone() } + let mut slug_signal = use_signal(|| slug.clone()); + if slug_signal() != slug { + slug_signal.set(slug.clone()); } -} -#[component] -fn PostDetailContent(slug: String) -> Element { - let post_res = use_server_future(move || get_post_by_slug(slug.clone()))?; + let post = use_server_future(move || { + let s = slug_signal(); + get_post_by_slug(s) + })?; - let post_data = post_res.read().as_ref().map(|r| match r { + 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"), @@ -57,11 +60,9 @@ fn PostDetailContent(slug: String) -> Element { p { class: "text-paper-secondary mb-6", "这篇文章可能已被删除或移动。" } - button { + Link { 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("/"); - }, + to: Route::Home {}, "返回首页" } } diff --git a/src/pages/register.rs b/src/pages/register.rs index 73008c7..09676a5 100644 --- a/src/pages/register.rs +++ b/src/pages/register.rs @@ -1,6 +1,8 @@ use dioxus::prelude::*; +use dioxus::router::components::Link; use crate::api::auth::{register, AuthResponse}; +use crate::router::Route; #[component] pub fn Register() -> Element { @@ -60,8 +62,8 @@ pub fn Register() -> Element { if success() { div { class: "mb-4 p-3 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-lg text-center", "注册成功!" - button { class: "block mt-2 text-gray-700 dark:text-[#dadadb] hover:underline cursor-pointer", - onclick: move |_| { dioxus::router::navigator().push("/login"); }, + Link { class: "block mt-2 text-gray-700 dark:text-[#dadadb] hover:underline cursor-pointer", + to: Route::Login {}, "去登录" } } @@ -130,8 +132,8 @@ pub fn Register() -> Element { } p { class: "mt-4 text-center text-sm text-gray-500 dark:text-[#9b9c9d]", "已有账号?" - button { class: "text-gray-700 dark:text-[#dadadb] hover:underline cursor-pointer", - onclick: move |_| { dioxus::router::navigator().push("/login"); }, + Link { class: "text-gray-700 dark:text-[#dadadb] hover:underline cursor-pointer", + to: Route::Login {}, "去登录" } } diff --git a/src/pages/tags.rs b/src/pages/tags.rs index c2e6752..fd1afbe 100644 --- a/src/pages/tags.rs +++ b/src/pages/tags.rs @@ -1,9 +1,11 @@ use dioxus::prelude::*; +use dioxus::router::components::Link; use crate::api::posts::{get_posts_by_tag, list_tags, PostListResponse, TagListResponse}; use crate::components::post_card::PostCard; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; use crate::components::skeletons::tags_skeleton::{TagsSkeleton, TagDetailSkeleton}; +use crate::router::Route; #[component] pub fn Tags() -> Element { @@ -40,13 +42,9 @@ fn TagsContent() -> Element { ul { class: "flex flex-wrap gap-4 mt-6", for tag in tags { li { - a { + Link { class: "inline-flex items-center px-3 py-1.5 text-base font-medium bg-gray-100 dark:bg-[#2e2e33] text-gray-700 dark:text-[#9b9c9d] rounded-lg hover:bg-gray-200 dark:hover:bg-[#333] transition-colors", - href: "/tags/{tag.name}", - onclick: move |evt| { - evt.prevent_default(); - dioxus::router::navigator().push(format!("/tags/{}", tag.name).as_str()); - }, + to: Route::TagDetail { tag: tag.name.clone() }, "{tag.name}" sup { class: "ml-1 text-sm text-gray-500 dark:text-[#9b9c9d]", "{tag.post_count}" } }