- 更新 DashboardLayout 组件,统一使用演示模式布局 - 实现仪表盘页面的完整演示数据和功能 - 完成用户管理页面的演示模式,包含搜索、过滤、分页等功能 - 实现通话记录页面的演示数据和录音播放功能 - 完成翻译员管理页面的演示模式 - 实现订单管理页面的完整功能 - 完成发票管理页面的演示数据 - 更新文档管理页面 - 添加 utils.ts 工具函数库 - 完善 API 路由和数据库结构 - 修复各种 TypeScript 类型错误 - 统一界面风格和用户体验
813 lines
34 KiB
TypeScript
813 lines
34 KiB
TypeScript
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>
|
||
);
|
||
}
|