Skip to main content

yggdrasil/
router.rs

1//! 全站路由配置
2//!
3//! 使用 Dioxus Router 定义前端路由层级,包含前台布局、后台管理布局、
4//! 独立的登录与注册页面。`Route` 枚举上的 `#[route("/path")]` 属性
5//! 既用于生成 URL 匹配规则,也用于组件导航。
6
7use 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, Mcp, Posts, Runner, System,
16    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/// 全站路由枚举,每个变体对应一个页面路径
31#[derive(Clone, Routable, Debug, PartialEq)]
32#[rustfmt::skip]
33pub enum Route {
34    // 前台页面共享布局,最外层嵌套错误边界布局以拦截报错
35    #[layout(ErrorLayout)]
36        #[layout(FrontendLayout)]
37            /// 首页
38            #[route("/")]
39            Home {},
40            /// 首页分页
41            #[route("/page/:page")]
42            HomePage { page: i32 },
43            /// 文章归档页
44            #[route("/archives")]
45            Archives {},
46            /// 标签列表页
47            #[route("/tags")]
48            Tags {},
49            /// 单个标签下的文章列表
50            #[route("/tags/:tag")]
51            TagDetail { tag: String },
52            /// 文章详情页,按 slug 匹配
53            #[route("/post/:slug")]
54            PostDetail { slug: String },
55            /// 搜索页
56            #[route("/search")]
57            Search {},
58            /// 关于页面
59            #[route("/about")]
60            About {},
61            /// 友链页
62            #[route("/friends")]
63            Friends {},
64            /// 更新日志页
65            #[route("/changelog")]
66            Changelog {},
67            /// 404 兜底路由,匹配所有未命中路径
68            #[route("/:..segments")]
69            NotFound { segments: Vec<String> },
70        #[end_layout]
71    #[end_layout]
72
73    // 后台管理路由嵌套在 `/admin` 下
74    #[nest("/admin")]
75    // 后台页面共享管理布局
76    #[layout(AdminLayout)]
77        /// 后台仪表盘
78        #[route("/")]
79        Admin {},
80        /// 写文章页
81        #[route("/write")]
82        Write {},
83        /// 编辑文章页
84        #[route("/write/:id")]
85        WriteEdit { id: i32 },
86        /// 文章管理(列表 + 回收站,客户端 tab 切换,单一路由)
87        #[route("/posts")]
88        Posts {},
89        /// 素材管理(上传图片注册表:浏览/搜索/删除/孤儿清理)
90        #[route("/assets")]
91        Assets {},
92        /// 友链管理
93        #[route("/friends")]
94        FriendsAdmin {},
95        /// 评论管理
96        #[route("/comments")]
97        AdminComments {},
98        /// 评论管理分页
99        #[route("/comments/:page")]
100        AdminCommentsPage { page: i32 },
101        /// 系统管理(数据库 + 服务器状态 + SQL 控制台 + 导出 + 备份)
102        #[route("/system")]
103        System {},
104        /// 代码试运行沙箱(作者预览可运行代码块输出)
105        #[route("/runner")]
106        Runner {},
107        /// MCP 令牌管理 + 客户端配置生成
108        #[route("/mcp")]
109        Mcp {},
110    #[end_layout]
111    #[end_nest]
112
113    /// 登录页面
114    #[route("/login")]
115    Login {},
116    /// 注册页面
117    #[route("/register")]
118    Register {},
119}
120
121/// 应用路由器组件
122///
123/// 初始化主题提供者、全局用户上下文,并挂载样式表与 `Router`。
124#[component]
125pub fn AppRouter() -> Element {
126    let _theme = use_theme_provider();
127
128    // 提供全局用户上下文,供登录状态与路由守卫使用
129    let user = use_signal(|| None::<Arc<crate::models::user::PublicUser>>);
130    let checked = use_signal(|| false);
131    use_context_provider(|| UserContext { user, checked });
132
133    rsx! {
134        document::Stylesheet { href: "/style.css" }
135        document::Stylesheet { href: "/highlight.css" }
136        document::Title { "Yggdrasil Blog" }
137        document::Link { rel: "icon", href: "/favicon.ico" }
138        div {
139            ThemePreload {}
140            Router::<Route> {}
141        }
142    }
143}
144
145#[component]
146fn ErrorLayout() -> Element {
147    rsx! {
148        ErrorBoundary {
149            handle_error: move |err: ErrorContext| {
150                // 克隆一份错误边界句柄,供 fallback 内的「返回首页」按钮清除错误。
151                // ErrorContext 内部是 Rc<RefCell<...>>,clone 廉价。
152                // 不清除就导航会卡死:ErrorBoundary 持有错误时永远渲染 fallback,
153                // 不渲染 children(Outlet),导致 URL 变了但页面不变。
154                let err_ctx = err.clone();
155                // Commit the status code on the server side
156                #[cfg(feature = "server")]
157                {
158                    if let Some(captured_error) = err.error() {
159                        let _ = dioxus::fullstack::FullstackContext::commit_error_status(
160                            captured_error,
161                        );
162                    }
163                }
164                let mut is_404 = false;
165                if let Some(captured_error) = err.error() {
166                    if let Some(ServerFnError::ServerError { code, message, .. }) = captured_error
167                        .downcast_ref::<ServerFnError>()
168
169                    {
170                        if *code == 404 || message == "not_found" {
171                            is_404 = true;
172                        }
173                    } else { // Warning / Alert Icon in a soft red container
174                        let err_str = format!("{:?}", captured_error);
175                        if err_str.contains("NotFound") || err_str.contains("404")
176                            || err_str.contains("not found")
177                        {
178                            is_404 = true;
179                        }
180                    }
181                }
182                if is_404 {
183                    rsx! {
184                        NotFound { segments: vec![] }
185                    }
186                } else {
187                    rsx! {
188                        div { class: "flex flex-col items-center justify-center min-h-[50vh] md:min-h-[55vh] px-6 animate-page-enter",
189                            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",
190                                // Warning / Alert Icon in a soft red container
191                                div { class: "relative mb-6",
192                                    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",
193                                        svg {
194                                            xmlns: "http://www.w3.org/2000/svg",
195                                            width: "28",
196                                            height: "28",
197                                            view_box: "0 0 24 24",
198                                            fill: "none",
199                                            stroke: "currentColor",
200                                            stroke_width: "2",
201                                            stroke_linecap: "round",
202                                            stroke_linejoin: "round",
203                                            circle { cx: "12", cy: "12", r: "10" }
204                                            line {
205                                                x1: "12",
206                                                y1: "8",
207                                                x2: "12",
208                                                y2: "12",
209                                            }
210                                            line {
211                                                x1: "12",
212                                                y1: "16",
213                                                x2: "12.01",
214                                                y2: "16",
215                                            }
216                                        }
217                                    }
218                                }
219
220                                // Premium Typography
221                                h1 { class: "text-lg md:text-xl font-bold tracking-tight text-paper-primary mb-3",
222                                    "加载失败"
223                                }
224                                p { class: "text-sm text-paper-secondary leading-relaxed mb-8 max-w-[280px]",
225                                    "抱歉,加载页面时出现了一些错误,请稍后再试。"
226                                }
227
228                                // CTA: 清除错误边界后命令式导航回首页。
229                                // 必须用按钮而非 Link —— Link 无法在导航前清除错误,
230                                // 会导致 ErrorBoundary 卡在 fallback,页面不随路由切换更新。
231                                button {
232                                    r#type: "button",
233                                    onclick: {
234                                        let err_ctx = err_ctx.clone();
235                                        move |_| {
236                                            err_ctx.clear_errors();
237                                            let _ = dioxus::router::navigator().push(Route::Home {});
238                                        }
239                                    },
240                                    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]",
241                                    svg {
242                                        xmlns: "http://www.w3.org/2000/svg",
243                                        width: "16",
244                                        height: "16",
245                                        view_box: "0 0 24 24",
246                                        fill: "none",
247                                        stroke: "currentColor",
248                                        stroke_width: "2",
249                                        stroke_linecap: "round",
250                                        stroke_linejoin: "round",
251                                        class: "transition-transform group-hover:-translate-x-0.5",
252                                        path { d: "M19 12H5M12 19l-7-7 7-7" }
253                                    }
254                                    "返回首页"
255                                }
256                            }
257                        }
258                    }
259                }
260            },
261            Outlet::<Route> {}
262        }
263    }
264}