Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e82071f98e | |||
| aa9a44b7b8 | |||
| 12770b397f | |||
| abc5877bf6 |
@@ -2,130 +2,27 @@
|
|||||||
* 查重结果列表页面 — V21 设计系统
|
* 查重结果列表页面 — V21 设计系统
|
||||||
* 胶囊筛选 + 卡片列表,零 antd 依赖
|
* 胶囊筛选 + 卡片列表,零 antd 依赖
|
||||||
*/
|
*/
|
||||||
import React, { useState, useMemo } from "react"
|
import React from "react"
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
import { Button } from "@/components/ui"
|
||||||
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 PageHead from "@/components/layout/PageHead"
|
import PageHead from "@/components/layout/PageHead"
|
||||||
|
import "./duplication.css"
|
||||||
/** 风险等级分类 */
|
import useDuplicationResults from "./hooks/useDuplicationResults"
|
||||||
type RiskFilter = "all" | "high" | "medium" | "low"
|
import FilterBar from "./components/FilterBar"
|
||||||
|
import EmptyState from "./components/EmptyState"
|
||||||
/** 状态配置 */
|
import ResultCard from "./components/ResultCard"
|
||||||
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<string, string> = {
|
|
||||||
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: "高风险" },
|
|
||||||
]
|
|
||||||
|
|
||||||
const DuplicationResults: React.FC = () => {
|
const DuplicationResults: React.FC = () => {
|
||||||
const navigate = useNavigate()
|
const {
|
||||||
const queryClient = useQueryClient()
|
isLoading,
|
||||||
const [riskFilter, setRiskFilter] = useState<RiskFilter>("all")
|
filteredRecords,
|
||||||
const [toast, setToast] = useState<ToastState | null>(null)
|
riskFilter,
|
||||||
|
setRiskFilter,
|
||||||
const showToast = (message: string, type: "success" | "error" | "warning") => {
|
toast,
|
||||||
setToast({ message, type })
|
handleDelete,
|
||||||
setTimeout(() => setToast(null), 3000)
|
handleRetry,
|
||||||
}
|
handleView,
|
||||||
|
handleUpload,
|
||||||
// 获取查重记录
|
} = useDuplicationResults()
|
||||||
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])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dup-page">
|
<div className="dup-page">
|
||||||
@@ -136,136 +33,31 @@ const DuplicationResults: React.FC = () => {
|
|||||||
title="查重记录"
|
title="查重记录"
|
||||||
actions={
|
actions={
|
||||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||||
{/* 筛选胶囊 */}
|
<FilterBar value={riskFilter} onChange={setRiskFilter} />
|
||||||
<div className="dup-filter">
|
<Button buttonType="primary" buttonSize="md" onClick={handleUpload}>
|
||||||
{FILTER_OPTIONS.map((opt) => (
|
|
||||||
<button
|
|
||||||
key={opt.key}
|
|
||||||
className={`dup-filter-btn ${riskFilter === opt.key ? "active" : ""}`}
|
|
||||||
onClick={() => setRiskFilter(opt.key)}
|
|
||||||
>
|
|
||||||
{opt.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
buttonType="primary"
|
|
||||||
buttonSize="md"
|
|
||||||
onClick={() => navigate("/app/duplication")}
|
|
||||||
>
|
|
||||||
📤 上传查重
|
📤 上传查重
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 加载中 */}
|
{/* 加载中 / 空状态 */}
|
||||||
{isLoading && (
|
{(isLoading || filteredRecords.length === 0) && (
|
||||||
<div className="dup-results-empty">
|
<EmptyState isLoading={isLoading} riskFilter={riskFilter} />
|
||||||
<div className="dup-results-empty-icon">⏳</div>
|
|
||||||
<p>加载中...</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 空状态 */}
|
|
||||||
{!isLoading && filteredRecords.length === 0 && (
|
|
||||||
<div className="dup-results-empty">
|
|
||||||
<div className="dup-results-empty-icon">📭</div>
|
|
||||||
<p>
|
|
||||||
{riskFilter === "all"
|
|
||||||
? "暂无查重记录,上传视频开始查重吧"
|
|
||||||
: `没有${RISK_LABELS[riskFilter]}的记录`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 结果卡片列表 */}
|
{/* 结果卡片列表 */}
|
||||||
{!isLoading && filteredRecords.length > 0 && (
|
{!isLoading && filteredRecords.length > 0 && (
|
||||||
<div className="dup-results-list">
|
<div className="dup-results-list">
|
||||||
{filteredRecords.map((record) => {
|
{filteredRecords.map((record) => (
|
||||||
const statusCfg = STATUS_CONFIG[record.status]
|
<ResultCard
|
||||||
const riskLevel = getRiskLevel(record.duplicate_rate)
|
key={record.id}
|
||||||
const rateValue = record.duplicate_rate
|
record={record}
|
||||||
|
onView={handleView}
|
||||||
return (
|
onDelete={handleDelete}
|
||||||
<div
|
onRetry={handleRetry}
|
||||||
key={record.id}
|
/>
|
||||||
className="dup-result-card"
|
))}
|
||||||
onClick={() => {
|
|
||||||
if (record.status === "completed") {
|
|
||||||
navigate(`/duplication/${record.id}`)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* 缩略图 */}
|
|
||||||
<div className="dup-result-card-thumb">🎬</div>
|
|
||||||
|
|
||||||
{/* 信息区 */}
|
|
||||||
<div className="dup-result-card-body">
|
|
||||||
<h4>{record.filename}</h4>
|
|
||||||
<div className="dup-result-card-meta">
|
|
||||||
<Tag variant={statusCfg.variant}>
|
|
||||||
{statusCfg.icon} {statusCfg.text}
|
|
||||||
</Tag>
|
|
||||||
<span>{formatSize(record.file_size)}</span>
|
|
||||||
<span>{formatDuration(record.duration_seconds)}</span>
|
|
||||||
<span>{new Date(record.created_at).toLocaleDateString("zh-CN")}</span>
|
|
||||||
{record.status === "completed" && record.duplicate_count !== undefined && (
|
|
||||||
<span>{record.duplicate_count} 个重复片段</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 查重率 */}
|
|
||||||
<div className="dup-result-card-score">
|
|
||||||
{record.status === "completed" && rateValue !== undefined ? (
|
|
||||||
<>
|
|
||||||
<div className="dup-score-bar">
|
|
||||||
<div
|
|
||||||
className={`dup-score-bar-fill ${riskLevel}`}
|
|
||||||
style={{ width: `${Math.min(rateValue, 100)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className={`dup-score-value ${riskLevel}`}>
|
|
||||||
{rateValue.toFixed(1)}%
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
) : record.status === "failed" ? (
|
|
||||||
<Tooltip title="重新查重">
|
|
||||||
<Button
|
|
||||||
buttonType="ghost"
|
|
||||||
buttonSize="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
retryMutation.mutate(record.id)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
🔄 重试
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<span style={{ color: "var(--text-secondary)", fontSize: 12 }}>
|
|
||||||
{record.status === "processing" ? "分析中..." : "—"}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 删除按钮 */}
|
|
||||||
<Button
|
|
||||||
buttonType="ghost"
|
|
||||||
buttonSize="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
if (window.confirm("确定删除此记录?")) {
|
|
||||||
deleteMutation.mutate(record.id)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
🗑️
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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<EmptyStateProps> = ({ isLoading, riskFilter }) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="dup-results-empty">
|
||||||
|
<div className="dup-results-empty-icon">⏳</div>
|
||||||
|
<p>加载中...</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dup-results-empty">
|
||||||
|
<div className="dup-results-empty-icon">📭</div>
|
||||||
|
<p>
|
||||||
|
{riskFilter === "all"
|
||||||
|
? "暂无查重记录,上传视频开始查重吧"
|
||||||
|
: `没有${RISK_LABELS[riskFilter]}的记录`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmptyState
|
||||||
@@ -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<FilterBarProps> = ({ value, onChange }) => {
|
||||||
|
return (
|
||||||
|
<div className="dup-filter">
|
||||||
|
{FILTER_OPTIONS.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.key}
|
||||||
|
className={`dup-filter-btn ${value === opt.key ? "active" : ""}`}
|
||||||
|
onClick={() => onChange(opt.key)}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterBar
|
||||||
@@ -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<ResultCardProps> = ({ 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 (
|
||||||
|
<div key={record.id} className="dup-result-card" onClick={handleClick}>
|
||||||
|
{/* 缩略图 */}
|
||||||
|
<div className="dup-result-card-thumb">🎬</div>
|
||||||
|
|
||||||
|
{/* 信息区 */}
|
||||||
|
<div className="dup-result-card-body">
|
||||||
|
<h4>{record.filename}</h4>
|
||||||
|
<div className="dup-result-card-meta">
|
||||||
|
<Tag variant={statusCfg.variant}>
|
||||||
|
{statusCfg.icon} {statusCfg.text}
|
||||||
|
</Tag>
|
||||||
|
<span>{formatSize(record.file_size)}</span>
|
||||||
|
<span>{formatDuration(record.duration_seconds)}</span>
|
||||||
|
<span>{new Date(record.created_at).toLocaleDateString("zh-CN")}</span>
|
||||||
|
{record.status === "completed" && record.duplicate_count !== undefined && (
|
||||||
|
<span>{record.duplicate_count} 个重复片段</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 查重率 */}
|
||||||
|
<div className="dup-result-card-score">
|
||||||
|
{record.status === "completed" && rateValue !== undefined ? (
|
||||||
|
<>
|
||||||
|
<div className="dup-score-bar">
|
||||||
|
<div
|
||||||
|
className={`dup-score-bar-fill ${riskLevel}`}
|
||||||
|
style={{ width: `${Math.min(rateValue, 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className={`dup-score-value ${riskLevel}`}>{rateValue.toFixed(1)}%</span>
|
||||||
|
</>
|
||||||
|
) : record.status === "failed" ? (
|
||||||
|
<Tooltip title="重新查重">
|
||||||
|
<Button
|
||||||
|
buttonType="ghost"
|
||||||
|
buttonSize="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onRetry(record.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🔄 重试
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: "var(--text-secondary)", fontSize: 12 }}>
|
||||||
|
{record.status === "processing" ? "分析中..." : "—"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 删除按钮 */}
|
||||||
|
<Button
|
||||||
|
buttonType="ghost"
|
||||||
|
buttonSize="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (window.confirm("确定删除此记录?")) {
|
||||||
|
onDelete(record.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResultCard
|
||||||
@@ -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<string, string> = {
|
||||||
|
low: "低风险",
|
||||||
|
medium: "中风险",
|
||||||
|
high: "高风险",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FILTER_OPTIONS: { key: RiskFilter; label: string }[] = [
|
||||||
|
{ key: "all", label: "全部" },
|
||||||
|
{ key: "low", label: "低风险" },
|
||||||
|
{ key: "medium", label: "中风险" },
|
||||||
|
{ key: "high", label: "高风险" },
|
||||||
|
]
|
||||||
@@ -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<RiskFilter>("all")
|
||||||
|
const [toast, setToast] = useState<ToastState | null>(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
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/** 风险等级分类 */
|
||||||
|
export type RiskFilter = "all" | "high" | "medium" | "low"
|
||||||
|
|
||||||
|
/** 简易 toast */
|
||||||
|
export interface ToastState {
|
||||||
|
message: string
|
||||||
|
type: "success" | "error" | "warning"
|
||||||
|
}
|
||||||
@@ -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}秒`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user