feat: add real-time video upload progress bar with stage indicators

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name 2026-01-31 21:17:35 +08:00
parent 10fecf0892
commit 344c9bc956

View File

@ -580,6 +580,8 @@ export function AdminSettingsClient() {
// 视频上传状态 // 视频上传状态
const [videoUploading, setVideoUploading] = useState(false); const [videoUploading, setVideoUploading] = useState(false);
const [videoUploadProgress, setVideoUploadProgress] = useState(""); const [videoUploadProgress, setVideoUploadProgress] = useState("");
const [videoUploadPercent, setVideoUploadPercent] = useState(0);
const [videoUploadStage, setVideoUploadStage] = useState<"idle" | "uploading" | "compressing" | "saving" | "done">("idle");
const [videoQuality, setVideoQuality] = useState<"low" | "medium" | "high">("medium"); const [videoQuality, setVideoQuality] = useState<"low" | "medium" | "high">("medium");
// 视频教程管理状态 // 视频教程管理状态
@ -2718,8 +2720,18 @@ export function AdminSettingsClient() {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (!file) return; if (!file) return;
// 文件大小检查
const maxSize = 500 * 1024 * 1024; // 500MB
if (file.size > maxSize) {
toast.error("视频文件不能超过500MB");
e.target.value = "";
return;
}
setVideoUploading(true); setVideoUploading(true);
setVideoUploadProgress("上传中..."); setVideoUploadPercent(0);
setVideoUploadStage("uploading");
setVideoUploadProgress(`上传中 0%`);
try { try {
const formData = new FormData(); const formData = new FormData();
@ -2727,55 +2739,124 @@ export function AdminSettingsClient() {
formData.append("quality", videoQuality); formData.append("quality", videoQuality);
formData.append("compress", "true"); formData.append("compress", "true");
const response = await fetch("/api/video/upload", { // 使用 XMLHttpRequest 以获取真实上传进度
method: "POST", const result = await new Promise<{ url: string; compression?: { compressed: boolean; ratio: string; originalSize: string; compressedSize: string } }>((resolve, reject) => {
body: formData, const xhr = new XMLHttpRequest();
// 上传进度
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percent = Math.round((event.loaded / event.total) * 100);
setVideoUploadPercent(percent);
if (percent < 100) {
setVideoUploadProgress(`上传中 ${percent}%`);
} else {
setVideoUploadStage("compressing");
setVideoUploadProgress("压缩处理中...");
}
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const response = JSON.parse(xhr.responseText);
if (response.error) {
reject(new Error(response.error));
} else {
setVideoUploadStage("done");
resolve(response);
}
} catch {
reject(new Error("解析响应失败"));
}
} else {
try {
const response = JSON.parse(xhr.responseText);
reject(new Error(response.error || `上传失败 (${xhr.status})`));
} catch {
reject(new Error(`上传失败 (${xhr.status})`));
}
}
};
xhr.onerror = () => reject(new Error("网络错误,请检查连接"));
xhr.ontimeout = () => reject(new Error("上传超时"));
xhr.open("POST", "/api/video/upload");
xhr.timeout = 600000; // 10分钟超时
xhr.send(formData);
}); });
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || "上传失败");
}
setTutorialVideoForm({ ...tutorialVideoForm, videoUrl: result.url }); setTutorialVideoForm({ ...tutorialVideoForm, videoUrl: result.url });
if (result.compression?.compressed) { if (result.compression?.compressed) {
toast.success(`压缩率: ${result.compression.ratio}%`); toast.success(
`上传成功!压缩率: ${result.compression.ratio}% (${result.compression.originalSize} → ${result.compression.compressedSize})`,
{ duration: 5000 }
);
} else { } else {
toast.success("上传成功!"); toast.success("视频上传成功!");
} }
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : "上传失败"); toast.error(error instanceof Error ? error.message : "上传失败");
} finally { } finally {
setVideoUploading(false); setVideoUploading(false);
setVideoUploadProgress(""); setVideoUploadProgress("");
setVideoUploadPercent(0);
setVideoUploadStage("idle");
e.target.value = ""; e.target.value = "";
} }
}} }}
disabled={videoUploading} disabled={videoUploading}
/> />
<Button <div className="w-full">
type="button" <Button
variant="outline" type="button"
className="w-full cursor-pointer" variant="outline"
disabled={videoUploading} className="w-full cursor-pointer"
asChild disabled={videoUploading}
> asChild
<span> >
{videoUploading ? ( <span>
<> {videoUploading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <>
{videoUploadProgress} <Loader2 className="mr-2 h-4 w-4 animate-spin" />
</> {videoUploadProgress}
) : ( </>
<> ) : (
<Upload className="mr-2 h-4 w-4" /> <>
上传视频 <Upload className="mr-2 h-4 w-4" />
</> 上传视频
)} </>
</span> )}
</Button> </span>
</Button>
{/* 上传进度条 */}
{videoUploading && (
<div className="mt-2 space-y-1">
<div className="h-2 w-full bg-gray-200 rounded-full overflow-hidden dark:bg-gray-700">
<div
className={`h-full transition-all duration-300 ${
videoUploadStage === "compressing"
? "bg-yellow-500 animate-pulse"
: videoUploadStage === "done"
? "bg-green-500"
: "bg-blue-500"
}`}
style={{
width: videoUploadStage === "compressing" ? "100%" : `${videoUploadPercent}%`
}}
/>
</div>
<p className="text-xs text-muted-foreground text-center">
{videoUploadStage === "uploading" && `已上传 ${videoUploadPercent}%`}
{videoUploadStage === "compressing" && "服务器正在压缩视频,请耐心等待..."}
{videoUploadStage === "done" && "处理完成!"}
</p>
</div>
)}
</div>
</label> </label>
</div> </div>
</div> </div>