diff --git a/apps/web/src/pages/duplication/DuplicationUpload.tsx b/apps/web/src/pages/duplication/DuplicationUpload.tsx old mode 100644 new mode 100755 index f486ea331..8c3dba064 --- a/apps/web/src/pages/duplication/DuplicationUpload.tsx +++ b/apps/web/src/pages/duplication/DuplicationUpload.tsx @@ -1,116 +1,35 @@ /** * 查重上传页面 — 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 React from "react" import { useNavigate } from "react-router-dom" -import "./duplication.css" +import { Card } from "@/components/ui" 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" -} +import UploadZone from "./duplication-upload/UploadZone" +import UploadActions from "./duplication-upload/UploadActions" +import UploadProgress from "./duplication-upload/UploadProgress" +import UploadResultPanel from "./duplication-upload/UploadResultPanel" +import InfoSidebar from "./duplication-upload/InfoSidebar" +import { useDuplicationUpload } from "./duplication-upload/useDuplicationUpload" +import { ACCEPT_FORMATS } from "./duplication-upload/constants" +import "./duplication.css" const DuplicationUpload: React.FC = () => { const navigate = useNavigate() - const fileInputRef = useRef(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(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) => { - const file = e.target.files?.[0] - if (file) handleFile(file) - // 重置 input 以便重复选择同一文件 - e.target.value = "" - } - - /** 重置状态 */ - const handleReset = () => { - setUploadResult(null) - setUploading(false) - } + const { + fileInputRef, + dragging, + uploading, + uploadResult, + toast, + handleDragOver, + handleDragLeave, + handleDrop, + handleSelectFile, + handleFileChange, + handleReset, + } = useDuplicationUpload() return (
@@ -125,25 +44,14 @@ const DuplicationUpload: React.FC = () => {
{/* 左侧:上传区域 */} - {/* 拖拽上传区 */} -
-
{uploading ? "⏳" : "📁"}
-

{uploading ? "正在上传并查重..." : "点击或拖拽视频文件到此区域"}

-

支持 MP4、AVI、MOV、MKV 等格式,单个文件不超过 2GB

-
- {FORMAT_LIST.map((fmt) => ( - - {fmt} - - ))} -
-
+ onClick={handleSelectFile} + /> {/* 隐藏的文件 input */} { onChange={handleFileChange} /> - {/* 上传按钮 */} -
- -
+ - {/* 上传进度 */} - {uploading && ( -
-
- - 查重中... -
-

正在分析视频内容,请稍候...

-
- )} + {uploading && } - {/* 上传结果 */} {uploadResult && !uploading && ( -
-
-

查重任务已提交

-

{uploadResult.message}

-
- - -
-
+ navigate("/app/duplication/results")} + onReset={handleReset} + /> )}
{/* 右侧:格式说明 + 提示 */} -
-

📋 查重说明

-
    -
  • 系统会对比您上传的视频与视频库中的已有视频
  • -
  • 查重完成后,可查看重复片段的具体位置
  • -
  • 查重过程通常需要几分钟,取决于视频大小
  • -
  • 高相似度片段建议进行替换或裁剪
  • -
- -

🎬 支持格式

-
- {FORMAT_LIST.map((fmt) => ( - - {fmt} - - ))} -
- -

💡 温馨提示

-
    -
  • 单个文件不超过 2GB
  • -
  • 视频时长建议不超过 60 分钟
  • -
  • 查重结果可在「查重记录」中随时查看
  • -
-
+
) diff --git a/apps/web/src/pages/duplication/constants.ts b/apps/web/src/pages/duplication/constants.ts old mode 100644 new mode 100755 index 571bf8afb..289bc3ddc --- a/apps/web/src/pages/duplication/constants.ts +++ b/apps/web/src/pages/duplication/constants.ts @@ -51,7 +51,13 @@ export const FILTER_OPTIONS: { key: RiskFilter; label: string }[] = [ { key: "high", label: "高风险" }, ] -/** Toast 类型 */ +/** 支持的视频格式 */ +export const ACCEPT_FORMATS = ".mp4,.avi,.mov,.mkv,.wmv,.flv,.webm" +export const FORMAT_LIST = ["MP4", "AVI", "MOV", "MKV", "WMV", "FLV", "WebM"] +/** 最大文件大小:2GB */ +export const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024 + +/** 简易 toast */ export interface ToastState { message: string type: "success" | "error" | "warning" diff --git a/apps/web/src/pages/duplication/duplication-upload/InfoSidebar.tsx b/apps/web/src/pages/duplication/duplication-upload/InfoSidebar.tsx new file mode 100755 index 000000000..c226f2db4 --- /dev/null +++ b/apps/web/src/pages/duplication/duplication-upload/InfoSidebar.tsx @@ -0,0 +1,36 @@ +import React from "react" +import { Tag } from "@/components/ui" +import { FORMAT_LIST } from "./constants" + +/** 右侧说明卡 */ +const InfoSidebar: React.FC = () => { + return ( +
+

📋 查重说明

+
    +
  • 系统会对比您上传的视频与视频库中的已有视频
  • +
  • 查重完成后,可查看重复片段的具体位置
  • +
  • 查重过程通常需要几分钟,取决于视频大小
  • +
  • 高相似度片段建议进行替换或裁剪
  • +
+ +

🎬 支持格式

+
+ {FORMAT_LIST.map((fmt) => ( + + {fmt} + + ))} +
+ +

💡 温馨提示

+
    +
  • 单个文件不超过 2GB
  • +
  • 视频时长建议不超过 60 分钟
  • +
  • 查重结果可在「查重记录」中随时查看
  • +
+
+ ) +} + +export default InfoSidebar diff --git a/apps/web/src/pages/duplication/duplication-upload/UploadActions.tsx b/apps/web/src/pages/duplication/duplication-upload/UploadActions.tsx new file mode 100755 index 000000000..743e995c1 --- /dev/null +++ b/apps/web/src/pages/duplication/duplication-upload/UploadActions.tsx @@ -0,0 +1,20 @@ +import React from "react" +import { Button } from "@/components/ui" + +interface UploadActionsProps { + uploading: boolean + onSelectFile: () => void +} + +/** 上传按钮区 */ +const UploadActions: React.FC = ({ uploading, onSelectFile }) => { + return ( +
+ +
+ ) +} + +export default UploadActions diff --git a/apps/web/src/pages/duplication/duplication-upload/UploadProgress.tsx b/apps/web/src/pages/duplication/duplication-upload/UploadProgress.tsx new file mode 100755 index 000000000..85aad6e87 --- /dev/null +++ b/apps/web/src/pages/duplication/duplication-upload/UploadProgress.tsx @@ -0,0 +1,16 @@ +import React from "react" + +/** 上传进度展示 */ +const UploadProgress: React.FC = () => { + return ( +
+
+ + 查重中... +
+

正在分析视频内容,请稍候...

+
+ ) +} + +export default UploadProgress diff --git a/apps/web/src/pages/duplication/duplication-upload/UploadResultPanel.tsx b/apps/web/src/pages/duplication/duplication-upload/UploadResultPanel.tsx new file mode 100755 index 000000000..643c10b05 --- /dev/null +++ b/apps/web/src/pages/duplication/duplication-upload/UploadResultPanel.tsx @@ -0,0 +1,30 @@ +import React from "react" +import { Button } from "@/components/ui" +import type { UploadResult } from "./constants" + +interface UploadResultPanelProps { + result: UploadResult + onViewResult: () => void + onReset: () => void +} + +/** 上传结果展示 */ +const UploadResultPanel: React.FC = ({ result, onViewResult, onReset }) => { + return ( +
+
+

查重任务已提交

+

{result.message}

+
+ + +
+
+ ) +} + +export default UploadResultPanel diff --git a/apps/web/src/pages/duplication/duplication-upload/UploadZone.tsx b/apps/web/src/pages/duplication/duplication-upload/UploadZone.tsx new file mode 100755 index 000000000..053ff21de --- /dev/null +++ b/apps/web/src/pages/duplication/duplication-upload/UploadZone.tsx @@ -0,0 +1,45 @@ +import React from "react" +import { Tag } from "@/components/ui" +import { FORMAT_LIST } from "./constants" + +interface UploadZoneProps { + dragging: boolean + uploading: boolean + onDragOver: (e: React.DragEvent) => void + onDragLeave: () => void + onDrop: (e: React.DragEvent) => void + onClick: () => void +} + +/** 拖拽上传区 */ +const UploadZone: React.FC = ({ + dragging, + uploading, + onDragOver, + onDragLeave, + onDrop, + onClick, +}) => { + return ( +
+
{uploading ? "⏳" : "📁"}
+

{uploading ? "正在上传并查重..." : "点击或拖拽视频文件到此区域"}

+

支持 MP4、AVI、MOV、MKV 等格式,单个文件不超过 2GB

+
+ {FORMAT_LIST.map((fmt) => ( + + {fmt} + + ))} +
+
+ ) +} + +export default UploadZone diff --git a/apps/web/src/pages/duplication/duplication-upload/constants.ts b/apps/web/src/pages/duplication/duplication-upload/constants.ts new file mode 100755 index 000000000..97ed63f8a --- /dev/null +++ b/apps/web/src/pages/duplication/duplication-upload/constants.ts @@ -0,0 +1,17 @@ +/** 支持的视频格式 */ +export const ACCEPT_FORMATS = ".mp4,.avi,.mov,.mkv,.wmv,.flv,.webm" +export const FORMAT_LIST = ["MP4", "AVI", "MOV", "MKV", "WMV", "FLV", "WebM"] +/** 最大文件大小:2GB */ +export const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024 + +/** 简易 toast */ +export interface ToastState { + message: string + type: "success" | "error" | "warning" +} + +/** 上传结果 */ +export interface UploadResult { + id: string + message: string +} diff --git a/apps/web/src/pages/duplication/duplication-upload/useDuplicationUpload.ts b/apps/web/src/pages/duplication/duplication-upload/useDuplicationUpload.ts new file mode 100755 index 000000000..ce996a107 --- /dev/null +++ b/apps/web/src/pages/duplication/duplication-upload/useDuplicationUpload.ts @@ -0,0 +1,115 @@ +import { useState, useRef, useCallback } from "react" +import { useMutation } from "@tanstack/react-query" +import { uploadForDuplication } from "@/api/duplication" +import { ACCEPT_FORMATS, FORMAT_LIST, MAX_FILE_SIZE } from "./constants" +import type { ToastState, UploadResult } from "./constants" + +/** + * 查重上传逻辑 Hook + * 封装文件校验、上传 mutation、toast 提示 + */ +export function useDuplicationUpload() { + const fileInputRef = useRef(null) + const [dragging, setDragging] = useState(false) + const [uploading, setUploading] = useState(false) + const [uploadResult, setUploadResult] = useState(null) + const [toast, setToast] = useState(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 = useCallback(() => { + fileInputRef.current?.click() + }, []) + + const handleFileChange = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) handleFile(file) + // 重置 input 以便重复选择同一文件 + e.target.value = "" + }, + [handleFile], + ) + + /** 重置状态 */ + const handleReset = useCallback(() => { + setUploadResult(null) + setUploading(false) + }, []) + + return { + // refs + fileInputRef, + // 状态 + dragging, + uploading, + uploadResult, + toast, + // 事件 + handleDragOver, + handleDragLeave, + handleDrop, + handleSelectFile, + handleFileChange, + handleReset, + } +} diff --git a/apps/web/src/test/api/duplication.test.ts b/apps/web/src/test/api/duplication.test.ts old mode 100644 new mode 100755 index d7028b154..69718a88c --- a/apps/web/src/test/api/duplication.test.ts +++ b/apps/web/src/test/api/duplication.test.ts @@ -1,3 +1,4 @@ +// 重构:DuplicationUpload 页面已拆分为子组件(UploadZone/InfoSidebar/UploadProgress等) import { describe, expect, it, vi, beforeEach } from "vitest" import { uploadForDuplication,