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:
parent
8b1d55613e
commit
cb8c42e833
@ -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-ratio:SSR 时读图片真实尺寸。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),
|
||||
"✕"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
/// 文章详情组件。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user