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:
1 parent
72b0c9d465
commit
89a51a63b1
24 files changed
+2000
-340
No files matched your search
@@ -0,0 +1,148 @@
|
||||
//! 主版本分组:把一串版本 id 归成「大版本」桶(第二级侧栏的数据源)。
|
||||
//!
|
||||
//! ## 为什么不能按第一个 `.` 切
|
||||
//!
|
||||
//! 真实的版本清单里同时存在**四种形态**(拿真样本说,不用「第几个点」这种相对说法):
|
||||
//!
|
||||
//! | 样本 | 是什么 | `split('.')` 会得到什么 | 正确的主版本 |
|
||||
//! |---|---|---|---|
|
||||
//! | `1.21.4` | 正式版 | `["1", "21", "4"]` | `1.21` |
|
||||
//! | `1.21.9-pre3` | 预发布(正式版的兄弟) | `["1", "21", "9-pre3"]` | `1.21` |
|
||||
//! | `24w14a` | 周更快照 | `["24w14a"]`(一个都没有) | `24w`(周更的年桶) |
|
||||
//! | `rd-132211` / `a1.2.6` | 远古版(Classic / Alpha) | `["rd-132211"]` / `["a1", "2", "6"]` | **没有主版本** |
|
||||
//!
|
||||
//! 「按第一个 `.` 切」在 `1.21.4` 上给出 `1`,把 1.7 到 1.21 的全部正式版塞进一个桶;
|
||||
//! 在 `a1.2.6` 上给出 `a1`,凭空造出一个「主版本」——远古版根本没有 major.minor 这层概念。
|
||||
//!
|
||||
//! ## 规则(三条,按顺序命中即返回)
|
||||
//!
|
||||
//! 1. **带点的数值前缀**(正式版 / 预发布):id 在第一个 `-` 之前的部分,前两段都是纯数字
|
||||
//! ⇒ 主版本 = `第一段.第二段`。`1.21.9-pre3` 先切掉 `-pre3` 再取 `1.21`。
|
||||
//! 2. **周更快照**:id 形如 `24w14a`(两位年 + `w` + 两位周 + 一个修订字母)⇒ 主版本 = `24w`。
|
||||
//! 周更版本每年几十个,年桶是它的「大版本」等价物;不按周切是因为一周通常只有 1 个版本,
|
||||
//! 按周分组等于每个桶一个版本(侧栏退化成重复的列表)。
|
||||
//! 3. **其余**:返回 [`None`](没有主版本概念)。远古版(`rd-132211` / `a1.2.6` / `c0.30_01c`)
|
||||
//! 走这一条 ⇒ 第二级侧栏只有一个「全部」项。
|
||||
//!
|
||||
//! 判据是**形态**(有没有 `-` 后缀、前两段是不是纯数字、有没有 `NNwNNx` 的形状),
|
||||
//! 不是「版本号大小」,所以新增一个 `1.22.0` 或 `25w01a` 不需要改规则。
|
||||
|
||||
use unlucky_presenter::dto::VersionKind;
|
||||
|
||||
/// 主版本桶的一个标签(`1.21` / `24w`);没有主版本概念时是 [`None`]。
|
||||
pub type MajorKey = String;
|
||||
|
||||
/// 这个版本属于哪个主版本桶(规则见模块文档)。
|
||||
///
|
||||
/// 远古版**直接**返回 [`None`]:它们的 id 形态(`a1.2.6`)与正式版同族,靠形态规则会误判,
|
||||
/// 所以先按 `kind` 分流——「有没有主版本」是**版本类型**的属性,不是 id 字符串的属性。
|
||||
#[must_use]
|
||||
pub fn key(id: &str, kind: VersionKind) -> Option<MajorKey> {
|
||||
if kind.is_ancient() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 规则 1:切掉 `-pre3` / `-rc1` 这类后缀,再取前两段数值
|
||||
let head = id.split('-').next().unwrap_or(id);
|
||||
let mut parts = head.split('.');
|
||||
if let (Some(major), Some(minor)) = (parts.next(), parts.next()) {
|
||||
if is_number(major) && is_number(minor) {
|
||||
return Some(format!("{major}.{minor}"));
|
||||
}
|
||||
}
|
||||
|
||||
// 规则 2:周更快照 `24w14a` → `24w`
|
||||
if let Some((year, tail)) = head.split_once('w') {
|
||||
let week_like = tail.chars().next().is_some_and(|ch| ch.is_ascii_digit());
|
||||
if year.len() == 2 && is_number(year) && week_like {
|
||||
return Some(format!("{year}w"));
|
||||
}
|
||||
}
|
||||
|
||||
// 规则 3:没有主版本概念
|
||||
None
|
||||
}
|
||||
|
||||
/// 纯数字(空串不算)。
|
||||
fn is_number(text: &str) -> bool {
|
||||
!text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit())
|
||||
}
|
||||
|
||||
/// 两条主版本标签的**版本序**(降序用):`1.21 > 1.9`、`24w > 14w`,数字按数值比、字母按字节比。
|
||||
///
|
||||
/// 为什么不是字符串序:`"1.9" > "1.21"` 在字典序里成立(`9 > 2`),而版本序里 1.21 更新。
|
||||
/// 逐段比:连续数字读成整数、其余字符逐字节比。
|
||||
#[must_use]
|
||||
pub fn compare(left: &str, right: &str) -> std::cmp::Ordering {
|
||||
let mut left_rest = left;
|
||||
let mut right_rest = right;
|
||||
|
||||
loop {
|
||||
match (left_rest.chars().next(), right_rest.chars().next()) {
|
||||
(None, None) => return std::cmp::Ordering::Equal,
|
||||
(None, Some(_)) => return std::cmp::Ordering::Less,
|
||||
(Some(_), None) => return std::cmp::Ordering::Greater,
|
||||
(Some(left_char), Some(right_char)) => {
|
||||
if left_char.is_ascii_digit() && right_char.is_ascii_digit() {
|
||||
let (left_number, left_tail) = take_number(left_rest);
|
||||
let (right_number, right_tail) = take_number(right_rest);
|
||||
match left_number.cmp(&right_number) {
|
||||
std::cmp::Ordering::Equal => {
|
||||
left_rest = left_tail;
|
||||
right_rest = right_tail;
|
||||
}
|
||||
other => return other,
|
||||
}
|
||||
} else if left_char == right_char {
|
||||
left_rest = &left_rest[left_char.len_utf8()..];
|
||||
right_rest = &right_rest[right_char.len_utf8()..];
|
||||
} else {
|
||||
return left_char.cmp(&right_char);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 取出开头的连续数字(返回数值与剩余部分)。
|
||||
fn take_number(text: &str) -> (u64, &str) {
|
||||
let end = text
|
||||
.find(|ch: char| !ch.is_ascii_digit())
|
||||
.unwrap_or(text.len());
|
||||
(text[..end].parse().unwrap_or(0), &text[end..])
|
||||
}
|
||||
|
||||
/// 一个主版本桶(第二级侧栏的一行):桶标签 + 桶里有多少个版本。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Bucket {
|
||||
/// 桶标签(`1.21` / `24w`);[`None`] = 这一组版本没有主版本概念(远古版)。
|
||||
pub key: Option<MajorKey>,
|
||||
/// 桶里的版本数(列表列与命中数都读它)。
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
/// 把版本行归成桶:**按版本序降序**(`1.21` 在 `1.20` 前面),无主版本概念的桶排最后。
|
||||
///
|
||||
/// 只做聚合与排序,不决定「当前选中哪个桶」——那是页面状态,住在 `pages/Download` 里。
|
||||
#[must_use]
|
||||
pub fn buckets(rows: &[super::model::VersionRow]) -> Vec<Bucket> {
|
||||
let mut grouped: Vec<Bucket> = Vec::new();
|
||||
for row in rows {
|
||||
let key = row.major();
|
||||
match grouped.iter_mut().find(|bucket| bucket.key == key) {
|
||||
Some(bucket) => bucket.count += 1,
|
||||
None => grouped.push(Bucket { key, count: 1 }),
|
||||
}
|
||||
}
|
||||
|
||||
grouped.sort_by(|left, right| match (&left.key, &right.key) {
|
||||
(Some(a), Some(b)) => compare(b, a),
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||
(None, None) => std::cmp::Ordering::Equal,
|
||||
});
|
||||
grouped
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
Reference in new issue
Block a user