## 修复内容 ### 1. 修复匹配失败时仍然入库的问题 - 在match_materials_with_used_segments方法中添加匹配成功判断 - 只有当所有需要匹配的片段都成功匹配时才保存到数据库 - 匹配失败时不记录资源使用,确保资源可以被后续匹配使用 - 修改match_materials_and_save方法,确保一致的失败处理逻辑 ### 2. 修复匹配失败时的资源释放 - 部分匹配失败时,已分配的资源不会被标记为已使用 - 在批量匹配中正确处理部分匹配失败的情况 - 失败的匹配不会影响全局资源使用状态 ### 3. 修复成功率计算超过100%的问题 - 统一所有地方的成功率计算逻辑,确保基于可匹配片段计算 - 在前端显示时添加Math.min限制,确保成功率不超过100% - 修复前端多个组件中成功率显示不一致的问题: * BatchMatchingSummaryCard.tsx * BatchMatchingResultDialog.tsx * TemplateMatchingResultCard.tsx * TemplateMatchingResultDetailModal.tsx * TemplateMatchingResultStatsPanel.tsx * materialMatchingService.ts ### 4. 改进批量匹配逻辑 - 区分完全匹配失败和部分匹配失败 - 部分匹配失败时提供详细的失败原因 - 保持匹配结果用于分析,但不保存到数据库 ## 技术细节 - 后端成功率统一为0-1的小数格式 - 前端显示时统一乘以100并限制最大值为100 - 确保匹配失败时的事务一致性 - 添加详细的日志输出便于调试
247 lines
8.9 KiB
TypeScript
247 lines
8.9 KiB
TypeScript
import React from 'react';
|
|
import { TemplateMatchingResult, MatchingResultStatus } from '../types/templateMatchingResult';
|
|
|
|
interface TemplateMatchingResultCardProps {
|
|
result: TemplateMatchingResult;
|
|
onViewDetail: () => void;
|
|
onDelete: () => void;
|
|
onEdit?: () => void;
|
|
onExportToJianying?: () => void;
|
|
onExportToJianyingV2?: () => void;
|
|
isSelected?: boolean;
|
|
onToggleSelect?: () => void;
|
|
showExportStatus?: boolean;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProps> = ({
|
|
result,
|
|
onViewDetail,
|
|
onDelete,
|
|
onEdit,
|
|
onExportToJianyingV2,
|
|
isSelected = false,
|
|
onToggleSelect,
|
|
showExportStatus = false,
|
|
disabled = false,
|
|
}) => {
|
|
// 格式化时长显示
|
|
const formatDuration = (ms: number): string => {
|
|
if (ms < 1000) return `${ms}ms`;
|
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
return `${(ms / 60000).toFixed(1)}min`;
|
|
};
|
|
|
|
// 格式化日期显示
|
|
const formatDate = (dateString: string): string => {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
// 获取状态样式
|
|
const getStatusStyle = (status: MatchingResultStatus) => {
|
|
switch (status) {
|
|
case MatchingResultStatus.Success:
|
|
return 'bg-green-100 text-green-800 border-green-200';
|
|
case MatchingResultStatus.PartialSuccess:
|
|
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
|
|
case MatchingResultStatus.Failed:
|
|
return 'bg-red-100 text-red-800 border-red-200';
|
|
case MatchingResultStatus.Cancelled:
|
|
return 'bg-gray-100 text-gray-800 border-gray-200';
|
|
default:
|
|
return 'bg-gray-100 text-gray-800 border-gray-200';
|
|
}
|
|
};
|
|
|
|
// 获取状态显示文本
|
|
const getStatusText = (status: MatchingResultStatus) => {
|
|
switch (status) {
|
|
case MatchingResultStatus.Success:
|
|
return '匹配成功';
|
|
case MatchingResultStatus.PartialSuccess:
|
|
return '部分成功';
|
|
case MatchingResultStatus.Failed:
|
|
return '匹配失败';
|
|
case MatchingResultStatus.Cancelled:
|
|
return '已取消';
|
|
default:
|
|
return '未知状态';
|
|
}
|
|
};
|
|
|
|
// 获取成功率颜色
|
|
const getSuccessRateColor = (rate: number) => {
|
|
if (rate >= 90) return 'text-green-600';
|
|
if (rate >= 70) return 'text-yellow-600';
|
|
if (rate >= 50) return 'text-orange-600';
|
|
return 'text-red-600';
|
|
};
|
|
|
|
return (
|
|
<div className={`card card-interactive group ${isSelected ? 'ring-2 ring-blue-500 bg-blue-50' : ''} ${disabled ? 'opacity-75' : ''}`}>
|
|
{/* 卡片头部 */}
|
|
<div className="card-header">
|
|
<div className="flex items-start justify-between">
|
|
{/* 选择框 */}
|
|
{onToggleSelect && (
|
|
<div className="flex items-center mr-3">
|
|
<input
|
|
type="checkbox"
|
|
checked={isSelected}
|
|
onChange={onToggleSelect}
|
|
disabled={disabled}
|
|
className={`form-checkbox h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500 ${
|
|
disabled ? 'opacity-50 cursor-not-allowed' : ''
|
|
}`}
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center space-x-2">
|
|
<h3 className="text-lg font-semibold text-gray-900 truncate group-hover:text-primary-600 transition-colors">
|
|
{result.result_name}
|
|
</h3>
|
|
{/* 导出状态标识 */}
|
|
{showExportStatus && (
|
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
|
result.is_exported
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-gray-100 text-gray-800'
|
|
}`}>
|
|
{result.is_exported ? '✓ 已导出' : '○ 未导出'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{result.description && (
|
|
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
|
|
{result.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className={`ml-3 px-3 py-1 rounded-full text-xs font-medium ${getStatusStyle(result.status)}`}>
|
|
{getStatusText(result.status)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 统计信息 */}
|
|
<div className="card-body">
|
|
<div className="grid grid-cols-2 gap-6 mb-6">
|
|
{/* 成功率 */}
|
|
<div className="text-center p-3 bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl">
|
|
<div className={`text-2xl font-bold ${getSuccessRateColor(Math.min(result.success_rate * 100, 100))}`}>
|
|
{Math.min(result.success_rate * 100, 100).toFixed(1)}%
|
|
</div>
|
|
<div className="text-xs text-gray-600 font-medium mt-1">成功率</div>
|
|
</div>
|
|
|
|
{/* 片段统计 */}
|
|
<div className="text-center p-3 bg-gradient-to-br from-primary-50 to-primary-100 rounded-xl">
|
|
<div className="text-2xl font-bold text-primary-700">
|
|
{result.matched_segments}/{result.total_segments}
|
|
</div>
|
|
<div className="text-xs text-primary-600 font-medium mt-1">匹配片段</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 详细统计 */}
|
|
<div className="space-y-3 text-sm">
|
|
<div className="flex justify-between items-center py-1">
|
|
<span className="text-gray-600">失败片段</span>
|
|
<span className="font-medium text-gray-900">{result.failed_segments}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-1">
|
|
<span className="text-gray-600">使用素材</span>
|
|
<span className="font-medium text-gray-900">{result.used_materials}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-1">
|
|
<span className="text-gray-600">使用模特</span>
|
|
<span className="font-medium text-gray-900">{result.used_models}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center py-1">
|
|
<span className="text-gray-600">匹配耗时</span>
|
|
<span className="font-medium text-gray-900">{formatDuration(result.matching_duration_ms)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 质量评分 */}
|
|
{result.quality_score && (
|
|
<div className="mt-4 pt-4 border-t border-gray-100">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-600 font-medium">质量评分</span>
|
|
<div className="flex items-center">
|
|
<div className="flex text-yellow-400">
|
|
{[1, 2, 3, 4, 5].map((star) => (
|
|
<svg
|
|
key={star}
|
|
className={`w-4 h-4 ${
|
|
star <= result.quality_score! ? 'fill-current' : 'text-gray-300'
|
|
}`}
|
|
viewBox="0 0 20 20"
|
|
>
|
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
|
</svg>
|
|
))}
|
|
</div>
|
|
<span className="ml-2 text-sm font-semibold text-gray-900">
|
|
{result.quality_score.toFixed(1)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 时间信息 */}
|
|
<div className="card-footer">
|
|
<div className="flex justify-between text-xs text-gray-500">
|
|
<span>创建: {formatDate(result.created_at)}</span>
|
|
<span>更新: {formatDate(result.updated_at)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 操作按钮 */}
|
|
<div className="px-6 py-4 border-t border-gray-100 flex justify-end space-x-3">
|
|
<button
|
|
onClick={onViewDetail}
|
|
className="btn btn-primary btn-sm"
|
|
>
|
|
查看详情
|
|
</button>
|
|
{onEdit && (
|
|
<button
|
|
onClick={onEdit}
|
|
className="btn btn-secondary btn-sm"
|
|
>
|
|
编辑
|
|
</button>
|
|
)}
|
|
{onExportToJianyingV2 && (
|
|
<button
|
|
onClick={onExportToJianyingV2}
|
|
className="btn btn-primary btn-sm"
|
|
title="导出到剪映 (V2版本 - 基于原始模板)"
|
|
>
|
|
导出到剪映
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={onDelete}
|
|
className="btn btn-danger btn-sm"
|
|
>
|
|
删除
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|