import { useEffect, useRef, useState } from 'react'; import { Button, Checkbox, Form, Input, Modal, Popconfirm, Radio, Select, Switch, Tabs, Tag, Upload, message, } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; import type { UploadFile } from 'antd'; import { ProTable, type ActionType, type ProColumns } from '@ant-design/pro-components'; import dayjs from 'dayjs'; import { listLampTests, createLampTest, updateLampTest, deleteLampTest, listLampSteps, updateLampStep, uploadLampResultImage, judgeQPCR, uploadLampSpectrum, listSpectrumEntries, createSpectrumEntry, uploadSpectrumEntryFile, deleteSpectrumEntry, type LampTest, type LampTestStep, type SpectrumEntry, } from '../dal/lamp'; import { listDiseases } from '../dal/knowledge'; import { listHouses, type SilkwormHouse } from '../dal/silkworm'; import { listBatches, type Batch } from '../dal/trayBatch'; import { authService } from '../services/auth'; const STATUS_LABELS: Record = { pending: '待检测', testing: '检测中', resulted: '已出结果', }; const RESULT_LABELS: Record = { positive: '阳性', negative: '阴性', invalid: '无效', }; const METHOD_LABELS: Record = { lamp: 'LAMP', qpcr: 'qPCR', sers: 'SERS', hyperspectral: '高光谱', }; const canWrite = () => authService.hasPermission('lamp:write'); function LampTab() { const [rooms, setRooms] = useState([]); const [batches, setBatches] = useState([]); const [diseaseOptions, setDiseaseOptions] = useState<{ value: string; label: string }[]>([]); const actionRef = useRef(); const [createOpen, setCreateOpen] = useState(false); const [createForm] = Form.useForm>(); const [resultOpen, setResultOpen] = useState(false); const [resultForm] = Form.useForm>(); const [resultTarget, setResultTarget] = useState(null); const [imageList, setImageList] = useState([]); const [stepsOpen, setStepsOpen] = useState(false); const [stepsTarget, setStepsTarget] = useState(null); const [steps, setSteps] = useState([]); const [ctValues, setCtValues] = useState(''); const [threshold, setThreshold] = useState(35); useEffect(() => { Promise.all([ listHouses().catch(() => ({ items: [] as SilkwormHouse[] })), listBatches().catch(() => [] as Batch[]), listDiseases().catch(() => [] as any[]), ]).then(([houseRes, batchRes, diseaseRes]) => { setRooms(houseRes.items); setBatches(batchRes); setDiseaseOptions(diseaseRes.map((d: any) => ({ value: d.name, label: d.name }))); }); }, []); const roomName = (id?: string) => rooms.find((r) => r.id === id)?.name || id || '-'; const roomEnum = Object.fromEntries(rooms.map((r) => [r.id, { text: r.name }])); const openSteps = async (t: LampTest) => { setStepsTarget(t); setSteps(await listLampSteps(t.id).catch(() => [] as LampTestStep[])); setStepsOpen(true); }; const openResult = (t: LampTest) => { setResultTarget(t); resultForm.setFieldsValue({ result: t.result }); setImageList( t.resultImageUrl ? [{ uid: '-1', name: '结果照片', status: 'done', url: t.resultImageUrl }] : [], ); setResultOpen(true); }; const columns: ProColumns[] = [ { title: '序号', valueType: 'indexBorder', search: false, width: 60 }, { title: '方式', dataIndex: 'method', search: false, width: 90, render: (_, r) => {METHOD_LABELS[r.method || 'lamp']} }, { title: '蚕房', dataIndex: 'roomId', valueType: 'select', valueEnum: roomEnum, render: (_, r) => roomName(r.roomId) }, { title: '检测病种', dataIndex: 'diseases', search: false, render: (_, r) => (r.diseases || []).join('、') || '-' }, { title: '采样信息', dataIndex: 'sampleInfo', search: false, render: (_, r) => r.sampleInfo || '-' }, { title: '状态', dataIndex: 'status', valueType: 'select', valueEnum: Object.fromEntries(Object.entries(STATUS_LABELS).map(([k, v]) => [k, { text: v }])), render: (_, r) => {STATUS_LABELS[r.status] || r.status}, }, { title: '结果', dataIndex: 'result', search: false, render: (_, r) => (r.result ? RESULT_LABELS[r.result] || r.result : '-') }, { title: '交叉验证', dataIndex: 'crossStatus', search: false, render: (_, r) => r.crossStatus === 'consistent' ? ( 一致 ) : r.crossStatus === 'inconsistent' ? ( 不一致 ) : ( '-' ), }, { title: '创建时间', dataIndex: 'createdAt', search: false, render: (_, r) => (r.createdAt ? dayjs(r.createdAt).format('YYYY-MM-DD HH:mm') : '-') }, { title: '操作', valueType: 'option', render: (_, r) => [ openSteps(r)}> 步骤 , openResult(r)}> 结果 , ...(canWrite() ? [ { await deleteLampTest(r.id); message.success('已删除'); actionRef.current?.reload(); }} > 删除 , ] : []), ], }, ]; return ( <> actionRef={actionRef} rowKey="id" columns={columns} search={{ labelWidth: 'auto' }} request={async (params) => { const res = await listLampTests({ roomId: params.roomId, status: params.status }); return { data: res, total: res.length, success: true }; }} toolBarRender={() => canWrite() ? [ , ] : [] } /> setCreateOpen(false)} onOk={async () => { const v = await createForm.validateFields(); await createLampTest(v); message.success('已创建(自动生成 5 步流程)'); setCreateOpen(false); actionRef.current?.reload(); }} >
({ value, label }))} />
setResultOpen(false)} onOk={async () => { if (!resultTarget) return; const v = await resultForm.validateFields(); await updateLampTest(resultTarget.id, { result: v.result }); message.success('结果已保存'); setResultOpen(false); actionRef.current?.reload(); }} >
{resultTarget?.method === 'qpcr' ? (
setCtValues(e.target.value)} style={{ flex: 1 }} /> setThreshold(Number(e.target.value))} style={{ width: 90 }} />
) : ( )}
{resultTarget?.method === 'sers' ? ( { if (!resultTarget) return; try { const res = await uploadLampSpectrum(resultTarget.id, file as File); message.success('光谱已上传'); onSuccess?.(res); } catch (e) { onError?.(e as Error); } }} > ) : null} {resultTarget?.crossStatus && resultTarget.crossStatus !== 'pending' ? (
{resultTarget.crossStatus === 'consistent' ? '交叉验证:一致' : '交叉验证:不一致'} {resultTarget.crossReason ? ( {resultTarget.crossReason} ) : null}
) : null} { if (!resultTarget) return; try { const res = await uploadLampResultImage(resultTarget.id, file as File); setImageList([{ uid: '-1', name: '结果照片', status: 'done', url: res.url }]); onSuccess?.(res); } catch (e) { onError?.(e as Error); } }} onRemove={() => setImageList([])} > {imageList.length >= 1 ? null : (
上传
)}
setStepsOpen(false)} footer={null} > {steps.map((s) => (
{`${s.stepNo}. ${s.name}`} { await updateLampStep(s.lampTestId, s.stepNo, { done }); setSteps(await listLampSteps(s.lampTestId)); message.success('已更新'); }} />
))}
); } function SpectrumTab() { const actionRef = useRef(); const [modalOpen, setModalOpen] = useState(false); const [form] = Form.useForm>(); const [uploadedUrl, setUploadedUrl] = useState(''); const columns: ProColumns[] = [ { title: '序号', valueType: 'indexBorder', search: false, width: 60 }, { title: '病种', dataIndex: 'disease' }, { title: '来源', dataIndex: 'source', search: false, render: (_, r) => r.source || '-' }, { title: '数据文件', dataIndex: 'dataUrl', search: false, render: (_, r) => (r.dataUrl ? 查看 : '-') }, { title: '备注', dataIndex: 'note', search: false, render: (_, r) => r.note || '-' }, { title: '操作', valueType: 'option', render: (_, r) => [ { await deleteSpectrumEntry(r.id); message.success('已删除'); actionRef.current?.reload(); }} > 删除 , ], }, ]; return ( <> actionRef={actionRef} rowKey="id" columns={columns} search={false} request={async () => { const res = await listSpectrumEntries(); return { data: res, total: res.length, success: true }; }} toolBarRender={() => [ , ]} /> setModalOpen(false)} onOk={async () => { const v = await form.validateFields(); await createSpectrumEntry({ ...v, dataUrl: uploadedUrl || v.dataUrl }); message.success('已保存'); setModalOpen(false); actionRef.current?.reload(); }} >
{ try { const res = await uploadSpectrumEntryFile(file as File); setUploadedUrl(res.url); message.success('文件已上传'); onSuccess?.(res); } catch (e) { onError?.(e as Error); } }} >
); } export default function LampTestsPage() { return ( }, { key: 'spectrum', label: '光谱库', children: }, ]} /> ); }