diff --git a/apps/web/src/components/common/uploader/AvatarUploader.tsx b/apps/web/src/components/common/uploader/AvatarUploader.tsx new file mode 100644 index 0000000..6efc6c9 --- /dev/null +++ b/apps/web/src/components/common/uploader/AvatarUploader.tsx @@ -0,0 +1,131 @@ +import { env } from "@web/src/env"; +import { message, Progress, Spin, theme } from "antd"; +import React, { useState, useEffect, useRef } from "react"; +import { useTusUpload } from "@web/src/hooks/useTusUpload"; + +export interface AvatarUploaderProps { + value?: string; + placeholder?: string; + className?: string; + onChange?: (value: string) => void; + style?: React.CSSProperties; // 添加style属性 +} + +interface UploadingFile { + name: string; + progress: number; + status: "uploading" | "done" | "error"; + fileId?: string; + fileKey?: string; +} + +const AvatarUploader: React.FC = ({ + value, + onChange, + className, + placeholder = "点击上传", + style, // 解构style属性 +}) => { + const { handleFileUpload, uploadProgress } = useTusUpload(); + const [file, setFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(value || ""); + const [uploading, setUploading] = useState(false); + const inputRef = useRef(null); + + const { token } = theme.useToken(); + + const handleChange = async (event: React.ChangeEvent) => { + const selectedFile = event.target.files?.[0]; + if (!selectedFile) return; + + setFile({ + name: selectedFile.name, + progress: 0, + status: "uploading", + fileKey: `${selectedFile.name}-${Date.now()}`, + }); + setUploading(true); + + try { + const fileId = await new Promise((resolve, reject) => { + handleFileUpload( + selectedFile, + (result) => { + setFile((prev) => ({ + ...prev!, + progress: 100, + status: "done", + fileId: result.fileId, + })); + resolve(result.fileId); + }, + (error) => { + reject(error); + }, + file?.fileKey + ); + }); + setPreviewUrl(`${env.SERVER_IP}/uploads/${fileId}`); + onChange?.(fileId); + message.success("头像上传成功"); + } catch (error) { + console.error("上传错误:", error); + message.error("头像上传失败"); + setFile((prev) => ({ ...prev!, status: "error" })); + } finally { + setUploading(false); + } + }; + + const triggerUpload = () => { + inputRef.current?.click(); + }; + + return ( +
+ + {previewUrl ? ( + Avatar + ) : ( +
+ {placeholder} +
+ )} + {uploading && ( +
+ +
+ )} + {file && file.status === "uploading" && ( +
+ +
+ )} +
+ ); +}; + +export default AvatarUploader; diff --git a/apps/web/src/components/common/uploader/FileUploader.tsx b/apps/web/src/components/common/uploader/FileUploader.tsx deleted file mode 100644 index 99bb2af..0000000 --- a/apps/web/src/components/common/uploader/FileUploader.tsx +++ /dev/null @@ -1,241 +0,0 @@ -// FileUploader.tsx -import React, { useRef, memo, useState } from "react"; -import { - CloudArrowUpIcon, - XMarkIcon, - DocumentIcon, - ExclamationCircleIcon, - CheckCircleIcon, -} from "@heroicons/react/24/outline"; -import { motion, AnimatePresence } from "framer-motion"; -import { toast } from "react-hot-toast"; -import { useTusUpload } from "@web/src/hooks/useTusUpload"; - -interface FileUploaderProps { - endpoint?: string; - onSuccess?: (result: { url: string; fileId: string }) => void; - onError?: (error: Error) => void; - maxSize?: number; - allowedTypes?: string[]; - placeholder?: string; -} - -interface FileItemProps { - file: File; - progress?: number; - onRemove: (name: string) => void; - isUploaded: boolean; -} - -const FileItem: React.FC = memo( - ({ file, progress, onRemove, isUploaded }) => ( - - -
-
-

- {file.name} -

- -
- {!isUploaded && progress !== undefined && ( -
-
- -
- - {progress}% - -
- )} - {isUploaded && ( -
- - 上传完成 -
- )} -
-
- ) -); - -const FileUploader: React.FC = ({ - onSuccess, - onError, - maxSize = 100, - placeholder = "点击或拖拽文件到这里上传", - allowedTypes = ["*/*"], -}) => { - const [isDragging, setIsDragging] = useState(false); - const [files, setFiles] = useState< - Array<{ file: File; isUploaded: boolean }> - >([]); - const fileInputRef = useRef(null); - - const { progress, isUploading, uploadError, handleFileUpload } = - useTusUpload(); - - const handleError = (error: Error) => { - toast.error(error.message); - onError?.(error); - }; - - const handleDrag = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - if (e.type === "dragenter" || e.type === "dragover") { - setIsDragging(true); - } else if (e.type === "dragleave") { - setIsDragging(false); - } - }; - - const validateFile = (file: File) => { - if (file.size > maxSize * 1024 * 1024) { - throw new Error(`文件大小不能超过 ${maxSize}MB`); - } - if ( - !allowedTypes.includes("*/*") && - !allowedTypes.includes(file.type) - ) { - throw new Error( - `不支持的文件类型。支持的类型: ${allowedTypes.join(", ")}` - ); - } - }; - - const uploadFile = (file: File) => { - try { - validateFile(file); - handleFileUpload( - file, - (upload) => { - console.log("Upload complete:", { - url: upload.url, - fileId: upload.fileId, - // resource: upload.resource - }); - onSuccess?.(upload); - setFiles((prev) => - prev.map((f) => - f.file.name === file.name - ? { ...f, isUploaded: true } - : f - ) - ); - }, - handleError - ); - } catch (error) { - handleError(error as Error); - } - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - - const droppedFiles = Array.from(e.dataTransfer.files); - setFiles((prev) => [ - ...prev, - ...droppedFiles.map((file) => ({ file, isUploaded: false })), - ]); - droppedFiles.forEach(uploadFile); - }; - - const handleFileSelect = (e: React.ChangeEvent) => { - if (e.target.files) { - const selectedFiles = Array.from(e.target.files); - setFiles((prev) => [ - ...prev, - ...selectedFiles.map((file) => ({ file, isUploaded: false })), - ]); - selectedFiles.forEach(uploadFile); - } - }; - - const removeFile = (fileName: string) => { - setFiles((prev) => prev.filter(({ file }) => file.name !== fileName)); - }; - - const handleClick = () => { - fileInputRef.current?.click(); - }; - - return ( -
-
- - -

{placeholder}

- {isDragging && ( -
-

- 释放文件以上传 -

-
- )} -
- - -
- {files.map(({ file, isUploaded }) => ( - - ))} -
-
- - {uploadError && ( -
- - {uploadError} -
- )} -
- ); -}; - -export default FileUploader; diff --git a/apps/web/src/components/layout/element/usermenu.tsx b/apps/web/src/components/layout/element/usermenu.tsx index 208dc34..6363dfb 100644 --- a/apps/web/src/components/layout/element/usermenu.tsx +++ b/apps/web/src/components/layout/element/usermenu.tsx @@ -96,24 +96,38 @@ export function UserMenu() { whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} onClick={toggleMenu} - className="relative rounded-full focus:outline-none - focus:ring-2 focus:ring-[#00538E]/80 focus:ring-offset-2 - focus:ring-offset-white transition-all duration-200 ease-in-out"> - -