Skip to main content

yggdrasil/pages/
search.rs

1//! 搜索页面模块。
2//!
3//! 对应路由 `/search`。
4//!
5//! 数据获取:用户在输入框中键入关键词并触发搜索后,
6//! 通过 Dioxus 的 `spawn` 在本地启动异步任务,调用 `search_posts` server function。
7//! 与首页/归档不同,搜索是交互式客户端行为,不在服务端渲染阶段预取数据。
8//! 在 `wasm32` 目标下,该 server function 的函数体被替换为向服务端端点发起 HTTP POST 请求的客户端存根;
9//! 实际的数据库访问逻辑仅在 `feature = "server"` 启用时运行。
10
11use dioxus::prelude::*;
12
13use crate::api::posts::{search_posts, PostListResponse};
14use crate::components::empty_state::EmptyState;
15use crate::components::post_card::PostCard;
16use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
17use crate::components::skeletons::search_skeleton::SearchSkeleton;
18
19/// 搜索页面组件,对应路由 `/search`。
20///
21/// 维护搜索关键词、搜索结果与加载状态,渲染搜索框与结果列表。
22/// 结果通过客户端异步请求获取,而非 `use_server_future` 预取。
23#[component]
24pub fn Search() -> Element {
25    // 当前输入框中的搜索关键词。
26    let mut query = use_signal(|| "".to_string());
27    // 搜索结果:None 表示尚未执行搜索或已清空。
28    let mut search_res = use_signal(|| None::<Result<PostListResponse, ServerFnError>>);
29    // 是否正在发起搜索请求。
30    let mut is_searching = use_signal(|| false);
31    // 触发搜索的回调:校验空查询后启动异步请求。
32    let mut on_search = move || {
33        let q = query().trim().to_string();
34        if q.is_empty() {
35            return;
36        }
37        is_searching.set(true);
38        search_res.set(None);
39        spawn(async move {
40            let res = search_posts(q).await;
41            search_res.set(Some(res));
42            is_searching.set(false);
43        });
44    };
45
46    rsx! {
47        div { class: "animate-page-enter",
48            header { class: "page-header mb-6",
49                h1 { class: "text-4xl font-bold text-paper-primary tracking-tight",
50                    "搜索"
51                }
52            }
53            div { class: "mb-8",
54                div { class: "flex gap-2",
55                    input {
56                        class: "flex-1 px-4 py-2 border border-paper-border rounded-lg bg-paper-entry text-paper-primary placeholder:text-paper-tertiary focus:outline-none focus:border-paper-accent focus:ring-1 focus:ring-paper-accent/30",
57                        r#type: "text",
58                        placeholder: "输入关键词搜索文章...",
59                        value: query(),
60                        oninput: move |e| query.set(e.value()),
61                        onkeydown: move |e| {
62                            if e.key() == Key::Enter {
63                                on_search()
64                            }
65                        },
66                    }
67                    button {
68                        class: "px-6 py-2 bg-paper-accent text-white rounded-full font-medium hover:brightness-110 active:scale-[0.98] transition-all duration-200",
69                        onclick: move |_| on_search(),
70                        "搜索"
71                    }
72                }
73            }
74            // 根据搜索状态展示骨架屏、结果列表、空状态或错误提示。
75            if is_searching() {
76                DelayedSkeleton { SearchSkeleton {} }
77            } else if let Some(Ok(PostListResponse { posts, total: _ })) = search_res() {
78                if posts.is_empty() {
79                    EmptyState {
80                        title: "未找到相关文章",
81                        description: "换个关键词再试一次吧",
82                    }
83                } else {
84                    for post in posts.iter() {
85                        PostCard { key: "{post.id}", post: post.clone() }
86                    }
87                }
88            } else if search_res().as_ref().map(|r| r.is_err()).unwrap_or(false) {
89                div { class: "text-center text-red-500 dark:text-red-400 py-20", "搜索失败" }
90            } else {
91                div { class: "flex flex-col items-center justify-center mt-24 mb-12",
92                    img {
93                        class: "w-56 h-auto rounded-lg select-none dark:brightness-90",
94                        src: "/images/xiantiaoxiaogou_02.webp",
95                        alt: "空状态提示",
96                        draggable: "false",
97                    }
98                }
99            }
100        }
101    }
102}