feat(packui): M113 第一批 —— 内容库页的导入/导出区(界面 + DTO + i18n + 拖拽落点)
导入入口(拖拽 + 路径输入 + 目标实例)与导出(格式选择 + 预检)的**界面层**,
数据面镜像 M111/M112 的管线报告(presenter DTO),进度整块复用下载页的安装面板。
## 落了什么
- pages/content/packui*:入口 / 进度块 / 结果块 / 导出预检块(每文件 ≤400)
- 拖拽区(hover 变主色描边)+ 路径输入框 + 目标实例 chips
- 进度:一行「步骤 n/5 · 解包覆盖内容」+ **下载页那块安装面板**(不另画进度条)
- 结果:三态(Success/Partial/Failed)+ 缺文件逐条(Warn/Tertiary)+ 冲突逐条
(成因 + 本次处置 + 可展开三方 SHA-1)+ 致命块(Error/ErrorContainer)
- 导出:mrpack / CF 二选一 + 包名/版本/路径 + 预检「将内联 N 个文件 · 无法导出 M 项」
+ 逐条无法导出项 + 已知损失 + 确认写盘
- presenter/src/dto/pack{,/format,/import,/conflict,/export}.rs:镜像 core 报告的 DTO
(三态、五阶段、三分类、十一条原因,全部 from_slug 与 core 稳定码逐字对齐)
- i18n:81 条 Pack* 键(content_entries! 尾部追加;golden.rs 同序同文)
- app/update.rs:FileDropped / FileHovered / FilesHoveredLeft → 内容库页(本版唯一的选文件通道)
- M109 的四个文件各一处插入:mod 声明 + Pack(..) 变体 + pack 字段 + 正列插入点
## 边界(如实)
- 没有系统文件选择框:依赖树里无对话框依赖,加它要动根 Cargo.toml + 许可门禁 +
Linux 后端选型(影响单文件产物)。入口是拖拽 + 路径输入,原因写在界面上。
- 冲突没有逐文件决策:core 的 ConflictPolicy 是整次导入一个全局档,ConflictFile 是事后账。
本批给「逐文件看 + 整体翻档(以包为准重新导入)」,文案明写「两者都留(改名)暂不支持」。
默认一律 KeepUser——绝不默认覆盖用户改过的文件。
- 本批按钮半通:拖拽/路径/格式/实例/展开都真生效;「点导入真的开跑」「预检/确认写盘」
要等第二批的 AppCommand 与事件通道(M109 rebase 到新 main 后按新形状写一次)。
This commit is contained in:
1 parent
0367073d32
commit
fbeff94e55
31 files changed
+3646
-9
No files matched your search
@@ -0,0 +1,104 @@
|
||||
//! 导入/导出区的守门(M113):四种出图态都装得满、入口文案都在。
|
||||
//!
|
||||
//! 出图(D30)靠这几条夹具:任何一态装不满,截图就会是一块空容器,而不是「有话说」的界面。
|
||||
//! 视图模型的判定在 `model/tests.rs`,这里只管**夹具本身**。
|
||||
|
||||
use unlucky_i18n::Lang;
|
||||
|
||||
use super::demo::pack_in;
|
||||
use super::model::Pack;
|
||||
|
||||
/// D30 要求的四态(导入中 / 部分成功 / 冲突选择 / 导出预检)。
|
||||
const STATES: [&str; 4] = ["importing", "partial", "conflict", "export"];
|
||||
|
||||
#[test]
|
||||
fn every_screenshot_state_fills_something() {
|
||||
for state in STATES {
|
||||
let (model, _page) = pack_in(state, Lang::En);
|
||||
assert!(
|
||||
!model.is_idle(),
|
||||
"{state} 这一态什么都没画(截图会是一块空容器)"
|
||||
);
|
||||
assert_eq!(model.targets.len(), 2, "{state} 的实例候选不足");
|
||||
assert!(!model.drop_hint.is_empty());
|
||||
assert!(
|
||||
!model.no_dialog_note.is_empty(),
|
||||
"没有系统文件框这件事必须写在界面上"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn importing_shows_the_shared_install_panel() {
|
||||
let (model, _page) = pack_in("importing", Lang::En);
|
||||
let running = model.running.expect("导入中要有进度");
|
||||
// 进度形状来自下载页那块面板(`Download.install`),不是本区另造的
|
||||
assert!(
|
||||
running.panel.install.is_some(),
|
||||
"进度必须走下载页的安装面板视图模型"
|
||||
);
|
||||
assert!(
|
||||
running.stage_line.contains("2/5"),
|
||||
"解包阶段是第 2/5 步:{}",
|
||||
running.stage_line
|
||||
);
|
||||
assert!(model.report.is_none(), "导入中不该同时有结果");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_keeps_the_tolerable_rows_one_by_one() {
|
||||
let (model, _page) = pack_in("partial", Lang::En);
|
||||
let report = model.report.expect("部分成功要有报告");
|
||||
assert!(report.installed(), "Partial 仍是「装上了」");
|
||||
assert!(report.fatal.is_none(), "部分成功不是致命失败");
|
||||
assert_eq!(report.missing.len(), 3, "缺文件要逐条列出");
|
||||
assert!(report.missing_title.is_some());
|
||||
assert!(!report.rejected.is_empty(), "被拒 component 也要逐条");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conflict_state_defaults_to_keeping_the_user_copy_and_expands_one_row() {
|
||||
let (model, page) = pack_in("conflict", Lang::En);
|
||||
let report = model.report.expect("冲突态要有报告");
|
||||
assert_eq!(report.conflicts.len(), 3);
|
||||
assert!(
|
||||
report.conflicts.iter().all(|row| !row.expanded)
|
||||
|| report.conflicts.iter().filter(|row| row.expanded).count() == 1,
|
||||
"只展开用户点过的那一条"
|
||||
);
|
||||
assert_eq!(
|
||||
report.conflicts.iter().filter(|row| row.expanded).count(),
|
||||
1,
|
||||
"夹具要展开一条,截图才看得见三方哈希"
|
||||
);
|
||||
assert!(page.expanded.is_some());
|
||||
assert!(report.can_overwrite);
|
||||
assert!(report.overwrite_note.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_state_has_a_preview_but_has_not_written_anything() {
|
||||
let (model, _page) = pack_in("export", Lang::En);
|
||||
assert!(model.export.open);
|
||||
let preview = model.export.preview.expect("预检要有结果");
|
||||
assert!(
|
||||
preview.head.contains('2'),
|
||||
"内联数要进主结论:{}",
|
||||
preview.head
|
||||
);
|
||||
assert!(!preview.skipped.is_empty(), "无法导出的项要逐条");
|
||||
assert!(!preview.complete, "有跳过项 ⇒ 包不完整");
|
||||
assert!(!preview.notes.is_empty(), "已知损失要列出来");
|
||||
assert!(
|
||||
model.export.done.is_none(),
|
||||
"预检阶段不许出现「已写出」(那是写盘之后的事)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_state_offers_the_entry_with_nothing_else() {
|
||||
let (model, page): (Pack, _) = pack_in("", Lang::En);
|
||||
assert!(model.is_idle());
|
||||
assert!(!model.export.open);
|
||||
assert!(page.draft.is_empty());
|
||||
}
|
||||
Reference in new issue
Block a user