483 lines
18 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,
EyeIcon,
ClockIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon,
ChevronLeftIcon,
ChevronRightIcon,
PhoneIcon,
VideoCameraIcon,
DocumentTextIcon,
HandRaisedIcon,
SpeakerWaveIcon
} from '@heroicons/react/24/outline';
import { supabase, TABLES } from '@/lib/supabase';
import { getDemoData } from '@/lib/demo-data';
import { formatTime, formatCurrency } from '@/utils';
import Layout from '@/components/Layout';
interface Order {
id: string;
order_number: string;
user_id: string;
user_name: string;
user_email: string;
service_type: 'ai_voice_translation' | 'ai_video_translation' | 'sign_language_translation' | 'human_interpretation' | 'document_translation';
service_name: string;
source_language: string;
target_language: string;
duration?: number; // 分钟
pages?: number; // 页数
status: 'pending' | 'processing' | 'completed' | 'cancelled' | 'failed';
priority: 'low' | 'normal' | 'high' | 'urgent';
cost: number;
currency: string;
// 时间信息
scheduled_at?: string;
started_at?: string;
completed_at?: string;
created_at: string;
updated_at: string;
// 额外信息
notes?: string;
interpreter_id?: string;
interpreter_name?: string;
}
interface OrderFilters {
search: string;
status: 'all' | 'pending' | 'processing' | 'completed' | 'cancelled' | 'failed';
service_type: 'all' | 'ai_voice_translation' | 'ai_video_translation' | 'sign_language_translation' | 'human_interpretation' | 'document_translation';
priority: 'all' | 'low' | 'normal' | 'high' | 'urgent';
date_range: 'all' | 'today' | 'week' | 'month';
sortBy: 'created_at' | 'cost' | 'scheduled_at';
sortOrder: 'asc' | 'desc';
}
export default function OrdersPage() {
const [orders, setOrders] = useState<Order[]>([]);
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<OrderFilters>({
search: '',
status: 'all',
service_type: 'all',
priority: 'all',
date_range: 'all',
sortBy: 'created_at',
sortOrder: 'desc'
});
const router = useRouter();
const pageSize = 20;
// 获取订单数据
const fetchOrders = 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.orders();
setOrders(result);
setTotalCount(result.length);
setTotalPages(Math.ceil(result.length / pageSize));
setCurrentPage(page);
} else {
// 使用真实数据 - 这里需要根据实际数据库结构调整
// 暂时使用演示数据
const result = await getDemoData.orders();
setOrders(result);
setTotalCount(result.length);
setTotalPages(Math.ceil(result.length / pageSize));
setCurrentPage(page);
}
} catch (error) {
console.error('Error fetching orders:', error);
toast.error('获取订单数据失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchOrders(1);
}, []);
// 处理筛选变更
const handleFilterChange = (key: keyof OrderFilters, value: any) => {
setFilters(prev => ({
...prev,
[key]: value
}));
};
// 应用筛选
const applyFilters = () => {
setCurrentPage(1);
fetchOrders(1);
};
// 重置筛选
const resetFilters = () => {
setFilters({
search: '',
status: 'all',
service_type: 'all',
priority: 'all',
date_range: 'all',
sortBy: 'created_at',
sortOrder: 'desc'
});
setCurrentPage(1);
fetchOrders(1);
};
// 获取状态颜色
const getStatusColor = (status: string) => {
switch (status) {
case 'pending':
return 'bg-yellow-100 text-yellow-800';
case 'processing':
return 'bg-blue-100 text-blue-800';
case 'completed':
return 'bg-green-100 text-green-800';
case 'cancelled':
return 'bg-gray-100 text-gray-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 'pending':
return '待处理';
case 'processing':
return '处理中';
case 'completed':
return '已完成';
case 'cancelled':
return '已取消';
case 'failed':
return '失败';
default:
return status;
}
};
// 获取优先级颜色
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'urgent':
return 'bg-red-100 text-red-800';
case 'high':
return 'bg-orange-100 text-orange-800';
case 'normal':
return 'bg-blue-100 text-blue-800';
case 'low':
return 'bg-gray-100 text-gray-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
// 获取优先级文本
const getPriorityText = (priority: string) => {
switch (priority) {
case 'urgent':
return '紧急';
case 'high':
return '高';
case 'normal':
return '普通';
case 'low':
return '低';
default:
return priority;
}
};
// 获取服务类型图标
const getServiceIcon = (serviceType: string) => {
switch (serviceType) {
case 'ai_voice_translation':
return <SpeakerWaveIcon className="h-4 w-4" />;
case 'ai_video_translation':
return <VideoCameraIcon className="h-4 w-4" />;
case 'sign_language_translation':
return <HandRaisedIcon className="h-4 w-4" />;
case 'human_interpretation':
return <PhoneIcon className="h-4 w-4" />;
case 'document_translation':
return <DocumentTextIcon className="h-4 w-4" />;
default:
return <DocumentTextIcon className="h-4 w-4" />;
}
};
return (
<Layout>
<Head>
<title> - </title>
</Head>
<div className="px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<h1 className="text-2xl font-semibold text-gray-900"></h1>
<p className="mt-2 text-sm text-gray-700">
</p>
</div>
</div>
{/* 搜索和筛选 */}
<div className="mt-8 bg-white shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-6">
<div className="relative">
<MagnifyingGlassIcon className="h-5 w-5 absolute left-3 top-3 text-gray-400" />
<input
type="text"
placeholder="搜索订单号或用户..."
className="pl-10 w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
value={filters.search}
onChange={(e) => handleFilterChange('search', e.target.value)}
/>
</div>
<select
className="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
value={filters.status}
onChange={(e) => handleFilterChange('status', e.target.value)}
>
<option value="all"></option>
<option value="pending"></option>
<option value="processing"></option>
<option value="completed"></option>
<option value="cancelled"></option>
<option value="failed"></option>
</select>
<select
className="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
value={filters.service_type}
onChange={(e) => handleFilterChange('service_type', e.target.value)}
>
<option value="all"></option>
<option value="ai_voice_translation">AI语音翻译</option>
<option value="ai_video_translation">AI视频翻译</option>
<option value="sign_language_translation"></option>
<option value="human_interpretation"></option>
<option value="document_translation"></option>
</select>
<select
className="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
value={filters.priority}
onChange={(e) => handleFilterChange('priority', e.target.value)}
>
<option value="all"></option>
<option value="urgent"></option>
<option value="high"></option>
<option value="normal"></option>
<option value="low"></option>
</select>
<select
className="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
value={filters.date_range}
onChange={(e) => handleFilterChange('date_range', e.target.value)}
>
<option value="all"></option>
<option value="today"></option>
<option value="week"></option>
<option value="month"></option>
</select>
<div className="flex space-x-2">
<button
onClick={applyFilters}
className="flex-1 bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
</button>
<button
onClick={resetFilters}
className="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500"
>
</button>
</div>
</div>
</div>
</div>
{/* 订单列表 */}
<div className="mt-8 bg-white shadow rounded-lg overflow-hidden">
{loading ? (
<div className="p-8 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600 mx-auto"></div>
<p className="mt-2 text-sm text-gray-500">...</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide">
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wide">
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{orders.map((order) => (
<tr key={order.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900">
{order.order_number}
</div>
<div className="text-sm text-gray-500 flex items-center">
{getServiceIcon(order.service_type)}
<span className="ml-1">{order.service_name}</span>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900">
{order.user_name}
</div>
<div className="text-sm text-gray-500">{order.user_email}</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm text-gray-900">
{order.source_language} {order.target_language}
</div>
<div className="text-sm text-gray-500">
{order.duration && `时长: ${order.duration}分钟`}
{order.pages && `页数: ${order.pages}`}
</div>
{order.interpreter_name && (
<div className="text-xs text-gray-400">
: {order.interpreter_name}
</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(order.status)}`}>
{getStatusText(order.status)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getPriorityColor(order.priority)}`}>
{getPriorityText(order.priority)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{formatCurrency(order.cost)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div>
<div>: {formatTime(order.created_at)}</div>
{order.scheduled_at && (
<div>: {formatTime(order.scheduled_at)}</div>
)}
{order.completed_at && (
<div>: {formatTime(order.completed_at)}</div>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button
className="text-indigo-600 hover:text-indigo-900"
title="查看详情"
>
<EyeIcon className="h-4 w-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* 分页 */}
{totalPages > 1 && (
<div className="mt-6 flex items-center justify-between">
<div className="text-sm text-gray-700">
{(currentPage - 1) * pageSize + 1} - {Math.min(currentPage * pageSize, totalCount)} {totalCount}
</div>
<div className="flex space-x-2">
<button
onClick={() => fetchOrders(currentPage - 1)}
disabled={currentPage === 1}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronLeftIcon className="h-4 w-4" />
</button>
<span className="px-3 py-2 text-sm font-medium text-gray-700">
{currentPage} / {totalPages}
</span>
<button
onClick={() => fetchOrders(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronRightIcon className="h-4 w-4" />
</button>
</div>
</div>
)}
</div>
</Layout>
);
}