feat: 补齐消毒、种源与二维码身份链
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { get, post } from './request';
|
||||
|
||||
export interface QRResolveResult {
|
||||
entityType: string;
|
||||
publicId: string;
|
||||
entity: {
|
||||
id: string;
|
||||
name?: string;
|
||||
roomId?: string;
|
||||
batchId?: string;
|
||||
sampleNo?: string;
|
||||
state?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DisinfectionRecord {
|
||||
roomId?: string;
|
||||
batchId?: string;
|
||||
kind: 'plan' | 'execution';
|
||||
agent: string;
|
||||
concentration: string;
|
||||
amount?: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export const resolveQR = (payload: string) =>
|
||||
post<QRResolveResult>('/biosecurity/qr/resolve', { payload });
|
||||
|
||||
export const createDisinfectionRecord = (data: DisinfectionRecord) =>
|
||||
post('/biosecurity/disinfection-records', data as unknown as Record<string, unknown>);
|
||||
|
||||
export const listDisinfectionRecords = (params?: any) =>
|
||||
get<DisinfectionRecord[]>('/biosecurity/disinfection-records', { params });
|
||||
@@ -16,6 +16,7 @@ export default defineAppConfig({
|
||||
'pages/inspection/index',
|
||||
'pages/notification/index',
|
||||
'pages/lamp/index',
|
||||
'pages/biosecurity/index',
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: 'dark',
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
background: #f5f6f7;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 26px;
|
||||
color: #4e5969;
|
||||
margin: 18px 0 8px;
|
||||
}
|
||||
|
||||
.input {
|
||||
height: 76px;
|
||||
background: #f2f3f5;
|
||||
border-radius: 8px;
|
||||
padding: 0 20px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.btnPrimary {
|
||||
margin-top: 24px;
|
||||
background: #10b981;
|
||||
color: #ffffff;
|
||||
border-radius: 8px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.entityBox {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f0fdf4;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.entityType {
|
||||
display: block;
|
||||
color: #4e5969;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.entityName {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Input, Text, View } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import styles from './index.module.scss';
|
||||
import { createDisinfectionRecord, resolveQR, type QRResolveResult } from '@/api/biosecurity';
|
||||
|
||||
const BiosecurityPage: React.FC = () => {
|
||||
const [entity, setEntity] = useState<QRResolveResult | null>(null);
|
||||
const [agent, setAgent] = useState('');
|
||||
const [concentration, setConcentration] = useState('');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [note, setNote] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleScan = async () => {
|
||||
try {
|
||||
const res = await Taro.scanCode({ scanType: ['qrCode'] });
|
||||
if (!res.result) throw new Error('未识别到二维码');
|
||||
const data = await resolveQR(res.result);
|
||||
setEntity(data);
|
||||
Taro.showToast({ title: '扫码成功', icon: 'success' });
|
||||
} catch (err) {
|
||||
Taro.showToast({
|
||||
title: err instanceof Error ? err.message : '扫码失败',
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!entity) {
|
||||
Taro.showToast({ title: '请先扫码', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (!agent || !concentration) {
|
||||
Taro.showToast({ title: '请填写药剂和浓度', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await createDisinfectionRecord({
|
||||
kind: 'execution',
|
||||
roomId: entity.entity.roomId,
|
||||
batchId: entity.entityType === 'batch' ? entity.entity.id : entity.entity.batchId,
|
||||
agent,
|
||||
concentration,
|
||||
amount: amount || undefined,
|
||||
note: note || undefined,
|
||||
});
|
||||
Taro.showToast({ title: '消毒记录已提交', icon: 'success' });
|
||||
setAgent('');
|
||||
setConcentration('');
|
||||
setAmount('');
|
||||
setNote('');
|
||||
} catch (err) {
|
||||
Taro.showToast({
|
||||
title: err instanceof Error ? err.message : '提交失败',
|
||||
icon: 'none',
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View className={styles.page}>
|
||||
<View className={styles.card}>
|
||||
<Text className={styles.title}>二维码身份</Text>
|
||||
<Button className={styles.btnPrimary} onClick={handleScan}>
|
||||
扫描蚕房/批次/样本二维码
|
||||
</Button>
|
||||
{entity ? (
|
||||
<View className={styles.entityBox}>
|
||||
<Text className={styles.entityType}>类型:{entity.entityType}</Text>
|
||||
<Text className={styles.entityName}>
|
||||
{entity.entity.name || entity.entity.sampleNo || entity.entity.id}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{entity ? (
|
||||
<View className={styles.card}>
|
||||
<Text className={styles.title}>现场消毒执行</Text>
|
||||
<Text className={styles.label}>消毒药剂</Text>
|
||||
<Input
|
||||
className={styles.input}
|
||||
value={agent}
|
||||
placeholder="例如:漂白粉"
|
||||
onInput={(e) => setAgent(e.detail.value)}
|
||||
/>
|
||||
<Text className={styles.label}>浓度</Text>
|
||||
<Input
|
||||
className={styles.input}
|
||||
value={concentration}
|
||||
placeholder="例如:1%"
|
||||
onInput={(e) => setConcentration(e.detail.value)}
|
||||
/>
|
||||
<Text className={styles.label}>用量</Text>
|
||||
<Input
|
||||
className={styles.input}
|
||||
value={amount}
|
||||
placeholder="可选"
|
||||
onInput={(e) => setAmount(e.detail.value)}
|
||||
/>
|
||||
<Text className={styles.label}>备注</Text>
|
||||
<Input
|
||||
className={styles.input}
|
||||
value={note}
|
||||
placeholder="可选"
|
||||
onInput={(e) => setNote(e.detail.value)}
|
||||
/>
|
||||
<Button
|
||||
className={styles.btnPrimary}
|
||||
loading={submitting}
|
||||
disabled={submitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
提交消毒记录
|
||||
</Button>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default BiosecurityPage;
|
||||
@@ -59,6 +59,9 @@ const SettingsPage: React.FC = () => {
|
||||
case 'lamp':
|
||||
Taro.navigateTo({ url: '/pages/lamp/index' });
|
||||
break;
|
||||
case 'biosecurity':
|
||||
Taro.navigateTo({ url: '/pages/biosecurity/index' });
|
||||
break;
|
||||
case 'about':
|
||||
Taro.showModal({
|
||||
title: '关于',
|
||||
@@ -190,6 +193,11 @@ const SettingsPage: React.FC = () => {
|
||||
<Text className={styles.menuLabel}>LAMP 检测</Text>
|
||||
<Text className={styles.menuArrow}>›</Text>
|
||||
</View>
|
||||
<View className={styles.menuItem} onClick={() => handleMenuTap('biosecurity')}>
|
||||
<Text className={styles.menuIcon}>🛡️</Text>
|
||||
<Text className={styles.menuLabel}>生物安全</Text>
|
||||
<Text className={styles.menuArrow}>›</Text>
|
||||
</View>
|
||||
<View className={styles.menuItem} onClick={() => handleMenuTap('wsStatus')}>
|
||||
<Text className={styles.menuIcon}>🔗</Text>
|
||||
<Text className={styles.menuLabel}>连接状态</Text>
|
||||
|
||||
Reference in New Issue
Block a user