Files
mixvideo-v2/apps/desktop/src/components/BatchDeleteConfirmDialog.tsx
imeepos 6bd12a4a63 feat: 实现项目素材批量删除功能
- 后端实现:
  * 在MaterialRepository中添加batch_delete方法,支持事务处理
  * 在MaterialService中添加batch_delete_materials业务逻辑
  * 添加BatchDeleteResult和BatchDeleteFailedItem数据结构
  * 新增batch_delete_materials Tauri命令接口
  * 实现参数验证和错误处理机制

- 前端实现:
  * 创建useBatchSelection Hook管理批量选择状态
  * 实现BatchDeleteConfirmDialog批量删除确认对话框
  * 在MaterialCard组件中添加批量选择支持
  * 在ProjectDetails页面集成批量选择和删除功能
  * 添加批量操作UI控件(全选/取消全选/批量删除按钮)

- 功能特性:
  * 支持最多50个素材的批量选择
  * 单次最多删除100个素材的限制
  * 详细的删除结果反馈(成功/失败统计)
  * 失败项目的具体错误信息显示
  * 批量选择模式的视觉反馈
  * 完善的用户确认和通知机制

- 测试:
  * 添加批量删除功能的单元测试
  * 测试数据结构创建和验证逻辑

遵循Tauri开发规范和前端UI/UX设计标准,提供安全可靠的批量删除体验。
2025-07-21 14:28:06 +08:00

207 lines
8.0 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 } from 'react';
import { ExclamationTriangleIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/outline';
import { LoadingSpinner } from './LoadingSpinner';
import { Modal } from './Modal';
import { Material, BatchDeleteResult } from '../types/material';
interface BatchDeleteConfirmDialogProps {
/** 是否显示对话框 */
isOpen: boolean;
/** 要删除的素材列表 */
materials: Material[];
/** 是否正在删除 */
deleting?: boolean;
/** 删除结果(删除完成后显示) */
deleteResult?: BatchDeleteResult | null;
/** 确认删除回调 */
onConfirm: () => void;
/** 取消回调 */
onCancel: () => void;
/** 关闭结果对话框回调 */
onCloseResult?: () => void;
}
/**
* 批量删除确认对话框组件
* 遵循前端开发规范的确认对话框设计,提供安全的批量删除确认流程
*/
export const BatchDeleteConfirmDialog: React.FC<BatchDeleteConfirmDialogProps> = ({
isOpen,
materials,
deleting,
deleteResult,
onConfirm,
onCancel,
onCloseResult,
}) => {
const [showDetails, setShowDetails] = useState(false);
// 如果有删除结果,显示结果对话框
if (deleteResult) {
return (
<Modal
isOpen={isOpen}
onClose={deleting ? () => {} : (onCloseResult || onCancel)}
title="批量删除结果"
icon={deleteResult.failed_count > 0 ?
<ExclamationTriangleIcon className="h-6 w-6" /> :
<CheckCircleIcon className="h-6 w-6" />
}
size="md"
variant={deleteResult.failed_count > 0 ? "warning" : "success"}
closeOnBackdropClick={!deleting}
closeOnEscape={!deleting}
>
<div className="p-6">
{/* 删除结果统计 */}
<div className="mb-6">
<div className="grid grid-cols-3 gap-4 text-center">
<div className="p-4 bg-gray-50 rounded-lg">
<div className="text-2xl font-bold text-gray-900">{deleteResult.total_count}</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="p-4 bg-green-50 rounded-lg">
<div className="text-2xl font-bold text-green-600">{deleteResult.success_count}</div>
<div className="text-sm text-green-600"></div>
</div>
<div className="p-4 bg-red-50 rounded-lg">
<div className="text-2xl font-bold text-red-600">{deleteResult.failed_count}</div>
<div className="text-sm text-red-600"></div>
</div>
</div>
</div>
{/* 成功消息 */}
{deleteResult.success_count > 0 && (
<div className="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-start gap-2">
<CheckCircleIcon className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm font-medium text-green-800">
{deleteResult.success_count}
</p>
</div>
</div>
</div>
)}
{/* 失败详情 */}
{deleteResult.failed_count > 0 && (
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start gap-2">
<XCircleIcon className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-red-800 mb-2">
{deleteResult.failed_count}
</p>
<button
onClick={() => setShowDetails(!showDetails)}
className="text-sm text-red-700 hover:text-red-800 underline"
>
{showDetails ? '隐藏详情' : '查看详情'}
</button>
{showDetails && (
<div className="mt-3 space-y-2">
{deleteResult.failed_items.map((item, index) => (
<div key={index} className="text-xs bg-white p-2 rounded border">
<div className="font-medium text-gray-900 truncate" title={item.id}>
ID: {item.id}
</div>
<div className="text-red-600 mt-1">{item.error_message}</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
)}
{/* 按钮区域 */}
<div className="flex justify-end">
<button
type="button"
onClick={onCloseResult || onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
>
</button>
</div>
</div>
</Modal>
);
}
// 确认删除对话框
return (
<Modal
isOpen={isOpen}
onClose={deleting ? () => {} : onCancel}
title="批量删除素材"
icon={<ExclamationTriangleIcon className="h-6 w-6" />}
size="md"
variant="danger"
closeOnBackdropClick={!deleting}
closeOnEscape={!deleting}
>
<div className="p-6">
<p className="text-gray-600 mb-4 leading-relaxed">
<span className="font-semibold text-gray-900">{materials.length}</span>
</p>
{/* 素材列表预览 */}
<div className="mb-4 p-4 bg-gray-50 rounded-lg border max-h-48 overflow-y-auto">
<p className="text-sm font-medium text-gray-700 mb-2"></p>
<div className="space-y-1">
{materials.slice(0, 10).map((material) => (
<div key={material.id} className="text-sm text-gray-600 truncate" title={material.name}>
{material.name}
</div>
))}
{materials.length > 10 && (
<div className="text-sm text-gray-500 italic">
... {materials.length - 10}
</div>
)}
</div>
</div>
<div className="p-4 bg-red-50 border border-red-200 rounded-lg mb-6">
<div className="flex items-start gap-2">
<ExclamationTriangleIcon className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm font-medium text-red-800 mb-1"></p>
<p className="text-sm text-red-700">
</p>
</div>
</div>
</div>
{/* 按钮区域 */}
<div className="flex gap-3 justify-end">
<button
type="button"
onClick={onCancel}
disabled={deleting}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
</button>
<button
type="button"
onClick={onConfirm}
disabled={deleting}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
>
{deleting && <LoadingSpinner size="small" />}
{deleting ? '删除中...' : '确认删除'}
</button>
</div>
</div>
</Modal>
);
};