Initial commit
This commit is contained in:
commit
5cae20cd58
|
|
@ -0,0 +1,9 @@
|
|||
target/
|
||||
target-*/
|
||||
node_modules/
|
||||
ui/dist/
|
||||
dist/
|
||||
src-tauri/gen/
|
||||
*.log
|
||||
.claude/settings.local.json
|
||||
temp/
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ceramic-radioactivity"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "ceramic-radioactivity"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "ceramic_radioactivity"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# 设置代理环境变量
|
||||
$env:HTTP_PROXY = "http://127.0.0.1:7897"
|
||||
$env:HTTPS_PROXY = "http://127.0.0.1:7897"
|
||||
|
||||
# 调用 claude 命令,传递所有参数
|
||||
& "claude" @args
|
||||
|
||||
# 检查上一条命令的退出代码
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "按任意键继续..." -ForegroundColor Yellow
|
||||
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# 设置代理环境变量
|
||||
$env:HTTP_PROXY = "http://127.0.0.1:7897"
|
||||
$env:HTTPS_PROXY = "http://127.0.0.1:7897"
|
||||
|
||||
# 调用 codex 命令,传递所有参数
|
||||
& "codex" @args
|
||||
|
||||
# 检查上一条命令的退出代码
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "按任意键继续..." -ForegroundColor Yellow
|
||||
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
# Rust Tauri MVP Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build the first usable Rust + Tauri MVP for manual measurement entry and formula-based judgment.
|
||||
|
||||
**Architecture:** The Rust calculator is a pure library with explicit domain types and tests. Tauri commands and React UI are added after calculator behavior is verified.
|
||||
|
||||
**Tech Stack:** Rust, Cargo tests, Tauri, React, TypeScript, Ant Design, SQLite later.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Calculator Crate
|
||||
|
||||
**Files:**
|
||||
- Create: `Cargo.toml`
|
||||
- Create: `src/lib.rs`
|
||||
- Create: `src/domain.rs`
|
||||
- Create: `src/calculator.rs`
|
||||
- Create: `tests/calculator_tests.rs`
|
||||
|
||||
- [x] **Step 1: Write failing Rust tests**
|
||||
|
||||
Write tests that call the desired public API before implementation.
|
||||
|
||||
- [x] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test`
|
||||
|
||||
Expected: fail because the calculator modules and functions are not implemented yet.
|
||||
|
||||
- [x] **Step 3: Implement domain and calculator**
|
||||
|
||||
Implement:
|
||||
|
||||
- `NuclideMeasurements`
|
||||
- `CalibrationParams`
|
||||
- `SampleInput`
|
||||
- `CalculationResult`
|
||||
- `Conclusion`
|
||||
- `calculate_sample`
|
||||
|
||||
- [x] **Step 4: Run tests**
|
||||
|
||||
Run: `cargo test`
|
||||
|
||||
Expected: all calculator tests pass.
|
||||
|
||||
### Task 2: Tauri Shell
|
||||
|
||||
**Files:**
|
||||
- Create/modify Tauri scaffold files under the repository root.
|
||||
|
||||
- [x] **Step 1: Add Tauri + React scaffold**
|
||||
|
||||
Use a Vite React TypeScript frontend and Tauri Rust backend.
|
||||
|
||||
- [x] **Step 2: Add calculate command**
|
||||
|
||||
Expose a command that accepts `SampleInput` JSON and returns `CalculationResult` JSON.
|
||||
|
||||
- [x] **Step 3: Build check**
|
||||
|
||||
Run: `npm run build`
|
||||
|
||||
Expected: frontend builds.
|
||||
|
||||
Run: `cargo check`
|
||||
|
||||
Expected: Rust side compiles.
|
||||
|
||||
### Task 3: MVP UI
|
||||
|
||||
**Files:**
|
||||
- Modify React app files after scaffold exists.
|
||||
|
||||
- [x] **Step 1: Build input form**
|
||||
|
||||
Add manual entry fields for Ra, Th, and K repeated values.
|
||||
|
||||
- [x] **Step 2: Call Rust calculator**
|
||||
|
||||
Invoke the Tauri command and display returned values.
|
||||
|
||||
- [x] **Step 3: Show conclusion**
|
||||
|
||||
Display `OK`, `请增加试验次数至 6 次`, or `校准仪器后重新测量`.
|
||||
|
||||
- [x] **Step 4: Build verification**
|
||||
|
||||
Run: `npm run build`
|
||||
|
||||
Expected: build exits with code 0.
|
||||
|
||||
### Task 4: Next Stage Backlog
|
||||
|
||||
After the MVP is verified:
|
||||
|
||||
- Add SQLite history.
|
||||
- Add Excel import/export.
|
||||
- Add PDF or HTML report export.
|
||||
- Add Windows packaging.
|
||||
|
||||
## Self-Review
|
||||
|
||||
- The plan implements the approved MVP only.
|
||||
- The formula engine is isolated and testable before GUI work.
|
||||
- No first-stage requirement depends on network services or external databases.
|
||||
- History and report features are explicitly deferred to keep the first version small.
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
# Rust Tauri MVP Design
|
||||
|
||||
## Goal
|
||||
|
||||
Build a Windows desktop MVP for the building-material radioactivity judgment system. The first version covers manual entry of repeated Ra-226, Th-232, and K-40 measurements, Rust-side uncertainty calculation, and display of IRa, Ir, relative uncertainty, and the acceptance conclusion.
|
||||
|
||||
## Source Documents
|
||||
|
||||
- `temp/项目报价单.md`: original Rust + Tauri quotation scope.
|
||||
- `temp/项目报价单_B方案.md`: C# alternative quotation.
|
||||
- `temp/项目报价单_C方案.md`: Python alternative quotation.
|
||||
- `temp/不确定度公式.pdf`: formula reference for calculation and judgment.
|
||||
|
||||
## Technology Choice
|
||||
|
||||
Use Rust + Tauri + React + TypeScript. Rust owns the formula engine and later storage/reporting commands. React owns data entry and presentation. Ant Design is the preferred UI library because this app is form-heavy and table-heavy.
|
||||
|
||||
## MVP Scope
|
||||
|
||||
Included:
|
||||
|
||||
- Manual entry of repeated measured values for Ra-226, Th-232, and K-40.
|
||||
- Configurable calibration parameters with defaults from the PDF table 3:
|
||||
- Ra: `a = 0.916`, `U = 6.3%`, `k = 2`
|
||||
- Th: `a = 0.884`, `U = 6.9%`, `k = 2`
|
||||
- K: `a = 0.961`, `U = 6.7%`, `k = 2`
|
||||
- A-type uncertainty:
|
||||
- `n >= 6`: sample standard deviation divided by `sqrt(n)`.
|
||||
- `2 <= n < 6`: range method using table 2 coefficients.
|
||||
- B-type uncertainty:
|
||||
- `urB = U / k`
|
||||
- `uB = a * urB`
|
||||
- Calibrated activity values:
|
||||
- `C = measured * calibration_factor`
|
||||
- Indices:
|
||||
- `IRa = C_Ra / 200`
|
||||
- `Ir = C_Ra / 370 + C_Th / 260 + C_K / 4200`
|
||||
- Combined nuclide uncertainty and index uncertainty.
|
||||
- Relative uncertainty and three-state conclusion:
|
||||
- both relative uncertainties <= 20%: `OK`
|
||||
- otherwise, if sample count is below 6: `请增加试验次数至 6 次`
|
||||
- otherwise: `校准仪器后重新测量`
|
||||
|
||||
Excluded from first pass:
|
||||
|
||||
- SQLite history.
|
||||
- Excel import/export.
|
||||
- PDF report export.
|
||||
- Installer packaging.
|
||||
|
||||
These are second-stage modules after the formula path is verified.
|
||||
|
||||
## Architecture
|
||||
|
||||
The repository root starts as a Rust library crate named `ceramic-radioactivity`. It exposes calculation data structures and a pure `calculate_sample` function. Tauri integration will call this function from a command, and the React UI will pass plain JSON data to the command.
|
||||
|
||||
Planned modules:
|
||||
|
||||
- `src/lib.rs`: public exports.
|
||||
- `src/calculator.rs`: formula implementation.
|
||||
- `src/domain.rs`: input, output, calibration parameters, and conclusion types.
|
||||
- `tests/calculator_tests.rs`: formula behavior tests.
|
||||
|
||||
The front end will be added after Rust tests pass:
|
||||
|
||||
- `src-tauri`: Tauri application shell.
|
||||
- `ui`: React + TypeScript + Ant Design UI.
|
||||
|
||||
## Error Handling
|
||||
|
||||
The calculator returns validation errors for:
|
||||
|
||||
- fewer than 2 measurements for any nuclide;
|
||||
- unequal measurement counts across nuclides;
|
||||
- non-finite numeric values;
|
||||
- missing range coefficient for the `2 <= n < 6` range method.
|
||||
|
||||
The UI displays validation failures without saving or exporting anything.
|
||||
|
||||
## Testing
|
||||
|
||||
The Rust calculator is tested first. Tests cover:
|
||||
|
||||
- mean and calibrated activity calculation;
|
||||
- A-type uncertainty for both standard-deviation and range methods;
|
||||
- default B-type uncertainty;
|
||||
- index calculation;
|
||||
- conclusion switching between `OK`, `请增加试验次数至 6 次`, and `校准仪器后重新测量`.
|
||||
|
||||
The first implementation target is a deterministic unit test based on hand-computed small input values, then a sample-shaped test using the PDF table 1 data.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "ceramic-radioactivity-app",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --config ui/vite.config.ts",
|
||||
"build": "vite build --config ui/vite.config.ts",
|
||||
"preview": "vite preview --config ui/vite.config.ts",
|
||||
"tauri": "tauri",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"antd": "^5.25.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"typescript": "^5.8.0",
|
||||
"vite": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "ceramic-radioactivity-tauri"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
ceramic-radioactivity = { path = ".." }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tauri = { version = "2", features = [] }
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build();
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,13 @@
|
|||
use ceramic_radioactivity::{calculate_sample, CalculationResult, SampleInput};
|
||||
|
||||
#[tauri::command]
|
||||
fn calculate(input: SampleInput) -> Result<CalculationResult, String> {
|
||||
calculate_sample(input).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![calculate])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("failed to run Tauri application");
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "建筑材料放射性判定",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.tcjs.ceramic-radioactivity",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"frontendDist": "../ui/dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "建筑材料放射性判定",
|
||||
"width": 1180,
|
||||
"height": 820,
|
||||
"minWidth": 900,
|
||||
"minHeight": 640
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": false,
|
||||
"targets": "all",
|
||||
"icon": []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
use crate::domain::{
|
||||
CalculationError, CalculationResult, Conclusion, IndexResult, NuclideMeasurements,
|
||||
NuclideResult, SampleInput,
|
||||
};
|
||||
|
||||
const ACCEPTANCE_LIMIT_PERCENT: f64 = 20.0;
|
||||
|
||||
pub fn calculate_sample(input: SampleInput) -> Result<CalculationResult, CalculationError> {
|
||||
validate_input(&input)?;
|
||||
|
||||
let n = input.ra.measured_values.len();
|
||||
let ra = calculate_nuclide("Ra", &input.ra)?;
|
||||
let th = calculate_nuclide("Th", &input.th)?;
|
||||
let k = calculate_nuclide("K", &input.k)?;
|
||||
|
||||
let ira = calculate_ira(&ra);
|
||||
let ir = calculate_ir(&ra, &th, &k);
|
||||
let conclusion = if ira.relative_uncertainty_percent <= ACCEPTANCE_LIMIT_PERCENT
|
||||
&& ir.relative_uncertainty_percent <= ACCEPTANCE_LIMIT_PERCENT
|
||||
{
|
||||
Conclusion::Ok
|
||||
} else if n < 6 {
|
||||
Conclusion::IncreaseMeasurementsToSix
|
||||
} else {
|
||||
Conclusion::RecalibrateInstrument
|
||||
};
|
||||
|
||||
Ok(CalculationResult {
|
||||
measurement_count: n,
|
||||
ra,
|
||||
th,
|
||||
k,
|
||||
ira,
|
||||
ir,
|
||||
conclusion,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_input(input: &SampleInput) -> Result<(), CalculationError> {
|
||||
let counts = [
|
||||
("Ra", input.ra.measured_values.len()),
|
||||
("Th", input.th.measured_values.len()),
|
||||
("K", input.k.measured_values.len()),
|
||||
];
|
||||
|
||||
for (nuclide, count) in counts {
|
||||
if count < 2 {
|
||||
return Err(CalculationError::TooFewMeasurements { nuclide, count });
|
||||
}
|
||||
}
|
||||
|
||||
if input.ra.measured_values.len() != input.th.measured_values.len()
|
||||
|| input.ra.measured_values.len() != input.k.measured_values.len()
|
||||
{
|
||||
return Err(CalculationError::MismatchedMeasurementCounts);
|
||||
}
|
||||
|
||||
validate_nuclide("Ra", &input.ra)?;
|
||||
validate_nuclide("Th", &input.th)?;
|
||||
validate_nuclide("K", &input.k)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_nuclide(
|
||||
nuclide: &'static str,
|
||||
measurements: &NuclideMeasurements,
|
||||
) -> Result<(), CalculationError> {
|
||||
let calibration = measurements.calibration;
|
||||
if !calibration.factor.is_finite()
|
||||
|| !calibration.expanded_uncertainty_percent.is_finite()
|
||||
|| !calibration.coverage_factor.is_finite()
|
||||
|| calibration.coverage_factor == 0.0
|
||||
{
|
||||
return Err(CalculationError::InvalidCalibration { nuclide });
|
||||
}
|
||||
|
||||
for value in &measurements.measured_values {
|
||||
if !value.is_finite() {
|
||||
return Err(CalculationError::NonFiniteValue {
|
||||
field: "measured value",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calculate_nuclide(
|
||||
nuclide: &'static str,
|
||||
measurements: &NuclideMeasurements,
|
||||
) -> Result<NuclideResult, CalculationError> {
|
||||
let n = measurements.measured_values.len();
|
||||
let mean_measured = mean(&measurements.measured_values);
|
||||
let mean_calibrated = mean_measured * measurements.calibration.factor;
|
||||
let type_a_uncertainty = type_a_uncertainty(&measurements.measured_values)?;
|
||||
let type_b_relative =
|
||||
measurements.calibration.expanded_uncertainty_percent / 100.0 / measurements.calibration.coverage_factor;
|
||||
let type_b_uncertainty = measurements.calibration.factor * type_b_relative;
|
||||
let sensitivity_coefficient = mean_measured;
|
||||
let combined_uncertainty = (type_a_uncertainty.powi(2)
|
||||
+ (sensitivity_coefficient * type_b_uncertainty).powi(2))
|
||||
.sqrt();
|
||||
|
||||
if n < 6 && range_coefficient(n).is_none() {
|
||||
return Err(CalculationError::UnsupportedRangeMethodCount { count: n });
|
||||
}
|
||||
|
||||
let _ = nuclide;
|
||||
Ok(NuclideResult {
|
||||
mean_measured,
|
||||
mean_calibrated,
|
||||
type_a_uncertainty,
|
||||
type_b_relative,
|
||||
type_b_uncertainty,
|
||||
sensitivity_coefficient,
|
||||
combined_uncertainty,
|
||||
})
|
||||
}
|
||||
|
||||
fn calculate_ira(ra: &NuclideResult) -> IndexResult {
|
||||
let value = ra.mean_calibrated / 200.0;
|
||||
let standard_uncertainty = ra.combined_uncertainty / 200.0;
|
||||
IndexResult {
|
||||
value,
|
||||
standard_uncertainty,
|
||||
relative_uncertainty_percent: relative_percent(standard_uncertainty, value),
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_ir(ra: &NuclideResult, th: &NuclideResult, k: &NuclideResult) -> IndexResult {
|
||||
let value = ra.mean_calibrated / 370.0 + th.mean_calibrated / 260.0 + k.mean_calibrated / 4200.0;
|
||||
let standard_uncertainty = ((ra.combined_uncertainty / 370.0).powi(2)
|
||||
+ (th.combined_uncertainty / 260.0).powi(2)
|
||||
+ (k.combined_uncertainty / 4200.0).powi(2))
|
||||
.sqrt();
|
||||
IndexResult {
|
||||
value,
|
||||
standard_uncertainty,
|
||||
relative_uncertainty_percent: relative_percent(standard_uncertainty, value),
|
||||
}
|
||||
}
|
||||
|
||||
fn type_a_uncertainty(values: &[f64]) -> Result<f64, CalculationError> {
|
||||
let n = values.len();
|
||||
if n >= 6 {
|
||||
Ok(sample_standard_deviation(values) / (n as f64).sqrt())
|
||||
} else {
|
||||
let coefficient =
|
||||
range_coefficient(n).ok_or(CalculationError::UnsupportedRangeMethodCount { count: n })?;
|
||||
let (min, max) = min_max(values);
|
||||
Ok((max - min) / (coefficient * (n as f64).sqrt()))
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_standard_deviation(values: &[f64]) -> f64 {
|
||||
let avg = mean(values);
|
||||
let sum_squared: f64 = values.iter().map(|value| (value - avg).powi(2)).sum();
|
||||
(sum_squared / (values.len() as f64 - 1.0)).sqrt()
|
||||
}
|
||||
|
||||
fn mean(values: &[f64]) -> f64 {
|
||||
values.iter().sum::<f64>() / values.len() as f64
|
||||
}
|
||||
|
||||
fn min_max(values: &[f64]) -> (f64, f64) {
|
||||
values
|
||||
.iter()
|
||||
.fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), value| {
|
||||
(min.min(*value), max.max(*value))
|
||||
})
|
||||
}
|
||||
|
||||
fn range_coefficient(n: usize) -> Option<f64> {
|
||||
match n {
|
||||
2 => Some(1.13),
|
||||
3 => Some(1.69),
|
||||
4 => Some(2.06),
|
||||
5 => Some(2.33),
|
||||
6 => Some(2.53),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn relative_percent(uncertainty: f64, value: f64) -> f64 {
|
||||
if value == 0.0 {
|
||||
f64::INFINITY
|
||||
} else {
|
||||
uncertainty / value.abs() * 100.0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SampleInput {
|
||||
pub ra: NuclideMeasurements,
|
||||
pub th: NuclideMeasurements,
|
||||
pub k: NuclideMeasurements,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NuclideMeasurements {
|
||||
pub measured_values: Vec<f64>,
|
||||
pub calibration: CalibrationParams,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CalibrationParams {
|
||||
pub factor: f64,
|
||||
pub expanded_uncertainty_percent: f64,
|
||||
pub coverage_factor: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CalculationResult {
|
||||
pub measurement_count: usize,
|
||||
pub ra: NuclideResult,
|
||||
pub th: NuclideResult,
|
||||
pub k: NuclideResult,
|
||||
pub ira: IndexResult,
|
||||
pub ir: IndexResult,
|
||||
pub conclusion: Conclusion,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NuclideResult {
|
||||
pub mean_measured: f64,
|
||||
pub mean_calibrated: f64,
|
||||
pub type_a_uncertainty: f64,
|
||||
pub type_b_relative: f64,
|
||||
pub type_b_uncertainty: f64,
|
||||
pub sensitivity_coefficient: f64,
|
||||
pub combined_uncertainty: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct IndexResult {
|
||||
pub value: f64,
|
||||
pub standard_uncertainty: f64,
|
||||
pub relative_uncertainty_percent: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Conclusion {
|
||||
Ok,
|
||||
IncreaseMeasurementsToSix,
|
||||
RecalibrateInstrument,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum CalculationError {
|
||||
TooFewMeasurements { nuclide: &'static str, count: usize },
|
||||
MismatchedMeasurementCounts,
|
||||
NonFiniteValue { field: &'static str },
|
||||
UnsupportedRangeMethodCount { count: usize },
|
||||
InvalidCalibration { nuclide: &'static str },
|
||||
}
|
||||
|
||||
impl fmt::Display for CalculationError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::TooFewMeasurements { nuclide, count } => {
|
||||
write!(f, "{nuclide} requires at least 2 measurements, got {count}")
|
||||
}
|
||||
Self::MismatchedMeasurementCounts => {
|
||||
write!(f, "Ra, Th, and K must have the same measurement count")
|
||||
}
|
||||
Self::NonFiniteValue { field } => write!(f, "{field} must be a finite number"),
|
||||
Self::UnsupportedRangeMethodCount { count } => {
|
||||
write!(f, "range method coefficient is not defined for n = {count}")
|
||||
}
|
||||
Self::InvalidCalibration { nuclide } => {
|
||||
write!(f, "{nuclide} calibration parameters are invalid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for CalculationError {}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
mod calculator;
|
||||
mod domain;
|
||||
|
||||
pub use calculator::calculate_sample;
|
||||
pub use domain::{
|
||||
CalculationError, CalculationResult, CalibrationParams, Conclusion, IndexResult,
|
||||
NuclideMeasurements, NuclideResult, SampleInput,
|
||||
};
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
use ceramic_radioactivity::{
|
||||
calculate_sample, CalibrationParams, Conclusion, NuclideMeasurements, SampleInput,
|
||||
};
|
||||
|
||||
fn default_input() -> SampleInput {
|
||||
SampleInput {
|
||||
ra: NuclideMeasurements {
|
||||
measured_values: vec![100.0, 102.0, 98.0, 101.0, 99.0, 100.0],
|
||||
calibration: CalibrationParams {
|
||||
factor: 0.916,
|
||||
expanded_uncertainty_percent: 6.3,
|
||||
coverage_factor: 2.0,
|
||||
},
|
||||
},
|
||||
th: NuclideMeasurements {
|
||||
measured_values: vec![110.0, 111.0, 109.0, 110.0, 112.0, 108.0],
|
||||
calibration: CalibrationParams {
|
||||
factor: 0.884,
|
||||
expanded_uncertainty_percent: 6.9,
|
||||
coverage_factor: 2.0,
|
||||
},
|
||||
},
|
||||
k: NuclideMeasurements {
|
||||
measured_values: vec![560.0, 565.0, 555.0, 562.0, 558.0, 561.0],
|
||||
calibration: CalibrationParams {
|
||||
factor: 0.961,
|
||||
expanded_uncertainty_percent: 6.7,
|
||||
coverage_factor: 2.0,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculates_indices_and_ok_conclusion_for_six_measurements() {
|
||||
let result = calculate_sample(default_input()).expect("valid sample should calculate");
|
||||
|
||||
assert_close(result.ra.mean_measured, 100.0, 1e-9);
|
||||
assert_close(result.ra.mean_calibrated, 91.6, 1e-9);
|
||||
assert_close(result.th.mean_calibrated, 97.24, 1e-9);
|
||||
assert_close(result.k.mean_calibrated, 538.320_166_666_666_6, 1e-9);
|
||||
assert_close(result.ira.value, 0.458, 1e-9);
|
||||
assert_close(result.ir.value, 0.749_739_035_821_535_9, 1e-9);
|
||||
assert_eq!(result.conclusion, Conclusion::Ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asks_for_more_measurements_when_uncertainty_is_high_and_n_is_below_six() {
|
||||
let mut input = default_input();
|
||||
input.ra.measured_values = vec![10.0, 200.0, 400.0];
|
||||
input.th.measured_values = vec![10.0, 200.0, 400.0];
|
||||
input.k.measured_values = vec![10.0, 200.0, 400.0];
|
||||
|
||||
let result = calculate_sample(input).expect("valid sample should calculate");
|
||||
|
||||
assert_eq!(result.measurement_count, 3);
|
||||
assert_eq!(result.conclusion, Conclusion::IncreaseMeasurementsToSix);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asks_for_recalibration_when_uncertainty_is_high_after_six_measurements() {
|
||||
let mut input = default_input();
|
||||
input.ra.measured_values = vec![10.0, 200.0, 400.0, 10.0, 200.0, 400.0];
|
||||
input.th.measured_values = vec![10.0, 200.0, 400.0, 10.0, 200.0, 400.0];
|
||||
input.k.measured_values = vec![10.0, 200.0, 400.0, 10.0, 200.0, 400.0];
|
||||
|
||||
let result = calculate_sample(input).expect("valid sample should calculate");
|
||||
|
||||
assert_eq!(result.measurement_count, 6);
|
||||
assert_eq!(result.conclusion, Conclusion::RecalibrateInstrument);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_mismatched_measurement_counts() {
|
||||
let mut input = default_input();
|
||||
input.k.measured_values.pop();
|
||||
|
||||
let err = calculate_sample(input).expect_err("mismatched counts should fail");
|
||||
|
||||
assert!(err.to_string().contains("same measurement count"));
|
||||
}
|
||||
|
||||
fn assert_close(actual: f64, expected: f64, tolerance: f64) {
|
||||
assert!(
|
||||
(actual - expected).abs() <= tolerance,
|
||||
"actual {actual} expected {expected} tolerance {tolerance}"
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>建筑材料放射性判定</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,287 @@
|
|||
import { useMemo, useRef, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
ConfigProvider,
|
||||
Descriptions,
|
||||
Form,
|
||||
InputNumber,
|
||||
Table,
|
||||
Tag
|
||||
} from "antd";
|
||||
import zhCN from "antd/locale/zh_CN";
|
||||
|
||||
type Conclusion = "Ok" | "IncreaseMeasurementsToSix" | "RecalibrateInstrument";
|
||||
|
||||
type CalibrationParams = {
|
||||
factor: number;
|
||||
expanded_uncertainty_percent: number;
|
||||
coverage_factor: number;
|
||||
};
|
||||
|
||||
type NuclideMeasurements = {
|
||||
measured_values: number[];
|
||||
calibration: CalibrationParams;
|
||||
};
|
||||
|
||||
type SampleInput = {
|
||||
ra: NuclideMeasurements;
|
||||
th: NuclideMeasurements;
|
||||
k: NuclideMeasurements;
|
||||
};
|
||||
|
||||
type NuclideResult = {
|
||||
mean_measured: number;
|
||||
mean_calibrated: number;
|
||||
type_a_uncertainty: number;
|
||||
type_b_relative: number;
|
||||
type_b_uncertainty: number;
|
||||
sensitivity_coefficient: number;
|
||||
combined_uncertainty: number;
|
||||
};
|
||||
|
||||
type IndexResult = {
|
||||
value: number;
|
||||
standard_uncertainty: number;
|
||||
relative_uncertainty_percent: number;
|
||||
};
|
||||
|
||||
type CalculationResult = {
|
||||
measurement_count: number;
|
||||
ra: NuclideResult;
|
||||
th: NuclideResult;
|
||||
k: NuclideResult;
|
||||
ira: IndexResult;
|
||||
ir: IndexResult;
|
||||
conclusion: Conclusion;
|
||||
};
|
||||
|
||||
type MeasurementRow = {
|
||||
key: number;
|
||||
ra: number | null;
|
||||
th: number | null;
|
||||
k: number | null;
|
||||
};
|
||||
|
||||
type ResultRow = { name: string } & NuclideResult;
|
||||
|
||||
type FocusableInput = {
|
||||
focus: () => void;
|
||||
};
|
||||
|
||||
const defaultCalibration = {
|
||||
ra: { factor: 0.916, expanded_uncertainty_percent: 6.3, coverage_factor: 2 },
|
||||
th: { factor: 0.884, expanded_uncertainty_percent: 6.9, coverage_factor: 2 },
|
||||
k: { factor: 0.961, expanded_uncertainty_percent: 6.7, coverage_factor: 2 }
|
||||
};
|
||||
|
||||
const initialRows: MeasurementRow[] = [
|
||||
{ key: 1, ra: 100, th: 110, k: 560 },
|
||||
{ key: 2, ra: 102, th: 111, k: 565 },
|
||||
{ key: 3, ra: 98, th: 109, k: 555 },
|
||||
{ key: 4, ra: 101, th: 110, k: 562 },
|
||||
{ key: 5, ra: 99, th: 112, k: 558 },
|
||||
{ key: 6, ra: 100, th: 108, k: 561 }
|
||||
];
|
||||
|
||||
const conclusionText: Record<Conclusion, string> = {
|
||||
Ok: "OK",
|
||||
IncreaseMeasurementsToSix: "请增加试验次数至 6 次",
|
||||
RecalibrateInstrument: "校准仪器后重新测量"
|
||||
};
|
||||
|
||||
function formatNumber(value: number, digits = 4) {
|
||||
if (!Number.isFinite(value)) return "-";
|
||||
return value.toFixed(digits);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [rows, setRows] = useState<MeasurementRow[]>(initialRows);
|
||||
const [result, setResult] = useState<CalculationResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const firstCellRefs = useRef<Record<number, FocusableInput | null>>({});
|
||||
|
||||
const dataSource = useMemo(() => rows, [rows]);
|
||||
|
||||
const updateRow = (key: number, field: keyof Omit<MeasurementRow, "key">, value: number | null) => {
|
||||
setRows((current) =>
|
||||
current.map((row) => (row.key === key ? { ...row, [field]: value } : row))
|
||||
);
|
||||
};
|
||||
|
||||
const addRow = () => {
|
||||
const key = Date.now();
|
||||
setRows((current) => [...current, { key, ra: null, th: null, k: null }]);
|
||||
window.setTimeout(() => firstCellRefs.current[key]?.focus(), 0);
|
||||
};
|
||||
|
||||
const removeRow = (key: number) => {
|
||||
setRows((current) => current.filter((row) => row.key !== key));
|
||||
};
|
||||
|
||||
const buildInput = (): SampleInput => {
|
||||
const toValues = (field: keyof Omit<MeasurementRow, "key">) =>
|
||||
rows.map((row) => row[field]).filter((value): value is number => typeof value === "number");
|
||||
|
||||
return {
|
||||
ra: { measured_values: toValues("ra"), calibration: defaultCalibration.ra },
|
||||
th: { measured_values: toValues("th"), calibration: defaultCalibration.th },
|
||||
k: { measured_values: toValues("k"), calibration: defaultCalibration.k }
|
||||
};
|
||||
};
|
||||
|
||||
const calculate = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await invoke<CalculationResult>("calculate", { input: buildInput() });
|
||||
setResult(response);
|
||||
} catch (err) {
|
||||
setResult(null);
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<main className="app-shell">
|
||||
<section className="workspace">
|
||||
<div className="content-grid">
|
||||
<Card
|
||||
title="重复测量值"
|
||||
className="panel measurements-panel"
|
||||
>
|
||||
<div className="measurement-table">
|
||||
<Table<MeasurementRow>
|
||||
pagination={false}
|
||||
dataSource={dataSource}
|
||||
rowKey="key"
|
||||
size="small"
|
||||
columns={[
|
||||
{
|
||||
title: "序号",
|
||||
key: "index",
|
||||
width: 56,
|
||||
align: "center",
|
||||
render: (_, _row, index) => index + 1
|
||||
},
|
||||
{
|
||||
title: "Ra-226",
|
||||
dataIndex: "ra",
|
||||
render: (_, row) => (
|
||||
<InputNumber
|
||||
ref={(instance) => {
|
||||
firstCellRefs.current[row.key] = instance;
|
||||
}}
|
||||
value={row.ra}
|
||||
min={0}
|
||||
onChange={(value) => updateRow(row.key, "ra", value)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Th-232",
|
||||
dataIndex: "th",
|
||||
render: (_, row) => (
|
||||
<InputNumber value={row.th} min={0} onChange={(value) => updateRow(row.key, "th", value)} />
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "K-40",
|
||||
dataIndex: "k",
|
||||
render: (_, row) => (
|
||||
<InputNumber value={row.k} min={0} onChange={(value) => updateRow(row.key, "k", value)} />
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "",
|
||||
key: "action",
|
||||
width: 88,
|
||||
render: (_, row) => (
|
||||
<Button danger size="small" disabled={rows.length <= 2} onClick={() => removeRow(row.key)}>
|
||||
删除
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="measurement-actions">
|
||||
<Button size="small" block onClick={addRow}>
|
||||
添加次数
|
||||
</Button>
|
||||
<Button type="primary" size="small" block loading={loading} onClick={calculate}>
|
||||
计算
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="校准参数" className="panel compact-panel">
|
||||
<Form layout="vertical">
|
||||
{Object.entries(defaultCalibration).map(([key, calibration]) => (
|
||||
<Descriptions key={key} bordered size="small" column={1} className="calibration-block">
|
||||
<Descriptions.Item label={key.toUpperCase()}>{calibration.factor}</Descriptions.Item>
|
||||
<Descriptions.Item label="U">{calibration.expanded_uncertainty_percent}%</Descriptions.Item>
|
||||
<Descriptions.Item label="k">{calibration.coverage_factor}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
))}
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{error ? <Alert type="error" message={error} showIcon /> : null}
|
||||
|
||||
{result ? (
|
||||
<Card title="计算结果" className="panel">
|
||||
<div className="result-grid">
|
||||
<ResultTile title="IRa" value={result.ira.value} uncertainty={result.ira.relative_uncertainty_percent} />
|
||||
<ResultTile title="Ir" value={result.ir.value} uncertainty={result.ir.relative_uncertainty_percent} />
|
||||
<div className="result-tile conclusion-tile">
|
||||
<span>判定</span>
|
||||
<Tag color={result.conclusion === "Ok" ? "success" : "warning"}>
|
||||
{conclusionText[result.conclusion]}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<Table<ResultRow>
|
||||
pagination={false}
|
||||
rowKey="name"
|
||||
size="small"
|
||||
dataSource={[
|
||||
{ name: "Ra-226", ...result.ra },
|
||||
{ name: "Th-232", ...result.th },
|
||||
{ name: "K-40", ...result.k }
|
||||
]}
|
||||
columns={[
|
||||
{ title: "核素", dataIndex: "name" },
|
||||
{ title: "均值", dataIndex: "mean_measured", render: (value: number) => formatNumber(value) },
|
||||
{ title: "校准活度", dataIndex: "mean_calibrated", render: (value: number) => formatNumber(value) },
|
||||
{ title: "A 类不确定度", dataIndex: "type_a_uncertainty", render: (value: number) => formatNumber(value) },
|
||||
{ title: "B 类相对值", dataIndex: "type_b_relative", render: (value) => formatNumber(value * 100, 3) + "%" },
|
||||
{ title: "合成不确定度", dataIndex: "combined_uncertainty", render: (value: number) => formatNumber(value) }
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
) : null}
|
||||
</section>
|
||||
</main>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ResultTile(props: { title: string; value: number; uncertainty: number }) {
|
||||
return (
|
||||
<div className="result-tile">
|
||||
<span>{props.title}</span>
|
||||
<strong>{formatNumber(props.value)}</strong>
|
||||
<small>相对不确定度 {formatNumber(props.uncertainty, 2)}%</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { App };
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: #202124;
|
||||
background: #f4f6f8;
|
||||
font-family: Inter, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 240px;
|
||||
grid-auto-rows: 500px;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.content-grid > .panel {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel .ant-card-head {
|
||||
min-height: 38px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.panel .ant-card-head-title {
|
||||
padding: 9px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.panel .ant-card-body {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.measurements-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.measurements-panel .ant-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.measurement-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.compact-panel .ant-card-body {
|
||||
height: calc(100% - 38px);
|
||||
overflow: hidden;
|
||||
padding: 8px 10px 6px;
|
||||
}
|
||||
|
||||
.calibration-block {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ant-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.measurement-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.result-tile {
|
||||
min-height: 68px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.result-tile span {
|
||||
color: #5f6368;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.result-tile strong {
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.result-tile small {
|
||||
color: #5f6368;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.conclusion-tile .ant-tag {
|
||||
width: fit-content;
|
||||
font-size: 14px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.ant-table-small .ant-table-thead > tr > th,
|
||||
.ant-table-small .ant-table-tbody > tr > td {
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.ant-input-number {
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.ant-input-number .ant-input-number-input {
|
||||
height: 24px;
|
||||
padding: 0 7px;
|
||||
}
|
||||
|
||||
.ant-descriptions.ant-descriptions-small .ant-descriptions-item-label,
|
||||
.ant-descriptions.ant-descriptions-small .ant-descriptions-item-content {
|
||||
padding: 5px 8px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.ant-descriptions.ant-descriptions-small .ant-descriptions-item-label {
|
||||
width: 54px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.app-shell {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.content-grid,
|
||||
.result-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
grid-auto-rows: auto;
|
||||
}
|
||||
|
||||
.content-grid > .panel {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.measurements-panel {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": []
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
root: "ui",
|
||||
plugins: [react()],
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true
|
||||
},
|
||||
build: {
|
||||
outDir: "dist",
|
||||
emptyOutDir: true
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue