feat: support upload cover for people
- wrappe the post and delete people image api, and upload image api - add cover preview when register and edit - support upload image when edit cover of people in edit and register
This commit is contained in:
@@ -17,7 +17,9 @@ export const API_ENDPOINTS = {
|
|||||||
// 新增单个资源路径 /people
|
// 新增单个资源路径 /people
|
||||||
PEOPLE: '/people',
|
PEOPLE: '/people',
|
||||||
PEOPLE_BY_ID: (id: string) => `/people/${id}`,
|
PEOPLE_BY_ID: (id: string) => `/people/${id}`,
|
||||||
|
PEOPLE_IMAGE_BY_ID: (id: string) => `/people/${id}/image`,
|
||||||
PEOPLE_REMARK_BY_ID: (id: string) => `/people/${id}/remark`,
|
PEOPLE_REMARK_BY_ID: (id: string) => `/people/${id}/remark`,
|
||||||
|
UPLOAD_IMAGE: '/upload/image',
|
||||||
// 用户相关
|
// 用户相关
|
||||||
SEND_CODE: '/user/send_code',
|
SEND_CODE: '/user/send_code',
|
||||||
REGISTER: '/user',
|
REGISTER: '/user',
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// 人员管理相关 API
|
// 人员管理相关 API
|
||||||
|
|
||||||
import { get, post, del, put } from './request';
|
import { get, post, del, put, upload } from './request';
|
||||||
import { API_ENDPOINTS } from './config';
|
import { API_ENDPOINTS } from './config';
|
||||||
import type {
|
import type {
|
||||||
PostPeopleRequest,
|
PostPeopleRequest,
|
||||||
GetPeoplesParams,
|
GetPeoplesParams,
|
||||||
People,
|
People,
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
PaginatedResponse
|
PaginatedResponse
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,6 +140,25 @@ export async function deleteRemark(peopleId: string): Promise<ApiResponse> {
|
|||||||
return del<ApiResponse>(API_ENDPOINTS.PEOPLE_REMARK_BY_ID(peopleId));
|
return del<ApiResponse>(API_ENDPOINTS.PEOPLE_REMARK_BY_ID(peopleId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传人员照片
|
||||||
|
* @param peopleId 人员ID
|
||||||
|
* @param file 照片文件
|
||||||
|
* @returns Promise<ApiResponse>
|
||||||
|
*/
|
||||||
|
export async function uploadPeopleImage(peopleId: string, file: File): Promise<ApiResponse<string>> {
|
||||||
|
return upload<ApiResponse<string>>(API_ENDPOINTS.PEOPLE_IMAGE_BY_ID(peopleId), file, 'image');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除人员照片
|
||||||
|
* @param peopleId 人员ID
|
||||||
|
* @returns Promise<ApiResponse>
|
||||||
|
*/
|
||||||
|
export async function deletePeopleImage(peopleId: string): Promise<ApiResponse> {
|
||||||
|
return del<ApiResponse>(API_ENDPOINTS.PEOPLE_IMAGE_BY_ID(peopleId));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量创建人员信息
|
* 批量创建人员信息
|
||||||
* @param peopleList 人员信息数组
|
* @param peopleList 人员信息数组
|
||||||
|
|||||||
@@ -106,4 +106,8 @@ export function validateImageFile(file: File, maxSize = 10 * 1024 * 1024): { val
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadImage(file: File): Promise<ApiResponse<string>> {
|
||||||
|
return upload<ApiResponse<string>>(API_ENDPOINTS.UPLOAD_IMAGE, file, 'image');
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Form, Input, Select, InputNumber, Button, message, Row, Col } from 'antd';
|
import { Form, Input, Select, InputNumber, Button, message, Row, Col, Image, Modal } from 'antd';
|
||||||
|
import 'react-image-crop/dist/ReactCrop.css';
|
||||||
|
import ReactCrop, { centerCrop, makeAspectCrop, type Crop } from 'react-image-crop';
|
||||||
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
import type { FormInstance } from 'antd';
|
import type { FormInstance } from 'antd';
|
||||||
|
|
||||||
import './PeopleForm.css';
|
import './PeopleForm.css';
|
||||||
import KeyValueList from './KeyValueList.tsx'
|
import KeyValueList from './KeyValueList.tsx'
|
||||||
import { createPeople, type People } from '../apis';
|
import { createPeople, type People, uploadPeopleImage, uploadImage } from '../apis';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
@@ -18,6 +22,13 @@ interface PeopleFormProps {
|
|||||||
const PeopleForm: React.FC<PeopleFormProps> = ({ initialData, hideSubmitButton = false, onFormReady }) => {
|
const PeopleForm: React.FC<PeopleFormProps> = ({ initialData, hideSubmitButton = false, onFormReady }) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [imgSrc, setImgSrc] = React.useState('')
|
||||||
|
const [crop, setCrop] = React.useState<Crop>()
|
||||||
|
const [completedCrop, setCompletedCrop] = React.useState<Crop>()
|
||||||
|
const [modalVisible, setModalVisible] = React.useState(false)
|
||||||
|
const imgRef = React.useRef<HTMLImageElement>(null)
|
||||||
|
const previewCanvasRef = React.useRef<HTMLCanvasElement>(null)
|
||||||
|
|
||||||
// 当 initialData 变化时,自动填充表单
|
// 当 initialData 变化时,自动填充表单
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -97,6 +108,136 @@ const PeopleForm: React.FC<PeopleFormProps> = ({ initialData, hideSubmitButton =
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onSelectFile = (file: File) => {
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.addEventListener('load', () => {
|
||||||
|
setImgSrc(reader.result?.toString() || '')
|
||||||
|
setModalVisible(true)
|
||||||
|
})
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||||
|
const { width, height } = e.currentTarget
|
||||||
|
const crop = centerCrop(
|
||||||
|
makeAspectCrop(
|
||||||
|
{
|
||||||
|
unit: '%',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
)
|
||||||
|
setCrop(crop)
|
||||||
|
}
|
||||||
|
|
||||||
|
function canvasPreview(
|
||||||
|
image: HTMLImageElement,
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
crop: Crop,
|
||||||
|
) {
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('No 2d context')
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaleX = image.naturalWidth / image.width
|
||||||
|
const scaleY = image.naturalHeight / image.height
|
||||||
|
const pixelRatio = window.devicePixelRatio
|
||||||
|
canvas.width = Math.floor(crop.width * scaleX * pixelRatio)
|
||||||
|
canvas.height = Math.floor(crop.height * scaleY * pixelRatio)
|
||||||
|
|
||||||
|
ctx.scale(pixelRatio, pixelRatio)
|
||||||
|
ctx.imageSmoothingQuality = 'high'
|
||||||
|
|
||||||
|
const cropX = crop.x * scaleX
|
||||||
|
const cropY = crop.y * scaleY
|
||||||
|
|
||||||
|
const centerX = image.naturalWidth / 2
|
||||||
|
const centerY = image.naturalHeight / 2
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(-cropX, -cropY)
|
||||||
|
ctx.translate(centerX, centerY)
|
||||||
|
ctx.translate(-centerX, -centerY)
|
||||||
|
ctx.drawImage(
|
||||||
|
image,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
image.naturalWidth,
|
||||||
|
image.naturalHeight,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
image.naturalWidth,
|
||||||
|
image.naturalHeight,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOk = async () => {
|
||||||
|
if (completedCrop && previewCanvasRef.current && imgRef.current) {
|
||||||
|
canvasPreview(imgRef.current, previewCanvasRef.current, completedCrop)
|
||||||
|
previewCanvasRef.current.toBlob(async (blob) => {
|
||||||
|
if (blob) {
|
||||||
|
setUploading(true)
|
||||||
|
try {
|
||||||
|
const response = initialData?.id
|
||||||
|
? await uploadPeopleImage(initialData.id, blob as File)
|
||||||
|
: await uploadImage(blob as File);
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
form.setFieldsValue({ cover: response.data })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
message.error('图片上传失败')
|
||||||
|
} finally {
|
||||||
|
setUploading(false)
|
||||||
|
setModalVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 'image/png')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const coverUrl = Form.useWatch('cover', form);
|
||||||
|
|
||||||
|
const coverPreviewNode = (
|
||||||
|
<div style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
minHeight: '264px', // 预览区固定高度,与表单保持高度对齐
|
||||||
|
maxHeight: '264px', // 预览区固定高度,与表单保持高度对齐
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
border: '1px dashed #d9d9d9',
|
||||||
|
borderRadius: '8px',
|
||||||
|
background: '#fafafa',
|
||||||
|
padding: '8px'
|
||||||
|
}}>
|
||||||
|
{coverUrl ? (
|
||||||
|
<Image
|
||||||
|
src={coverUrl}
|
||||||
|
alt="封面预览"
|
||||||
|
style={{ height: '100%', maxHeight: '248px', objectFit: 'contain' }}
|
||||||
|
fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2IChhEhKKTMoZ0hTwIQYUXOgjpAhLwzpDbQCBCwh_gswOQDz12JoLPj+7YM..."
|
||||||
|
preview={false}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ color: '#999' }}>封面预览</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="people-form">
|
<div className="people-form">
|
||||||
<Form
|
<Form
|
||||||
@@ -105,10 +246,9 @@ const PeopleForm: React.FC<PeopleFormProps> = ({ initialData, hideSubmitButton =
|
|||||||
size="large"
|
size="large"
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
>
|
>
|
||||||
|
|
||||||
<Row gutter={[12, 12]}>
|
<Row gutter={[12, 12]}>
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Form.Item name="name" label="姓名" rules={[{ required: true, message: '请输入姓名' }]}>
|
<Form.Item name="name" label="姓名" rules={[{ required: true, message: '请输入姓名' }]}>
|
||||||
<Input placeholder="如:张三" />
|
<Input placeholder="如:张三" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -119,48 +259,83 @@ const PeopleForm: React.FC<PeopleFormProps> = ({ initialData, hideSubmitButton =
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Row gutter={[12, 12]}>
|
<Row gutter={[24, 24]}>
|
||||||
<Col xs={24}>
|
{/* Left Side: Form Fields */}
|
||||||
<Form.Item name="cover" label="人物封面">
|
<Col xs={24} md={12}>
|
||||||
<Input placeholder="请输入图片链接(可留空)" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
|
|
||||||
<Row gutter={[12, 12]}>
|
<Row gutter={[12, 12]}>
|
||||||
<Col xs={24} md={6}>
|
<Col xs={24}>
|
||||||
<Form.Item name="gender" label="性别" rules={[{ required: true, message: '请选择性别' }]}>
|
<Form.Item name="cover" label="人物封面">
|
||||||
<Select
|
<Input
|
||||||
placeholder="请选择性别"
|
placeholder="请输入图片链接(可留空)"
|
||||||
options={[
|
suffix={
|
||||||
{ label: '男', value: '男' },
|
<Button icon={<UploadOutlined />} type="text" size="small" loading={uploading} onClick={() => {
|
||||||
{ label: '女', value: '女' },
|
const input = document.createElement('input');
|
||||||
{ label: '其他/保密', value: '其他/保密' },
|
input.type = 'file';
|
||||||
]}
|
input.accept = 'image/*';
|
||||||
/>
|
input.onchange = (e) => {
|
||||||
</Form.Item>
|
const target = e.target as HTMLInputElement;
|
||||||
|
if (target.files?.[0]) {
|
||||||
|
onSelectFile(target.files[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
}} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
{/* Mobile Only Preview */}
|
||||||
|
<Col xs={24} md={0} className="ant-visible-xs">
|
||||||
|
<Form.Item label="封面预览">
|
||||||
|
{coverPreviewNode}
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={[12, 12]}>
|
||||||
|
<Col xs={24} md={12}>
|
||||||
|
<Form.Item name="gender" label="性别" rules={[{ required: true, message: '请选择性别' }]}>
|
||||||
|
<Select
|
||||||
|
placeholder="请选择性别"
|
||||||
|
options={[
|
||||||
|
{ label: '男', value: '男' },
|
||||||
|
{ label: '女', value: '女' },
|
||||||
|
{ label: '其他/保密', value: '其他/保密' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={12}>
|
||||||
|
<Form.Item name="age" label="年龄" rules={[{ required: true, message: '请输入年龄' }]}>
|
||||||
|
<InputNumber min={0} max={120} style={{ width: '100%' }} placeholder="如:28" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={[12, 12]}>
|
||||||
|
<Col xs={24} md={12}>
|
||||||
|
<Form.Item name="height" label="身高(cm)">
|
||||||
|
<InputNumber
|
||||||
|
min={0}
|
||||||
|
max={250}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="如:175(可留空)"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} md={12}>
|
||||||
|
<Form.Item name="marital_status" label="婚姻状况">
|
||||||
|
<Input placeholder="可自定义输入,例如:未婚、已婚、离异等" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={24} md={6}>
|
{/* Right Side: Cover Preview (PC) */}
|
||||||
<Form.Item name="age" label="年龄" rules={[{ required: true, message: '请输入年龄' }]}>
|
<Col xs={0} md={12} className="ant-hidden-xs">
|
||||||
<InputNumber min={0} max={120} style={{ width: '100%' }} placeholder="如:28" />
|
<Form.Item label="封面预览">
|
||||||
</Form.Item>
|
{coverPreviewNode}
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col xs={24} md={6}>
|
|
||||||
<Form.Item name="height" label="身高(cm)">
|
|
||||||
<InputNumber
|
|
||||||
min={0}
|
|
||||||
max={250}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder="如:175(可留空)"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col xs={24} md={6}>
|
|
||||||
<Form.Item name="marital_status" label="婚姻状况">
|
|
||||||
<Input placeholder="可自定义输入,例如:未婚、已婚、离异等" />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -181,8 +356,41 @@ const PeopleForm: React.FC<PeopleFormProps> = ({ initialData, hideSubmitButton =
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
|
<Modal
|
||||||
|
title="裁剪图片"
|
||||||
|
open={modalVisible}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={() => setModalVisible(false)}
|
||||||
|
okText="上传"
|
||||||
|
cancelText="取消"
|
||||||
|
|
||||||
|
>
|
||||||
|
{imgSrc && (
|
||||||
|
<ReactCrop
|
||||||
|
crop={crop}
|
||||||
|
onChange={(_, percentCrop) => setCrop(percentCrop)}
|
||||||
|
onComplete={(c) => setCompletedCrop(c)}
|
||||||
|
aspect={1}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
ref={imgRef}
|
||||||
|
alt="Crop me"
|
||||||
|
src={imgSrc}
|
||||||
|
onLoad={onImageLoad}
|
||||||
|
/>
|
||||||
|
</ReactCrop>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
<canvas
|
||||||
|
ref={previewCanvasRef}
|
||||||
|
style={{
|
||||||
|
display: 'none',
|
||||||
|
width: Math.round(completedCrop?.width ?? 0),
|
||||||
|
height: Math.round(completedCrop?.height ?? 0),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PeopleForm;
|
export default PeopleForm;
|
||||||
|
|||||||
Reference in New Issue
Block a user