From f408bf5312be84513963fd771707a4a14a38b433 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 4 Nov 2026 19:34:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(app-iced):=20M107=20r2=20=E5=A4=8D=E5=AE=A1?= =?UTF-8?q?=20=E2=80=94=E2=80=94=20clippy=20needless=5Flifetimes=20+=20?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E4=B8=AD=E7=82=B9=E6=A1=B6=E7=AB=8B=E5=88=BB?= =?UTF-8?q?=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **P1-1**:`group::active_bucket` 的显式生命周期是多余的(`needless_lifetimes`),省掉之后 `cargo clippy -p unlucky-app-iced -p unlucky-i18n -p unlucky-presenter --all-targets -j 2 -- -D warnings` 才真的 exit 0。上一轮报告的那条「clippy 本轮未复跑」正是红的根因—— 本轮把 clippy 纳入每次门禁(见报告)。 - **P2-1(b)**:搜索期间第二级侧栏原来「可点但没反应」(一致性放宽让桶失效,点击要等清空搜索 才生效)。现在**点桶 = 退出搜索 + 按该桶过滤**:`SelectMajor` 发现 `has_query()` 就先落选择 再 `rebuild_after_query("")`,一次点击做完「我要看这个桶」这件事。 判据:app 层 `clicking_a_bucket_during_a_search_leaves_the_search`(432 passed)。 门禁(真实退出码,无管道): `cargo fmt --all --check` = 0;`clippy … -D warnings -j 2` = 0;`cargo test -p unlucky-app-iced --lib -j 2` = 432 passed / 0 failed --- crates/app-iced/src/app/update/download.rs | 55 ++++++++++++++++++++- crates/app-iced/src/pages/download.rs | 1 + crates/app-iced/src/pages/download/group.rs | 2 +- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/app-iced/src/app/update/download.rs b/crates/app-iced/src/app/update/download.rs index 7996fcd..cff3c40 100644 --- a/crates/app-iced/src/app/update/download.rs +++ b/crates/app-iced/src/app/update/download.rs @@ -91,8 +91,19 @@ impl App { self.rebuild_after_query(String::new()); } // M107:主版本桶的选中是纯视图态,不重建、不派命令(同 `SelectVersion`)。 + // + // **搜索期间点桶 = 「我就是要看这个桶」**(复审 r2 P2-1(b)):搜索让第二级失效(P2-1 的 + // 一致性放宽),如果这里只改选择而不清查询,用户会看到「点了没反应、清空搜索后才生效」 + // 的活控件。所以搜索中点桶顺带退出搜索,按新桶过滤——一次点击做完用户想要的两件事。 pages::Message::SelectMajor(key) => { - self.view.download.selected_major = key.clone(); + if self.view.download.has_query() { + // 先退出搜索再落选择:`rebuild_after_query` 会用**全量行**重算桶并回填选择 + // (它带回 `selected_major`,且校验桶仍存在)。 + self.view.download.selected_major = key.clone(); + self.rebuild_after_query(String::new()); + } else { + self.view.download.selected_major = key.clone(); + } } // M107:类型栏的手动折叠也是纯视图态。 pages::Message::ToggleTypeRail => { @@ -297,6 +308,48 @@ mod tests { assert_eq!(app.view.download.rows.len(), 1, "列表仍收窄"); } + /// **搜索期间点主版本桶 = 退出搜索并按该桶过滤**(复审 r2 P2-1(b))。 + /// + /// 抓得住:`SelectMajor` 只改选择不清查询 —— 那时搜索让第二级失效(P2-1 的一致性放宽), + /// 用户点了当场没反应、要等清空搜索那次点击才生效,就是「可点但无反应」的活控件。 + /// 抓不住:真机点击是否走到这里(`ListItem`/`Chip` 的 `on_press` 是组件既有实现)。 + #[test] + fn clicking_a_bucket_during_a_search_leaves_the_search() { + let mut app = app(); + let _ = app.on_page(pages::Message::SetVersionQuery("1.20".to_string())); + assert_eq!(app.view.download.rows.len(), 2, "此刻在搜 1.20"); + + // 点「1.21」这个桶 + let _ = app.on_page(pages::Message::SelectMajor(Some("1.21".to_string()))); + + assert!(app.state.download_query.is_empty(), "点桶顺带退出搜索"); + assert!(!app.view.download.has_query()); + assert_eq!( + app.view.download.selected_major.as_deref(), + Some("1.21"), + "选择落在刚点的那个桶上" + ); + assert_eq!( + app.view.download.rows.len(), + 3, + "查询词清掉之后模型回到全量(桶过滤发生在渲染时的 visible 里)" + ); + assert_eq!( + app.view + .download + .buckets + .iter() + .find(|bucket| bucket.key.as_deref() == Some("1.21")) + .map(|bucket| bucket.count), + Some(1), + "1.21 桶里只有一个版本 ⇒ 列表会显示一行" + ); + + // 没有搜索词时点桶照旧只改选择(不触发多余重建) + let _ = app.on_page(pages::Message::SelectMajor(None)); + assert_eq!(app.view.download.selected_major, None); + } + /// 搜索时侧栏的**位置与折叠**不许丢(`rebuild_after_query` 的三行)。 /// /// 抓得住:删掉那两行回填(敲第一个字符,类型栏自己弹开、主版本跳回「全部」)。 diff --git a/crates/app-iced/src/pages/download.rs b/crates/app-iced/src/pages/download.rs index b207cad..96e7b3d 100644 --- a/crates/app-iced/src/pages/download.rs +++ b/crates/app-iced/src/pages/download.rs @@ -62,6 +62,7 @@ //! | 有命中 | 列表照常;侧栏**位置不动**;搜索条右侧给「N 个匹配」(跨类型读数) | //! | 一个都不命中 | [`states::no_match`]:没有匹配 + 换短片段 + 清空 | //! | 有命中但不在当前类型 | [`states::elsewhere`]:N 个在别的类型 + 「看全部类型」 | +//! | 搜索中点第二级桶 | **退出搜索 + 按那个桶过滤**(点桶 = 「我就是要看这个桶」,见 `on_download`) | //! //! 查询词写在 presenter 快照的 `download_query` 里(页面无状态,见 [`model::Download::from_snapshot`]), //! 所以「搜索 → 任何事件回来之后仍然在搜」这条不需要页面持有任何字段。 diff --git a/crates/app-iced/src/pages/download/group.rs b/crates/app-iced/src/pages/download/group.rs index ac50acf..51ff5e0 100644 --- a/crates/app-iced/src/pages/download/group.rs +++ b/crates/app-iced/src/pages/download/group.rs @@ -151,7 +151,7 @@ pub fn buckets(rows: &[super::model::VersionRow]) -> Vec { /// 放宽之后「列表行数 = 当前分类里的命中数」,侧栏也不会有「选中项不在桶列表里」的幽灵态。 /// `selected_major` 一直留在模型里,只是搜索期间不生效——清空搜索后原来的桶自动回来。 #[must_use] -pub fn active_bucket<'a>(model: &'a super::model::Download) -> Option<&'a str> { +pub fn active_bucket(model: &super::model::Download) -> Option<&str> { if model.has_query() { None } else {