fix: 修复批量删除需要点击两次的问题

问题分析:
- 第一次点击批量删除时,onConfirm回调中使用的batchDeleteConfirm.resultIds可能因为React状态更新的异步性而被清空
- handleBatchDelete函数内部会重置batchDeleteConfirm状态,导致竞态条件

解决方案:
- 将对话框关闭逻辑从handleBatchDelete中移出
- 创建handleConfirmBatchDelete函数,在调用删除前先复制resultIds数组并立即关闭对话框
- 修复数据库查询中缺少is_exported和last_exported_at字段的问题
- 添加更好的loading状态管理和用户体验优化

修复内容:
- 修复TemplateMatchingResultRepository中SELECT语句缺少新字段的问题
- 重构批量删除的状态管理逻辑,避免竞态条件
- 添加调试日志帮助问题诊断
- 改进loading状态的视觉反馈
This commit is contained in:
imeepos 2025-07-18 13:03:17 +08:00
parent f6041c6eea
commit 66ceaf3274
3 changed files with 60 additions and 13 deletions

View File

@ -85,7 +85,8 @@ impl TemplateMatchingResultRepository {
"SELECT id, project_id, template_id, binding_id, result_name, description,
total_segments, matched_segments, failed_segments, success_rate,
used_materials, used_models, matching_duration_ms, quality_score,
status, metadata, export_count, created_at, updated_at, is_active
status, metadata, export_count, is_exported, last_exported_at,
created_at, updated_at, is_active
FROM template_matching_results WHERE id = ?1"
)?;
@ -171,7 +172,8 @@ impl TemplateMatchingResultRepository {
let mut query = "SELECT id, project_id, template_id, binding_id, result_name, description,
total_segments, matched_segments, failed_segments, success_rate,
used_materials, used_models, matching_duration_ms, quality_score,
status, metadata, export_count, created_at, updated_at, is_active
status, metadata, export_count, is_exported, last_exported_at,
created_at, updated_at, is_active
FROM template_matching_results WHERE is_active = 1".to_string();
let mut params: Vec<String> = Vec::new();

View File

@ -11,6 +11,7 @@ interface TemplateMatchingResultCardProps {
isSelected?: boolean;
onToggleSelect?: () => void;
showExportStatus?: boolean;
disabled?: boolean;
}
export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProps> = ({
@ -22,6 +23,7 @@ export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProp
isSelected = false,
onToggleSelect,
showExportStatus = false,
disabled = false,
}) => {
// 格式化时长显示
const formatDuration = (ms: number): string => {
@ -83,7 +85,7 @@ export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProp
};
return (
<div className={`card card-interactive group ${isSelected ? 'ring-2 ring-blue-500 bg-blue-50' : ''}`}>
<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">
@ -94,7 +96,10 @@ export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProp
type="checkbox"
checked={isSelected}
onChange={onToggleSelect}
className="form-checkbox h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
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>

View File

@ -132,29 +132,41 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
// 批量删除匹配结果
const handleBatchDelete = async (resultIds: string[]) => {
console.log('开始批量删除ID列表:', resultIds);
setBatchOperationLoading(true);
try {
const [deletedResults, deletedUsageRecords] = await invoke<[number, number]>(
'batch_soft_delete_matching_results_with_usage_reset',
{ resultIds }
);
console.log(`删除结果: ${deletedResults} 个匹配结果,${deletedUsageRecords} 条使用记录`);
success(`成功删除 ${deletedResults} 个匹配结果,重置 ${deletedUsageRecords} 条使用记录`);
// 重新加载列表
console.log('重新加载列表...');
await loadResults();
await loadStatistics();
console.log('列表重新加载完成');
// 清空选择
setSelectedResults(new Set());
setBatchDeleteConfirm({ show: false, resultIds: [] });
} catch (err) {
console.error('批量删除失败:', err);
setError(`批量删除失败: ${err}`);
} finally {
setBatchOperationLoading(false);
}
};
// 确认批量删除
const handleConfirmBatchDelete = async () => {
const resultIds = [...batchDeleteConfirm.resultIds]; // 复制数组避免状态变化影响
setBatchDeleteConfirm({ show: false, resultIds: [] }); // 立即关闭对话框
await handleBatchDelete(resultIds);
};
// 切换选择状态
const handleToggleSelect = (resultId: string) => {
const newSelected = new Set(selectedResults);
@ -291,7 +303,7 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
}
return (
<div className="template-matching-result-manager space-y-6">
<div className="template-matching-result-manager space-y-6 relative">
{/* 统计面板 */}
{showStats && statistics && (
<TemplateMatchingResultStatsPanel statistics={statistics} />
@ -378,13 +390,20 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
resultIds: Array.from(selectedResults)
})}
disabled={batchOperationLoading}
className="btn btn-danger btn-sm"
className={`btn btn-danger btn-sm ${batchOperationLoading ? 'opacity-75 cursor-not-allowed' : ''}`}
>
{batchOperationLoading && (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{batchOperationLoading ? '删除中...' : `批量删除 (${selectedResults.size})`}
</button>
<button
onClick={() => setSelectedResults(new Set())}
className="btn btn-secondary btn-sm"
disabled={batchOperationLoading}
className={`btn btn-secondary btn-sm ${batchOperationLoading ? 'opacity-50 cursor-not-allowed' : ''}`}
>
</button>
@ -423,12 +442,13 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
{/* 列表控制栏 */}
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border border-gray-200/50 p-4">
<div className="flex items-center space-x-3">
<label className="flex items-center space-x-2 cursor-pointer">
<label className={`flex items-center space-x-2 ${batchOperationLoading ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}>
<input
type="checkbox"
checked={selectedResults.size === results.length && results.length > 0}
onChange={handleToggleSelectAll}
className="form-checkbox h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
disabled={batchOperationLoading}
className="form-checkbox h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500 disabled:opacity-50"
/>
<span className="text-sm font-medium text-gray-700">
{selectedResults.size === results.length && results.length > 0 ? '取消全选' : '全选'}
@ -465,8 +485,9 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
onExportToJianying={() => handleExportToJianying(result)}
onExportToJianyingV2={() => handleExportToJianyingV2(result)}
isSelected={selectedResults.has(result.id)}
onToggleSelect={() => handleToggleSelect(result.id)}
onToggleSelect={() => !batchOperationLoading && handleToggleSelect(result.id)}
showExportStatus={true}
disabled={batchOperationLoading}
/>
))}
</div>
@ -527,9 +548,28 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
isOpen={batchDeleteConfirm.show}
title="批量删除匹配结果"
message={`确定要删除选中的 ${batchDeleteConfirm.resultIds.length} 个匹配结果吗?此操作将同时重置相关资源的使用状态,且不可撤销。`}
onConfirm={() => handleBatchDelete(batchDeleteConfirm.resultIds)}
onCancel={() => setBatchDeleteConfirm({ show: false, resultIds: [] })}
deleting={batchOperationLoading}
onConfirm={handleConfirmBatchDelete}
onCancel={() => !batchOperationLoading && setBatchDeleteConfirm({ show: false, resultIds: [] })}
/>
{/* 批量操作Loading遮罩 */}
{batchOperationLoading && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 shadow-xl max-w-sm w-full mx-4">
<div className="flex items-center space-x-3">
<svg className="animate-spin h-6 w-6 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<div>
<h3 className="text-lg font-medium text-gray-900"></h3>
<p className="text-sm text-gray-500"> {selectedResults.size} ...</p>
</div>
</div>
</div>
</div>
)}
</div>
);
};