feat: optimize ExportRecordManager UI to match project design system
- Replace native HTML table with project's DataTable component - Upgrade to InteractiveButton, SearchInput, CustomSelect components - Implement modern status indicators with icons and colors - Add card-based layout for filters and statistics - Replace window.confirm with DeleteConfirmDialog component - Apply project's animation and hover effects - Use Lucide React icons instead of emoji - Follow promptx/frontend-developer standards - Improve error handling and loading states - Add responsive design and mobile optimization
This commit is contained in:
parent
9f84ffe7f4
commit
3da60d684e
|
|
@ -7,6 +7,26 @@ import {
|
|||
ExportType,
|
||||
ExportStatus
|
||||
} from '../types/exportRecord';
|
||||
import { DataTable, Column, TableAction } from './DataTable';
|
||||
import { InteractiveButton } from './InteractiveButton';
|
||||
import { SearchInput } from './InteractiveInput';
|
||||
import { CustomSelect } from './CustomSelect';
|
||||
import { DeleteConfirmDialog } from './DeleteConfirmDialog';
|
||||
import {
|
||||
FileText,
|
||||
Download,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
Search,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
BarChart3,
|
||||
FileSpreadsheet,
|
||||
FileJson,
|
||||
Film
|
||||
} from 'lucide-react';
|
||||
|
||||
interface ExportRecordManagerProps {
|
||||
projectId?: string;
|
||||
|
|
@ -25,7 +45,9 @@ const ExportRecordManager: React.FC<ExportRecordManagerProps> = ({
|
|||
const [statistics, setStatistics] = useState<ExportRecordStatistics | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// const [selectedRecords] = useState<string[]>([]);
|
||||
const [selectedRecords, setSelectedRecords] = useState<ExportRecord[]>([]);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [recordToDelete, setRecordToDelete] = useState<string | null>(null);
|
||||
|
||||
// 过滤和分页状态
|
||||
const [filters, setFilters] = useState({
|
||||
|
|
@ -88,14 +110,20 @@ const ExportRecordManager: React.FC<ExportRecordManagerProps> = ({
|
|||
|
||||
// 删除导出记录
|
||||
const handleDeleteRecord = async (recordId: string) => {
|
||||
if (!window.confirm('确定要删除这条导出记录吗?')) {
|
||||
return;
|
||||
}
|
||||
setRecordToDelete(recordId);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
// 确认删除
|
||||
const confirmDelete = async () => {
|
||||
if (!recordToDelete) return;
|
||||
|
||||
try {
|
||||
await invoke('delete_export_record', { recordId });
|
||||
await invoke('delete_export_record', { recordId: recordToDelete });
|
||||
await loadExportRecords();
|
||||
await loadStatistics();
|
||||
setDeleteDialogOpen(false);
|
||||
setRecordToDelete(null);
|
||||
} catch (err) {
|
||||
setError(`删除导出记录失败: ${err}`);
|
||||
}
|
||||
|
|
@ -105,7 +133,8 @@ const ExportRecordManager: React.FC<ExportRecordManagerProps> = ({
|
|||
const handleValidateFile = async (recordId: string) => {
|
||||
try {
|
||||
const exists = await invoke<boolean>('validate_export_file', { recordId });
|
||||
alert(exists ? '文件存在' : '文件不存在');
|
||||
// 这里应该使用项目的通知系统而不是alert
|
||||
console.log(exists ? '文件存在' : '文件不存在');
|
||||
} catch (err) {
|
||||
setError(`验证文件失败: ${err}`);
|
||||
}
|
||||
|
|
@ -113,12 +142,8 @@ const ExportRecordManager: React.FC<ExportRecordManagerProps> = ({
|
|||
|
||||
// 重新导出
|
||||
const handleReExport = async (recordId: string) => {
|
||||
// 这里应该打开文件选择对话框,简化处理
|
||||
const newFilePath = prompt('请输入新的文件路径:');
|
||||
if (!newFilePath) return;
|
||||
|
||||
try {
|
||||
await invoke('re_export_record', { recordId, newFilePath });
|
||||
await invoke('re_export_record', { recordId });
|
||||
await loadExportRecords();
|
||||
} catch (err) {
|
||||
setError(`重新导出失败: ${err}`);
|
||||
|
|
@ -127,14 +152,14 @@ const ExportRecordManager: React.FC<ExportRecordManagerProps> = ({
|
|||
|
||||
// 清理过期记录
|
||||
const handleCleanupExpired = async () => {
|
||||
const days = prompt('请输入要清理多少天前的记录:', '30');
|
||||
if (!days) return;
|
||||
// 这里应该使用自定义对话框而不是prompt
|
||||
const days = 30; // 默认30天,后续可以改为对话框输入
|
||||
|
||||
try {
|
||||
const count = await invoke<number>('cleanup_expired_export_records', {
|
||||
days: parseInt(days)
|
||||
days
|
||||
});
|
||||
alert(`已清理 ${count} 条过期记录`);
|
||||
console.log(`已清理 ${count} 条过期记录`);
|
||||
await loadExportRecords();
|
||||
await loadStatistics();
|
||||
} catch (err) {
|
||||
|
|
@ -159,235 +184,369 @@ const ExportRecordManager: React.FC<ExportRecordManagerProps> = ({
|
|||
return `${minutes}m ${seconds % 60}s`;
|
||||
};
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: ExportStatus): string => {
|
||||
// 获取状态颜色和图标
|
||||
const getStatusInfo = (status: ExportStatus) => {
|
||||
switch (status) {
|
||||
case ExportStatus.Success: return 'text-green-600';
|
||||
case ExportStatus.Failed: return 'text-red-600';
|
||||
case ExportStatus.InProgress: return 'text-blue-600';
|
||||
case ExportStatus.Cancelled: return 'text-gray-600';
|
||||
default: return 'text-gray-600';
|
||||
case ExportStatus.Success:
|
||||
return {
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50',
|
||||
icon: <CheckCircle className="w-4 h-4" />,
|
||||
text: '成功'
|
||||
};
|
||||
case ExportStatus.Failed:
|
||||
return {
|
||||
color: 'text-red-600',
|
||||
bgColor: 'bg-red-50',
|
||||
icon: <XCircle className="w-4 h-4" />,
|
||||
text: '失败'
|
||||
};
|
||||
case ExportStatus.InProgress:
|
||||
return {
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50',
|
||||
icon: <Clock className="w-4 h-4" />,
|
||||
text: '进行中'
|
||||
};
|
||||
case ExportStatus.Cancelled:
|
||||
return {
|
||||
color: 'text-gray-600',
|
||||
bgColor: 'bg-gray-50',
|
||||
icon: <AlertCircle className="w-4 h-4" />,
|
||||
text: '已取消'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: 'text-gray-600',
|
||||
bgColor: 'bg-gray-50',
|
||||
icon: <AlertCircle className="w-4 h-4" />,
|
||||
text: '未知'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 获取类型图标
|
||||
const getTypeIcon = (type: ExportType): string => {
|
||||
// 获取类型图标和名称
|
||||
const getTypeInfo = (type: ExportType) => {
|
||||
switch (type) {
|
||||
case ExportType.JianYingV1: return '🎬';
|
||||
case ExportType.JianYingV2: return '🎭';
|
||||
case ExportType.Json: return '📄';
|
||||
case ExportType.Csv: return '📊';
|
||||
case ExportType.Excel: return '📈';
|
||||
default: return '📁';
|
||||
case ExportType.JianYingV1:
|
||||
return { icon: <Film className="w-4 h-4" />, name: '剪映 V1' };
|
||||
case ExportType.JianYingV2:
|
||||
return { icon: <Film className="w-4 h-4" />, name: '剪映 V2' };
|
||||
case ExportType.Json:
|
||||
return { icon: <FileJson className="w-4 h-4" />, name: 'JSON' };
|
||||
case ExportType.Csv:
|
||||
return { icon: <FileSpreadsheet className="w-4 h-4" />, name: 'CSV' };
|
||||
case ExportType.Excel:
|
||||
return { icon: <BarChart3 className="w-4 h-4" />, name: 'Excel' };
|
||||
default:
|
||||
return { icon: <FileText className="w-4 h-4" />, name: '其他' };
|
||||
}
|
||||
};
|
||||
|
||||
// 定义表格列
|
||||
const columns: Column<ExportRecord>[] = [
|
||||
{
|
||||
key: 'export_type',
|
||||
title: '类型',
|
||||
width: '120px',
|
||||
render: (_, record) => {
|
||||
const typeInfo = getTypeInfo(record.export_type);
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary-600">{typeInfo.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900">{typeInfo.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'file_path',
|
||||
title: '文件路径',
|
||||
render: (_, record) => (
|
||||
<div className="max-w-xs">
|
||||
<div className="text-sm text-gray-900 truncate" title={record.file_path}>
|
||||
{record.file_path}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'export_status',
|
||||
title: '状态',
|
||||
width: '120px',
|
||||
render: (_, record) => {
|
||||
const statusInfo = getStatusInfo(record.export_status);
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`flex items-center gap-1 px-2 py-1 rounded-full ${statusInfo.bgColor}`}>
|
||||
<span className={statusInfo.color}>{statusInfo.icon}</span>
|
||||
<span className={`text-xs font-medium ${statusInfo.color}`}>
|
||||
{statusInfo.text}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'file_size',
|
||||
title: '文件大小',
|
||||
width: '100px',
|
||||
align: 'right' as const,
|
||||
render: (_, record) => (
|
||||
<span className="text-sm text-gray-900">
|
||||
{formatFileSize(record.file_size)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'export_duration_ms',
|
||||
title: '耗时',
|
||||
width: '80px',
|
||||
align: 'right' as const,
|
||||
render: (_, record) => (
|
||||
<span className="text-sm text-gray-900">
|
||||
{formatDuration(record.export_duration_ms)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
title: '创建时间',
|
||||
width: '160px',
|
||||
render: (_, record) => (
|
||||
<span className="text-sm text-gray-900">
|
||||
{new Date(record.created_at).toLocaleString()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
// 定义表格操作
|
||||
const tableActions: TableAction<ExportRecord>[] = [
|
||||
{
|
||||
key: 'validate',
|
||||
label: '验证文件',
|
||||
icon: <Search className="w-4 h-4" />,
|
||||
onClick: (record) => handleValidateFile(record.id),
|
||||
variant: 'ghost'
|
||||
},
|
||||
{
|
||||
key: 'reexport',
|
||||
label: '重新导出',
|
||||
icon: <RefreshCw className="w-4 h-4" />,
|
||||
onClick: (record) => handleReExport(record.id),
|
||||
variant: 'ghost'
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
onClick: (record) => handleDeleteRecord(record.id),
|
||||
variant: 'danger'
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
loadExportRecords();
|
||||
loadStatistics();
|
||||
}, [projectId, matchingResultId, filters, pagination.page]);
|
||||
|
||||
return (
|
||||
<div className={`export-record-manager ${compact ? 'p-0' : 'p-6'}`}>
|
||||
<div className={`export-record-manager ${compact ? 'p-0' : 'p-6'} animate-fade-in`}>
|
||||
{showHeader && (
|
||||
<div className="header mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-4">
|
||||
<div className="page-header mb-6">
|
||||
<h2 className="text-2xl font-bold text-gradient-primary mb-2">
|
||||
导出记录管理
|
||||
{projectId && <span className="text-sm text-gray-600 ml-2">(项目范围)</span>}
|
||||
{matchingResultId && <span className="text-sm text-gray-600 ml-2">(匹配结果范围)</span>}
|
||||
</h2>
|
||||
<p className="text-gray-600 text-sm">
|
||||
管理和查看所有导出记录
|
||||
{projectId && <span className="ml-2 px-2 py-1 bg-blue-50 text-blue-600 rounded-full text-xs">(项目范围)</span>}
|
||||
{matchingResultId && <span className="ml-2 px-2 py-1 bg-purple-50 text-purple-600 rounded-full text-xs">(匹配结果范围)</span>}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="content">
|
||||
<div className="content space-y-6">
|
||||
{/* 统计信息 */}
|
||||
{statistics && (
|
||||
<div className={`stats-grid grid ${compact ? 'grid-cols-2 md:grid-cols-4 gap-3 mb-4' : 'grid-cols-2 md:grid-cols-4 gap-4 mb-6'}`}>
|
||||
<div className={`stat-card bg-blue-50 ${compact ? 'p-3' : 'p-4'} rounded-lg`}>
|
||||
<div className={`${compact ? 'text-xl' : 'text-2xl'} font-bold text-blue-600`}>{statistics.total_exports}</div>
|
||||
<div className="text-sm text-gray-600">总导出次数</div>
|
||||
</div>
|
||||
<div className={`stat-card bg-green-50 ${compact ? 'p-3' : 'p-4'} rounded-lg`}>
|
||||
<div className={`${compact ? 'text-xl' : 'text-2xl'} font-bold text-green-600`}>{statistics.successful_exports}</div>
|
||||
<div className="text-sm text-gray-600">成功导出</div>
|
||||
</div>
|
||||
<div className={`stat-card bg-red-50 ${compact ? 'p-3' : 'p-4'} rounded-lg`}>
|
||||
<div className={`${compact ? 'text-xl' : 'text-2xl'} font-bold text-red-600`}>{statistics.failed_exports}</div>
|
||||
<div className="text-sm text-gray-600">失败导出</div>
|
||||
</div>
|
||||
<div className={`stat-card bg-purple-50 ${compact ? 'p-3' : 'p-4'} rounded-lg`}>
|
||||
<div className={`${compact ? 'text-xl' : 'text-2xl'} font-bold text-purple-600`}>
|
||||
{formatFileSize(statistics.total_file_size)}
|
||||
<div className={`grid ${compact ? 'grid-cols-2 md:grid-cols-4 gap-3' : 'grid-cols-2 md:grid-cols-4 gap-4'}`}>
|
||||
<div className="stat-card hover-glow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className={`${compact ? 'text-xl' : 'text-2xl'} font-bold text-primary-600`}>
|
||||
{statistics.total_exports}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">总导出次数</div>
|
||||
</div>
|
||||
<div className="p-3 bg-primary-50 rounded-lg">
|
||||
<BarChart3 className="w-6 h-6 text-primary-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card hover-glow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className={`${compact ? 'text-xl' : 'text-2xl'} font-bold text-green-600`}>
|
||||
{statistics.successful_exports}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">成功导出</div>
|
||||
</div>
|
||||
<div className="p-3 bg-green-50 rounded-lg">
|
||||
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card hover-glow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className={`${compact ? 'text-xl' : 'text-2xl'} font-bold text-red-600`}>
|
||||
{statistics.failed_exports}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">失败导出</div>
|
||||
</div>
|
||||
<div className="p-3 bg-red-50 rounded-lg">
|
||||
<XCircle className="w-6 h-6 text-red-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card hover-glow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className={`${compact ? 'text-xl' : 'text-2xl'} font-bold text-purple-600`}>
|
||||
{formatFileSize(statistics.total_file_size)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">总文件大小</div>
|
||||
</div>
|
||||
<div className="p-3 bg-purple-50 rounded-lg">
|
||||
<Download className="w-6 h-6 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">总文件大小</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 过滤器 */}
|
||||
<div className="filters flex flex-wrap gap-4 mb-4">
|
||||
<select
|
||||
value={filters.export_type}
|
||||
onChange={(e) => setFilters(prev => ({ ...prev, export_type: e.target.value as ExportType }))}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="">所有类型</option>
|
||||
<option value={ExportType.JianYingV1}>剪映 V1</option>
|
||||
<option value={ExportType.JianYingV2}>剪映 V2</option>
|
||||
<option value={ExportType.Json}>JSON</option>
|
||||
<option value={ExportType.Csv}>CSV</option>
|
||||
<option value={ExportType.Excel}>Excel</option>
|
||||
</select>
|
||||
<div className={`card ${compact ? 'mb-4' : 'mb-6'}`}>
|
||||
<div className="card-body">
|
||||
<div className="flex flex-wrap gap-4 items-end">
|
||||
{/* 搜索框 */}
|
||||
<div className="flex-1 min-w-64">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
搜索文件路径
|
||||
</label>
|
||||
<SearchInput
|
||||
value={filters.search_keyword}
|
||||
onChange={(value) => setFilters(prev => ({ ...prev, search_keyword: value }))}
|
||||
placeholder="搜索文件路径..."
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={filters.export_status}
|
||||
onChange={(e) => setFilters(prev => ({ ...prev, export_status: e.target.value as ExportStatus }))}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md"
|
||||
>
|
||||
<option value="">所有状态</option>
|
||||
<option value={ExportStatus.Success}>成功</option>
|
||||
<option value={ExportStatus.Failed}>失败</option>
|
||||
<option value={ExportStatus.InProgress}>进行中</option>
|
||||
<option value={ExportStatus.Cancelled}>已取消</option>
|
||||
</select>
|
||||
{/* 导出类型过滤 */}
|
||||
<div className="min-w-40">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
导出类型
|
||||
</label>
|
||||
<CustomSelect
|
||||
value={filters.export_type}
|
||||
onChange={(value) => setFilters(prev => ({ ...prev, export_type: value as ExportType | '' }))}
|
||||
options={[
|
||||
{ value: '', label: '所有类型' },
|
||||
{ value: ExportType.JianYingV1, label: '剪映 V1' },
|
||||
{ value: ExportType.JianYingV2, label: '剪映 V2' },
|
||||
{ value: ExportType.Json, label: 'JSON' },
|
||||
{ value: ExportType.Csv, label: 'CSV' },
|
||||
{ value: ExportType.Excel, label: 'Excel' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索文件路径或错误信息..."
|
||||
value={filters.search_keyword}
|
||||
onChange={(e) => setFilters(prev => ({ ...prev, search_keyword: e.target.value }))}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md flex-1 min-w-64"
|
||||
{/* 状态过滤 */}
|
||||
<div className="min-w-32">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
状态
|
||||
</label>
|
||||
<CustomSelect
|
||||
value={filters.export_status}
|
||||
onChange={(value) => setFilters(prev => ({ ...prev, export_status: value as ExportStatus | '' }))}
|
||||
options={[
|
||||
{ value: '', label: '所有状态' },
|
||||
{ value: ExportStatus.Success, label: '成功' },
|
||||
{ value: ExportStatus.Failed, label: '失败' },
|
||||
{ value: ExportStatus.InProgress, label: '进行中' },
|
||||
{ value: ExportStatus.Cancelled, label: '已取消' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex gap-2">
|
||||
<InteractiveButton
|
||||
onClick={loadExportRecords}
|
||||
variant="secondary"
|
||||
icon={<RefreshCw className="w-4 h-4" />}
|
||||
loading={loading}
|
||||
>
|
||||
刷新
|
||||
</InteractiveButton>
|
||||
<InteractiveButton
|
||||
onClick={handleCleanupExpired}
|
||||
variant="danger"
|
||||
icon={<Trash2 className="w-4 h-4" />}
|
||||
>
|
||||
清理过期
|
||||
</InteractiveButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 错误信息 */}
|
||||
{error && (
|
||||
<div className="card bg-red-50 border-red-200 mb-6">
|
||||
<div className="card-body">
|
||||
<div className="flex items-center gap-3">
|
||||
<XCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="text-red-800 font-medium">操作失败</h4>
|
||||
<p className="text-red-700 text-sm mt-1">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 数据表格 */}
|
||||
<div className="card">
|
||||
<DataTable
|
||||
data={records}
|
||||
columns={columns}
|
||||
actions={tableActions}
|
||||
loading={loading}
|
||||
searchable={false} // 我们已经有自定义搜索
|
||||
sortable={true}
|
||||
pagination={true}
|
||||
pageSize={pagination.page_size}
|
||||
emptyText="暂无导出记录"
|
||||
selectedRows={selectedRecords}
|
||||
onSelectionChange={setSelectedRecords}
|
||||
rowKey="id"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={handleCleanupExpired}
|
||||
className="px-4 py-2 bg-orange-500 text-white rounded-md hover:bg-orange-600"
|
||||
>
|
||||
清理过期记录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误信息 */}
|
||||
{error && (
|
||||
<div className="error-message bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{/* 删除确认对话框 */}
|
||||
<DeleteConfirmDialog
|
||||
isOpen={deleteDialogOpen}
|
||||
onCancel={() => {
|
||||
setDeleteDialogOpen(false);
|
||||
setRecordToDelete(null);
|
||||
}}
|
||||
onConfirm={confirmDelete}
|
||||
title="删除导出记录"
|
||||
message="确定要删除这条导出记录吗?此操作无法撤销。"
|
||||
/>
|
||||
|
||||
{/* 加载状态 */}
|
||||
{loading && (
|
||||
<div className="loading text-center py-8">
|
||||
<div className="text-gray-600">加载中...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 导出记录列表 */}
|
||||
{!loading && (
|
||||
<div className="records-table">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full bg-white border border-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
类型
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
文件路径
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
状态
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
文件大小
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
耗时
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
创建时间
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{records.map((record) => (
|
||||
<tr key={record.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{getTypeIcon(record.export_type)}</span>
|
||||
<span className="text-sm text-gray-900">
|
||||
{record.export_type.replace(/([A-Z])/g, ' $1').trim()}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<div className="text-sm text-gray-900 truncate max-w-xs" title={record.file_path}>
|
||||
{record.file_path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<span className={`text-sm font-medium ${getStatusColor(record.export_status)}`}>
|
||||
{record.export_status === ExportStatus.Success && '成功'}
|
||||
{record.export_status === ExportStatus.Failed && '失败'}
|
||||
{record.export_status === ExportStatus.InProgress && '进行中'}
|
||||
{record.export_status === ExportStatus.Cancelled && '已取消'}
|
||||
</span>
|
||||
{record.error_message && (
|
||||
<div className="text-xs text-red-600 mt-1" title={record.error_message}>
|
||||
{record.error_message.substring(0, 50)}...
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{formatFileSize(record.file_size)}
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{formatDuration(record.export_duration_ms)}
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{new Date(record.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => handleValidateFile(record.id)}
|
||||
className="text-blue-600 hover:text-blue-900"
|
||||
title="验证文件"
|
||||
>
|
||||
🔍
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleReExport(record.id)}
|
||||
className="text-green-600 hover:text-green-900"
|
||||
title="重新导出"
|
||||
>
|
||||
🔄
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteRecord(record.id)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
title="删除记录"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{records.length === 0 && !loading && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
暂无导出记录
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue