From abc5877bf660633b5d12f56223e5a6ef30be1427 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 09:39:23 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20=E6=8B=86=E5=88=86=20Duplicatio?= =?UTF-8?q?nResults=20=E9=A1=B5=E9=9D=A2=EF=BC=88275=E2=86=9267=E8=A1=8C?= =?UTF-8?q?=EF=BC=8C-76%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 抽 types/constants/utils 为独立文件 - 抽 3 个 UI 组件:FilterBar / EmptyState / ResultCard - 抽业务 Hook:useDuplicationResults - 主文件精简为纯组合层,保持导入路径向后兼容 --- .../pages/duplication/DuplicationResults.tsx | 272 +++--------------- .../duplication/components/EmptyState.tsx | 32 +++ .../duplication/components/FilterBar.tsx | 26 ++ .../duplication/components/ResultCard.tsx | 95 ++++++ apps/web/src/pages/duplication/constants.ts | 31 ++ .../hooks/useDuplicationResults.ts | 106 +++++++ apps/web/src/pages/duplication/types.ts | 8 + apps/web/src/pages/duplication/utils.ts | 23 ++ 8 files changed, 353 insertions(+), 240 deletions(-) create mode 100644 apps/web/src/pages/duplication/components/EmptyState.tsx create mode 100644 apps/web/src/pages/duplication/components/FilterBar.tsx create mode 100644 apps/web/src/pages/duplication/components/ResultCard.tsx create mode 100644 apps/web/src/pages/duplication/constants.ts create mode 100644 apps/web/src/pages/duplication/hooks/useDuplicationResults.ts create mode 100644 apps/web/src/pages/duplication/types.ts create mode 100644 apps/web/src/pages/duplication/utils.ts diff --git a/apps/web/src/pages/duplication/DuplicationResults.tsx b/apps/web/src/pages/duplication/DuplicationResults.tsx index e311c9a89..68cd47cb2 100644 --- a/apps/web/src/pages/duplication/DuplicationResults.tsx +++ b/apps/web/src/pages/duplication/DuplicationResults.tsx @@ -2,130 +2,27 @@ * 查重结果列表页面 — V21 设计系统 * 胶囊筛选 + 卡片列表,零 antd 依赖 */ -import React, { useState, useMemo } from "react" -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" -import { Button, Tag, Tooltip } from "@/components/ui" -import { useNavigate } from "react-router-dom" -import { - getDuplicationRecords, - deleteDuplicationRecord, - retryDuplication, - type DuplicationStatus, -} from "@/api/duplication" -import "./duplication.css" +import React from "react" +import { Button } from "@/components/ui" import PageHead from "@/components/layout/PageHead" - -/** 风险等级分类 */ -type RiskFilter = "all" | "high" | "medium" | "low" - -/** 状态配置 */ -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: "❌" }, -} - -/** 根据查重率获取风险等级 */ -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_LABELS: Record = { - low: "低风险", - medium: "中风险", - high: "高风险", -} - -/** 格式化文件大小 */ -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}秒` -} - -/** 简易 toast */ -interface ToastState { - message: string - type: "success" | "error" | "warning" -} - -const FILTER_OPTIONS: { key: RiskFilter; label: string }[] = [ - { key: "all", label: "全部" }, - { key: "low", label: "低风险" }, - { key: "medium", label: "中风险" }, - { key: "high", label: "高风险" }, -] +import "./duplication.css" +import useDuplicationResults from "./hooks/useDuplicationResults" +import FilterBar from "./components/FilterBar" +import EmptyState from "./components/EmptyState" +import ResultCard from "./components/ResultCard" const DuplicationResults: React.FC = () => { - const navigate = useNavigate() - const queryClient = useQueryClient() - const [riskFilter, setRiskFilter] = useState("all") - const [toast, setToast] = useState(null) - - const showToast = (message: string, type: "success" | "error" | "warning") => { - setToast({ message, type }) - setTimeout(() => setToast(null), 3000) - } - - // 获取查重记录 - const { data: records = [], isLoading } = useQuery({ - queryKey: ["duplication-records"], - queryFn: getDuplicationRecords, - }) - - // 删除 - const deleteMutation = useMutation({ - mutationFn: deleteDuplicationRecord, - onSuccess: () => { - showToast("已删除", "success") - queryClient.invalidateQueries({ queryKey: ["duplication-records"] }) - }, - onError: () => { - showToast("删除失败", "error") - }, - }) - - // 重新查重 - const retryMutation = useMutation({ - mutationFn: retryDuplication, - onSuccess: () => { - showToast("已重新提交查重", "success") - queryClient.invalidateQueries({ queryKey: ["duplication-records"] }) - }, - onError: () => { - showToast("重新查重失败", "error") - }, - }) - - /** 按风险等级筛选 */ - const filteredRecords = useMemo(() => { - if (riskFilter === "all") return records - return records.filter((r) => { - if (r.status !== "completed") return riskFilter === "low" - return getRiskLevel(r.duplicate_rate) === riskFilter - }) - }, [records, riskFilter]) + const { + isLoading, + filteredRecords, + riskFilter, + setRiskFilter, + toast, + handleDelete, + handleRetry, + handleView, + handleUpload, + } = useDuplicationResults() return (
@@ -136,136 +33,31 @@ const DuplicationResults: React.FC = () => { title="查重记录" actions={
- {/* 筛选胶囊 */} -
- {FILTER_OPTIONS.map((opt) => ( - - ))} -
-
} /> - {/* 加载中 */} - {isLoading && ( -
-
-

加载中...

-
- )} - - {/* 空状态 */} - {!isLoading && filteredRecords.length === 0 && ( -
-
📭
-

- {riskFilter === "all" - ? "暂无查重记录,上传视频开始查重吧" - : `没有${RISK_LABELS[riskFilter]}的记录`} -

-
+ {/* 加载中 / 空状态 */} + {(isLoading || filteredRecords.length === 0) && ( + )} {/* 结果卡片列表 */} {!isLoading && filteredRecords.length > 0 && (
- {filteredRecords.map((record) => { - const statusCfg = STATUS_CONFIG[record.status] - const riskLevel = getRiskLevel(record.duplicate_rate) - const rateValue = record.duplicate_rate - - return ( -
{ - if (record.status === "completed") { - navigate(`/duplication/${record.id}`) - } - }} - > - {/* 缩略图 */} -
🎬
- - {/* 信息区 */} -
-

{record.filename}

-
- - {statusCfg.icon} {statusCfg.text} - - {formatSize(record.file_size)} - {formatDuration(record.duration_seconds)} - {new Date(record.created_at).toLocaleDateString("zh-CN")} - {record.status === "completed" && record.duplicate_count !== undefined && ( - {record.duplicate_count} 个重复片段 - )} -
-
- - {/* 查重率 */} -
- {record.status === "completed" && rateValue !== undefined ? ( - <> -
-
-
- - {rateValue.toFixed(1)}% - - - ) : record.status === "failed" ? ( - - - - ) : ( - - {record.status === "processing" ? "分析中..." : "—"} - - )} -
- - {/* 删除按钮 */} - -
- ) - })} + {filteredRecords.map((record) => ( + + ))}
)}
diff --git a/apps/web/src/pages/duplication/components/EmptyState.tsx b/apps/web/src/pages/duplication/components/EmptyState.tsx new file mode 100644 index 000000000..2b14c6944 --- /dev/null +++ b/apps/web/src/pages/duplication/components/EmptyState.tsx @@ -0,0 +1,32 @@ +import React from "react" +import { RISK_LABELS } from "../constants" +import type { RiskFilter } from "../types" + +interface EmptyStateProps { + isLoading: boolean + riskFilter: RiskFilter +} + +const EmptyState: React.FC = ({ isLoading, riskFilter }) => { + if (isLoading) { + return ( +
+
+

加载中...

+
+ ) + } + + return ( +
+
📭
+

+ {riskFilter === "all" + ? "暂无查重记录,上传视频开始查重吧" + : `没有${RISK_LABELS[riskFilter]}的记录`} +

+
+ ) +} + +export default EmptyState diff --git a/apps/web/src/pages/duplication/components/FilterBar.tsx b/apps/web/src/pages/duplication/components/FilterBar.tsx new file mode 100644 index 000000000..a945841e4 --- /dev/null +++ b/apps/web/src/pages/duplication/components/FilterBar.tsx @@ -0,0 +1,26 @@ +import React from "react" +import type { RiskFilter } from "../types" +import { FILTER_OPTIONS } from "../constants" + +interface FilterBarProps { + value: RiskFilter + onChange: (value: RiskFilter) => void +} + +const FilterBar: React.FC = ({ value, onChange }) => { + return ( +
+ {FILTER_OPTIONS.map((opt) => ( + + ))} +
+ ) +} + +export default FilterBar diff --git a/apps/web/src/pages/duplication/components/ResultCard.tsx b/apps/web/src/pages/duplication/components/ResultCard.tsx new file mode 100644 index 000000000..11d68139f --- /dev/null +++ b/apps/web/src/pages/duplication/components/ResultCard.tsx @@ -0,0 +1,95 @@ +import React from "react" +import { Button, Tag, Tooltip } from "@/components/ui" +import type { DuplicationRecord } from "@/api/duplication" +import { STATUS_CONFIG } from "../constants" +import { getRiskLevel, formatSize, formatDuration } from "../utils" + +interface ResultCardProps { + record: DuplicationRecord + onView: (id: string | number) => void + onDelete: (id: string | number) => void + onRetry: (id: string | number) => void +} + +const ResultCard: React.FC = ({ record, onView, onDelete, onRetry }) => { + const statusCfg = STATUS_CONFIG[record.status] + const riskLevel = getRiskLevel(record.duplicate_rate) + const rateValue = record.duplicate_rate + + const handleClick = () => { + if (record.status === "completed") { + onView(record.id) + } + } + + return ( +
+ {/* 缩略图 */} +
🎬
+ + {/* 信息区 */} +
+

{record.filename}

+
+ + {statusCfg.icon} {statusCfg.text} + + {formatSize(record.file_size)} + {formatDuration(record.duration_seconds)} + {new Date(record.created_at).toLocaleDateString("zh-CN")} + {record.status === "completed" && record.duplicate_count !== undefined && ( + {record.duplicate_count} 个重复片段 + )} +
+
+ + {/* 查重率 */} +
+ {record.status === "completed" && rateValue !== undefined ? ( + <> +
+
+
+ {rateValue.toFixed(1)}% + + ) : record.status === "failed" ? ( + + + + ) : ( + + {record.status === "processing" ? "分析中..." : "—"} + + )} +
+ + {/* 删除按钮 */} + +
+ ) +} + +export default ResultCard diff --git a/apps/web/src/pages/duplication/constants.ts b/apps/web/src/pages/duplication/constants.ts new file mode 100644 index 000000000..16d6d65ff --- /dev/null +++ b/apps/web/src/pages/duplication/constants.ts @@ -0,0 +1,31 @@ +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_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: "高风险" }, +] diff --git a/apps/web/src/pages/duplication/hooks/useDuplicationResults.ts b/apps/web/src/pages/duplication/hooks/useDuplicationResults.ts new file mode 100644 index 000000000..bfa10bf50 --- /dev/null +++ b/apps/web/src/pages/duplication/hooks/useDuplicationResults.ts @@ -0,0 +1,106 @@ +import { useState, useMemo } from "react" +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { useNavigate } from "react-router-dom" +import { + getDuplicationRecords, + deleteDuplicationRecord, + retryDuplication, + type DuplicationRecord, +} from "@/api/duplication" +import type { RiskFilter, ToastState } from "../types" +import { getRiskLevel } from "../utils" + +interface UseDuplicationResultsReturn { + records: DuplicationRecord[] + isLoading: boolean + filteredRecords: DuplicationRecord[] + riskFilter: RiskFilter + setRiskFilter: (filter: RiskFilter) => void + toast: ToastState | null + handleDelete: (id: string | number) => void + handleRetry: (id: string | number) => void + handleView: (id: string | number) => void + handleUpload: () => void +} + +const useDuplicationResults = (): UseDuplicationResultsReturn => { + const navigate = useNavigate() + const queryClient = useQueryClient() + const [riskFilter, setRiskFilter] = useState("all") + const [toast, setToast] = useState(null) + + const showToast = (message: string, type: "success" | "error" | "warning") => { + setToast({ message, type }) + setTimeout(() => setToast(null), 3000) + } + + // 获取查重记录 + const { data: records = [], isLoading } = useQuery({ + queryKey: ["duplication-records"], + queryFn: getDuplicationRecords, + }) + + // 删除 + const deleteMutation = useMutation({ + mutationFn: deleteDuplicationRecord, + onSuccess: () => { + showToast("已删除", "success") + queryClient.invalidateQueries({ queryKey: ["duplication-records"] }) + }, + onError: () => { + showToast("删除失败", "error") + }, + }) + + // 重新查重 + const retryMutation = useMutation({ + mutationFn: retryDuplication, + onSuccess: () => { + showToast("已重新提交查重", "success") + queryClient.invalidateQueries({ queryKey: ["duplication-records"] }) + }, + onError: () => { + showToast("重新查重失败", "error") + }, + }) + + /** 按风险等级筛选 */ + const filteredRecords = useMemo(() => { + if (riskFilter === "all") return records + return records.filter((r) => { + if (r.status !== "completed") return riskFilter === "low" + return getRiskLevel(r.duplicate_rate) === riskFilter + }) + }, [records, riskFilter]) + + const handleDelete = (id: string | number) => { + deleteMutation.mutate(id) + } + + const handleRetry = (id: string | number) => { + retryMutation.mutate(id) + } + + const handleView = (id: string | number) => { + navigate(`/duplication/${id}`) + } + + const handleUpload = () => { + navigate("/app/duplication") + } + + return { + records, + isLoading, + filteredRecords, + riskFilter, + setRiskFilter, + toast, + handleDelete, + handleRetry, + handleView, + handleUpload, + } +} + +export default useDuplicationResults diff --git a/apps/web/src/pages/duplication/types.ts b/apps/web/src/pages/duplication/types.ts new file mode 100644 index 000000000..fffe5694c --- /dev/null +++ b/apps/web/src/pages/duplication/types.ts @@ -0,0 +1,8 @@ +/** 风险等级分类 */ +export type RiskFilter = "all" | "high" | "medium" | "low" + +/** 简易 toast */ +export interface ToastState { + message: string + type: "success" | "error" | "warning" +} diff --git a/apps/web/src/pages/duplication/utils.ts b/apps/web/src/pages/duplication/utils.ts new file mode 100644 index 000000000..673df26e3 --- /dev/null +++ b/apps/web/src/pages/duplication/utils.ts @@ -0,0 +1,23 @@ +/** 根据查重率获取风险等级 */ +export const getRiskLevel = (rate?: number): "low" | "medium" | "high" => { + if (rate === undefined) return "low" + if (rate <= 10) return "low" + if (rate <= 30) return "medium" + return "high" +} + +/** 格式化文件大小 */ +export 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` +} + +/** 格式化时长 */ +export 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}秒` +} -- 2.54.0 From aa9a44b7b8d5baf572206fe97fe7ca26850ecd8b Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 12:42:57 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=20id=20=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E7=B1=BB=E5=9E=8B=E4=B8=BA=20string=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20TS2345=20=E7=B1=BB=E5=9E=8B=E4=B8=8D=E5=85=BC?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/duplication/hooks/useDuplicationResults.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/src/pages/duplication/hooks/useDuplicationResults.ts b/apps/web/src/pages/duplication/hooks/useDuplicationResults.ts index bfa10bf50..23118fe15 100644 --- a/apps/web/src/pages/duplication/hooks/useDuplicationResults.ts +++ b/apps/web/src/pages/duplication/hooks/useDuplicationResults.ts @@ -17,9 +17,9 @@ interface UseDuplicationResultsReturn { riskFilter: RiskFilter setRiskFilter: (filter: RiskFilter) => void toast: ToastState | null - handleDelete: (id: string | number) => void - handleRetry: (id: string | number) => void - handleView: (id: string | number) => void + handleDelete: (id: string) => void + handleRetry: (id: string) => void + handleView: (id: string) => void handleUpload: () => void } @@ -73,15 +73,15 @@ const useDuplicationResults = (): UseDuplicationResultsReturn => { }) }, [records, riskFilter]) - const handleDelete = (id: string | number) => { + const handleDelete = (id: string) => { deleteMutation.mutate(id) } - const handleRetry = (id: string | number) => { + const handleRetry = (id: string) => { retryMutation.mutate(id) } - const handleView = (id: string | number) => { + const handleView = (id: string) => { navigate(`/duplication/${id}`) } -- 2.54.0 From e82071f98ef9766dde6577d7e3762d4d3afe4ffa Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 12:42:59 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=20ResultCard=20id?= =?UTF-8?q?=20=E5=8F=82=E6=95=B0=E7=B1=BB=E5=9E=8B=E4=B8=BA=20string?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/duplication/components/ResultCard.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/pages/duplication/components/ResultCard.tsx b/apps/web/src/pages/duplication/components/ResultCard.tsx index 11d68139f..27881f3af 100644 --- a/apps/web/src/pages/duplication/components/ResultCard.tsx +++ b/apps/web/src/pages/duplication/components/ResultCard.tsx @@ -6,9 +6,9 @@ import { getRiskLevel, formatSize, formatDuration } from "../utils" interface ResultCardProps { record: DuplicationRecord - onView: (id: string | number) => void - onDelete: (id: string | number) => void - onRetry: (id: string | number) => void + onView: (id: string) => void + onDelete: (id: string) => void + onRetry: (id: string) => void } const ResultCard: React.FC = ({ record, onView, onDelete, onRetry }) => { -- 2.54.0