2025-06-30 19:42:43 +08:00
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
import DashboardLayout from '../../components/Layout/DashboardLayout';
|
2025-06-29 16:13:50 +08:00
|
|
|
|
import {
|
2025-06-30 19:42:43 +08:00
|
|
|
|
DocumentDuplicateIcon,
|
2025-06-29 16:13:50 +08:00
|
|
|
|
MagnifyingGlassIcon,
|
2025-06-30 19:42:43 +08:00
|
|
|
|
PlusIcon,
|
2025-06-29 16:13:50 +08:00
|
|
|
|
EyeIcon,
|
|
|
|
|
PencilIcon,
|
2025-06-30 19:42:43 +08:00
|
|
|
|
TrashIcon,
|
|
|
|
|
ArrowDownTrayIcon,
|
|
|
|
|
FolderIcon,
|
|
|
|
|
DocumentTextIcon,
|
|
|
|
|
PhotoIcon,
|
|
|
|
|
FilmIcon,
|
|
|
|
|
MusicalNoteIcon,
|
|
|
|
|
ArchiveBoxIcon,
|
2025-06-29 16:13:50 +08:00
|
|
|
|
ChevronLeftIcon,
|
2025-06-30 19:42:43 +08:00
|
|
|
|
ChevronRightIcon,
|
|
|
|
|
CalendarIcon,
|
|
|
|
|
UserIcon,
|
|
|
|
|
TagIcon,
|
|
|
|
|
StarIcon,
|
|
|
|
|
ClockIcon,
|
|
|
|
|
CheckCircleIcon,
|
|
|
|
|
XCircleIcon
|
2025-06-29 16:13:50 +08:00
|
|
|
|
} from '@heroicons/react/24/outline';
|
|
|
|
|
|
|
|
|
|
interface Document {
|
|
|
|
|
id: string;
|
2025-06-30 19:42:43 +08:00
|
|
|
|
name: string;
|
|
|
|
|
originalName: string;
|
|
|
|
|
type: 'pdf' | 'word' | 'excel' | 'ppt' | 'image' | 'video' | 'audio' | 'text' | 'other';
|
|
|
|
|
category: 'contract' | 'translation' | 'template' | 'manual' | 'certificate' | 'report' | 'other';
|
|
|
|
|
size: number;
|
|
|
|
|
uploadedBy: string;
|
|
|
|
|
uploadedAt: string;
|
|
|
|
|
lastModified: string;
|
|
|
|
|
status: 'active' | 'archived' | 'deleted';
|
|
|
|
|
tags: string[];
|
|
|
|
|
description?: string;
|
|
|
|
|
version: string;
|
|
|
|
|
downloadCount: number;
|
|
|
|
|
isPublic: boolean;
|
|
|
|
|
language?: string;
|
|
|
|
|
relatedOrderId?: string;
|
|
|
|
|
thumbnailUrl?: string;
|
|
|
|
|
url: string;
|
2025-06-29 16:13:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
export default function Documents() {
|
2025-06-29 16:13:50 +08:00
|
|
|
|
const [documents, setDocuments] = useState<Document[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
2025-06-30 19:42:43 +08:00
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
|
const [filterType, setFilterType] = useState<'all' | 'pdf' | 'word' | 'excel' | 'ppt' | 'image' | 'video' | 'audio' | 'text' | 'other'>('all');
|
|
|
|
|
const [filterCategory, setFilterCategory] = useState<'all' | 'contract' | 'translation' | 'template' | 'manual' | 'certificate' | 'report' | 'other'>('all');
|
|
|
|
|
const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'archived' | 'deleted'>('all');
|
2025-06-29 16:13:50 +08:00
|
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
2025-06-30 19:42:43 +08:00
|
|
|
|
const [selectedDocuments, setSelectedDocuments] = useState<string[]>([]);
|
|
|
|
|
const [viewMode, setViewMode] = useState<'list' | 'grid'>('list');
|
|
|
|
|
const documentsPerPage = 12;
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadDocuments();
|
|
|
|
|
}, [searchTerm, filterType, filterCategory, filterStatus, currentPage]);
|
|
|
|
|
|
|
|
|
|
const loadDocuments = async () => {
|
2025-06-29 16:13:50 +08:00
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
// 模拟API调用
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const mockData: Document[] = [
|
|
|
|
|
{
|
|
|
|
|
id: '1',
|
|
|
|
|
name: '服务合同模板-2024版',
|
|
|
|
|
originalName: 'service_contract_template_2024.pdf',
|
|
|
|
|
type: 'pdf',
|
|
|
|
|
category: 'contract',
|
|
|
|
|
size: 2048576, // 2MB
|
|
|
|
|
uploadedBy: '管理员',
|
|
|
|
|
uploadedAt: '2024-01-15 10:30',
|
|
|
|
|
lastModified: '2024-01-20 14:45',
|
|
|
|
|
status: 'active',
|
|
|
|
|
tags: ['合同', '模板', '服务'],
|
|
|
|
|
description: '标准服务合同模板,适用于翻译服务业务',
|
|
|
|
|
version: '2.1',
|
|
|
|
|
downloadCount: 156,
|
|
|
|
|
isPublic: true,
|
|
|
|
|
language: '中文',
|
|
|
|
|
url: '/documents/service_contract_template_2024.pdf'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '2',
|
|
|
|
|
name: '翻译质量评估报告',
|
|
|
|
|
originalName: 'quality_assessment_report.docx',
|
|
|
|
|
type: 'word',
|
|
|
|
|
category: 'report',
|
|
|
|
|
size: 1536000, // 1.5MB
|
|
|
|
|
uploadedBy: '张经理',
|
|
|
|
|
uploadedAt: '2024-01-18 16:20',
|
|
|
|
|
lastModified: '2024-01-19 09:15',
|
|
|
|
|
status: 'active',
|
|
|
|
|
tags: ['质量', '评估', '报告'],
|
|
|
|
|
description: '2024年第一季度翻译质量评估报告',
|
|
|
|
|
version: '1.0',
|
|
|
|
|
downloadCount: 89,
|
|
|
|
|
isPublic: false,
|
|
|
|
|
language: '中文',
|
|
|
|
|
relatedOrderId: 'ORD-2024-001',
|
|
|
|
|
url: '/documents/quality_assessment_report.docx'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '3',
|
|
|
|
|
name: '翻译员认证证书',
|
|
|
|
|
originalName: 'translator_certificate.jpg',
|
|
|
|
|
type: 'image',
|
|
|
|
|
category: 'certificate',
|
|
|
|
|
size: 512000, // 512KB
|
|
|
|
|
uploadedBy: '李翻译',
|
|
|
|
|
uploadedAt: '2024-01-10 11:45',
|
|
|
|
|
lastModified: '2024-01-10 11:45',
|
|
|
|
|
status: 'active',
|
|
|
|
|
tags: ['证书', '认证', '翻译员'],
|
|
|
|
|
description: '专业翻译员资格认证证书',
|
|
|
|
|
version: '1.0',
|
|
|
|
|
downloadCount: 23,
|
|
|
|
|
isPublic: false,
|
|
|
|
|
thumbnailUrl: '/thumbnails/translator_certificate_thumb.jpg',
|
|
|
|
|
url: '/documents/translator_certificate.jpg'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '4',
|
|
|
|
|
name: '用户操作手册',
|
|
|
|
|
originalName: 'user_manual_v3.pdf',
|
|
|
|
|
type: 'pdf',
|
|
|
|
|
category: 'manual',
|
|
|
|
|
size: 3072000, // 3MB
|
|
|
|
|
uploadedBy: '产品经理',
|
|
|
|
|
uploadedAt: '2024-01-12 13:30',
|
|
|
|
|
lastModified: '2024-01-16 10:20',
|
|
|
|
|
status: 'active',
|
|
|
|
|
tags: ['手册', '用户指南', '操作'],
|
|
|
|
|
description: '系统用户操作手册第三版',
|
|
|
|
|
version: '3.0',
|
|
|
|
|
downloadCount: 234,
|
|
|
|
|
isPublic: true,
|
|
|
|
|
language: '中文',
|
|
|
|
|
url: '/documents/user_manual_v3.pdf'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '5',
|
|
|
|
|
name: '翻译项目演示视频',
|
|
|
|
|
originalName: 'project_demo.mp4',
|
|
|
|
|
type: 'video',
|
|
|
|
|
category: 'other',
|
|
|
|
|
size: 15728640, // 15MB
|
|
|
|
|
uploadedBy: '市场部',
|
|
|
|
|
uploadedAt: '2024-01-08 14:15',
|
|
|
|
|
lastModified: '2024-01-08 14:15',
|
|
|
|
|
status: 'active',
|
|
|
|
|
tags: ['演示', '视频', '项目'],
|
|
|
|
|
description: '翻译项目流程演示视频',
|
|
|
|
|
version: '1.0',
|
|
|
|
|
downloadCount: 67,
|
|
|
|
|
isPublic: true,
|
|
|
|
|
thumbnailUrl: '/thumbnails/project_demo_thumb.jpg',
|
|
|
|
|
url: '/documents/project_demo.mp4'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '6',
|
|
|
|
|
name: '财务报表模板',
|
|
|
|
|
originalName: 'financial_template.xlsx',
|
|
|
|
|
type: 'excel',
|
|
|
|
|
category: 'template',
|
|
|
|
|
size: 1024000, // 1MB
|
|
|
|
|
uploadedBy: '财务部',
|
|
|
|
|
uploadedAt: '2024-01-05 09:45',
|
|
|
|
|
lastModified: '2024-01-14 16:30',
|
|
|
|
|
status: 'archived',
|
|
|
|
|
tags: ['财务', '模板', '报表'],
|
|
|
|
|
description: '月度财务报表模板',
|
|
|
|
|
version: '2.5',
|
|
|
|
|
downloadCount: 45,
|
|
|
|
|
isPublic: false,
|
|
|
|
|
url: '/documents/financial_template.xlsx'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '7',
|
|
|
|
|
name: '产品介绍PPT',
|
|
|
|
|
originalName: 'product_introduction.pptx',
|
|
|
|
|
type: 'ppt',
|
|
|
|
|
category: 'other',
|
|
|
|
|
size: 5120000, // 5MB
|
|
|
|
|
uploadedBy: '销售部',
|
|
|
|
|
uploadedAt: '2024-01-03 15:20',
|
|
|
|
|
lastModified: '2024-01-11 11:10',
|
|
|
|
|
status: 'active',
|
|
|
|
|
tags: ['产品', '介绍', '演示'],
|
|
|
|
|
description: '公司产品介绍演示文稿',
|
|
|
|
|
version: '1.3',
|
|
|
|
|
downloadCount: 112,
|
|
|
|
|
isPublic: true,
|
|
|
|
|
language: '中文',
|
|
|
|
|
url: '/documents/product_introduction.pptx'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: '8',
|
|
|
|
|
name: '客户反馈音频',
|
|
|
|
|
originalName: 'customer_feedback.mp3',
|
|
|
|
|
type: 'audio',
|
|
|
|
|
category: 'other',
|
|
|
|
|
size: 2560000, // 2.5MB
|
|
|
|
|
uploadedBy: '客服部',
|
|
|
|
|
uploadedAt: '2024-01-01 10:00',
|
|
|
|
|
lastModified: '2024-01-01 10:00',
|
|
|
|
|
status: 'active',
|
|
|
|
|
tags: ['客户', '反馈', '音频'],
|
|
|
|
|
description: '客户服务质量反馈录音',
|
|
|
|
|
version: '1.0',
|
|
|
|
|
downloadCount: 34,
|
|
|
|
|
isPublic: false,
|
|
|
|
|
url: '/documents/customer_feedback.mp3'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 应用过滤器
|
|
|
|
|
let filteredData = mockData;
|
|
|
|
|
|
|
|
|
|
if (searchTerm) {
|
|
|
|
|
const term = searchTerm.toLowerCase();
|
|
|
|
|
filteredData = filteredData.filter(doc =>
|
|
|
|
|
doc.name.toLowerCase().includes(term) ||
|
|
|
|
|
doc.originalName.toLowerCase().includes(term) ||
|
|
|
|
|
doc.description?.toLowerCase().includes(term) ||
|
|
|
|
|
doc.tags.some(tag => tag.toLowerCase().includes(term))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filterType !== 'all') {
|
|
|
|
|
filteredData = filteredData.filter(doc => doc.type === filterType);
|
|
|
|
|
}
|
2025-06-29 16:13:50 +08:00
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
if (filterCategory !== 'all') {
|
|
|
|
|
filteredData = filteredData.filter(doc => doc.category === filterCategory);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filterStatus !== 'all') {
|
|
|
|
|
filteredData = filteredData.filter(doc => doc.status === filterStatus);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setDocuments(filteredData);
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}, 1000);
|
2025-06-29 16:13:50 +08:00
|
|
|
|
} catch (error) {
|
2025-06-30 19:42:43 +08:00
|
|
|
|
console.error('加载文档数据失败:', error);
|
2025-06-29 16:13:50 +08:00
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
const handleSelectDocument = (documentId: string) => {
|
|
|
|
|
setSelectedDocuments(prev =>
|
|
|
|
|
prev.includes(documentId)
|
|
|
|
|
? prev.filter(id => id !== documentId)
|
|
|
|
|
: [...prev, documentId]
|
|
|
|
|
);
|
2025-06-29 16:13:50 +08:00
|
|
|
|
};
|
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
const handleSelectAll = () => {
|
|
|
|
|
if (selectedDocuments.length === documents.length) {
|
|
|
|
|
setSelectedDocuments([]);
|
|
|
|
|
} else {
|
|
|
|
|
setSelectedDocuments(documents.map(doc => doc.id));
|
|
|
|
|
}
|
2025-06-29 16:13:50 +08:00
|
|
|
|
};
|
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
const getTypeIcon = (type: string) => {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'pdf':
|
|
|
|
|
case 'word':
|
|
|
|
|
case 'text':
|
|
|
|
|
return <DocumentTextIcon className="h-5 w-5" />;
|
|
|
|
|
case 'excel':
|
|
|
|
|
return <DocumentDuplicateIcon className="h-5 w-5" />;
|
|
|
|
|
case 'ppt':
|
|
|
|
|
return <DocumentDuplicateIcon className="h-5 w-5" />;
|
|
|
|
|
case 'image':
|
|
|
|
|
return <PhotoIcon className="h-5 w-5" />;
|
|
|
|
|
case 'video':
|
|
|
|
|
return <FilmIcon className="h-5 w-5" />;
|
|
|
|
|
case 'audio':
|
|
|
|
|
return <MusicalNoteIcon className="h-5 w-5" />;
|
|
|
|
|
default:
|
|
|
|
|
return <DocumentDuplicateIcon className="h-5 w-5" />;
|
|
|
|
|
}
|
2025-06-29 16:13:50 +08:00
|
|
|
|
};
|
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
const getTypeColor = (type: string) => {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'pdf': return 'text-red-600';
|
|
|
|
|
case 'word': return 'text-blue-600';
|
|
|
|
|
case 'excel': return 'text-green-600';
|
|
|
|
|
case 'ppt': return 'text-orange-600';
|
|
|
|
|
case 'image': return 'text-purple-600';
|
|
|
|
|
case 'video': return 'text-pink-600';
|
|
|
|
|
case 'audio': return 'text-indigo-600';
|
|
|
|
|
case 'text': return 'text-gray-600';
|
|
|
|
|
default: return 'text-gray-600';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getCategoryText = (category: string) => {
|
|
|
|
|
switch (category) {
|
|
|
|
|
case 'contract': return '合同';
|
|
|
|
|
case 'translation': return '翻译';
|
|
|
|
|
case 'template': return '模板';
|
|
|
|
|
case 'manual': return '手册';
|
|
|
|
|
case 'certificate': return '证书';
|
|
|
|
|
case 'report': return '报告';
|
|
|
|
|
case 'other': return '其他';
|
|
|
|
|
default: return category;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getCategoryColor = (category: string) => {
|
|
|
|
|
switch (category) {
|
|
|
|
|
case 'contract': return 'bg-red-100 text-red-800';
|
|
|
|
|
case 'translation': return 'bg-blue-100 text-blue-800';
|
|
|
|
|
case 'template': return 'bg-green-100 text-green-800';
|
|
|
|
|
case 'manual': return 'bg-yellow-100 text-yellow-800';
|
|
|
|
|
case 'certificate': return 'bg-purple-100 text-purple-800';
|
|
|
|
|
case 'report': return 'bg-indigo-100 text-indigo-800';
|
|
|
|
|
case 'other': return 'bg-gray-100 text-gray-800';
|
|
|
|
|
default: return 'bg-gray-100 text-gray-800';
|
2025-06-29 16:13:50 +08:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getStatusText = (status: string) => {
|
|
|
|
|
switch (status) {
|
2025-06-30 19:42:43 +08:00
|
|
|
|
case 'active': return '正常';
|
|
|
|
|
case 'archived': return '已归档';
|
|
|
|
|
case 'deleted': return '已删除';
|
|
|
|
|
default: return status;
|
2025-06-29 16:13:50 +08:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
const getStatusColor = (status: string) => {
|
2025-06-29 16:13:50 +08:00
|
|
|
|
switch (status) {
|
2025-06-30 19:42:43 +08:00
|
|
|
|
case 'active': return 'bg-green-100 text-green-800';
|
|
|
|
|
case 'archived': return 'bg-yellow-100 text-yellow-800';
|
|
|
|
|
case 'deleted': return 'bg-red-100 text-red-800';
|
|
|
|
|
default: return 'bg-gray-100 text-gray-800';
|
2025-06-29 16:13:50 +08:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatFileSize = (bytes: number) => {
|
2025-06-30 19:42:43 +08:00
|
|
|
|
if (bytes === 0) return '0 B';
|
2025-06-29 16:13:50 +08:00
|
|
|
|
const k = 1024;
|
2025-06-30 19:42:43 +08:00
|
|
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
2025-06-29 16:13:50 +08:00
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
const totalPages = Math.ceil(documents.length / documentsPerPage);
|
|
|
|
|
const currentDocuments = documents.slice(
|
|
|
|
|
(currentPage - 1) * documentsPerPage,
|
|
|
|
|
currentPage * documentsPerPage
|
|
|
|
|
);
|
2025-06-29 16:13:50 +08:00
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<DashboardLayout title="文档管理">
|
|
|
|
|
<div className="flex items-center justify-center h-64">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
|
|
|
|
|
<div className="mt-4 text-lg text-gray-600">加载文档数据中...</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</DashboardLayout>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-06-29 16:13:50 +08:00
|
|
|
|
|
|
|
|
|
return (
|
2025-06-30 19:42:43 +08:00
|
|
|
|
<DashboardLayout title="文档管理">
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* 页面标题 */}
|
|
|
|
|
<div className="sm:flex sm:items-center sm:justify-between">
|
|
|
|
|
<div>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
<h1 className="text-2xl font-bold text-gray-900">文档管理</h1>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
<p className="mt-1 text-sm text-gray-600">
|
|
|
|
|
管理系统文档、合同模板、翻译资料等各类文件,支持分类存储和权限控制。
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-4 sm:mt-0 flex space-x-3">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setViewMode(viewMode === 'list' ? 'grid' : 'list')}
|
|
|
|
|
className="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
|
|
|
>
|
|
|
|
|
{viewMode === 'list' ? '网格视图' : '列表视图'}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
|
|
|
>
|
|
|
|
|
<PlusIcon className="h-4 w-4 mr-2" />
|
2025-06-29 16:13:50 +08:00
|
|
|
|
上传文档
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
</div>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
{/* 统计卡片 */}
|
|
|
|
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
|
|
|
|
<div className="bg-white overflow-hidden shadow rounded-lg">
|
|
|
|
|
<div className="p-5">
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<div className="flex-shrink-0">
|
|
|
|
|
<DocumentDuplicateIcon className="h-6 w-6 text-gray-400" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="ml-5 w-0 flex-1">
|
|
|
|
|
<dl>
|
|
|
|
|
<dt className="text-sm font-medium text-gray-500 truncate">文档总数</dt>
|
|
|
|
|
<dd className="text-lg font-medium text-gray-900">{documents.length}</dd>
|
|
|
|
|
</dl>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
<div className="bg-white overflow-hidden shadow rounded-lg">
|
|
|
|
|
<div className="p-5">
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<div className="flex-shrink-0">
|
|
|
|
|
<CheckCircleIcon className="h-6 w-6 text-green-400" />
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
<div className="ml-5 w-0 flex-1">
|
|
|
|
|
<dl>
|
|
|
|
|
<dt className="text-sm font-medium text-gray-500 truncate">正常文档</dt>
|
|
|
|
|
<dd className="text-lg font-medium text-gray-900">
|
|
|
|
|
{documents.filter(d => d.status === 'active').length}
|
|
|
|
|
</dd>
|
|
|
|
|
</dl>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
<div className="bg-white overflow-hidden shadow rounded-lg">
|
|
|
|
|
<div className="p-5">
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<div className="flex-shrink-0">
|
|
|
|
|
<ArchiveBoxIcon className="h-6 w-6 text-yellow-400" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="ml-5 w-0 flex-1">
|
|
|
|
|
<dl>
|
|
|
|
|
<dt className="text-sm font-medium text-gray-500 truncate">已归档</dt>
|
|
|
|
|
<dd className="text-lg font-medium text-gray-900">
|
|
|
|
|
{documents.filter(d => d.status === 'archived').length}
|
|
|
|
|
</dd>
|
|
|
|
|
</dl>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
<div className="bg-white overflow-hidden shadow rounded-lg">
|
|
|
|
|
<div className="p-5">
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<div className="flex-shrink-0">
|
|
|
|
|
<ArrowDownTrayIcon className="h-6 w-6 text-blue-400" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="ml-5 w-0 flex-1">
|
|
|
|
|
<dl>
|
|
|
|
|
<dt className="text-sm font-medium text-gray-500 truncate">总下载次数</dt>
|
|
|
|
|
<dd className="text-lg font-medium text-gray-900">
|
|
|
|
|
{documents.reduce((sum, d) => sum + d.downloadCount, 0)}
|
|
|
|
|
</dd>
|
|
|
|
|
</dl>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
{/* 搜索和过滤 */}
|
|
|
|
|
<div className="bg-white shadow rounded-lg p-6">
|
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-4">
|
|
|
|
|
{/* 搜索框 */}
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
|
|
|
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" />
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="搜索文档名称、描述或标签..."
|
|
|
|
|
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
|
|
|
|
|
value={searchTerm}
|
|
|
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 文件类型过滤 */}
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<select
|
|
|
|
|
className="block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md"
|
|
|
|
|
value={filterType}
|
|
|
|
|
onChange={(e) => setFilterType(e.target.value as any)}
|
|
|
|
|
>
|
|
|
|
|
<option value="all">所有类型</option>
|
|
|
|
|
<option value="pdf">PDF</option>
|
|
|
|
|
<option value="word">Word</option>
|
|
|
|
|
<option value="excel">Excel</option>
|
|
|
|
|
<option value="ppt">PPT</option>
|
|
|
|
|
<option value="image">图片</option>
|
|
|
|
|
<option value="video">视频</option>
|
|
|
|
|
<option value="audio">音频</option>
|
|
|
|
|
<option value="text">文本</option>
|
|
|
|
|
<option value="other">其他</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 分类过滤 */}
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<select
|
|
|
|
|
className="block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md"
|
|
|
|
|
value={filterCategory}
|
|
|
|
|
onChange={(e) => setFilterCategory(e.target.value as any)}
|
|
|
|
|
>
|
|
|
|
|
<option value="all">所有分类</option>
|
|
|
|
|
<option value="contract">合同</option>
|
|
|
|
|
<option value="translation">翻译</option>
|
|
|
|
|
<option value="template">模板</option>
|
|
|
|
|
<option value="manual">手册</option>
|
|
|
|
|
<option value="certificate">证书</option>
|
|
|
|
|
<option value="report">报告</option>
|
|
|
|
|
<option value="other">其他</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 状态过滤 */}
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<select
|
|
|
|
|
className="block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md"
|
|
|
|
|
value={filterStatus}
|
|
|
|
|
onChange={(e) => setFilterStatus(e.target.value as any)}
|
|
|
|
|
>
|
|
|
|
|
<option value="all">所有状态</option>
|
|
|
|
|
<option value="active">正常</option>
|
|
|
|
|
<option value="archived">已归档</option>
|
|
|
|
|
<option value="deleted">已删除</option>
|
|
|
|
|
</select>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
{/* 批量操作 */}
|
|
|
|
|
{selectedDocuments.length > 0 && (
|
|
|
|
|
<div className="mt-4 flex items-center justify-between bg-gray-50 p-3 rounded-md">
|
|
|
|
|
<span className="text-sm text-gray-700">
|
|
|
|
|
已选择 {selectedDocuments.length} 个文档
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex space-x-2">
|
|
|
|
|
<button className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
|
|
|
|
批量下载
|
|
|
|
|
</button>
|
|
|
|
|
<button className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500">
|
|
|
|
|
批量归档
|
|
|
|
|
</button>
|
|
|
|
|
<button className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
|
|
|
|
|
批量删除
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
|
2025-06-30 19:42:43 +08:00
|
|
|
|
{/* 文档列表/网格 */}
|
|
|
|
|
{viewMode === 'list' ? (
|
|
|
|
|
<div className="bg-white shadow rounded-lg overflow-hidden">
|
|
|
|
|
<div className="overflow-x-auto">
|
|
|
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
|
|
|
<thead className="bg-gray-50">
|
|
|
|
|
<tr>
|
|
|
|
|
<th scope="col" className="relative px-6 py-3">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
className="absolute left-4 top-1/2 -mt-2 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
|
|
|
checked={documents.length > 0 && selectedDocuments.length === documents.length}
|
|
|
|
|
onChange={handleSelectAll}
|
|
|
|
|
/>
|
|
|
|
|
</th>
|
|
|
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
文档信息
|
|
|
|
|
</th>
|
|
|
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
分类
|
|
|
|
|
</th>
|
|
|
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
大小
|
|
|
|
|
</th>
|
|
|
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
上传信息
|
|
|
|
|
</th>
|
|
|
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
状态
|
|
|
|
|
</th>
|
|
|
|
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
下载次数
|
|
|
|
|
</th>
|
|
|
|
|
<th scope="col" className="relative px-6 py-3">
|
|
|
|
|
<span className="sr-only">操作</span>
|
|
|
|
|
</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
|
|
|
{currentDocuments.map((document) => (
|
|
|
|
|
<tr key={document.id} className="hover:bg-gray-50">
|
|
|
|
|
<td className="relative px-6 py-4">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
className="absolute left-4 top-1/2 -mt-2 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
|
|
|
checked={selectedDocuments.includes(document.id)}
|
|
|
|
|
onChange={() => handleSelectDocument(document.id)}
|
|
|
|
|
/>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<div className={`flex-shrink-0 ${getTypeColor(document.type)}`}>
|
|
|
|
|
{getTypeIcon(document.type)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="ml-4">
|
|
|
|
|
<div className="text-sm font-medium text-gray-900">{document.name}</div>
|
|
|
|
|
<div className="text-sm text-gray-500">{document.originalName}</div>
|
|
|
|
|
{document.description && (
|
|
|
|
|
<div className="text-xs text-gray-400 mt-1">{document.description}</div>
|
|
|
|
|
)}
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
|
|
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getCategoryColor(document.category)}`}>
|
|
|
|
|
{getCategoryText(document.category)}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
|
|
|
{formatFileSize(document.size)}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center text-sm text-gray-900">
|
|
|
|
|
<UserIcon className="h-4 w-4 mr-1" />
|
|
|
|
|
{document.uploadedBy}
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
<div className="flex items-center text-sm text-gray-500 mt-1">
|
|
|
|
|
<CalendarIcon className="h-4 w-4 mr-1" />
|
|
|
|
|
{document.uploadedAt}
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
|
|
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(document.status)}`}>
|
|
|
|
|
{getStatusText(document.status)}
|
|
|
|
|
</span>
|
|
|
|
|
{document.isPublic && (
|
|
|
|
|
<div className="text-xs text-blue-500 mt-1">公开</div>
|
|
|
|
|
)}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
|
|
|
{document.downloadCount}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<button className="text-indigo-600 hover:text-indigo-900" title="预览">
|
|
|
|
|
<EyeIcon className="h-4 w-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<button className="text-blue-600 hover:text-blue-900" title="下载">
|
|
|
|
|
<ArrowDownTrayIcon className="h-4 w-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<button className="text-green-600 hover:text-green-900" title="编辑">
|
|
|
|
|
<PencilIcon className="h-4 w-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<button className="text-red-600 hover:text-red-900" title="删除">
|
|
|
|
|
<TrashIcon className="h-4 w-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
))}
|
2025-06-30 19:42:43 +08:00
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="bg-white shadow rounded-lg p-6">
|
|
|
|
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
|
|
|
{currentDocuments.map((document) => (
|
|
|
|
|
<div key={document.id} className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow">
|
|
|
|
|
<div className="flex items-center justify-between mb-3">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
|
|
|
checked={selectedDocuments.includes(document.id)}
|
|
|
|
|
onChange={() => handleSelectDocument(document.id)}
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex space-x-1">
|
|
|
|
|
<button className="text-indigo-600 hover:text-indigo-900" title="预览">
|
|
|
|
|
<EyeIcon className="h-4 w-4" />
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</button>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
<button className="text-blue-600 hover:text-blue-900" title="下载">
|
|
|
|
|
<ArrowDownTrayIcon className="h-4 w-4" />
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="text-center mb-4">
|
|
|
|
|
{document.thumbnailUrl ? (
|
|
|
|
|
<img
|
|
|
|
|
src={document.thumbnailUrl}
|
|
|
|
|
alt={document.name}
|
|
|
|
|
className="w-16 h-16 mx-auto object-cover rounded"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className={`w-16 h-16 mx-auto flex items-center justify-center rounded bg-gray-100 ${getTypeColor(document.type)}`}>
|
|
|
|
|
{getTypeIcon(document.type)}
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<h3 className="text-sm font-medium text-gray-900 mb-1">{document.name}</h3>
|
|
|
|
|
<p className="text-xs text-gray-500 mb-2">{formatFileSize(document.size)}</p>
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-center mb-2">
|
|
|
|
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getCategoryColor(document.category)}`}>
|
|
|
|
|
{getCategoryText(document.category)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="text-xs text-gray-500">
|
|
|
|
|
<div>{document.uploadedBy}</div>
|
|
|
|
|
<div>{document.uploadedAt}</div>
|
|
|
|
|
<div>下载 {document.downloadCount} 次</div>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 分页 */}
|
|
|
|
|
{totalPages > 1 && (
|
|
|
|
|
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
|
|
|
|
<div className="flex-1 flex justify-between sm:hidden">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
|
|
|
|
disabled={currentPage === 1}
|
|
|
|
|
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
|
>
|
|
|
|
|
上一页
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
|
|
|
|
|
disabled={currentPage === totalPages}
|
|
|
|
|
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
|
>
|
|
|
|
|
下一页
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm text-gray-700">
|
|
|
|
|
显示第 <span className="font-medium">{(currentPage - 1) * documentsPerPage + 1}</span> 到{' '}
|
|
|
|
|
<span className="font-medium">{Math.min(currentPage * documentsPerPage, documents.length)}</span> 条,
|
|
|
|
|
共 <span className="font-medium">{documents.length}</span> 条记录
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
|
|
|
|
disabled={currentPage === 1}
|
|
|
|
|
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
|
>
|
|
|
|
|
<ChevronLeftIcon className="h-5 w-5" />
|
|
|
|
|
</button>
|
|
|
|
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
|
|
|
|
|
<button
|
|
|
|
|
key={page}
|
|
|
|
|
onClick={() => setCurrentPage(page)}
|
|
|
|
|
className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium ${
|
|
|
|
|
page === currentPage
|
|
|
|
|
? 'z-10 bg-indigo-50 border-indigo-500 text-indigo-600'
|
|
|
|
|
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{page}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
|
|
|
|
|
disabled={currentPage === totalPages}
|
|
|
|
|
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
|
>
|
|
|
|
|
<ChevronRightIcon className="h-5 w-5" />
|
|
|
|
|
</button>
|
|
|
|
|
</nav>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-06-29 16:13:50 +08:00
|
|
|
|
</div>
|
2025-06-30 19:42:43 +08:00
|
|
|
|
</DashboardLayout>
|
2025-06-29 16:13:50 +08:00
|
|
|
|
);
|
|
|
|
|
}
|