- 主面板:阶段1扫描全盘 → 阶段2抽样 → 阶段3抽检,每阶段独立进度条/已用时/分类型 chips - 日志:按类型着色(命中红/未命中绿/警告黄/阶段青) - 主题:暗绿底 + 鲜绿/青色强调,圆角胶囊按钮(material::security_dark) - 抽检:SampleMode 枚举支持按份数/百分比/全部;设置页 C 组动态切换 - 抽检:XLSX 检查器(zip + quick-xml 解析 sharedStrings 与 sheet) - 扫描:walker 进度回调(已访问、命中候选、当前目录) - 兼容:quick-xml 0.36 使用 reader.config_mut().trim_text() - 仓库:新增 .gitignore 忽略 venv/pyc/target/构建产物
315 lines
17 KiB
Rust
315 lines
17 KiB
Rust
// 设置页: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> 给 string_list_editor
|
||
let mut wl_strs: Vec<String> = 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<ReportFormat> 的成员
|
||
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<ReportFormat>, fmt: ReportFormat, on: bool) {
|
||
let has = list.contains(&fmt);
|
||
if on && !has { list.push(fmt); }
|
||
if !on && has { list.retain(|f| *f != fmt); }
|
||
}
|