From 45122fc687e67ebca7dc92bae5dc50a7093ba15d Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 16 Jul 2026 16:22:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(post):=20scrollToHash=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E6=80=A7=E5=AE=88=E5=8D=AB=EF=BC=8C=E5=88=87?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E4=B8=8D=E5=86=8D=E8=B7=B3=E5=9B=9E=20URL=20?= =?UTF-8?q?hash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostContent 的 use_effect 读取 resolved_theme() 建立订阅,主题切换时重跑 (为重跑 mermaid 主题)。但该 effect 同时调用 __scrollToHash,主题切换会 把页面跳回 URL hash 位置——即使该 hash 早已不在用户视野。 scrollToHash 是首屏异步取数后的补救滚动(弥补骨架屏阶段标题 DOM 缺失导致的 原生 fragment-scroll 失效),本质上是一次性的,不应随主题切换重跑。增加 did_scroll 信号守卫,仅首次 effect 运行时调用。 --- src/components/post/post_content.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/post/post_content.rs b/src/components/post/post_content.rs index 43af239..a8f7876 100644 --- a/src/components/post/post_content.rs +++ b/src/components/post/post_content.rs @@ -143,6 +143,13 @@ pub fn PostContent(content_html: String) -> Element { #[cfg(target_arch = "wasm32")] let resolved_theme = crate::theme::use_resolved_theme(); + // scrollToHash 的一次性守卫:仅首次 effect 运行时调用。 + // 下方 use_effect 因读取 resolved_theme() 建立订阅,主题切换时会重跑(为重跑 + // mermaid 主题)。但 scrollToHash 是首屏异步取数后的补救滚动,不应随主题切换 + // 再次触发——否则切换主题会把页面跳回 URL hash 位置(即使 hash 已不在用户视野)。 + #[cfg(target_arch = "wasm32")] + let mut did_scroll = use_signal(|| false); + #[cfg(target_arch = "wasm32")] use_effect(move || { let window = web_sys::window().unwrap(); @@ -181,7 +188,13 @@ pub fn PostContent(content_html: String) -> Element { // 解决骨架屏阶段标题 DOM 缺失导致浏览器原生 fragment-scroll 失效的问题: // PostDetail 用 use_server_future 异步取数,首屏渲染骨架屏,此时标题 DOM // 不存在;浏览器尝试滚动到 #hash 找不到目标留在顶部。此处标题已就绪,补一次。 - invoke_optional_global(&window, "__scrollToHash", &[]); + // + // 仅首次运行:此 effect 因读取 resolved_theme() 而在主题切换时重跑,但 + // hash 滚动是首屏补救措施,重跑会导致切主题时页面跳回 URL hash 位置。 + if !did_scroll() { + did_scroll.set(true); + invoke_optional_global(&window, "__scrollToHash", &[]); + } }); rsx! {