添加iPad签到功能
This commit is contained in:
@@ -12,6 +12,8 @@ import type {
|
|||||||
PhysicalExamProgressDetailResponse,
|
PhysicalExamProgressDetailResponse,
|
||||||
InputCustomerDetail,
|
InputCustomerDetail,
|
||||||
CustomerDetailResponse,
|
CustomerDetailResponse,
|
||||||
|
InputMedicalExamCenterSignIn,
|
||||||
|
PhysicalExamSignInResponse,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,3 +100,15 @@ export const getCustomerDetail = (
|
|||||||
).then(res => res.data);
|
).then(res => res.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iPad 体检中心签到
|
||||||
|
*/
|
||||||
|
export const signInMedicalExamCenter = (
|
||||||
|
data: InputMedicalExamCenterSignIn
|
||||||
|
): Promise<PhysicalExamSignInResponse> => {
|
||||||
|
return request.post<PhysicalExamSignInResponse>(
|
||||||
|
`${MEDICAL_EXAM_BASE_PATH}/sign-in`,
|
||||||
|
data
|
||||||
|
).then(res => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -251,3 +251,24 @@ export interface OutputCustomerDetail {
|
|||||||
*/
|
*/
|
||||||
export type CustomerDetailResponse = CommonActionResult<OutputCustomerDetail>;
|
export type CustomerDetailResponse = CommonActionResult<OutputCustomerDetail>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检中心签到入参
|
||||||
|
*/
|
||||||
|
export interface InputMedicalExamCenterSignIn {
|
||||||
|
/** 身份证号 */
|
||||||
|
id_no: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检中心签到出参
|
||||||
|
*/
|
||||||
|
export interface OutputPhysicalExamSignIn {
|
||||||
|
/** 是否签到成功(0-成功 1-失败) */
|
||||||
|
is_success?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 体检中心签到响应
|
||||||
|
*/
|
||||||
|
export type PhysicalExamSignInResponse = CommonActionResult<OutputPhysicalExamSignIn>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import type { ExamClient, ExamModalTab } from '../../data/mockData';
|
import type { ExamClient, ExamModalTab } from '../../data/mockData';
|
||||||
import type {
|
import type {
|
||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
CustomerInfo,
|
CustomerInfo,
|
||||||
PhysicalExamProgressItem,
|
PhysicalExamProgressItem,
|
||||||
} from '../../api';
|
} from '../../api';
|
||||||
import { getCustomerDetail, getPhysicalExamProgressDetail } from '../../api';
|
import { getCustomerDetail, getPhysicalExamProgressDetail, signInMedicalExamCenter } from '../../api';
|
||||||
import { Button, Input } from '../ui';
|
import { Button, Input } from '../ui';
|
||||||
|
|
||||||
interface ExamModalProps {
|
interface ExamModalProps {
|
||||||
@@ -175,28 +175,104 @@ export const ExamModal = ({ client, tab, onTabChange, onClose }: ExamModalProps)
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ExamSignPanel = () => (
|
const ExamSignPanel = () => {
|
||||||
<div className='grid grid-cols-2 gap-4 text-sm'>
|
const [idNo, setIdNo] = useState('');
|
||||||
<div className='p-4 rounded-2xl border bg-gray-50/60 flex flex-col gap-3'>
|
const [ocrLoading, setOcrLoading] = useState(false);
|
||||||
<div className='font-medium'>身份证上传</div>
|
const [signLoading, setSignLoading] = useState(false);
|
||||||
<div className='text-xs text-gray-500'>支持身份证正反面拍照或读取设备,自动识别姓名、证件号等信息。</div>
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
<div className='flex gap-2 text-xs'>
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
<Button className='py-1.5 px-3'>上传身份证正面</Button>
|
|
||||||
<Button className='py-1.5 px-3'>上传身份证反面</Button>
|
const handlePickFile = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockOcr = async (file: File) => {
|
||||||
|
// 简单模拟 OCR:提取文件名中的数字,或返回示例身份证
|
||||||
|
const match = file.name.match(/\d{6,18}/);
|
||||||
|
await new Promise((r) => setTimeout(r, 600));
|
||||||
|
return match?.[0] || '440101199001010010';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
setOcrLoading(true);
|
||||||
|
setMessage(null);
|
||||||
|
try {
|
||||||
|
const ocrId = await mockOcr(file);
|
||||||
|
setIdNo(ocrId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setMessage('OCR 识别失败,请重试或手动输入');
|
||||||
|
} finally {
|
||||||
|
setOcrLoading(false);
|
||||||
|
e.target.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSign = async () => {
|
||||||
|
const trimmed = idNo.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
setMessage('请输入身份证号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSignLoading(true);
|
||||||
|
setMessage(null);
|
||||||
|
try {
|
||||||
|
const res = await signInMedicalExamCenter({ id_no: trimmed });
|
||||||
|
const ok = res.Status === 200 && res.Data?.is_success === 0;
|
||||||
|
setMessage(ok ? '签到成功' : res.Message || '签到失败');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setMessage('签到请求失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
setSignLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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='font-medium'>身份证扫描与签到</div>
|
||||||
|
<div className='text-xs text-gray-500'>
|
||||||
|
支持扫描身份证识别证号,确认后再点击签到。
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<Button className='py-1.5 px-3' onClick={handlePickFile} disabled={ocrLoading || signLoading}>
|
||||||
|
{ocrLoading ? '识别中...' : '扫描身份证'}
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type='file'
|
||||||
|
accept='image/*'
|
||||||
|
className='hidden'
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder='身份证号'
|
||||||
|
value={idNo}
|
||||||
|
onChange={(e) => setIdNo(e.target.value)}
|
||||||
|
className='flex-1'
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className='py-1.5 px-4'
|
||||||
|
onClick={handleSign}
|
||||||
|
disabled={signLoading}
|
||||||
|
>
|
||||||
|
{signLoading ? '签到中...' : '签到'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{message && <div className='text-xs text-gray-600'>{message}</div>}
|
||||||
|
<div className='text-[11px] text-gray-400'>如识别不准,可手动修改后再签到。</div>
|
||||||
|
</div>
|
||||||
|
<div className='p-4 rounded-2xl border bg-gray-50/60 flex flex-col gap-3'>
|
||||||
|
<div className='font-medium'>体检知情同意书</div>
|
||||||
|
<div className='text-xs text-gray-500'>点击后弹出知情同意书全文及签名区域,签署完成后回到签到页面。</div>
|
||||||
|
<Button className='py-1.5 px-3'>打开知情同意书并签署</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-[11px] text-gray-400'>上传后进入预览界面,确认无误后返回签到界面。</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='p-4 rounded-2xl border bg-gray-50/60 flex flex-col gap-3'>
|
);
|
||||||
<div className='font-medium'>体检知情同意书</div>
|
};
|
||||||
<div className='text-xs text-gray-500'>点击后弹出知情同意书全文及签名区域,签署完成后回到签到页面。</div>
|
|
||||||
{/* <div className='flex gap-2 text-xs text-gray-600'>
|
|
||||||
<Badge>阅读记录</Badge>
|
|
||||||
<Badge>签名图片</Badge>
|
|
||||||
</div> */}
|
|
||||||
<Button className='py-1.5 px-3'>打开知情同意书并签署</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
interface AddonTag {
|
interface AddonTag {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user