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.
375 lines
14 KiB
TypeScript
375 lines
14 KiB
TypeScript
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;
|