feat(app-iced): M107 DLVRAIL —— 版本管理页三栏侧栏 + 跨类型搜索

用户令(信息架构):这一页从「下载」改称「版本管理」,版面从「一排 tab + 平铺列表」
改成**页面内三栏**:版本类型栏 + 主版本栏 + 列表/详情,并加搜索框。

- **三栏几何**(`pages/layout/rails.rs`,纯函数 + 数值表):类型栏 140/64、主版本栏 140/64、
  间距 24、列表列下限 424(行宽红线)。四档由**可用宽**裁决:Wide ≥888 / Narrow ≥676 /
  Compact ≥600 / Chips <600,三个门槛都是 `栏宽 + 间距 + 424` 算出来的,不是拍的
- **主版本分组规则**(`download/group.rs`):正式版/预发布按 `major.minor`(`1.21.9-pre3` → `1.21`)、
  周更按年桶(`24w14a` → `24w`)、远古版**没有主版本**(`rd-132211`/`a1.2.6` → 第二级只有「全部」);
  判据用真实版本样本钉住,不按「第一个点」切
- **跨类型搜索**(`search_bar` + 清空 + 命中数):查询词写进 `AppState.download_query`(页面无状态,
  只能这么活),`hits` 是**跨分类**读数;三条口径分开:「有命中」/「没有匹配」/「命中在别的类型里」
- 两级侧栏都用 `navigation::list_item` / `tabs` / `chip` / `buttons::icon`:Tab 可达、hover/pressed/
  选中态全部来自组件,页面不自己写状态层
- 右栏展开(M104 `content_width` 852)⇒ 可用宽 716 ⇒ Narrow 档,列表 464(任务书那句 492 漏扣
  96px 的页边距与栏间距,报告里给了完整数值表)
- 文案 11 条中英进 `crates/i18n`(新域宏接在装配链末尾 ⇒ `Key::ALL` 只追加,金表只追加)
- 护栏:两层侧栏都存在 + 列表按第二级选中态过滤(变异可证)、三档门槛、424 下限扫描、
  版本行单行宽账、搜索条真的装着
- `install.rs` 只动了「已装」分支(加一句「已安装」解释禁用按钮),没碰 M105 的选项区

取证:`DevDocs/artifacts/m107/`(10 张:三档 × 浅深 + 搜索命中 + 空 + 异地 + 远古版)
This commit is contained in:
AskaEth committed 2026-11-05 01:59:13 +08:00
1 parent 72b0c9d465
commit 89a51a63b1
24 files changed
+2000 -340

No files matched your search

+93 -58
View File
@@ -1,39 +1,32 @@
//! 下载页:**单列**版面的用例(页面块吃满页宽,散文才受 424 行宽约束)。
//! **版本管理页**(原下载页):三栏版面 —— 版本类型 / 主版本 / 列表 + 安装面板。
//!
//! 这一页永远单列,理由不是「简单」,而是官方对「一次只做一件事」的入口(下载队列)的口径:
//! 内容是一串线性进度项,分栏没有第二个阅读对象。
//!
//! **M102 订正**:旧版把整页块压到 424 并水平居中(1180 宽下左缘 422,页头标题在 112,
//! 用户令(信息架构调整)把这一页从「下载」改称「版本管理」:下载页只负责整合包与 Mod/光影,
//! 而这一页管的是**游戏版本与加载器安装**。本文件是那次改版后的页面装配点。
//! **M102 版面订正**:旧版把整页块压到 424 并水平居中(1180 宽下左缘 422、页头标题在 112,
//! 左右各空 ~300px,视觉复审判为「内容缩在中间一条窄栏」)。现在页面块走 [`frame::body`]
//! ——左缘与页头标题同一条线(= 页边距 24),宽度 = 可用宽度(1180 下 1044)。
//! 424 只约束**成段散文**(本页没有,散文在实例详情页),列表行 / Tabs / 分隔线吃满页宽。
//! ——左缘与页头标题同一条线(= 页边距 24),宽 = 块宽 `frame::body_width`(1180 下 1044,
//! 极宽窗封顶 **1440**)。424 只约束**成段散文**(本页没有),三栏的每一栏都吃块宽。
//!
//! ## M94:占位控件 → 真组件 + 真数据 + 真指令(逐项对照)
//! ## 版面:三栏,宽度由布局系统裁决
//!
//! | 位置 | M84(占位) | M94(真组件) |
//! |---|---|---|
//! | 页头动作 | `frame::text_action`(`text` + 手搓 hover) | `buttons::text`(五式 + 状态层) |
//! | 分类 | 无 | `navigation::tabs`(Primary,48 高 / 3px `Primary` 指示条) |
//! | 列表行 | 无 | `navigation::list_item`(两行档 **60**:`row_height` 44 + 每多一行 16) |
//! | 分隔线 | `frame::divider` | `components::divider::full` / `list` |
//! | 安装按钮 | 无 | `buttons::filled`(**本页唯一实心 accent**,rubric K2) |
//! | 进度 | 无 | `feedback::progress::linear`(已知 = determinate,未知 = indeterminate) |
//! | 空态 | `frame::empty_state` | `feedback::empty_state`(`EmptyState` 真组件) |
//! | 数据 | `"3 sources healthy · …"` 硬编码假状态行 | [`Download`](`app/view.rs` 从 presenter 快照转) |
//! | 文案 | 英文字面量 | `unlucky_i18n` 的 `Key`(拼错编译失败;中英两套目录) |
//! ```text
//! ┌─────────┬─────────┬──────────────────────────────┐
//! │ 版本类型 │ 主版本 │ 搜索条 · 命中数 · 计数头 │
//! │ 140/64 │ 140/64 │ 版本行(单行四项) │
//! │ │ │ … │
//! │ │ │ 安装面板(M105 的选项区在这里)│
//! └─────────┴─────────┴──────────────────────────────┘
//! ```
//!
//! ## 状态矩阵(四个状态都能从快照推出来,都出图)
//! 三个数字**没有一个是在本文件里拍的**:它们来自
//! [`VersionRails`](super::layout::VersionRails)(宽档裁决 + 数值表在 `pages/layout/rails.rs`)。
//! 页面只做一件事:把裁决结果交给 [`rails`] 画出来。
//!
//! | 状态 | 条件 | 组件 |
//! |---|---|---|
//! | 加载 | 快照里没有版本列表([`Download::is_loading`]) | 标题 + 说明,**居中、无进度形状**([`states::loading`]) |
//! | 空 | 列表拉到了、过滤后为空([`Download::is_empty`]) | `EmptyState` + 刷新 CTA,**同样居中**([`states::empty`]) |
//! | 有数据 | 有版本行 | Tabs + 计数头 + `ListItem` 列表([`rows`]) |
//! | 安装中 | `AppState::install` 在跑 | 阶段 + **进度条(仅当进度已知)** + 计数 + 「取消」([`install`]) |
//! | 失败 | `install.stage == Failed` | **`ErrorContainer` 面板**:`Error` 图标/标题 + 实心「打开日志」,**没有进度条**([`install`]) |
//! ## 原顶部 4 个 tab 的去处(不是新造分类)
//!
//! 两个占位状态(加载 / 空)走**同一套规则**:内容在可用区居中(依据:`feedback::empty_state`
//! 的既有口径 `12b §15.4`,且两态互切时内容不会纵向跳)。
//! 四个 chip(全部 / 正式版 / 预览版 / 远古版)**语义一字不改**,只是从顶部横排搬到第一栏竖排;
//! 窄到三栏不成立时(可用宽 < 600)它们**原样回到顶部**([`rails::stacked_chips`])——同一条
//! [`Category`] 谓词、同一条 `AppCommand::ListVersions`。
//!
//! ## 意图 → 消息 → 指令(页面只发意图,不做乐观回显)
//!
@@ -57,17 +50,51 @@
//! 进度条只读 `model.install`:它由 presenter 的 `InstallProgress` 事件经 `apply` 灌进来。
//! 于是「点了没变」只有一种解释——后端还没回事件,这正是要的(P3-2 的纪律)。
//!
//! ## 版面判据(M102 修订了「页头 vs 正文」那一条)
//! ## M107:两级侧栏从哪来(第一级 = 版本类型,第二级 = 主版本桶)
//!
//! 正文块左缘 = 页头标题左缘 = `rail 88 + gutter 24 = 112`([`frame::body`]);
//! 断点 600 / 840、rail 88、散文行宽 ≤424([`super::layout::measure_cap_px`])都由
//! [`super::layout`] 与 `shell` 无条件施加。视觉复审偏离 #1(页头铺满、不与正文列对齐)
//! 因此**收口**:页头与正文块现在共用同一条左缘,右缘也都停在页边距上。
//! 第一级 = 版本类型(4 项,固定);第二级 = 主版本桶(由快照里的版本 id 按 [`group`] 的规则归纳,
//! 远古版这类「没有主版本概念」的只有「全部版本」一项)。
//!
//! ## 搜索(三层口径)
//!
//! | 情形 | 画面 |
//! |---|---|
//! | 有命中 | 列表照常;侧栏**位置不动**;搜索条右侧给「N 个匹配」(跨类型读数) |
//! | 一个都不命中 | [`states::no_match`]:没有匹配 + 换短片段 + 清空 |
//! | 有命中但不在当前类型 | [`states::elsewhere`]:N 个在别的类型 + 「看全部类型」 |
//!
//! 查询词写在 presenter 快照的 `download_query` 里(页面无状态,见 [`model::Download::from_snapshot`]),
//! 所以「搜索 → 任何事件回来之后仍然在搜」这条不需要页面持有任何字段。
//!
//! ## 状态矩阵(加载 / 空 / 有数据 / 安装中 / 失败)
//!
//! 与 M94 时一致,唯一的改动是**空态与搜索态都留在三栏骨架里**:侧栏与搜索框照旧在,
//! 只有列表区换成占位内容——白屏会让人以为列表坏了。
//!
//! ## 两个纯视图态的字段(快照里没有,重建会丢)
//!
//! `selected_major`(第二级侧栏选中)与 `type_rail_folded`(类型栏手动折叠)**不在 presenter 快照里**
//! (`AppState` 没有这两个字段,加字段要动 `pages/mod.rs` 的偏离表与 presenter,见 M107 报告)。
//! 所以它们与既有的 `selected` 同一档处理:`on_download` 直接改视图模型,**不重建**;
//! 唯一会重建的路径是搜索(`rebuild_after_query`),那里把两个字段在重建前后各搬一次。
//! 其余事件(安装进度、版本列表刷新)回来时这两个字段会回到默认值——与 `selected` 的既有缺口同源,
//! 长期要靠 presenter 加字段,已登记在报告里。
//!
//! ## 版面判据(M102 口径:页面块 vs 散文列)
//!
//! 页面仍是 `PaneKit::Single`(页内自己切三栏)。整页内容走 [`frame::body`]:块左缘 =
//! 页头标题左缘 = `rail 88 + gutter 24 = 112`,块宽 = [`frame::body_width`](pane 宽封顶
//! **1440**)——视觉复审 ①(内容缩在中间一条窄栏、与页头错位)因此收口。
//! **三栏的列宽从「块宽」算**([`VersionRails::of_metrics`] 读 `frame::body_width`,不是
//! `metrics.available`):否则 1576+ 的超宽窗上三列之和会超出 1440 的块、右侧被裁。
//! 424([`super::layout::measure_cap_px`])在本页只对**成段散文**有效——这一页没有散文。
mod demo;
mod group;
mod install;
mod model;
mod options;
mod rails;
mod rows;
mod states;
@@ -90,7 +117,7 @@ use crate::app::components::divider;
use crate::theme::Ui;
use super::frame::{self, Header};
use super::layout::FrameMetrics;
use super::layout::{FrameMetrics, VersionRails};
use super::{Action, Message};
/// 整页(**唯一**的真数据入口:`model` 由 `app/view.rs` 从 presenter 快照建)。
@@ -118,24 +145,18 @@ pub fn view<'a>(
frame::page(ui, metrics, header, body(ui, metrics, catalog, model))
}
/// 正文:加载 / 空 / 列表 + 底部安装面板(列宽由 [`super::layout`] 决议,页面只吃结果)。
/// 正文:加载态仍是居中占位;其余状态都长在三栏骨架上。
fn body<'a>(
ui: &'a Ui,
metrics: FrameMetrics,
catalog: Catalog,
model: &'a Download,
) -> Element<'a, Message> {
if model.is_loading() || model.is_empty() {
let placeholder = if model.is_loading() {
states::loading(ui, catalog)
} else {
states::empty(ui, catalog)
};
// 占位状态下**也**把安装面板带上(复审 p3):列表空/还没到,不代表安装停了——
// 没有这一段的话,安装进行中一旦列表被过滤空,进度条会凭空消失。
if model.is_loading() {
// 还没拉到版本列表:三栏没有内容可填,给占位(安装面板照旧带着,见 M94 复审 p3)
let content: Element<'a, Message> = if model.install.is_some() {
column![
placeholder,
states::loading(ui, catalog),
divider::full(ui),
install::panel(ui, catalog, model)
]
@@ -144,23 +165,37 @@ fn body<'a>(
.height(Length::Fill)
.into()
} else {
placeholder
states::loading(ui, catalog)
};
return frame::body(metrics, content);
}
let content: Element<'a, Message> = column![
rows::categories(ui, catalog, model.category),
rows::head(ui, catalog, model),
divider::full(ui),
rows::list(ui, catalog, model),
divider::full(ui),
install::panel(ui, catalog, model),
]
.spacing(ui.space(Space::X3))
.width(Length::Fill)
.height(Length::Fill)
.into();
// 宽度裁决:档位来自布局系统,「用户手动折叠了类型栏」这一条覆盖叠在上面。
let rails = {
let rails = VersionRails::of_metrics(metrics);
if model.type_rail_folded {
rails.with_type_folded()
} else {
rails
}
};
let content: Element<'a, Message> = if rails.tier.is_three_column() {
rails::three_column(ui, catalog, model, rails)
} else {
// 三栏不成立(可用宽 < 600):类型与主版本原样退回顶部 chips,列表吃掉整条可用宽
column![
rails::stacked_chips(ui, catalog, model),
divider::full(ui),
rows::content(ui, catalog, model),
]
.spacing(ui.space(Space::X2))
.width(Length::Fill)
.height(Length::Fill)
.into()
};
// M102:整页块走 `frame::body`(贴左、块宽封顶 1440),与页头标题同一条左缘。
// 三栏都在这个块里切,所以 `VersionRails` 读的是块宽而不是 `metrics.available`。
frame::body(metrics, content)
}