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