From c5d1eb117ca54ad68d5b4306275329cc8b39600a Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 12 Jun 2026 19:28:08 +0800 Subject: [PATCH] =?UTF-8?q?docs(auth-pages,=20router,=20main):=20=E8=A1=A5?= =?UTF-8?q?=E5=85=85=E4=B8=AD=E6=96=87=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.rs | 28 ++++++++++++++++++++++++++++ src/pages/login.rs | 13 +++++++++++++ src/pages/register.rs | 11 +++++++++++ src/router.rs | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+) diff --git a/src/main.rs b/src/main.rs index 08a61f7..56e75b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,22 @@ +//! 服务端入口与启动配置 +//! +//! 本文件是 Dioxus fullstack 应用的启动入口。 +//! 当启用 `server` feature 时,启动 Axum 服务器并挂载: +//! - Dioxus server function(由 `serve_dioxus_application` 自动注册); +//! - 自定义 Axum 路由:图片上传 `/api/upload`、图片服务 `/uploads/{*path}`; +//! - 增量渲染(Incremental Rendering)缓存配置。 +//! +//! 当未启用 `server` feature(例如编译为 WASM 前端)时, +//! 仅调用 `dioxus::launch` 启动客户端应用。 + +// 业务模块 mod api; mod auth; mod cache; mod components; mod context; mod db; +// highlight 模块仅在服务端构建时编译 #[cfg(feature = "server")] mod highlight; mod hooks; @@ -15,10 +28,14 @@ mod theme; mod utils; mod webp; +/// 程序入口 fn main() { + // server feature:启动服务端 #[cfg(feature = "server")] { + // 加载 .env 环境变量 dotenvy::dotenv().ok(); + // 初始化 tracing 日志,默认级别为 info tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() @@ -26,23 +43,28 @@ fn main() { ) .init(); + // 校验数据库连接串,未设置则直接退出 if std::env::var("DATABASE_URL").is_err() { tracing::error!("DATABASE_URL environment variable not set. Make sure .env exists or the variable is exported."); eprintln!("ERROR: DATABASE_URL environment variable not set"); std::process::exit(1); } + // 启动 Dioxus 服务端,返回构建好的 Axum Router dioxus::server::serve(|| async move { use dioxus::server::{axum, DioxusRouterExt, ServeConfig}; + // 启动后台定时任务:IP 信息清理 tokio::spawn(async { tasks::ip_purge::run_purge().await; }); + // 启动后台定时任务:过期 session 清理 tokio::spawn(async { tasks::session_cleanup::run_cleanup().await; }); + // 配置增量渲染缓存,默认缓存 3600 秒,可通过 SSR_CACHE_SECS 覆盖 let config = ServeConfig::builder().incremental( dioxus::server::IncrementalRendererConfig::default().invalidate_after( std::time::Duration::from_secs( @@ -53,26 +75,32 @@ fn main() { ), ), ); + + // 自定义 API 路由:图片上传,禁用默认请求体大小限制以支持大文件 let api_routes = axum::Router::new().route( "/api/upload", axum::routing::post(crate::api::upload::upload_image) .layer(axum::extract::DefaultBodyLimit::disable()), ); + // 静态资源路由:图片文件服务,支持动态裁剪/旋转/格式转换 let static_routes = axum::Router::new().route( "/uploads/{*path}", axum::routing::get(crate::api::image::serve_image), ); + // Dioxus 应用路由:自动挂载所有 server function 并渲染前端组件 let dioxus_app = axum::Router::new().serve_dioxus_application(config, router::AppRouter); + // 合并三条路由:自定义 API、静态资源、Dioxus 主应用 let router = api_routes.merge(static_routes).merge(dioxus_app); Ok(router) }); } + // 非 server feature(通常为 WASM 前端):启动客户端应用 #[cfg(not(feature = "server"))] { use router::AppRouter; diff --git a/src/pages/login.rs b/src/pages/login.rs index 8ea8b9d..3370210 100644 --- a/src/pages/login.rs +++ b/src/pages/login.rs @@ -1,3 +1,8 @@ +//! 登录页面 +//! +//! 提供用户名/密码表单,前端校验通过后调用 `login` server function, +//! 登录成功时跳转到管理后台首页。 + use dioxus::prelude::*; use dioxus::router::components::Link; @@ -6,14 +11,19 @@ use crate::components::forms::{AlertBox, FormInput, FormLabel, BUTTON_PRIMARY_CL use crate::context::UserContext; use crate::router::Route; +/// 登录页面组件 #[component] pub fn Login() -> Element { + // 表单输入状态 let mut username = use_signal(|| "".to_string()); let mut password = use_signal(|| "".to_string()); + // 错误提示与加载状态 let mut error = use_signal(|| None::); let mut loading = use_signal(|| false); + // 全局用户上下文,用于触发登录后的状态刷新 let mut ctx: UserContext = use_context(); + // 提交登录表单 let on_submit = Callback::new(move |_| { if loading() { return; @@ -24,6 +34,7 @@ pub fn Login() -> Element { let username_val = username(); let password_val = password(); + // 在异步任务中调用 server function 登录 spawn(async move { match login(username_val, password_val).await { Ok(AuthResponse { @@ -31,6 +42,7 @@ pub fn Login() -> Element { token: Some(_token), .. }) => { + // 登录成功:重置上下文检查标记并跳转到后台 ctx.checked.set(false); let _ = dioxus::router::navigator().push(Route::Admin {}); } @@ -78,6 +90,7 @@ pub fn Login() -> Element { value: username(), disabled: is_loading, oninput: move |v: String| username.set(v), + // 回车键触发提交 onkeydown: Some(EventHandler::new(move |e: KeyboardEvent| if e.key() == Key::Enter { on_submit(()) })), } } diff --git a/src/pages/register.rs b/src/pages/register.rs index 3780485..b720e63 100644 --- a/src/pages/register.rs +++ b/src/pages/register.rs @@ -1,3 +1,8 @@ +//! 注册页面 +//! +//! 提供新用户注册表单。首个注册成功的用户将自动成为管理员, +//! 后续注册请求会被服务端拒绝。 + use dioxus::prelude::*; use dioxus::router::components::Link; @@ -5,16 +10,20 @@ use crate::api::auth::{register, AuthResponse}; use crate::components::forms::{AlertBox, FormInput, FormLabel, BUTTON_PRIMARY_CLASS}; use crate::router::Route; +/// 注册页面组件 #[component] pub fn Register() -> Element { + // 表单输入状态 let mut username = use_signal(|| "".to_string()); let mut email = use_signal(|| "".to_string()); let mut password = use_signal(|| "".to_string()); let mut confirm_password = use_signal(|| "".to_string()); + // 错误提示、成功提示与加载状态 let mut error = use_signal(|| None::); let mut success = use_signal(|| false); let mut loading = use_signal(|| false); + // 提交注册表单 let on_submit = Callback::new(move |_| { if loading() { return; @@ -22,6 +31,7 @@ pub fn Register() -> Element { error.set(None); success.set(false); + // 前端基础校验:密码长度与一致性 if password().len() < 8 { error.set(Some("密码长度至少 8 位".to_string())); return; @@ -37,6 +47,7 @@ pub fn Register() -> Element { loading.set(true); + // 在异步任务中调用 server function 注册 spawn(async move { match register(username_val, email_val, password_val).await { Ok(AuthResponse { success: true, .. }) => { diff --git a/src/router.rs b/src/router.rs index 2ff8fe6..642a239 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,3 +1,9 @@ +//! 全站路由配置 +//! +//! 使用 Dioxus Router 定义前端路由层级,包含前台布局、后台管理布局、 +//! 独立的登录与注册页面。`Route` 枚举上的 `#[route("/path")]` 属性 +//! 既用于生成 URL 匹配规则,也用于组件导航。 + use dioxus::prelude::*; use std::sync::Arc; @@ -18,63 +24,90 @@ use crate::pages::search::Search; use crate::pages::tags::{TagDetail, Tags}; use crate::theme::{use_theme_provider, Theme, ThemePreload}; +/// 全站路由枚举,每个变体对应一个页面路径 #[derive(Clone, Routable, Debug, PartialEq)] #[rustfmt::skip] pub enum Route { + // 前台页面共享布局 #[layout(FrontendLayout)] + /// 首页 #[route("/")] Home {}, + /// 首页分页 #[route("/page/:page")] HomePage { page: i32 }, + /// 文章归档页 #[route("/archives")] Archives {}, + /// 标签列表页 #[route("/tags")] Tags {}, + /// 单个标签下的文章列表 #[route("/tags/:tag")] TagDetail { tag: String }, + /// 文章详情页,按 slug 匹配 #[route("/post/:slug")] PostDetail { slug: String }, + /// 搜索页 #[route("/search")] Search {}, + /// 关于页面 #[route("/about")] About {}, + /// 404 兜底路由,匹配所有未命中路径 #[route("/:..segments")] NotFound { segments: Vec }, #[end_layout] + // 后台管理路由嵌套在 `/admin` 下 #[nest("/admin")] + // 后台页面共享管理布局 #[layout(AdminLayout)] + /// 后台仪表盘 #[route("/")] Admin {}, + /// 写文章页 #[route("/write")] Write {}, + /// 编辑文章页 #[route("/write/:id")] WriteEdit { id: i32 }, + /// 文章管理列表 #[route("/posts")] Posts {}, + /// 文章管理列表分页 #[route("/posts/:page")] PostsPage { page: i32 }, + /// 评论管理 #[route("/comments")] AdminComments {}, + /// 评论管理分页 #[route("/comments/:page")] AdminCommentsPage { page: i32 }, #[end_layout] #[end_nest] + /// 登录页面 #[route("/login")] Login {}, + /// 注册页面 #[route("/register")] Register {}, } +/// 应用路由器组件 +/// +/// 初始化主题提供者、全局用户上下文,并挂载样式表与 `Router`。 #[component] pub fn AppRouter() -> Element { + // 获取当前主题以设置顶层 dark 类 let theme = use_theme_provider(); let theme_class = match theme() { Theme::Dark => "dark", Theme::Light => "", }; + // 提供全局用户上下文,供登录状态与路由守卫使用 let user = use_signal(|| None::>); let checked = use_signal(|| false); use_context_provider(|| UserContext { user, checked });