diff --git a/src/scan/runner.rs b/src/scan/runner.rs index fa2955e..cca5e4c 100644 --- a/src/scan/runner.rs +++ b/src/scan/runner.rs @@ -164,6 +164,15 @@ pub async fn run( // 扫完清空 ETA if let Ok(mut e) = scan_eta.lock() { e.clear(); } + // 中断检查:若用户在扫描阶段点了"取消",直接退出,避免继续进入抽样/抽检 + if cancel.load(Ordering::Relaxed) { + push_log("⏹ 已取消(扫描阶段)".into()); + set_state(RunState::Cancelled); + set_step(String::new()); + set_current(None); + return Ok(()); + } + if candidates_count == 0 { push_log("⚠ 没有可抽检的文件,请检查扫描范围/白名单".into()); set_state(RunState::Done); diff --git a/src/ui/home.rs b/src/ui/home.rs index 83ad7af..9314f70 100644 --- a/src/ui/home.rs +++ b/src/ui/home.rs @@ -12,29 +12,12 @@ use crate::ui::material; /// 主面板 pub fn draw(ui: &mut egui::Ui, app: &mut App) { - // 1. 主控制 - material::group(ui, "主控制", |ui| { - ui.horizontal(|ui| { - let can_start = matches!(app.state, RunState::Idle | RunState::Done | RunState::Cancelled | RunState::Error(_)); - let can_cancel = matches!(app.state, RunState::Scanning | RunState::Sampling | RunState::Inspecting | RunState::Reporting); - - if ui.add_enabled(can_start, material::primary_button("▶ 开始检测")).clicked() { - start_inspection(app); - } - if ui.add_enabled(can_cancel, material::danger_button("⏹ 取消")).clicked() { - app.cancel_flag.store(true, Ordering::Relaxed); - app.state = RunState::Cancelled; - } - if ui.button("📂 打开报告目录").clicked() { - let _ = std::fs::create_dir_all(&app.config.report.output_dir); - let _ = open_in_explorer(&app.config.report.output_dir); - } - }); - }); + // 0. 扫描控制(开始/继续/取消) + draw_scan_controls(ui, app); ui.add_space(6.0); - // 2. 状态卡片 + // 1. 状态卡片 material::group(ui, "当前状态", |ui| { let state_str = match &app.state { RunState::Idle => "● 空闲".to_string(), @@ -65,7 +48,7 @@ pub fn draw(ui: &mut egui::Ui, app: &mut App) { ui.add_space(6.0); - // 3. 阶段 1:扫描进度 + // 2. 阶段 1:扫描进度 material::group(ui, "阶段 1/3:扫描全盘候选文件", |ui| { let scanned = app.scan_scanned.load(Ordering::Relaxed); let found = app.scan_found.load(Ordering::Relaxed); @@ -122,8 +105,18 @@ pub fn draw(ui: &mut egui::Ui, app: &mut App) { ui.add_space(6.0); - // 4. 阶段 2/3:抽样结果 + 抽检进度 + // 3. 阶段 2/3:抽样结果 + 抽检进度 material::group(ui, "阶段 2/3:抽样结果 / 阶段 3/3:抽检", |ui| { + // 阶段 1 还在进行时,阶段 2/3 信息无意义,给出占位提示 + if matches!(app.state, RunState::Idle | RunState::Scanning) { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("⏳ 等待阶段 1 扫描完成……") + .strong().size(14.0) + .color(material::ON_SURFACE_DIM)); + }); + return; + } + let p = app.progress.load(Ordering::Relaxed); let t = app.total.load(Ordering::Relaxed); let frac = if t == 0 { 0.0 } else { p as f32 / t as f32 }; @@ -165,14 +158,16 @@ pub fn draw(ui: &mut egui::Ui, app: &mut App) { } }); - // 当前正在处理 - if let Ok(cur) = app.current_file.lock() { - if let Some(f) = cur.as_ref() { - ui.add_space(4.0); - ui.horizontal(|ui| { - ui.label("📄 正在处理:"); - ui.label(egui::RichText::new(shorten_path(f, 100)).monospace().color(material::PRIMARY_DARK)); - }); + // 当前正在处理(仅在阶段 2/3 时显示;阶段 1 的当前文件由上面"阶段 1"组显示) + if matches!(app.state, RunState::Sampling | RunState::Inspecting) { + if let Ok(cur) = app.current_file.lock() { + if let Some(f) = cur.as_ref() { + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label("📄 正在处理:"); + ui.label(egui::RichText::new(shorten_path(f, 100)).monospace().color(material::PRIMARY_DARK)); + }); + } } } }); @@ -251,8 +246,126 @@ fn classify_log(line: &str) -> (egui::Color32, &'static str) { } } +/// 扫描控制区:开始扫描 / 继续扫描 / 取消 +fn draw_scan_controls(ui: &mut egui::Ui, app: &mut App) { + material::group(ui, "扫描控制", |ui| { + // 状态判断 + let running = matches!( + app.state, + RunState::Scanning | RunState::Sampling | RunState::Inspecting | RunState::Reporting + ); + let can_start = !running; + let can_cancel = running; + + // 续扫进度文件是否存在 + let pf = crate::scan::progress_store::progress_file(); + let has_progress = pf.exists(); + + ui.horizontal(|ui| { + // ▶ 开始扫描:全新扫描(先清空旧续扫进度) + if ui + .add_enabled(can_start, material::primary_button("▶ 开始扫描")) + .on_hover_text("从头开始扫描:先清空旧续扫进度,再启动新的扫描流程") + .clicked() + { + start_inspection(app, true); + } + + // ⏩ 继续扫描:使用已有续扫进度跳过已扫过的文件 + if ui + .add_enabled(can_start && has_progress, material::primary_button("⏩ 继续扫描")) + .on_hover_text("使用已有的续扫进度,跳过上次已扫过的文件") + .clicked() + { + start_inspection(app, false); + } + + // ⏸ 取消:中断当前正在运行的扫描/抽检 + if ui + .add_enabled(can_cancel, material::danger_button("⏸ 取消")) + .on_hover_text("中断当前正在进行的扫描/抽检(可点'继续扫描'从断点恢复)") + .clicked() + { + app.cancel_flag.store(true, Ordering::Relaxed); + // 不立刻覆盖 state:让后台任务在合适的检查点写 Cancelled,避免 UI 与后端不一致 + } + + ui.separator(); + + if ui.button("📂 打开报告目录").clicked() { + let _ = std::fs::create_dir_all(&app.config.report.output_dir); + let _ = open_in_explorer(&app.config.report.output_dir); + } + }); + + // 续扫进度提示 + if has_progress { + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label( + egui::RichText::new("✔ 检测到续扫进度文件") + .color(material::SUCCESS) + .size(12.0), + ); + ui.label( + egui::RichText::new(format!("({})", pf.display())) + .color(material::ON_SURFACE_DIM) + .size(11.0), + ); + if ui.small_button("🗑 清空进度").clicked() { + let _ = std::fs::remove_file(&pf); + } + }); + } else { + ui.add_space(2.0); + ui.label( + egui::RichText::new("✖ 暂无续扫进度;点击'开始扫描'会从零开始") + .color(material::ON_SURFACE_DIM) + .size(11.0), + ); + } + + // 当前正在扫描/处理的文件(统一显示入口) + if running { + if let Ok(cur) = app.current_file.lock() { + if let Some(f) = cur.as_ref() { + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label( + egui::RichText::new("📄 当前文件:") + .strong() + .color(material::PRIMARY_DARK) + .size(13.0), + ); + ui.label( + egui::RichText::new(shorten_path(f, 100)) + .monospace() + .color(material::ON_SURFACE) + .size(12.0), + ); + }); + } + } + } + }); +} + /// 启动后台抽检任务 -fn start_inspection(app: &mut App) { +/// +/// - `clear_progress`:true 表示全新扫描(先清空续扫进度文件);false 表示续扫(保留续扫进度) +fn start_inspection(app: &mut App, clear_progress: bool) { + // 全新扫描:先删掉续扫进度文件,确保不会跳过任何文件 + if clear_progress { + let pf = crate::scan::progress_store::progress_file(); + if pf.exists() { + if let Err(e) = std::fs::remove_file(&pf) { + tracing::warn!("清空续扫进度失败:{}", e); + } else { + tracing::info!("已清空续扫进度:{}", pf.display()); + } + } + } + app.state = RunState::Scanning; app.progress.store(0, Ordering::Relaxed); app.total.store(0, Ordering::Relaxed);