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

205 lines
5.4 KiB
TypeScript
Raw Normal View History

2025-01-25 22:39:22 +08:00
import { useCallback, useState } from "react";
2025-01-25 02:28:28 +08:00
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";
2025-01-25 00:46:59 +08:00
2025-01-25 02:28:28 +08:00
export interface TusUploaderProps {
value?: string[];
onChange?: (value: string[]) => void;
}
interface UploadingFile {
name: string;
progress: number;
status: "uploading" | "done" | "error";
fileId?: string;
2025-01-25 23:19:03 +08:00
fileKey?: string;
2025-01-25 02:28:28 +08:00
}
export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
2025-01-25 23:19:03 +08:00
const { handleFileUpload, uploadProgress } = useTusUpload();
2025-01-25 02:28:28 +08:00
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]);
2025-01-25 22:39:22 +08:00
const [completedFiles, setCompletedFiles] = useState<UploadingFile[]>(
() =>
value?.map((fileId) => ({
2025-01-25 23:19:03 +08:00
name: `文件 ${fileId}`,
progress: 100,
2025-01-25 22:39:22 +08:00
status: "done" as const,
fileId,
})) || []
2025-01-25 02:28:28 +08:00
);
const [uploadResults, setUploadResults] = useState<string[]>(value || []);
const handleRemoveFile = useCallback(
(fileId: string) => {
setCompletedFiles((prev) =>
prev.filter((f) => f.fileId !== fileId)
);
2025-01-25 22:39:22 +08:00
const newResults = uploadResults.filter((id) => id !== fileId);
2025-01-25 02:28:28 +08:00
setUploadResults(newResults);
onChange?.(newResults);
},
[uploadResults, onChange]
);
const handleChange = useCallback(
async (fileList: UploadFile | UploadFile[]) => {
const files = Array.isArray(fileList) ? fileList : [fileList];
2025-01-25 22:39:22 +08:00
console.log("文件", files);
2025-01-25 02:28:28 +08:00
if (!files.every((f) => f instanceof File)) {
2025-01-25 22:39:22 +08:00
message.error("无效的文件格式");
2025-01-25 02:28:28 +08:00
return false;
}
const newFiles: UploadingFile[] = files.map((f) => ({
name: f.name,
progress: 0,
status: "uploading" as const,
2025-01-25 23:19:03 +08:00
fileKey: `${f.name}-${Date.now()}`, // 为每个文件创建唯一标识
2025-01-25 02:28:28 +08:00
}));
2025-01-25 23:19:03 +08:00
2025-01-25 02:28:28 +08:00
setUploadingFiles((prev) => [...prev, ...newFiles]);
2025-01-25 22:39:22 +08:00
const newUploadResults: string[] = [];
2025-01-25 02:28:28 +08:00
try {
for (const [index, f] of files.entries()) {
if (!f) {
2025-01-25 22:39:22 +08:00
throw new Error(`文件 ${f.name} 无效`);
2025-01-25 02:28:28 +08:00
}
2025-01-25 23:19:03 +08:00
const fileKey = newFiles[index].fileKey!;
2025-01-25 02:28:28 +08:00
const fileId = await new Promise<string>(
(resolve, reject) => {
handleFileUpload(
f as File,
(result) => {
2025-01-25 22:39:22 +08:00
console.log("上传成功:", result);
2025-01-25 02:28:28 +08:00
const completedFile = {
name: f.name,
2025-01-25 23:19:03 +08:00
progress: 100,
2025-01-25 02:28:28 +08:00
status: "done" as const,
fileId: result.fileId,
};
setCompletedFiles((prev) => [
...prev,
completedFile,
]);
setUploadingFiles((prev) =>
2025-01-25 23:19:03 +08:00
prev.filter(
(file) => file.fileKey !== fileKey
)
2025-01-25 02:28:28 +08:00
);
resolve(result.fileId);
},
(error) => {
2025-01-25 22:39:22 +08:00
console.error("上传错误:", error);
2025-01-25 02:28:28 +08:00
reject(error);
2025-01-25 23:19:03 +08:00
},
fileKey
2025-01-25 02:28:28 +08:00
);
}
);
newUploadResults.push(fileId);
}
2025-01-25 22:39:22 +08:00
const newValue = Array.from(
new Set([...uploadResults, ...newUploadResults])
);
2025-01-25 02:28:28 +08:00
setUploadResults(newValue);
onChange?.(newValue);
2025-01-25 22:39:22 +08:00
message.success(`${files.length} 个文件上传成功`);
2025-01-25 02:28:28 +08:00
} catch (error) {
2025-01-25 22:39:22 +08:00
console.error("上传错误详情:", error);
2025-01-25 02:28:28 +08:00
message.error(
2025-01-25 22:39:22 +08:00
`上传失败: ${error instanceof Error ? error.message : "未知错误"}`
2025-01-25 02:28:28 +08:00
);
setUploadingFiles((prev) =>
prev.map((f) => ({ ...f, status: "error" }))
);
}
return false;
},
[uploadResults, onChange, handleFileUpload]
);
return (
2025-01-25 22:39:22 +08:00
<div className="space-y-1">
2025-01-25 02:28:28 +08:00
<Upload.Dragger
name="files"
showUploadList={false}
2025-01-26 18:24:16 +08:00
style={{ background: "transparent", borderStyle: "none" }}
2025-01-25 22:39:22 +08:00
beforeUpload={handleChange}>
2025-01-25 02:28:28 +08:00
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
2025-01-25 22:39:22 +08:00
2025-01-25 02:28:28 +08:00
</p>
2025-01-25 22:39:22 +08:00
<p className="ant-upload-hint"></p>
{/* 正在上传的文件 */}
{(uploadingFiles.length > 0 || completedFiles.length > 0) && (
2025-01-26 18:24:16 +08:00
<div className=" px-2 py-0 rounded mt-1 ">
2025-01-25 23:19:03 +08:00
{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>
2025-01-25 22:39:22 +08:00
</div>
2025-01-25 23:19:03 +08:00
<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>
))}
2025-01-25 22:39:22 +08:00
{completedFiles.length > 0 &&
completedFiles.map((file, index) => (
<div
key={index}
2025-01-26 12:45:59 +08:00
className="flex items-center justify-between gap-2">
2025-01-25 22:39:22 +08:00
<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 />}
2025-01-26 12:45:59 +08:00
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡
if (file.fileId) {
handleRemoveFile(file.fileId); // 只删除文件
}
}}
2025-01-25 22:39:22 +08:00
/>
</div>
))}
</div>
)}
2025-01-25 02:28:28 +08:00
</Upload.Dragger>
</div>
);
2025-01-25 00:46:59 +08:00
};