feat(app-iced): M101 r2 设置页状态漏斗 + pages/mod.rs 拆分 + r1 复审订正

r1 复审的 P1:消息漏斗只解决了「加一支消息」,一组要画真值仍要改 `settings/mod.rs`
(`Group.body` 与 `view` 只认 `appearance::State`)。本 commit 补齐与消息漏斗同构的状态漏斗:

- 新增 `pages/settings/state.rs`:`Groups`(一个组一个字段,字段类型在组自己文件里)+
  `States`(按值传给渲染器的只读视图:外观状态 + 组状态)+ `demo_groups()`(harness 的
  `'static` 默认状态)。
- `Group.body` 签名改成 `for<'a> fn(&'a Ui, States<'a>) -> Option<Element>`:每组自己从
  `States` 取自己要的数据,「没有可画的状态」返回 `None` 由 `form` 退回演示行。
  `appearance` 组照旧接 `pages/appearance/section.rs`(M93 路径行为不变)。
- `App.settings: pages::settings::Groups`(`app/state.rs` 字段列表末尾)+ `app/view.rs` 构造
  `States` 走新入口 `pages::page_with_settings`。加/改某一组的字段只改那一组的文件。
- 四个组各自的 `State` 落在各自文件;`java::Message` 留一支只有测试构造的 `Ping`,配
  `app/update/settings/tests.rs` 的 `a_group_message_reaches_its_handler_through_the_funnel`
  ——消息漏斗第一次有**运行时**证据。

r1 其余条目:
- P2-1 删掉 `only_the_appearance_group_carries_a_real_control_body`(被四条 per-group 断言蕴含),
  并在同处写明删掉之后「唯一性」没人守;三份 section 文档从「改 3 个文件」订正为 2 个。
- P2-2 `pub mod settings` → `pub(crate) mod settings`(四个消息口模块 `pub(crate)`),
  并订正两处理由(lint 按类型自身可见性判,不是可达性)。
- P2-3 `pages/mod.rs` 397 行 → 拆出 `pages/message.rs` / `pages/screen.rs`(纯搬移,`pub use`
  保证 `pages::Message` / `Action` / `Screen` 路径不变),并删掉 M101 自己那 18 行重复文档。
- P3-1 订正共享点清单(`Key` 由宏从域文件累积生成,加 key 不改 `catalog.rs`)。
- P3-2 守门改成「除已知臂外任何含 `=>` 的行都算兜底」(`other =>` 也拦),并加「恰好四臂」断言。
- P3-3 `ai.rs` → `ai_about.rs` 单独一条 R100 改名提交,`git log --follow` 不断链。
- P3-4 `message.rs` 的「加一支消息改 1 个文件」订正为 2 个。
This commit is contained in:
AskaEth committed 2026-11-03 20:57:08 +08:00
1 parent 423ea77758
commit 029ece3f17
25 files changed
+1004 -240

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
//! 页面枚举 `Screen` 与它的三张对照表(版面 / 导航项 / 路由;M101 r2 从 `pages/mod.rs` **原样搬出**)。
//!
//! 为什么拆:合并 M100/M101 之后 `pages/mod.rs` 是 397 行,只剩 3 行余量。纯搬移,路径靠
//! `pages/mod.rs` 的 `pub use screen::Screen;` 保持不变(`pages::Screen`)。
use unlucky_presenter::router::{NavSection, Route};
use super::layout::PaneKit;
/// 四个页面。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Screen {
/// 实例(list-detail)。
Instances,
/// 下载(单列)。
Download,
/// 设置(supporting-pane)。
Settings,
/// 日志(单列,两行页头)。
Logs,
}
impl Screen {
/// 全部页面(截图矩阵按这个顺序铺)。
pub const ALL: [Screen; 4] = [
Screen::Instances,
Screen::Download,
Screen::Settings,
Screen::Logs,
];
/// 稳定 slug(命令行参数与文件名用)。
pub const fn slug(self) -> &'static str {
match self {
Screen::Instances => "instances",
Screen::Download => "download",
Screen::Settings => "settings",
Screen::Logs => "logs",
}
}
/// 反向解析(`--screen=instances`)。
pub fn from_slug(slug: &str) -> Option<Self> {
Self::ALL.into_iter().find(|screen| screen.slug() == slug)
}
/// 页面声明的**最宽**版面;实际用哪个由 [`layout::resolve`](super::layout) 按窗口宽度决定。
pub const fn kit(self) -> PaneKit {
match self {
Screen::Instances => PaneKit::ListDetail,
Screen::Settings => PaneKit::Supporting,
Screen::Download | Screen::Logs => PaneKit::Single,
}
}
/// 路由落在哪一页上(`None` = 这一页还没有实现,由 app 层决定回退)。
///
/// `Route::Detail(_)` 仍属实例页(窄窗时是「整页切换」的那一屏)。
pub fn of(route: &Route) -> Option<Self> {
Some(match route {
Route::Instances | Route::Detail(_) => Screen::Instances,
Route::Download => Screen::Download,
Route::Logs => Screen::Logs,
Route::Settings => Screen::Settings,
Route::Agent => return None,
})
}
/// 该页高亮的导航项(与 `Route::nav` 同源,但这里是「按页问」的口径)。
pub const fn nav(self) -> NavSection {
match self {
Screen::Instances => NavSection::Instances,
Screen::Download => NavSection::Download,
Screen::Logs => NavSection::Logs,
Screen::Settings => NavSection::Settings,
}
}
}