| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- import React, { useRef, useState } from "react";
- import TcVod from "vod-js-sdk-v6";
- import { toast } from "sonner";
- import { vodClient } from "@/client/api";
- import { Button } from "@/client/components/ui/button";
- import { Card, CardContent, CardHeader, CardTitle } from "@/client/components/ui/card";
- import { Progress } from "@/client/components/ui/progress";
- import { Badge } from "@/client/components/ui/badge";
- interface UploadTask {
- file: File;
- progress: number;
- status: "pending" | "uploading" | "success" | "error" | "canceled";
- fileId?: string;
- videoUrl?: string;
- coverUrl?: string;
- cancel?: () => void;
- }
- interface VodUploadProps {
- onUploadSuccess?: (result: { fileId: string; videoUrl: string; coverUrl?: string }) => void;
- onUploadError?: (error: Error) => void;
- }
- export const VodUpload: React.FC<VodUploadProps> = ({
- onUploadSuccess,
- onUploadError
- }) => {
- const videoInputRef = useRef<HTMLInputElement>(null);
- const coverInputRef = useRef<HTMLInputElement>(null);
- const [uploadTasks, setUploadTasks] = useState<UploadTask[]>([]);
- const [selectedVideo, setSelectedVideo] = useState<File | null>(null);
- const [selectedCover, setSelectedCover] = useState<File | null>(null);
- // 获取上传签名
- const getSignature = async () => {
- try {
- const response = await vodClient.signature.$get();
- if (response.status !== 200) {
- throw new Error("获取上传签名失败");
- }
- const { signature } = await response.json();
- return signature;
- } catch (error) {
- toast.error("获取上传签名失败");
- throw error;
- }
- };
- // 初始化VOD SDK
- const tcVod = new TcVod({
- getSignature: getSignature,
- });
- // 处理视频文件选择
- const handleVideoSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
- if (e.target.files && e.target.files[0]) {
- setSelectedVideo(e.target.files[0]);
- }
- };
- // 处理封面文件选择
- const handleCoverSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
- if (e.target.files && e.target.files[0]) {
- setSelectedCover(e.target.files[0]);
- }
- };
- // 开始上传
- const startUpload = async () => {
- if (!selectedVideo) {
- toast.warning("请先选择视频文件");
- return;
- }
- const newTask: UploadTask = {
- file: selectedVideo,
- progress: 0,
- status: "pending",
- };
- setUploadTasks((prev) => [...prev, newTask]);
- try {
- const uploader = tcVod.upload({
- mediaFile: selectedVideo,
- coverFile: selectedCover || undefined,
- });
- const updatedTask: UploadTask = {
- ...newTask,
- status: "uploading",
- cancel: () => {
- uploader.cancel();
- setUploadTasks((prev) =>
- prev.map((task) =>
- task.file === newTask.file
- ? { ...task, status: "canceled" }
- : task
- )
- );
- },
- };
- setUploadTasks((prev) =>
- prev.map((task) => task.file === newTask.file ? updatedTask : task)
- );
- // 监听上传进度
- uploader.on("media_progress", (info) => {
- setUploadTasks((prev) =>
- prev.map((task) =>
- task.file === newTask.file
- ? { ...task, progress: info.percent * 100 }
- : task
- )
- );
- });
- // 监听上传完成
- uploader.on("media_upload", (info) => {
- setUploadTasks((prev) =>
- prev.map((task) =>
- task.file === newTask.file ? { ...task, status: "success" } : task
- )
- );
- });
- // 执行上传
- const result = await uploader.done();
- const successTask = {
- ...updatedTask,
- fileId: result.fileId,
- videoUrl: result.video.url,
- coverUrl: result.cover?.url,
- status: "success",
- };
- setUploadTasks((prev) =>
- prev.map((task) =>
- task.file === newTask.file ? successTask : task
- )
- );
- toast.success("视频上传成功");
-
- // 调用成功回调
- if (onUploadSuccess) {
- onUploadSuccess({
- fileId: result.fileId,
- videoUrl: result.video.url,
- coverUrl: result.cover?.url
- });
- }
- } catch (error) {
- setUploadTasks((prev) =>
- prev.map((task) =>
- task.file === newTask.file ? { ...task, status: "error" } : task
- )
- );
- toast.error("视频上传失败");
-
- // 调用错误回调
- if (onUploadError) {
- onUploadError(error as Error);
- }
- } finally {
- setSelectedVideo(null);
- setSelectedCover(null);
- if (videoInputRef.current) videoInputRef.current.value = "";
- if (coverInputRef.current) coverInputRef.current.value = "";
- }
- };
- // 渲染上传状态标签
- const renderStatusTag = (status: UploadTask["status"]) => {
- switch (status) {
- case "pending":
- return <Badge variant="outline">等待上传</Badge>;
- case "uploading":
- return <Badge variant="secondary">上传中</Badge>;
- case "success":
- return <Badge variant="default">上传成功</Badge>;
- case "error":
- return <Badge variant="destructive">上传失败</Badge>;
- case "canceled":
- return <Badge variant="outline">已取消</Badge>;
- default:
- return <Badge variant="outline">未知状态</Badge>;
- }
- };
- return (
- <Card className="mb-6">
- <CardHeader>
- <CardTitle>视频上传</CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="flex flex-wrap items-center gap-3">
- <input
- type="file"
- ref={videoInputRef}
- onChange={handleVideoSelect}
- accept="video/*"
- className="hidden"
- />
- <Button
- onClick={() => videoInputRef.current?.click()}
- variant="default"
- >
- {selectedVideo ? selectedVideo.name : "选择视频文件"}
- </Button>
- <input
- type="file"
- ref={coverInputRef}
- onChange={handleCoverSelect}
- accept="image/*"
- className="hidden"
- />
- <Button
- onClick={() => coverInputRef.current?.click()}
- variant="outline"
- >
- {selectedCover ? selectedCover.name : "选择封面(可选)"}
- </Button>
- <Button
- onClick={startUpload}
- disabled={!selectedVideo}
- >
- 开始上传
- </Button>
- </div>
- <div className="space-y-4">
- {uploadTasks.map((task, index) => (
- <div key={index} className="border rounded-lg p-4">
- <div className="flex items-center gap-2 mb-3">
- <span className="font-medium">{task.file.name}</span>
- {renderStatusTag(task.status)}
- {task.status === "uploading" && task.cancel && (
- <Button
- variant="ghost"
- size="sm"
- onClick={task.cancel}
- className="text-destructive hover:text-destructive"
- >
- 取消上传
- </Button>
- )}
- </div>
- {task.status === "uploading" && (
- <Progress value={task.progress} className="w-full" />
- )}
- {task.fileId && (
- <div className="space-y-2 text-sm mt-3">
- <div className="font-medium">File ID: {task.fileId}</div>
- {task.videoUrl && (
- <div>
- 视频地址:{" "}
- <a
- href={task.videoUrl}
- target="_blank"
- rel="noreferrer"
- className="text-primary hover:underline break-all"
- >
- {task.videoUrl}
- </a>
- </div>
- )}
- {task.coverUrl && (
- <div>
- 封面地址:{" "}
- <a
- href={task.coverUrl}
- target="_blank"
- rel="noreferrer"
- className="text-primary hover:underline break-all"
- >
- {task.coverUrl}
- </a>
- </div>
- )}
- </div>
- )}
- </div>
- ))}
- </div>
- </CardContent>
- </Card>
- );
- };
|