|
|
|
@ -2,18 +2,65 @@ import Cropper from 'react-easy-crop';
|
|
|
|
|
import type { Area } from 'react-easy-crop/types';
|
|
|
|
|
import _isNil from 'lodash/isNil';
|
|
|
|
|
import { showToasts, t } from 'tailchat-shared';
|
|
|
|
|
import React, { useRef, useState } from 'react';
|
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { Button } from 'antd';
|
|
|
|
|
import { ModalWrapper } from '../Modal';
|
|
|
|
|
|
|
|
|
|
const createImage = (url: string): Promise<HTMLImageElement> =>
|
|
|
|
|
new Promise((resolve, reject) => {
|
|
|
|
|
/**
|
|
|
|
|
* 头像裁剪模态框
|
|
|
|
|
*/
|
|
|
|
|
export const ImageCropperModal: React.FC<{
|
|
|
|
|
imageUrl: string;
|
|
|
|
|
aspect?: number;
|
|
|
|
|
onConfirm: (croppedImageBlobUrl: string) => void;
|
|
|
|
|
}> = React.memo((props) => {
|
|
|
|
|
const aspect = props.aspect ?? 1;
|
|
|
|
|
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
|
|
|
|
const [zoom, setZoom] = useState(1);
|
|
|
|
|
const [area, setArea] = useState<Area>({ width: 0, height: 0, x: 0, y: 0 });
|
|
|
|
|
|
|
|
|
|
const handleConfirm = async () => {
|
|
|
|
|
const blobUrl = await getCroppedImg(
|
|
|
|
|
await createImage(props.imageUrl),
|
|
|
|
|
area
|
|
|
|
|
);
|
|
|
|
|
props.onConfirm(blobUrl);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ModalWrapper
|
|
|
|
|
className="flex flex-col"
|
|
|
|
|
style={{ width: '80vw', height: '80vh' }}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex-1 relative mb-4">
|
|
|
|
|
<Cropper
|
|
|
|
|
image={props.imageUrl}
|
|
|
|
|
crop={crop}
|
|
|
|
|
zoom={zoom}
|
|
|
|
|
aspect={aspect}
|
|
|
|
|
onCropChange={setCrop}
|
|
|
|
|
onZoomChange={setZoom}
|
|
|
|
|
onCropComplete={(_, area) => setArea(area)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Button type="primary" onClick={handleConfirm}>
|
|
|
|
|
{t('确认')}
|
|
|
|
|
</Button>
|
|
|
|
|
</ModalWrapper>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
ImageCropperModal.displayName = 'ImageCropperModal';
|
|
|
|
|
|
|
|
|
|
function createImage(url: string): Promise<HTMLImageElement> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const image = new Image();
|
|
|
|
|
image.addEventListener('load', () => resolve(image));
|
|
|
|
|
image.addEventListener('error', (error) => reject(error));
|
|
|
|
|
image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox
|
|
|
|
|
image.src = url;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getRadianAngle(degreeValue: number) {
|
|
|
|
|
return (degreeValue * Math.PI) / 180;
|
|
|
|
@ -93,47 +140,3 @@ function getCroppedImg(
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 头像裁剪模态框
|
|
|
|
|
*/
|
|
|
|
|
export const ModalAvatarCropper: React.FC<{
|
|
|
|
|
imageUrl: string;
|
|
|
|
|
onConfirm: (croppedImageBlobUrl: string) => void;
|
|
|
|
|
}> = React.memo((props) => {
|
|
|
|
|
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
|
|
|
|
const [zoom, setZoom] = useState(1);
|
|
|
|
|
const [area, setArea] = useState<Area>({ width: 0, height: 0, x: 0, y: 0 });
|
|
|
|
|
|
|
|
|
|
const handleConfirm = async () => {
|
|
|
|
|
const blobUrl = await getCroppedImg(
|
|
|
|
|
await createImage(props.imageUrl),
|
|
|
|
|
area
|
|
|
|
|
);
|
|
|
|
|
props.onConfirm(blobUrl);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ModalWrapper
|
|
|
|
|
className="flex flex-col"
|
|
|
|
|
style={{ width: '80vw', height: '80vh' }}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex-1 relative mb-4">
|
|
|
|
|
<Cropper
|
|
|
|
|
image={props.imageUrl}
|
|
|
|
|
crop={crop}
|
|
|
|
|
zoom={zoom}
|
|
|
|
|
aspect={1}
|
|
|
|
|
onCropChange={setCrop}
|
|
|
|
|
onZoomChange={setZoom}
|
|
|
|
|
onCropComplete={(_, area) => setArea(area)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Button type="primary" onClick={handleConfirm}>
|
|
|
|
|
{t('确认')}
|
|
|
|
|
</Button>
|
|
|
|
|
</ModalWrapper>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
ModalAvatarCropper.displayName = 'ModalAvatarCropper';
|