refactor: remove obsolete ImageViewer component

both call sites (PostCover, PostCard) now render raw .blur-img, so the
Dioxus ImageViewer component — with its leaky closure.forget() and dead
js_sys::eval delay — is no longer needed. remove the file and its mod
declaration.
This commit is contained in:
xfy 2026-06-23 15:51:40 +08:00
parent 8b1d55613e
commit cb8c42e833
2 changed files with 1 additions and 166 deletions

View File

@ -1,163 +0,0 @@
//! 图片查看器组件
//!
//! 提供缩略图展示与点击放大后的全屏灯箱lightbox查看
//! 支持自定义缩略图参数、alt 文本与懒加载。
//! 灯箱支持键盘操作Escape 关闭、Tab 在关闭按钮与图片间循环。
use dioxus::prelude::*;
/// 图片查看器组件。
///
/// Props
/// - `src`:原图 URL
/// - `thumb_params`:缩略图 URL 参数,默认 `"?w=800"`
/// - `alt`:图片替代文本,默认 `"图片"`
/// - `lazy_load`:是否启用懒加载,默认 `false`
/// - `lightbox`:是否启用点击放大灯箱,默认 `true`。卡片等场景设为 `false`
/// 以禁用灯箱交互,使整张卡片归一为单一跳转行为(避免嵌套交互)。
///
/// 关键事件(仅在 `lightbox == true` 时生效):
/// - 点击缩略图:打开全屏灯箱
/// - 点击遮罩或关闭按钮:关闭灯箱
/// - 点击灯箱内容区:阻止事件冒泡,避免误关闭
/// - 键盘 Escape关闭灯箱
#[component]
pub fn ImageViewer(
src: String,
#[props(default = "?w=800".to_string())] thumb_params: String,
#[props(default = "?w=20".to_string())] placeholder_params: String,
#[props(default = "图片".to_string())] alt: String,
#[props(default = false)] lazy_load: bool,
#[props(default = true)] lightbox: bool,
) -> Element {
let mut is_open = use_signal(|| false);
// 打开灯箱时聚焦关闭按钮,并监听 Escape 键关闭。
// 灯箱禁用时跳过键盘监听与焦点管理,避免无谓的 DOM 操作。
#[cfg(target_arch = "wasm32")]
{
use wasm_bindgen::{closure::Closure, JsCast};
use_effect(move || {
if !lightbox || !is_open() {
return;
}
wasm_bindgen_futures::spawn_local(async move {
// 下一帧再聚焦,确保 DOM 已渲染。
let _ = js_sys::eval("new Promise(r => requestAnimationFrame(r))").unwrap();
if let Some(btn) = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.query_selector(".image-viewer-close").ok())
.flatten()
.and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok())
{
let _ = btn.focus();
}
});
let closure = Closure::wrap(Box::new(move |e: web_sys::KeyboardEvent| {
if e.key() == "Escape" {
is_open.set(false);
}
}) as Box<dyn FnMut(_)>);
if let Some(window) = web_sys::window() {
let _ = window.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref());
}
closure.forget();
});
}
// 计算 aspect-ratioSSR 时读图片真实尺寸。WASM 端不读(--ar 已在 SSR 写入 HTML
// 非 /uploads/ 的外链图或读不到尺寸时不设 --ar。
let ar_style = {
// `mut` 仅 server 构建需要WASM 构建剥离 #[cfg(feature = "server")] 块后,
// s 从未被重新赋值,故对 WASM 抑制 unused_mut。
#[cfg_attr(not(feature = "server"), allow(unused_mut))]
let mut s = String::new();
#[cfg(feature = "server")]
{
if let Some(rel) = src
.strip_prefix("/uploads/")
.map(|p| p.split('?').next().unwrap_or(p))
{
if let Some((w, h)) = crate::api::image::get_image_dimensions(rel) {
// 注意CSS aspect-ratio 用斜杠分隔width / height不是冒号
s = format!("--ar:{} / {};", w, h);
}
}
}
s
};
// 拼接占位图 URL 和高清图 URL
let placeholder_src = if src.contains('?') {
format!(
"{}&{}",
src.split('?').next().unwrap_or(&src),
placeholder_params.trim_start_matches('?')
)
} else {
format!("{}{}", src, placeholder_params)
};
let full_src = if src.contains('?') {
format!(
"{}&{}",
src.split('?').next().unwrap_or(&src),
thumb_params.trim_start_matches('?')
)
} else {
format!("{}{}", src, thumb_params)
};
rsx! {
// blur-up 双层:底层占位图 + 上层高清图data-src 由前端 JS 懒加载)
// 灯箱禁用时不绑定 onclick使缩略图成为纯展示元素卡片由外层覆盖链接接管跳转
span {
class: "blur-img",
style: "{ar_style}",
onclick: move |_| if lightbox { is_open.set(true) },
img {
class: "blur-img-placeholder",
src: "{placeholder_src}",
alt: "{alt}",
loading: if lazy_load { "lazy" } else { "eager" },
}
img {
class: "blur-img-full",
"data-src": "{full_src}",
alt: "{alt}",
}
}
// 全屏灯箱(仅 lightbox 启用时可能打开)
if lightbox && is_open() {
div {
class: "image-viewer-overlay",
role: "dialog",
aria_modal: "true",
aria_label: "图片预览",
onclick: move |_| is_open.set(false),
div {
class: "image-viewer-content",
onclick: move |evt: dioxus::events::MouseEvent| evt.stop_propagation(),
img {
class: "image-viewer-img",
src: "{src}",
alt: "{alt}",
tabindex: 0,
}
button {
class: "image-viewer-close",
r#type: "button",
aria_label: "关闭图片预览",
onclick: move |_| is_open.set(false),
""
}
}
}
}
}
}

View File

@ -3,7 +3,7 @@
//! 提供 Dioxus UI 组件,供前端页面(`src/pages/`)使用。
//! 包括布局(`frontend_layout`、`admin_layout`)、导航(`header`、`nav`、`footer`)、
//! 文章展示(`post`、`post_card`)、评论(`comments`)、骨架屏(`skeletons`)、
//! 表单控件(`forms`以及图片查看器(`image_viewer`等共享组件。
//! 表单控件(`forms`等共享组件。
/// 后台布局组件。
pub mod admin_layout;
@ -19,8 +19,6 @@ pub mod forms;
pub mod frontend_layout;
/// 顶部导航栏组件。
pub mod header;
/// 图片查看器组件。
pub mod image_viewer;
/// 导航组件。
pub mod nav;
/// 文章详情组件。