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

186 lines
5.4 KiB
TypeScript
Raw Normal View History

2025-01-26 21:46:26 +08:00
import { useCallback, useState } from "react";
2025-01-26 20:38:25 +08:00
import {
2025-01-26 21:46:26 +08:00
UploadOutlined,
CheckCircleOutlined,
DeleteOutlined,
2025-01-25 02:28:28 +08:00
} from "@ant-design/icons";
2025-01-26 19:33:45 +08:00
import { Upload, Progress, Button } from "antd";
2025-01-26 21:46:26 +08:00
import type { UploadFile } from "antd";
2025-01-25 02:28:28 +08:00
import { useTusUpload } from "@web/src/hooks/useTusUpload";
2025-01-26 18:32:47 +08:00
import toast from "react-hot-toast";
2025-01-26 21:46:26 +08:00
import { getCompressedImageUrl } from "@nice/utils";
2025-01-26 20:38:25 +08:00
2025-01-25 02:28:28 +08:00
export interface TusUploaderProps {
2025-01-26 21:46:26 +08:00
value?: string[];
onChange?: (value: string[]) => void;
2025-01-26 20:38:25 +08:00
}
2025-01-25 02:28:28 +08:00
interface UploadingFile {
2025-01-26 21:46:26 +08:00
name: string;
progress: number;
status: "uploading" | "done" | "error";
fileId?: string;
fileKey?: string;
2025-01-25 02:28:28 +08:00
}
2025-01-26 21:46:26 +08:00
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 || []);
2025-01-26 20:38:25 +08:00
2025-01-26 21:46:26 +08:00
const handleRemoveFile = useCallback(
(fileId: string) => {
setCompletedFiles((prev) => prev.filter((f) => f.fileId !== fileId));
setUploadResults((prev) => {
const newValue = prev.filter((id) => id !== fileId);
onChange?.(newValue);
return newValue;
});
},
[onChange]
);
2025-01-26 20:38:25 +08:00
2025-01-26 21:46:26 +08:00
// 新增:处理删除上传中的失败文件
const handleRemoveUploadingFile = useCallback((fileKey: string) => {
setUploadingFiles((prev) => prev.filter((f) => f.fileKey !== fileKey));
}, []);
2025-01-25 02:28:28 +08:00
2025-01-26 21:46:26 +08:00
const handleBeforeUpload = useCallback(
(file: File) => {
const fileKey = `${file.name}-${Date.now()}`;
2025-01-25 22:39:22 +08:00
2025-01-26 21:46:26 +08:00
setUploadingFiles((prev) => [
...prev,
{
name: file.name,
progress: 0,
status: "uploading",
fileKey,
},
]);
2025-01-25 02:28:28 +08:00
2025-01-26 21:46:26 +08:00
handleFileUpload(
file,
(result) => {
setCompletedFiles((prev) => [
...prev,
{
name: file.name,
progress: 100,
status: "done",
fileId: result.fileId,
},
]);
2025-01-25 23:19:03 +08:00
2025-01-26 21:46:26 +08:00
setUploadingFiles((prev) =>
prev.filter((f) => f.fileKey !== fileKey)
);
2025-01-25 02:28:28 +08:00
2025-01-26 21:46:26 +08:00
setUploadResults((prev) => {
const newValue = [...prev, result.fileId];
onChange?.(newValue);
return newValue;
});
},
(error) => {
console.error("上传错误:", error);
toast.error(
`上传失败: ${error instanceof Error ? error.message : "未知错误"}`
);
setUploadingFiles((prev) =>
prev.map((f) =>
f.fileKey === fileKey ? { ...f, status: "error" } : f
)
);
},
fileKey
);
2025-01-25 02:28:28 +08:00
2025-01-26 21:46:26 +08:00
return false;
},
[handleFileUpload, onChange]
);
2025-01-25 02:28:28 +08:00
2025-01-26 21:46:26 +08:00
return (
<div className="space-y-1">
<Upload.Dragger
name="files"
multiple
showUploadList={false}
style={{ background: "transparent", borderStyle: "none" }}
beforeUpload={handleBeforeUpload}
>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text"></p>
<p className="ant-upload-hint"></p>
2025-01-25 23:19:03 +08:00
2025-01-26 21:46:26 +08:00
<div className="px-2 py-0 rounded mt-1">
{uploadingFiles.map((file) => (
<div
key={file.fileKey}
className="flex flex-col gap-1 mb-2"
>
<div className="flex items-center gap-2">
<span className="text-sm">{file.name}</span>
</div>
<div className="flex items-center gap-2">
<Progress
percent={
file.status === "done"
? 100
: Math.round(uploadProgress?.[file.fileKey!] || 0)
}
status={file.status === "error" ? "exception" : "active"}
className="flex-1"
/>
{file.status === "error" && (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
if (file.fileKey) handleRemoveUploadingFile(file.fileKey);
}}
/>
)}
</div>
</div>
))}
2025-01-26 19:33:45 +08:00
2025-01-26 21:46:26 +08:00
{completedFiles.map((file) => (
<div
key={file.fileId}
className="flex items-center justify-between gap-2 mb-2"
>
<div className="flex items-center gap-2">
<CheckCircleOutlined className="text-green-500" />
<span className="text-sm">{file.name}</span>
</div>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
if (file.fileId) handleRemoveFile(file.fileId);
}}
/>
</div>
))}
</div>
</Upload.Dragger>
</div>
);
};