修复退出登录重定向问题和相关功能优化

- 修复DashboardLayout中的退出登录函数,确保清除所有认证信息
- 恢复_app.tsx中的认证逻辑,确保仪表盘页面需要登录访问
- 完善退出登录流程:清除本地存储 -> 调用登出API -> 重定向到登录页面
- 添加错误边界组件提升用户体验
- 优化React水合错误处理
- 添加JWT令牌验证API
- 完善各个仪表盘页面的功能和样式
This commit is contained in:
2025-07-03 20:56:17 +08:00
parent 211e0306b5
commit 1ba859196a
17 changed files with 1656 additions and 462 deletions
+208 -351
View File
@@ -1,53 +1,16 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Head from 'next/head';
import DashboardLayout from '../../components/Layout/DashboardLayout';
import {
UserGroupIcon,
PhoneIcon,
DocumentTextIcon,
CurrencyDollarIcon,
ChartBarIcon,
ClockIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
ArrowUpIcon,
ArrowDownIcon,
EyeIcon,
PencilIcon,
TrashIcon,
PlayIcon,
PauseIcon,
StopIcon,
MicrophoneIcon,
VideoCameraIcon,
GlobeAltIcon,
BellIcon,
CogIcon,
UserIcon,
BuildingOfficeIcon,
CalendarDaysIcon,
ChatBubbleLeftRightIcon,
BanknotesIcon,
UsersIcon,
LanguageIcon,
DocumentDuplicateIcon,
InboxIcon,
PhoneArrowUpRightIcon,
PhoneArrowDownLeftIcon,
TrophyIcon,
StarIcon,
HeartIcon,
FireIcon,
LightBulbIcon,
ShieldCheckIcon,
SparklesIcon,
RocketLaunchIcon,
MegaphoneIcon,
GiftIcon,
AcademicCapIcon,
MapIcon,
SunIcon,
MoonIcon,
ComputerDesktopIcon,
VideoCameraIcon,
LanguageIcon,
CurrencyDollarIcon,
} from '@heroicons/react/24/outline';
import {
CheckCircleIcon as CheckCircleIconSolid,
@@ -58,23 +21,7 @@ import {
PhoneIcon as PhoneIconSolid,
DocumentTextIcon as DocumentTextIconSolid,
CurrencyDollarIcon as CurrencyDollarIconSolid,
ChartBarIcon as ChartBarIconSolid,
BellIcon as BellIconSolid,
StarIcon as StarIconSolid,
HeartIcon as HeartIconSolid,
FireIcon as FireIconSolid,
TrophyIcon as TrophyIconSolid,
SparklesIcon as SparklesIconSolid,
RocketLaunchIcon as RocketLaunchIconSolid,
GiftIcon as GiftIconSolid,
AcademicCapIcon as AcademicCapIconSolid,
ShieldCheckIcon as ShieldCheckIconSolid,
LightBulbIcon as LightBulbIconSolid,
MegaphoneIcon as MegaphoneIconSolid,
MapIcon as MapIconSolid,
SunIcon as SunIconSolid,
MoonIcon as MoonIconSolid,
ComputerDesktopIcon as ComputerDesktopIconSolid,
UsersIcon as UsersIconSolid,
} from '@heroicons/react/24/solid';
import { toast } from 'react-hot-toast';
import { statsAPI } from '../../lib/api-service';
@@ -133,8 +80,8 @@ export default function Dashboard() {
activities.push({
id: order.id,
type: 'order',
title: `订单 ${order.order_number}`,
description: `${order.user_name} - ${order.service_name}`,
title: `订单 ${order.order_number || order.id}`,
description: `${order.user_name || '用户'} - ${order.service_name || '服务'}`,
time: formatTime(order.created_at),
status: getOrderStatus(order.status),
icon: getOrderIcon(order.service_type)
@@ -194,6 +141,7 @@ export default function Dashboard() {
};
const formatTime = (dateString: string) => {
if (!dateString) return '未知时间';
const date = new Date(dateString);
const now = new Date();
const diff = now.getTime() - date.getTime();
@@ -201,25 +149,23 @@ export default function Dashboard() {
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}天前`;
if (hours > 0) return `${hours}小时`;
if (minutes > 0) return `${minutes}分钟前`;
return '刚刚';
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(amount);
if (days > 0) {
return `${days}`;
} else if (hours > 0) {
return `${hours}小时前`;
} else if (minutes > 0) {
return `${minutes}分钟前`;
} else {
return '刚刚';
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'success': return 'text-green-600 bg-green-50';
case 'warning': return 'text-yellow-600 bg-yellow-50';
case 'error': return 'text-red-600 bg-red-50';
default: return 'text-blue-600 bg-blue-50';
case 'success': return 'bg-green-100 text-green-800';
case 'warning': return 'bg-yellow-100 text-yellow-800';
case 'error': return 'bg-red-100 text-red-800';
default: return 'bg-blue-100 text-blue-800';
}
};
@@ -234,294 +180,205 @@ export default function Dashboard() {
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">...</p>
</div>
</div>
<>
<Head>
<title> - </title>
</Head>
<DashboardLayout title="仪表盘">
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
<p className="ml-4 text-gray-600">...</p>
</div>
</DashboardLayout>
</>
);
}
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<div className="bg-white shadow">
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900"></h1>
<p className="mt-1 text-sm text-gray-600">
</p>
</div>
<div className="flex items-center space-x-4">
<button
onClick={() => router.push('/dashboard/notifications')}
className="relative p-2 text-gray-400 hover:text-gray-500"
>
<BellIcon className="h-6 w-6" />
<span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-red-400 ring-2 ring-white" />
</button>
<button
onClick={() => router.push('/dashboard/settings')}
className="p-2 text-gray-400 hover:text-gray-500"
>
<CogIcon className="h-6 w-6" />
</button>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<UserGroupIconSolid className="h-8 w-8 text-blue-600" />
</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">
{stats.totalUsers.toLocaleString()}
</dd>
</dl>
</div>
</div>
</div>
<div className="bg-gray-50 px-5 py-3">
<div className="text-sm">
<span className="text-green-600 font-medium">
{stats.activeUsers}
</span>
</div>
</div>
<>
<Head>
<title> - </title>
</Head>
<DashboardLayout title="仪表盘">
<div className="space-y-6">
{/* 页面标题和描述 */}
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="mt-2 text-sm text-gray-700">
</p>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<UsersIcon className="h-8 w-8 text-green-600" />
</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">
{stats.totalInterpreters.toLocaleString()}
</dd>
</dl>
</div>
</div>
</div>
<div className="bg-gray-50 px-5 py-3">
<div className="text-sm">
<span className="text-green-600 font-medium">
线
</span>
</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">
<DocumentTextIconSolid className="h-8 w-8 text-purple-600" />
</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">
{stats.totalOrders.toLocaleString()}
</dd>
</dl>
</div>
</div>
</div>
<div className="bg-gray-50 px-5 py-3">
<div className="text-sm">
<span className="text-green-600 font-medium">
</span>
</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">
<PhoneIconSolid className="h-8 w-8 text-orange-600" />
</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">
{stats.totalCalls.toLocaleString()}
</dd>
</dl>
</div>
</div>
</div>
<div className="bg-gray-50 px-5 py-3">
<div className="text-sm">
<span className="text-orange-600 font-medium">
{stats.activeCalls}
</span>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Recent Activity */}
<div className="lg:col-span-2">
<div className="bg-white shadow rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900"></h3>
</div>
<div className="divide-y divide-gray-200">
{recentActivity.length === 0 ? (
<div className="px-6 py-8 text-center">
<InboxIcon className="mx-auto h-12 w-12 text-gray-400" />
<p className="mt-2 text-sm text-gray-500"></p>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<UserGroupIconSolid className="h-8 w-8 text-blue-600" />
</div>
) : (
recentActivity.map((activity) => {
const StatusIcon = getStatusIcon(activity.status);
const ActivityIcon = activity.icon;
return (
<div key={activity.id} className="px-6 py-4">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className={`p-2 rounded-full ${getStatusColor(activity.status)}`}>
<ActivityIcon className="h-5 w-5" />
</div>
</div>
<div className="ml-4 flex-1">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-900">
{activity.title}
</p>
<div className="flex items-center">
<StatusIcon className={`h-4 w-4 mr-1 ${
activity.status === 'success' ? 'text-green-500' :
activity.status === 'warning' ? 'text-yellow-500' :
activity.status === 'error' ? 'text-red-500' :
'text-blue-500'
}`} />
<span className="text-xs text-gray-500">
{activity.time}
</span>
</div>
</div>
<p className="text-sm text-gray-500">
{activity.description}
</p>
<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">
{stats.totalUsers.toLocaleString()}
</dd>
</dl>
</div>
</div>
</div>
<div className="bg-gray-50 px-5 py-3">
<div className="text-sm">
<span className="text-green-600 font-medium">
{stats.activeUsers}
</span>
</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">
<UsersIconSolid className="h-8 w-8 text-green-600" />
</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">
{stats.totalInterpreters.toLocaleString()}
</dd>
</dl>
</div>
</div>
</div>
<div className="bg-gray-50 px-5 py-3">
<div className="text-sm">
<span className="text-green-600 font-medium">
线
</span>
</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">
<DocumentTextIconSolid className="h-8 w-8 text-purple-600" />
</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">
{stats.totalOrders.toLocaleString()}
</dd>
</dl>
</div>
</div>
</div>
<div className="bg-gray-50 px-5 py-3">
<div className="text-sm">
<span className="text-green-600 font-medium">
</span>
</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">
<PhoneIconSolid className="h-8 w-8 text-orange-600" />
</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">
{stats.totalCalls.toLocaleString()}
</dd>
</dl>
</div>
</div>
</div>
<div className="bg-gray-50 px-5 py-3">
<div className="text-sm">
<span className="text-orange-600 font-medium">
{stats.activeCalls}
</span>
</div>
</div>
</div>
</div>
{/* Recent Activity */}
<div className="bg-white shadow rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900 flex items-center">
<ChartBarIcon className="h-5 w-5 text-indigo-600 mr-2" />
</h3>
</div>
<div className="divide-y divide-gray-200">
{recentActivity.length === 0 ? (
<div className="px-6 py-8 text-center">
<InboxIcon className="mx-auto h-12 w-12 text-gray-400" />
<p className="mt-2 text-sm text-gray-500"></p>
</div>
) : (
recentActivity.map((activity) => {
const StatusIcon = getStatusIcon(activity.status);
const ActivityIcon = activity.icon;
return (
<div key={activity.id} className="px-6 py-4 hover:bg-gray-50 transition-colors duration-200">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className={`p-2 rounded-full ${getStatusColor(activity.status)}`}>
<ActivityIcon className="h-5 w-5" />
</div>
</div>
<div className="ml-4 flex-1">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-gray-900">
{activity.title}
</p>
<div className="flex items-center">
<StatusIcon className={`h-4 w-4 mr-1 ${
activity.status === 'success' ? 'text-green-500' :
activity.status === 'warning' ? 'text-yellow-500' :
activity.status === 'error' ? 'text-red-500' :
'text-blue-500'
}`} />
<span className="text-xs text-gray-500">
{activity.time}
</span>
</div>
</div>
<p className="text-sm text-gray-500 mt-1">
{activity.description}
</p>
</div>
</div>
);
})
)}
</div>
</div>
</div>
{/* Quick Actions */}
<div>
<div className="bg-white shadow rounded-lg">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900"></h3>
</div>
<div className="p-6">
<div className="space-y-3">
<button
onClick={() => router.push('/dashboard/users')}
className="w-full flex items-center justify-between p-3 text-left border border-gray-200 rounded-md hover:bg-gray-50"
>
<div className="flex items-center">
<UserGroupIcon className="h-5 w-5 text-blue-600 mr-3" />
<span className="text-sm font-medium text-gray-900"></span>
</div>
<ArrowUpIcon className="h-4 w-4 text-gray-400 transform rotate-45" />
</button>
<button
onClick={() => router.push('/dashboard/interpreters')}
className="w-full flex items-center justify-between p-3 text-left border border-gray-200 rounded-md hover:bg-gray-50"
>
<div className="flex items-center">
<UsersIcon className="h-5 w-5 text-green-600 mr-3" />
<span className="text-sm font-medium text-gray-900"></span>
</div>
<ArrowUpIcon className="h-4 w-4 text-gray-400 transform rotate-45" />
</button>
<button
onClick={() => router.push('/dashboard/orders')}
className="w-full flex items-center justify-between p-3 text-left border border-gray-200 rounded-md hover:bg-gray-50"
>
<div className="flex items-center">
<DocumentTextIcon className="h-5 w-5 text-purple-600 mr-3" />
<span className="text-sm font-medium text-gray-900"></span>
</div>
<ArrowUpIcon className="h-4 w-4 text-gray-400 transform rotate-45" />
</button>
<button
onClick={() => router.push('/dashboard/calls')}
className="w-full flex items-center justify-between p-3 text-left border border-gray-200 rounded-md hover:bg-gray-50"
>
<div className="flex items-center">
<PhoneIcon className="h-5 w-5 text-orange-600 mr-3" />
<span className="text-sm font-medium text-gray-900"></span>
</div>
<ArrowUpIcon className="h-4 w-4 text-gray-400 transform rotate-45" />
</button>
<button
onClick={() => router.push('/dashboard/invoices')}
className="w-full flex items-center justify-between p-3 text-left border border-gray-200 rounded-md hover:bg-gray-50"
>
<div className="flex items-center">
<CurrencyDollarIcon className="h-5 w-5 text-emerald-600 mr-3" />
<span className="text-sm font-medium text-gray-900"></span>
</div>
<ArrowUpIcon className="h-4 w-4 text-gray-400 transform rotate-45" />
</button>
<button
onClick={() => router.push('/dashboard/documents')}
className="w-full flex items-center justify-between p-3 text-left border border-gray-200 rounded-md hover:bg-gray-50"
>
<div className="flex items-center">
<DocumentDuplicateIcon className="h-5 w-5 text-indigo-600 mr-3" />
<span className="text-sm font-medium text-gray-900"></span>
</div>
<ArrowUpIcon className="h-4 w-4 text-gray-400 transform rotate-45" />
</button>
</div>
</div>
);
})
)}
</div>
</div>
</div>
</div>
</div>
</DashboardLayout>
</>
);
}
+275 -2
View File
@@ -23,7 +23,8 @@ import {
MapPinIcon,
CalendarIcon,
CurrencyDollarIcon,
AcademicCapIcon
AcademicCapIcon,
XMarkIcon
} from '@heroicons/react/24/outline';
import { getDemoData } from '../../lib/demo-data';
import { formatTime } from '../../lib/utils';
@@ -74,6 +75,23 @@ export default function Interpreters() {
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(() => {
@@ -395,6 +413,258 @@ export default function Interpreters() {
);
};
// 添加翻译员提交函数
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>
@@ -420,7 +690,7 @@ export default function Interpreters() {
</button>
<button
onClick={() => router.push('/dashboard/interpreters/new')}
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" />
@@ -784,6 +1054,9 @@ export default function Interpreters() {
)}
</div>
</div>
{/* 添加翻译员模态框 */}
<AddInterpreterModal />
</DashboardLayout>
</>
);
+240 -2
View File
@@ -23,7 +23,8 @@ import {
DocumentTextIcon,
PlayIcon,
PauseIcon,
StopIcon
StopIcon,
XMarkIcon
} from '@heroicons/react/24/outline';
import { getDemoData } from '../../lib/demo-data';
import { formatTime } from '../../lib/utils';
@@ -71,6 +72,21 @@ export default function Orders() {
date_range: ''
});
// 添加模态框状态
const [showCreateOrderModal, setShowCreateOrderModal] = useState(false);
const [newOrder, setNewOrder] = useState({
user_name: '',
user_email: '',
interpreter_name: '',
language_pair: '',
service_type: 'audio' as 'audio' | 'video' | 'onsite',
start_time: '',
duration: 60,
amount: 0,
notes: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const pageSize = 10;
useEffect(() => {
@@ -396,6 +412,225 @@ export default function Orders() {
return `${mins}分钟`;
};
// 创建订单提交函数
const handleCreateOrder = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 创建新订单对象
const newOrderData: Order = {
id: Date.now().toString(),
order_number: `ORD-${Date.now()}`,
...newOrder,
status: 'pending',
payment_status: 'pending',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
// 添加到订单列表
setOrders(prev => [newOrderData, ...prev]);
// 重置表单
setNewOrder({
user_name: '',
user_email: '',
interpreter_name: '',
language_pair: '',
service_type: 'audio',
start_time: '',
duration: 60,
amount: 0,
notes: ''
});
// 关闭模态框
setShowCreateOrderModal(false);
// 可以添加成功提示
alert('订单创建成功!');
} catch (error) {
console.error('创建订单失败:', error);
alert('创建订单失败,请重试');
} finally {
setIsSubmitting(false);
}
};
// 创建订单模态框组件
const CreateOrderModal = () => (
<div className={`fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 ${showCreateOrderModal ? '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={() => setShowCreateOrderModal(false)}
className="text-gray-400 hover:text-gray-600"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
<form onSubmit={handleCreateOrder} 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={newOrder.user_name}
onChange={(e) => setNewOrder({...newOrder, user_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={newOrder.user_email}
onChange={(e) => setNewOrder({...newOrder, user_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="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={newOrder.interpreter_name}
onChange={(e) => setNewOrder({...newOrder, interpreter_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="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={newOrder.language_pair}
onChange={(e) => setNewOrder({...newOrder, language_pair: 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>
<select
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={newOrder.service_type}
onChange={(e) => setNewOrder({...newOrder, service_type: e.target.value as 'audio' | 'video' | 'onsite'})}
>
<option value="audio"></option>
<option value="video"></option>
<option value="onsite"></option>
</select>
</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="15"
step="15"
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={newOrder.duration}
onChange={(e) => setNewOrder({...newOrder, duration: parseInt(e.target.value) || 60})}
/>
</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="datetime-local"
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={newOrder.start_time}
onChange={(e) => setNewOrder({...newOrder, start_time: 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="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={newOrder.amount}
onChange={(e) => setNewOrder({...newOrder, amount: parseFloat(e.target.value) || 0})}
/>
</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={newOrder.notes}
onChange={(e) => setNewOrder({...newOrder, notes: e.target.value})}
placeholder="请输入订单相关的备注信息..."
/>
</div>
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={() => setShowCreateOrderModal(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>
@@ -421,7 +656,7 @@ export default function Orders() {
</button>
<button
onClick={() => router.push('/dashboard/orders/new')}
onClick={() => setShowCreateOrderModal(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"
>
<PlusIcon className="h-4 w-4 mr-2" />
@@ -787,6 +1022,9 @@ export default function Orders() {
)}
</div>
</div>
{/* 创建订单模态框 */}
<CreateOrderModal />
</DashboardLayout>
</>
);
+182 -2
View File
@@ -18,7 +18,8 @@ import {
XCircleIcon,
ExclamationTriangleIcon,
ArrowDownTrayIcon,
FunnelIcon
FunnelIcon,
XMarkIcon
} from '@heroicons/react/24/outline';
import { getDemoData } from '../../lib/demo-data';
import { formatTime } from '../../lib/utils';
@@ -59,6 +60,18 @@ export default function Users() {
company: ''
});
// 添加模态框状态
const [showAddUserModal, setShowAddUserModal] = useState(false);
const [newUser, setNewUser] = useState({
name: '',
email: '',
phone: '',
company: '',
role: 'user' as 'admin' | 'user' | 'interpreter',
status: 'active' as 'active' | 'inactive' | 'pending'
});
const [isSubmitting, setIsSubmitting] = useState(false);
const pageSize = 10;
useEffect(() => {
@@ -318,6 +331,170 @@ export default function Users() {
}
};
// 添加用户提交函数
const handleAddUser = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 创建新用户对象
const newUserData: User = {
id: Date.now().toString(),
...newUser,
created_at: new Date().toISOString(),
last_login: '从未登录',
total_calls: 0,
total_spent: 0
};
// 添加到用户列表
setUsers(prev => [newUserData, ...prev]);
// 重置表单
setNewUser({
name: '',
email: '',
phone: '',
company: '',
role: 'user',
status: 'active'
});
// 关闭模态框
setShowAddUserModal(false);
// 可以添加成功提示
alert('用户添加成功!');
} catch (error) {
console.error('添加用户失败:', error);
alert('添加用户失败,请重试');
} finally {
setIsSubmitting(false);
}
};
// 添加用户模态框组件
const AddUserModal = () => (
<div className={`fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 ${showAddUserModal ? 'block' : 'hidden'}`}>
<div className="relative top-20 mx-auto p-5 border w-96 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={() => setShowAddUserModal(false)}
className="text-gray-400 hover:text-gray-600"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
<form onSubmit={handleAddUser} className="space-y-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={newUser.name}
onChange={(e) => setNewUser({...newUser, 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={newUser.email}
onChange={(e) => setNewUser({...newUser, email: 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="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={newUser.phone}
onChange={(e) => setNewUser({...newUser, 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={newUser.company}
onChange={(e) => setNewUser({...newUser, company: e.target.value})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<span className="text-red-500">*</span>
</label>
<select
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={newUser.role}
onChange={(e) => setNewUser({...newUser, role: e.target.value as 'admin' | 'user' | 'interpreter'})}
>
<option value="user"></option>
<option value="admin"></option>
<option value="interpreter"></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={newUser.status}
onChange={(e) => setNewUser({...newUser, status: e.target.value as 'active' | 'inactive' | 'pending'})}
>
<option value="active"></option>
<option value="inactive"></option>
<option value="pending"></option>
</select>
</div>
<div className="flex justify-end space-x-3 pt-4">
<button
type="button"
onClick={() => setShowAddUserModal(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>
@@ -343,7 +520,7 @@ export default function Users() {
</button>
<button
onClick={() => router.push('/dashboard/users/new')}
onClick={() => setShowAddUserModal(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"
>
<PlusIcon className="h-4 w-4 mr-2" />
@@ -656,6 +833,9 @@ export default function Users() {
</div>
</div>
</DashboardLayout>
{/* 添加用户模态框 */}
<AddUserModal />
</>
);
}