|
@@ -0,0 +1,433 @@
|
|
|
|
|
+import React, { useEffect, useRef, useState } from 'react';
|
|
|
|
|
+import { Modal, Button, Space, message, Spin, Upload } from 'antd';
|
|
|
|
|
+import {
|
|
|
|
|
+ CameraOutlined,
|
|
|
|
|
+ CloseOutlined,
|
|
|
|
|
+ ReloadOutlined,
|
|
|
|
|
+ UploadOutlined,
|
|
|
|
|
+ IdcardOutlined,
|
|
|
|
|
+} from '@ant-design/icons';
|
|
|
|
|
+import { CameraServiceFactory } from '@/services/camera/CameraServiceFactory';
|
|
|
|
|
+import axiosInstance from '@/API/interceptor';
|
|
|
|
|
+
|
|
|
|
|
+interface IDCardRecognitionModalProps {
|
|
|
|
|
+ visible: boolean;
|
|
|
|
|
+ onClose: () => void;
|
|
|
|
|
+ onRecognitionSuccess: (data: IDCardData) => void;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface IDCardData {
|
|
|
|
|
+ name?: string;
|
|
|
|
|
+ idNumber?: string;
|
|
|
|
|
+ gender?: string;
|
|
|
|
|
+ birthDate?: string;
|
|
|
|
|
+ address?: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 身份证识别 Modal 组件
|
|
|
|
|
+ * 支持自动拍照和手动上传图片两种方式
|
|
|
|
|
+ */
|
|
|
|
|
+const IDCardRecognitionModal: React.FC<IDCardRecognitionModalProps> = ({
|
|
|
|
|
+ visible,
|
|
|
|
|
+ onClose,
|
|
|
|
|
+ onRecognitionSuccess,
|
|
|
|
|
+}) => {
|
|
|
|
|
+ const videoRef = useRef<HTMLVideoElement>(null);
|
|
|
|
|
+ const streamRef = useRef<MediaStream | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+ const [capturedImage, setCapturedImage] = useState<string | null>(null);
|
|
|
|
|
+ const [uploadedImage, setUploadedImage] = useState<string | null>(null);
|
|
|
|
|
+ const [isInitializing, setIsInitializing] = useState(false);
|
|
|
|
|
+ const [isRecognizing, setIsRecognizing] = useState(false);
|
|
|
|
|
+ const [mode, setMode] = useState<'camera' | 'upload'>('camera');
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 初始化摄像头
|
|
|
|
|
+ */
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (visible && mode === 'camera' && !capturedImage) {
|
|
|
|
|
+ initCamera();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ stopCamera();
|
|
|
|
|
+ };
|
|
|
|
|
+ }, [visible, mode, capturedImage]);
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 初始化摄像头
|
|
|
|
|
+ */
|
|
|
|
|
+ const initCamera = async () => {
|
|
|
|
|
+ setIsInitializing(true);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ console.log('[IDCardRecognition] 初始化摄像头...');
|
|
|
|
|
+
|
|
|
|
|
+ const cameraService = CameraServiceFactory.create();
|
|
|
|
|
+
|
|
|
|
|
+ const hasPermission = await cameraService.requestPermission();
|
|
|
|
|
+ if (!hasPermission) {
|
|
|
|
|
+ throw new Error('用户拒绝了摄像头权限');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const stream = await cameraService.getMediaStream({
|
|
|
|
|
+ video: {
|
|
|
|
|
+ width: { ideal: 1280 },
|
|
|
|
|
+ height: { ideal: 720 },
|
|
|
|
|
+ facingMode: 'environment', // 使用后置摄像头
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ streamRef.current = stream;
|
|
|
|
|
+
|
|
|
|
|
+ if (videoRef.current) {
|
|
|
|
|
+ videoRef.current.onloadedmetadata = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (videoRef.current) {
|
|
|
|
|
+ await videoRef.current.play();
|
|
|
|
|
+ console.log('[IDCardRecognition] 视频开始播放');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (playError: any) {
|
|
|
|
|
+ console.error('[IDCardRecognition] 视频播放失败:', playError);
|
|
|
|
|
+ message.error('视频播放失败: ' + playError.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ videoRef.current.srcObject = stream;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[IDCardRecognition] 摄像头初始化成功');
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error('[IDCardRecognition] 摄像头初始化失败:', err);
|
|
|
|
|
+ message.error(err.message || '摄像头初始化失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setIsInitializing(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 停止摄像头
|
|
|
|
|
+ */
|
|
|
|
|
+ const stopCamera = () => {
|
|
|
|
|
+ if (streamRef.current) {
|
|
|
|
|
+ console.log('[IDCardRecognition] 停止摄像头');
|
|
|
|
|
+ streamRef.current.getTracks().forEach((track) => track.stop());
|
|
|
|
|
+ streamRef.current = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 拍照
|
|
|
|
|
+ */
|
|
|
|
|
+ const handleCapture = async () => {
|
|
|
|
|
+ if (!streamRef.current) {
|
|
|
|
|
+ message.error('摄像头未就绪');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ console.log('[IDCardRecognition] 开始拍照...');
|
|
|
|
|
+
|
|
|
|
|
+ const cameraService = CameraServiceFactory.create();
|
|
|
|
|
+ const base64Image = await cameraService.capturePhoto(streamRef.current);
|
|
|
|
|
+
|
|
|
|
|
+ console.log(
|
|
|
|
|
+ '[IDCardRecognition] 拍照成功,图片大小:',
|
|
|
|
|
+ base64Image.length
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ setCapturedImage(base64Image);
|
|
|
|
|
+ stopCamera();
|
|
|
|
|
+
|
|
|
|
|
+ message.success('拍照成功');
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error('[IDCardRecognition] 拍照失败:', err);
|
|
|
|
|
+ message.error(err.message || '拍照失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 重拍
|
|
|
|
|
+ */
|
|
|
|
|
+ const handleRetake = () => {
|
|
|
|
|
+ console.log('[IDCardRecognition] 重拍');
|
|
|
|
|
+ setCapturedImage(null);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上传文件处理
|
|
|
|
|
+ */
|
|
|
|
|
+ const handleUpload = (file: File) => {
|
|
|
|
|
+ const reader = new FileReader();
|
|
|
|
|
+ reader.onload = (e) => {
|
|
|
|
|
+ const result = e.target?.result as string;
|
|
|
|
|
+ setUploadedImage(result);
|
|
|
|
|
+ setCapturedImage(null);
|
|
|
|
|
+ message.success('图片上传成功');
|
|
|
|
|
+ };
|
|
|
|
|
+ reader.readAsDataURL(file);
|
|
|
|
|
+ return false; // 阻止自动上传
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 识别身份证
|
|
|
|
|
+ */
|
|
|
|
|
+ const handleRecognize = async () => {
|
|
|
|
|
+ const imageData = capturedImage || uploadedImage;
|
|
|
|
|
+ if (!imageData) {
|
|
|
|
|
+ message.error('请先拍照或上传身份证图片');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ setIsRecognizing(true);
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[IDCardRecognition] 开始识别身份证...');
|
|
|
|
|
+
|
|
|
|
|
+ // 上传图片到后端进行识别
|
|
|
|
|
+ const response = await axiosInstance.post('/auth/task/ocr/id_card', {
|
|
|
|
|
+ type: 'png',
|
|
|
|
|
+ data: imageData.replace(/^data:image\/[a-z]+;base64,/, ''),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.data.code !== '0x000000') {
|
|
|
|
|
+ throw new Error(response.data.description || '识别失败');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const recognitionData: IDCardData = response?.data?.data;
|
|
|
|
|
+
|
|
|
|
|
+ console.log('[IDCardRecognition] 识别成功:', recognitionData);
|
|
|
|
|
+
|
|
|
|
|
+ message.success('身份证识别成功');
|
|
|
|
|
+ await onRecognitionSuccess(recognitionData);
|
|
|
|
|
+ handleClose();
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error('[IDCardRecognition] 识别失败:', err);
|
|
|
|
|
+ message.error(
|
|
|
|
|
+ err.response?.data?.description || err.message || '识别失败'
|
|
|
|
|
+ );
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setIsRecognizing(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 关闭 Modal
|
|
|
|
|
+ */
|
|
|
|
|
+ const handleClose = () => {
|
|
|
|
|
+ console.log('[IDCardRecognition] 关闭 Modal');
|
|
|
|
|
+ stopCamera();
|
|
|
|
|
+ setCapturedImage(null);
|
|
|
|
|
+ setUploadedImage(null);
|
|
|
|
|
+ onClose();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const uploadProps = {
|
|
|
|
|
+ beforeUpload: handleUpload,
|
|
|
|
|
+ showUploadList: false,
|
|
|
|
|
+ accept: 'image/*',
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Modal
|
|
|
|
|
+ title={
|
|
|
|
|
+ <Space>
|
|
|
|
|
+ <IdcardOutlined />
|
|
|
|
|
+ 身份证识别
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ }
|
|
|
|
|
+ open={visible}
|
|
|
|
|
+ onCancel={handleClose}
|
|
|
|
|
+ footer={null}
|
|
|
|
|
+ width={800}
|
|
|
|
|
+ centered
|
|
|
|
|
+ destroyOnClose
|
|
|
|
|
+ >
|
|
|
|
|
+ <div style={{ textAlign: 'center' }}>
|
|
|
|
|
+ {/* 模式选择 */}
|
|
|
|
|
+ <Space style={{ marginBottom: 16 }}>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type={mode === 'camera' ? 'primary' : 'default'}
|
|
|
|
|
+ onClick={() => setMode('camera')}
|
|
|
|
|
+ >
|
|
|
|
|
+ 自动拍照
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type={mode === 'upload' ? 'primary' : 'default'}
|
|
|
|
|
+ onClick={() => setMode('upload')}
|
|
|
|
|
+ >
|
|
|
|
|
+ 手动上传
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 摄像头模式 */}
|
|
|
|
|
+ {mode === 'camera' && (
|
|
|
|
|
+ <div style={{ position: 'relative' }}>
|
|
|
|
|
+ {!capturedImage && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <video
|
|
|
|
|
+ ref={videoRef}
|
|
|
|
|
+ autoPlay
|
|
|
|
|
+ playsInline
|
|
|
|
|
+ muted
|
|
|
|
|
+ style={{
|
|
|
|
|
+ width: '100%',
|
|
|
|
|
+ maxHeight: '400px',
|
|
|
|
|
+ backgroundColor: '#000',
|
|
|
|
|
+ borderRadius: 8,
|
|
|
|
|
+ display: isInitializing ? 'none' : 'block',
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ {isInitializing && (
|
|
|
|
|
+ <div
|
|
|
|
|
+ style={{
|
|
|
|
|
+ width: '100%',
|
|
|
|
|
+ height: '400px',
|
|
|
|
|
+ display: 'flex',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ backgroundColor: '#000',
|
|
|
|
|
+ borderRadius: 8,
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Spin size="large" tip="正在初始化摄像头..." />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {!isInitializing && (
|
|
|
|
|
+ <Space style={{ marginTop: 16 }} size="large">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ size="large"
|
|
|
|
|
+ icon={<CameraOutlined />}
|
|
|
|
|
+ onClick={handleCapture}
|
|
|
|
|
+ >
|
|
|
|
|
+ 拍照
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ size="large"
|
|
|
|
|
+ icon={<CloseOutlined />}
|
|
|
|
|
+ onClick={handleClose}
|
|
|
|
|
+ >
|
|
|
|
|
+ 取消
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {capturedImage && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <img
|
|
|
|
|
+ src={capturedImage}
|
|
|
|
|
+ alt="拍摄的身份证"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ width: '100%',
|
|
|
|
|
+ maxHeight: '400px',
|
|
|
|
|
+ borderRadius: 8,
|
|
|
|
|
+ objectFit: 'contain',
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Space style={{ marginTop: 16 }} size="large">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ size="large"
|
|
|
|
|
+ icon={<IdcardOutlined />}
|
|
|
|
|
+ onClick={handleRecognize}
|
|
|
|
|
+ loading={isRecognizing}
|
|
|
|
|
+ >
|
|
|
|
|
+ 识别身份证
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ size="large"
|
|
|
|
|
+ icon={<ReloadOutlined />}
|
|
|
|
|
+ onClick={handleRetake}
|
|
|
|
|
+ disabled={isRecognizing}
|
|
|
|
|
+ >
|
|
|
|
|
+ 重拍
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ size="large"
|
|
|
|
|
+ icon={<CloseOutlined />}
|
|
|
|
|
+ onClick={handleClose}
|
|
|
|
|
+ disabled={isRecognizing}
|
|
|
|
|
+ >
|
|
|
|
|
+ 取消
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 上传模式 */}
|
|
|
|
|
+ {mode === 'upload' && (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ {!uploadedImage ? (
|
|
|
|
|
+ <div
|
|
|
|
|
+ style={{
|
|
|
|
|
+ padding: '40px',
|
|
|
|
|
+ border: '2px dashed #d9d9d9',
|
|
|
|
|
+ borderRadius: 8,
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Upload {...uploadProps}>
|
|
|
|
|
+ <Space direction="vertical">
|
|
|
|
|
+ <UploadOutlined
|
|
|
|
|
+ style={{ fontSize: 48, color: '#1890ff' }}
|
|
|
|
|
+ />
|
|
|
|
|
+ <div>点击或拖拽身份证图片到此处上传</div>
|
|
|
|
|
+ <div style={{ color: '#999' }}>只支持 PNG 格式</div>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ </Upload>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <img
|
|
|
|
|
+ src={uploadedImage}
|
|
|
|
|
+ alt="上传的身份证"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ width: '100%',
|
|
|
|
|
+ maxHeight: '400px',
|
|
|
|
|
+ borderRadius: 8,
|
|
|
|
|
+ objectFit: 'contain',
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Space style={{ marginTop: 16 }} size="large">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ size="large"
|
|
|
|
|
+ icon={<IdcardOutlined />}
|
|
|
|
|
+ onClick={handleRecognize}
|
|
|
|
|
+ loading={isRecognizing}
|
|
|
|
|
+ >
|
|
|
|
|
+ 识别身份证
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ size="large"
|
|
|
|
|
+ onClick={() => setUploadedImage(null)}
|
|
|
|
|
+ disabled={isRecognizing}
|
|
|
|
|
+ >
|
|
|
|
|
+ 重新上传
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ size="large"
|
|
|
|
|
+ icon={<CloseOutlined />}
|
|
|
|
|
+ onClick={handleClose}
|
|
|
|
|
+ disabled={isRecognizing}
|
|
|
|
|
+ >
|
|
|
|
|
+ 取消
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Modal>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+export default IDCardRecognitionModal;
|