完善身份证签到
This commit is contained in:
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -255,8 +255,8 @@ export type CustomerDetailResponse = CommonActionResult<OutputCustomerDetail>;
|
|||||||
* 体检中心签到入参
|
* 体检中心签到入参
|
||||||
*/
|
*/
|
||||||
export interface InputMedicalExamCenterSignIn {
|
export interface InputMedicalExamCenterSignIn {
|
||||||
/** 身份证号 */
|
/** 身份证件照片(格式:jpg) */
|
||||||
id_no: string;
|
id_no_pic: File | Blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user