feat: 群组头像上传与修改

pull/13/head
moonrailgun 4 years ago
parent 9e23483034
commit 53d8fc3bb8

@ -101,6 +101,7 @@ export {
isDevelopment,
version,
} from './utils/environment';
export { getTextColorHex } from './utils/string-helper';
export { getTextColorHex, isValidStr } from './utils/string-helper';
export { uploadFile } from './utils/upload-helper';
export type { UploadFileResult } from './utils/upload-helper';
export { sleep } from './utils/utils';

@ -3,7 +3,7 @@ import { request } from '../api/request';
interface UploadFileOptions {
onProgress?: (percent: number, progressEvent: unknown) => void;
}
interface UploadFileResult {
export interface UploadFileResult {
etag: string;
path: string;
url: string;
@ -19,7 +19,7 @@ export async function uploadFile(
const form = new FormData();
form.append('file', file);
const { data } = await request.post('/file/v2/image/upload', form, {
const { data } = await request.post('/upload', form, {
onUploadProgress(progressEvent) {
if (progressEvent.lengthComputable) {
if (typeof options.onProgress === 'function') {

@ -6,7 +6,7 @@ import _isNil from 'lodash/isNil';
import _isEmpty from 'lodash/isEmpty';
import _isNumber from 'lodash/isNumber';
import type { AvatarProps as AntdAvatarProps } from 'antd/lib/avatar';
import { getTextColorHex } from 'tailchat-shared';
import { getTextColorHex, isValidStr } from 'tailchat-shared';
interface AvatarProps extends AntdAvatarProps {
name?: string;
@ -14,7 +14,7 @@ interface AvatarProps extends AntdAvatarProps {
}
export const Avatar: React.FC<AvatarProps> = React.memo((_props) => {
const { isOnline, ...props } = _props;
const src = typeof props.src !== 'string' ? props.src : undefined;
const src = isValidStr(props.src) ? props.src : undefined;
const name = useMemo(() => _upperCase(_head(props.name)), [props.name]);

@ -0,0 +1,44 @@
import { blobUrlToFile } from '@/utils/file-helper';
import React, { useState } from 'react';
import { uploadFile, UploadFileResult, useAsyncRequest } from 'tailchat-shared';
import { AvatarPicker } from './AvatarPicker';
export const AvatarUploader: React.FC<{
onUploadSuccess: (fileInfo: UploadFileResult) => void;
}> = React.memo((props) => {
const [uploadProgress, setUploadProgress] = useState(0); // 0 - 100
const [{ loading }, handlePickImage] = useAsyncRequest(
async (blobUrl: string) => {
const file = await blobUrlToFile(blobUrl);
const fileInfo = await uploadFile(file, {
onProgress(percent) {
const uploadProgress = Number((percent * 100).toFixed());
console.log(`进度:${uploadProgress}`);
setUploadProgress(uploadProgress);
},
});
console.log('上传成功', fileInfo);
props.onUploadSuccess(fileInfo);
},
[]
);
return (
<AvatarPicker
className="relative"
disabled={loading}
onChange={handlePickImage}
>
{props.children}
{loading && (
<div
className="absolute bottom-0 left-0 h-1"
style={{ width: `${uploadProgress}%` }}
/>
)}
</AvatarPicker>
);
});
AvatarUploader.displayName = 'AvatarUploader';

@ -1,4 +1,5 @@
import { Avatar } from '@/components/Avatar';
import { AvatarUploader } from '@/components/AvatarUploader';
import {
DefaultFullModalInputEditorRender,
FullModalField,
@ -7,7 +8,9 @@ import { NoData } from '@/components/NoData';
import React from 'react';
import {
modifyGroupField,
showToasts,
t,
UploadFileResult,
useAsyncRequest,
useGroupInfo,
} from 'tailchat-shared';
@ -20,6 +23,15 @@ export const GroupSummary: React.FC<{
const [, handleUpdateGroupName] = useAsyncRequest(
async (newName: string) => {
await modifyGroupField(groupId, 'name', newName);
showToasts(t('修改群组名成功'), 'success');
},
[groupId]
);
const [, handleGroupAvatarChange] = useAsyncRequest(
async (fileInfo: UploadFileResult) => {
await modifyGroupField(groupId, 'avatar', fileInfo.url);
showToasts(t('修改群组头像成功'), 'success');
},
[groupId]
);
@ -30,11 +42,13 @@ export const GroupSummary: React.FC<{
return (
<div className="flex">
<div className="w-1/2">
<Avatar size={128} name={groupInfo.name} src={groupInfo.avatar} />
<div className="w-1/3">
<AvatarUploader onUploadSuccess={handleGroupAvatarChange}>
<Avatar size={128} name={groupInfo.name} src={groupInfo.avatar} />
</AvatarUploader>
</div>
<div className="w-1/2">
<div className="w-2/3">
<FullModalField
title={t('群组名')}
value={groupInfo.name}

@ -0,0 +1,116 @@
import _get from 'lodash/get';
/**
* Base64
* @param img
*/
export function fileToDataUrl(img: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', () => resolve(String(reader.result ?? '')));
reader.addEventListener('error', () => reject(reader.error));
reader.readAsDataURL(img);
});
}
/**
*
* @param file
*/
export function fileToText(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', () => resolve(String(reader.result ?? '')));
reader.addEventListener('error', () => reject(reader.error));
reader.readAsText(file);
});
}
/**
* BlobUrlBlob
* @param blobUrl blobUrl URL.createObjectURL
*/
export function blobFromUrl(blobUrl: string): Promise<Blob> {
return new Promise((resolve, reject) => {
const req = new XMLHttpRequest();
req.responseType = 'blob';
req.onload = () => {
resolve(req.response);
};
req.onerror = () => {
reject(req.responseText);
};
req.open('get', blobUrl);
req.send();
});
}
/**
* Blob
* @param blob Blob
* @param fileName
*/
export function blobToFile(blob: Blob, fileName: string): File {
return new File([blob], fileName, {
lastModified: new Date().valueOf(),
type: blob.type,
});
}
/**
* blobUrl
* @param blobUrl blobUrl
* @param fileName
*/
export async function blobUrlToFile(
blobUrl: string,
fileName = 'image.jpg'
): Promise<File> {
const blob = await blobFromUrl(blobUrl);
return blobToFile(blob, fileName);
}
/**
* Bloburl
*/
export async function downloadBlobUrl(blobUrl: string, fileName: string) {
const a = document.createElement('a');
a.href = blobUrl;
a.download = fileName; // 这里填保存成的文件名
a.click();
}
/**
* Blob
*/
export async function downloadBlob(blob: Blob, fileName: string) {
const url = String(URL.createObjectURL(blob));
downloadBlobUrl(url, fileName);
URL.revokeObjectURL(url);
}
/**
* ,
*/
interface FileDialogOptions {
accept?: string;
}
export async function openFile(
fileDialogOptions?: FileDialogOptions
): Promise<File | null> {
return new Promise((resolve) => {
const fileEl = document.createElement('input');
fileEl.setAttribute('type', 'file');
if (typeof fileDialogOptions?.accept === 'string') {
fileEl.accept = fileDialogOptions.accept;
}
fileEl.addEventListener('change', function (e) {
const file: File | null = _get(this, ['files', 0], null);
resolve(file);
fileEl.remove();
});
fileEl.click();
});
}
Loading…
Cancel
Save