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..27881f3af --- /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) => void + onDelete: (id: string) => void + onRetry: (id: string) => 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..23118fe15 --- /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) => void + handleRetry: (id: string) => void + handleView: (id: string) => 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) => { + deleteMutation.mutate(id) + } + + const handleRetry = (id: string) => { + retryMutation.mutate(id) + } + + const handleView = (id: string) => { + 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}秒` +}