feat(phase2): 查重功能前端页面
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled

- 新增查重 API 模块(mock 数据),含上传、记录列表、详情、删除、重试接口
- 新增上传查重页(DuplicationUpload):拖拽上传、格式校验、上传进度
- 新增查重结果列表页(DuplicationResults):表格展示、状态标签、查重率进度条、批量删除
- 新增重复视频对比详情页(DuplicationDetail):片段时间线可视化、相似度展示
- 路由新增 /duplication、/duplication/results、/duplication/:id
- Header 导航新增「查重」入口

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Audit Bot
2026-06-28 15:20:37 +08:00
parent abcca337e7
commit 92bf5d6cfa
6 changed files with 1116 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
/**
* 查重 API 模块
* 提供视频查重相关接口(当前使用 mock 数据,后端就绪后切换)
*/
import apiClient from './client';
/** 查重记录状态 */
export type DuplicationStatus = 'pending' | 'processing' | 'completed' | 'failed';
/** 查重记录 */
export interface DuplicationRecord {
id: string;
/** 原始文件名 */
filename: string;
/** 文件大小(字节) */
file_size: number;
/** 时长(秒) */
duration_seconds?: number;
/** 状态 */
status: DuplicationStatus;
/** 查重率(0-100) */
duplicate_rate?: number;
/** 重复片段数 */
duplicate_count?: number;
/** 创建时间 */
created_at: string;
/** 更新时间 */
updated_at: string;
}
/** 重复片段详情 */
export interface DuplicateSegment {
id: string;
/** 在原始视频中的起始时间(秒) */
source_start: number;
/** 在原始视频中的结束时间(秒) */
source_end: number;
/** 匹配到的已有视频 ID */
matched_video_id: string;
/** 匹配到的已有视频名称 */
matched_video_name: string;
/** 匹配片段在已有视频中的起始时间 */
matched_start: number;
/** 匹配片段在已有视频中的结束时间 */
matched_end: number;
/** 相似度(0-100) */
similarity: number;
}
/** 查重详情 */
export interface DuplicationDetail extends DuplicationRecord {
/** 重复片段列表 */
segments: DuplicateSegment[];
}
/** 上传查重响应 */
export interface DuplicationUploadResponse {
/** 查重记录 ID */
id: string;
/** 状态 */
status: DuplicationStatus;
/** 消息 */
message: string;
}
// ============ Mock 数据 ============
/** mock 查重记录列表 */
const MOCK_RECORDS: DuplicationRecord[] = [
{
id: 'dup-001',
filename: '日常vlog_01.mp4',
file_size: 125_000_000,
duration_seconds: 180,
status: 'completed',
duplicate_rate: 23.5,
duplicate_count: 3,
created_at: '2026-06-27T10:00:00Z',
updated_at: '2026-06-27T10:05:00Z',
},
{
id: 'dup-002',
filename: '美食分享_片段.mp4',
file_size: 45_000_000,
duration_seconds: 60,
status: 'completed',
duplicate_rate: 5.2,
duplicate_count: 1,
created_at: '2026-06-27T11:30:00Z',
updated_at: '2026-06-27T11:32:00Z',
},
{
id: 'dup-003',
filename: '旅行记录_巴黎.mp4',
file_size: 320_000_000,
duration_seconds: 420,
status: 'processing',
created_at: '2026-06-28T09:00:00Z',
updated_at: '2026-06-28T09:00:00Z',
},
{
id: 'dup-004',
filename: '产品展示_新版.mp4',
file_size: 88_000_000,
duration_seconds: 90,
status: 'completed',
duplicate_rate: 67.8,
duplicate_count: 8,
created_at: '2026-06-26T15:00:00Z',
updated_at: '2026-06-26T15:10:00Z',
},
{
id: 'dup-005',
filename: '教程_剪辑技巧.mp4',
file_size: 200_000_000,
duration_seconds: 300,
status: 'failed',
created_at: '2026-06-26T14:00:00Z',
updated_at: '2026-06-26T14:01:00Z',
},
];
/** mock 查重详情 */
const MOCK_DETAIL: DuplicationDetail = {
...MOCK_RECORDS[0],
segments: [
{
id: 'seg-001',
source_start: 10,
source_end: 25,
matched_video_id: 'asset-101',
matched_video_name: '日常vlog_素材库.mp4',
matched_start: 45,
matched_end: 60,
similarity: 92.3,
},
{
id: 'seg-002',
source_start: 60,
source_end: 78,
matched_video_id: 'asset-205',
matched_video_name: '城市风光_合集.mp4',
matched_start: 120,
matched_end: 138,
similarity: 85.7,
},
{
id: 'seg-003',
source_start: 150,
source_end: 165,
matched_video_id: 'asset-310',
matched_video_name: '背景音乐_配套画面.mp4',
matched_start: 30,
matched_end: 45,
similarity: 78.1,
},
],
};
/** 是否使用 mock 数据(后端就绪后改为 false) */
const USE_MOCK = true;
// ============ API 函数 ============
/** 上传视频进行查重 */
export const uploadForDuplication = async (
file: File
): Promise<DuplicationUploadResponse> => {
if (USE_MOCK) {
// 模拟上传延迟
await new Promise((resolve) => setTimeout(resolve, 1500));
return {
id: `dup-${Date.now()}`,
status: 'processing',
message: `文件 "${file.name}" 已上传,正在查重中...`,
};
}
const formData = new FormData();
formData.append('file', file);
const response = await apiClient.post('/duplication/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
};
/** 获取查重记录列表 */
export const getDuplicationRecords = async (): Promise<DuplicationRecord[]> => {
if (USE_MOCK) {
await new Promise((resolve) => setTimeout(resolve, 300));
return MOCK_RECORDS;
}
const response = await apiClient.get('/duplication/records');
return response.data;
};
/** 获取查重详情 */
export const getDuplicationDetail = async (
recordId: string
): Promise<DuplicationDetail> => {
if (USE_MOCK) {
await new Promise((resolve) => setTimeout(resolve, 300));
// 返回第一条 mock 详情,实际应按 ID 查找
return { ...MOCK_DETAIL, id: recordId };
}
const response = await apiClient.get(`/duplication/records/${recordId}`);
return response.data;
};
/** 删除查重记录 */
export const deleteDuplicationRecord = async (
recordId: string
): Promise<void> => {
if (USE_MOCK) {
await new Promise((resolve) => setTimeout(resolve, 200));
return;
}
await apiClient.delete(`/duplication/records/${recordId}`);
};
/** 重新查重 */
export const retryDuplication = async (
recordId: string
): Promise<DuplicationUploadResponse> => {
if (USE_MOCK) {
await new Promise((resolve) => setTimeout(resolve, 500));
return {
id: recordId,
status: 'processing',
message: '已重新提交查重',
};
}
const response = await apiClient.post(`/duplication/records/${recordId}/retry`);
return response.data;
};