tcjs/ui/src/App.tsx

400 lines
14 KiB
TypeScript
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.

import { useMemo, useRef, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { Alert, Button, Card, ConfigProvider, 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 AcceptanceLimits = {
ira_limit: number;
ir_limit: number;
};
type SampleInput = {
ra: NuclideMeasurements;
th: NuclideMeasurements;
k: NuclideMeasurements;
limits: AcceptanceLimits;
};
type McmIndexStats = {
mean: number;
std_dev: number;
p2_5: number;
p97_5: number;
standard_value: number;
pass_probability: number;
fail_probability: number;
};
type McmResult = {
iterations: number;
ira: McmIndexStats;
ir: McmIndexStats;
overall_pass_probability: number;
overall_fail_probability: number;
};
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;
mcm: McmResult;
};
type MeasurementRow = {
key: number;
ra: number | null;
th: number | null;
k: number | null;
};
type ResultRow = { name: string } & NuclideResult;
type McmRow = { name: string } & McmIndexStats;
type CalibrationRow = { key: string; name: string } & CalibrationParams;
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 defaultLimits: AcceptanceLimits = { ira_limit: 1.0, ir_limit: 1.0 };
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 formatPercent(value: number, digits = 2) {
if (!Number.isFinite(value)) return "-";
return (value * 100).toFixed(digits) + "%";
}
function App() {
const [rows, setRows] = useState<MeasurementRow[]>(initialRows);
const [limits, setLimits] = useState<AcceptanceLimits>(defaultLimits);
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 },
limits
};
};
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">
<Table<CalibrationRow>
className="calibration-table"
pagination={false}
size="small"
rowKey="key"
dataSource={[
{ key: "ra", name: "Ra", ...defaultCalibration.ra },
{ key: "th", name: "Th", ...defaultCalibration.th },
{ key: "k", name: "K", ...defaultCalibration.k }
]}
columns={[
{ title: "核素", dataIndex: "name", align: "center" },
{ title: "a", dataIndex: "factor", align: "center" },
{
title: "U",
dataIndex: "expanded_uncertainty_percent",
align: "center",
render: (value: number) => `${value}%`
},
{ title: "k", dataIndex: "coverage_factor", align: "center" }
]}
/>
<div className="limits-form">
<div className="limit-field">
<span className="limit-label">IRa </span>
<InputNumber
value={limits.ira_limit}
min={0}
step={0.1}
onChange={(value) => setLimits((current) => ({ ...current, ira_limit: value ?? 0 }))}
/>
</div>
<div className="limit-field">
<span className="limit-label">Ir </span>
<InputNumber
value={limits.ir_limit}
min={0}
step={0.1}
onChange={(value) => setLimits((current) => ({ ...current, ir_limit: value ?? 0 }))}
/>
</div>
</div>
</Card>
</div>
{error ? <Alert type="error" message={error} showIcon /> : null}
<div className="results-area">
{result ? (
<div className="results-row">
<Card title="计算结果GUM 法)" 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>
<Card title={`蒙特卡洛仿真MCM${result.mcm.iterations} 次)`} className="panel">
<div className="result-grid">
<div className="result-tile">
<span></span>
<strong>{formatPercent(result.mcm.overall_pass_probability)}</strong>
<small>IRa Ir </small>
</div>
<div className="result-tile">
<span></span>
<strong>{formatPercent(result.mcm.overall_fail_probability)}</strong>
<small></small>
</div>
<div className="result-tile conclusion-tile">
<span>仿</span>
<Tag color={result.mcm.overall_pass_probability >= 0.95 ? "success" : "warning"}>
{result.mcm.overall_pass_probability >= 0.95 ? "合格" : "不合格风险"}
</Tag>
</div>
</div>
<Table<McmRow>
pagination={false}
rowKey="name"
size="small"
dataSource={[
{ name: "IRa", ...result.mcm.ira },
{ name: "Ir", ...result.mcm.ir }
]}
columns={[
{ title: "指数", dataIndex: "name" },
{ title: "平均值", dataIndex: "mean", render: (value: number) => formatNumber(value) },
{ title: "标准偏差", dataIndex: "std_dev", render: (value: number) => formatNumber(value) },
{ title: "P2.5", dataIndex: "p2_5", render: (value: number) => formatNumber(value) },
{ title: "P97.5", dataIndex: "p97_5", render: (value: number) => formatNumber(value) },
{ title: "标准值", dataIndex: "standard_value", render: (value: number) => formatNumber(value, 2) },
{
title: "合格概率",
dataIndex: "pass_probability",
render: (value: number) => (
<Tag color={value >= 0.95 ? "success" : "warning"}>{formatPercent(value)}</Tag>
)
}
]}
/>
</Card>
</div>
) : (
<div className="results-placeholder"></div>
)}
</div>
</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 };