feat(phase2): 查重功能前端页面
- 新增查重 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:
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user