# 历史存储与报告导出设计(三期) > 关联:一期 `2026-05-15-rust-tauri-mvp-design.md`、二期 `2026-06-11-judgment-logic-design.md`。 > 对应 PDF 第 6 节「输出」:样品编号、计算日期、最佳估计值、95% 置信区间、k=1 相对扩展不确定度、不符合概率。 ## 1. 目标 一/二期已完成计算与判定。本期补齐"留痕与出报告"两块工程能力: - **历史存储**:每次计算的输入与结果落库,可检索、回看、复算、删除。 - **报告导出**:将一次完整结果导出为可交付文件(PDF 检测报告 + Excel 数据表)。 非目标:多用户/权限、云同步、检测报告的官方排版模板(先满足内容完整、可打印)。 ## 2. 范围 | 能力 | 内容 | |---|---| | 历史存储 | 保存/列表/详情/删除/复算一条记录 | | Excel 导出 | 单条记录导出为 `.xlsx`(输入表 + 结果表 + 判定) | | PDF 报告 | 单条记录导出为 A4 检测报告(中文) | | 列表检索 | 按样品编号 / 日期范围 / 材料类型 / 判定结论过滤 | ## 3. 数据模型与存储 ### 3.1 选型 SQLite,经由 **`rusqlite`(bundled 特性)** 直接在 `src-tauri` 内访问,不引入 `tauri-plugin-sql`。理由:计算结果是结构化 JSON,存取简单;bundled 模式免去系统 SQLite 依赖,Windows 打包干净。 库文件位置:`tauri::api::path::app_data_dir()` 下的 `history.db`(随应用数据目录,卸载可清理)。 ### 3.2 表结构 ```sql CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, sample_id TEXT, -- 样品编号(可空) material_type TEXT NOT NULL, -- MaterialType 序列化 calc_date TEXT, -- 计算日期 YYYY-MM-DD created_at TEXT NOT NULL, -- 入库时间 RFC3339(前端传入) ira_value REAL NOT NULL, -- 冗余列,供列表展示/排序/过滤 ir_value REAL NOT NULL, validity TEXT NOT NULL, -- Validity 序列化 verdict TEXT NOT NULL, -- Verdict 序列化(用于列表标签与过滤) input_json TEXT NOT NULL, -- 完整 SampleInput result_json TEXT NOT NULL -- 完整 CalculationResult ); CREATE INDEX IF NOT EXISTS idx_records_created ON records(created_at); CREATE INDEX IF NOT EXISTS idx_records_sample ON records(sample_id); ``` 设计要点:完整 `input_json` / `result_json` 保证回看与复算的保真;冗余标量列只为列表查询与过滤,避免反序列化全表。 ### 3.3 复算保真 复算 = 读 `input_json` → `calculate_sample` 重新计算。因 MCM 用固定种子,结果可复现,可与存档 `result_json` 比对作为一致性自检(库内核版本升级时用于回归)。 ## 4. 后端命令(`src-tauri`) 新增 Tauri 命令,全部返回 `Result<_, String>`: ```rust save_record(input: SampleInput, result: CalculationResult, created_at: String) -> i64 // 返回 id list_records(filter: RecordFilter) -> Vec get_record(id: i64) -> RecordDetail // 含 input + result delete_record(id: i64) -> () export_excel(id: i64, path: String) -> () // 写 .xlsx 到用户选定路径 export_report(id: i64, path: String) -> () // 见第 6 节 ``` ```rust struct RecordFilter { sample_id: Option, // 模糊匹配 material_type: Option, date_from: Option, date_to: Option, verdict_kind: Option, // "Qualified" / "Unqualified" / ... } struct RecordSummary { id: i64, sample_id: Option, material_type: MaterialType, calc_date: Option, created_at: String, ira_value: f64, ir_value: f64, validity: Validity, verdict: Verdict, } ``` 数据库连接用 `tauri::State>` 在 `setup` 中初始化并建表。 新增 crate(`src-tauri/Cargo.toml`):`rusqlite { features = ["bundled"] }`、`rust_xlsxwriter`。PDF 方案见第 6 节(可能 0 依赖)。 ## 5. Excel 导出 用 `rust_xlsxwriter`(纯 Rust、原生 UTF-8,无 CJK 字体问题)。生成单工作簿三段: 1. **样品信息**:编号、日期、材料类型、有效性、最终判定。 2. **测量与校准**:各次 Ra/Th/K 测量值;校准系数 a/U/k。 3. **结果**:IRa/Ir 值、u、U、相对扩展不确定度(k=1/k=2)、真值区间;各核素均值/校准活度/A 类/B 类/合成;MCM 平均值/标准偏差/区间/合格概率/不符合概率。 路径经 `@tauri-apps/plugin-dialog` 的保存对话框选定,Rust 侧写文件。 ## 6. PDF 报告 中文 PDF 字体内嵌是主要难点,给两条路线,**推荐路线 A**: - **路线 A(推荐)— Webview 打印**:前端用一个独立的、A4 排版的「报告视图」组件渲染完整结果,调用 `window.print()`(或 Tauri 打印 API),用户在打印对话框选「另存为 PDF」。零新依赖、零字体问题、排版用 CSS 即可,所见即所得。代价:依赖系统打印到 PDF,非完全静默导出。 - **路线 B — Rust 生成**:`printpdf` + 内嵌中文子集字体(如 Noto Sans SC)。可完全静默、可定制页眉页脚,但需打包字体、自行处理换行与分页,工作量大。 建议本期落 A,把"报告视图"做成可复用组件(同一组件既用于屏幕预览也用于打印),路线 B 留作后续真正"一键静默导出"需求时再做。 报告内容(对应 PDF 6.1–6.3): - 抬头:样品编号、计算日期、材料类型、限值表。 - 检测结果:IRa、Ir 最佳估计值;95% 真值区间 [P2.5, P97.5];k=1 / k=2 相对扩展不确定度。 - 有效性判定(总比活度 vs 37)、最终判定(合格/不合格/A·B·C/建议增加次数)。 - MCM:95% 置信概率下不符合概率。 - 各核素中间量明细表。 ## 7. 界面变更 - 顶部加「保存到历史」按钮(计算成功后可用);保存成功后提示。 - 新增「历史」入口(抽屉或独立 Tab):列表(编号/日期/材料/IRa/Ir/判定)+ 过滤栏;行操作「查看 / 复算 / 导出 Excel / 导出 PDF / 删除」。 - 「查看」打开报告视图(即打印用组件)。 - 计算日期从已有的 DatePicker 复用,`created_at` 取前端当前时间戳。 ## 8. 测试 - Rust:DB 用内存库(`:memory:`)测 save→list→get→delete 往返;过滤条件命中;`export_excel` 生成非空且能被重新打开(校验首部魔数/可解析)。 - 复算一致性:存档 `result_json` 与重算结果逐字段相等(固定种子)。 - 前端:报告视图快照(关键字段渲染)、历史列表过滤交互。 ## 9. 已确认决策 1. **PDF 路线**:路线 A —— 前端 A4 报告视图 + `window.print()` 另存为 PDF。✅ 2. **历史入口形态**:顶部 Tab 切换(计算 / 历史两页)。✅ 3. **保存时机**:仅用户点「保存到历史」时入库,避免调参脏数据。✅ 4. **导出范围**:PDF 报告 + Excel 数据表都做(实现独立,可分批落地)。✅ ## 9.5 实现说明(落地后补记) - **rusqlite 版本**:本地 Rust 为 1.93,`libsqlite3-sys 0.38` 使用了未稳定的 `cfg_select!`,编译失败。已将 `rusqlite` 固定为 `0.37`(拉取 `libsqlite3-sys 0.35`)规避。升级工具链到 ≥1.94 后可放开。 - **Excel 落盘**:`export_excel` 写入系统下载目录(取不到则回退文档目录),返回完整路径,前端以提示展示,无需对话框插件。 - **PDF**:前端 `ReportView` + `ReportModal`,「打印 / 导出 PDF」走 `window.print()`,打印 CSS 用 visibility 隔离 `.report-root`,仅输出报告本体(隐藏 Modal 外框与遮罩)。 - **命令入参**:`save_record` 用 `SaveArgs` 结构体包裹 `input/result/created_at`,避免 JS↔Rust 多词参数名映射歧义。 - **f64 存储**:完整结果以 JSON 落库;serde_json 解析可能有 ~1e-14 的末位 ULP 偏差,对指数无实际影响,测试以容差比较。 ## 10. 实施顺序 1. `src-tauri`:加 `rusqlite`,`setup` 建表 + 连接 State。 2. 实现 save/list/get/delete 命令 + Rust 往返测试。 3. 前端历史列表 + 过滤 + 保存按钮。 4. 报告视图组件(屏幕预览)→ 接 `window.print()` 出 PDF。 5. `rust_xlsxwriter` 导出命令 + 保存对话框。 6. 端到端:算→存→列表过滤→查看→导出。