import React, { useState, useRef } from 'react';
|
import { Button, message } from 'antd';
|
import * as $$ from '@/utils/utility';
|
import { Form, Input, Modal, Upload } from '@arco-design/web-react';
|
import { IconLink } from '@arco-design/web-react/icon';
|
import './index.less'
|
import ReactCrop, { centerCrop, makeAspectCrop } from 'react-image-crop';
|
import 'react-image-crop/dist/ReactCrop.css';
|
import { request, ax } from '@/api/apiHandler';
|
|
const FormItem = Form.Item;
|
const appUrl = $$.appUrl;
|
|
|
const DocumentScanner = ({
|
visible,
|
onConfirm,
|
onCancel
|
|
}) => {
|
const formRef = useRef();
|
const [scanFile, setScanFile] = useState(false);
|
const [scanImage, setScanImage] = useState(false);
|
const [scaned, setScaned] = useState(false);
|
const [fileView, setFileView] = useState();
|
const [scanContent, setScanContent] = useState('');
|
const [crop, setCrop] = useState(null);
|
const [completedCrop, setCompletedCrop] = useState(null);
|
const imgRef = useRef(null);
|
const canvasRef = useRef(null);
|
const [loading, setLoading] = useState(false);
|
const [imgDimensions, setImgDimensions] = useState({
|
width: 0,
|
height: 0,
|
naturalWidth: 0,
|
naturalHeight: 0
|
});
|
|
const handleUploadChange = (info, currentFile) => {
|
if (info.length > 0) {
|
setScanImage(true);
|
setCrop(null);
|
setCompletedCrop(null);
|
}
|
setFileView({
|
...currentFile,
|
url: URL.createObjectURL(currentFile.originFile),
|
});
|
};
|
|
const handleScaned = async () => {
|
if (!imgRef.current) {
|
message.error('图片加载失败');
|
return;
|
}
|
|
setLoading(true);
|
|
try {
|
const canvas = document.createElement('canvas');
|
const ctx = canvas.getContext('2d');
|
|
// 检查是否有有效的裁剪区域
|
const hasSelectedRegion = completedCrop &&
|
typeof completedCrop.width === 'number' &&
|
typeof completedCrop.height === 'number' &&
|
completedCrop.width > 0 &&
|
completedCrop.height > 0;
|
|
console.log('hasSelectedRegion', hasSelectedRegion, completedCrop);
|
|
if (hasSelectedRegion) {
|
// 获取图片元素
|
const img = imgRef.current;
|
|
// 获取滚动容器
|
const scrollContainer = img.closest('div[style*="overflow: auto"]');
|
|
// 使用存储的图片尺寸(可能更准确)
|
const { naturalWidth, naturalHeight, width, height } = imgDimensions.width > 0
|
? imgDimensions
|
: {
|
naturalWidth: img.naturalWidth,
|
naturalHeight: img.naturalHeight,
|
width: img.width,
|
height: img.height
|
};
|
|
// 计算比例
|
let scaleX = naturalWidth / width;
|
let scaleY = naturalHeight / height;
|
|
// 获取滚动位置
|
const scrollTop = scrollContainer ? scrollContainer.scrollTop : 0;
|
console.log('Scroll position:', scrollTop);
|
|
// 直接使用ReactCrop提供的坐标,不再额外计算偏移
|
// ReactCrop已经处理了相对于图片的坐标
|
const cropX = completedCrop.x;
|
const cropY = completedCrop.y;
|
const cropWidth = completedCrop.width;
|
const cropHeight = completedCrop.height;
|
|
console.log('Crop values:', cropX, cropY, cropWidth, cropHeight);
|
|
// 将裁剪坐标转换为原始图片上的坐标
|
const sourceX = Math.round(cropX * scaleX);
|
const sourceY = Math.round(cropY * scaleY);
|
const sourceWidth = Math.round(cropWidth * scaleX);
|
const sourceHeight = Math.round(cropHeight * scaleY);
|
|
console.log('Final crop coordinates:', sourceX, sourceY, sourceWidth, sourceHeight);
|
|
// 设置canvas尺寸
|
canvas.width = sourceWidth;
|
canvas.height = sourceHeight;
|
|
// 绘制裁剪区域到canvas
|
ctx.drawImage(
|
img,
|
sourceX, sourceY,
|
sourceWidth, sourceHeight,
|
0, 0,
|
sourceWidth, sourceHeight
|
);
|
} else {
|
// 使用整张图片
|
const sourceWidth = imgRef.current.naturalWidth;
|
const sourceHeight = imgRef.current.naturalHeight;
|
|
canvas.width = sourceWidth;
|
canvas.height = sourceHeight;
|
|
ctx.drawImage(
|
imgRef.current,
|
0, 0,
|
sourceWidth, sourceHeight,
|
0, 0,
|
sourceWidth, sourceHeight
|
);
|
}
|
|
canvas.toBlob(async (blob) => {
|
if (!blob) {
|
message.error('图片处理失败');
|
setLoading(false);
|
return;
|
}
|
console.log('blob',blob)
|
const formData = new FormData();
|
formData.append('file', blob, 'image-to-recognize.png');
|
|
try {
|
// 使用axios直接发送请求
|
const token = $$.getSessionStorage('customerSystemToken');
|
const url = `${$$.appUrl.fileUrl}/${$$.appUrl.sys}/api/web/fileInfo/recognitionText`;
|
|
const res = await ax.post(url, formData, {
|
headers: {
|
Authorization: token,
|
'Content-Type': 'multipart/form-data'
|
}
|
});
|
|
if (res.data && (res.data.code === 0 || res.data.code === 200)) {
|
// 提取识别的文本内容
|
let recognizedText = '';
|
if (res.data.data && res.data.data.ocrResult && res.data.data.ocrResult.wordsResult) {
|
recognizedText = res.data.data.ocrResult.wordsResult.join('\n');
|
}
|
|
// 设置识别内容
|
setScanContent(recognizedText);
|
|
// 在表单中设置识别内容
|
setTimeout(() => {
|
if (formRef.current) {
|
formRef.current.setFieldValue('scanContent', recognizedText);
|
}
|
}, 0);
|
|
message.success('识别成功');
|
|
// 打开结果弹窗,关闭识别区域选择弹窗
|
setScaned(true); // 打开识别结果弹窗
|
} else {
|
message.error(res.data?.msg || '识别失败');
|
}
|
} catch (error) {
|
console.error('识别错误:', error);
|
message.error('识别过程中发生错误');
|
} finally {
|
setLoading(false);
|
}
|
}, 'image/png');
|
} catch (err) {
|
console.error('处理图片错误:', err);
|
message.error('处理图片过程中发生错误');
|
setLoading(false);
|
}
|
};
|
|
const handleText = () => {
|
onConfirm(scanContent);
|
setScanFile(false);
|
setScanImage(false);
|
setScaned(false);
|
}
|
|
const onImageLoad = (e) => {
|
const { width, height, naturalWidth, naturalHeight } = e.currentTarget;
|
console.log('Image loaded:', width, height);
|
console.log('Natural size:', naturalWidth, naturalHeight);
|
setImgDimensions({
|
width,
|
height,
|
naturalWidth,
|
naturalHeight
|
});
|
|
// 解决图片加载后可能的坐标计算问题
|
if (crop) {
|
setCrop(null);
|
setCompletedCrop(null);
|
}
|
};
|
|
return (
|
<>
|
|
<Modal
|
visible={scanImage}
|
onCancel={() => {
|
// 当正在加载时,阻止关闭弹窗
|
if (!loading) {
|
setScanImage(false);
|
} else {
|
message.warning('识别进行中,请勿关闭窗口');
|
}
|
}}
|
footer={null}
|
title='选择识别范围'
|
centered
|
unmountOnExit={true}
|
maskClosable={!loading} // 识别中禁用点击遮罩关闭
|
style={{ maxWidth: '90%', minWidth: '600px' }}
|
className="image-crop-modal"
|
closable={!loading} // 识别中禁用关闭按钮
|
>
|
<div style={{ position: 'relative' }}>
|
<div style={{ marginBottom: '10px', color: '#666' }}>
|
提示:默认将识别整张图片,您也可以在图片上拖动鼠标选择需要识别的特定区域。当图片较长时,可以滚动查看完整图片。
|
</div>
|
{/* 将固定高度容器改为可滚动容器 */}
|
<div style={{
|
height: 'calc(100vh - 270px)',
|
position: 'relative',
|
display: 'flex',
|
justifyContent: 'center',
|
alignItems: 'flex-start',
|
overflow: 'auto' /* 改为auto允许滚动 */
|
}}>
|
{/* 识别中的遮罩层 */}
|
{loading && (
|
<div style={{
|
position: 'absolute',
|
top: 0,
|
left: 0,
|
right: 0,
|
bottom: 0,
|
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
zIndex: 10,
|
display: 'flex',
|
justifyContent: 'center',
|
alignItems: 'center',
|
flexDirection: 'column'
|
}}>
|
<div style={{
|
color: '#fff',
|
fontSize: '16px',
|
marginBottom: '10px'
|
}}>识别中,请稍候...</div>
|
</div>
|
)}
|
<div style={{
|
position: 'relative',
|
width: '100%',
|
display: 'flex',
|
justifyContent: 'center',
|
padding: '10px 20px 30px 20px'
|
}}>
|
<div style={{
|
position: 'relative',
|
display: 'inline-block',
|
boxShadow: '0 0 5px rgba(0,0,0,0.1)'
|
}}>
|
<ReactCrop
|
crop={crop}
|
onChange={(_, percentCrop) => setCrop(percentCrop)}
|
onComplete={(c) => {
|
console.log('Completed crop:', c);
|
setCompletedCrop(c);
|
}}
|
style={{
|
maxWidth: '100%',
|
display: 'block'
|
}}
|
disabled={false}
|
circularCrop={false}
|
className="scan-image-crop"
|
keepSelection={true}
|
ruleOfThirds={true}
|
minWidth={10}
|
minHeight={10}
|
crossorigin="anonymous"
|
renderSelectionAddon={() => null}
|
onDragStart={(e) => {
|
// 阻止父容器的滚动事件
|
e.preventDefault();
|
e.stopPropagation();
|
}}
|
onDragEnd={() => {
|
// 确保裁剪完成后重新计算
|
if (imgRef.current) {
|
const { width, height, naturalWidth, naturalHeight } = imgRef.current;
|
setImgDimensions({
|
width,
|
height,
|
naturalWidth,
|
naturalHeight
|
});
|
}
|
}}
|
>
|
<img
|
ref={imgRef}
|
src={fileView?.url}
|
alt=""
|
style={{
|
maxWidth: '100%',
|
objectFit: 'contain'
|
}}
|
onLoad={onImageLoad}
|
/>
|
</ReactCrop>
|
</div>
|
</div>
|
</div>
|
<div style={{ position: 'relative', bottom: '0', width: "100%", textAlign: 'left', marginTop: '15px', paddingLeft: '20px' }}>
|
<Button
|
type="primary"
|
onClick={() => handleScaned()}
|
style={{ marginTop: '5px' }}
|
loading={loading}
|
>
|
{completedCrop &&
|
typeof completedCrop.width === 'number' &&
|
typeof completedCrop.height === 'number' &&
|
completedCrop.width > 0 &&
|
completedCrop.height > 0
|
? '识别选中区域'
|
: '识别全图'
|
}
|
</Button>
|
<Button
|
onClick={() => {
|
setCrop(null);
|
setCompletedCrop(null);
|
}}
|
style={{ marginTop: '5px', marginLeft: '10px' }}
|
disabled={loading}
|
>
|
重置选择
|
</Button>
|
</div>
|
</div>
|
</Modal>
|
<Modal
|
// style={{ width: '1200px' }}
|
visible={visible}
|
onCancel={onCancel}
|
title='识别上传材料'
|
centered
|
footer={null}
|
unmountOnExit={true}
|
maskClosable={false}
|
>
|
<Form
|
layout='vertical'
|
requiredSymbol={false}
|
initialValues={{
|
}}//默认值
|
style={{ marginTop: '4px' }}
|
>
|
<FormItem
|
label='选择图片'
|
field='file'
|
>
|
<Upload
|
drag
|
// multiple
|
limit={1}
|
accept='image/*'
|
headers={{ Authorization: $$.getSessionStorage('customerSystemToken') }}
|
// action={`${appUrl.fileUrl}/${appUrl.sys}/api/web/fileInfo/recognitionText`}
|
onDrop={(e) => {
|
}}
|
tip='支持png、 jpg、pdf等格式文件上传,每次上传大小不超过10M'
|
showUploadList={{
|
fileIcon: <IconLink style={{ color: '#1D2129' }} />,
|
}}
|
// onChange={(info, currentFile) => {
|
// console.log(currentFile, info, 'info', 'currentFile')
|
// if (info.length > 0) {
|
// setScanImage(true);
|
// }
|
// setFileView({
|
// ...currentFile,
|
// url: URL.createObjectURL(currentFile.originFile),
|
|
// });
|
|
// }}
|
onChange={handleUploadChange}
|
// onSuccess={() => setScanImage(true)}
|
/>
|
{/* <img src={file?.url} alt=""/> */}
|
</FormItem>
|
|
</Form>
|
|
</Modal >
|
<Modal
|
className='scan-modal'
|
// style={{ width: '1200px' }}
|
wrapStyle={{ zIndex: 1002 }}
|
visible={scaned}
|
onCancel={() => setScaned(false)}
|
footer={null}
|
title='识别上传材料'
|
centered
|
unmountOnExit={true}
|
maskClosable={false}
|
mountOnEnter={false}
|
>
|
<Form
|
ref={formRef}
|
layout='vertical'
|
requiredSymbol={false}
|
scrollToFirstError={true}
|
initialValues={{
|
}}//默认值
|
>
|
<FormItem
|
label='识别内容'
|
field='scanContent'
|
>
|
<Input.TextArea
|
showWordLimit
|
rows={10}
|
placeholder=''
|
wrapperStyle={{ width: '100%' }}
|
onChange={(v) => console.log(v, 'vvvvvv')}
|
/>
|
</FormItem>
|
<div style={{ marginTop: '24px' }}><Button type="primary" onClick={() => { handleText() }}>使用文字</Button></div>
|
</Form>
|
</Modal>
|
|
</>
|
);
|
};
|
|
export default DocumentScanner;
|