refactor: replace string-based navigation with typed Route and Link components
This commit is contained in:
parent
aebdcd6d99
commit
ce14c476b5
@ -26,11 +26,11 @@ pub fn AdminLayout() -> Element {
|
|||||||
if let Some(user) = response.user {
|
if let Some(user) = response.user {
|
||||||
ctx.user.set(Some(std::sync::Arc::new(user)));
|
ctx.user.set(Some(std::sync::Arc::new(user)));
|
||||||
} else {
|
} else {
|
||||||
let _ = navigator.push("/login");
|
let _ = navigator.push(Route::Login {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let _ = navigator.push("/login");
|
let _ = navigator.push(Route::Login {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -39,22 +39,22 @@ pub fn AdminLayout() -> Element {
|
|||||||
|
|
||||||
let admin_nav_items = vec![
|
let admin_nav_items = vec![
|
||||||
NavItemConfig {
|
NavItemConfig {
|
||||||
href: "/admin",
|
route: Route::Admin {},
|
||||||
label: "仪表盘",
|
label: "仪表盘",
|
||||||
is_active: matches!(route, Route::Admin {}),
|
is_active: matches!(route, Route::Admin {}),
|
||||||
},
|
},
|
||||||
NavItemConfig {
|
NavItemConfig {
|
||||||
href: "/admin/write",
|
route: Route::Write {},
|
||||||
label: "写文章",
|
label: "写文章",
|
||||||
is_active: matches!(route, Route::Write {}),
|
is_active: matches!(route, Route::Write {}),
|
||||||
},
|
},
|
||||||
NavItemConfig {
|
NavItemConfig {
|
||||||
href: "/admin/posts",
|
route: Route::Posts {},
|
||||||
label: "管理文章",
|
label: "管理文章",
|
||||||
is_active: matches!(route, Route::Posts {}),
|
is_active: matches!(route, Route::Posts {}),
|
||||||
},
|
},
|
||||||
NavItemConfig {
|
NavItemConfig {
|
||||||
href: "/",
|
route: Route::Home {},
|
||||||
label: "前台",
|
label: "前台",
|
||||||
is_active: false,
|
is_active: false,
|
||||||
},
|
},
|
||||||
@ -66,7 +66,7 @@ pub fn AdminLayout() -> Element {
|
|||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let _ = logout().await;
|
let _ = logout().await;
|
||||||
let _ = navigator.push("/login");
|
let _ = navigator.push(Route::Login {});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
"登出"
|
"登出"
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct NavItemConfig {
|
pub struct NavItemConfig {
|
||||||
pub href: &'static str,
|
pub route: Route,
|
||||||
pub label: &'static str,
|
pub label: &'static str,
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
}
|
}
|
||||||
@ -12,20 +15,16 @@ pub fn Header(nav_items: Vec<NavItemConfig>, right_content: Element) -> Element
|
|||||||
rsx! {
|
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",
|
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",
|
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",
|
class: "text-2xl font-bold text-gray-900 dark:text-[#dadadb] hover:opacity-80 transition-opacity",
|
||||||
href: "/",
|
to: Route::Home {},
|
||||||
onclick: move |evt| {
|
|
||||||
evt.prevent_default();
|
|
||||||
dioxus::router::navigator().push("/");
|
|
||||||
},
|
|
||||||
"Yggdrasil"
|
"Yggdrasil"
|
||||||
}
|
}
|
||||||
div { class: "flex items-center gap-2",
|
div { class: "flex items-center gap-2",
|
||||||
ul { class: "hidden md:flex items-center gap-1",
|
ul { class: "hidden md:flex items-center gap-1",
|
||||||
for item in nav_items.iter().cloned() {
|
for item in nav_items.iter().cloned() {
|
||||||
NavItem {
|
NavItem {
|
||||||
href: item.href,
|
route: item.route,
|
||||||
label: item.label,
|
label: item.label,
|
||||||
is_active: item.is_active,
|
is_active: item.is_active,
|
||||||
}
|
}
|
||||||
@ -39,7 +38,7 @@ pub fn Header(nav_items: Vec<NavItemConfig>, right_content: Element) -> Element
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[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 base_class = "px-3 py-1 text-base rounded-lg transition-colors";
|
||||||
let class_str = if is_active {
|
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)
|
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! {
|
rsx! {
|
||||||
li {
|
li {
|
||||||
a {
|
Link {
|
||||||
class: "{class_str}",
|
class: "{class_str}",
|
||||||
href: "{href}",
|
to: route,
|
||||||
onclick: move |evt| {
|
|
||||||
evt.prevent_default();
|
|
||||||
dioxus::router::navigator().push(href);
|
|
||||||
},
|
|
||||||
"{label}"
|
"{label}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,27 +4,27 @@ use crate::router::Route;
|
|||||||
pub fn use_nav_items(route: Route) -> Vec<NavItemConfig> {
|
pub fn use_nav_items(route: Route) -> Vec<NavItemConfig> {
|
||||||
vec![
|
vec![
|
||||||
NavItemConfig {
|
NavItemConfig {
|
||||||
href: "/",
|
route: Route::Home {},
|
||||||
label: "首页",
|
label: "首页",
|
||||||
is_active: matches!(route, Route::Home {}),
|
is_active: matches!(route, Route::Home {}),
|
||||||
},
|
},
|
||||||
NavItemConfig {
|
NavItemConfig {
|
||||||
href: "/archives",
|
route: Route::Archives {},
|
||||||
label: "归档",
|
label: "归档",
|
||||||
is_active: matches!(route, Route::Archives {}),
|
is_active: matches!(route, Route::Archives {}),
|
||||||
},
|
},
|
||||||
NavItemConfig {
|
NavItemConfig {
|
||||||
href: "/tags",
|
route: Route::Tags {},
|
||||||
label: "标签",
|
label: "标签",
|
||||||
is_active: matches!(route, Route::Tags {}) || matches!(route, Route::TagDetail { .. }),
|
is_active: matches!(route, Route::Tags {}) || matches!(route, Route::TagDetail { .. }),
|
||||||
},
|
},
|
||||||
NavItemConfig {
|
NavItemConfig {
|
||||||
href: "/search",
|
route: Route::Search {},
|
||||||
label: "搜索",
|
label: "搜索",
|
||||||
is_active: matches!(route, Route::Search {}),
|
is_active: matches!(route, Route::Search {}),
|
||||||
},
|
},
|
||||||
NavItemConfig {
|
NavItemConfig {
|
||||||
href: "/about",
|
route: Route::About {},
|
||||||
label: "关于",
|
label: "关于",
|
||||||
is_active: matches!(route, Route::About {}),
|
is_active: matches!(route, Route::About {}),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Breadcrumbs(title: String) -> Element {
|
pub fn Breadcrumbs(title: String) -> Element {
|
||||||
@ -7,12 +10,8 @@ pub fn Breadcrumbs(title: String) -> Element {
|
|||||||
class: "breadcrumbs",
|
class: "breadcrumbs",
|
||||||
role: "navigation",
|
role: "navigation",
|
||||||
aria_label: "Breadcrumb",
|
aria_label: "Breadcrumb",
|
||||||
a {
|
Link {
|
||||||
href: "/",
|
to: Route::Home {},
|
||||||
onclick: move |evt| {
|
|
||||||
evt.prevent_default();
|
|
||||||
dioxus::router::navigator().push("/");
|
|
||||||
},
|
|
||||||
"Home"
|
"Home"
|
||||||
}
|
}
|
||||||
svg {
|
svg {
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::components::post::post_nav_links::PostNavLinks;
|
use crate::components::post::post_nav_links::PostNavLinks;
|
||||||
use crate::models::post::Post;
|
use crate::models::post::Post;
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn PostFooter(post: Post) -> Element {
|
pub fn PostFooter(post: Post) -> Element {
|
||||||
@ -13,12 +15,8 @@ pub fn PostFooter(post: Post) -> Element {
|
|||||||
ul { class: "post-tags",
|
ul { class: "post-tags",
|
||||||
for tag in tags.into_iter() {
|
for tag in tags.into_iter() {
|
||||||
li {
|
li {
|
||||||
a {
|
Link {
|
||||||
href: "/tags/{tag}",
|
to: Route::TagDetail { tag: tag.clone() },
|
||||||
onclick: move |evt| {
|
|
||||||
evt.prevent_default();
|
|
||||||
dioxus::router::navigator().push(format!("/tags/{}", tag));
|
|
||||||
},
|
|
||||||
"{tag}"
|
"{tag}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,10 +32,8 @@ pub fn PostFooter(post: Post) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
div { class: "back-to-home",
|
div { class: "back-to-home",
|
||||||
button {
|
Link {
|
||||||
onclick: move |_| {
|
to: Route::Home {},
|
||||||
let _ = dioxus::router::navigator().push("/");
|
|
||||||
},
|
|
||||||
"← Back to Home"
|
"← Back to Home"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,25 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::models::post::PostNav;
|
use crate::models::post::PostNav;
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn PostNavLinks(prev: Option<PostNav>, next: Option<PostNav>) -> Element {
|
pub fn PostNavLinks(prev: Option<PostNav>, next: Option<PostNav>) -> 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! {
|
rsx! {
|
||||||
nav { class: "paginav",
|
nav { class: "paginav",
|
||||||
if let Some(prev_post) = prev {
|
if let Some(prev_post) = prev {
|
||||||
a {
|
Link {
|
||||||
class: "prev",
|
class: "prev",
|
||||||
href: "/post/{prev_post.slug}",
|
to: Route::PostDetail { slug: prev_post.slug.clone() },
|
||||||
onclick: move |evt| {
|
onclick: move |evt: dioxus::events::MouseEvent| {
|
||||||
evt.prevent_default();
|
println!("[PostNavLinks] clicked prev: {}", prev_post.slug);
|
||||||
dioxus::router::navigator().push(format!("/post/{}", prev_post.slug));
|
|
||||||
},
|
},
|
||||||
span { class: "title", "« Prev" }
|
span { class: "title", "« Prev" }
|
||||||
span { class: "post-title-nav", "{prev_post.title}" }
|
span { class: "post-title-nav", "{prev_post.title}" }
|
||||||
@ -22,12 +29,11 @@ pub fn PostNavLinks(prev: Option<PostNav>, next: Option<PostNav>) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(next_post) = next {
|
if let Some(next_post) = next {
|
||||||
a {
|
Link {
|
||||||
class: "next",
|
class: "next",
|
||||||
href: "/post/{next_post.slug}",
|
to: Route::PostDetail { slug: next_post.slug.clone() },
|
||||||
onclick: move |evt| {
|
onclick: move |evt: dioxus::events::MouseEvent| {
|
||||||
evt.prevent_default();
|
println!("[PostNavLinks] clicked next: {}", next_post.slug);
|
||||||
dioxus::router::navigator().push(format!("/post/{}", next_post.slug));
|
|
||||||
},
|
},
|
||||||
span { class: "title", "Next »" }
|
span { class: "title", "Next »" }
|
||||||
span { class: "post-title-nav", "{next_post.title}" }
|
span { class: "post-title-nav", "{next_post.title}" }
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::models::post::Post;
|
use crate::models::post::Post;
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn PostCard(post: Post) -> Element {
|
pub fn PostCard(post: Post) -> Element {
|
||||||
@ -10,20 +12,16 @@ pub fn PostCard(post: Post) -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
article {
|
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",
|
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",
|
class: "block group",
|
||||||
href: "/post/{post_slug}",
|
to: Route::PostDetail { slug: post_slug },
|
||||||
onclick: move |evt| {
|
|
||||||
evt.prevent_default();
|
|
||||||
dioxus::router::navigator().push(format!("/post/{}", post_slug).as_str());
|
|
||||||
},
|
|
||||||
h2 {
|
h2 {
|
||||||
class: "text-2xl font-bold leading-tight text-gray-900 dark:text-[#dadadb] group-hover:opacity-80 transition-opacity",
|
class: "text-2xl font-bold leading-tight text-gray-900 dark:text-[#dadadb] group-hover:opacity-80 transition-opacity",
|
||||||
"{post.title}"
|
"{post.title}"
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
class: "mt-2 text-sm text-gray-500 dark:text-[#9b9c9d] leading-relaxed line-clamp-2",
|
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 {
|
div {
|
||||||
class: "mt-3 flex items-center gap-3 text-[13px] text-gray-400 dark:text-[#9b9c9d]",
|
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 { "·" }
|
span { "·" }
|
||||||
for tag in post.tags.clone().into_iter() {
|
for tag in post.tags.clone().into_iter() {
|
||||||
span {
|
span {
|
||||||
a {
|
Link {
|
||||||
class: "hover:text-gray-600 dark:hover:text-[#dadadb] transition-colors",
|
class: "hover:text-gray-600 dark:hover:text-[#dadadb] transition-colors",
|
||||||
href: "/tags/{tag}",
|
to: Route::TagDetail { tag: tag.clone() },
|
||||||
onclick: move |evt| {
|
onclick: move |evt: dioxus::events::MouseEvent| evt.stop_propagation(),
|
||||||
evt.prevent_default();
|
|
||||||
evt.stop_propagation();
|
|
||||||
dioxus::router::navigator().push(format!("/tags/{}", tag).as_str());
|
|
||||||
},
|
|
||||||
"{tag}"
|
"{tag}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::api::posts::{get_post_stats, list_posts, PostListResponse, PostStatsResponse};
|
use crate::api::posts::{get_post_stats, list_posts, PostListResponse, PostStatsResponse};
|
||||||
use crate::hooks::delayed_loading::use_delayed_loading;
|
use crate::hooks::delayed_loading::use_delayed_loading;
|
||||||
use crate::models::post::Post;
|
use crate::models::post::Post;
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Admin() -> Element {
|
pub fn Admin() -> Element {
|
||||||
@ -36,18 +38,14 @@ pub fn Admin() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
div { class: "grid grid-cols-1 md:grid-cols-2 gap-4",
|
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",
|
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 |_| {
|
to: Route::Write {},
|
||||||
dioxus::router::navigator().push("/admin/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",
|
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 |_| {
|
to: Route::Posts {},
|
||||||
dioxus::router::navigator().push("/admin/posts");
|
|
||||||
},
|
|
||||||
"管理文章"
|
"管理文章"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::api::posts::{delete_post, list_posts, CreatePostResponse, PostListResponse};
|
use crate::api::posts::{delete_post, list_posts, CreatePostResponse, PostListResponse};
|
||||||
use crate::hooks::delayed_loading::use_delayed_loading;
|
use crate::hooks::delayed_loading::use_delayed_loading;
|
||||||
use crate::models::post::{Post, PostStatus};
|
use crate::models::post::{Post, PostStatus};
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Posts() -> Element {
|
pub fn Posts() -> Element {
|
||||||
@ -16,11 +18,9 @@ pub fn Posts() -> Element {
|
|||||||
h1 { class: "text-2xl font-bold text-gray-900 dark:text-[#dadadb]",
|
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",
|
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 |_| {
|
to: Route::Write {},
|
||||||
dioxus::router::navigator().push("/admin/write");
|
|
||||||
},
|
|
||||||
"+ 写文章"
|
"+ 写文章"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,13 +123,9 @@ fn PostRow(post: Post, deleting: bool, on_delete: EventHandler<i32>) -> Element
|
|||||||
rsx! {
|
rsx! {
|
||||||
tr { class: "border-b border-gray-100 dark:border-[#333] last:border-0 hover:bg-gray-50 dark:hover:bg-[#2a2a2a] transition-colors",
|
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",
|
td { class: "px-4 py-3",
|
||||||
a {
|
Link {
|
||||||
class: "text-gray-900 dark:text-[#dadadb] hover:opacity-80 transition-opacity",
|
class: "text-gray-900 dark:text-[#dadadb] hover:opacity-80 transition-opacity",
|
||||||
href: "/post/{post.slug}",
|
to: Route::PostDetail { slug: post.slug.clone() },
|
||||||
onclick: move |evt| {
|
|
||||||
evt.prevent_default();
|
|
||||||
dioxus::router::navigator().push(format!("/post/{}", post.slug).as_str());
|
|
||||||
},
|
|
||||||
"{post.title}"
|
"{post.title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ use wasm_bindgen::JsCast;
|
|||||||
|
|
||||||
use crate::api::posts::{create_post, CreatePostResponse};
|
use crate::api::posts::{create_post, CreatePostResponse};
|
||||||
use crate::components::write_skeleton::WriteSkeleton;
|
use crate::components::write_skeleton::WriteSkeleton;
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
#[allow(unused_mut, unused_variables)]
|
#[allow(unused_mut, unused_variables)]
|
||||||
@ -147,7 +148,7 @@ pub fn Write() -> Element {
|
|||||||
{
|
{
|
||||||
let _ = js_sys::eval("new Promise(r => setTimeout(r, 800))");
|
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 {
|
Ok(CreatePostResponse {
|
||||||
success: false,
|
success: false,
|
||||||
@ -262,7 +263,7 @@ pub fn Write() -> Element {
|
|||||||
button {
|
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",
|
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 |_| {
|
onclick: move |_| {
|
||||||
let _ = dioxus::router::navigator().push("/admin");
|
let _ = dioxus::router::navigator().push(Route::Admin {});
|
||||||
},
|
},
|
||||||
"取消"
|
"取消"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::api::posts::{list_published_posts, PostListResponse};
|
use crate::api::posts::{list_published_posts, PostListResponse};
|
||||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||||
use crate::components::skeletons::archive_skeleton::ArchiveSkeleton;
|
use crate::components::skeletons::archive_skeleton::ArchiveSkeleton;
|
||||||
use crate::models::post::Post;
|
use crate::models::post::Post;
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
struct YearGroup {
|
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",
|
div { class: "archive-meta text-sm text-gray-400 dark:text-[#9b9c9d] mt-1",
|
||||||
"{date_str}"
|
"{date_str}"
|
||||||
}
|
}
|
||||||
a {
|
Link {
|
||||||
class: "entry-link absolute inset-0 z-10",
|
class: "entry-link absolute inset-0 z-10",
|
||||||
aria_label: "post link to {post.title}",
|
aria_label: "post link to {post.title}",
|
||||||
href: "/post/{post.slug}",
|
to: Route::PostDetail { slug: post.slug.clone() },
|
||||||
onclick: move |evt| {
|
|
||||||
evt.prevent_default();
|
|
||||||
dioxus::router::navigator().push(format!("/post/{}", post.slug).as_str());
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::api::posts::{list_published_posts, PostListResponse};
|
use crate::api::posts::{list_published_posts, PostListResponse};
|
||||||
use crate::components::post_card::PostCard;
|
use crate::components::post_card::PostCard;
|
||||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||||
use crate::components::skeletons::home_skeleton::HomeSkeleton;
|
use crate::components::skeletons::home_skeleton::HomeSkeleton;
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
const POSTS_PER_PAGE: i32 = 10;
|
const POSTS_PER_PAGE: i32 = 10;
|
||||||
|
|
||||||
@ -78,30 +80,27 @@ fn HomeInfo() -> Element {
|
|||||||
fn Pagination(current_page: i32, posts: Vec<crate::models::post::Post>) -> Element {
|
fn Pagination(current_page: i32, posts: Vec<crate::models::post::Post>) -> Element {
|
||||||
let has_prev = current_page > 1;
|
let has_prev = current_page > 1;
|
||||||
let has_next = posts.len() >= POSTS_PER_PAGE as usize;
|
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! {
|
rsx! {
|
||||||
nav { class: "flex mt-10 mb-6 justify-between",
|
nav { class: "flex mt-10 mb-6 justify-between",
|
||||||
if has_prev {
|
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",
|
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 |_| {
|
to: prev_route,
|
||||||
let prev = current_page - 1;
|
|
||||||
if prev <= 1 {
|
|
||||||
dioxus::router::navigator().push("/");
|
|
||||||
} else {
|
|
||||||
dioxus::router::navigator().push(format!("/page/{}", prev).as_str());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
span { class: "mr-1", "«" }
|
span { class: "mr-1", "«" }
|
||||||
"上一页"
|
"上一页"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if has_next {
|
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",
|
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 |_| {
|
to: Route::HomePage { page: current_page + 1 },
|
||||||
dioxus::router::navigator().push(format!("/page/{}", current_page + 1).as_str());
|
|
||||||
},
|
|
||||||
"下一页"
|
"下一页"
|
||||||
span { class: "ml-1", "»" }
|
span { class: "ml-1", "»" }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::api::auth::{login, AuthResponse};
|
use crate::api::auth::{login, AuthResponse};
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Login() -> Element {
|
pub fn Login() -> Element {
|
||||||
@ -21,7 +23,7 @@ pub fn Login() -> Element {
|
|||||||
token: Some(_token),
|
token: Some(_token),
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
let _ = dioxus::router::navigator().push("/admin");
|
let _ = dioxus::router::navigator().push(Route::Admin {});
|
||||||
}
|
}
|
||||||
Ok(AuthResponse {
|
Ok(AuthResponse {
|
||||||
success: false,
|
success: false,
|
||||||
@ -89,9 +91,9 @@ pub fn Login() -> Element {
|
|||||||
onclick: move |_| on_submit(()),
|
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",
|
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 {},
|
||||||
"还没有账号?去注册"
|
"还没有账号?去注册"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::api::posts::{get_post_by_slug, SinglePostResponse};
|
use crate::api::posts::{get_post_by_slug, SinglePostResponse};
|
||||||
use crate::components::post::post_content::PostContent;
|
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::post::post_toc::PostToc;
|
||||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||||
use crate::components::skeletons::post_detail_skeleton::PostDetailSkeleton;
|
use crate::components::skeletons::post_detail_skeleton::PostDetailSkeleton;
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn PostDetail(slug: String) -> Element {
|
pub fn PostDetail(slug: String) -> Element {
|
||||||
rsx! {
|
let mut slug_signal = use_signal(|| slug.clone());
|
||||||
PostDetailContent { slug: slug.clone() }
|
if slug_signal() != slug {
|
||||||
|
slug_signal.set(slug.clone());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
let post = use_server_future(move || {
|
||||||
fn PostDetailContent(slug: String) -> Element {
|
let s = slug_signal();
|
||||||
let post_res = use_server_future(move || get_post_by_slug(slug.clone()))?;
|
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: Some(post) }) => Ok(post.clone()),
|
||||||
Ok(SinglePostResponse { post: None }) => Err("not_found"),
|
Ok(SinglePostResponse { post: None }) => Err("not_found"),
|
||||||
Err(_) => Err("error"),
|
Err(_) => Err("error"),
|
||||||
@ -57,11 +60,9 @@ fn PostDetailContent(slug: String) -> Element {
|
|||||||
p { class: "text-paper-secondary mb-6",
|
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",
|
class: "px-6 py-2 bg-paper-primary text-paper-theme rounded-full font-medium hover:opacity-80 transition-opacity",
|
||||||
onclick: move |_| {
|
to: Route::Home {},
|
||||||
let _ = dioxus::router::navigator().push("/");
|
|
||||||
},
|
|
||||||
"返回首页"
|
"返回首页"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::api::auth::{register, AuthResponse};
|
use crate::api::auth::{register, AuthResponse};
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Register() -> Element {
|
pub fn Register() -> Element {
|
||||||
@ -60,8 +62,8 @@ pub fn Register() -> Element {
|
|||||||
if success() {
|
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",
|
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",
|
Link { class: "block mt-2 text-gray-700 dark:text-[#dadadb] hover:underline cursor-pointer",
|
||||||
onclick: move |_| { dioxus::router::navigator().push("/login"); },
|
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]",
|
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",
|
Link { class: "text-gray-700 dark:text-[#dadadb] hover:underline cursor-pointer",
|
||||||
onclick: move |_| { dioxus::router::navigator().push("/login"); },
|
to: Route::Login {},
|
||||||
"去登录"
|
"去登录"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus::router::components::Link;
|
||||||
|
|
||||||
use crate::api::posts::{get_posts_by_tag, list_tags, PostListResponse, TagListResponse};
|
use crate::api::posts::{get_posts_by_tag, list_tags, PostListResponse, TagListResponse};
|
||||||
use crate::components::post_card::PostCard;
|
use crate::components::post_card::PostCard;
|
||||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||||
use crate::components::skeletons::tags_skeleton::{TagsSkeleton, TagDetailSkeleton};
|
use crate::components::skeletons::tags_skeleton::{TagsSkeleton, TagDetailSkeleton};
|
||||||
|
use crate::router::Route;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Tags() -> Element {
|
pub fn Tags() -> Element {
|
||||||
@ -40,13 +42,9 @@ fn TagsContent() -> Element {
|
|||||||
ul { class: "flex flex-wrap gap-4 mt-6",
|
ul { class: "flex flex-wrap gap-4 mt-6",
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
li {
|
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",
|
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}",
|
to: Route::TagDetail { tag: tag.name.clone() },
|
||||||
onclick: move |evt| {
|
|
||||||
evt.prevent_default();
|
|
||||||
dioxus::router::navigator().push(format!("/tags/{}", tag.name).as_str());
|
|
||||||
},
|
|
||||||
"{tag.name}"
|
"{tag.name}"
|
||||||
sup { class: "ml-1 text-sm text-gray-500 dark:text-[#9b9c9d]", "{tag.post_count}" }
|
sup { class: "ml-1 text-sm text-gray-500 dark:text-[#9b9c9d]", "{tag.post_count}" }
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user