Files
mixvideo-v2/apps/desktop/src/pages/TemplateManagement.tsx
imeepos 1b7f7b44a8 feat: Complete comprehensive UI/UX enhancement phase
Enhanced Interactive Components:
- Created InteractiveButton with ripple effects, haptic feedback, and multiple variants
- Developed InteractiveInput and InteractiveTextarea with real-time validation and status indicators
- Added FloatingActionButton for quick actions with elegant tooltips
- Implemented comprehensive micro-interactions and animations

 Advanced Loading & Skeleton States:
- Enhanced SkeletonLoader with multiple variants (model, material, template, table-row)
- Added specialized skeleton components (ModelCardSkeleton, MaterialCardSkeleton, etc.)
- Created EnhancedLoadingState with progress indicators and operation tracking
- Implemented BatchOperationLoading for complex workflows

 Optimized Form Experience:
- Upgraded ProjectForm with new interactive components
- Added real-time validation feedback and error animations
- Implemented smart input states (success, error, loading)
- Enhanced user feedback with visual and haptic responses

 Perfected Empty States:
- Redesigned EmptyState with multiple variants and illustrations
- Created specialized empty state components (EmptyProjectList, EmptyModelList, etc.)
- Added contextual tips and guidance for better user onboarding
- Implemented error states and recovery actions

 Advanced Data Display:
- Built comprehensive DataTable with search, sort, filter, and pagination
- Created flexible CardGrid with view switching and bulk operations
- Added row selection, bulk actions, and advanced filtering
- Implemented responsive layouts and mobile optimization

 Rich Animation System:
- Added 20+ new micro-interaction animations
- Implemented button press, success pulse, error shake effects
- Created smooth slide-in animations for all directions
- Added loading dots, heartbeat, and bounce-in animations

 Key Features:
- Ripple effects on button clicks with haptic feedback
- Real-time form validation with animated error states
- Contextual empty states with actionable guidance
- Advanced data tables with full CRUD operations
- Responsive card grids with multiple view modes
- Comprehensive loading states for better perceived performance

All components now provide rich visual feedback, smooth animations, and professional user experience that matches modern design standards.
2025-07-15 20:38:34 +08:00

375 lines
14 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, useEffect } from 'react';
import { Plus, Upload, Search, Filter, FileText, Clock, CheckCircle, XCircle, AlertCircle } from 'lucide-react';
import { Template, ImportStatus } from '../types/template';
import { TemplateCard } from '../components/template/TemplateCard';
import { ImportTemplateModal } from '../components/template/ImportTemplateModal';
import { BatchImportModal } from '../components/template/BatchImportModal';
import { TemplateDetailModal } from '../components/template/TemplateDetailModal';
import { ImportProgressModal } from '../components/template/ImportProgressModal';
import { BatchImportProgressModal } from '../components/template/BatchImportProgressModal';
import { CustomSelect } from '../components/CustomSelect';
import { DeleteConfirmDialog } from '../components/DeleteConfirmDialog';
import { TemplateCardSkeleton } from '../components/SkeletonLoader';
import { useTemplateStore } from '../stores/templateStore';
const TemplateManagement: React.FC = () => {
const [templates, setTemplates] = useState<Template[]>([]);
const [loading, setLoading] = useState(true);
const [searchKeyword, setSearchKeyword] = useState('');
const [statusFilter, setStatusFilter] = useState<ImportStatus | 'all'>('all');
const [showImportModal, setShowImportModal] = useState(false);
const [showBatchImportModal, setShowBatchImportModal] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
const [showImportProgress, setShowImportProgress] = useState(false);
const [showBatchProgress, setShowBatchProgress] = useState(false);
const [currentImportId, setCurrentImportId] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [templateToDelete, setTemplateToDelete] = useState<string | null>(null);
const [deleting, setDeleting] = useState(false);
const pageSize = 12;
const {
fetchTemplates,
importTemplate,
batchImportTemplates,
deleteTemplate,
isLoading
} = useTemplateStore();
// 加载模板列表
const loadTemplates = async () => {
try {
setLoading(true);
const response = await fetchTemplates({
search_keyword: searchKeyword || undefined,
import_status: statusFilter === 'all' ? undefined : statusFilter,
page: currentPage,
page_size: pageSize,
});
console.log(response)
setTemplates(response.templates);
setTotalPages(Math.ceil(response.total / pageSize));
} catch (error) {
console.error('加载模板列表失败:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadTemplates();
}, [searchKeyword, statusFilter, currentPage]);
// 处理搜索
const handleSearch = (value: string) => {
setSearchKeyword(value);
setCurrentPage(1);
};
// 处理状态筛选
const handleStatusFilter = (value: string) => {
const status = value as ImportStatus | 'all';
setStatusFilter(status);
setCurrentPage(1);
};
// 处理单个模板导入
const handleImportTemplate = async (request: any) => {
try {
const templateId = await importTemplate(request);
setShowImportModal(false);
setCurrentImportId(templateId);
setShowImportProgress(true);
} catch (error) {
console.error('模板导入失败:', error);
}
};
// 处理批量导入
const handleBatchImport = async (request: any) => {
try {
await batchImportTemplates(request);
setShowBatchImportModal(false);
setShowBatchProgress(true);
} catch (error) {
console.error('批量导入失败:', error);
}
};
// 处理导入完成
const handleImportComplete = () => {
// 立即刷新模板列表,显示新导入的模板
loadTemplates();
// 注意:不在这里关闭弹窗,弹窗会自动关闭
};
// 处理批量导入完成
const handleBatchImportComplete = () => {
setShowBatchProgress(false);
loadTemplates();
};
// 处理删除模板
const handleDeleteTemplate = (templateId: string) => {
setTemplateToDelete(templateId);
setShowDeleteConfirm(true);
};
// 确认删除模板
const confirmDeleteTemplate = async () => {
if (!templateToDelete) return;
try {
setDeleting(true);
await deleteTemplate(templateToDelete);
loadTemplates();
setShowDeleteConfirm(false);
setTemplateToDelete(null);
} catch (error) {
console.error('删除模板失败:', error);
} finally {
setDeleting(false);
}
};
// 取消删除
const cancelDeleteTemplate = () => {
setShowDeleteConfirm(false);
setTemplateToDelete(null);
};
// 获取状态图标
const getStatusIcon = (status: ImportStatus) => {
switch (status) {
case ImportStatus.Completed:
return <CheckCircle className="w-4 h-4 text-green-500" />;
case ImportStatus.Failed:
return <XCircle className="w-4 h-4 text-red-500" />;
case ImportStatus.Processing:
case ImportStatus.Uploading:
case ImportStatus.Parsing:
return <Clock className="w-4 h-4 text-blue-500 animate-spin" />;
default:
return <AlertCircle className="w-4 h-4 text-yellow-500" />;
}
};
// 获取状态文本
const getStatusText = (status: ImportStatus) => {
switch (status) {
case ImportStatus.Pending:
return '等待导入';
case ImportStatus.Parsing:
return '解析中';
case ImportStatus.Uploading:
return '上传中';
case ImportStatus.Processing:
return '处理中';
case ImportStatus.Completed:
return '已完成';
case ImportStatus.Failed:
return '导入失败';
default:
return '未知状态';
}
};
const statusOptions = [
{ value: 'all', label: '全部状态' },
{ value: ImportStatus.Completed, label: '已完成' },
{ value: ImportStatus.Processing, label: '处理中' },
{ value: ImportStatus.Failed, label: '失败' },
{ value: ImportStatus.Pending, label: '等待中' },
];
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto">
{/* 美观的页面头部 */}
<div className="mb-6">
<div className="bg-gradient-to-r from-white via-indigo-50/30 to-white rounded-xl shadow-sm border border-gray-200/50 p-6 mb-6 relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-indigo-100/30 to-purple-100/30 rounded-full -translate-y-16 translate-x-16 opacity-50"></div>
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 relative z-10">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-xl flex items-center justify-center shadow-sm">
<FileText className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-1"></h1>
<p className="text-sm text-gray-600"></p>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setShowImportModal(true)}
className="inline-flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 text-white rounded-lg transition-all duration-200 hover:scale-105 shadow-sm hover:shadow-md text-sm font-medium"
>
<Plus className="w-4 h-4" />
</button>
<button
onClick={() => setShowBatchImportModal(true)}
className="inline-flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white rounded-lg transition-all duration-200 hover:scale-105 shadow-sm hover:shadow-md text-sm font-medium"
>
<Upload className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* 美观的搜索和筛选栏 */}
<div className="flex items-center space-x-4 bg-white p-5 rounded-xl shadow-sm border border-gray-200/50">
<div className="flex-1 relative">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="搜索模板名称..."
value={searchKeyword}
onChange={(e) => handleSearch(e.target.value)}
className="w-full pl-11 pr-4 py-3 border border-gray-200 rounded-xl bg-white focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all duration-200 placeholder-gray-400 text-sm text-gray-900 hover:border-gray-300 shadow-sm"
/>
</div>
<div className="flex items-center space-x-3">
<div className="flex items-center gap-2 text-gray-500">
<Filter className="w-4 h-4" />
<span className="text-sm font-medium"></span>
</div>
<CustomSelect
value={statusFilter}
onChange={handleStatusFilter}
options={statusOptions}
placeholder="筛选状态"
className="w-44"
/>
</div>
</div>
</div>
{/* 模板列表 - 优化滚动 */}
<div className="max-h-[calc(100vh-16rem)] overflow-y-auto custom-scrollbar">
{loading ? (
<TemplateCardSkeleton count={8} />
) : templates.length === 0 ? (
<div className="text-center py-16">
<div className="w-20 h-20 bg-gradient-to-br from-gray-100 to-gray-200 rounded-2xl flex items-center justify-center mx-auto mb-6">
<FileText className="w-10 h-10 text-gray-400" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2"></h3>
<p className="text-gray-500 mb-8 max-w-md mx-auto">便</p>
<button
onClick={() => setShowImportModal(true)}
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 text-white rounded-xl transition-all duration-200 hover:scale-105 shadow-sm hover:shadow-md font-medium"
>
<Plus className="w-5 h-5" />
</button>
</div>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 mb-8">
{templates.map((template) => (
<TemplateCard
key={template.id}
template={template}
onView={() => setSelectedTemplate(template)}
onDelete={() => handleDeleteTemplate(template.id)}
getStatusIcon={getStatusIcon}
getStatusText={getStatusText}
/>
))}
</div>
{/* 美观的分页组件 */}
{totalPages > 1 && (
<div className="flex items-center justify-center space-x-3 mt-8">
<button
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
className="inline-flex items-center px-4 py-2.5 bg-white border border-gray-200 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 hover:border-gray-300 transition-all duration-200 text-sm font-medium text-gray-700"
>
</button>
<div className="flex items-center px-4 py-2.5 bg-gradient-to-r from-primary-50 to-primary-100 border border-primary-200 rounded-lg">
<span className="text-sm font-medium text-primary-700">
{currentPage} {totalPages}
</span>
</div>
<button
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
className="inline-flex items-center px-4 py-2.5 bg-white border border-gray-200 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 hover:border-gray-300 transition-all duration-200 text-sm font-medium text-gray-700"
>
</button>
</div>
)}
</>
)}
</div>
</div>
{/* 模态框 */}
{showImportModal && (
<ImportTemplateModal
onClose={() => setShowImportModal(false)}
onImport={handleImportTemplate}
isLoading={isLoading}
/>
)}
{showBatchImportModal && (
<BatchImportModal
onClose={() => setShowBatchImportModal(false)}
onImport={handleBatchImport}
isLoading={isLoading}
/>
)}
{selectedTemplate && (
<TemplateDetailModal
template={selectedTemplate}
onClose={() => setSelectedTemplate(null)}
/>
)}
{showImportProgress && currentImportId && (
<ImportProgressModal
templateId={currentImportId}
onClose={() => {
setShowImportProgress(false);
setCurrentImportId(null);
}}
onComplete={handleImportComplete}
/>
)}
{showBatchProgress && (
<BatchImportProgressModal
onClose={() => setShowBatchProgress(false)}
onComplete={handleBatchImportComplete}
/>
)}
<DeleteConfirmDialog
isOpen={showDeleteConfirm}
title="删除模板"
message="确定要删除这个模板吗?此操作不可撤销。"
deleting={deleting}
onConfirm={confirmDeleteTemplate}
onCancel={cancelDeleteTemplate}
/>
</div>
);
};
export default TemplateManagement;