完善身份证签到

This commit is contained in:
xianyi
2025-12-15 10:26:59 +08:00
parent 08382a0794
commit 6ccf5fb2f7
3 changed files with 117 additions and 36 deletions

View File

@@ -110,9 +110,17 @@ export const getCustomerDetail = (
export const signInMedicalExamCenter = ( export const signInMedicalExamCenter = (
data: InputMedicalExamCenterSignIn data: InputMedicalExamCenterSignIn
): Promise<PhysicalExamSignInResponse> => { ): Promise<PhysicalExamSignInResponse> => {
const formData = new FormData();
formData.append('id_no_pic', data.id_no_pic);
return request.post<PhysicalExamSignInResponse>( return request.post<PhysicalExamSignInResponse>(
`${MEDICAL_EXAM_BASE_PATH}/sign-in`, `${MEDICAL_EXAM_BASE_PATH}/sign-in`,
data formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
).then(res => res.data); ).then(res => res.data);
}; };

View File

@@ -255,8 +255,8 @@ export type CustomerDetailResponse = CommonActionResult<OutputCustomerDetail>;
* 体检中心签到入参 * 体检中心签到入参
*/ */
export interface InputMedicalExamCenterSignIn { export interface InputMedicalExamCenterSignIn {
/** 身份证 */ /** 身份证件照片格式jpg */
id_no: string; id_no_pic: File | Blob;
} }
/** /**

View File

@@ -178,8 +178,9 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps)
}; };
const ExamSignPanel = ({ examId }: { examId?: number }) => { const ExamSignPanel = ({ examId }: { examId?: number }) => {
const [idNo, setIdNo] = useState(''); const [idCardFile, setIdCardFile] = useState<File | null>(null);
const [ocrLoading, setOcrLoading] = useState(false); const [previewImage, setPreviewImage] = useState<string | null>(null);
const [showImagePreview, setShowImagePreview] = useState(false);
const [signLoading, setSignLoading] = useState(false); const [signLoading, setSignLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null); const [message, setMessage] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null); const fileInputRef = useRef<HTMLInputElement | null>(null);
@@ -214,40 +215,78 @@ const ExamSignPanel = ({ examId }: { examId?: number }) => {
} }
}, []); }, []);
const mockOcr = async (file: File) => { const convertToJpg = async (file: File): Promise<File> => {
// 简单模拟 OCR提取文件名中的数字或返回示例身份证 return new Promise((resolve, reject) => {
const match = file.name.match(/\d{6,18}/); const reader = new FileReader();
await new Promise((r) => setTimeout(r, 600)); reader.onload = (event) => {
return match?.[0] || '440101199001010010'; const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('无法创建画布上下文'));
return;
}
ctx.drawImage(img, 0, 0);
canvas.toBlob(
(blob) => {
if (!blob) {
reject(new Error('图片转换失败'));
return;
}
const jpgFile = new File([blob], 'id_card.jpg', { type: 'image/jpeg' });
resolve(jpgFile);
},
'image/jpeg',
0.92 // 质量 92%
);
};
img.onerror = () => reject(new Error('图片加载失败'));
img.src = event.target?.result as string;
};
reader.onerror = () => reject(new Error('文件读取失败'));
reader.readAsDataURL(file);
});
}; };
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (!file) return; if (!file) return;
setOcrLoading(true);
setMessage(null); setMessage(null);
try { try {
const ocrId = await mockOcr(file); // 转换为 JPG 格式
setIdNo(ocrId); const jpgFile = await convertToJpg(file);
// 保存文件用于签到
setIdCardFile(jpgFile);
// 生成预览图
const reader = new FileReader();
reader.onload = (event) => {
setPreviewImage(event.target?.result as string);
};
reader.readAsDataURL(jpgFile);
} catch (err) { } catch (err) {
console.error(err); console.error('图片转换失败', err);
setMessage('OCR 识别失败,请重试或手动输入'); setMessage('图片处理失败,请重试');
} finally {
setOcrLoading(false);
e.target.value = '';
} }
e.target.value = '';
}; };
const handleSign = async () => { const handleSign = async () => {
const trimmed = idNo.trim(); if (!idCardFile) {
if (!trimmed) { setMessage('请先上传身份证照片');
setMessage('请输入身份证号');
return; return;
} }
setSignLoading(true); setSignLoading(true);
setMessage(null); setMessage(null);
try { try {
const res = await signInMedicalExamCenter({ id_no: trimmed }); const res = await signInMedicalExamCenter({ id_no_pic: idCardFile });
const ok = res.Status === 200 && res.Data?.is_success === 0; const ok = res.Status === 200 && res.Data?.is_success === 0;
setMessage(ok ? '签到成功' : res.Message || '签到失败'); setMessage(ok ? '签到成功' : res.Message || '签到失败');
} catch (err) { } catch (err) {
@@ -339,38 +378,72 @@ const ExamSignPanel = ({ examId }: { examId?: number }) => {
return ( return (
<div className='grid grid-cols-2 gap-4 text-sm'> <div className='grid grid-cols-2 gap-4 text-sm'>
<div className='p-4 rounded-2xl border bg-gray-50/60 flex flex-col gap-3'> <div className='p-4 rounded-2xl border bg-gray-50/60 flex flex-col gap-3'>
<div className='font-medium'></div> <div className='font-medium'></div>
<div className='text-xs text-gray-500'> <div className='text-xs text-gray-500'>
</div> </div>
<div className='flex items-center gap-3'> <div className='flex items-center gap-3'>
<Button className='py-1.5 px-3' onClick={handlePickFile} disabled={ocrLoading || signLoading}> <Button
{ocrLoading ? '识别中...' : '扫描身份证'} className='py-1.5 px-3'
onClick={handlePickFile}
disabled={signLoading}
>
{idCardFile ? '重新拍照' : '拍照'}
</Button> </Button>
<input <input
ref={fileInputRef} ref={fileInputRef}
type='file' type='file'
accept='image/*' accept='image/*'
capture='environment'
className='hidden' className='hidden'
onChange={handleFileChange} onChange={handleFileChange}
/> />
<Input {previewImage && (
placeholder='身份证号' <div
value={idNo} className='w-16 h-16 rounded-lg border-2 border-gray-300 overflow-hidden cursor-pointer hover:border-blue-500 transition-colors'
onChange={(e) => setIdNo(e.target.value)} onClick={() => setShowImagePreview(true)}
className='flex-1' >
/> <img
src={previewImage}
alt='身份证预览'
className='w-full h-full object-cover'
/>
</div>
)}
<Button <Button
className='py-1.5 px-4' className='py-1.5 px-4 flex-1'
onClick={handleSign} onClick={handleSign}
disabled={signLoading} disabled={signLoading || !idCardFile}
> >
{signLoading ? '签到中...' : '签到'} {signLoading ? '签到中...' : '签到'}
</Button> </Button>
</div> </div>
{message && <div className='text-xs text-gray-600'>{message}</div>} {message && (
<div className='text-[11px] text-gray-400'></div> <div className={`text-xs ${message.includes('成功') ? 'text-green-600' : 'text-amber-600'}`}>
{message}
</div>
)}
</div> </div>
{showImagePreview && previewImage && (
<div
className='fixed inset-0 z-[80] bg-black/90 flex items-center justify-center p-6'
onClick={() => setShowImagePreview(false)}
>
<div className='relative max-w-full max-h-full'>
<img
src={previewImage}
alt='身份证预览'
className='max-w-full max-h-[90vh] object-contain'
/>
<Button
className='absolute top-4 right-4 bg-white/90 hover:bg-white'
onClick={() => setShowImagePreview(false)}
>
</Button>
</div>
</div>
)}
<div className='p-4 rounded-2xl border bg-gray-50/60 flex flex-col gap-3'> <div className='p-4 rounded-2xl border bg-gray-50/60 flex flex-col gap-3'>
<div className='font-medium'></div> <div className='font-medium'></div>
<div className='text-xs text-gray-500'></div> <div className='text-xs text-gray-500'></div>