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::{
16 Admin, AdminComments, AdminCommentsPage, Assets, FriendsAdmin, Logs, Mcp, PostPreview, Posts,
17 PostsTrash, Profile, Runner, SiteSettingsPage, System, Write, WriteEdit,
18};
19use crate::pages::archives::Archives;
20use crate::pages::changelog::Changelog;
21use crate::pages::friends::Friends;
22use crate::pages::home::{Home, HomePage};
23use crate::pages::login::Login;
24use crate::pages::not_found::NotFound;
25use crate::pages::post_detail::PostDetail;
26use crate::pages::register::Register;
27use crate::pages::search::Search;
28use crate::pages::tags::TagDetail;
29use crate::theme::{use_theme_provider, ThemePreload};
30
31#[derive(Clone, Routable, Debug, PartialEq)]
33#[rustfmt::skip]
34pub enum Route {
35 #[layout(NavigationLayout)]
37 #[layout(ErrorLayout)]
39 #[layout(FrontendLayout)]
40 #[route("/")]
42 Home {},
43 #[route("/page/:page")]
45 HomePage { page: i32 },
46 #[redirect("/tags", || Route::Archives {})]
48 #[route("/archives")]
49 Archives {},
50 #[route("/tags/:tag")]
52 TagDetail { tag: String },
53 #[route("/post/:slug")]
55 PostDetail { slug: String },
56 #[route("/search")]
58 Search {},
59 #[route("/about")]
61 About {},
62 #[route("/friends")]
64 Friends {},
65 #[route("/changelog")]
67 Changelog {},
68 #[route("/:..segments")]
70 NotFound { segments: Vec<String> },
71 #[end_layout]
72 #[end_layout]
73
74 #[nest("/admin")]
76 #[layout(AdminLayout)]
78 #[route("/")]
80 Admin {},
81 #[route("/write")]
83 Write {},
84 #[route("/write/:id")]
86 WriteEdit { id: i32 },
87 #[route("/preview/:slug")]
89 PostPreview { slug: String },
90 #[route("/posts")]
92 Posts {},
93 #[route("/posts/trash")]
95 PostsTrash {},
96 #[route("/assets")]
98 Assets {},
99 #[route("/friends")]
101 FriendsAdmin {},
102 #[route("/comments")]
104 AdminComments {},
105 #[route("/comments/:page")]
107 AdminCommentsPage { page: i32 },
108 #[route("/system")]
110 System {},
111 #[route("/runner")]
113 Runner {},
114 #[route("/mcp")]
116 Mcp {},
117 #[route("/logs")]
119 Logs {},
120 #[route("/settings")]
122 SiteSettingsPage {},
123 #[route("/profile")]
125 Profile {},
126 #[end_layout]
127 #[end_nest]
128
129 #[route("/login")]
131 Login {},
132 #[route("/register")]
134 Register {},
135}
136
137#[component]
141pub fn AppRouter() -> Element {
142 let _theme = use_theme_provider();
143
144 let user = use_signal(|| None::<Arc<crate::models::user::PublicUser>>);
146 let checked = use_signal(|| false);
147 use_context_provider(|| UserContext { user, checked });
148
149 rsx! {
150 document::Title { "Yggdrasil Blog" }
151 document::Link { rel: "icon", href: "/favicon.ico" }
152 div {
153 ThemePreload {}
154 HistoryProvider {
155 history: move |_| crate::navigation::transition_history(dioxus::history::history()),
156 Router::<Route> {}
157 }
158 }
159 }
160}
161
162#[component]
163fn NavigationLayout() -> Element {
164 let route = dioxus::router::router().full_route_string();
165 let navigation = crate::bridges::navigation::navigation_id();
166 use_effect(use_reactive(
167 (&route, &navigation),
168 |(route, navigation)| {
169 crate::bridges::navigation::rendered(navigation, &route);
170 },
171 ));
172
173 rsx! {
174 div {
175 style: "display: contents",
176 "data-vt-route": route,
177 "data-vt-navigation": navigation,
178 Outlet::<Route> {}
179 }
180 }
181}
182
183#[component]
184fn ErrorLayout() -> Element {
185 rsx! {
186 ErrorBoundary {
187 handle_error: move |err: ErrorContext| {
188 let err_ctx = err.clone();
193 #[cfg(feature = "server")]
195 {
196 if let Some(captured_error) = err.error() {
197 let _ = dioxus::fullstack::FullstackContext::commit_error_status(
198 captured_error,
199 );
200 }
201 }
202 let mut is_404 = false;
203 if let Some(captured_error) = err.error() {
204 if let Some(ServerFnError::ServerError { code, message, .. }) = captured_error
205 .downcast_ref::<ServerFnError>()
206
207 {
208 if *code == 404 || message == "not_found" {
209 is_404 = true;
210 }
211 } else { let err_str = format!("{:?}", captured_error);
213 if err_str.contains("NotFound") || err_str.contains("404")
214 || err_str.contains("not found")
215 {
216 is_404 = true;
217 }
218 }
219 }
220 if is_404 {
221 rsx! {
222 NotFound { segments: vec![] }
223 }
224 } else {
225 rsx! {
226 div { class: "flex flex-col items-center justify-center min-h-[50vh] md:min-h-[55vh] px-6 animate-page-enter",
227 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",
228 div { class: "relative mb-6",
230 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",
231 svg {
232 xmlns: "http://www.w3.org/2000/svg",
233 width: "28",
234 height: "28",
235 view_box: "0 0 24 24",
236 fill: "none",
237 stroke: "currentColor",
238 stroke_width: "2",
239 stroke_linecap: "round",
240 stroke_linejoin: "round",
241 circle { cx: "12", cy: "12", r: "10" }
242 line {
243 x1: "12",
244 y1: "8",
245 x2: "12",
246 y2: "12",
247 }
248 line {
249 x1: "12",
250 y1: "16",
251 x2: "12.01",
252 y2: "16",
253 }
254 }
255 }
256 }
257
258 h1 { class: "text-lg md:text-xl font-bold tracking-tight text-paper-primary mb-3",
260 "加载失败"
261 }
262 p { class: "text-sm text-paper-secondary leading-relaxed mb-8 max-w-[280px]",
263 "抱歉,加载页面时出现了一些错误,请稍后再试。"
264 }
265
266 button {
270 r#type: "button",
271 onclick: {
272 let err_ctx = err_ctx.clone();
273 move |_| {
274 err_ctx.clear_errors();
275 let _ = dioxus::router::navigator().push(Route::Home {});
276 }
277 },
278 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]",
279 svg {
280 xmlns: "http://www.w3.org/2000/svg",
281 width: "16",
282 height: "16",
283 view_box: "0 0 24 24",
284 fill: "none",
285 stroke: "currentColor",
286 stroke_width: "2",
287 stroke_linecap: "round",
288 stroke_linejoin: "round",
289 class: "transition-transform group-hover:-translate-x-0.5",
290 path { d: "M19 12H5M12 19l-7-7 7-7" }
291 }
292 "返回首页"
293 }
294 }
295 }
296 }
297 }
298 },
299 Outlet::<Route> {}
300 }
301 }
302}