// 设置页:A-F 六分组 use eframe::egui; use crate::config::{AppConfig, KeywordSettings, LogLevel, ReportFormat, SampleMode, SampleStrategy, ScreenshotMode, Theme}; use crate::ui::{material, widgets}; /// 绘制设置页 pub fn draw(ui: &mut egui::Ui, cfg: &mut AppConfig) { egui::ScrollArea::vertical() .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) .auto_shrink([false, false]) .show(ui, |ui| { group_a_general(ui, &mut cfg.general); ui.add_space(8.0); group_b_scan(ui, &mut cfg.scan); ui.add_space(8.0); group_c_inspect(ui, &mut cfg.inspect); ui.add_space(8.0); group_d_viewer(ui, &mut cfg.viewer); ui.add_space(8.0); group_e_keyword(ui, &mut cfg.keyword); ui.add_space(8.0); group_f_report(ui, &mut cfg.report); ui.add_space(8.0); material::group(ui, "操作", |ui| { ui.horizontal(|ui| { if ui.button("💾 立即保存").clicked() { if let Err(e) = cfg.save() { tracing::warn!("保存失败:{}", e); } } if ui.button("🔄 恢复默认").clicked() { *cfg = AppConfig::default(); } if ui.button("📂 打开配置文件").clicked() { let path = crate::config::config_path(); let _ = std::fs::create_dir_all(path.parent().unwrap()); let _ = open_in_explorer(&path); } }); }); ui.add_space(20.0); // 底部留白,避免被裁切 }); } fn group_a_general(ui: &mut egui::Ui, g: &mut crate::config::GeneralSettings) { material::group(ui, "A. 常规", |ui| { ui.checkbox(&mut g.auto_run, "启动时自动开始检测"); ui.checkbox(&mut g.start_minimized, "启动时最小化到托盘"); ui.checkbox(&mut g.auto_start, "开机自启"); ui.horizontal(|ui| { ui.label("主题:"); egui::ComboBox::from_id_source("theme") .selected_text(match g.theme { Theme::Light => "Light", Theme::Dark => "Dark", Theme::Follow => "Follow System" }) .show_ui(ui, |ui| { ui.selectable_value(&mut g.theme, Theme::Light, "Light"); ui.selectable_value(&mut g.theme, Theme::Dark, "Dark"); ui.selectable_value(&mut g.theme, Theme::Follow, "Follow System"); }); }); ui.horizontal(|ui| { ui.label("日志级别:"); egui::ComboBox::from_id_source("log_level") .selected_text(format!("{:?}", g.log_level)) .show_ui(ui, |ui| { for l in [LogLevel::Error, LogLevel::Warn, LogLevel::Info, LogLevel::Debug, LogLevel::Trace] { ui.selectable_value(&mut g.log_level, l, format!("{:?}", l)); } }); }); ui.horizontal(|ui| { ui.label("日志保留天数:"); ui.add(egui::DragValue::new(&mut g.log_retention_days).clamp_range(1..=365)); }); ui.checkbox(&mut g.single_instance, "单实例锁"); ui.checkbox(&mut g.clear_temp_on_start, "启动时清空临时目录"); ui.checkbox(&mut g.auto_exit, "检查完自动退出"); if g.auto_exit { ui.horizontal(|ui| { ui.label("倒计时(秒):"); ui.add(egui::DragValue::new(&mut g.auto_exit_seconds).clamp_range(1..=600)); }); } }); } fn group_b_scan(ui: &mut egui::Ui, s: &mut crate::config::ScanSettings) { material::group(ui, "B. 扫描范围(默认全盘 + 白名单 = 不扫)", |ui| { ui.label("白名单目录(不扫描):"); // 先把 s.whitelist 转 Vec 给 string_list_editor let mut wl_strs: Vec = s.whitelist.iter().map(|p| p.display().to_string()).collect(); widgets::string_list_editor(ui, &mut wl_strs, r"例如 C:\Windows"); // 写回 s.whitelist = wl_strs.iter().map(|x| std::path::PathBuf::from(x)).collect(); ui.horizontal(|ui| { if ui.button("➕ 系统目录").clicked() { s.whitelist.push(std::path::PathBuf::from(r"C:\Windows")); } if ui.button("➕ Program Files").clicked() { s.whitelist.push(std::path::PathBuf::from(r"C:\Program Files")); } if ui.button("➕ ProgramData").clicked() { s.whitelist.push(std::path::PathBuf::from(r"C:\ProgramData")); } if ui.button("➕ 回收站").clicked() { s.whitelist.push(std::path::PathBuf::from(r"C:\$Recycle.Bin")); } }); // 把编辑框的字符串结果写回 s.whitelist // (简化:用 text 列表同步一次 —— 实际可用 table state;这里临时方案) // 提示:上面的 string_list_editor 改的是临时 Vec,需要保留后再写回。 // 简单起见:使用一个固定 placeholder ui.checkbox(&mut s.include_hidden, "包含隐藏文件"); ui.checkbox(&mut s.include_system, "包含系统文件"); ui.checkbox(&mut s.follow_symlinks, "跟随符号链接"); ui.horizontal(|ui| { ui.label("文件最小大小 KB:"); ui.add(egui::DragValue::new(&mut s.min_size_kb).clamp_range(0..=1024 * 1024)); }); ui.horizontal(|ui| { ui.label("最大遍历深度(0=无限):"); ui.add(egui::DragValue::new(&mut s.max_depth).clamp_range(0..=1000)); }); ui.label("扩展名白名单(不含点,逗号分隔):"); let mut ext_str = s.extensions.join(","); if ui.text_edit_singleline(&mut ext_str).changed() { s.extensions = ext_str.split(',').map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect(); } ui.horizontal(|ui| { ui.label("单次扫描超时(分钟,0=无限):"); ui.add(egui::DragValue::new(&mut s.scan_timeout_minutes).clamp_range(0..=24 * 60)); }); }); } fn group_c_inspect(ui: &mut egui::Ui, i: &mut crate::config::InspectSettings) { material::group(ui, "C. 抽检(先扫全盘,再按下方配置抽样检查)", |ui| { // 抽样数量模式 ui.horizontal(|ui| { ui.label("抽样模式:"); egui::ComboBox::from_id_source("sample_mode") .selected_text(match i.sample_mode { SampleMode::Count => "📦 按份数", SampleMode::Percent => "📊 按百分比", SampleMode::All => "🌐 全部", }) .show_ui(ui, |ui| { ui.selectable_value(&mut i.sample_mode, SampleMode::Count, "📦 按份数"); ui.selectable_value(&mut i.sample_mode, SampleMode::Percent, "📊 按百分比"); ui.selectable_value(&mut i.sample_mode, SampleMode::All, "🌐 全部"); }); }); ui.horizontal(|ui| { match i.sample_mode { SampleMode::Count => { ui.label("抽检份数:"); ui.add(egui::DragValue::new(&mut i.sample_count).clamp_range(1..=100_000)); } SampleMode::Percent => { ui.label("百分比:"); ui.add(egui::DragValue::new(&mut i.sample_percent).clamp_range(0.1..=100.0).speed(0.5)); ui.label("%"); } SampleMode::All => { ui.label("(将抽检全部候选文件)"); } } }); ui.horizontal(|ui| { ui.label("抽样策略:"); egui::ComboBox::from_id_source("strategy") .selected_text(match i.strategy { SampleStrategy::Random => "完全随机", SampleStrategy::Stratified => "分层", SampleStrategy::Quota => "类型配额" }) .show_ui(ui, |ui| { ui.selectable_value(&mut i.strategy, SampleStrategy::Random, "完全随机"); ui.selectable_value(&mut i.strategy, SampleStrategy::Stratified, "分层"); ui.selectable_value(&mut i.strategy, SampleStrategy::Quota, "类型配额"); }); }); ui.horizontal(|ui| { ui.label("DOC 配额:"); ui.add(egui::DragValue::new(&mut i.doc_quota).clamp_range(0..=100_000)); ui.label("DOCX 配额:"); ui.add(egui::DragValue::new(&mut i.docx_quota).clamp_range(0..=100_000)); ui.label("PDF 配额:"); ui.add(egui::DragValue::new(&mut i.pdf_quota).clamp_range(0..=100_000)); ui.label("XLSX 配额:"); ui.add(egui::DragValue::new(&mut i.xlsx_quota).clamp_range(0..=100_000)); }); ui.horizontal(|ui| { ui.label("单文件总超时(秒):"); ui.add(egui::DragValue::new(&mut i.per_file_timeout_sec).clamp_range(10..=3600)); }); ui.horizontal(|ui| { ui.label("同文件 N 天内不重复:"); ui.add(egui::DragValue::new(&mut i.dedup_days).clamp_range(0..=365)); }); ui.checkbox(&mut i.skip_locked, "跳过锁定/无权限文件"); ui.checkbox(&mut i.stop_on_first_hit, "命中后停止后续抽检"); }); } fn group_d_viewer(ui: &mut egui::Ui, v: &mut crate::config::ViewerSettings) { material::group(ui, "D. 查看器与截图", |ui| { ui.horizontal(|ui| { ui.label("截图模式:"); egui::ComboBox::from_id_source("screenshot_mode") .selected_text(match v.screenshot_mode { ScreenshotMode::Manual => "Manual(Win+Shift+S + 手动框选)", ScreenshotMode::AutoPrintWindow => "Auto(PrintWindow 全自动)", ScreenshotMode::AutoWithFallback => "Auto + 手动兜底", }) .show_ui(ui, |ui| { ui.selectable_value(&mut v.screenshot_mode, ScreenshotMode::Manual, "Manual(Win+Shift+S + 手动框选)"); ui.selectable_value(&mut v.screenshot_mode, ScreenshotMode::AutoPrintWindow, "Auto(PrintWindow 全自动)"); ui.selectable_value(&mut v.screenshot_mode, ScreenshotMode::AutoWithFallback, "Auto + 手动兜底"); }); }); widgets::file_picker(ui, ".doc 查看器(默认 exe 同目录 doclite.exe)", &mut v.doc_viewer, &["exe"]); ui.horizontal(|ui| { ui.label("启动参数模板:"); ui.text_edit_singleline(&mut v.doc_args); }); widgets::file_picker(ui, ".pdf 查看器(默认 Windows 关联)", &mut v.pdf_viewer, &["exe"]); ui.horizontal(|ui| { ui.label("启动参数模板:"); ui.text_edit_singleline(&mut v.pdf_args); }); ui.horizontal(|ui| { ui.label("截图前等待时间 ms:"); ui.add(egui::DragValue::new(&mut v.pre_capture_wait_ms).clamp_range(0..=30_000)); }); ui.horizontal(|ui| { ui.label("手动截图超时秒:"); ui.add(egui::DragValue::new(&mut v.manual_capture_timeout_sec).clamp_range(5..=600)); }); ui.horizontal(|ui| { ui.label("自动模式空位图判定阈值 (max_black_ratio):"); ui.add(egui::DragValue::new(&mut v.max_black_ratio).clamp_range(0.0..=1.0)); }); ui.checkbox(&mut v.auto_close_after, "截图后自动关闭查看器"); ui.horizontal(|ui| { ui.label("关闭等待 ms:"); ui.add(egui::DragValue::new(&mut v.close_wait_ms).clamp_range(0..=30_000)); }); ui.horizontal(|ui| { ui.label("强杀超时 ms:"); ui.add(egui::DragValue::new(&mut v.kill_timeout_ms).clamp_range(0..=30_000)); }); ui.add_space(6.0); ui.label(egui::RichText::new("Umi-OCR").strong()); ui.horizontal(|ui| { ui.label("HTTP 地址:"); ui.text_edit_singleline(&mut v.umi_ocr_url); }); widgets::file_picker(ui, "Umi-OCR.exe 路径(默认 exe 同目录)", &mut v.umi_ocr_exe, &["exe"]); ui.horizontal(|ui| { ui.label("启动后等待秒:"); ui.add(egui::DragValue::new(&mut v.umi_ocr_startup_wait_sec).clamp_range(0..=60)); }); ui.horizontal(|ui| { ui.label("调用超时秒:"); ui.add(egui::DragValue::new(&mut v.umi_ocr_call_timeout_sec).clamp_range(5..=600)); }); ui.horizontal(|ui| { ui.label("OCR 语言:"); ui.text_edit_singleline(&mut v.ocr_language); }); ui.checkbox(&mut v.ocr_cls, "启用文本方向校正"); ui.horizontal(|ui| { ui.label("限制边长 px:"); ui.add(egui::DragValue::new(&mut v.ocr_limit_side_len).clamp_range(0..=99999)); }); }); } fn group_e_keyword(ui: &mut egui::Ui, k: &mut KeywordSettings) { material::group(ui, "E. 关键词", |ui| { ui.label("全局关键词(每行一个):"); widgets::string_list_editor(ui, &mut k.global, "新增关键词"); ui.horizontal(|ui| { if ui.button("📥 导入 txt").clicked() { if let Some(p) = rfd::FileDialog::new().add_filter("text", &["txt"]).pick_file() { if let Ok(s) = std::fs::read_to_string(&p) { k.global = s.lines().map(|l| l.trim().to_string()).filter(|l| !l.is_empty()).collect(); } } } if ui.button("📤 导出 txt").clicked() { if let Some(p) = rfd::FileDialog::new().add_filter("text", &["txt"]).save_file() { let _ = std::fs::write(p, k.global.join("\n")); } } }); ui.checkbox(&mut k.case_sensitive, "区分大小写"); ui.checkbox(&mut k.use_regex, "使用正则"); ui.checkbox(&mut k.whole_word, "整词匹配"); ui.label("DOC 追加关键词:"); widgets::string_list_editor(ui, &mut k.doc_extra, "新增"); ui.label("DOCX 追加关键词:"); widgets::string_list_editor(ui, &mut k.docx_extra, "新增"); ui.label("PDF 追加关键词:"); widgets::string_list_editor(ui, &mut k.pdf_extra, "新增"); ui.label("XLSX 追加关键词:"); widgets::string_list_editor(ui, &mut k.xlsx_extra, "新增"); ui.horizontal(|ui| { ui.label("最低置信度:"); ui.add(egui::DragValue::new(&mut k.min_confidence).clamp_range(0.0..=1.0)); }); ui.label("误报白名单指纹(每行一个,可清除):"); widgets::string_list_editor(ui, &mut k.false_positive_fingerprints, "新增"); }); } fn group_f_report(ui: &mut egui::Ui, r: &mut crate::config::ReportSettings) { material::group(ui, "F. 报告", |ui| { widgets::folder_picker(ui, "输出目录:", &mut Some(r.output_dir.clone())); ui.label("输出格式:"); ui.horizontal(|ui| { // 用三个 bool 缓冲切换 Vec 的成员 let mut has_html = r.formats.contains(&ReportFormat::Html); let mut has_json = r.formats.contains(&ReportFormat::Json); let mut has_png = r.formats.contains(&ReportFormat::Png); if ui.checkbox(&mut has_html, "HTML").changed() { toggle_format(&mut r.formats, ReportFormat::Html, has_html); } if ui.checkbox(&mut has_json, "JSON").changed() { toggle_format(&mut r.formats, ReportFormat::Json, has_json); } if ui.checkbox(&mut has_png, "PNG 组图").changed() { toggle_format(&mut r.formats, ReportFormat::Png, has_png); } }); ui.horizontal(|ui| { ui.label("文件名前缀:"); ui.text_edit_singleline(&mut r.file_prefix); }); ui.checkbox(&mut r.include_screenshots, "包含截图"); ui.checkbox(&mut r.highlight_sensitive, "敏感词高亮"); ui.horizontal(|ui| { ui.label("截图最大边长 px:"); ui.add(egui::DragValue::new(&mut r.max_screenshot_side).clamp_range(320..=8192)); }); ui.horizontal(|ui| { ui.label("历史保留条数:"); ui.add(egui::DragValue::new(&mut r.history_keep).clamp_range(1..=1000)); }); ui.checkbox(&mut r.auto_open, "检查完自动打开报告"); ui.checkbox(&mut r.copy_summary_to_clipboard, "复制摘要到剪贴板"); }); } fn open_in_explorer(path: &std::path::Path) -> std::io::Result<()> { #[cfg(windows)] { std::process::Command::new("explorer").arg(path).spawn().map(|_| ()) } #[cfg(not(windows))] { let _ = path; Ok(()) } } /// 切换 formats 列表中的某一项 fn toggle_format(list: &mut Vec, fmt: ReportFormat, on: bool) { let has = list.contains(&fmt); if on && !has { list.push(fmt); } if !on && has { list.retain(|f| *f != fmt); } }