153 lines
8.3 KiB
Markdown
153 lines
8.3 KiB
Markdown
# 历史存储与报告导出设计(三期)
|
||
|
||
> 关联:一期 `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<RecordSummary>
|
||
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<String>, // 模糊匹配
|
||
material_type: Option<MaterialType>,
|
||
date_from: Option<String>,
|
||
date_to: Option<String>,
|
||
verdict_kind: Option<String>, // "Qualified" / "Unqualified" / ...
|
||
}
|
||
struct RecordSummary {
|
||
id: i64, sample_id: Option<String>, material_type: MaterialType,
|
||
calc_date: Option<String>, created_at: String,
|
||
ira_value: f64, ir_value: f64, validity: Validity, verdict: Verdict,
|
||
}
|
||
```
|
||
|
||
数据库连接用 `tauri::State<Mutex<Connection>>` 在 `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. 端到端:算→存→列表过滤→查看→导出。
|