//! 导入/导出区的画法:入口(拖拽 + 路径)→ 进度(**下载页那块面板**)→ 结果 → 导出预检。 //! //! ## 版面 //! //! ```text //! ┌ 导入整合包 ──────────────────────────────────┐ //! │ ┌ 拖拽区(hover 时变主色描边) ─────────────┐ │ //! │ └ 路径输入框 ──────────┘ [导入] │ │ //! │ [装到实例:chips] [实例名] │ │ //! ├ 进度:步骤 2/5 · 解包覆盖内容 │ //! │ <下载页的安装面板:进度条 + 计数 + 取消> │ //! ├ 探测失败:看起来像 X,但缺 Y │ //! ├ 结果:三态标题 + 缺文件逐条 + 冲突逐条 + 致命块 │ //! └ 导出:格式 chips + 预检(将内联 N / 无法导出 M)│ //! ``` //! //! ## 三处「视觉可区分」不是靠颜色一种通道 //! //! | 域 | 图标 | 颜色角色 | 容器 | 措辞 | //! |---|---|---|---|---| //! | 容忍(缺文件 / 被拒 component) | `Warn` | `Tertiary` | 无底色 | 「可容忍」 | //! | 致命(已回滚) | `Error` | `Error` | `ErrorContainer` | 「已回滚 —— 零残留」 | //! | 成功 | `Check` | `Primary` | 无底色 | 「已导入」 | //! //! 关掉颜色也还认得出(图标 + 措辞),这是任务书「必须视觉可区分」的落点。 //! //! ## 进度只有一份 //! //! 进行中的进度**整块交给** `pages::download::install_panel`(同一个函数、同一套几何与文案); //! 本区只额外画一行 [`Running::stage_line`] 补足「解包 / overrides / 补文件」三档的粒度。 //! //! ## 文件分工(守 400 行) //! //! | 文件 | 职责 | //! |---|---| //! | 本文件 | 区入口 + 拖拽区 + 路径输入 + 目标实例 | //! | [`parts`] | 三个共用小件(底色容器 / 容忍标题 / 逐条列表) | //! | [`progress`] | 进度块(复用面板)与探测失败块 | //! | [`report`] | 结果块:三态 + 冲突 + 致命 | //! | [`export`] | 导出块与预检块 | mod export; mod parts; mod pick; mod progress; mod report; use iced::widget::{column, container, row}; use iced::{Alignment, Background, Border, Element, Length}; use unlucky_design::color::Role; use unlucky_design::icons::IconKind; use unlucky_design::shape::ShapeScale; use unlucky_design::spacing::Space; use unlucky_design::typography::TypeRole; use unlucky_i18n::{Catalog, Key}; use crate::app::components::buttons::{self, Button}; use crate::app::components::divider; use crate::app::components::inputs::{text_field, Field}; use crate::app::components::selection::{chip, Chip}; use crate::pages::frame; use crate::pages::Message as PageMessage; use crate::theme::Ui; use self::export::export_block; use self::pick::pick_block; use self::progress::{probe_block, running_block}; use self::report::report_block; use super::model::Pack; use super::PackMessage; /// 内容库意图 → 页面消息(唯一的一层包装)。 pub(super) fn intent(message: PackMessage) -> PageMessage { PageMessage::Content(super::super::Message::Pack(message)) } /// 整块(导入入口 + 进度 + 结果 + 导出)。 pub(crate) fn section<'a>( ui: &'a Ui, catalog: Catalog, model: &'a Pack, ) -> Element<'a, PageMessage> { let mut block = column![].spacing(ui.space(Space::X3)).width(Length::Fill); // **一次只摊开一件事**:入口(拖什么/装到哪)、进度、结果、导出预检各占一态。 // 全都堆在一起时,内容库页正列没有整页滚动 ⇒ 动作按钮被顶出屏幕(出图实测:冲突态与 // 导出预检态的「确认写盘 / 关闭」都落在 900px 以下)。收起的入口不会丢功能:拖拽是窗口级 // 事件,任何时候把文件拖进来都算数。 if model.export.open { block = block.push(export_block(ui, catalog, &model.export)); } else if let Some(pick) = &model.pick { // 冲突清单优先于一切:用户此刻要做的决定就是"哪几个文件覆盖" block = block.push(pick_block(ui, catalog, pick)); } else if let Some(running) = &model.running { block = block.push(running_block(ui, catalog, running)); } else if let Some(report) = &model.report { block = block.push(report_block(ui, catalog, report)); } else if model.cancelled { // 取消过、还没有新结果:把「后台还在跑」这句话留在屏上(复审 r1 P1-2) block = block.push( column![ frame::text_role( ui, TypeRole::BodySmall, false, model.cancelled_note, ui.color(Role::Tertiary), ), buttons::text( ui, Button::new(catalog.text(Key::PackDismiss), intent(PackMessage::Dismiss)), ), ] .spacing(ui.space(Space::X1)) .width(Length::Fill), ); } else { block = block.push(entry(ui, catalog, model)); if let Some(probe) = &model.probe { // 探测失败要和路径输入框在一起:用户下一步就是改路径 block = block.push(probe_block(ui, catalog, probe)); } block = block.push(divider::full(ui)); block = block.push(export_block(ui, catalog, &model.export)); } container(block).width(Length::Fill).into() } /// 入口:拖拽区 + 路径输入 + 目标实例。 fn entry<'a>(ui: &'a Ui, catalog: Catalog, model: &'a Pack) -> Element<'a, PageMessage> { let zone = container( column![ frame::text_role( ui, TypeRole::BodyMedium, false, if model.hover { catalog.text(Key::PackDropActive) } else { model.drop_hint }, ui.color(if model.hover { Role::Primary } else { Role::OnSurface }), ), // 没有系统文件框这件事**写在界面上**:不能让用户点了半天「选择文件」找不到按钮 frame::text_role( ui, TypeRole::BodySmall, false, model.no_dialog_note, ui.color(Role::OnSurfaceVariant), ), ] .spacing(ui.space(Space::X1)) .width(Length::Fill), ) .width(Length::Fill) .padding(ui.space(Space::X4)) .style(move |_theme| container::Style { border: Border { color: ui.color(if model.hover { Role::Primary } else { Role::Outline }), width: 1.0, radius: ui.radius_dp(ShapeScale::Large), }, background: model .hover .then(|| Background::Color(ui.color(Role::PrimaryContainer))), ..container::Style::default() }); let field = text_field( ui, Field::new(model.draft.as_str()) .label(catalog.text(Key::PackPathLabel)) .placeholder(catalog.text(Key::PackPathPlaceholder)) .leading(IconKind::Folder), move |text| intent(PackMessage::PathChanged(text)), ); let submit = buttons::filled( ui, Button::new( catalog.text(Key::PackImportAction), intent(PackMessage::Import), ) .leading(IconKind::Folder), ); let mut block = column![ frame::text_role( ui, TypeRole::TitleSmall, true, catalog.text(Key::PackImportTitle), ui.color(Role::OnSurface), ), zone, row![field, submit] .spacing(ui.space(Space::X2)) .align_y(Alignment::End) .width(Length::Fill), targets(ui, catalog, model), ] .spacing(ui.space(Space::X2)) .width(Length::Fill); if model.instance.is_none() { block = block.push(text_field( ui, Field::new(model.instance_name.as_str()).label(catalog.text(Key::PackInstanceName)), move |text| intent(PackMessage::SetInstanceName(text)), )); } block.into() } /// 目标实例那一行(chips + 「新建实例」)。 fn targets<'a>(ui: &'a Ui, catalog: Catalog, model: &'a Pack) -> Element<'a, PageMessage> { let mut chips: Vec> = Vec::new(); for target in &model.targets { chips.push(chip( ui, Chip::filter( target.label.as_str(), model.instance.as_deref() == Some(target.id.as_str()), intent(PackMessage::SetInstance(Some(target.id.clone()))), ), )); } chips.push(chip( ui, Chip::filter( catalog.text(Key::PackTargetNew), model.instance.is_none(), intent(PackMessage::SetInstance(None)), ), )); column![ frame::text_role( ui, TypeRole::LabelMedium, false, catalog.text(Key::PackTargetLabel), ui.color(Role::OnSurfaceVariant), ), row(chips).spacing(ui.space(Space::X2)).wrap(), ] .spacing(ui.space(Space::X1)) .width(Length::Fill) .into() }