1use dioxus::prelude::*;
8use dioxus::router::components::HistoryProvider;
9use std::sync::Arc;
10
11use crate::components::admin_layout::AdminLayout;
12use crate::components::frontend_layout::FrontendLayout;
13use crate::context::UserContext;
14use crate::pages::about::About;
15use crate::pages::admin::note_editor::{EditNote, NewNote};
16use crate::pages::admin::notes::{AdminNotebooks, AdminNotes};
17use crate::pages::admin::{
18 Admin, AdminComments, AdminCommentsPage, Assets, FriendsAdmin, Logs, Mcp, PostPreview, Posts,
19 PostsTrash, Profile, Runner, SiteSettingsPage, System, Write, WriteEdit,
20};
21use crate::pages::archives::Archives;
22use crate::pages::changelog::Changelog;
23use crate::pages::components_showcase::{ComponentDetail, ComponentShowcase};
24use crate::pages::friends::Friends;
25use crate::pages::home::{Home, HomePage};
26use crate::pages::login::Login;
27use crate::pages::not_found::NotFound;
28use crate::pages::notes::{NoteDetail, NotebookDetail, Notes};
29use crate::pages::post_detail::PostDetail;
30use crate::pages::register::Register;
31use crate::pages::search::Search;
32use crate::pages::tags::TagDetail;
33use crate::pages::writing_guide::WritingGuide;
34use crate::theme::{use_theme_provider, ThemePreload};
35
36#[derive(Clone, Routable, Debug, PartialEq)]
38#[rustfmt::skip]
39pub enum Route {
40 #[layout(NavigationLayout)]
42 #[layout(ErrorLayout)]
44 #[layout(FrontendLayout)]
45 #[route("/")]
47 Home {},
48 #[route("/page/:page")]
50 HomePage { page: i32 },
51 #[route("/notes")]
52 Notes {},
53 #[route("/notes/book/:id")]
54 NotebookDetail { id: i32 },
55 #[route("/notes/:slug")]
56 NoteDetail { slug: String },
57 #[redirect("/tags", || Route::Archives {})]
59 #[route("/archives")]
60 Archives {},
61 #[route("/tags/:tag")]
63 TagDetail { tag: String },
64 #[route("/post/:slug")]
66 PostDetail { slug: String },
67 #[route("/search")]
69 Search {},
70 #[route("/about")]
72 About {},
73 #[route("/about/writing")]
75 WritingGuide {},
76 #[route("/about/components")]
78 ComponentShowcase {},
79 #[route("/about/components/:component")]
81 ComponentDetail { component: String },
82 #[route("/friends")]
84 Friends {},
85 #[route("/changelog")]
87 Changelog {},
88 #[route("/:..segments")]
90 NotFound { segments: Vec<String> },
91 #[end_layout]
92 #[end_layout]
93
94 #[nest("/admin")]
96 #[layout(AdminLayout)]
98 #[route("/")]
100 Admin {},
101 #[route("/write")]
103 Write {},
104 #[route("/write/:id")]
106 WriteEdit { id: i32 },
107 #[route("/preview/:slug")]
109 PostPreview { slug: String },
110 #[route("/posts")]
112 Posts {},
113 #[route("/notes")]
114 AdminNotes {},
115 #[route("/notes/new")]
116 NewNote {},
117 #[route("/notes/edit/:id")]
118 EditNote { id: i32 },
119 #[route("/notebooks")]
120 AdminNotebooks {},
121 #[route("/posts/trash")]
123 PostsTrash {},
124 #[route("/assets")]
126 Assets {},
127 #[route("/friends")]
129 FriendsAdmin {},
130 #[route("/comments")]
132 AdminComments {},
133 #[route("/comments/:page")]
135 AdminCommentsPage { page: i32 },
136 #[route("/system")]
138 System {},
139 #[route("/runner")]
141 Runner {},
142 #[route("/mcp")]
144 Mcp {},
145 #[route("/logs")]
147 Logs {},
148 #[route("/settings")]
150 SiteSettingsPage {},
151 #[route("/profile")]
153 Profile {},
154 #[end_layout]
155 #[end_nest]
156
157 #[route("/login")]
159 Login {},
160 #[route("/register")]
162 Register {},
163}
164
165#[component]
169pub fn AppRouter() -> Element {
170 let _theme = use_theme_provider();
171
172 let user = use_signal(|| None::<Arc<crate::models::user::PublicUser>>);
174 let checked = use_signal(|| false);
175 use_context_provider(|| UserContext { user, checked });
176 let showcase_filters = use_signal(crate::pages::components_showcase::ShowcaseFilters::default);
177 use_context_provider(|| showcase_filters);
178
179 rsx! {
180 document::Title { "Yggdrasil Blog" }
181 document::Link { rel: "icon", href: "/favicon.ico" }
182 div {
183 ThemePreload {}
184 HistoryProvider {
185 history: move |_| crate::navigation::transition_history(dioxus::history::history()),
186 Router::<Route> {}
187 }
188 }
189 }
190}
191
192#[component]
193fn NavigationLayout() -> Element {
194 let route = dioxus::router::router().full_route_string();
195 let navigation = crate::bridges::navigation::navigation_id();
196 use_effect(use_reactive(
197 (&route, &navigation),
198 |(route, navigation)| {
199 crate::bridges::navigation::rendered(navigation, &route);
200 },
201 ));
202
203 rsx! {
204 div {
205 style: "display: contents",
206 "data-vt-route": route,
207 "data-vt-navigation": navigation,
208 Outlet::<Route> {}
209 }
210 }
211}
212
213#[component]
214fn ErrorLayout() -> Element {
215 rsx! {
216 ErrorBoundary {
217 handle_error: move |err: ErrorContext| {
218 let err_ctx = err.clone();
223 #[cfg(feature = "server")]
225 {
226 if let Some(captured_error) = err.error() {
227 let _ = dioxus::fullstack::FullstackContext::commit_error_status(
228 captured_error,
229 );
230 }
231 }
232 let mut is_404 = false;
233 if let Some(captured_error) = err.error() {
234 if let Some(ServerFnError::ServerError { code, message, .. }) = captured_error
235 .downcast_ref::<ServerFnError>()
236
237 {
238 if *code == 404 || message == "not_found" {
239 is_404 = true;
240 }
241 } else { let err_str = format!("{:?}", captured_error);
243 if err_str.contains("NotFound") || err_str.contains("404")
244 || err_str.contains("not found")
245 {
246 is_404 = true;
247 }
248 }
249 }
250 if is_404 {
251 rsx! {
252 NotFound { segments: vec![] }
253 }
254 } else {
255 rsx! {
256 div { class: "flex flex-col items-center justify-center min-h-[50vh] md:min-h-[55vh] px-6 animate-page-enter",
257 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",
258 div { class: "relative mb-6",
260 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",
261 svg {
262 xmlns: "http://www.w3.org/2000/svg",
263 width: "28",
264 height: "28",
265 view_box: "0 0 24 24",
266 fill: "none",
267 stroke: "currentColor",
268 stroke_width: "2",
269 stroke_linecap: "round",
270 stroke_linejoin: "round",
271 circle { cx: "12", cy: "12", r: "10" }
272 line {
273 x1: "12",
274 y1: "8",
275 x2: "12",
276 y2: "12",
277 }
278 line {
279 x1: "12",
280 y1: "16",
281 x2: "12.01",
282 y2: "16",
283 }
284 }
285 }
286 }
287
288 h1 { class: "text-lg md:text-xl font-bold tracking-tight text-paper-primary mb-3",
290 "加载失败"
291 }
292 p { class: "text-sm text-paper-secondary leading-relaxed mb-8 max-w-[280px]",
293 "抱歉,加载页面时出现了一些错误,请稍后再试。"
294 }
295
296 button {
300 r#type: "button",
301 onclick: {
302 let err_ctx = err_ctx.clone();
303 move |_| {
304 err_ctx.clear_errors();
305 let _ = dioxus::router::navigator().push(Route::Home {});
306 }
307 },
308 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]",
309 svg {
310 xmlns: "http://www.w3.org/2000/svg",
311 width: "16",
312 height: "16",
313 view_box: "0 0 24 24",
314 fill: "none",
315 stroke: "currentColor",
316 stroke_width: "2",
317 stroke_linecap: "round",
318 stroke_linejoin: "round",
319 class: "transition-transform group-hover:-translate-x-0.5",
320 path { d: "M19 12H5M12 19l-7-7 7-7" }
321 }
322 "返回首页"
323 }
324 }
325 }
326 }
327 }
328 },
329 Outlet::<Route> {}
330 }
331 }
332}