Files
mixvideo-v2/apps/desktop/src/components/TemplateMatchingResultManager.tsx
imeepos 1d6de409ed feat: 实现模板匹配结果保存和管理功能
- 新增模板匹配结果数据模型和数据库表结构
- 实现匹配结果Repository层,支持CRUD操作和查询
- 实现匹配结果Service层,提供业务逻辑和统计功能
- 新增Tauri命令接口,支持前端调用
- 实现前端TypeScript类型定义
- 更新MaterialMatchingService,支持自动保存匹配结果
- 新增前端管理界面组件:
  - TemplateMatchingResultManager: 主管理界面
  - TemplateMatchingResultCard: 结果卡片组件
  - TemplateMatchingResultDetailModal: 详情模态框
  - TemplateMatchingResultStatsPanel: 统计面板
- 编写完整的单元测试
- 新增API文档

功能特性:
- 保存匹配结果到数据库,包含成功和失败片段详情
- 支持匹配结果的查询、过滤、排序和分页
- 提供匹配统计信息和质量评分
- 支持软删除和批量操作
- 完整的前端管理界面,支持查看、编辑、删除操作
2025-07-16 13:34:32 +08:00

333 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import {
TemplateMatchingResult,
TemplateMatchingResultQueryOptions,
MatchingResultStatus,
MatchingStatistics
} from '../types/templateMatchingResult';
import { LoadingSpinner } from './LoadingSpinner';
import { ErrorMessage } from './ErrorMessage';
import { EmptyState } from './EmptyState';
import { DeleteConfirmDialog } from './DeleteConfirmDialog';
import { CustomSelect } from './CustomSelect';
import { TemplateMatchingResultCard } from './TemplateMatchingResultCard';
import { TemplateMatchingResultDetailModal } from './TemplateMatchingResultDetailModal';
import { TemplateMatchingResultStatsPanel } from './TemplateMatchingResultStatsPanel';
interface TemplateMatchingResultManagerProps {
projectId?: string;
templateId?: string;
bindingId?: string;
showStats?: boolean;
onResultSelect?: (result: TemplateMatchingResult) => void;
}
export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManagerProps> = ({
projectId,
templateId,
bindingId,
showStats = true,
onResultSelect,
}) => {
const [results, setResults] = useState<TemplateMatchingResult[]>([]);
const [statistics, setStatistics] = useState<MatchingStatistics | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedResult, setSelectedResult] = useState<TemplateMatchingResult | null>(null);
const [showDetailModal, setShowDetailModal] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState<{
show: boolean;
result: TemplateMatchingResult | null;
}>({ show: false, result: null });
// 过滤和排序状态
const [filters, setFilters] = useState<{
status?: MatchingResultStatus;
searchKeyword?: string;
sortBy: string;
sortOrder: string;
}>({
sortBy: 'created_at',
sortOrder: 'desc',
});
const [pagination, setPagination] = useState({
page: 1,
pageSize: 20,
total: 0,
});
// 加载匹配结果列表
const loadResults = async () => {
try {
setLoading(true);
setError(null);
const queryOptions: TemplateMatchingResultQueryOptions = {
project_id: projectId,
template_id: templateId,
binding_id: bindingId,
status: filters.status,
search_keyword: filters.searchKeyword,
sort_by: filters.sortBy,
sort_order: filters.sortOrder,
limit: pagination.pageSize,
offset: (pagination.page - 1) * pagination.pageSize,
};
const resultList = await invoke<TemplateMatchingResult[]>('list_matching_results', {
options: queryOptions,
});
setResults(resultList);
setPagination(prev => ({ ...prev, total: resultList.length }));
} catch (err) {
setError(err as string);
} finally {
setLoading(false);
}
};
// 加载统计信息
const loadStatistics = async () => {
if (!showStats) return;
try {
const stats = await invoke<MatchingStatistics>('get_matching_statistics', {
projectId,
});
setStatistics(stats);
} catch (err) {
console.error('加载统计信息失败:', err);
}
};
// 删除匹配结果
const handleDelete = async (result: TemplateMatchingResult) => {
try {
await invoke<boolean>('soft_delete_matching_result', {
resultId: result.id,
});
// 重新加载列表
await loadResults();
await loadStatistics();
setDeleteConfirm({ show: false, result: null });
} catch (err) {
setError(`删除失败: ${err}`);
}
};
// 查看详情
const handleViewDetail = (result: TemplateMatchingResult) => {
setSelectedResult(result);
setShowDetailModal(true);
onResultSelect?.(result);
};
// 处理过滤器变化
const handleFilterChange = (newFilters: Partial<typeof filters>) => {
setFilters(prev => ({ ...prev, ...newFilters }));
setPagination(prev => ({ ...prev, page: 1 })); // 重置到第一页
};
// 处理分页变化
const handlePageChange = (page: number) => {
setPagination(prev => ({ ...prev, page }));
};
// 初始加载
useEffect(() => {
loadResults();
loadStatistics();
}, [projectId, templateId, bindingId, filters, pagination.page]);
// 状态选项
const statusOptions = [
{ value: '', label: '全部状态' },
{ value: 'Success', label: '匹配成功' },
{ value: 'PartialSuccess', label: '部分成功' },
{ value: 'Failed', label: '匹配失败' },
{ value: 'Cancelled', label: '已取消' },
];
// 排序选项
const sortOptions = [
{ value: 'created_at', label: '创建时间' },
{ value: 'updated_at', label: '更新时间' },
{ value: 'success_rate', label: '成功率' },
{ value: 'result_name', label: '结果名称' },
{ value: 'total_segments', label: '片段数量' },
{ value: 'matching_duration_ms', label: '匹配耗时' },
];
const sortOrderOptions = [
{ value: 'desc', label: '降序' },
{ value: 'asc', label: '升序' },
];
if (loading && results.length === 0) {
return <LoadingSpinner text="加载匹配结果..." />;
}
return (
<div className="template-matching-result-manager">
{/* 统计面板 */}
{showStats && statistics && (
<TemplateMatchingResultStatsPanel statistics={statistics} />
)}
{/* 过滤器和搜索 */}
<div className="filters-section bg-white rounded-lg shadow-sm p-4 mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* 搜索框 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="text"
placeholder="搜索结果名称或描述..."
value={filters.searchKeyword || ''}
onChange={(e) => handleFilterChange({ searchKeyword: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* 状态过滤 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<CustomSelect
options={statusOptions}
value={filters.status || ''}
onChange={(value) => handleFilterChange({ status: value as MatchingResultStatus })}
placeholder="选择状态"
/>
</div>
{/* 排序字段 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<CustomSelect
options={sortOptions}
value={filters.sortBy}
onChange={(value) => handleFilterChange({ sortBy: value })}
/>
</div>
{/* 排序顺序 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<CustomSelect
options={sortOrderOptions}
value={filters.sortOrder}
onChange={(value) => handleFilterChange({ sortOrder: value })}
/>
</div>
</div>
{/* 操作按钮 */}
<div className="flex justify-between items-center mt-4">
<div className="text-sm text-gray-500">
{pagination.total}
</div>
<button
onClick={() => {
loadResults();
loadStatistics();
}}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
</button>
</div>
</div>
{/* 错误信息 */}
{error && (
<ErrorMessage
message={error}
onDismiss={() => setError(null)}
/>
)}
{/* 结果列表 */}
{results.length === 0 ? (
<EmptyState
title="暂无匹配结果"
description="还没有任何模板匹配结果,请先执行模板匹配操作。"
icon="📊"
/>
) : (
<div className="results-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{results.map((result) => (
<TemplateMatchingResultCard
key={result.id}
result={result}
onViewDetail={() => handleViewDetail(result)}
onDelete={() => setDeleteConfirm({ show: true, result })}
/>
))}
</div>
)}
{/* 分页 */}
{pagination.total > pagination.pageSize && (
<div className="pagination-section mt-6 flex justify-center">
<div className="flex items-center space-x-2">
<button
onClick={() => handlePageChange(pagination.page - 1)}
disabled={pagination.page <= 1}
className="px-3 py-2 border border-gray-300 rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
>
</button>
<span className="px-3 py-2 text-sm text-gray-700">
{pagination.page} {Math.ceil(pagination.total / pagination.pageSize)}
</span>
<button
onClick={() => handlePageChange(pagination.page + 1)}
disabled={pagination.page >= Math.ceil(pagination.total / pagination.pageSize)}
className="px-3 py-2 border border-gray-300 rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
>
</button>
</div>
</div>
)}
{/* 详情模态框 */}
{showDetailModal && selectedResult && (
<TemplateMatchingResultDetailModal
resultId={selectedResult.id}
onClose={() => {
setShowDetailModal(false);
setSelectedResult(null);
}}
onUpdate={() => {
loadResults();
loadStatistics();
}}
/>
)}
{/* 删除确认对话框 */}
<DeleteConfirmDialog
isOpen={deleteConfirm.show}
title="删除匹配结果"
message={`确定要删除匹配结果 "${deleteConfirm.result?.result_name}" 吗?此操作不可撤销。`}
onConfirm={() => deleteConfirm.result && handleDelete(deleteConfirm.result)}
onCancel={() => setDeleteConfirm({ show: false, result: null })}
/>
</div>
);
};