refactor: AvatarPicker -> ImagePicker

pull/105/head
moonrailgun 2 years ago
parent 09d7fd15e1
commit 59720e716e

@ -1,95 +0,0 @@
import React, { PropsWithChildren, useRef, useState } from 'react';
import { closeModal, openModal } from './Modal';
import { showToasts, t } from 'tailchat-shared';
import { Avatar } from 'antd';
import { Icon } from 'tailchat-design';
import { ImageCropperModal } from './modals/ImageCropper';
import { isGIF } from '@/utils/file-helper';
interface AvatarPickerProps extends PropsWithChildren {
className?: string;
imageUrl?: string; // 初始image url, 仅children为空时生效
onChange?: (blobUrl: string) => void;
disabled?: boolean; // 禁用选择
}
/**
*
*/
export const AvatarPicker: React.FC<AvatarPickerProps> = React.memo((props) => {
const fileRef = useRef<HTMLInputElement>(null);
const [avatarUrl, setAvatarUrl] = useState<string>(props.imageUrl || ''); // 裁剪后并使用的url/或者未经裁剪的 gif url
const updateAvatar = (imageBlobUrl: string) => {
setAvatarUrl(imageBlobUrl);
if (typeof props.onChange === 'function') {
props.onChange(imageBlobUrl);
}
};
const handleSelectFile = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const pickedFile = e.target.files[0];
if (!pickedFile) {
return;
}
if (isGIF(pickedFile)) {
updateAvatar(URL.createObjectURL(pickedFile));
} else {
const reader = new FileReader();
reader.addEventListener('load', () => {
if (reader.result) {
const key = openModal(
<ImageCropperModal
imageUrl={reader.result.toString()}
onConfirm={(croppedImageBlobUrl) => {
closeModal(key);
updateAvatar(croppedImageBlobUrl);
}}
/>,
{
maskClosable: false,
closable: true,
}
);
} else {
showToasts(t('文件读取失败'), 'error');
}
});
reader.readAsDataURL(pickedFile);
}
// 清理选中状态
e.target.files = null;
e.target.value = '';
}
};
return (
<div className={props.className}>
<div
className="cursor-pointer inline-block relative"
onClick={() => !props.disabled && fileRef.current?.click()}
>
<input
ref={fileRef}
type="file"
className="hidden"
onChange={handleSelectFile}
accept="image/*"
/>
{props.children ? (
props.children
) : (
<Avatar
size={64}
icon={<Icon className="anticon" icon="mdi:account" />}
src={avatarUrl}
/>
)}
</div>
</div>
);
});
AvatarPicker.displayName = 'AvatarPicker';

@ -1,9 +1,8 @@
import { blobUrlToFile } from '@/utils/file-helper';
import { Icon } from 'tailchat-design';
import clsx from 'clsx';
import React, { PropsWithChildren, useState } from 'react';
import { uploadFile, UploadFileResult, useAsyncRequest } from 'tailchat-shared';
import { AvatarPicker } from './AvatarPicker';
import { ImagePicker } from './ImagePicker';
export const AvatarUploader: React.FC<
PropsWithChildren<{
@ -32,32 +31,22 @@ export const AvatarUploader: React.FC<
);
return (
<AvatarPicker
className="relative"
<ImagePicker
className={clsx('relative', {
'rounded-full overflow-hidden': props.circle,
})}
disabled={loading}
onChange={handlePickImage}
>
<div className={clsx('group', props.className)}>
{props.children}
{loading && (
<div
className="absolute bottom-0 left-0 h-1"
style={{ width: `${uploadProgress}%` }}
/>
)}
{loading && (
<div
className={clsx(
'absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center transition opacity-0 group-hover:opacity-100',
{
'rounded-1/2': props.circle,
}
)}
>
<Icon className="text-white opacity-80" icon="mdi:camera-outline" />
</div>
</div>
</AvatarPicker>
className="absolute bottom-0 left-0 h-1"
style={{ width: `${uploadProgress}%` }}
/>
)}
{props.children}
</ImagePicker>
);
});
AvatarUploader.displayName = 'AvatarUploader';

@ -0,0 +1,102 @@
import React, { PropsWithChildren, useRef } from 'react';
import { closeModal, openModal } from './Modal';
import { showToasts, t, useEvent } from 'tailchat-shared';
import { Icon } from 'tailchat-design';
import { ImageCropperModal } from './modals/ImageCropper';
import { isGIF } from '@/utils/file-helper';
import clsx from 'clsx';
interface ImagePickerProps extends PropsWithChildren {
className?: string;
imageUrl?: string; // 初始image url, 仅children为空时生效
aspect?: number;
onChange?: (blobUrl: string) => void;
disabled?: boolean; // 禁用选择
}
/**
*
*/
export const ImagePicker: React.FC<ImagePickerProps> = React.memo((props) => {
const fileRef = useRef<HTMLInputElement>(null);
const updateAvatar = (imageBlobUrl: string) => {
if (typeof props.onChange === 'function') {
props.onChange(imageBlobUrl);
}
};
const handleSelectFile = useEvent(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const pickedFile = e.target.files[0];
if (!pickedFile) {
return;
}
if (isGIF(pickedFile)) {
updateAvatar(URL.createObjectURL(pickedFile));
} else {
const reader = new FileReader();
reader.addEventListener('load', () => {
if (reader.result) {
const key = openModal(
<ImageCropperModal
imageUrl={reader.result.toString()}
aspect={props.aspect}
onConfirm={(croppedImageBlobUrl) => {
closeModal(key);
updateAvatar(croppedImageBlobUrl);
}}
/>,
{
maskClosable: false,
closable: true,
}
);
} else {
showToasts(t('文件读取失败'), 'error');
}
});
reader.readAsDataURL(pickedFile);
}
// 清理选中状态
e.target.files = null;
e.target.value = '';
}
}
);
return (
<div className={props.className}>
<div
className="cursor-pointer inline-block relative"
onClick={() => !props.disabled && fileRef.current?.click()}
>
<input
ref={fileRef}
type="file"
className="hidden"
onChange={handleSelectFile}
accept="image/*"
/>
<div className={clsx('group', props.className)}>
{props.children}
<div
className={
'absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center transition opacity-0 group-hover:opacity-100'
}
>
<Icon
className="text-white opacity-80 text-4xl"
icon="mdi:camera-outline"
/>
</div>
</div>
</div>
</div>
);
});
ImagePicker.displayName = 'ImagePicker';
Loading…
Cancel
Save