fix(home,tags): 同变体路由分页/切标签后列表不更新

延续上一个 commit(fix(post))发现的同类 bug,统一修复 home 和 tags:

- home: /page/1 → /page/2(同 Route::HomePage 变体)后文章列表不更新
- tags: /tags/a → /tags/b(同 Route::TagDetail 变体)后标签下文章不更新

根因完全一致:use_server_future 闭包内读取的 current_page / tag 是普通 prop
(i32 / String),move 进闭包后成为冻结快照,不建立反应式订阅,同变体导航
复用组件实例时 future 不重跑。

注意 / → /page/2(Route::Home → Route::HomePage,跨变体)不受影响,因为跨
变体会新挂载组件、future 首次运行;只有同变体间的 prop 变化才触发 bug。

修复:闭包内通过 router().current::<Route>() 读取当前 page/tag 建立订阅。
Home(/ 路由,Route::Home 变体无 page 字段)调用 HomePosts 时走兜底分支用
传入的 current_page。

验证(headless chromium,本地设 APP_BASE_URL 绕过 CSRF 403):
- home 修复前:/page/1 点下一页,URL 变 /page/2,首篇仍是 page1 内容(复现)
- home 修复后:首篇随分页变为 page2 内容
- tags 因数据库无多文章标签,无法端到端验证;代码模式与 home 一致且编译通过
This commit is contained in:
xfy 2026-07-09 16:22:08 +08:00
parent 79978aa4a4
commit 1c56fd8ec1
2 changed files with 34 additions and 3 deletions

View File

@ -51,10 +51,26 @@ pub fn HomePage(page: i32) -> Element {
/// ///
/// 通过 `use_server_future` 异步获取当前页文章; /// 通过 `use_server_future` 异步获取当前页文章;
/// 加载中显示骨架屏,加载失败显示错误提示,成功则渲染文章卡片与分页。 /// 加载中显示骨架屏,加载失败显示错误提示,成功则渲染文章卡片与分页。
///
/// # 反应式取数
/// 同 `post_detail.rs``current_page` 是普通 `i32` prop被 `move` 进
/// `use_server_future` 闭包后成为冻结快照,读取它不建立反应式订阅。因此
/// `/page/1 → /page/2`(同为 `Route::HomePage` 变体,复用组件实例)这样的
/// 同变体分页导航无法触发 future 重跑。修复方式:在闭包内通过
/// `router().current::<Route>()` 读取当前 page建立订阅路由变化即重跑。
/// `Home``/`)调用时路由变体无 page 字段,兜底用传入的 `current_page`。
#[component] #[component]
fn HomePosts(current_page: i32) -> Element { fn HomePosts(current_page: i32) -> Element {
// 调用 server function 获取已发布文章分页数据。 let router = dioxus::router::router();
let posts_res = use_server_future(move || list_published_posts(current_page, POSTS_PER_PAGE))?;
let posts_res = use_server_future(move || {
let page = match router.current::<Route>() {
Route::HomePage { page } => page.max(1),
// / 路由Route::Home及其它变体用 prop 兜底。
_ => current_page,
};
list_published_posts(page, POSTS_PER_PAGE)
})?;
// 将结果映射为更便于本地使用的 (posts, total) 形式。 // 将结果映射为更便于本地使用的 (posts, total) 形式。
let posts_data = posts_res.read().as_ref().map(|r| match r { let posts_data = posts_res.read().as_ref().map(|r| match r {

View File

@ -120,9 +120,24 @@ pub fn TagDetail(tag: String) -> Element {
/// ///
/// 通过 `use_server_future` 调用 `get_posts_by_tag` 获取指定标签下的文章; /// 通过 `use_server_future` 调用 `get_posts_by_tag` 获取指定标签下的文章;
/// 成功时渲染文章总数与文章卡片。 /// 成功时渲染文章总数与文章卡片。
///
/// # 反应式取数
/// 同 `post_detail.rs` / `home.rs``tag` 是普通 `String` prop被 `move` 进
/// `use_server_future` 闭包后成为冻结快照,不建立反应式订阅。从标签云点击
/// 切换标签(`/tags/a → /tags/b`,同为 `Route::TagDetail` 变体,复用组件实例)
/// 时 future 不会重跑。修复:在闭包内通过 `router().current::<Route>()` 读取
/// 当前 tag 建立订阅,路由变化即重跑。
#[component] #[component]
fn TagDetailContent(tag: String) -> Element { fn TagDetailContent(tag: String) -> Element {
let posts_res = use_server_future(move || get_posts_by_tag(tag.clone(), None, None))?; let router = dioxus::router::router();
let posts_res = use_server_future(move || {
let current_tag = match router.current::<Route>() {
Route::TagDetail { tag } => tag,
_ => tag.clone(),
};
get_posts_by_tag(current_tag, None, None)
})?;
// 将结果映射为 (posts, total) 形式以便渲染。 // 将结果映射为 (posts, total) 形式以便渲染。
let posts_data = posts_res.read().as_ref().map(|r| match r { let posts_data = posts_res.read().as_ref().map(|r| match r {