doctor-mail/apps/web/src/components/common/uploader/TusUploader.tsx

205 lines
5.4 KiB
TypeScript

import { useCallback, useState } from "react";
import {
UploadOutlined,
CheckCircleOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import { Upload, message, Progress, Button } from "antd";
import type { UploadFile } from "antd";
import { useTusUpload } from "@web/src/hooks/useTusUpload";
export interface TusUploaderProps {
value?: string[];
onChange?: (value: string[]) => void;
}
interface UploadingFile {
name: string;
progress: number;
status: "uploading" | "done" | "error";
fileId?: string;
fileKey?: string;
}
export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
const { handleFileUpload, uploadProgress } = useTusUpload();
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]);
const [completedFiles, setCompletedFiles] = useState<UploadingFile[]>(
() =>
value?.map((fileId) => ({
name: `文件 ${fileId}`,
progress: 100,
status: "done" as const,
fileId,
})) || []
);
const [uploadResults, setUploadResults] = useState<string[]>(value || []);
const handleRemoveFile = useCallback(
(fileId: string) => {
setCompletedFiles((prev) =>
prev.filter((f) => f.fileId !== fileId)
);
const newResults = uploadResults.filter((id) => id !== fileId);
setUploadResults(newResults);
onChange?.(newResults);
},
[uploadResults, onChange]
);
const handleChange = useCallback(
async (fileList: UploadFile | UploadFile[]) => {
const files = Array.isArray(fileList) ? fileList : [fileList];
console.log("文件", files);
if (!files.every((f) => f instanceof File)) {
message.error("无效的文件格式");
return false;
}
const newFiles: UploadingFile[] = files.map((f) => ({
name: f.name,
progress: 0,
status: "uploading" as const,
fileKey: `${f.name}-${Date.now()}`, // 为每个文件创建唯一标识
}));
setUploadingFiles((prev) => [...prev, ...newFiles]);
const newUploadResults: string[] = [];
try {
for (const [index, f] of files.entries()) {
if (!f) {
throw new Error(`文件 ${f.name} 无效`);
}
const fileKey = newFiles[index].fileKey!;
const fileId = await new Promise<string>(
(resolve, reject) => {
handleFileUpload(
f as File,
(result) => {
console.log("上传成功:", result);
const completedFile = {
name: f.name,
progress: 100,
status: "done" as const,
fileId: result.fileId,
};
setCompletedFiles((prev) => [
...prev,
completedFile,
]);
setUploadingFiles((prev) =>
prev.filter(
(file) => file.fileKey !== fileKey
)
);
resolve(result.fileId);
},
(error) => {
console.error("上传错误:", error);
reject(error);
},
fileKey
);
}
);
newUploadResults.push(fileId);
}
const newValue = Array.from(
new Set([...uploadResults, ...newUploadResults])
);
setUploadResults(newValue);
onChange?.(newValue);
message.success(`${files.length} 个文件上传成功`);
} catch (error) {
console.error("上传错误详情:", error);
message.error(
`上传失败: ${error instanceof Error ? error.message : "未知错误"}`
);
setUploadingFiles((prev) =>
prev.map((f) => ({ ...f, status: "error" }))
);
}
return false;
},
[uploadResults, onChange, handleFileUpload]
);
return (
<div className="space-y-1">
<Upload.Dragger
name="files"
showUploadList={false}
style={{ background: "transparent", borderStyle: "none" }}
beforeUpload={handleChange}>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
</p>
<p className="ant-upload-hint"></p>
{/* 正在上传的文件 */}
{(uploadingFiles.length > 0 || completedFiles.length > 0) && (
<div className=" px-2 py-0 rounded mt-1 ">
{uploadingFiles.map((file) => (
<div
key={file.fileKey}
className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="text-sm">{file.name}</div>
</div>
<Progress
className="flex-1 w-full"
percent={
file.status === "done"
? 100
: Math.round(
uploadProgress?.[
file?.fileKey
] || 0
)
}
status={
file.status === "error"
? "exception"
: file.status === "done"
? "success"
: "active"
}
/>
</div>
))}
{completedFiles.length > 0 &&
completedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<CheckCircleOutlined className="text-green-500" />
<div className="text-sm">
{file.name}
</div>
</div>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡
if (file.fileId) {
handleRemoveFile(file.fileId); // 只删除文件
}
}}
/>
</div>
))}
</div>
)}
</Upload.Dragger>
</div>
);
};