feat: add real-time video upload progress bar with stage indicators
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
10fecf0892
commit
344c9bc956
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user