依据 Dioxus 0.7 optimizing 指南做两项改进: - 为数据驱动列表渲染补全显式 key(13 处,9 个文件): home/archives/tags/search 文章与标签列表、post_card/post_footer 标签、header 桌面/移动导航、admin dashboard/posts/write 列表。 使用各类型稳定唯一字段(post.id / tag / tag.name / item.label / err.id / year_group.year / month_group.month_en)作为 key, 让 Dioxus diff 正确识别元素身份。骨架屏 for _ in 0..N 静态占位 循环与已带 key 的评论树/admin 行不动。 - [profile.release] 加 panic = "abort":WASM 去掉 unwind 元数据减小 体积;server 端错误处理走 Result+?(见 error.rs),不依赖 panic unwind,无副作用。 其余文件为本机格式化(单行折叠/多行展开),一并提交。 验证:dx check 通过、cargo test 405 passed。
86 lines
3.7 KiB
Rust
86 lines
3.7 KiB
Rust
//! 搜索页面模块。
|
||
//!
|
||
//! 对应路由 `/search`。
|
||
//!
|
||
//! 数据获取:用户在输入框中键入关键词并触发搜索后,
|
||
//! 通过 Dioxus 的 `spawn` 在本地启动异步任务,调用 `search_posts` server function。
|
||
//! 与首页/归档不同,搜索是交互式客户端行为,不在服务端渲染阶段预取数据。
|
||
//! 在 `wasm32` 目标下,该 server function 的函数体被替换为向服务端端点发起 HTTP POST 请求的客户端存根;
|
||
//! 实际的数据库访问逻辑仅在 `feature = "server"` 启用时运行。
|
||
|
||
use dioxus::prelude::*;
|
||
|
||
use crate::api::posts::{search_posts, PostListResponse};
|
||
use crate::components::post_card::PostCard;
|
||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||
use crate::components::skeletons::search_skeleton::SearchSkeleton;
|
||
|
||
/// 搜索页面组件,对应路由 `/search`。
|
||
///
|
||
/// 维护搜索关键词、搜索结果与加载状态,渲染搜索框与结果列表。
|
||
/// 结果通过客户端异步请求获取,而非 `use_server_future` 预取。
|
||
#[component]
|
||
pub fn Search() -> Element {
|
||
// 当前输入框中的搜索关键词。
|
||
let mut query = use_signal(|| "".to_string());
|
||
// 搜索结果:None 表示尚未执行搜索或已清空。
|
||
let mut search_res = use_signal(|| None::<Result<PostListResponse, ServerFnError>>);
|
||
// 是否正在发起搜索请求。
|
||
let mut is_searching = use_signal(|| false);
|
||
// 触发搜索的回调:校验空查询后启动异步请求。
|
||
let mut on_search = move || {
|
||
let q = query().trim().to_string();
|
||
if q.is_empty() {
|
||
return;
|
||
}
|
||
is_searching.set(true);
|
||
search_res.set(None);
|
||
spawn(async move {
|
||
let res = search_posts(q).await;
|
||
search_res.set(Some(res));
|
||
is_searching.set(false);
|
||
});
|
||
};
|
||
|
||
rsx! {
|
||
header { class: "page-header mb-6",
|
||
h1 { class: "text-4xl font-bold text-paper-primary tracking-tight", "搜索" }
|
||
}
|
||
div { class: "mb-8",
|
||
div { class: "flex gap-2",
|
||
input {
|
||
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",
|
||
r#type: "text",
|
||
placeholder: "输入关键词搜索文章...",
|
||
value: query(),
|
||
oninput: move |e| query.set(e.value()),
|
||
onkeydown: move |e| {
|
||
if e.key() == Key::Enter {
|
||
on_search()
|
||
}
|
||
},
|
||
}
|
||
button {
|
||
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",
|
||
onclick: move |_| on_search(),
|
||
"搜索"
|
||
}
|
||
}
|
||
}
|
||
// 根据搜索状态展示骨架屏、结果列表、空状态或错误提示。
|
||
if is_searching() {
|
||
DelayedSkeleton { SearchSkeleton {} }
|
||
} else if let Some(Ok(PostListResponse { posts, total: _ })) = search_res() {
|
||
if posts.is_empty() {
|
||
div { class: "text-center text-paper-secondary py-20", "未找到相关文章" }
|
||
} else {
|
||
for post in posts.iter() {
|
||
PostCard { key: "{post.id}", post: post.clone() }
|
||
}
|
||
}
|
||
} else if search_res().as_ref().map(|r| r.is_err()).unwrap_or(false) {
|
||
div { class: "text-center text-red-500 dark:text-red-400 py-20", "搜索失败" }
|
||
}
|
||
}
|
||
}
|