205 lines
5.4 KiB
TypeScript
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>
|
|
);
|
|
};
|