Files
xiaoxia-saas/apps/web/src/pages/duplication/DuplicationUpload.tsx
T
CI Test f0eed0ca3f
Deploy / Deploy Staging (push) Failing after 192h26m58s
CI/CD Pipeline / Frontend Lint (push) Failing after 192h27m37s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 192h27m46s
Deploy / Production Browser E2E (push) Failing after 1851h1m12s
Deploy / Deploy Production (push) Failing after 1851h1m14s
Deploy / Build Production Runtime Images (push) Failing after 1851h1m17s
style: fix prettier formatting issues
2026-07-01 12:26:48 +08:00

247 lines
7.5 KiB
TypeScript

/**
* 查重上传页面 — V21 设计系统
* 左右分栏:拖拽上传区 + 格式说明
* 零 antd 依赖
*/
import React, { useState, useRef, useCallback } from "react";
import { useMutation } from "@tanstack/react-query";
import { Button, Card, Tag } from "@/components/ui";
import { uploadForDuplication } from "@/api/duplication";
import { useNavigate } from "react-router-dom";
import "./duplication.css";
import PageHead from "@/components/layout/PageHead";
/** 支持的视频格式 */
const ACCEPT_FORMATS = ".mp4,.avi,.mov,.mkv,.wmv,.flv,.webm";
const FORMAT_LIST = ["MP4", "AVI", "MOV", "MKV", "WMV", "FLV", "WebM"];
/** 最大文件大小:2GB */
const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024;
/** 简易 toast */
interface ToastState {
message: string;
type: "success" | "error" | "warning";
}
const DuplicationUpload: React.FC = () => {
const navigate = useNavigate();
const fileInputRef = useRef<HTMLInputElement>(null);
const [dragging, setDragging] = useState(false);
const [uploading, setUploading] = useState(false);
const [uploadResult, setUploadResult] = useState<{
id: string;
message: string;
} | null>(null);
const [toast, setToast] = useState<ToastState | null>(null);
/** 显示 toast */
const showToast = useCallback(
(message: string, type: "success" | "error" | "warning") => {
setToast({ message, type });
setTimeout(() => setToast(null), 3000);
},
[],
);
// 上传查重 mutation
const uploadMutation = useMutation({
mutationFn: (file: File) => uploadForDuplication(file),
onSuccess: (data) => {
setUploading(false);
setUploadResult({ id: data.id, message: data.message });
showToast("查重任务已提交", "success");
},
onError: () => {
setUploading(false);
showToast("上传失败,请重试", "error");
},
});
/** 校验并上传文件 */
const handleFile = useCallback(
(file: File) => {
if (file.size > MAX_FILE_SIZE) {
showToast("文件大小不能超过 2GB", "error");
return;
}
const ext = file.name.toLowerCase().split(".").pop();
const allowedExts = ACCEPT_FORMATS.replace(/\./g, "").split(",");
if (!allowedExts.includes(ext || "")) {
showToast(`不支持的文件格式,支持:${FORMAT_LIST.join("、")}`, "error");
return;
}
setUploading(true);
setUploadResult(null);
uploadMutation.mutate(file);
},
[uploadMutation, showToast],
);
/** 拖拽事件 */
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragging(true);
}, []);
const handleDragLeave = useCallback(() => {
setDragging(false);
}, []);
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setDragging(false);
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
},
[handleFile],
);
/** 点击选择文件 */
const handleSelectFile = () => {
fileInputRef.current?.click();
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) handleFile(file);
// 重置 input 以便重复选择同一文件
e.target.value = "";
};
/** 重置状态 */
const handleReset = () => {
setUploadResult(null);
setUploading(false);
};
return (
<div className="dup-page">
{/* Toast */}
{toast && (
<div className={`dup-toast dup-toast--${toast.type}`}>
{toast.message}
</div>
)}
<PageHead
title="视频查重"
description="上传视频文件,系统将自动检测与已有素材的重复片段,帮助您避免重复内容。"
/>
<div className="dup-upload-grid">
{/* 左侧:上传区域 */}
<Card>
{/* 拖拽上传区 */}
<div
className={`dup-upload-zone ${dragging ? "dragging" : ""} ${uploading ? "disabled" : ""}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={uploading ? undefined : handleSelectFile}
>
<div className="dup-upload-icon">{uploading ? "⏳" : "📁"}</div>
<h3>
{uploading ? "正在上传并查重..." : "点击或拖拽视频文件到此区域"}
</h3>
<p>支持 MP4、AVI、MOV、MKV 等格式,单个文件不超过 2GB</p>
<div className="dup-upload-formats">
{FORMAT_LIST.map((fmt) => (
<Tag key={fmt} variant="info">
{fmt}
</Tag>
))}
</div>
</div>
{/* 隐藏的文件 input */}
<input
ref={fileInputRef}
type="file"
accept={ACCEPT_FORMATS}
style={{ display: "none" }}
onChange={handleFileChange}
/>
{/* 上传按钮 */}
<div className="dup-upload-actions">
<Button
buttonType="primary"
buttonSize="md"
onClick={handleSelectFile}
disabled={uploading}
>
📂 选择文件
</Button>
</div>
{/* 上传进度 */}
{uploading && (
<div className="dup-progress">
<div className="dup-progress-circle">
<span className="dup-progress-icon">⏳</span>
<span className="dup-progress-text">查重中...</span>
</div>
<p>正在分析视频内容,请稍候...</p>
</div>
)}
{/* 上传结果 */}
{uploadResult && !uploading && (
<div className="dup-result">
<div className="dup-result-icon">✅</div>
<h3>查重任务已提交</h3>
<p>{uploadResult.message}</p>
<div className="dup-result-actions">
<Button
buttonType="primary"
buttonSize="md"
onClick={() => navigate("/duplication/results")}
>
查看结果
</Button>
<Button
buttonType="secondary"
buttonSize="md"
onClick={handleReset}
>
继续上传
</Button>
</div>
</div>
)}
</Card>
{/* 右侧:格式说明 + 提示 */}
<div className="dup-info-card">
<h3>📋 查重说明</h3>
<ul className="dup-info-list">
<li>系统会对比您上传的视频与素材库中的已有视频</li>
<li>查重完成后,可查看重复片段的具体位置</li>
<li>查重过程通常需要几分钟,取决于视频大小</li>
<li>高相似度片段建议进行替换或裁剪</li>
</ul>
<h3 style={{ marginTop: 24 }}>🎬 支持格式</h3>
<div className="dup-format-tags">
{FORMAT_LIST.map((fmt) => (
<Tag key={fmt} variant="info">
{fmt}
</Tag>
))}
</div>
<h3 style={{ marginTop: 24 }}>💡 温馨提示</h3>
<ul className="dup-info-list">
<li>单个文件不超过 2GB</li>
<li>视频时长建议不超过 60 分钟</li>
<li>查重结果可在「查重记录」中随时查看</li>
</ul>
</div>
</div>
</div>
);
};
export default DuplicationUpload;