P2-1(行为):`Busy::Importing` 的早退只盖了 Close。「返回」会派 Scan、「换目标档」会派 零写盘 Preview,两条都把 `adopt_epoch` +1,于是在飞的 Confirm 交卷时 Done 被代次丢掉—— 实例写进去了,界面什么都没说(正是 Close 守卫要防的形状)。两处补同款早退,且守卫写在 改本地状态**之前**:挡下来的换档不该把界面改成 B 档而盘上写的是 A 档。 P2-2(显示陈旧):`Form::fresh_plan` 原来只按目标档过期。先选 1.20.1 + 版本目录档,再改选 1.19.4,目标档没变,旧预检的 notes(说的是 `versions/1.20.1`)会继续当新版本的后果原文画。 现在连 `plan.version_id == selected` 一起判;没选中版本时也返回 None。同处那句 「版本一致性由 Self::candidate 那一路保证(选中即重扫)」与代码不符(Select 只改本地字段、 不发命令),改成如实说明过期判定只能在这里做。 P2-3(文档):`app/adopt/shell.rs` 的「纯搬移:函数体一行未改」不成立——同一提交里卡壳几何、 页脚去重、预览摘要都改过。改成逐条列出改了什么与为什么。 用例(两条都是先写红再修,红时 panic 信息就是错在哪): - back_and_target_are_ignored_while_the_registration_is_in_flight(记账后端断言「不许派 Scan / 不许派 Preview」);红:`登记在飞时 Back 不许派 Scan(会把 Done 的代次顶掉)`; - fresh_plan_expires_when_the_selected_version_changes;红:`换了版本之后旧预检必须过期 (否则画的是 1.20.1 的后果原文)`。 顺带补图:`ULL_ADOPT_STATE` 加 `pick-selected` 一档(选中行底色 + 预检启用态), 出图 `DevDocs/artifacts/m223/adopt-pick-selected-light.png`(r1 复审说这格只有代码没截图)。
199 lines
7.8 KiB
Rust
199 lines
7.8 KiB
Rust
//! 向导卡的外壳与页脚(M223 从 `app/adopt.rs` 搬出,守 400 行硬线)。
|
||
//!
|
||
//! | 函数 | 职责 |
|
||
//! |---|---|
|
||
//! | [`card`] | 卡壳(标题 + 说明 + 中间区 + 页脚)、定高/收高两种几何、遮罩上的那一张卡 |
|
||
//! | [`title_text`] / [`subtitle`] | 四段的标题与一句说明(文案全部来自 i18n 目录) |
|
||
//! | [`footer`] | 按段给按钮组合;在等回程时主按钮禁用 |
|
||
//!
|
||
//! ## 这个文件不是「纯搬移」
|
||
//!
|
||
//! 它**起于**一次搬移:`app/adopt.rs` 落笔 412 行越过 `components::gate` 的 400 行硬线,
|
||
//! 于是把卡壳 / 标题 / 页脚搬到这里。但**同一个提交里**这三块还改过行为与几何,逐条如下
|
||
//! (复审对 diff 时按这几条看,别按「一行未改」看):
|
||
//!
|
||
//! | 改了什么 | 为什么 |
|
||
//! |---|---|
|
||
//! | `card` 拆成「定高 + 中间区可滚」(Pick / Failed)与「按内容收高」(Preview / Done)两种几何 | 原来只给 `max_height`,内容一高就被裁:页脚按钮与实例名整排不可见(出图时抓到) |
|
||
//! | `footer` 的「关闭」只在 Pick / Preview 出现 | Failed / Done 的主按钮就是关闭,再给一枚文字关闭就是两枚一模一样的按钮 |
|
||
//! | `subtitle` 的 Preview 文案改成「版本名 · 目标档」 | 原来打「隔离档 · 目标档」,同屏两个「共用目录」 |
|
||
//!
|
||
//! 调用方仍是 `app/adopt.rs` 的 `layer` 与 `middle`。
|
||
|
||
use iced::widget::{column, container, row, scrollable, Space};
|
||
use iced::{Background, Element, Length};
|
||
|
||
use unlucky_design::color::Role;
|
||
use unlucky_design::elevation::MaterialLayer;
|
||
use unlucky_design::shape::ShapeScale;
|
||
use unlucky_design::spacing::Space as Sp;
|
||
use unlucky_design::typography::TypeRole;
|
||
use unlucky_i18n::{Catalog, Key};
|
||
use unlucky_presenter::dto::{AdoptStage, AdoptState};
|
||
|
||
use crate::app::components::buttons::{self, Button};
|
||
use crate::pages::frame;
|
||
use crate::theme::Ui;
|
||
|
||
use super::super::message::Message;
|
||
use super::text::{target_label, wrapped};
|
||
use super::{adopt, middle, AdoptMessage, Form, CARD_MAX_HEIGHT, CARD_WIDTH};
|
||
|
||
/// 卡片本体:标题 + 一句说明 + 中间(按段) + 页脚按钮。
|
||
pub(super) fn card<'a>(
|
||
ui: &'a Ui,
|
||
form: &'a Form,
|
||
adopt_state: &'a AdoptState,
|
||
catalog: Catalog,
|
||
) -> Element<'a, Message> {
|
||
let stage = form.stage(adopt_state);
|
||
let middle = middle(ui, form, adopt_state, stage, catalog);
|
||
let head = column![
|
||
frame::text_role(
|
||
ui,
|
||
TypeRole::HeadlineSmall,
|
||
true,
|
||
title_text(stage, catalog),
|
||
ui.color(Role::OnSurface),
|
||
)
|
||
.map(Message::Page),
|
||
wrapped(
|
||
ui,
|
||
subtitle(stage, adopt_state, catalog),
|
||
ui.color(Role::OnSurfaceVariant)
|
||
),
|
||
frame::divider(ui).map(Message::Page),
|
||
]
|
||
.spacing(ui.space(Sp::X3))
|
||
.width(Length::Fill);
|
||
|
||
let tail = column![
|
||
frame::divider(ui).map(Message::Page),
|
||
footer(ui, form, adopt_state, stage, catalog),
|
||
]
|
||
.spacing(ui.space(Sp::X3))
|
||
.width(Length::Fill);
|
||
|
||
// 列表段(Pick / Failed)**定高 + 中间区可滚**:内容再长,页脚按钮也不会被裁掉
|
||
// (第一版就是被裁掉的)。短内容段(Preview / Done)按内容高收,不留一大片空白。
|
||
let list_stage = matches!(stage, AdoptStage::Pick | AdoptStage::Failed);
|
||
let card = if list_stage {
|
||
let body = column![
|
||
head,
|
||
scrollable(middle)
|
||
.height(Length::Fill)
|
||
.style(frame::scroll_style(ui)),
|
||
tail,
|
||
]
|
||
.spacing(ui.space(Sp::X3))
|
||
.width(Length::Fill);
|
||
container(body)
|
||
.width(Length::Fixed(CARD_WIDTH))
|
||
.height(Length::Fixed(CARD_MAX_HEIGHT))
|
||
} else {
|
||
let body = column![head, middle, tail]
|
||
.spacing(ui.space(Sp::X3))
|
||
.width(Length::Fill);
|
||
container(body)
|
||
.width(Length::Fixed(CARD_WIDTH))
|
||
.max_height(CARD_MAX_HEIGHT)
|
||
};
|
||
|
||
card.padding(ui.space(Sp::X6))
|
||
.style(move |_theme| container::Style {
|
||
background: Some(Background::Color(ui.layer(MaterialLayer::Overlay))),
|
||
border: ui.outlined(ShapeScale::ExtraLarge),
|
||
..container::Style::default()
|
||
})
|
||
.into()
|
||
}
|
||
|
||
/// 段标题(四段各一句)。
|
||
pub(super) fn title_text(stage: AdoptStage, catalog: Catalog) -> &'static str {
|
||
match stage {
|
||
AdoptStage::Pick => catalog.text(Key::AdoptTitle),
|
||
AdoptStage::Preview => catalog.text(Key::AdoptPlanTitle),
|
||
AdoptStage::Failed => catalog.text(Key::AdoptFailureTitle),
|
||
AdoptStage::Done => catalog.text(Key::AdoptDoneTitle),
|
||
}
|
||
}
|
||
|
||
/// 标题下那句说明(Pick 段说清「什么都不复制」;Done 段说清「去实例列表启动」)。
|
||
pub(super) fn subtitle(stage: AdoptStage, adopt_state: &AdoptState, catalog: Catalog) -> String {
|
||
match stage {
|
||
AdoptStage::Pick => catalog.text(Key::AdoptSubtitle).to_string(),
|
||
// 预检:版本名 · 目标档(隔离档在正文里单独一行,重复一遍只会更吵)
|
||
AdoptStage::Preview => adopt_state
|
||
.plan
|
||
.as_ref()
|
||
.map(|plan| format!("{} · {}", plan.name, target_label(plan.target, catalog)))
|
||
.unwrap_or_default(),
|
||
AdoptStage::Failed => adopt_state
|
||
.failure
|
||
.as_ref()
|
||
.map(|failure| failure.message.clone())
|
||
.unwrap_or_default(),
|
||
AdoptStage::Done => catalog.text(Key::AdoptDoneHint).to_string(),
|
||
}
|
||
}
|
||
|
||
/// 页脚按钮(按段给不同组合;在等回程时主按钮禁用)。
|
||
pub(super) fn footer<'a>(
|
||
ui: &'a Ui,
|
||
form: &'a Form,
|
||
adopt_state: &'a AdoptState,
|
||
stage: AdoptStage,
|
||
catalog: Catalog,
|
||
) -> Element<'a, Message> {
|
||
let busy = form.busy.waiting();
|
||
|
||
let primary: Element<'a, Message> = match stage {
|
||
AdoptStage::Pick => {
|
||
let ready = form.candidate(adopt_state).is_some_and(|c| c.adoptable()) && !busy;
|
||
if ready {
|
||
buttons::filled(
|
||
ui,
|
||
Button::new(catalog.text(Key::AdoptPreview), adopt(AdoptMessage::Check)),
|
||
)
|
||
} else {
|
||
buttons::filled(ui, Button::disabled(catalog.text(Key::AdoptPreview)))
|
||
}
|
||
}
|
||
AdoptStage::Preview if !busy => buttons::filled(
|
||
ui,
|
||
Button::new(
|
||
catalog.text(Key::AdoptConfirm),
|
||
adopt(AdoptMessage::Confirm),
|
||
),
|
||
),
|
||
AdoptStage::Preview => {
|
||
buttons::filled(ui, Button::disabled(catalog.text(Key::AdoptConfirm)))
|
||
}
|
||
// 失败 / 完成:主按钮就是关掉(Done 不用「去列表看」——实例已经在列表里了)
|
||
AdoptStage::Failed | AdoptStage::Done => buttons::filled(
|
||
ui,
|
||
Button::new(catalog.text(Key::AdoptClose), adopt(AdoptMessage::Close)),
|
||
),
|
||
};
|
||
|
||
let mut actions = row![].spacing(ui.space(Sp::X2)).width(Length::Fill);
|
||
// Preview / Failed 给一条「退回候选列表」(= 重新扫一次,代价只有一次读目录)
|
||
if matches!(stage, AdoptStage::Preview | AdoptStage::Failed) {
|
||
actions = actions.push(buttons::text(
|
||
ui,
|
||
Button::new(catalog.text(Key::AdoptBack), adopt(AdoptMessage::Back)),
|
||
));
|
||
}
|
||
actions = actions.push(container(Space::new()).width(Length::Fill));
|
||
// 失败/完成段的主按钮就是「关闭」,再给一枚文字「关闭」就是两枚一模一样的按钮
|
||
if matches!(stage, AdoptStage::Pick | AdoptStage::Preview) {
|
||
actions = actions.push(buttons::text(
|
||
ui,
|
||
Button::new(catalog.text(Key::AdoptClose), adopt(AdoptMessage::Close)),
|
||
));
|
||
}
|
||
actions = actions.push(primary);
|
||
|
||
column![actions].width(Length::Fill).into()
|
||
}
|