docs(auth-pages, router, main): 补充中文注释

This commit is contained in:
xfy 2026-06-12 19:28:08 +08:00
parent 18500c9496
commit c5d1eb117c
4 changed files with 85 additions and 0 deletions

View File

@ -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 api;
mod auth; mod auth;
mod cache; mod cache;
mod components; mod components;
mod context; mod context;
mod db; mod db;
// highlight 模块仅在服务端构建时编译
#[cfg(feature = "server")] #[cfg(feature = "server")]
mod highlight; mod highlight;
mod hooks; mod hooks;
@ -15,10 +28,14 @@ mod theme;
mod utils; mod utils;
mod webp; mod webp;
/// 程序入口
fn main() { fn main() {
// server feature启动服务端
#[cfg(feature = "server")] #[cfg(feature = "server")]
{ {
// 加载 .env 环境变量
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
// 初始化 tracing 日志,默认级别为 info
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter( .with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env() tracing_subscriber::EnvFilter::try_from_default_env()
@ -26,23 +43,28 @@ fn main() {
) )
.init(); .init();
// 校验数据库连接串,未设置则直接退出
if std::env::var("DATABASE_URL").is_err() { 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."); 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"); eprintln!("ERROR: DATABASE_URL environment variable not set");
std::process::exit(1); std::process::exit(1);
} }
// 启动 Dioxus 服务端,返回构建好的 Axum Router
dioxus::server::serve(|| async move { dioxus::server::serve(|| async move {
use dioxus::server::{axum, DioxusRouterExt, ServeConfig}; use dioxus::server::{axum, DioxusRouterExt, ServeConfig};
// 启动后台定时任务IP 信息清理
tokio::spawn(async { tokio::spawn(async {
tasks::ip_purge::run_purge().await; tasks::ip_purge::run_purge().await;
}); });
// 启动后台定时任务:过期 session 清理
tokio::spawn(async { tokio::spawn(async {
tasks::session_cleanup::run_cleanup().await; tasks::session_cleanup::run_cleanup().await;
}); });
// 配置增量渲染缓存,默认缓存 3600 秒,可通过 SSR_CACHE_SECS 覆盖
let config = ServeConfig::builder().incremental( let config = ServeConfig::builder().incremental(
dioxus::server::IncrementalRendererConfig::default().invalidate_after( dioxus::server::IncrementalRendererConfig::default().invalidate_after(
std::time::Duration::from_secs( std::time::Duration::from_secs(
@ -53,26 +75,32 @@ fn main() {
), ),
), ),
); );
// 自定义 API 路由:图片上传,禁用默认请求体大小限制以支持大文件
let api_routes = axum::Router::new().route( let api_routes = axum::Router::new().route(
"/api/upload", "/api/upload",
axum::routing::post(crate::api::upload::upload_image) axum::routing::post(crate::api::upload::upload_image)
.layer(axum::extract::DefaultBodyLimit::disable()), .layer(axum::extract::DefaultBodyLimit::disable()),
); );
// 静态资源路由:图片文件服务,支持动态裁剪/旋转/格式转换
let static_routes = axum::Router::new().route( let static_routes = axum::Router::new().route(
"/uploads/{*path}", "/uploads/{*path}",
axum::routing::get(crate::api::image::serve_image), axum::routing::get(crate::api::image::serve_image),
); );
// Dioxus 应用路由:自动挂载所有 server function 并渲染前端组件
let dioxus_app = let dioxus_app =
axum::Router::new().serve_dioxus_application(config, router::AppRouter); axum::Router::new().serve_dioxus_application(config, router::AppRouter);
// 合并三条路由:自定义 API、静态资源、Dioxus 主应用
let router = api_routes.merge(static_routes).merge(dioxus_app); let router = api_routes.merge(static_routes).merge(dioxus_app);
Ok(router) Ok(router)
}); });
} }
// 非 server feature通常为 WASM 前端):启动客户端应用
#[cfg(not(feature = "server"))] #[cfg(not(feature = "server"))]
{ {
use router::AppRouter; use router::AppRouter;

View File

@ -1,3 +1,8 @@
//! 登录页面
//!
//! 提供用户名/密码表单,前端校验通过后调用 `login` server function
//! 登录成功时跳转到管理后台首页。
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::router::components::Link; 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::context::UserContext;
use crate::router::Route; use crate::router::Route;
/// 登录页面组件
#[component] #[component]
pub fn Login() -> Element { pub fn Login() -> Element {
// 表单输入状态
let mut username = use_signal(|| "".to_string()); let mut username = use_signal(|| "".to_string());
let mut password = use_signal(|| "".to_string()); let mut password = use_signal(|| "".to_string());
// 错误提示与加载状态
let mut error = use_signal(|| None::<String>); let mut error = use_signal(|| None::<String>);
let mut loading = use_signal(|| false); let mut loading = use_signal(|| false);
// 全局用户上下文,用于触发登录后的状态刷新
let mut ctx: UserContext = use_context(); let mut ctx: UserContext = use_context();
// 提交登录表单
let on_submit = Callback::new(move |_| { let on_submit = Callback::new(move |_| {
if loading() { if loading() {
return; return;
@ -24,6 +34,7 @@ pub fn Login() -> Element {
let username_val = username(); let username_val = username();
let password_val = password(); let password_val = password();
// 在异步任务中调用 server function 登录
spawn(async move { spawn(async move {
match login(username_val, password_val).await { match login(username_val, password_val).await {
Ok(AuthResponse { Ok(AuthResponse {
@ -31,6 +42,7 @@ pub fn Login() -> Element {
token: Some(_token), token: Some(_token),
.. ..
}) => { }) => {
// 登录成功:重置上下文检查标记并跳转到后台
ctx.checked.set(false); ctx.checked.set(false);
let _ = dioxus::router::navigator().push(Route::Admin {}); let _ = dioxus::router::navigator().push(Route::Admin {});
} }
@ -78,6 +90,7 @@ pub fn Login() -> Element {
value: username(), value: username(),
disabled: is_loading, disabled: is_loading,
oninput: move |v: String| username.set(v), oninput: move |v: String| username.set(v),
// 回车键触发提交
onkeydown: Some(EventHandler::new(move |e: KeyboardEvent| if e.key() == Key::Enter { on_submit(()) })), onkeydown: Some(EventHandler::new(move |e: KeyboardEvent| if e.key() == Key::Enter { on_submit(()) })),
} }
} }

View File

@ -1,3 +1,8 @@
//! 注册页面
//!
//! 提供新用户注册表单。首个注册成功的用户将自动成为管理员,
//! 后续注册请求会被服务端拒绝。
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::router::components::Link; 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::components::forms::{AlertBox, FormInput, FormLabel, BUTTON_PRIMARY_CLASS};
use crate::router::Route; use crate::router::Route;
/// 注册页面组件
#[component] #[component]
pub fn Register() -> Element { pub fn Register() -> Element {
// 表单输入状态
let mut username = use_signal(|| "".to_string()); let mut username = use_signal(|| "".to_string());
let mut email = use_signal(|| "".to_string()); let mut email = use_signal(|| "".to_string());
let mut password = use_signal(|| "".to_string()); let mut password = use_signal(|| "".to_string());
let mut confirm_password = use_signal(|| "".to_string()); let mut confirm_password = use_signal(|| "".to_string());
// 错误提示、成功提示与加载状态
let mut error = use_signal(|| None::<String>); let mut error = use_signal(|| None::<String>);
let mut success = use_signal(|| false); let mut success = use_signal(|| false);
let mut loading = use_signal(|| false); let mut loading = use_signal(|| false);
// 提交注册表单
let on_submit = Callback::new(move |_| { let on_submit = Callback::new(move |_| {
if loading() { if loading() {
return; return;
@ -22,6 +31,7 @@ pub fn Register() -> Element {
error.set(None); error.set(None);
success.set(false); success.set(false);
// 前端基础校验:密码长度与一致性
if password().len() < 8 { if password().len() < 8 {
error.set(Some("密码长度至少 8 位".to_string())); error.set(Some("密码长度至少 8 位".to_string()));
return; return;
@ -37,6 +47,7 @@ pub fn Register() -> Element {
loading.set(true); loading.set(true);
// 在异步任务中调用 server function 注册
spawn(async move { spawn(async move {
match register(username_val, email_val, password_val).await { match register(username_val, email_val, password_val).await {
Ok(AuthResponse { success: true, .. }) => { Ok(AuthResponse { success: true, .. }) => {

View File

@ -1,3 +1,9 @@
//! 全站路由配置
//!
//! 使用 Dioxus Router 定义前端路由层级,包含前台布局、后台管理布局、
//! 独立的登录与注册页面。`Route` 枚举上的 `#[route("/path")]` 属性
//! 既用于生成 URL 匹配规则,也用于组件导航。
use dioxus::prelude::*; use dioxus::prelude::*;
use std::sync::Arc; use std::sync::Arc;
@ -18,63 +24,90 @@ use crate::pages::search::Search;
use crate::pages::tags::{TagDetail, Tags}; use crate::pages::tags::{TagDetail, Tags};
use crate::theme::{use_theme_provider, Theme, ThemePreload}; use crate::theme::{use_theme_provider, Theme, ThemePreload};
/// 全站路由枚举,每个变体对应一个页面路径
#[derive(Clone, Routable, Debug, PartialEq)] #[derive(Clone, Routable, Debug, PartialEq)]
#[rustfmt::skip] #[rustfmt::skip]
pub enum Route { pub enum Route {
// 前台页面共享布局
#[layout(FrontendLayout)] #[layout(FrontendLayout)]
/// 首页
#[route("/")] #[route("/")]
Home {}, Home {},
/// 首页分页
#[route("/page/:page")] #[route("/page/:page")]
HomePage { page: i32 }, HomePage { page: i32 },
/// 文章归档页
#[route("/archives")] #[route("/archives")]
Archives {}, Archives {},
/// 标签列表页
#[route("/tags")] #[route("/tags")]
Tags {}, Tags {},
/// 单个标签下的文章列表
#[route("/tags/:tag")] #[route("/tags/:tag")]
TagDetail { tag: String }, TagDetail { tag: String },
/// 文章详情页,按 slug 匹配
#[route("/post/:slug")] #[route("/post/:slug")]
PostDetail { slug: String }, PostDetail { slug: String },
/// 搜索页
#[route("/search")] #[route("/search")]
Search {}, Search {},
/// 关于页面
#[route("/about")] #[route("/about")]
About {}, About {},
/// 404 兜底路由,匹配所有未命中路径
#[route("/:..segments")] #[route("/:..segments")]
NotFound { segments: Vec<String> }, NotFound { segments: Vec<String> },
#[end_layout] #[end_layout]
// 后台管理路由嵌套在 `/admin` 下
#[nest("/admin")] #[nest("/admin")]
// 后台页面共享管理布局
#[layout(AdminLayout)] #[layout(AdminLayout)]
/// 后台仪表盘
#[route("/")] #[route("/")]
Admin {}, Admin {},
/// 写文章页
#[route("/write")] #[route("/write")]
Write {}, Write {},
/// 编辑文章页
#[route("/write/:id")] #[route("/write/:id")]
WriteEdit { id: i32 }, WriteEdit { id: i32 },
/// 文章管理列表
#[route("/posts")] #[route("/posts")]
Posts {}, Posts {},
/// 文章管理列表分页
#[route("/posts/:page")] #[route("/posts/:page")]
PostsPage { page: i32 }, PostsPage { page: i32 },
/// 评论管理
#[route("/comments")] #[route("/comments")]
AdminComments {}, AdminComments {},
/// 评论管理分页
#[route("/comments/:page")] #[route("/comments/:page")]
AdminCommentsPage { page: i32 }, AdminCommentsPage { page: i32 },
#[end_layout] #[end_layout]
#[end_nest] #[end_nest]
/// 登录页面
#[route("/login")] #[route("/login")]
Login {}, Login {},
/// 注册页面
#[route("/register")] #[route("/register")]
Register {}, Register {},
} }
/// 应用路由器组件
///
/// 初始化主题提供者、全局用户上下文,并挂载样式表与 `Router`。
#[component] #[component]
pub fn AppRouter() -> Element { pub fn AppRouter() -> Element {
// 获取当前主题以设置顶层 dark 类
let theme = use_theme_provider(); let theme = use_theme_provider();
let theme_class = match theme() { let theme_class = match theme() {
Theme::Dark => "dark", Theme::Dark => "dark",
Theme::Light => "", Theme::Light => "",
}; };
// 提供全局用户上下文,供登录状态与路由守卫使用
let user = use_signal(|| None::<Arc<crate::models::user::PublicUser>>); let user = use_signal(|| None::<Arc<crate::models::user::PublicUser>>);
let checked = use_signal(|| false); let checked = use_signal(|| false);
use_context_provider(|| UserContext { user, checked }); use_context_provider(|| UserContext { user, checked });