mars f20988b90c feat: 完成所有页面的演示模式实现
- 更新 DashboardLayout 组件,统一使用演示模式布局
- 实现仪表盘页面的完整演示数据和功能
- 完成用户管理页面的演示模式,包含搜索、过滤、分页等功能
- 实现通话记录页面的演示数据和录音播放功能
- 完成翻译员管理页面的演示模式
- 实现订单管理页面的完整功能
- 完成发票管理页面的演示数据
- 更新文档管理页面
- 添加 utils.ts 工具函数库
- 完善 API 路由和数据库结构
- 修复各种 TypeScript 类型错误
- 统一界面风格和用户体验
2025-06-30 19:42:43 +08:00

813 lines
34 KiB
TypeScript
Raw Permalink 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 DashboardLayout from '../../components/Layout/DashboardLayout';
import {
DocumentDuplicateIcon,
MagnifyingGlassIcon,
PlusIcon,
EyeIcon,
PencilIcon,
TrashIcon,
ArrowDownTrayIcon,
FolderIcon,
DocumentTextIcon,
PhotoIcon,
FilmIcon,
MusicalNoteIcon,
ArchiveBoxIcon,
ChevronLeftIcon,
ChevronRightIcon,
CalendarIcon,
UserIcon,
TagIcon,
StarIcon,
ClockIcon,
CheckCircleIcon,
XCircleIcon
} from '@heroicons/react/24/outline';
interface Document {
id: string;
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;
}
export default function Documents() {
const [documents, setDocuments] = useState<Document[]>([]);
const [loading, setLoading] = useState(true);
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');
const [currentPage, setCurrentPage] = useState(1);
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 () => {
try {
setLoading(true);
// 模拟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);
}
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);
} catch (error) {
console.error('加载文档数据失败:', error);
setLoading(false);
}
};
const handleSelectDocument = (documentId: string) => {
setSelectedDocuments(prev =>
prev.includes(documentId)
? prev.filter(id => id !== documentId)
: [...prev, documentId]
);
};
const handleSelectAll = () => {
if (selectedDocuments.length === documents.length) {
setSelectedDocuments([]);
} else {
setSelectedDocuments(documents.map(doc => doc.id));
}
};
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" />;
}
};
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';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'active': return '正常';
case 'archived': return '已归档';
case 'deleted': return '已删除';
default: return status;
}
};
const getStatusColor = (status: string) => {
switch (status) {
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';
}
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const totalPages = Math.ceil(documents.length / documentsPerPage);
const currentDocuments = documents.slice(
(currentPage - 1) * documentsPerPage,
currentPage * documentsPerPage
);
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>
);
}
return (
<DashboardLayout title="文档管理">
<div className="space-y-6">
{/* 页面标题 */}
<div className="sm:flex sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
<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" />
</button>
</div>
</div>
{/* 统计卡片 */}
<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>
</div>
</div>
</div>
</div>
<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" />
</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 === 'active').length}
</dd>
</dl>
</div>
</div>
</div>
</div>
<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>
</div>
</div>
</div>
</div>
<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>
</div>
</div>
</div>
</div>
</div>
{/* 搜索和过滤 */}
<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" />
</div>
<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>
</div>
</div>
{/* 批量操作 */}
{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>
</div>
)}
</div>
{/* 文档列表/网格 */}
{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>
)}
</div>
</div>
</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}
</div>
<div className="flex items-center text-sm text-gray-500 mt-1">
<CalendarIcon className="h-4 w-4 mr-1" />
{document.uploadedAt}
</div>
</div>
</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>
))}
</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" />
</button>
<button className="text-blue-600 hover:text-blue-900" title="下载">
<ArrowDownTrayIcon className="h-4 w-4" />
</button>
</div>
</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)}
</div>
)}
</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>
</div>
</div>
</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>
</div>
</div>
</div>
)}
</div>
</DashboardLayout>
);
}