diff --git a/src/api/assets/mod.rs b/src/api/assets/mod.rs index 311d8b9..da7e45a 100644 --- a/src/api/assets/mod.rs +++ b/src/api/assets/mod.rs @@ -3,6 +3,8 @@ //! 管理 `uploads/` 图片的注册表(assets 表)与引用关系(asset_refs 表): //! 分页列表、删除保护、孤儿清理、全量重建索引。 //! 全部为 Dioxus server function,仅 admin 可用。 +// 与 posts / comments 模块一致:Dioxus `#[server]` 宏触发 deprecated/unit 提示,按项目惯例放行。 +#![allow(clippy::unused_unit, deprecated, unused_imports)] /// 素材删除与孤儿清理。 pub mod delete; diff --git a/src/api/image.rs b/src/api/image.rs index b4ef67c..8312906 100644 --- a/src/api/image.rs +++ b/src/api/image.rs @@ -321,22 +321,14 @@ fn check_image_dimensions(width: u32, height: u32) -> Result<(), StatusCode> { } #[cfg(feature = "server")] -/// 仅读取 header 校验上传图片的尺寸/像素是否超限。 +/// 仅读取 header 校验上传图片的尺寸/像素是否超限,并返回 (width, height)。 /// -/// 与 `check_image_dimensions` 的区别: /// - 输入是原始字节 + MIME,内部按 MIME 分发只解析 header 拿尺寸(不解码像素) /// - 返回带友好中文提示的 `&'static str`(供上传接口直接回给用户) /// /// 上传入口三种格式在此统一拦截至尺寸上限;WebP 走 `zenwebp` header, /// JPEG/PNG/GIF 走 `image` crate 的 `into_dimensions`(均只读 header)。 -pub fn check_upload_dimensions(data: &[u8], mime_type: &str) -> Result<(), &'static str> { - upload_dimensions(data, mime_type).map(|_| ()) -} - -#[cfg(feature = "server")] -/// 与 [`check_upload_dimensions`] 相同的校验,但返回 (width, height)。 -/// -/// 供 `upload_image` 在校验通过后直接把尺寸写入 assets 表,避免二次解析 header。 +/// 尺寸随结果返回,供 `upload_image` 校验通过后直接写入 assets 表,避免二次解析。 pub(crate) fn upload_dimensions(data: &[u8], mime_type: &str) -> Result<(u32, u32), &'static str> { let dims = read_dimensions_by_mime(data, mime_type)?; let (width, height) = dims; @@ -796,7 +788,7 @@ mod tests { assert_eq!(dims, None); } - // —— check_upload_dimensions:统一上传尺寸/像素上限校验 —— + // —— upload_dimensions:统一上传尺寸/像素上限校验 —— /// 构造指定尺寸的 PNG 字节(内存占用 = 尺寸,仅用于 header 校验测试)。 fn make_png_bytes(w: u32, h: u32) -> Vec { @@ -807,54 +799,54 @@ mod tests { } #[test] - fn check_upload_dimensions_accepts_small_png() { + fn upload_dimensions_accepts_small_png() { let data = make_png_bytes(100, 100); - assert!(check_upload_dimensions(&data, "image/png").is_ok()); + assert!(upload_dimensions(&data, "image/png").is_ok()); } #[test] - fn check_upload_dimensions_accepts_boundary_png() { + fn upload_dimensions_accepts_boundary_png() { // 用 7000×7000(≈49M 像素):单边 7000 < 默认 8192 上限,且总像素 < 默认 50M 上限,应放行。 // 注意不能直接用 *MAX_IMAGE_DIMENSION 做正方形边——8192²≈67M 会触发像素上限拒绝。 let data = make_png_bytes(7000, 7000); - assert!(check_upload_dimensions(&data, "image/png").is_ok()); + assert!(upload_dimensions(&data, "image/png").is_ok()); } #[test] - fn check_upload_dimensions_rejects_oversized_width() { + fn upload_dimensions_rejects_oversized_width() { // 单边超限:(上限+1)×1,像素远低于上限,但单边越界 let data = make_png_bytes(*MAX_IMAGE_DIMENSION + 1, 1); - let err = check_upload_dimensions(&data, "image/png").unwrap_err(); + let err = upload_dimensions(&data, "image/png").unwrap_err(); assert!(err.contains("尺寸过大")); } #[test] - fn check_upload_dimensions_rejects_oversized_height() { + fn upload_dimensions_rejects_oversized_height() { let data = make_png_bytes(1, *MAX_IMAGE_DIMENSION + 1); - let err = check_upload_dimensions(&data, "image/png").unwrap_err(); + let err = upload_dimensions(&data, "image/png").unwrap_err(); assert!(err.contains("尺寸过大")); } #[test] - fn check_upload_dimensions_accepts_small_webp() { + fn upload_dimensions_accepts_small_webp() { let img = image::DynamicImage::new_rgb8(64, 48); let webp_bytes = crate::webp::encode(&img, 85.0, 2).unwrap(); - assert!(check_upload_dimensions(&webp_bytes, "image/webp").is_ok()); + assert!(upload_dimensions(&webp_bytes, "image/webp").is_ok()); } #[test] - fn check_upload_dimensions_accepts_gif() { + fn upload_dimensions_accepts_gif() { // image crate 默认启用 gif feature,into_dimensions 可读 GIF header let img = image::DynamicImage::new_rgb8(32, 32); let mut buf = std::io::Cursor::new(Vec::new()); img.write_to(&mut buf, image::ImageFormat::Gif).unwrap(); - assert!(check_upload_dimensions(&buf.into_inner(), "image/gif").is_ok()); + assert!(upload_dimensions(&buf.into_inner(), "image/gif").is_ok()); } #[test] - fn check_upload_dimensions_rejects_corrupt_bytes() { + fn upload_dimensions_rejects_corrupt_bytes() { // 非 magic bytes,与现有损坏文件校验文案对齐 - let err = check_upload_dimensions(b"not an image at all", "image/png").unwrap_err(); + let err = upload_dimensions(b"not an image at all", "image/png").unwrap_err(); assert_eq!(err, "图片文件损坏或格式不正确"); } diff --git a/src/pages/admin/asset_picker.rs b/src/pages/admin/asset_picker.rs index 875d4af..6f14e47 100644 --- a/src/pages/admin/asset_picker.rs +++ b/src/pages/admin/asset_picker.rs @@ -18,11 +18,13 @@ use crate::models::asset::{AssetFilter, AssetSort}; /// - `on_select`:选中回填,参数为 `/uploads/` URL。 /// - `cover_uploading`:modal 内上传新图时置位,供父页面拦截保存(与 CoverUploader 语义一致)。 #[component] +#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))] pub fn AssetPickerModal( mut visible: Signal, on_select: EventHandler, cover_uploading: Signal, ) -> Element { + #[allow(unused_mut)] let mut assets: Signal> = use_signal(Vec::new); #[allow(unused_mut)] let mut loading = use_signal(|| false); @@ -34,21 +36,20 @@ pub fn AssetPickerModal( use_effect(move || { let open = visible(); let q = query(); - if !open { - return; - } - #[cfg(target_arch = "wasm32")] - spawn(async move { - loading.set(true); - match list_assets(AssetFilter::All, q, AssetSort::CreatedDesc, 1).await { - Ok(resp) => { - assets.set(resp.assets); - error.set(None); + if open { + #[cfg(target_arch = "wasm32")] + spawn(async move { + loading.set(true); + match list_assets(AssetFilter::All, q, AssetSort::CreatedDesc, 1).await { + Ok(resp) => { + assets.set(resp.assets); + error.set(None); + } + Err(e) => error.set(Some(e.to_string())), } - Err(e) => error.set(Some(e.to_string())), - } - loading.set(false); - }); + loading.set(false); + }); + } }); if !visible() { diff --git a/src/pages/admin/assets.rs b/src/pages/admin/assets.rs index d2e2c06..5d24eb4 100644 --- a/src/pages/admin/assets.rs +++ b/src/pages/admin/assets.rs @@ -6,12 +6,17 @@ use dioxus::prelude::*; +// server fn 仅在 WASM 前端调用(全部包在 cfg(wasm32) 块内),server SSR 只编译类型。 +#[cfg(target_arch = "wasm32")] use crate::api::assets::{ delete_asset, list_assets, purge_orphan_assets, rebuild_assets_index, update_asset_alt, }; -use crate::api::assets::{AssetListResponse, PurgeOrphansResponse, RebuildAssetsResponse}; +use crate::api::assets::AssetListResponse; +#[cfg(target_arch = "wasm32")] +use crate::api::assets::{PurgeOrphansResponse, RebuildAssetsResponse}; use crate::components::empty_state::EmptyState; use crate::components::ui::{FilterTabs, Pagination}; +#[cfg(target_arch = "wasm32")] use crate::models::asset::{AssetFilter, AssetSort}; /// 每页素材数,与服务端 list.rs 的 PER_PAGE 对齐。 @@ -35,6 +40,8 @@ fn format_bytes(bytes: i64) -> String { } /// 素材管理入口组件。 +// 交互逻辑全部 cfg(wasm32) 门控,server SSR 编译时一批绑定未使用,按 CoverUploader 惯例放行。 +#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))] #[component] pub fn Assets() -> Element { // 筛选/搜索/排序/分页状态:全部客户端驱动(单路由 + signal,对齐「管理文章」模式)。 @@ -43,6 +50,7 @@ pub fn Assets() -> Element { let mut sort = use_signal(|| "created".to_string()); let mut page = use_signal(|| 1_i32); + #[allow(unused_mut)] let mut data: Signal> = use_signal(|| None); #[allow(unused_mut)] let mut loading: Signal = use_signal(|| true);