- 新增模板匹配结果数据模型和数据库表结构 - 实现匹配结果Repository层,支持CRUD操作和查询 - 实现匹配结果Service层,提供业务逻辑和统计功能 - 新增Tauri命令接口,支持前端调用 - 实现前端TypeScript类型定义 - 更新MaterialMatchingService,支持自动保存匹配结果 - 新增前端管理界面组件: - TemplateMatchingResultManager: 主管理界面 - TemplateMatchingResultCard: 结果卡片组件 - TemplateMatchingResultDetailModal: 详情模态框 - TemplateMatchingResultStatsPanel: 统计面板 - 编写完整的单元测试 - 新增API文档 功能特性: - 保存匹配结果到数据库,包含成功和失败片段详情 - 支持匹配结果的查询、过滤、排序和分页 - 提供匹配统计信息和质量评分 - 支持软删除和批量操作 - 完整的前端管理界面,支持查看、编辑、删除操作
333 lines
10 KiB
TypeScript
333 lines
10 KiB
TypeScript
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>
|
||
);
|
||
};
|