diff --git a/apps/web/src/pages/duplication/DuplicationDetail.tsx b/apps/web/src/pages/duplication/DuplicationDetail.tsx old mode 100644 new mode 100755 index 2cfe869a8..c2107adbc --- a/apps/web/src/pages/duplication/DuplicationDetail.tsx +++ b/apps/web/src/pages/duplication/DuplicationDetail.tsx @@ -3,137 +3,29 @@ * 风险评估 + 基本信息 + 检测项列表 + 匹配片段 * 零 antd 依赖 */ -import React, { useState, useCallback } from "react" -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" -import { Button, Tag, Tooltip } from "@/components/ui" -import { useParams, useNavigate } from "react-router-dom" -import { getDuplicationDetail, retryDuplication, type DuplicateSegment } from "@/api/duplication" -import "./duplication.css" +import React from "react" +import { Button, Tag } from "@/components/ui" import PageHead from "@/components/layout/PageHead" - -/** 格式化时间(秒 → mm:ss) */ -const formatTime = (seconds: number) => { - const m = Math.floor(seconds / 60) - const s = Math.floor(seconds % 60) - return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}` -} - -/** 格式化文件大小 */ -const formatSize = (bytes: number) => { - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB` - if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB` - return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB` -} - -/** 格式化时长 */ -const formatDuration = (seconds?: number) => { - if (!seconds) return "-" - const totalSec = Math.round(seconds) - const m = Math.floor(totalSec / 60) - const s = totalSec % 60 - return m > 0 ? `${m}分${s}秒` : `${s}秒` -} - -/** 根据查重率获取风险等级 */ -const getRiskLevel = (rate?: number): "low" | "medium" | "high" => { - if (rate === undefined) return "low" - if (rate <= 10) return "low" - if (rate <= 30) return "medium" - return "high" -} - -/** 风险等级描述 */ -const RISK_DESC: Record = { - low: "查重率较低,内容原创度高", - medium: "存在一定重复,建议修改部分片段", - high: "重复率较高,建议大幅修改或替换", -} - -/** 风险等级标签变体 */ -const RISK_TAG_VARIANT: Record = { - low: "success", - medium: "warning", - high: "error", -} - -/** 风险等级文字 */ -const RISK_LABEL: Record = { - low: "低风险", - medium: "中风险", - high: "高风险", -} - -/** 简易 toast */ -interface ToastState { - message: string - type: "success" | "error" | "warning" -} - -/** 单个重复片段卡片 */ -const SegmentCard: React.FC<{ segment: DuplicateSegment; index: number }> = ({ - segment, - index, -}) => { - const sourceDuration = segment.source_end - segment.source_start - const matchedDuration = segment.matched_end - segment.matched_start - const riskLevel = segment.similarity >= 90 ? "high" : segment.similarity >= 70 ? "medium" : "low" - - return ( -
-
🎬
-
-

- 片段 {index + 1}:{segment.matched_video_name} -

-

- 原始 {formatTime(segment.source_start)} - {formatTime(segment.source_end)}( - {sourceDuration.toFixed(0)}s)→ 匹配 {formatTime(segment.matched_start)} -{" "} - {formatTime(segment.matched_end)}({matchedDuration.toFixed(0)}s) -

-
-
-
-
- {segment.similarity.toFixed(1)}% -
- ) -} +import { RiskCard } from "./components/RiskCard" +import { InfoCard } from "./components/InfoCard" +import { SegmentsSection } from "./components/SegmentsSection" +import { useDuplicationDetail } from "./hooks/useDuplicationDetail" +import { RISK_TAG_VARIANT, RISK_LABEL } from "./constants" +import { formatSize, formatDuration } from "./utils" +import "./duplication.css" const DuplicationDetail: React.FC = () => { - const { id } = useParams<{ id: string }>() - const navigate = useNavigate() - const queryClient = useQueryClient() - const [toast, setToast] = useState(null) - - const showToast = useCallback((message: string, type: "success" | "error" | "warning") => { - setToast({ message, type }) - setTimeout(() => setToast(null), 3000) - }, []) - const { - data: detail, + detail, isLoading, isError, - } = useQuery({ - queryKey: ["duplication-detail", id], - queryFn: () => getDuplicationDetail(id!), - enabled: !!id, - }) - - // 重新查重 - const retryMutation = useMutation({ - mutationFn: retryDuplication, - onSuccess: () => { - showToast("已重新提交查重", "success") - queryClient.invalidateQueries({ queryKey: ["duplication-detail", id] }) - }, - onError: () => { - showToast("重新查重失败", "error") - }, - }) + toast, + riskLevel, + similarityPercent, + handleRetry, + handleDownloadReport, + handleBack, + } = useDuplicationDetail() if (isLoading) { return ( @@ -155,7 +47,7 @@ const DuplicationDetail: React.FC = () => { {detail.status === "failed" && ( - )} @@ -218,103 +96,9 @@ const DuplicationDetail: React.FC = () => { {/* 内容网格 */}
- {/* 风险评估卡片 */} -
-

📊 风险评估

-
- {similarityPercent}% - 查重率 -
-

{RISK_DESC[riskLevel]}

-
- - {/* 基本信息卡片 */} -
-

📋 基本信息

-
-
- 文件名 - - - {detail.filename} - - -
-
- 文件大小 - {formatSize(detail.file_size)} -
-
- 视频时长 - {formatDuration(detail.duration_seconds)} -
-
- 查重状态 - - - {detail.status === "completed" - ? "✅ 已完成" - : detail.status === "failed" - ? "❌ 失败" - : detail.status === "processing" - ? "🔄 查重中" - : "⏳ 等待中"} - - -
-
- 重复片段数 - {detail.duplicate_count ?? 0} 个 -
-
- 提交时间 - - {new Date(detail.created_at).toLocaleString("zh-CN")} - -
-
-
- - {/* 检测项列表 */} -
-

- 🔍 重复片段详情 - - {detail.segments?.length ?? 0} 个片段 - -

- - {detail.segments && detail.segments.length > 0 ? ( -
- {detail.segments.map((segment, index) => ( - - ))} -
- ) : ( -
-
🎉
-

未发现重复片段,内容原创度很高

-
- )} -
+ + +
) diff --git a/apps/web/src/pages/duplication/components/InfoCard.tsx b/apps/web/src/pages/duplication/components/InfoCard.tsx new file mode 100644 index 000000000..2286246fc --- /dev/null +++ b/apps/web/src/pages/duplication/components/InfoCard.tsx @@ -0,0 +1,72 @@ +import React from "react" +import { Tag, Tooltip } from "@/components/ui" +import { formatSize, formatDuration } from "../utils" +import type { DuplicationDetail } from "@/api/duplication" + +interface InfoCardProps { + detail: DuplicationDetail +} + +/** + * 基本信息卡片 + */ +export const InfoCard: React.FC = ({ detail }) => { + const statusMap: Record = { + completed: { text: "✅ 已完成", variant: "success" }, + failed: { text: "❌ 失败", variant: "error" }, + processing: { text: "🔄 查重中", variant: "warning" }, + pending: { text: "⏳ 等待中", variant: "info" }, + } + const status = statusMap[detail.status] || { + text: detail.status, + variant: "info", + } + + return ( +
+

📋 基本信息

+
+
+ 文件名 + + + {detail.filename} + + +
+
+ 文件大小 + {formatSize(detail.file_size)} +
+
+ 视频时长 + {formatDuration(detail.duration_seconds)} +
+
+ 查重状态 + + {status.text} + +
+
+ 重复片段数 + {detail.duplicate_count ?? 0} 个 +
+
+ 提交时间 + + {new Date(detail.created_at).toLocaleString("zh-CN")} + +
+
+
+ ) +} diff --git a/apps/web/src/pages/duplication/components/RiskCard.tsx b/apps/web/src/pages/duplication/components/RiskCard.tsx new file mode 100644 index 000000000..cdc0e4856 --- /dev/null +++ b/apps/web/src/pages/duplication/components/RiskCard.tsx @@ -0,0 +1,21 @@ +import React from "react" +import { RISK_DESC } from "../constants" + +interface RiskCardProps { + riskLevel: string + similarityPercent: string +} + +/** + * 风险评估卡片 + */ +export const RiskCard: React.FC = ({ riskLevel, similarityPercent }) => ( +
+

📊 风险评估

+
+ {similarityPercent}% + 查重率 +
+

{RISK_DESC[riskLevel]}

+
+) diff --git a/apps/web/src/pages/duplication/components/SegmentCard.tsx b/apps/web/src/pages/duplication/components/SegmentCard.tsx new file mode 100644 index 000000000..fc66f0cb7 --- /dev/null +++ b/apps/web/src/pages/duplication/components/SegmentCard.tsx @@ -0,0 +1,40 @@ +import React from "react" +import type { DuplicateSegment } from "@/api/duplication" +import { formatTime } from "../utils" + +interface SegmentCardProps { + segment: DuplicateSegment + index: number +} + +/** + * 单个重复片段卡片 + */ +export const SegmentCard: React.FC = ({ segment, index }) => { + const sourceDuration = segment.source_end - segment.source_start + const matchedDuration = segment.matched_end - segment.matched_start + const riskLevel = segment.similarity >= 90 ? "high" : segment.similarity >= 70 ? "medium" : "low" + + return ( +
+
🎬
+
+

+ 片段 {index + 1}:{segment.matched_video_name} +

+

+ 原始 {formatTime(segment.source_start)} - {formatTime(segment.source_end)}( + {sourceDuration.toFixed(0)}s)→ 匹配 {formatTime(segment.matched_start)} -{" "} + {formatTime(segment.matched_end)}({matchedDuration.toFixed(0)}s) +

+
+
+
+
+ {segment.similarity.toFixed(1)}% +
+ ) +} diff --git a/apps/web/src/pages/duplication/components/SegmentsSection.tsx b/apps/web/src/pages/duplication/components/SegmentsSection.tsx new file mode 100644 index 000000000..c6ebaccb3 --- /dev/null +++ b/apps/web/src/pages/duplication/components/SegmentsSection.tsx @@ -0,0 +1,35 @@ +import React from "react" +import { Tag } from "@/components/ui" +import type { DuplicateSegment } from "@/api/duplication" +import { SegmentCard } from "./SegmentCard" + +interface SegmentsSectionProps { + segments?: DuplicateSegment[] +} + +/** + * 重复片段列表区域 + */ +export const SegmentsSection: React.FC = ({ segments = [] }) => ( +
+

+ 🔍 重复片段详情 + + {segments.length} 个片段 + +

+ + {segments.length > 0 ? ( +
+ {segments.map((segment, index) => ( + + ))} +
+ ) : ( +
+
🎉
+

未发现重复片段,内容原创度很高

+
+ )} +
+) diff --git a/apps/web/src/pages/duplication/constants.ts b/apps/web/src/pages/duplication/constants.ts index 16d6d65ff..571bf8afb 100644 --- a/apps/web/src/pages/duplication/constants.ts +++ b/apps/web/src/pages/duplication/constants.ts @@ -16,7 +16,28 @@ export const STATUS_CONFIG: Record< 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: "中风险", @@ -29,3 +50,9 @@ export const FILTER_OPTIONS: { key: RiskFilter; label: string }[] = [ { key: "medium", label: "中风险" }, { key: "high", label: "高风险" }, ] + +/** Toast 类型 */ +export interface ToastState { + message: string + type: "success" | "error" | "warning" +} diff --git a/apps/web/src/pages/duplication/hooks/useDuplicationDetail.ts b/apps/web/src/pages/duplication/hooks/useDuplicationDetail.ts new file mode 100644 index 000000000..74f15eea8 --- /dev/null +++ b/apps/web/src/pages/duplication/hooks/useDuplicationDetail.ts @@ -0,0 +1,77 @@ +import { useState, useCallback } from "react" +import { useParams, useNavigate } from "react-router-dom" +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { getDuplicationDetail, retryDuplication } from "@/api/duplication" +import type { ToastState } from "../constants" +import { getRiskLevel } from "../utils" + +/** + * 查重详情业务 Hook + */ +export const useDuplicationDetail = () => { + const { id } = useParams<{ id: string }>() + const navigate = useNavigate() + const queryClient = useQueryClient() + const [toast, setToast] = useState(null) + + const showToast = useCallback((message: string, type: "success" | "error" | "warning") => { + setToast({ message, type }) + setTimeout(() => setToast(null), 3000) + }, []) + + const { + data: detail, + isLoading, + isError, + } = useQuery({ + queryKey: ["duplication-detail", id], + queryFn: () => getDuplicationDetail(id!), + enabled: !!id, + }) + + // 重新查重 + const retryMutation = useMutation({ + mutationFn: retryDuplication, + onSuccess: () => { + showToast("已重新提交查重", "success") + queryClient.invalidateQueries({ queryKey: ["duplication-detail", id] }) + }, + onError: () => { + showToast("重新查重失败", "error") + }, + }) + + const handleRetry = useCallback(() => { + if (!detail) return + retryMutation.mutate(detail.id) + }, [detail, retryMutation]) + + const handleDownloadReport = useCallback(() => { + showToast("报告下载功能开发中", "warning") + }, [showToast]) + + const handleBack = useCallback(() => { + navigate("/app/duplication/results") + }, [navigate]) + + const riskLevel = detail ? getRiskLevel(detail.duplicate_rate) : "low" + const similarityPercent = + detail?.duplicate_rate !== undefined ? detail.duplicate_rate.toFixed(1) : "—" + + return { + // 数据 + detail, + isLoading, + isError, + // 状态 + toast, + riskLevel, + similarityPercent, + retryLoading: retryMutation.isPending, + // 操作 + showToast, + handleRetry, + handleDownloadReport, + handleBack, + } +} diff --git a/apps/web/src/pages/duplication/utils.ts b/apps/web/src/pages/duplication/utils.ts index 673df26e3..87b1798b4 100644 --- a/apps/web/src/pages/duplication/utils.ts +++ b/apps/web/src/pages/duplication/utils.ts @@ -6,6 +6,13 @@ export const getRiskLevel = (rate?: number): "low" | "medium" | "high" => { return "high" } +/** 格式化时间(秒 → mm:ss) */ +export const formatTime = (seconds: number) => { + const m = Math.floor(seconds / 60) + const s = Math.floor(seconds % 60) + return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}` +} + /** 格式化文件大小 */ export const formatSize = (bytes: number) => { if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`