496 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Head from 'next/head';
import { toast } from 'react-hot-toast';
import {
MagnifyingGlassIcon,
DocumentTextIcon,
CloudArrowUpIcon,
CloudArrowDownIcon,
EyeIcon,
TrashIcon,
PencilIcon,
CheckCircleIcon,
ClockIcon,
ExclamationCircleIcon,
ChevronLeftIcon,
ChevronRightIcon
} from '@heroicons/react/24/outline';
import { supabase, TABLES } from '@/lib/supabase';
import { getDemoData } from '@/lib/demo-data';
import { formatTime } from '@/utils';
import Layout from '@/components/Layout';
interface Document {
id: string;
user_id: string;
original_name: string;
file_size: number;
file_type: string;
source_language: string;
target_language: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
progress: number;
translated_url?: string;
cost: number;
created_at: string;
updated_at: string;
user_name?: string;
}
interface DocumentFilters {
search: string;
status: 'all' | 'pending' | 'processing' | 'completed' | 'failed';
language: string;
fileType: string;
sortBy: 'created_at' | 'file_size' | 'cost' | 'progress';
sortOrder: 'asc' | 'desc';
}
export default function DocumentsPage() {
const [documents, setDocuments] = useState<Document[]>([]);
const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalCount, setTotalCount] = useState(0);
const [isDemoMode, setIsDemoMode] = useState(false);
const [filters, setFilters] = useState<DocumentFilters>({
search: '',
status: 'all',
language: 'all',
fileType: 'all',
sortBy: 'created_at',
sortOrder: 'desc'
});
const router = useRouter();
const pageSize = 20;
// 获取文档数据
const fetchDocuments = async (page = 1) => {
try {
setLoading(true);
// 检查是否为演示模式
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const isDemo = !supabaseUrl || supabaseUrl === 'https://demo.supabase.co' || supabaseUrl === '';
setIsDemoMode(isDemo);
if (isDemo) {
// 使用演示数据
const result = await getDemoData.documents();
setDocuments(result);
setTotalCount(result.length);
setTotalPages(Math.ceil(result.length / pageSize));
setCurrentPage(page);
} else {
// 使用真实数据 - 这里需要根据实际数据库结构调整
// 暂时使用演示数据
const result = await getDemoData.documents();
setDocuments(result);
setTotalCount(result.length);
setTotalPages(Math.ceil(result.length / pageSize));
setCurrentPage(page);
}
} catch (error) {
console.error('Error fetching documents:', error);
toast.error('获取文档列表失败');
} finally {
setLoading(false);
}
};
// 处理筛选变更
const handleFilterChange = (key: keyof DocumentFilters, value: any) => {
setFilters(prev => ({
...prev,
[key]: value
}));
};
// 应用筛选
const applyFilters = () => {
setCurrentPage(1);
fetchDocuments(1);
};
// 重置筛选
const resetFilters = () => {
setFilters({
search: '',
status: 'all',
language: 'all',
fileType: 'all',
sortBy: 'created_at',
sortOrder: 'desc'
});
setCurrentPage(1);
fetchDocuments(1);
};
// 获取状态颜色
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
return 'bg-green-100 text-green-800';
case 'processing':
return 'bg-blue-100 text-blue-800';
case 'pending':
return 'bg-yellow-100 text-yellow-800';
case 'failed':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
// 获取状态文本
const getStatusText = (status: string) => {
switch (status) {
case 'completed':
return '已完成';
case 'processing':
return '处理中';
case 'pending':
return '等待中';
case 'failed':
return '失败';
default:
return '未知';
}
};
// 获取状态图标
const getStatusIcon = (status: string) => {
switch (status) {
case 'completed':
return <CheckCircleIcon className="h-4 w-4 text-green-600" />;
case 'processing':
return <ClockIcon className="h-4 w-4 text-blue-600" />;
case 'pending':
return <ClockIcon className="h-4 w-4 text-yellow-600" />;
case 'failed':
return <ExclamationCircleIcon className="h-4 w-4 text-red-600" />;
default:
return <ClockIcon className="h-4 w-4 text-gray-600" />;
}
};
// 格式化文件大小
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', '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 handleDeleteDocument = async (documentId: string) => {
if (confirm('确定要删除此文档吗?')) {
try {
// 这里应该调用删除API
toast.success('文档删除成功');
fetchDocuments();
} catch (error) {
toast.error('删除文档失败');
}
}
};
// 下载文档
const handleDownloadDocument = async (document: Document) => {
try {
// 这里应该调用下载API
toast.success('开始下载文档');
} catch (error) {
toast.error('下载文档失败');
}
};
useEffect(() => {
fetchDocuments();
}, []);
return (
<Layout>
<Head>
<title> - </title>
</Head>
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
{/* 页面标题 */}
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-gray-900"></h1>
<button className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700">
<CloudArrowUpIcon className="h-4 w-4 mr-2" />
</button>
</div>
{/* 搜索和筛选 */}
<div className="bg-white shadow rounded-lg mb-6">
<div className="px-4 py-5 sm:p-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
{/* 搜索框 */}
<div className="lg:col-span-2">
<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="搜索文档名称或用户..."
value={filters.search}
onChange={(e) => handleFilterChange('search', e.target.value)}
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-blue-500 focus:border-blue-500"
/>
</div>
</div>
{/* 状态筛选 */}
<div>
<select
value={filters.status}
onChange={(e) => handleFilterChange('status', e.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<option value="all"></option>
<option value="pending"></option>
<option value="processing"></option>
<option value="completed"></option>
<option value="failed"></option>
</select>
</div>
{/* 语言筛选 */}
<div>
<select
value={filters.language}
onChange={(e) => handleFilterChange('language', e.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<option value="all"></option>
<option value="zh-en"></option>
<option value="en-zh"></option>
<option value="zh-ja"></option>
<option value="ja-zh"></option>
<option value="zh-ko"></option>
<option value="ko-zh"></option>
</select>
</div>
{/* 文件类型筛选 */}
<div>
<select
value={filters.fileType}
onChange={(e) => handleFilterChange('fileType', e.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
>
<option value="all"></option>
<option value="pdf">PDF</option>
<option value="docx">Word</option>
<option value="txt"></option>
<option value="pptx">PowerPoint</option>
<option value="xlsx">Excel</option>
</select>
</div>
</div>
<div className="mt-4 flex space-x-3">
<button
onClick={applyFilters}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
</button>
<button
onClick={resetFilters}
className="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"
>
</button>
</div>
</div>
</div>
{/* 文档列表 */}
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="loading-spinner"></div>
</div>
) : (
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<div className="px-4 py-5 sm:p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg leading-6 font-medium text-gray-900">
({totalCount} )
</h3>
</div>
<div className="space-y-4">
{documents.map((document) => (
<div key={document.id} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-3">
<DocumentTextIcon className="h-8 w-8 text-gray-400" />
<div>
<h4 className="text-sm font-medium text-gray-900 truncate">
{document.original_name}
</h4>
<p className="text-sm text-gray-500">
{document.user_name} | {formatFileSize(document.file_size)} | {document.file_type.toUpperCase()}
</p>
<p className="text-xs text-gray-400">
{document.source_language} {document.target_language} | ¥{document.cost.toFixed(2)}
</p>
</div>
</div>
</div>
<div className="flex items-center space-x-4">
{/* 进度条 */}
{document.status === 'processing' && (
<div className="w-24">
<div className="bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${document.progress}%` }}
></div>
</div>
<p className="text-xs text-gray-500 mt-1">{document.progress}%</p>
</div>
)}
{/* 状态 */}
<div className="flex items-center space-x-2">
{getStatusIcon(document.status)}
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(document.status)}`}>
{getStatusText(document.status)}
</span>
</div>
{/* 操作按钮 */}
<div className="flex space-x-2">
<button
className="text-blue-600 hover:text-blue-900"
title="查看详情"
>
<EyeIcon className="h-4 w-4" />
</button>
{document.status === 'completed' && document.translated_url && (
<button
onClick={() => handleDownloadDocument(document)}
className="text-green-600 hover:text-green-900"
title="下载译文"
>
<CloudArrowDownIcon className="h-4 w-4" />
</button>
)}
<button className="text-yellow-600 hover:text-yellow-900">
<PencilIcon className="h-4 w-4" />
</button>
<button
onClick={() => handleDeleteDocument(document.id)}
className="text-red-600 hover:text-red-900"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
</div>
</div>
{/* 时间信息 */}
<div className="mt-3 text-xs text-gray-400">
: {formatTime(document.created_at)} |
: {formatTime(document.updated_at)}
</div>
</div>
))}
{documents.length === 0 && (
<div className="text-center py-12">
<DocumentTextIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900"></h3>
<p className="mt-1 text-sm text-gray-500"></p>
</div>
)}
</div>
{/* 分页 */}
{totalPages > 1 && (
<div className="mt-6 flex items-center justify-between">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => fetchDocuments(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"
>
</button>
<button
onClick={() => fetchDocuments(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"
>
</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) * pageSize + 1}</span> {' '}
<span className="font-medium">
{Math.min(currentPage * pageSize, totalCount)}
</span>{' '}
<span className="font-medium">{totalCount}</span>
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
<button
onClick={() => fetchDocuments(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"
>
<ChevronLeftIcon className="h-5 w-5" />
</button>
{[...Array(Math.min(totalPages, 5))].map((_, i) => {
const page = i + 1;
return (
<button
key={page}
onClick={() => fetchDocuments(page)}
className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium ${
page === currentPage
? 'z-10 bg-blue-50 border-blue-500 text-blue-600'
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
}`}
>
{page}
</button>
);
})}
<button
onClick={() => fetchDocuments(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"
>
<ChevronRightIcon className="h-5 w-5" />
</button>
</nav>
</div>
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
</Layout>
);
}