1use dioxus::prelude::*;
8use std::sync::Arc;
9
10use crate::components::admin_layout::AdminLayout;
11use crate::components::frontend_layout::FrontendLayout;
12use crate::context::UserContext;
13use crate::pages::about::About;
14use crate::pages::admin::{
15 Admin, AdminComments, AdminCommentsPage, Assets, FriendsAdmin, Logs, Mcp, PostPreview, Posts,
16 PostsTrash, Profile, Runner, SiteSettingsPage, System, Write, WriteEdit,
17};
18use crate::pages::archives::Archives;
19use crate::pages::changelog::Changelog;
20use crate::pages::friends::Friends;
21use crate::pages::home::{Home, HomePage};
22use crate::pages::login::Login;
23use crate::pages::not_found::NotFound;
24use crate::pages::post_detail::PostDetail;
25use crate::pages::register::Register;
26use crate::pages::search::Search;
27use crate::pages::tags::{TagDetail, Tags};
28use crate::theme::{use_theme_provider, ThemePreload};
29
30#[derive(Clone, Routable, Debug, PartialEq)]
32#[rustfmt::skip]
33pub enum Route {
34 #[layout(ErrorLayout)]
36 #[layout(FrontendLayout)]
37 #[route("/")]
39 Home {},
40 #[route("/page/:page")]
42 HomePage { page: i32 },
43 #[route("/archives")]
45 Archives {},
46 #[route("/tags")]
48 Tags {},
49 #[route("/tags/:tag")]
51 TagDetail { tag: String },
52 #[route("/post/:slug")]
54 PostDetail { slug: String },
55 #[route("/search")]
57 Search {},
58 #[route("/about")]
60 About {},
61 #[route("/friends")]
63 Friends {},
64 #[route("/changelog")]
66 Changelog {},
67 #[route("/:..segments")]
69 NotFound { segments: Vec<String> },
70 #[end_layout]
71 #[end_layout]
72
73 #[nest("/admin")]
75 #[layout(AdminLayout)]
77 #[route("/")]
79 Admin {},
80 #[route("/write")]
82 Write {},
83 #[route("/write/:id")]
85 WriteEdit { id: i32 },
86 #[route("/preview/:slug")]
88 PostPreview { slug: String },
89 #[route("/posts")]
91 Posts {},
92 #[route("/posts/trash")]
94 PostsTrash {},
95 #[route("/assets")]
97 Assets {},
98 #[route("/friends")]
100 FriendsAdmin {},
101 #[route("/comments")]
103 AdminComments {},
104 #[route("/comments/:page")]
106 AdminCommentsPage { page: i32 },
107 #[route("/system")]
109 System {},
110 #[route("/runner")]
112 Runner {},
113 #[route("/mcp")]
115 Mcp {},
116 #[route("/logs")]
118 Logs {},
119 #[route("/settings")]
121 SiteSettingsPage {},
122 #[route("/profile")]
124 Profile {},
125 #[end_layout]
126 #[end_nest]
127
128 #[route("/login")]
130 Login {},
131 #[route("/register")]
133 Register {},
134}
135
136#[component]
140pub fn AppRouter() -> Element {
141 let _theme = use_theme_provider();
142
143 let user = use_signal(|| None::<Arc<crate::models::user::PublicUser>>);
145 let checked = use_signal(|| false);
146 use_context_provider(|| UserContext { user, checked });
147
148 rsx! {
149 document::Stylesheet { href: "/style.css" }
150 document::Stylesheet { href: "/highlight.css" }
151 document::Title { "Yggdrasil Blog" }
152 document::Link { rel: "icon", href: "/favicon.ico" }
153 div {
154 ThemePreload {}
155 Router::<Route> {}
156 }
157 }
158}
159
160#[component]
161fn ErrorLayout() -> Element {
162 rsx! {
163 ErrorBoundary {
164 handle_error: move |err: ErrorContext| {
165 let err_ctx = err.clone();
170 #[cfg(feature = "server")]
172 {
173 if let Some(captured_error) = err.error() {
174 let _ = dioxus::fullstack::FullstackContext::commit_error_status(
175 captured_error,
176 );
177 }
178 }
179 let mut is_404 = false;
180 if let Some(captured_error) = err.error() {
181 if let Some(ServerFnError::ServerError { code, message, .. }) = captured_error
182 .downcast_ref::<ServerFnError>()
183
184 {
185 if *code == 404 || message == "not_found" {
186 is_404 = true;
187 }
188 } else { let err_str = format!("{:?}", captured_error);
190 if err_str.contains("NotFound") || err_str.contains("404")
191 || err_str.contains("not found")
192 {
193 is_404 = true;
194 }
195 }
196 }
197 if is_404 {
198 rsx! {
199 NotFound { segments: vec![] }
200 }
201 } else {
202 rsx! {
203 div { class: "flex flex-col items-center justify-center min-h-[50vh] md:min-h-[55vh] px-6 animate-page-enter",
204 div { class: "w-full max-w-md bg-paper-entry border border-paper-border rounded-[2rem] p-8 md:p-10 shadow-sm hover:border-paper-secondary transition-all duration-300 flex flex-col items-center text-center",
205 div { class: "relative mb-6",
207 div { class: "w-16 h-16 bg-red-500/10 dark:bg-red-500/20 text-red-500 rounded-full flex items-center justify-center",
208 svg {
209 xmlns: "http://www.w3.org/2000/svg",
210 width: "28",
211 height: "28",
212 view_box: "0 0 24 24",
213 fill: "none",
214 stroke: "currentColor",
215 stroke_width: "2",
216 stroke_linecap: "round",
217 stroke_linejoin: "round",
218 circle { cx: "12", cy: "12", r: "10" }
219 line {
220 x1: "12",
221 y1: "8",
222 x2: "12",
223 y2: "12",
224 }
225 line {
226 x1: "12",
227 y1: "16",
228 x2: "12.01",
229 y2: "16",
230 }
231 }
232 }
233 }
234
235 h1 { class: "text-lg md:text-xl font-bold tracking-tight text-paper-primary mb-3",
237 "加载失败"
238 }
239 p { class: "text-sm text-paper-secondary leading-relaxed mb-8 max-w-[280px]",
240 "抱歉,加载页面时出现了一些错误,请稍后再试。"
241 }
242
243 button {
247 r#type: "button",
248 onclick: {
249 let err_ctx = err_ctx.clone();
250 move |_| {
251 err_ctx.clear_errors();
252 let _ = dioxus::router::navigator().push(Route::Home {});
253 }
254 },
255 class: "group inline-flex items-center gap-2 px-6 py-2.5 text-sm font-medium text-paper-primary bg-paper-theme border border-paper-border rounded-full hover:border-paper-secondary hover:bg-paper-border transition-all duration-200 cursor-pointer shadow-sm active:scale-[0.98]",
256 svg {
257 xmlns: "http://www.w3.org/2000/svg",
258 width: "16",
259 height: "16",
260 view_box: "0 0 24 24",
261 fill: "none",
262 stroke: "currentColor",
263 stroke_width: "2",
264 stroke_linecap: "round",
265 stroke_linejoin: "round",
266 class: "transition-transform group-hover:-translate-x-0.5",
267 path { d: "M19 12H5M12 19l-7-7 7-7" }
268 }
269 "返回首页"
270 }
271 }
272 }
273 }
274 }
275 },
276 Outlet::<Route> {}
277 }
278 }
279}