docs(auth-pages, router, main): 补充中文注释
This commit is contained in:
parent
18500c9496
commit
c5d1eb117c
28
src/main.rs
28
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 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;
|
||||||
|
|||||||
@ -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(()) })),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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, .. }) => {
|
||||||
|
|||||||
@ -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 });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user