Skip to main content

yggdrasil/components/
footer.rs

1//! 页脚组件
2//!
3//! 单行:© · RSS · 更新日志 · 写作指南 · 构建说明 · GitHub(站点设置配置时显示)
4//! · 回到顶部。样式见 styles/components/shell.css。
5
6use leptos::prelude::*;
7
8use crate::api::settings::get_site_settings;
9use crate::components::ui::{Icon, IconName};
10use crate::hooks::ssr_resource::{route_data, unit, PageJson};
11use crate::models::settings::SiteSettings;
12use crate::routes::Route;
13
14/// 页脚的站点设置:懒加载分包,主包只留与类型无关的资源代码。
15#[leptos::lazy]
16async fn fetch_footer_settings() -> PageJson<SiteSettings> {
17    PageJson::new(get_site_settings().await)
18}
19
20/// 页脚组件:读取站点设置中的 GitHub 链接。
21///
22/// Props:
23/// - `max_width`:内部内容的宽度类,需与页头、正文一致以保证左右边缘对齐
24#[component]
25pub fn Footer(#[prop(default = "max-w-[880px]")] max_width: &'static str) -> impl IntoView {
26    // 读取站点公开配置(页脚 GitHub 链接)。公开接口,SSR 时解析并随页面序列化,
27    // 水合直接复用;服务端有 moka 缓存兜底,解析极快。route_data 只在 `<Suspense>` 内读取,
28    // 且 Suspense 只包住 GitHub 图标,页脚其余部分不等待数据。
29    let settings_res = route_data(|| (), unit(fetch_footer_settings));
30
31    view! {
32        <footer data-vt-shell="frontend-footer" class="site-footer">
33            <div class=format!("site-footer-inner {max_width}")>
34                <span class="tabular-nums">"© 2026 Yggdrasil"</span>
35                <div class="site-footer-links">
36                    // rel="external":让路由协调器放行,整页加载 RSS。
37                    <a href="/feed.xml" rel="external">
38                        "RSS"
39                    </a>
40                    <a href=Route::Changelog.to_string()>"更新日志"</a>
41                    <a href=Route::WritingGuide.to_string()>"写作指南"</a>
42                    <span>"由 Rust 与 Leptos 构建"</span>
43                    <Suspense>
44                        {move || Suspend::new(async move {
45                            let github_url = settings_res
46                                .await
47                                .ok()
48                                .map(|s| s.github_url)
49                                .filter(|u| !u.is_empty());
50                            github_url.map(|url| view! {
51                                <a
52                                    href=url
53                                    target="_blank"
54                                    rel="external noopener noreferrer"
55                                    aria-label="GitHub"
56                                    title="GitHub"
57                                >
58                                    <Icon name=IconName::Github size=16/>
59                                </a>
60                            })
61                        })}
62                    </Suspense>
63                    <button type="button" on:click=move |_| scroll_to_top()>
64                        "回到顶部"
65                        <Icon name=IconName::ArrowUp size=13/>
66                    </button>
67                </div>
68            </div>
69        </footer>
70    }
71}
72
73/// 平滑滚动到页面顶部,并清理 history 中的 `#` 哈希。
74///
75/// 仅在 `target_arch = "wasm32"` 下执行实际滚动,SSR 环境中为空操作。
76fn scroll_to_top() {
77    #[cfg(target_arch = "wasm32")]
78    {
79        if let Some(window) = web_sys::window() {
80            let options = web_sys::ScrollToOptions::new();
81            options.set_top(0.0);
82            options.set_behavior(web_sys::ScrollBehavior::Smooth);
83            window.scroll_to_with_scroll_to_options(&options);
84
85            if let Ok(history) = window.history() {
86                let _ = history.replace_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(" "));
87            }
88        }
89    }
90}