Twilioapp-admin/pages/dashboard/interpreters.tsx
mars 1ba859196a 修复退出登录重定向问题和相关功能优化
- 修复DashboardLayout中的退出登录函数,确保清除所有认证信息
- 恢复_app.tsx中的认证逻辑,确保仪表盘页面需要登录访问
- 完善退出登录流程:清除本地存储 -> 调用登出API -> 重定向到登录页面
- 添加错误边界组件提升用户体验
- 优化React水合错误处理
- 添加JWT令牌验证API
- 完善各个仪表盘页面的功能和样式
2025-07-03 20:56:17 +08:00

1063 lines
44 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 { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Head from 'next/head';
import DashboardLayout from '../../components/Layout/DashboardLayout';
import { toast } from 'react-hot-toast';
import {
UserIcon,
MagnifyingGlassIcon,
PlusIcon,
EyeIcon,
PencilIcon,
TrashIcon,
StarIcon,
LanguageIcon,
ClockIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon,
ArrowDownTrayIcon,
UserPlusIcon,
PhoneIcon,
EnvelopeIcon,
MapPinIcon,
CalendarIcon,
CurrencyDollarIcon,
AcademicCapIcon,
XMarkIcon
} from '@heroicons/react/24/outline';
import { getDemoData } from '../../lib/demo-data';
import { formatTime } from '../../lib/utils';
interface Interpreter {
id: string;
name: string;
email: string;
phone: string;
avatar?: string;
languages: string[];
specialties: string[];
experience_years: number;
rating: number;
total_calls: number;
total_hours: number;
hourly_rate: number;
status: 'active' | 'inactive' | 'busy' | 'offline';
location: string;
certifications: string[];
bio: string;
joined_at: string;
last_active: string;
availability: 'available' | 'busy' | 'offline';
}
interface InterpreterFilters {
search: string;
status: string;
language: string;
rating: string;
availability: string;
}
export default function Interpreters() {
const router = useRouter();
const [interpreters, setInterpreters] = useState<Interpreter[]>([]);
const [loading, setLoading] = useState(true);
const [selectedInterpreters, setSelectedInterpreters] = useState<string[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalCount, setTotalCount] = useState(0);
const [filters, setFilters] = useState<InterpreterFilters>({
search: '',
status: '',
language: '',
rating: '',
availability: ''
});
// 添加模态框状态
const [showAddInterpreterModal, setShowAddInterpreterModal] = useState(false);
const [newInterpreter, setNewInterpreter] = useState({
name: '',
email: '',
phone: '',
languages: '',
specialties: '',
experience_years: 0,
hourly_rate: 0,
location: '',
bio: '',
status: 'active' as 'active' | 'inactive' | 'busy' | 'offline',
availability: 'available' as 'available' | 'busy' | 'offline'
});
const [isSubmitting, setIsSubmitting] = useState(false);
const pageSize = 10;
useEffect(() => {
fetchInterpreters();
}, [currentPage, filters]);
const fetchInterpreters = async () => {
try {
setLoading(true);
// 模拟加载延迟
await new Promise(resolve => setTimeout(resolve, 800));
// 使用演示数据
const mockInterpreters: Interpreter[] = [
{
id: '1',
name: '王翻译',
email: 'wang@example.com',
phone: '+86 138-0000-0001',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
languages: ['中文', '英文', '日文'],
specialties: ['商务翻译', '法律翻译', '技术翻译'],
experience_years: 8,
rating: 4.9,
total_calls: 1250,
total_hours: 3200,
hourly_rate: 150,
status: 'active',
location: '北京',
certifications: ['CATTI二级', '商务英语高级', 'JLPT N1'],
bio: '资深翻译员,专注于商务和法律翻译,拥有丰富的国际会议翻译经验。',
joined_at: '2020-03-15T10:00:00Z',
last_active: '2024-01-20T15:30:00Z',
availability: 'available'
},
{
id: '2',
name: '李专家',
email: 'li@example.com',
phone: '+86 138-0000-0002',
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
languages: ['中文', '韩文', '英文'],
specialties: ['医学翻译', '学术翻译'],
experience_years: 12,
rating: 4.8,
total_calls: 980,
total_hours: 2800,
hourly_rate: 180,
status: 'active',
location: '上海',
certifications: ['医学翻译资格证', 'TOPIK 6级'],
bio: '医学翻译专家,在医疗器械和药物研发领域有深厚的专业背景。',
joined_at: '2019-08-20T10:00:00Z',
last_active: '2024-01-20T14:15:00Z',
availability: 'busy'
},
{
id: '3',
name: '张语言',
email: 'zhang@example.com',
phone: '+86 138-0000-0003',
avatar: 'https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
languages: ['中文', '德文', '法文'],
specialties: ['文学翻译', '艺术翻译'],
experience_years: 6,
rating: 4.7,
total_calls: 650,
total_hours: 1800,
hourly_rate: 120,
status: 'active',
location: '广州',
certifications: ['德语C2证书', '法语DALF C1'],
bio: '擅长文学和艺术类翻译,对欧洲文化有深入了解。',
joined_at: '2021-05-10T10:00:00Z',
last_active: '2024-01-19T16:45:00Z',
availability: 'available'
},
{
id: '4',
name: '陈口译',
email: 'chen@example.com',
phone: '+86 138-0000-0004',
languages: ['中文', '西班牙文', '葡萄牙文'],
specialties: ['旅游翻译', '贸易翻译'],
experience_years: 4,
rating: 4.5,
total_calls: 420,
total_hours: 1200,
hourly_rate: 100,
status: 'inactive',
location: '深圳',
certifications: ['DELE B2', '葡语中级证书'],
bio: '专注于拉美地区的商务和旅游翻译服务。',
joined_at: '2022-01-15T10:00:00Z',
last_active: '2024-01-18T09:30:00Z',
availability: 'offline'
},
{
id: '5',
name: '刘同传',
email: 'liu@example.com',
phone: '+86 138-0000-0005',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
languages: ['中文', '英文', '俄文'],
specialties: ['同声传译', '会议翻译'],
experience_years: 15,
rating: 5.0,
total_calls: 1800,
total_hours: 4500,
hourly_rate: 250,
status: 'active',
location: '北京',
certifications: ['同声传译资格证', '俄语专业八级'],
bio: '顶级同声传译员,曾为多个国际会议提供翻译服务。',
joined_at: '2018-12-01T10:00:00Z',
last_active: '2024-01-20T16:00:00Z',
availability: 'available'
},
{
id: '6',
name: '赵技术',
email: 'zhao@example.com',
phone: '+86 138-0000-0006',
languages: ['中文', '英文'],
specialties: ['IT翻译', '软件本地化'],
experience_years: 5,
rating: 4.6,
total_calls: 320,
total_hours: 900,
hourly_rate: 130,
status: 'active',
location: '杭州',
certifications: ['计算机技术翻译证书'],
bio: '专业IT翻译熟悉各种编程语言和技术文档翻译。',
joined_at: '2021-09-01T10:00:00Z',
last_active: '2024-01-20T11:20:00Z',
availability: 'busy'
}
];
// 应用过滤器
let filteredInterpreters = mockInterpreters;
if (filters.search) {
filteredInterpreters = filteredInterpreters.filter(interpreter =>
interpreter.name.toLowerCase().includes(filters.search.toLowerCase()) ||
interpreter.email.toLowerCase().includes(filters.search.toLowerCase()) ||
interpreter.languages.some(lang => lang.toLowerCase().includes(filters.search.toLowerCase())) ||
interpreter.specialties.some(spec => spec.toLowerCase().includes(filters.search.toLowerCase()))
);
}
if (filters.status) {
filteredInterpreters = filteredInterpreters.filter(interpreter => interpreter.status === filters.status);
}
if (filters.language) {
filteredInterpreters = filteredInterpreters.filter(interpreter =>
interpreter.languages.some(lang => lang.toLowerCase().includes(filters.language.toLowerCase()))
);
}
if (filters.rating) {
const minRating = parseFloat(filters.rating);
filteredInterpreters = filteredInterpreters.filter(interpreter => interpreter.rating >= minRating);
}
if (filters.availability) {
filteredInterpreters = filteredInterpreters.filter(interpreter => interpreter.availability === filters.availability);
}
// 分页
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedInterpreters = filteredInterpreters.slice(startIndex, endIndex);
setInterpreters(paginatedInterpreters);
setTotalCount(filteredInterpreters.length);
setTotalPages(Math.ceil(filteredInterpreters.length / pageSize));
} catch (error) {
console.error('Failed to fetch interpreters:', error);
toast.error('加载翻译员失败');
} finally {
setLoading(false);
}
};
const handleSearch = (value: string) => {
setFilters(prev => ({ ...prev, search: value }));
setCurrentPage(1);
};
const handleFilterChange = (key: keyof InterpreterFilters, value: string) => {
setFilters(prev => ({ ...prev, [key]: value }));
setCurrentPage(1);
};
const handleSelectInterpreter = (interpreterId: string) => {
setSelectedInterpreters(prev =>
prev.includes(interpreterId)
? prev.filter(id => id !== interpreterId)
: [...prev, interpreterId]
);
};
const handleSelectAll = () => {
if (selectedInterpreters.length === interpreters.length) {
setSelectedInterpreters([]);
} else {
setSelectedInterpreters(interpreters.map(interpreter => interpreter.id));
}
};
const handleBulkAction = async (action: 'activate' | 'deactivate' | 'delete') => {
if (selectedInterpreters.length === 0) {
toast.error('请选择要操作的翻译员');
return;
}
try {
const actionText = action === 'activate' ? '激活' : action === 'deactivate' ? '停用' : '删除';
toast.loading(`正在${actionText}翻译员...`, { id: 'bulk-action' });
// 模拟操作延迟
await new Promise(resolve => setTimeout(resolve, 1500));
toast.success(`成功${actionText} ${selectedInterpreters.length} 个翻译员`, { id: 'bulk-action' });
setSelectedInterpreters([]);
fetchInterpreters();
} catch (error) {
toast.error('操作失败', { id: 'bulk-action' });
}
};
const handleExport = async () => {
try {
toast.loading('正在导出翻译员数据...', { id: 'export' });
// 模拟导出延迟
await new Promise(resolve => setTimeout(resolve, 2000));
toast.success('翻译员数据导出成功', { id: 'export' });
} catch (error) {
toast.error('导出失败', { id: 'export' });
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'active':
return 'text-green-800 bg-green-100';
case 'inactive':
return 'text-gray-800 bg-gray-100';
case 'busy':
return 'text-yellow-800 bg-yellow-100';
case 'offline':
return 'text-red-800 bg-red-100';
default:
return 'text-gray-800 bg-gray-100';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'active':
return '活跃';
case 'inactive':
return '不活跃';
case 'busy':
return '忙碌';
case 'offline':
return '离线';
default:
return status;
}
};
const getAvailabilityColor = (availability: string) => {
switch (availability) {
case 'available':
return 'text-green-600';
case 'busy':
return 'text-yellow-600';
case 'offline':
return 'text-red-600';
default:
return 'text-gray-600';
}
};
const getAvailabilityText = (availability: string) => {
switch (availability) {
case 'available':
return '可接单';
case 'busy':
return '忙碌中';
case 'offline':
return '离线';
default:
return availability;
}
};
const renderStars = (rating: number) => {
return (
<div className="flex items-center">
{[...Array(5)].map((_, i) => (
<StarIcon
key={i}
className={`h-4 w-4 ${
i < Math.floor(rating) ? 'text-yellow-400 fill-current' : 'text-gray-300'
}`}
/>
))}
<span className="ml-1 text-sm text-gray-600">{rating.toFixed(1)}</span>
</div>
);
};
// 添加翻译员提交函数
const handleAddInterpreter = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 创建新翻译员对象
const newInterpreterData: Interpreter = {
id: Date.now().toString(),
...newInterpreter,
languages: newInterpreter.languages.split(',').map(lang => lang.trim()),
specialties: newInterpreter.specialties.split(',').map(spec => spec.trim()),
rating: 5.0,
total_calls: 0,
total_hours: 0,
certifications: [],
joined_at: new Date().toISOString(),
last_active: new Date().toISOString()
};
// 添加到翻译员列表
setInterpreters(prev => [newInterpreterData, ...prev]);
// 重置表单
setNewInterpreter({
name: '',
email: '',
phone: '',
languages: '',
specialties: '',
experience_years: 0,
hourly_rate: 0,
location: '',
bio: '',
status: 'active',
availability: 'available'
});
// 关闭模态框
setShowAddInterpreterModal(false);
// 可以添加成功提示
alert('翻译员添加成功!');
} catch (error) {
console.error('添加翻译员失败:', error);
alert('添加翻译员失败,请重试');
} finally {
setIsSubmitting(false);
}
};
// 添加翻译员模态框组件
const AddInterpreterModal = () => (
<div className={`fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 ${showAddInterpreterModal ? 'block' : 'hidden'}`}>
<div className="relative top-10 mx-auto p-5 border w-[600px] shadow-lg rounded-md bg-white">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900"></h3>
<button
onClick={() => setShowAddInterpreterModal(false)}
className="text-gray-400 hover:text-gray-600"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
<form onSubmit={handleAddInterpreter} className="space-y-4 max-h-[70vh] overflow-y-auto">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<span className="text-red-500">*</span>
</label>
<input
type="text"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={newInterpreter.name}
onChange={(e) => setNewInterpreter({...newInterpreter, name: e.target.value})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<span className="text-red-500">*</span>
</label>
<input
type="email"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={newInterpreter.email}
onChange={(e) => setNewInterpreter({...newInterpreter, email: e.target.value})}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<span className="text-red-500">*</span>
</label>
<input
type="tel"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={newInterpreter.phone}
onChange={(e) => setNewInterpreter({...newInterpreter, phone: e.target.value})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={newInterpreter.location}
onChange={(e) => setNewInterpreter({...newInterpreter, location: e.target.value})}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<span className="text-red-500">*</span>
<span className="text-gray-500 text-xs ml-1">()</span>
</label>
<input
type="text"
required
placeholder="例如:英语,中文,法语"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={newInterpreter.languages}
onChange={(e) => setNewInterpreter({...newInterpreter, languages: e.target.value})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<span className="text-gray-500 text-xs ml-1">()</span>
</label>
<input
type="text"
placeholder="例如:医疗,法律,商务"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={newInterpreter.specialties}
onChange={(e) => setNewInterpreter({...newInterpreter, specialties: e.target.value})}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<span className="text-red-500">*</span>
</label>
<input
type="number"
required
min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={newInterpreter.experience_years}
onChange={(e) => setNewInterpreter({...newInterpreter, experience_years: parseInt(e.target.value) || 0})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
(¥) <span className="text-red-500">*</span>
</label>
<input
type="number"
required
min="0"
step="0.01"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={newInterpreter.hourly_rate}
onChange={(e) => setNewInterpreter({...newInterpreter, hourly_rate: parseFloat(e.target.value) || 0})}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={newInterpreter.status}
onChange={(e) => setNewInterpreter({...newInterpreter, status: e.target.value as 'active' | 'inactive' | 'busy' | 'offline'})}
>
<option value="active"></option>
<option value="inactive"></option>
<option value="busy"></option>
<option value="offline">线</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={newInterpreter.availability}
onChange={(e) => setNewInterpreter({...newInterpreter, availability: e.target.value as 'available' | 'busy' | 'offline'})}
>
<option value="available"></option>
<option value="busy"></option>
<option value="offline">线</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<textarea
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={newInterpreter.bio}
onChange={(e) => setNewInterpreter({...newInterpreter, bio: e.target.value})}
placeholder="请简要介绍翻译员的背景和专业经验..."
/>
</div>
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={() => setShowAddInterpreterModal(false)}
className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50"
>
</button>
<button
type="submit"
disabled={isSubmitting}
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50"
>
{isSubmitting ? '添加中...' : '添加翻译员'}
</button>
</div>
</form>
</div>
</div>
);
return (
<>
<Head>
<title> - </title>
</Head>
<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-2 text-sm text-gray-700">
</p>
</div>
<div className="mt-4 sm:mt-0 sm:flex sm:space-x-3">
<button
onClick={handleExport}
className="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
<ArrowDownTrayIcon className="h-4 w-4 mr-2" />
</button>
<button
onClick={() => setShowAddInterpreterModal(true)}
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
>
<UserPlusIcon className="h-4 w-4 mr-2" />
</button>
</div>
</div>
{/* 搜索和过滤器 */}
<div className="bg-white shadow rounded-lg p-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
<div>
<label htmlFor="search" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<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"
id="search"
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"
placeholder="搜索姓名、邮箱、语言..."
value={filters.search}
onChange={(e) => handleSearch(e.target.value)}
/>
</div>
</div>
<div>
<label htmlFor="status" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
id="status"
className="block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={filters.status}
onChange={(e) => handleFilterChange('status', e.target.value)}
>
<option value=""></option>
<option value="active"></option>
<option value="inactive"></option>
<option value="busy"></option>
<option value="offline">线</option>
</select>
</div>
<div>
<label htmlFor="language" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="text"
id="language"
className="block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="过滤语言..."
value={filters.language}
onChange={(e) => handleFilterChange('language', e.target.value)}
/>
</div>
<div>
<label htmlFor="rating" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
id="rating"
className="block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={filters.rating}
onChange={(e) => handleFilterChange('rating', e.target.value)}
>
<option value=""></option>
<option value="4.5">4.5</option>
<option value="4.0">4.0</option>
<option value="3.5">3.5</option>
<option value="3.0">3.0</option>
</select>
</div>
<div>
<label htmlFor="availability" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
id="availability"
className="block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={filters.availability}
onChange={(e) => handleFilterChange('availability', e.target.value)}
>
<option value=""></option>
<option value="available"></option>
<option value="busy"></option>
<option value="offline">线</option>
</select>
</div>
</div>
</div>
{/* 批量操作 */}
{selectedInterpreters.length > 0 && (
<div className="bg-white shadow rounded-lg p-4">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-700">
{selectedInterpreters.length}
</span>
<div className="flex space-x-2">
<button
onClick={() => handleBulkAction('activate')}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700"
>
</button>
<button
onClick={() => handleBulkAction('deactivate')}
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"
>
</button>
<button
onClick={() => handleBulkAction('delete')}
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"
>
</button>
</div>
</div>
</div>
)}
{/* 翻译员列表 */}
<div className="bg-white shadow rounded-lg overflow-hidden">
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
) : (
<>
<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-blue-600 focus:ring-blue-500"
checked={selectedInterpreters.length === interpreters.length && interpreters.length > 0}
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">
{interpreters.map((interpreter) => (
<tr key={interpreter.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-blue-600 focus:ring-blue-500"
checked={selectedInterpreters.includes(interpreter.id)}
onChange={() => handleSelectInterpreter(interpreter.id)}
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
{interpreter.avatar ? (
<img className="h-10 w-10 rounded-full" src={interpreter.avatar} alt="" />
) : (
<div className="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
<UserIcon className="h-6 w-6 text-gray-600" />
</div>
)}
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{interpreter.name}</div>
<div className="text-sm text-gray-500 flex items-center">
<EnvelopeIcon className="h-4 w-4 mr-1" />
{interpreter.email}
</div>
<div className="text-sm text-gray-500 flex items-center">
<PhoneIcon className="h-4 w-4 mr-1" />
{interpreter.phone}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
<div className="flex items-center text-sm text-gray-900">
<LanguageIcon className="h-4 w-4 mr-1 text-gray-400" />
<span className="truncate max-w-32">{interpreter.languages.join(', ')}</span>
</div>
<div className="flex items-center text-sm text-gray-500">
<AcademicCapIcon className="h-4 w-4 mr-1 text-gray-400" />
<span className="truncate max-w-32">{interpreter.specialties.join(', ')}</span>
</div>
<div className="flex items-center text-sm text-gray-500">
<MapPinIcon className="h-4 w-4 mr-1 text-gray-400" />
{interpreter.location}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
{renderStars(interpreter.rating)}
<div className="text-sm text-gray-500">
{interpreter.total_calls}
</div>
<div className="text-sm text-gray-500">
{interpreter.total_hours}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
<div className="flex items-center text-sm font-medium text-gray-900">
<CurrencyDollarIcon className="h-4 w-4 mr-1 text-gray-400" />
¥{interpreter.hourly_rate}/
</div>
<div className="text-sm text-gray-500">
{interpreter.experience_years}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-2">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(interpreter.status)}`}>
{getStatusText(interpreter.status)}
</span>
<div className={`text-sm font-medium ${getAvailabilityColor(interpreter.availability)}`}>
{getAvailabilityText(interpreter.availability)}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div className="flex items-center">
<ClockIcon className="h-4 w-4 mr-1" />
{formatTime(interpreter.last_active)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center space-x-2">
<button
onClick={() => router.push(`/dashboard/interpreters/${interpreter.id}`)}
className="text-blue-600 hover:text-blue-900"
title="查看详情"
>
<EyeIcon className="h-4 w-4" />
</button>
<button
onClick={() => router.push(`/dashboard/interpreters/${interpreter.id}/edit`)}
className="text-green-600 hover:text-green-900"
title="编辑"
>
<PencilIcon className="h-4 w-4" />
</button>
<button
onClick={() => {
if (confirm('确定要删除这个翻译员吗?')) {
toast.success('翻译员删除成功');
fetchInterpreters();
}
}}
className="text-red-600 hover:text-red-900"
title="删除"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</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(prev => Math.max(prev - 1, 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(prev => Math.min(prev + 1, totalPages))}
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) * 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={() => setCurrentPage(prev => Math.max(prev - 1, 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"
>
</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-blue-50 border-blue-500 text-blue-600'
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
}`}
>
{page}
</button>
))}
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
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"
>
</button>
</nav>
</div>
</div>
</div>
)}
</>
)}
</div>
</div>
{/* 添加翻译员模态框 */}
<AddInterpreterModal />
</DashboardLayout>
</>
);
}