Files
work-secretfile-selfcheck/src/ui/settings.rs
xiaji 7e256c426f feat(ui): 安全软件风格主题 + 三阶段进度/日志 + XLSX 支持
- 主面板:阶段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/构建产物
2026-06-10 12:20:25 +08:00

315 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 设置页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 => "ManualWin+Shift+S + 手动框选)",
ScreenshotMode::AutoPrintWindow => "AutoPrintWindow 全自动)",
ScreenshotMode::AutoWithFallback => "Auto + 手动兜底",
})
.show_ui(ui, |ui| {
ui.selectable_value(&mut v.screenshot_mode, ScreenshotMode::Manual, "ManualWin+Shift+S + 手动框选)");
ui.selectable_value(&mut v.screenshot_mode, ScreenshotMode::AutoPrintWindow, "AutoPrintWindow 全自动)");
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); }
}