diff --git a/src/api/auth.rs b/src/api/auth.rs index 81ac3a3..9ea5799 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -19,9 +19,9 @@ use crate::auth::session::get_session_from_ctx; use crate::auth::{password, session}; #[cfg(feature = "server")] use crate::db::pool::get_conn; +use crate::models::user::PublicUser; #[cfg(feature = "server")] use crate::models::user::{SessionUser, UserRole}; -use crate::models::user::PublicUser; #[cfg(feature = "server")] fn validate_username(username: &str) -> Result<(), String> { @@ -119,12 +119,10 @@ pub async fn register( // Argon2 是 memory-hard 计算,必须在 spawn_blocking 中执行,避免阻塞 Tokio worker。 let pw_for_hash = password.clone(); - let password_hash = tokio::task::spawn_blocking(move || { - password::hash_password(&pw_for_hash) - }) - .await - .map_err(|_| AppError::Internal("密码处理任务失败"))? - .map_err(|_| AppError::Internal("密码处理失败"))?; + let password_hash = tokio::task::spawn_blocking(move || password::hash_password(&pw_for_hash)) + .await + .map_err(|_| AppError::Internal("密码处理任务失败"))? + .map_err(|_| AppError::Internal("密码处理失败"))?; // 使用 INSERT ON CONFLICT 原子性地完成“首个用户成为 admin”的竞争。 // 若已有 admin 或用户名/邮箱冲突,RETURNING 将返回空。 @@ -149,7 +147,10 @@ pub async fn register( // 插入失败:区分是已有 admin 还是用户名/邮箱冲突。 let admin_exists: bool = client - .query_one("SELECT EXISTS (SELECT 1 FROM users WHERE role = 'admin')", &[]) + .query_one( + "SELECT EXISTS (SELECT 1 FROM users WHERE role = 'admin')", + &[], + ) .await .map_err(AppError::query)? .get(0); diff --git a/src/api/comments/helpers.rs b/src/api/comments/helpers.rs index d8933bd..601bf3a 100644 --- a/src/api/comments/helpers.rs +++ b/src/api/comments/helpers.rs @@ -131,7 +131,9 @@ pub fn validate_comment_url(url: &str) -> Result<(), String> { if trimmed.len() > 200 { return Err("网址长度不能超过 200 个字符".to_string()); } - if trimmed.chars().any(|c| matches!(c, '<' | '>' | '"' | '\'' | '&' | ' ' | '\t' | '\n' | '\r')) + if trimmed + .chars() + .any(|c| matches!(c, '<' | '>' | '"' | '\'' | '&' | ' ' | '\t' | '\n' | '\r')) { return Err("网址包含非法字符".to_string()); } diff --git a/src/api/comments/read.rs b/src/api/comments/read.rs index 25198bc..7c7c7d5 100644 --- a/src/api/comments/read.rs +++ b/src/api/comments/read.rs @@ -51,4 +51,3 @@ pub async fn get_comments(post_id: i32) -> Result String { @@ -55,13 +55,7 @@ pub static MAX_IMAGE_DIMENSION: LazyLock = LazyLock::new(|| { let (val, clamped) = std::env::var("MAX_IMAGE_DIMENSION") .ok() .and_then(|s| s.parse::().ok()) - .map(|v| { - if v < MIN { - (MIN, true) - } else { - (v, false) - } - }) + .map(|v| if v < MIN { (MIN, true) } else { (v, false) }) .unwrap_or((DEFAULT, false)); if clamped { tracing::warn!( @@ -89,13 +83,7 @@ pub static MAX_IMAGE_PIXELS: LazyLock = LazyLock::new(|| { let (val, clamped) = std::env::var("MAX_IMAGE_PIXELS") .ok() .and_then(|s| s.parse::().ok()) - .map(|v| { - if v < MIN { - (MIN, true) - } else { - (v, false) - } - }) + .map(|v| if v < MIN { (MIN, true) } else { (v, false) }) .unwrap_or((DEFAULT, false)); if clamped { tracing::warn!( @@ -264,7 +252,10 @@ fn image_response( StatusCode::NOT_MODIFIED, [ (header::ETAG, HeaderValue::from_str(&etag).unwrap()), - (header::CACHE_CONTROL, HeaderValue::from_static(cache_control)), + ( + header::CACHE_CONTROL, + HeaderValue::from_static(cache_control), + ), (header::CONTENT_TYPE, content_type), // nosniff 防止浏览器对 content-type 错配的图片字节做 MIME sniff(M2)。 ( @@ -281,7 +272,10 @@ fn image_response( StatusCode::OK, [ (header::CONTENT_TYPE, content_type), - (header::CACHE_CONTROL, HeaderValue::from_static(cache_control)), + ( + header::CACHE_CONTROL, + HeaderValue::from_static(cache_control), + ), (header::ETAG, HeaderValue::from_str(&etag).unwrap()), ( header::X_CONTENT_TYPE_OPTIONS, @@ -347,15 +341,12 @@ pub fn check_upload_dimensions(data: &[u8], mime_type: &str) -> Result<(), &'sta #[cfg(feature = "server")] /// 按 MIME 只读 header 拿 (width, height)。失败返回损坏错误。 -fn read_dimensions_by_mime( - data: &[u8], - mime_type: &str, -) -> Result<(u32, u32), &'static str> { +fn read_dimensions_by_mime(data: &[u8], mime_type: &str) -> Result<(u32, u32), &'static str> { match mime_type { "image/webp" => { // zenwebp 的 WebPDecoder::build 只解析 RIFF header,不解码像素(与 webp::decode 同源)。 - let decoder = zenwebp::WebPDecoder::build(data) - .map_err(|_| "图片文件损坏或格式不正确")?; + let decoder = + zenwebp::WebPDecoder::build(data).map_err(|_| "图片文件损坏或格式不正确")?; let info = decoder.info(); Ok((info.width, info.height)) } @@ -624,7 +615,12 @@ pub async fn serve_image( Ok(_) => match tokio::fs::read(&file_path).await { Ok(data) => { let ct = content_type(detect_format(&path)); - image_response(Bytes::from(data), ct, "public, max-age=31536000, immutable", &headers) + image_response( + Bytes::from(data), + ct, + "public, max-age=31536000, immutable", + &headers, + ) } Err(_) => StatusCode::NOT_FOUND.into_response(), }, @@ -658,19 +654,18 @@ pub async fn serve_image( // runtime stays responsive to other requests. let path_for_blocking = path.clone(); let params_for_blocking = params.clone(); - let (processed, content_type) = - match tokio::task::spawn_blocking(move || { - process_image_blocking(data, params_for_blocking, path_for_blocking) - }) - .await - { - Ok(Ok(r)) => r, - Ok(Err(status)) => return status.into_response(), - Err(_) => { - tracing::error!("Image processing task panicked"); - return StatusCode::INTERNAL_SERVER_ERROR.into_response(); - } - }; + let (processed, content_type) = match tokio::task::spawn_blocking(move || { + process_image_blocking(data, params_for_blocking, path_for_blocking) + }) + .await + { + Ok(Ok(r)) => r, + Ok(Err(status)) => return status.into_response(), + Err(_) => { + tracing::error!("Image processing task panicked"); + return StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + }; let processed = Bytes::from(processed); let cached = CachedImage { @@ -1086,15 +1081,17 @@ mod tests { ); assert_eq!(resp.status(), StatusCode::OK); let headers = resp.headers(); - assert_eq!( - headers.get(header::CONTENT_TYPE).unwrap(), - "image/webp" - ); + assert_eq!(headers.get(header::CONTENT_TYPE).unwrap(), "image/webp"); assert_eq!( headers.get(header::CACHE_CONTROL).unwrap(), "public, max-age=86400" ); - assert!(headers.get(header::ETAG).unwrap().to_str().unwrap().starts_with('"')); + assert!(headers + .get(header::ETAG) + .unwrap() + .to_str() + .unwrap() + .starts_with('"')); } #[test] @@ -1102,10 +1099,7 @@ mod tests { let data = Bytes::from(vec![1, 2, 3]); let etag = etag_for(&data); let mut req_headers = HeaderMap::new(); - req_headers.insert( - header::IF_NONE_MATCH, - HeaderValue::from_str(&etag).unwrap(), - ); + req_headers.insert(header::IF_NONE_MATCH, HeaderValue::from_str(&etag).unwrap()); let resp = image_response( data, HeaderValue::from_static("image/webp"), diff --git a/src/api/markdown.rs b/src/api/markdown.rs index 766d4d7..94fc1fa 100644 --- a/src/api/markdown.rs +++ b/src/api/markdown.rs @@ -349,7 +349,10 @@ mod tests { // 用一个不含 dimensions 的路径验证 --ar 缺省时的结构正确性。 let html = r#"

test

"#; let result = wrap_images_with_blur(html); - assert!(result.contains("blur-img-placeholder"), "should have placeholder"); + assert!( + result.contains("blur-img-placeholder"), + "should have placeholder" + ); assert!(result.contains("blur-img-full"), "should have full layer"); assert!(result.contains("?w=20"), "placeholder should use ?w=20"); assert!(result.contains("?w=800"), "full should use ?w=800"); @@ -361,7 +364,10 @@ mod tests { let html = r#"ext"#; let result = wrap_images_with_blur(html); // 外链图不处理,保持原样 - assert!(!result.contains("blur-img"), "external image should not be wrapped"); + assert!( + !result.contains("blur-img"), + "external image should not be wrapped" + ); } #[test] @@ -372,7 +378,11 @@ mod tests { println!("ACTUAL OUTPUT: {}", result); // aspect-ratio 必须用斜杠分隔,如 "--ar:800 / 600;" assert!(result.contains("--ar:"), "should have --ar"); - assert!(result.contains(" / "), "aspect-ratio must use slash separator, got: {}", result); + assert!( + result.contains(" / "), + "aspect-ratio must use slash separator, got: {}", + result + ); } #[test] @@ -384,7 +394,11 @@ mod tests { let cleaned = clean_html(&wrapped); println!("WRAPPED: {}", wrapped); println!("CLEANED: {}", cleaned); - assert!(cleaned.contains(" / "), "clean_html must preserve slash in --ar, got: {}", cleaned); + assert!( + cleaned.contains(" / "), + "clean_html must preserve slash in --ar, got: {}", + cleaned + ); } #[test] diff --git a/src/api/mod.rs b/src/api/mod.rs index 4cb6e8f..8959294 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -6,10 +6,10 @@ /// 认证相关的 Dioxus server function。 pub mod auth; -/// CSRF 防护中间件。 -pub mod csrf; /// 评论相关接口。 pub mod comments; +/// CSRF 防护中间件。 +pub mod csrf; /// 应用错误类型与转换。 pub mod error; /// 健康检查端点(liveness / readiness)。 diff --git a/src/api/posts/list.rs b/src/api/posts/list.rs index 2ef67db..858fa4b 100644 --- a/src/api/posts/list.rs +++ b/src/api/posts/list.rs @@ -190,7 +190,10 @@ pub async fn list_deleted_posts( let client = get_conn().await.map_err(AppError::db_conn)?; let count_row = client - .query_one("SELECT COUNT(*) FROM posts WHERE deleted_at IS NOT NULL", &[]) + .query_one( + "SELECT COUNT(*) FROM posts WHERE deleted_at IS NOT NULL", + &[], + ) .await .map_err(AppError::query)?; let total: i64 = count_row.get(0); diff --git a/src/api/posts/mod.rs b/src/api/posts/mod.rs index 2d1852d..0ee592b 100644 --- a/src/api/posts/mod.rs +++ b/src/api/posts/mod.rs @@ -14,21 +14,21 @@ mod rebuild; mod search; mod stats; mod tags; +mod trash; mod types; mod update; -mod trash; /// 创建新文章。 #[allow(unused_imports)] pub use create::create_post; /// 删除指定文章。 pub use delete::delete_post; -/// 获取管理员视角的全部文章分页列表。 -#[allow(unused_imports)] -pub use list::list_posts; /// 获取回收站中已软删除的文章列表。 #[allow(unused_imports)] pub use list::list_deleted_posts; +/// 获取管理员视角的全部文章分页列表。 +#[allow(unused_imports)] +pub use list::list_posts; /// 获取已发布文章分页列表。 pub use list::{get_posts_by_tag, list_published_posts}; /// 根据 id 获取文章详情。 @@ -43,14 +43,14 @@ pub use search::search_posts; pub use stats::get_post_stats; /// 获取全部标签及其文章数量。 pub use tags::list_tags; +/// 恢复已删除文章。 +#[allow(unused_imports)] +pub use trash::{batch_purge_posts, batch_restore_posts, empty_trash, purge_post, restore_post}; /// 文章 API 的请求与响应数据结构。 pub use types::*; /// 更新指定文章。 #[allow(unused_imports)] pub use update::update_post; -/// 恢复已删除文章。 -#[allow(unused_imports)] -pub use trash::{batch_purge_posts, batch_restore_posts, empty_trash, purge_post, restore_post}; /// 将 Markdown 渲染为增强 HTML(含目录)。 #[cfg(feature = "server")] diff --git a/src/api/posts/trash.rs b/src/api/posts/trash.rs index 3af8f45..9329975 100644 --- a/src/api/posts/trash.rs +++ b/src/api/posts/trash.rs @@ -232,8 +232,7 @@ pub async fn batch_restore_posts(post_ids: Vec) -> Result = Vec::with_capacity(post_ids.len() * 2); - let mut affected_tags: std::collections::HashSet = - std::collections::HashSet::new(); + let mut affected_tags: std::collections::HashSet = std::collections::HashSet::new(); for id in &post_ids { let row = tx @@ -289,7 +288,8 @@ pub async fn batch_restore_posts(post_ids: Vec) -> Result>()).await; + crate::cache::invalidate_tag_posts_for(&affected_tags.into_iter().collect::>()) + .await; // 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。 crate::ssr_cache::bump_global_generation(); @@ -344,8 +344,7 @@ pub async fn batch_purge_posts(post_ids: Vec) -> Result = - std::collections::HashSet::new(); + let mut tags_set: std::collections::HashSet = std::collections::HashSet::new(); for id in &post_ids { let slug_row = tx @@ -445,8 +444,8 @@ pub async fn empty_trash() -> Result { ) .await .map_err(AppError::query)?; - let use_precise = !deleted_rows.is_empty() - && deleted_rows.len() <= PRECISE_INVALIDATION_LIMIT; + let use_precise = + !deleted_rows.is_empty() && deleted_rows.len() <= PRECISE_INVALIDATION_LIMIT; let (slugs, tags) = if use_precise { let slugs: Vec = deleted_rows.iter().map(|r| r.get("slug")).collect(); diff --git a/src/api/rate_limit.rs b/src/api/rate_limit.rs index 020c57c..b9050f9 100644 --- a/src/api/rate_limit.rs +++ b/src/api/rate_limit.rs @@ -426,7 +426,10 @@ mod tests { break; } } - assert!(blocked, "strict bucket should eventually block real IP burst"); + assert!( + blocked, + "strict bucket should eventually block real IP burst" + ); assert!( allowed <= 6, "strict burst is 5, allowed should be <= 6, got {allowed}" diff --git a/src/api/sanitizer.rs b/src/api/sanitizer.rs index 9a23619..c405ef1 100644 --- a/src/api/sanitizer.rs +++ b/src/api/sanitizer.rs @@ -395,7 +395,10 @@ mod tests { let input = r#"tt"#; let result = clean_html(input); assert!(result.contains("data-src"), "data-src should be allowed"); - assert!(result.contains("blur-img-placeholder"), "class should be allowed"); + assert!( + result.contains("blur-img-placeholder"), + "class should be allowed" + ); assert!(result.contains("--ar"), "style should be allowed"); } @@ -492,12 +495,24 @@ mod tests { let schemes = DEFAULT_ALLOWED_SCHEMES.clone(); // 仅在显式允许且 media type 为图片时通过 assert!(is_safe_url("data:image/png;base64,iVBOR", &schemes, true)); - assert!(is_safe_url("data:image/svg+xml;base64,PHN2Zz4=", &schemes, true)); + assert!(is_safe_url( + "data:image/svg+xml;base64,PHN2Zz4=", + &schemes, + true + )); // 禁用 data URI 时拒绝 assert!(!is_safe_url("data:image/png;base64,iVBOR", &schemes, false)); // 非图片 data URI 拒绝 - assert!(!is_safe_url("data:text/html,", &schemes, true)); - assert!(!is_safe_url("data:application/javascript,alert(1)", &schemes, true)); + assert!(!is_safe_url( + "data:text/html,", + &schemes, + true + )); + assert!(!is_safe_url( + "data:application/javascript,alert(1)", + &schemes, + true + )); } #[test] @@ -538,7 +553,11 @@ mod tests { let schemes = DEFAULT_ALLOWED_SCHEMES.clone(); // 未知 scheme 默认拒绝。 assert!(!is_safe_url("file:///etc/passwd", &schemes, false)); - assert!(!is_safe_url("blob:https://example.com/abc", &schemes, false)); + assert!(!is_safe_url( + "blob:https://example.com/abc", + &schemes, + false + )); assert!(!is_safe_url("about:blank", &schemes, false)); assert!(!is_safe_url("custom-app://open", &schemes, false)); } diff --git a/src/api/settings.rs b/src/api/settings.rs index 631f077..27617cd 100644 --- a/src/api/settings.rs +++ b/src/api/settings.rs @@ -8,13 +8,13 @@ use dioxus::prelude::*; -use crate::models::settings::TrashSettings; #[cfg(feature = "server")] use crate::api::auth::get_current_admin_user; #[cfg(feature = "server")] use crate::api::error::AppError; #[cfg(feature = "server")] use crate::db::pool::get_conn; +use crate::models::settings::TrashSettings; /// 读取回收站配置。 /// diff --git a/src/api/upload.rs b/src/api/upload.rs index 5f93400..9c8ab9e 100644 --- a/src/api/upload.rs +++ b/src/api/upload.rs @@ -12,9 +12,9 @@ use axum::{ response::Json, }; #[cfg(feature = "server")] -use std::net::SocketAddr; -#[cfg(feature = "server")] use serde_json::{json, Value}; +#[cfg(feature = "server")] +use std::net::SocketAddr; #[cfg(feature = "server")] use crate::auth::session::parse_session_token; @@ -47,9 +47,7 @@ fn validate_image_magic_bytes(data: &[u8], mime_type: &str) -> bool { "image/gif" => data.starts_with(b"GIF87a") || data.starts_with(b"GIF89a"), "image/webp" => { // RIFF....WEBP - data.len() >= 12 - && &data[0..4] == b"RIFF" - && &data[8..12] == b"WEBP" + data.len() >= 12 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP" } _ => false, } @@ -448,8 +446,14 @@ mod tests { #[test] fn validate_jpeg_magic_bytes() { - assert!(super::validate_image_magic_bytes(&[0xFF, 0xD8, 0xFF], "image/jpeg")); - assert!(!super::validate_image_magic_bytes(&[0x89, 0x50], "image/jpeg")); + assert!(super::validate_image_magic_bytes( + &[0xFF, 0xD8, 0xFF], + "image/jpeg" + )); + assert!(!super::validate_image_magic_bytes( + &[0x89, 0x50], + "image/jpeg" + )); } #[test] @@ -458,7 +462,10 @@ mod tests { &[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], "image/png" )); - assert!(!super::validate_image_magic_bytes(&[0xFF, 0xD8], "image/png")); + assert!(!super::validate_image_magic_bytes( + &[0xFF, 0xD8], + "image/png" + )); } #[test] @@ -472,6 +479,9 @@ mod tests { fn validate_webp_magic_bytes() { let webp = b"RIFF\x00\x00\x00\x00WEBPVP8 "; assert!(super::validate_image_magic_bytes(&webp[..12], "image/webp")); - assert!(!super::validate_image_magic_bytes(&[0xFF, 0xD8], "image/webp")); + assert!(!super::validate_image_magic_bytes( + &[0xFF, 0xD8], + "image/webp" + )); } } diff --git a/src/bin/generate_highlight_css.rs b/src/bin/generate_highlight_css.rs index f87ac00..496207d 100644 --- a/src/bin/generate_highlight_css.rs +++ b/src/bin/generate_highlight_css.rs @@ -113,10 +113,7 @@ fn strip_background_color(body: &str) -> String { body.split(';') .filter(|decl| { let trimmed = decl.trim(); - !trimmed.is_empty() - && !trimmed - .to_ascii_lowercase() - .starts_with("background-color") + !trimmed.is_empty() && !trimmed.to_ascii_lowercase().starts_with("background-color") }) .collect::>() .join(";") diff --git a/src/cache.rs b/src/cache.rs index fffed37..4debf69 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -77,7 +77,11 @@ pub enum CacheKey { /// 按标签查询的文章列表(不分页,返回全部)。 PostsByTag(String), /// 按标签查询的分页文章列表。 - PostsByTagPage { tag: String, page: i32, per_page: i32 }, + PostsByTagPage { + tag: String, + page: i32, + per_page: i32, + }, /// 文章统计信息。 PostStats, /// 某篇文章下的评论列表。 @@ -760,5 +764,4 @@ mod tests { invalidate_search_results(); assert!(get_search_results("tokio").await.is_none()); } - } diff --git a/src/components/comments/form.rs b/src/components/comments/form.rs index d83f369..47abe51 100644 --- a/src/components/comments/form.rs +++ b/src/components/comments/form.rs @@ -27,11 +27,7 @@ const COMMENT_SUBMIT_CLASS: &str = "py-2.5 px-6 bg-paper-accent text-white font- /// - 提交时校验必填项与蜜罐字段 /// - 提交成功后清空内容、保存作者信息、添加待审核评论并触发列表刷新 #[component] -pub fn CommentForm( - post_id: i32, - parent_id: Option, - parent_indent: Option, -) -> Element { +pub fn CommentForm(post_id: i32, parent_id: Option, parent_indent: Option) -> Element { let ctx: CommentContext = use_context(); let mut active_reply = ctx.active_reply; let mut refresh_trigger = ctx.refresh_trigger; diff --git a/src/components/post/post_content.rs b/src/components/post/post_content.rs index cfc7145..1ae213c 100644 --- a/src/components/post/post_content.rs +++ b/src/components/post/post_content.rs @@ -24,7 +24,7 @@ pub fn PostContent(content_html: String) -> Element { // 否则 lightbox.js 加载完后其 IIFE 尾部读到配置自启动。 let _ = js_sys::eval( "window.__lightboxSelectors = ['.post-content', '.entry-cover']; \ - if (window.__initLightbox) window.__initLightbox(window.__lightboxSelectors);" + if (window.__initLightbox) window.__initLightbox(window.__lightboxSelectors);", ); }); diff --git a/src/components/ui.rs b/src/components/ui.rs index bd0bd1e..9457ddb 100644 --- a/src/components/ui.rs +++ b/src/components/ui.rs @@ -16,8 +16,7 @@ use crate::router::Route; // =========================================================================== /// Admin 卡片容器:白底圆角描边,亮/暗双模式。用于 stat 卡片、面板等。 -pub const ADMIN_CARD_CLASS: &str = - "bg-paper-entry rounded-xl border border-paper-border"; +pub const ADMIN_CARD_CLASS: &str = "bg-paper-entry rounded-xl border border-paper-border"; /// Admin 表格容器:在卡片基础上加 `overflow-hidden`,圆角裁剪表格。 pub const ADMIN_TABLE_CLASS: &str = @@ -56,7 +55,8 @@ pub const BTN_TEXT_AMBER: &str = "text-xs text-amber-600 hover:text-amber-800 da pub const BTN_TEXT_RED: &str = "text-xs text-red-500 hover:text-red-700 dark:hover:text-red-300 transition-colors cursor-pointer"; /// 主题绿(鼠尾草)文字小按钮(行内恢复)。 -pub const BTN_TEXT_ACCENT: &str = "text-xs text-paper-accent hover:text-paper-primary transition-colors cursor-pointer"; +pub const BTN_TEXT_ACCENT: &str = + "text-xs text-paper-accent hover:text-paper-primary transition-colors cursor-pointer"; // --- 次要按钮(冷调玫瑰第二色,ghost 描边风格,从属于主色鼠尾草绿) --- @@ -192,7 +192,7 @@ static TAB_GROUP_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicU /// /// 用于切换不同的视图或筛选条件(例如:全部、待审核、已通过等)。 /// 具备高级的平滑滑动底部指示器动画。 -/// +/// /// Props: /// - `items`:选项卡列表,每一项为 `(value, label)` /// - `active_value`:当前选中的值 @@ -212,15 +212,16 @@ pub fn FilterTabs( #[cfg(target_arch = "wasm32")] { use wasm_bindgen::JsCast; - + // 等待 DOM 节点更新 let promise = js_sys::Promise::new(&mut |resolve, _| { if let Some(window) = web_sys::window() { - let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, 50); + let _ = window + .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, 50); } }); let _ = wasm_bindgen_futures::JsFuture::from(promise).await; - + if let Some(window) = web_sys::window() { if let Some(doc) = window.document() { let element_id = format!("tab-{}-{}", id_prefix, active); @@ -253,10 +254,10 @@ pub fn FilterTabs( button { id: "tab-{id_prefix}-{value}", key: "{value}", - class: if active_value == *value { - "cursor-pointer px-4 py-2 text-sm font-medium text-paper-primary transition-colors" - } else { - "cursor-pointer px-4 py-2 text-sm font-medium text-paper-secondary hover:text-paper-primary transition-colors" + class: if active_value == *value { + "cursor-pointer px-4 py-2 text-sm font-medium text-paper-primary transition-colors" + } else { + "cursor-pointer px-4 py-2 text-sm font-medium text-paper-secondary hover:text-paper-primary transition-colors" }, onclick: { let v = value.to_string(); diff --git a/src/db/migrate.rs b/src/db/migrate.rs index 7747034..48a8e0c 100644 --- a/src/db/migrate.rs +++ b/src/db/migrate.rs @@ -35,7 +35,10 @@ const MIGRATIONS: &[(&str, &str)] = &[ ("005", include_str!("../../migrations/005_comments.sql")), ("006", include_str!("../../migrations/006_add_toc_html.sql")), ("007", include_str!("../../migrations/007_settings.sql")), - ("008", include_str!("../../migrations/008_comments_cascade.sql")), + ( + "008", + include_str!("../../migrations/008_comments_cascade.sql"), + ), ( "009", include_str!("../../migrations/009_cleanup_duplicate_indexes.sql"), @@ -185,7 +188,9 @@ async fn ensure_versions_table(conn: &deadpool_postgres::Object) -> Result<(), M async fn applied_versions( conn: &deadpool_postgres::Object, ) -> Result, MigrateError> { - let rows = conn.query("SELECT version FROM schema_migrations", &[]).await?; + let rows = conn + .query("SELECT version FROM schema_migrations", &[]) + .await?; let mut set = HashSet::with_capacity(rows.len()); for row in rows { set.insert(row.get::<_, String>(0)); @@ -243,7 +248,10 @@ mod tests { let mut sorted = MIGRATIONS.iter().map(|(v, _)| *v).collect::>(); sorted.sort_unstable(); let original: Vec<&str> = MIGRATIONS.iter().map(|(v, _)| *v).collect(); - assert_eq!(original, sorted, "MIGRATIONS must be in ascending version order"); + assert_eq!( + original, sorted, + "MIGRATIONS must be in ascending version order" + ); } #[test] @@ -252,7 +260,11 @@ mod tests { let total = versions.len(); versions.sort_unstable(); versions.dedup(); - assert_eq!(versions.len(), total, "MIGRATIONS has duplicate version strings"); + assert_eq!( + versions.len(), + total, + "MIGRATIONS has duplicate version strings" + ); } #[test] @@ -296,8 +308,7 @@ mod tests { MIGRATIONS.iter().map(|(v, _)| v.to_string()).collect(); // 磁盘上有但数组里没有 → 忘记加行(会静默不执行该迁移)。 - let missing_in_array: Vec<&String> = - files_on_disk.difference(&versions_in_array).collect(); + let missing_in_array: Vec<&String> = files_on_disk.difference(&versions_in_array).collect(); assert!( missing_in_array.is_empty(), "migrations/*.sql files not registered in MIGRATIONS: {:?}. \ @@ -306,8 +317,7 @@ mod tests { ); // 数组里有但磁盘上没有 → include_str! 本就会编译失败,这里只是双保险。 - let missing_on_disk: Vec<&String> = - versions_in_array.difference(&files_on_disk).collect(); + let missing_on_disk: Vec<&String> = versions_in_array.difference(&files_on_disk).collect(); assert!( missing_on_disk.is_empty(), "MIGRATIONS rows without a corresponding .sql file: {:?}", diff --git a/src/db/pool.rs b/src/db/pool.rs index ec7a05f..1a752d7 100644 --- a/src/db/pool.rs +++ b/src/db/pool.rs @@ -22,9 +22,8 @@ use tokio_postgres::{Config, NoTls}; /// /// 返回 `Err(String)` 而非 panic,调用方决定如何向用户报告错误。 fn build_pg_config() -> Result { - let db_url = std::env::var("DATABASE_URL").map_err(|_| { - "DATABASE_URL environment variable not set".to_string() - })?; + let db_url = std::env::var("DATABASE_URL") + .map_err(|_| "DATABASE_URL environment variable not set".to_string())?; let mut pg_cfg = db_url .parse::() .map_err(|e| format!("Invalid DATABASE_URL format: {e}"))?; @@ -36,7 +35,10 @@ fn build_pg_config() -> Result { .and_then(|s| s.parse::().ok()) .unwrap_or(30); // 通过 libpq options 传递 GUC;tokio-postgres 在建连时执行。 - pg_cfg.options(format!("-c statement_timeout={}", statement_timeout_secs * 1000)); + pg_cfg.options(format!( + "-c statement_timeout={}", + statement_timeout_secs * 1000 + )); Ok(pg_cfg) } @@ -166,10 +168,7 @@ pub async fn get_conn_for_startup( match DB_POOL.get().await { Ok(conn) => { if attempt > 1 { - tracing::info!( - "connected to database after {} attempt(s)", - attempt - ); + tracing::info!("connected to database after {} attempt(s)", attempt); } return Ok(conn); } diff --git a/src/highlight.rs b/src/highlight.rs index 03b2887..9aed29a 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -25,9 +25,7 @@ pub mod server { tracing::info!( "SyntaxSet built: {} syntaxes, swift={:?}", built.syntaxes().len(), - built - .find_syntax_by_extension("swift") - .map(|s| &s.name) + built.find_syntax_by_extension("swift").map(|s| &s.name) ); built }); @@ -257,7 +255,8 @@ mod tests { #[test] fn highlight_code_swift_keyword_and_func() { // Swift 关键字 func/import/let 应生成 declaration/keyword span,而不是纯文本。 - let code = "import Foundation\nfunc greet(person: String) -> String {\n return \"Hi\"\n}"; + let code = + "import Foundation\nfunc greet(person: String) -> String {\n return \"Hi\"\n}"; let result = highlight_code(code, Some("swift")); assert!( result.contains("keyword"), @@ -338,11 +337,7 @@ mod tests { // Zig 关键字 const/fn/pub 与内建函数 @import 都应被高亮。 let code = "const std = @import(\"std\");\npub fn main() void {}"; let result = highlight_code(code, Some("zig")); - assert!( - result.contains("keyword"), - "Zig 关键字未被识别: {}", - result - ); + assert!(result.contains("keyword"), "Zig 关键字未被识别: {}", result); assert!( result.contains("name function"), "Zig 函数名未被识别: {}", @@ -365,15 +360,7 @@ mod tests { "Zig u32 类型未被识别: {}", result ); - assert!( - result.contains("string"), - "Zig 字符串未被识别: {}", - result - ); - assert!( - result.contains("numeric"), - "Zig 数字未被识别: {}", - result - ); + assert!(result.contains("string"), "Zig 字符串未被识别: {}", result); + assert!(result.contains("numeric"), "Zig 数字未被识别: {}", result); } } diff --git a/src/hooks/comment_storage.rs b/src/hooks/comment_storage.rs index 226a87c..2b8ed49 100644 --- a/src/hooks/comment_storage.rs +++ b/src/hooks/comment_storage.rs @@ -272,7 +272,11 @@ pub fn relative_label_from_millis(delta_millis: i64, created_iso: &str) -> (Stri .map(|dt| dt.format("%Y-%m-%d").to_string()) .unwrap_or_default(); - let label = if label.is_empty() { absolute.clone() } else { label }; + let label = if label.is_empty() { + absolute.clone() + } else { + label + }; (label, absolute) } diff --git a/src/main.rs b/src/main.rs index fea3db8..3a541ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -165,7 +165,9 @@ fn cache_control_for_path( || path == "/style.css" || path == "/highlight.css" { - return Some(HeaderValue::from_static("public, max-age=31536000, immutable")); + return Some(HeaderValue::from_static( + "public, max-age=31536000, immutable", + )); } // 公开页面:5 分钟新鲜期,过期后 1 小时内可提供过期内容并后台重新验证 @@ -190,7 +192,10 @@ async fn add_cache_control( if let Some(value) = cache_value { // 仅当响应尚未设置 Cache-Control 时才添加,避免覆盖已有策略 - response.headers_mut().entry(header::CACHE_CONTROL).or_insert(value); + response + .headers_mut() + .entry(header::CACHE_CONTROL) + .or_insert(value); } response @@ -215,7 +220,9 @@ fn main() { 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"); - eprintln!("HINT: create a .env file with DATABASE_URL=postgres://user:pass@host:5432/dbname"); + eprintln!( + "HINT: create a .env file with DATABASE_URL=postgres://user:pass@host:5432/dbname" + ); std::process::exit(1); } @@ -290,10 +297,10 @@ fn main() { // 启动 Dioxus 服务端,返回构建好的 Axum Router dioxus::server::serve(|| async move { + use axum::http::StatusCode; use dioxus::server::{axum, DioxusRouterExt, ServeConfig}; use std::time::Duration; use tower_http::timeout::TimeoutLayer; - use axum::http::StatusCode; // 启动后台定时任务:IP 信息清理 tokio::spawn(async { @@ -340,7 +347,9 @@ fn main() { let generation = crate::ssr_cache::current_global_generation(); let is_get = req.method() == axum::http::Method::GET; let (mut parts, body) = req.into_parts(); - parts.extensions.insert(crate::ssr_cache::SsrGeneration(generation)); + parts + .extensions + .insert(crate::ssr_cache::SsrGeneration(generation)); let mut response = next.run(axum::http::Request::from_parts(parts, body)).await; if is_get { response.headers_mut().insert( @@ -364,9 +373,7 @@ fn main() { StatusCode::REQUEST_TIMEOUT, Duration::from_secs(300), )) - .layer(axum::middleware::from_fn( - crate::api::csrf::csrf_middleware, - )); + .layer(axum::middleware::from_fn(crate::api::csrf::csrf_middleware)); // Dioxus 应用路由:自动挂载所有 server function 并渲染前端组件 let dioxus_app = @@ -377,9 +384,7 @@ fn main() { let mut app_routes = dioxus_app .layer(axum::middleware::from_fn(ssr_generation_middleware)) .layer(axum::middleware::from_fn(add_cache_control)) - .layer(axum::middleware::from_fn( - crate::api::csrf::csrf_middleware, - )); + .layer(axum::middleware::from_fn(crate::api::csrf::csrf_middleware)); if let Some(layer) = compression_layer_from_env() { app_routes = app_routes.layer(layer); } @@ -396,14 +401,8 @@ fn main() { // 优雅降级。生产环境应在反向代理后部署并配置 TRUSTED_PROXY_COUNT, // 使限流能拿到真实客户端 IP。 let static_routes = axum::Router::new() - .route( - "/healthz", - axum::routing::get(crate::api::health::healthz), - ) - .route( - "/readyz", - axum::routing::get(crate::api::health::readyz), - ) + .route("/healthz", axum::routing::get(crate::api::health::healthz)) + .route("/readyz", axum::routing::get(crate::api::health::readyz)) .route( "/uploads/{*path}", axum::routing::get(crate::api::image::serve_image), @@ -434,8 +433,7 @@ mod tests { use axum::http::Method; fn cache_value(path: &str, method: Method) -> Option { - cache_control_for_path(path, &method) - .map(|v| v.to_str().unwrap().to_string()) + cache_control_for_path(path, &method).map(|v| v.to_str().unwrap().to_string()) } #[test] diff --git a/src/models/mod.rs b/src/models/mod.rs index fa0c7e8..34751ae 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -7,7 +7,7 @@ pub mod comment; /// 文章模型、文章状态、标签与统计信息。 pub mod post; -/// 用户模型、用户角色与可公开用户信息。 -pub mod user; /// 回收站与站点配置模型。 pub mod settings; +/// 用户模型、用户角色与可公开用户信息。 +pub mod user; diff --git a/src/models/settings.rs b/src/models/settings.rs index cc41ca2..862fcae 100644 --- a/src/models/settings.rs +++ b/src/models/settings.rs @@ -72,7 +72,13 @@ mod tests { #[test] #[cfg(feature = "server")] fn clamp_retention_boundary() { - assert_eq!(TrashSettings::clamp_retention(MIN_RETENTION_DAYS), MIN_RETENTION_DAYS); - assert_eq!(TrashSettings::clamp_retention(MAX_RETENTION_DAYS), MAX_RETENTION_DAYS); + assert_eq!( + TrashSettings::clamp_retention(MIN_RETENTION_DAYS), + MIN_RETENTION_DAYS + ); + assert_eq!( + TrashSettings::clamp_retention(MAX_RETENTION_DAYS), + MAX_RETENTION_DAYS + ); } } diff --git a/src/pages/admin/comments.rs b/src/pages/admin/comments.rs index 97d6b9a..9f23b49 100644 --- a/src/pages/admin/comments.rs +++ b/src/pages/admin/comments.rs @@ -14,12 +14,12 @@ use crate::api::comments::trash_comment; use crate::api::comments::{approve_comment, batch_update_comment_status, spam_comment}; #[cfg(target_arch = "wasm32")] use crate::api::comments::{get_all_comments, AllCommentsResponse}; +use crate::components::empty_state::EmptyState; use crate::components::skeletons::atoms::SkeletonBox; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; -use crate::components::empty_state::EmptyState; use crate::components::ui::{ FilterTabs, Pagination, StatusBadge, ADMIN_CARD_CLASS, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, - BTN_TEXT_AMBER, BTN_TEXT_GREEN, BTN_TEXT_RED, BTN_SOLID_AMBER, BTN_SOLID_GREEN, BTN_SOLID_RED, + BTN_SOLID_AMBER, BTN_SOLID_GREEN, BTN_SOLID_RED, BTN_TEXT_AMBER, BTN_TEXT_GREEN, BTN_TEXT_RED, CHECKBOX_CLASS, }; use crate::models::comment::{AdminComment, CommentStatus}; @@ -470,4 +470,3 @@ fn CommentRow( } } } - diff --git a/src/pages/admin/dashboard.rs b/src/pages/admin/dashboard.rs index 4162a7f..b758b02 100644 --- a/src/pages/admin/dashboard.rs +++ b/src/pages/admin/dashboard.rs @@ -13,7 +13,7 @@ use crate::api::posts::{get_post_stats, list_posts}; #[cfg(target_arch = "wasm32")] use crate::api::posts::{PostListResponse, PostStatsResponse}; use crate::components::skeletons::atoms::SkeletonBox; -use crate::components::ui::{BTN_SECONDARY, ADMIN_CARD_CLASS}; +use crate::components::ui::{ADMIN_CARD_CLASS, BTN_SECONDARY}; use crate::models::post::{PostListItem, PostStats}; use crate::router::Route; diff --git a/src/pages/admin/posts.rs b/src/pages/admin/posts.rs index f5fce0c..dc9a9da 100644 --- a/src/pages/admin/posts.rs +++ b/src/pages/admin/posts.rs @@ -12,10 +12,12 @@ use crate::api::posts::list_posts; #[cfg(target_arch = "wasm32")] use crate::api::posts::PostListResponse; use crate::api::posts::{delete_post, rebuild_content_html, CreatePostResponse, RebuildResult}; +use crate::components::empty_state::{EmptyState, EmptyStateAction}; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; use crate::components::skeletons::posts_skeleton::PostsSkeleton; -use crate::components::empty_state::{EmptyState, EmptyStateAction}; -use crate::components::ui::{Pagination, StatusBadge, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_TEXT_RED}; +use crate::components::ui::{ + Pagination, StatusBadge, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_TEXT_RED, +}; use crate::models::post::PostListItem; use crate::router::Route; @@ -188,7 +190,11 @@ fn RebuildCacheBar() -> Element { rebuild_result.set(None); spawn(async move { match rebuild_content_html(rebuild_all).await { - Ok(RebuildResult { rebuilt, failed, errors }) => { + Ok(RebuildResult { + rebuilt, + failed, + errors, + }) => { if failed > 0 { let mut msg = format!("已重建 {rebuilt} 篇,失败 {failed} 篇"); if let Some(first) = errors.first() { diff --git a/src/pages/home.rs b/src/pages/home.rs index 80c76ef..e44034d 100644 --- a/src/pages/home.rs +++ b/src/pages/home.rs @@ -122,4 +122,3 @@ fn HomeInfo() -> Element { } } } - diff --git a/src/ssr_cache.rs b/src/ssr_cache.rs index e56543c..9e8e49c 100644 --- a/src/ssr_cache.rs +++ b/src/ssr_cache.rs @@ -42,7 +42,9 @@ pub struct SsrGeneration(pub u64); /// 原子递增并返回新的全局世代号。 pub fn bump_global_generation() -> u64 { - GLOBAL_GENERATION.fetch_add(1, Ordering::SeqCst).wrapping_add(1) + GLOBAL_GENERATION + .fetch_add(1, Ordering::SeqCst) + .wrapping_add(1) } /// 返回当前全局世代号。 diff --git a/src/tasks/image_cache_cleanup.rs b/src/tasks/image_cache_cleanup.rs index d6cac0f..58d2ae8 100644 --- a/src/tasks/image_cache_cleanup.rs +++ b/src/tasks/image_cache_cleanup.rs @@ -151,7 +151,11 @@ mod tests { .duration_since(UNIX_EPOCH) .unwrap() .as_nanos(); - std::env::temp_dir().join(format!("yggdrasil_image_cache_test_{}_{}", nanos, std::process::id())) + std::env::temp_dir().join(format!( + "yggdrasil_image_cache_test_{}_{}", + nanos, + std::process::id() + )) } #[tokio::test] diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index 9992bcc..a597cc2 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs @@ -2,15 +2,15 @@ //! //! 所有任务仅在 `server` feature 启用时编译,运行在服务端独立的 tokio 任务中。 -/// 定时清理评论过期的 IP 与用户代理信息,满足隐私保护要求。 -#[cfg(feature = "server")] -pub mod ip_purge; -/// 定时删除已过期会话,避免 `sessions` 表无限增长。 -#[cfg(feature = "server")] -pub mod session_cleanup; -/// 定时清理回收站中超过保留期的已删除文章。 -#[cfg(feature = "server")] -pub mod post_purge; /// 定时清理图片磁盘缓存,避免缓存目录无限增长。 #[cfg(feature = "server")] pub mod image_cache_cleanup; +/// 定时清理评论过期的 IP 与用户代理信息,满足隐私保护要求。 +#[cfg(feature = "server")] +pub mod ip_purge; +/// 定时清理回收站中超过保留期的已删除文章。 +#[cfg(feature = "server")] +pub mod post_purge; +/// 定时删除已过期会话,避免 `sessions` 表无限增长。 +#[cfg(feature = "server")] +pub mod session_cleanup; diff --git a/src/tasks/post_purge.rs b/src/tasks/post_purge.rs index 4cc8ea7..bc558b4 100644 --- a/src/tasks/post_purge.rs +++ b/src/tasks/post_purge.rs @@ -19,16 +19,14 @@ pub async fn run_purge() { let mut ticker = interval(Duration::from_secs(86400)); loop { match get_conn().await { - Ok(client) => { - match purge_expired(&client).await { - Ok(n) => { - if n > 0 { - tracing::info!("Post auto-purge: removed {} expired trashed posts", n); - } + Ok(client) => match purge_expired(&client).await { + Ok(n) => { + if n > 0 { + tracing::info!("Post auto-purge: removed {} expired trashed posts", n); } - Err(e) => tracing::error!("Post auto-purge error: {:?}", e), } - } + Err(e) => tracing::error!("Post auto-purge error: {:?}", e), + }, Err(e) => tracing::error!("Failed to get DB connection for post purge: {:?}", e), } ticker.tick().await; diff --git a/src/tiptap_bridge.rs b/src/tiptap_bridge.rs index 8b132eb..2ec10eb 100644 --- a/src/tiptap_bridge.rs +++ b/src/tiptap_bridge.rs @@ -64,7 +64,8 @@ pub mod wasm { /// unchecked_into 只做编译期类型标注,不做运行时校验(Reflect.get 已保证拿到的是目标对象)。 pub fn get_module() -> TiptapEditorModule { let window = web_sys::window().expect("no window"); - let val = js_sys::Reflect::get(&window, &"TiptapEditor".into()).expect("window.TiptapEditor missing"); + let val = js_sys::Reflect::get(&window, &"TiptapEditor".into()) + .expect("window.TiptapEditor missing"); val.unchecked_into::() } @@ -106,25 +107,25 @@ pub mod wasm { #[wasm_bindgen(method, setter, js_name = placeholder)] pub fn set_placeholder(this: &EditorOptions, v: &str); - /// 文档变更回调(ProseMirror transaction 提交时触发,参数为最新 Markdown)。 - #[wasm_bindgen(method, setter, js_name = onUpdate)] - pub fn set_on_update(this: &EditorOptions, cb: &Closure); + /// 文档变更回调(ProseMirror transaction 提交时触发,参数为最新 Markdown)。 + #[wasm_bindgen(method, setter, js_name = onUpdate)] + pub fn set_on_update(this: &EditorOptions, cb: &Closure); - /// JS 侧 onImageUpload: (file: File) => Promise。 - /// Rust closure 返回 js_sys::Promise,由 future_to_promise 包装。 - #[wasm_bindgen(method, setter, js_name = onImageUpload)] - pub fn set_on_image_upload( - this: &EditorOptions, - cb: &Closure js_sys::Promise>, - ); + /// JS 侧 onImageUpload: (file: File) => Promise。 + /// Rust closure 返回 js_sys::Promise,由 future_to_promise 包装。 + #[wasm_bindgen(method, setter, js_name = onImageUpload)] + pub fn set_on_image_upload( + this: &EditorOptions, + cb: &Closure js_sys::Promise>, + ); - /// 编辑器就绪回调(init 末尾同步触发一次)。 - #[wasm_bindgen(method, setter, js_name = onReady)] - pub fn set_on_ready(this: &EditorOptions, cb: &Closure); + /// 编辑器就绪回调(init 末尾同步触发一次)。 + #[wasm_bindgen(method, setter, js_name = onReady)] + pub fn set_on_ready(this: &EditorOptions, cb: &Closure); - /// 上传事件回调(coordinator.emit 时触发,携带 counts)。 - #[wasm_bindgen(method, setter, js_name = onUploadEvent)] - pub fn set_on_upload_event(this: &EditorOptions, cb: &Closure); + /// 上传事件回调(coordinator.emit 时触发,携带 counts)。 + #[wasm_bindgen(method, setter, js_name = onUploadEvent)] + pub fn set_on_upload_event(this: &EditorOptions, cb: &Closure); } // —— 上传事件(JS UploadEvent 的 Rust 映射)—— @@ -307,9 +308,7 @@ pub mod wasm { .map_err(|_| "上传响应类型异常".to_string())?; // 读响应体文本(无论 2xx 与否,服务端都返回 JSON) - let text_promise = resp - .text() - .map_err(|e| format!("读取响应失败: {:?}", e))?; + let text_promise = resp.text().map_err(|e| format!("读取响应失败: {:?}", e))?; let text_val = wasm_bindgen_futures::JsFuture::from(text_promise) .await .map_err(|e| format!("读取响应失败: {:?}", e))?; @@ -355,6 +354,6 @@ pub mod wasm { /// server 构建剥离该子模块,故此重导出仅对 WASM 前端生效。 #[cfg(target_arch = "wasm32")] pub use wasm::{ - consume_upload_event, make_upload_closure, upload_image_file, EditorHandle, EditorOptions, - UploadEventJs, get_module, + consume_upload_event, get_module, make_upload_closure, upload_image_file, EditorHandle, + EditorOptions, UploadEventJs, };