This commit is contained in:
ditiqi 2025-01-26 20:38:25 +08:00
parent b101cc8e3b
commit a5552dba6b
3 changed files with 192 additions and 161 deletions

View File

@ -1,19 +1,29 @@
import { useCallback, useState } from "react"; // TusUploader.tsx
import {
useCallback,
useState,
useEffect,
forwardRef,
useImperativeHandle,
} from "react";
import { import {
UploadOutlined, UploadOutlined,
CheckCircleOutlined, CheckCircleOutlined,
DeleteOutlined, DeleteOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Upload, Progress, Button } from "antd"; import { Upload, Progress, Button } from "antd";
import type { UploadFile } from "antd";
import { useTusUpload } from "@web/src/hooks/useTusUpload"; import { useTusUpload } from "@web/src/hooks/useTusUpload";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { getCompressedImageUrl } from "@nice/utils";
export interface TusUploaderProps { export interface TusUploaderProps {
value?: string[]; value?: string[];
onChange?: (value: string[]) => void; onChange?: (value: string[]) => void;
} }
export interface TusUploaderRef {
reset: () => void;
}
interface UploadingFile { interface UploadingFile {
name: string; name: string;
progress: number; progress: number;
@ -22,164 +32,179 @@ interface UploadingFile {
fileKey?: string; fileKey?: string;
} }
export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => { export const TusUploader = forwardRef<TusUploaderRef, TusUploaderProps>(
const { handleFileUpload, uploadProgress } = useTusUpload(); ({ value = [], onChange }, ref) => {
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]); const { handleFileUpload, uploadProgress } = useTusUpload();
const [completedFiles, setCompletedFiles] = useState<UploadingFile[]>( const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>(
() => []
value?.map((fileId) => ({ );
name: `文件 ${fileId}`, const [completedFiles, setCompletedFiles] = useState<UploadingFile[]>(
progress: 100, []
status: "done" as const, );
fileId,
})) || [] // 同步父组件value到completedFiles
); useEffect(() => {
// 恢复使用 uploadResults 状态跟踪最新结果 setCompletedFiles(
const [uploadResults, setUploadResults] = useState<string[]>(value || []); value.map((fileId) => ({
const handleRemoveFile = useCallback( name: `文件 ${fileId}`,
(fileId: string) => { progress: 100,
setCompletedFiles((prev) => status: "done" as const,
prev.filter((f) => f.fileId !== fileId) fileId,
}))
); );
// 使用函数式更新保证获取最新状态 }, [value]);
setUploadResults((prev) => {
const newValue = prev.filter((id) => id !== fileId);
onChange?.(newValue); // 同步更新父组件
return newValue;
});
},
[onChange]
);
const handleBeforeUpload = useCallback( // 暴露重置方法
(file: File) => { useImperativeHandle(ref, () => ({
const fileKey = `${file.name}-${Date.now()}`; reset: () => {
setCompletedFiles([]);
setUploadingFiles([]);
},
}));
setUploadingFiles((prev) => [ const handleRemoveFile = useCallback(
...prev, (fileId: string) => {
{ setCompletedFiles((prev) =>
name: file.name, prev.filter((f) => f.fileId !== fileId)
progress: 0, );
status: "uploading", onChange?.(value.filter((id) => id !== fileId));
fileKey, },
}, [onChange, value]
]); );
handleFileUpload( const handleRemoveUploadingFile = useCallback((fileKey: string) => {
file, setUploadingFiles((prev) =>
(result) => { prev.filter((f) => f.fileKey !== fileKey)
setCompletedFiles((prev) => [
...prev,
{
name: file.name,
progress: 100,
status: "done",
fileId: result.fileId,
},
]);
setUploadingFiles((prev) =>
prev.filter((f) => f.fileKey !== fileKey)
);
// 正确的状态更新方式
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
); );
}, []);
return false; const handleBeforeUpload = useCallback(
}, (file: File) => {
[handleFileUpload, onChange] const fileKey = `${file.name}-${Date.now()}`;
);
return ( setUploadingFiles((prev) => [
<div className="space-y-1"> ...prev,
<Upload.Dragger {
name="files" name: file.name,
multiple progress: 0,
showUploadList={false} status: "uploading",
style={{ background: "transparent", borderStyle: "none" }} fileKey,
beforeUpload={handleBeforeUpload}> },
<p className="ant-upload-drag-icon"> ]);
<UploadOutlined />
</p>
<p className="ant-upload-text">
</p>
<p className="ant-upload-hint"></p>
{/* 上传状态展示 */} handleFileUpload(
<div className="px-2 py-0 rounded mt-1"> file,
{/* 上传中的文件 */} (result) => {
{uploadingFiles.map((file) => ( const newValue = [...value, result.fileId];
<div onChange?.(newValue);
key={file.fileKey} setUploadingFiles((prev) =>
className="flex flex-col gap-1 mb-2"> prev.filter((f) => f.fileKey !== fileKey)
<div className="flex items-center gap-2"> );
<span className="text-sm">{file.name}</span> },
(error) => {
console.error("上传错误:", error);
toast.error(
`上传失败: ${error instanceof Error ? error.message : "未知错误"}`
);
setUploadingFiles((prev) =>
prev.map((f) =>
f.fileKey === fileKey
? { ...f, status: "error" }
: f
)
);
},
fileKey
);
return false;
},
[handleFileUpload, onChange, value]
);
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>
<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> </div>
<Progress ))}
percent={
file.status === "done"
? 100
: Math.round(
uploadProgress?.[
file.fileKey!
] || 0
)
}
status={
file.status === "error"
? "exception"
: "active"
}
/>
</div>
))}
{/* 已完成的文件 */} {completedFiles.map((file) => (
{completedFiles.map((file) => ( <div
<div key={file.fileId}
key={file.fileId} className="flex items-center justify-between gap-2 mb-2">
className="flex items-center justify-between gap-2 mb-2"> <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <CheckCircleOutlined className="text-green-500" />
<CheckCircleOutlined className="text-green-500" /> <span className="text-sm">{file.name}</span>
<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>
<Button ))}
type="text" </div>
danger </Upload.Dragger>
icon={<DeleteOutlined />} </div>
onClick={(e) => { );
e.stopPropagation(); }
if (file.fileId) );
handleRemoveFile(file.fileId);
}}
/>
</div>
))}
</div>
</Upload.Dragger>
</div>
);
};

View File

@ -1,4 +1,4 @@
import React, { useContext, useState } from "react"; import React, { useContext, useRef, useState } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Button, Input, Tabs } from "antd"; import { Button, Input, Tabs } from "antd";
import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor"; import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor";
@ -19,6 +19,7 @@ export default function PostCommentEditor() {
const [signature, setSignature] = useState<string | undefined>(undefined); const [signature, setSignature] = useState<string | undefined>(undefined);
const [fileIds, setFileIds] = useState<string[]>([]); const [fileIds, setFileIds] = useState<string[]>([]);
const { create } = usePost(); const { create } = usePost();
const uploaderRef = useRef<{ reset: () => void }>(null);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -46,7 +47,10 @@ export default function PostCommentEditor() {
}); });
toast.success("发布成功!"); toast.success("发布成功!");
setContent(""); setContent("");
setFileIds([]); setFileIds([]); // 重置上传组件状态
// if (uploaderRef.current) {
// uploaderRef.current.reset();
// }
} catch (error) { } catch (error) {
toast.error("发布失败,请稍后重试"); toast.error("发布失败,请稍后重试");
console.error("Error posting comment:", error); console.error("Error posting comment:", error);
@ -86,6 +90,8 @@ export default function PostCommentEditor() {
<TabPane tab="附件" key="2"> <TabPane tab="附件" key="2">
<div className="relative rounded-xl border border-white hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100"> <div className="relative rounded-xl border border-white hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100">
<TusUploader <TusUploader
ref={uploaderRef}
value={fileIds}
onChange={(value) => { onChange={(value) => {
console.log("ids", value); console.log("ids", value);
setFileIds(value); setFileIds(value);

View File

@ -45,12 +45,12 @@ export function useTusUpload() {
onError: (error: Error) => void, onError: (error: Error) => void,
fileKey: string // 添加文件唯一标识 fileKey: string // 添加文件唯一标识
) => { ) => {
if (!file || !file.name || !file.type) { // if (!file || !file.name || !file.type) {
const error = new Error("不可上传该类型文件"); // const error = new Error("不可上传该类型文件");
setUploadError(error.message); // setUploadError(error.message);
onError(error); // onError(error);
return; // return;
} // }
setIsUploading(true); setIsUploading(true);
setUploadProgress((prev) => ({ ...prev, [fileKey]: 0 })); setUploadProgress((prev) => ({ ...prev, [fileKey]: 0 }));