Files
silk/miniapp/src/pages/lamp/index.tsx
T
2026-08-13 22:18:35 +08:00

186 lines
6.5 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 React, { useCallback, useEffect, useState } from 'react';
import { Button, Image, Radio, RadioGroup, ScrollView, Text, View } from '@tarojs/components';
import Taro, { usePullDownRefresh } from '@tarojs/taro';
import styles from './index.module.scss';
import {
listLampSteps,
listLampTests,
updateLampResult,
updateLampStep,
uploadLampResultImage,
type LampTest,
type LampTestStep,
} from '@/api/lamp';
import { formatRelativeTime } from '@/utils/format';
const STATUS_LABELS: Record<string, string> = {
pending: '待检测',
testing: '检测中',
resulted: '已出结果',
};
const RESULT_LABELS: Record<string, string> = {
positive: '阳性',
negative: '阴性',
invalid: '无效',
};
const METHOD_LABELS: Record<string, string> = {
lamp: 'LAMP',
qpcr: 'qPCR',
sers: 'SERS',
hyperspectral: '高光谱',
};
const LampPage: React.FC = () => {
const [tests, setTests] = useState<LampTest[]>([]);
const [stepsMap, setStepsMap] = useState<Record<string, LampTestStep[]>>({});
const [expandedId, setExpandedId] = useState('');
const [result, setResult] = useState('');
const [loading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
try {
const list = await listLampTests().catch(() => [] as LampTest[]);
setTests(list);
const map: Record<string, LampTestStep[]> = {};
await Promise.all(
list.map(async (t) => {
map[t.id] = await listLampSteps(t.id).catch(() => [] as LampTestStep[]);
}),
);
setStepsMap(map);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
usePullDownRefresh(() => {
fetchData().then(() => Taro.stopPullDownRefresh());
});
const toggleExpand = (t: LampTest) => {
if (expandedId === t.id) {
setExpandedId('');
return;
}
setExpandedId(t.id);
setResult(t.result || '');
};
const handleStepToggle = async (t: LampTest, s: LampTestStep, done: boolean) => {
await updateLampStep(t.id, s.stepNo, done);
setStepsMap((prev) => ({
...prev,
[t.id]: (prev[t.id] || []).map((x) => (x.stepNo === s.stepNo ? { ...x, done } : x)),
}));
};
const handleChooseImage = (t: LampTest) => {
Taro.chooseImage({
count: 1,
sizeType: ['compressed'],
success: async (res) => {
const path = res.tempFilePaths[0];
try {
const up = await uploadLampResultImage(t.id, path);
setTests((prev) =>
prev.map((x) => (x.id === t.id ? { ...x, resultImageUrl: up.url } : x)),
);
Taro.showToast({ title: '照片已上传', icon: 'success' });
} catch (err) {
Taro.showToast({
title: err instanceof Error ? err.message : '上传失败',
icon: 'none',
});
}
},
});
};
const handleSaveResult = async (t: LampTest) => {
if (!result) {
Taro.showToast({ title: '请选择检测结果', icon: 'none' });
return;
}
await updateLampResult(t.id, result);
Taro.showToast({ title: '结果已保存', icon: 'success' });
fetchData();
};
return (
<ScrollView className={styles.page} scrollY>
{tests.map((t) => {
const steps = stepsMap[t.id] || [];
const doneCount = steps.filter((s) => s.done).length;
const expanded = expandedId === t.id;
return (
<View key={t.id} className={styles.card}>
<View className={styles.cardHeader} onClick={() => toggleExpand(t)}>
<View className={styles.cardInfo}>
<Text className={styles.cardTitle}>{(t.diseases || []).join('、') || 'LAMP 检测'}</Text>
<Text className={styles.cardMeta}>
{METHOD_LABELS[t.method || 'lamp']} · {STATUS_LABELS[t.status] || t.status}
{t.sampleInfo ? ` · ${t.sampleInfo}` : ''} · {formatRelativeTime(t.createdAt)}
</Text>
</View>
<Text className={styles.cardArrow}>{expanded ? '▲' : '▼'}</Text>
</View>
{expanded ? (
<View className={styles.cardBody}>
<Text className={styles.sectionLabel}>操作步骤({doneCount}/{steps.length}</Text>
{steps.map((s) => (
<View key={s.id} className={styles.stepRow} onClick={() => handleStepToggle(t, s, !s.done)}>
<Text className={styles.stepName}>
{s.done ? '✅' : '⬜'} {s.stepNo}. {s.name}
</Text>
</View>
))}
<Text className={styles.sectionLabel}>结果照片</Text>
<View className={styles.photoRow}>
{t.resultImageUrl ? (
<Image className={styles.photo} src={t.resultImageUrl} mode="widthFix" />
) : null}
<Button className={styles.btn} onClick={() => handleChooseImage(t)}>
拍照上传
</Button>
</View>
<Text className={styles.sectionLabel}>检测结果(天蓝=阳性,紫罗兰=阴性)</Text>
<RadioGroup onChange={(e) => setResult(e.detail.value[0])}>
<View className={styles.radioRow}>
<Radio value="positive" checked={result === 'positive'}>阳性</Radio>
<Radio value="negative" checked={result === 'negative'}>阴性</Radio>
<Radio value="invalid" checked={result === 'invalid'}>无效</Radio>
</View>
</RadioGroup>
{t.result ? <Text className={styles.resultHint}>当前结果:{RESULT_LABELS[t.result]}</Text> : null}
{t.crossStatus && t.crossStatus !== 'pending' ? (
<Text className={t.crossStatus === 'consistent' ? styles.crossOk : styles.crossWarn}>
{t.crossStatus === 'consistent'
? '交叉验证:一致(确认诊断)✓'
: `交叉验证:不一致 ⚠ ${t.crossReason || '建议专家会诊'}`}
</Text>
) : null}
<Button className={`${styles.btn} ${styles.btnPrimary}`} onClick={() => handleSaveResult(t)}>
保存结果
</Button>
</View>
) : null}
</View>
);
})}
{!loading && tests.length === 0 ? (
<Text className={styles.emptyText}>暂无检测任务单</Text>
) : null}
</ScrollView>
);
};
export default LampPage;