Skip to main content

yggdrasil/components/
footer.rs

1//! 页脚组件
2//!
3//! 提供站点版权信息,并在用户向下滚动超过一屏后显示"回到顶部"悬浮按钮。
4//! 回到顶部的滚动监听与平滑滚动逻辑仅在 WASM 前端生效。
5
6#[cfg(target_arch = "wasm32")]
7use crate::hooks::event_listener::use_event_listener;
8use dioxus::prelude::*;
9
10/// 页脚与回到顶部按钮组件。
11///
12/// Props:无。
13/// 关键行为:
14/// - 监听窗口滚动,超过一屏时显示回到顶部按钮
15/// - 点击按钮平滑滚动到顶部,并清理 URL 中的 `#`
16/// - 滚动监听与平滑滚动仅在 `target_arch = "wasm32"` 下执行
17#[component]
18#[allow(unused_mut)]
19pub fn Footer() -> Element {
20    let mut visible = use_signal(|| false);
21
22    // 根据 window 当前滚动位置同步 visible(注册监听后立即调用一次,避免首屏漏判)。
23    // 滚动事件回调里也复用同一份判断逻辑。
24    let mut sync_visible = move || {
25        #[cfg(target_arch = "wasm32")]
26        {
27            if let Some(w) = web_sys::window() {
28                let threshold = w
29                    .inner_height()
30                    .ok()
31                    .and_then(|h| h.as_f64())
32                    .unwrap_or(0.0);
33                let scroll_y = w.scroll_y().unwrap_or(0.0);
34                visible.set(scroll_y > threshold);
35            }
36        }
37    };
38
39    // 注册 scroll 监听:注册 / 卸载清理由 use_event_listener 负责。
40    // 仅 WASM 端调用(server 端 use_event_listener 是 noop,但 acquire 闭包内的
41    // web_sys 在非 wasm 下不可解析,故整块 cfg;hook 数量在 server build 中不影响,
42    // 因为 server 端该组件只跑一次 SSR)。
43    #[cfg(target_arch = "wasm32")]
44    use_event_listener(
45        web_sys::window,
46        "scroll",
47        // 滚动事件触发时复用同样的阈值判断。
48        sync_visible,
49    );
50
51    // 挂载时根据当前滚动位置初始化一次按钮可见性。
52    use_effect(move || {
53        sync_visible();
54    });
55
56    // 根据 visible 动态切换按钮显示/隐藏样式
57    let btn_class = use_memo(move || {
58        let base = "fixed bottom-16 right-8 z-50 w-10 h-10 rounded-full bg-paper-entry border border-paper-border shadow-sm flex items-center justify-center cursor-pointer transition-all duration-300 text-paper-secondary hover:text-paper-accent";
59        if visible() {
60            format!("{} opacity-100 translate-y-0", base)
61        } else {
62            format!("{} opacity-0 translate-y-2 pointer-events-none", base)
63        }
64    });
65
66    rsx! {
67        footer { class: "w-full border-t border-paper-border mt-auto",
68            div { class: "max-w-3xl mx-auto px-6 py-5 flex items-center justify-between text-sm text-paper-secondary",
69                span { "© 2026 Yggdrasil" }
70            }
71        }
72        a {
73            class: "{btn_class}",
74            href: "#top",
75            aria_label: "go to top",
76            title: "Go to Top (Alt + G)",
77            accesskey: "g",
78            onclick: move |evt| {
79                evt.prevent_default();
80                scroll_to_top();
81            },
82            svg {
83                xmlns: "http://www.w3.org/2000/svg",
84                height: "24px",
85                view_box: "0 -960 960 960",
86                width: "24px",
87                fill: "currentColor",
88                path { d: "m296-224-56-56 240-240 240 240-56 56-184-183-184 183Zm0-240-56-56 240-240 240 240-56 56-184-183-184 183Z" }
89            }
90        }
91    }
92}
93
94/// 平滑滚动到页面顶部,并清理 history 中的 `#` 哈希。
95///
96/// 仅在 `target_arch = "wasm32"` 下执行实际滚动,SSR 环境中为空操作。
97fn scroll_to_top() {
98    #[cfg(target_arch = "wasm32")]
99    {
100        if let Some(window) = web_sys::window() {
101            let options = web_sys::ScrollToOptions::new();
102            options.set_top(0.0);
103            options.set_behavior(web_sys::ScrollBehavior::Smooth);
104            window.scroll_to_with_scroll_to_options(&options);
105
106            if let Ok(history) = window.history() {
107                let _ = history.replace_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(" "));
108            }
109        }
110    }
111}