From 53d8fc3bb8da5a72d39be99bb56bcfc160f5fae3 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Thu, 19 Aug 2021 15:43:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BE=A4=E7=BB=84=E5=A4=B4=E5=83=8F?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=B8=8E=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/index.tsx | 3 +- shared/utils/upload-helper.ts | 4 +- web/src/components/Avatar.tsx | 4 +- web/src/components/AvatarUploader.tsx | 44 +++++++ .../components/modals/GroupDetail/Summary.tsx | 20 ++- web/src/utils/file-helper.ts | 116 ++++++++++++++++++ 6 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 web/src/components/AvatarUploader.tsx create mode 100644 web/src/utils/file-helper.ts diff --git a/shared/index.tsx b/shared/index.tsx index d5521f46..1d62e538 100644 --- a/shared/index.tsx +++ b/shared/index.tsx @@ -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'; diff --git a/shared/utils/upload-helper.ts b/shared/utils/upload-helper.ts index 77ca2ae4..5c82885b 100644 --- a/shared/utils/upload-helper.ts +++ b/shared/utils/upload-helper.ts @@ -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') { diff --git a/web/src/components/Avatar.tsx b/web/src/components/Avatar.tsx index 2a83106b..5353d2bb 100644 --- a/web/src/components/Avatar.tsx +++ b/web/src/components/Avatar.tsx @@ -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 = 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]); diff --git a/web/src/components/AvatarUploader.tsx b/web/src/components/AvatarUploader.tsx new file mode 100644 index 00000000..1d6e9138 --- /dev/null +++ b/web/src/components/AvatarUploader.tsx @@ -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 ( + + {props.children} + {loading && ( +
+ )} + + ); +}); +AvatarUploader.displayName = 'AvatarUploader'; diff --git a/web/src/components/modals/GroupDetail/Summary.tsx b/web/src/components/modals/GroupDetail/Summary.tsx index 06c8d252..24752258 100644 --- a/web/src/components/modals/GroupDetail/Summary.tsx +++ b/web/src/components/modals/GroupDetail/Summary.tsx @@ -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 (
-
- +
+ + +
-
+
{ + 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 { + 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); + }); +} + +/** + * 根据一个BlobUrl地址获取相应的Blob对象 + * @param blobUrl blobUrl 地址,由URL.createObjectURL方法生成 + */ +export function blobFromUrl(blobUrl: string): Promise { + 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 { + 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 { + 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(); + }); +}