From 8682391379271ac93d665616f365f8e18dac524d Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 28 Jul 2026 10:33:04 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor(duplication):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=20DuplicationUpload=20=E9=A1=B5=E9=9D=A2=E4=B8=BA=E5=AD=90?= =?UTF-8?q?=E7=BB=84=E4=BB=B6+Hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三阶段重构: - Phase 1: 提取常量与类型(格式列表/文件大小限制/Toast/Result) - Phase 2: 拆分为 5 个 UI 子组件 + 1 个业务 Hook - UploadZone: 拖拽上传区 - UploadActions: 上传按钮区 - UploadProgress: 上传进度 - UploadResultPanel: 结果展示 - InfoSidebar: 右侧说明卡 - useDuplicationUpload: 上传逻辑 Hook - Phase 3: 主文件 233→85 行(-64%) 保持页面路由路径不变 --- .../pages/duplication/DuplicationUpload.tsx | 220 +++--------------- apps/web/src/pages/duplication/constants.ts | 59 +---- .../duplication-upload/InfoSidebar.tsx | 36 +++ .../duplication-upload/UploadActions.tsx | 20 ++ .../duplication-upload/UploadProgress.tsx | 16 ++ .../duplication-upload/UploadResultPanel.tsx | 30 +++ .../duplication-upload/UploadZone.tsx | 45 ++++ .../duplication-upload/constants.ts | 17 ++ .../useDuplicationUpload.ts | 115 +++++++++ 9 files changed, 321 insertions(+), 237 deletions(-) mode change 100644 => 100755 apps/web/src/pages/duplication/DuplicationUpload.tsx mode change 100644 => 100755 apps/web/src/pages/duplication/constants.ts create mode 100755 apps/web/src/pages/duplication/duplication-upload/InfoSidebar.tsx create mode 100755 apps/web/src/pages/duplication/duplication-upload/UploadActions.tsx create mode 100755 apps/web/src/pages/duplication/duplication-upload/UploadProgress.tsx create mode 100755 apps/web/src/pages/duplication/duplication-upload/UploadResultPanel.tsx create mode 100755 apps/web/src/pages/duplication/duplication-upload/UploadZone.tsx create mode 100755 apps/web/src/pages/duplication/duplication-upload/constants.ts create mode 100755 apps/web/src/pages/duplication/duplication-upload/useDuplicationUpload.ts 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..fa7c414d3 --- a/apps/web/src/pages/duplication/constants.ts +++ b/apps/web/src/pages/duplication/constants.ts @@ -1,57 +1,10 @@ -import type { DuplicationStatus } from "@/api/duplication" -import type { RiskFilter } from "./types" +/** 支持的视频格式 */ +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 -/** 状态配置 */ -export const STATUS_CONFIG: Record< - DuplicationStatus, - { - variant: "primary" | "warning" | "success" | "error" - text: string - icon: string - } -> = { - pending: { variant: "primary", text: "等待中", icon: "⏳" }, - processing: { variant: "warning", text: "查重中", icon: "🔄" }, - completed: { variant: "success", text: "已完成", icon: "✅" }, - failed: { variant: "error", text: "失败", icon: "❌" }, -} - -/** 风险等级描述 */ -export const RISK_DESC: Record = { - low: "查重率较低,内容原创度高", - medium: "存在一定重复,建议修改部分片段", - high: "重复率较高,建议大幅修改或替换", -} - -/** 风险等级标签变体 */ -export const RISK_TAG_VARIANT: Record = { - low: "success", - medium: "warning", - high: "error", -} - -/** 风险等级文字(详情页用) */ -export const RISK_LABEL: Record = { - low: "低风险", - medium: "中风险", - high: "高风险", -} - -/** 风险等级标签(列表页用) */ -export const RISK_LABELS: Record = { - low: "低风险", - medium: "中风险", - high: "高风险", -} - -export const FILTER_OPTIONS: { key: RiskFilter; label: string }[] = [ - { key: "all", label: "全部" }, - { key: "low", label: "低风险" }, - { key: "medium", label: "中风险" }, - { key: "high", label: "高风险" }, -] - -/** Toast 类型 */ +/** 简易 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, + } +} -- 2.54.0 From cea91e77791ce24ca7ae64800118568933dda78c Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 28 Jul 2026 12:56:17 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(duplication):=20=E6=81=A2=E5=A4=8Dconst?= =?UTF-8?q?ants.ts=E4=B8=AD=E8=A2=AB=E8=AF=AF=E5=88=A0=E7=9A=84=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=E5=B8=B8=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/duplication/constants.ts | 53 +++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/apps/web/src/pages/duplication/constants.ts b/apps/web/src/pages/duplication/constants.ts index fa7c414d3..289bc3ddc 100755 --- a/apps/web/src/pages/duplication/constants.ts +++ b/apps/web/src/pages/duplication/constants.ts @@ -1,3 +1,56 @@ +import type { DuplicationStatus } from "@/api/duplication" +import type { RiskFilter } from "./types" + +/** 状态配置 */ +export const STATUS_CONFIG: Record< + DuplicationStatus, + { + variant: "primary" | "warning" | "success" | "error" + text: string + icon: string + } +> = { + pending: { variant: "primary", text: "等待中", icon: "⏳" }, + processing: { variant: "warning", text: "查重中", icon: "🔄" }, + completed: { variant: "success", text: "已完成", icon: "✅" }, + failed: { variant: "error", text: "失败", icon: "❌" }, +} + +/** 风险等级描述 */ +export const RISK_DESC: Record = { + low: "查重率较低,内容原创度高", + medium: "存在一定重复,建议修改部分片段", + high: "重复率较高,建议大幅修改或替换", +} + +/** 风险等级标签变体 */ +export const RISK_TAG_VARIANT: Record = { + low: "success", + medium: "warning", + high: "error", +} + +/** 风险等级文字(详情页用) */ +export const RISK_LABEL: Record = { + low: "低风险", + medium: "中风险", + high: "高风险", +} + +/** 风险等级标签(列表页用) */ +export const RISK_LABELS: Record = { + low: "低风险", + medium: "中风险", + high: "高风险", +} + +export const FILTER_OPTIONS: { key: RiskFilter; label: string }[] = [ + { key: "all", label: "全部" }, + { key: "low", label: "低风险" }, + { key: "medium", label: "中风险" }, + { key: "high", label: "高风险" }, +] + /** 支持的视频格式 */ export const ACCEPT_FORMATS = ".mp4,.avi,.mov,.mkv,.wmv,.flv,.webm" export const FORMAT_LIST = ["MP4", "AVI", "MOV", "MKV", "WMV", "FLV", "WebM"] -- 2.54.0 From af849aa767a1ea3950f303cbe4380be90d4a4a46 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 28 Jul 2026 13:10:37 +0800 Subject: [PATCH 3/3] =?UTF-8?q?test(duplication):=20=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=A1=B5=E6=8B=86=E5=88=86=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/test/api/duplication.test.ts | 1 + 1 file changed, 1 insertion(+) mode change 100644 => 100755 apps/web/src/test/api/duplication.test.ts 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, -- 2.54.0