import { useEffect, useMemo, useRef, useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { Alert, App, Button, Card, DatePicker, Input, InputNumber, Select, Table, Tag } from "antd"; import dayjs, { type Dayjs } from "dayjs"; import { ReportModal } from "./ReportView"; import { conclusionText, defaultCalibration, formatNumber, formatPercent, materialOptions, materialTiers, validityText, verdictDisplay, type CalculationResult, type LimitTier, type MaterialType, type SampleInput } from "./types"; type MeasurementRow = { key: number; ra: number | null; th: number | null; k: number | null; }; type CalibrationRow = { key: string; name: string } & (typeof defaultCalibration)["ra"]; type FocusableInput = { focus: () => void }; type Props = { initialInput?: SampleInput; onSaved: () => void; }; function rowsFromInput(input?: SampleInput): MeasurementRow[] { if (!input) return [{ key: 1, ra: 100, th: 110, k: 560 }]; const n = input.ra.measured_values.length; return Array.from({ length: n }, (_, i) => ({ key: i + 1, ra: input.ra.measured_values[i] ?? null, th: input.th.measured_values[i] ?? null, k: input.k.measured_values[i] ?? null })); } function CalculatorPanel({ initialInput, onSaved }: Props) { const { message } = App.useApp(); const [rows, setRows] = useState(() => rowsFromInput(initialInput)); const [materialType, setMaterialType] = useState( initialInput?.material_type ?? "BuildingMainBody" ); const [sampleId, setSampleId] = useState(initialInput?.sample_id ?? ""); const [calcDate, setCalcDate] = useState( initialInput?.calculation_date ? dayjs(initialInput.calculation_date) : dayjs() ); const [result, setResult] = useState(null); const [lastInput, setLastInput] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [reportOpen, setReportOpen] = useState(false); const firstCellRefs = useRef>({}); const dataSource = useMemo(() => rows, [rows]); const updateRow = (key: number, field: keyof Omit, 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) => 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 }, material_type: materialType, sample_id: sampleId.trim() ? sampleId.trim() : null, calculation_date: calcDate ? calcDate.format("YYYY-MM-DD") : null }; }; const calculate = async () => { setLoading(true); setError(null); const input = buildInput(); try { const response = await invoke("calculate", { input }); setResult(response); setLastInput(input); } catch (err) { setResult(null); setLastInput(null); setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } }; // 复算:带入历史记录后自动计算一次。 useEffect(() => { if (initialInput) void calculate(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const save = async () => { if (!result || !lastInput) return; setSaving(true); try { await invoke("save_record", { args: { input: lastInput, result, created_at: new Date().toISOString() } }); message.success("已保存到历史"); onSaved(); } catch (err) { message.error(`保存失败:${String(err)}`); } finally { setSaving(false); } }; const tiers = materialTiers[materialType]; return (
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) => ( { firstCellRefs.current[row.key] = instance; }} value={row.ra} min={0} onChange={(value) => updateRow(row.key, "ra", value)} /> ) }, { title: "Th-232", dataIndex: "th", render: (_, row) => ( updateRow(row.key, "th", value)} /> ) }, { title: "K-40", dataIndex: "k", render: (_, row) => ( updateRow(row.key, "k", value)} /> ) }, { title: "", key: "action", width: 88, render: (_, row) => ( ) } ]} />
样品编号 setSampleId(event.target.value)} />
计算日期
材料类型 value={materialType} options={materialOptions} onChange={setMaterialType} style={{ width: "100%" }} />
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" } ]} /> className="limit-table" pagination={false} size="small" rowKey="label" dataSource={tiers} columns={[ { title: "级别", dataIndex: "label", align: "center" }, { title: "IRa 限", dataIndex: "ira", align: "center", render: (value: number | null) => value ?? "—" }, { title: "Ir 限", dataIndex: "ir", align: "center", render: (value: number | null) => value ?? "—" } ]} />
{error ? : null}
{result && lastInput ? ( <> {sampleId.trim() || "未编号"} · {calcDate ? calcDate.format("YYYY-MM-DD") : "无日期"} ·{" "} {result.measurement_count} 次测量 } >
有效性 {validityText[result.analysis.validity].text} 总比活度 {formatNumber(result.analysis.total_calibrated_activity, 1)} Bq/kg(阈值 37)
最终判定 {verdictDisplay(result.analysis.verdict).text} {result.analysis.verdict === "NeedMoreMeasurements" ? 真值区间跨越极限值 : null}
IRa 真值区间 {formatNumber(result.ira.value)} [{formatNumber(result.ira.p2_5)}, {formatNumber(result.ira.p97_5)}] · k=2{" "} {formatNumber(result.ira.relative_expanded_uncertainty_percent, 2)}%
Ir 真值区间 {formatNumber(result.ir.value)} [{formatNumber(result.ir.p2_5)}, {formatNumber(result.ir.p97_5)}] · k=2{" "} {formatNumber(result.ir.relative_expanded_uncertainty_percent, 2)}%
不确定度判定 {conclusionText[result.conclusion]}
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) } ]} />
综合合格概率 {formatPercent(result.mcm.overall_pass_probability)} IRa 与 Ir 同时合格
不符合概率 {formatPercent(result.mcm.overall_fail_probability)} 95% 置信概率下
仿真判定 {result.mcm.overall_fail_probability < 0.05 ? "合格(不符合概率<5%)" : "不合格风险"}
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) => = 0.95 ? "success" : "warning"}>{formatPercent(value)} } ]} />
) : (
请计算后查看结果
)}
setReportOpen(false)} detail={result && lastInput ? { input: lastInput, result } : null} />
); } function ResultTile(props: { title: string; value: number; uncertainty: number }) { return (
{props.title} {formatNumber(props.value)} 相对不确定度 {formatNumber(props.uncertainty, 2)}%
); } export { CalculatorPanel };