feat(app-iced): 实例页「导入已有版本」向导(M223)

页头「导入」按钮(以前是占位动作,点了只打日志)现在打开四段向导:

| 段 | 画什么 |
|---|---|
| Pick | 候选版本(可导入的才可点)+ 目标三档 chip + 实例名(缺省 InstanceDir) |
| Preview | 生效运行目录(回程给的)+ 隔离档 + target_notes() 原文逐条 |
| Failed | 稳定码 + problems 逐条(不折叠成一句「导入失败」) |
| Done | 实例名 + 运行目录 + 提示原文 |

- 向导是叠在页面之上的一层(遮罩 + 居中卡),不搅乱 list-detail 的版式判据;
- 表单态在 App(app/adopt.rs 的 Form),事件真相在 presenter 的 AdoptState;
- 在等回程时主按钮禁用(连点「确认」不会写两条实例);
- i18n 中英目录 + 金表补 27 条向导文案(实例域末尾追加)。
This commit is contained in:
AskaEth committed 2026-11-10 19:37:42 +08:00
1 parent 5a59cb33b2
commit 41caae3571
16 files changed
+1265 -2

No files matched your search

+131
View File
@@ -0,0 +1,131 @@
//! Preview / Failed / Done 三段。
//!
//! | 段 | 画什么 | 判据 |
//! |---|---|---|
//! | Preview | 生效运行目录(**回程给的**)+ 隔离档 + `notes` 原文逐条 | `AdoptEvent::Planned` |
//! | Failed | 稳定码 + `problems[]` 逐条 | `AdoptEvent::Failed` |
//! | Done | 实例名 + 运行目录 + `notes` 原文逐条 | `AdoptEvent::Done` |
//!
//! 三段都不自己算路径、不自己写后果句:`run_dir` 与 `notes` 是 core 的返回值原样。
//! 缺省档会带 `-2`/`-3` 后缀(两个不同展示名可能落到同一个物理目录),**只有 core 知道**
//! 最后用了哪个——页面对它再做一次派生就会与真正启动时用的目录分叉。
use iced::widget::{column, scrollable};
use iced::{Element, Length};
use unlucky_design::color::Role;
use unlucky_design::spacing::Space as Sp;
use unlucky_i18n::{Catalog, Key};
use unlucky_presenter::dto::{AdoptDoneView, AdoptFailureView, AdoptPlanView};
use crate::pages::frame;
use crate::theme::Ui;
use super::super::message::Message;
use super::text::{field_line, isolation_label, mono, problem_line, wrapped};
use super::CARD_LIST_HEIGHT;
/// 预检结果:落点 + 隔离档 + 后果原文。
pub(super) fn preview<'a>(
ui: &'a Ui,
plan: Option<&'a AdoptPlanView>,
catalog: Catalog,
) -> Element<'a, Message> {
let Some(plan) = plan else {
return wrapped(
ui,
catalog.text(Key::AdoptScanning).to_string(),
ui.color(Role::OnSurfaceVariant),
);
};
let mut body = column![
field_line(
ui,
catalog.text(Key::AdoptPlanRunDir),
&plan.run_dir.display().to_string()
),
field_line(
ui,
catalog.text(Key::AdoptPlanIsolation),
&isolation_label(plan.isolation, catalog),
),
]
.spacing(ui.space(Sp::X2))
.width(Length::Fill);
// 后果话术:core `target_notes()` 的原文,一条一行、一个字不改
for note in &plan.notes {
body = body.push(wrapped(ui, note.clone(), ui.color(Role::OnSurfaceVariant)));
}
body.into()
}
/// 失败:稳定码 + **problems 逐条**。
pub(super) fn failed<'a>(
ui: &'a Ui,
failure: Option<&'a AdoptFailureView>,
catalog: Catalog,
) -> Element<'a, Message> {
let Some(failure) = failure else {
return wrapped(
ui,
catalog.text(Key::AdoptScanning).to_string(),
ui.color(Role::OnSurfaceVariant),
);
};
let mut body = column![mono(ui, failure.code.clone())]
.spacing(ui.space(Sp::X2))
.width(Length::Fill);
if failure.problems.is_empty() {
body = body.push(wrapped(
ui,
failure.message.clone(),
ui.color(Role::OnSurfaceVariant),
));
return body.into();
}
body = body.push(wrapped(
ui,
catalog.text(Key::AdoptProblemCount).to_string(),
ui.color(Role::OnSurfaceVariant),
));
for problem in &failure.problems {
body = body.push(problem_line(ui, problem));
}
scrollable(body)
.height(Length::Fixed(CARD_LIST_HEIGHT))
.style(frame::scroll_style(ui))
.into()
}
/// 完成:实例名 + 运行目录 + 提示原文。
pub(super) fn done<'a>(
ui: &'a Ui,
done: Option<&'a AdoptDoneView>,
catalog: Catalog,
) -> Element<'a, Message> {
let Some(done) = done else {
return wrapped(ui, String::new(), ui.color(Role::OnSurfaceVariant));
};
let mut body = column![
field_line(ui, catalog.text(Key::AdoptTitle), &done.name),
field_line(
ui,
catalog.text(Key::AdoptPlanRunDir),
&done.run_dir.display().to_string()
),
field_line(
ui,
catalog.text(Key::AdoptPlanIsolation),
&isolation_label(done.isolation, catalog),
),
]
.spacing(ui.space(Sp::X2))
.width(Length::Fill);
for note in &done.notes {
body = body.push(wrapped(ui, note.clone(), ui.color(Role::OnSurfaceVariant)));
}
body.into()
}