- 后端实现: * 在MaterialRepository中添加batch_delete方法,支持事务处理 * 在MaterialService中添加batch_delete_materials业务逻辑 * 添加BatchDeleteResult和BatchDeleteFailedItem数据结构 * 新增batch_delete_materials Tauri命令接口 * 实现参数验证和错误处理机制 - 前端实现: * 创建useBatchSelection Hook管理批量选择状态 * 实现BatchDeleteConfirmDialog批量删除确认对话框 * 在MaterialCard组件中添加批量选择支持 * 在ProjectDetails页面集成批量选择和删除功能 * 添加批量操作UI控件(全选/取消全选/批量删除按钮) - 功能特性: * 支持最多50个素材的批量选择 * 单次最多删除100个素材的限制 * 详细的删除结果反馈(成功/失败统计) * 失败项目的具体错误信息显示 * 批量选择模式的视觉反馈 * 完善的用户确认和通知机制 - 测试: * 添加批量删除功能的单元测试 * 测试数据结构创建和验证逻辑 遵循Tauri开发规范和前端UI/UX设计标准,提供安全可靠的批量删除体验。
207 lines
8.0 KiB
TypeScript
207 lines
8.0 KiB
TypeScript
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>
|
||
);
|
||
};
|