mirror of https://github.com/msgbyte/tailchat
refactor: AvatarPicker -> ImagePicker
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';
|
@ -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…
Reference in New Issue