- 修复DashboardLayout中的退出登录函数,确保清除所有认证信息 - 恢复_app.tsx中的认证逻辑,确保仪表盘页面需要登录访问 - 完善退出登录流程:清除本地存储 -> 调用登出API -> 重定向到登录页面 - 添加错误边界组件提升用户体验 - 优化React水合错误处理 - 添加JWT令牌验证API - 完善各个仪表盘页面的功能和样式
384 lines
13 KiB
TypeScript
384 lines
13 KiB
TypeScript
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,
|
||
ChartBarIcon,
|
||
InboxIcon,
|
||
VideoCameraIcon,
|
||
LanguageIcon,
|
||
CurrencyDollarIcon,
|
||
} from '@heroicons/react/24/outline';
|
||
import {
|
||
CheckCircleIcon as CheckCircleIconSolid,
|
||
ExclamationTriangleIcon as ExclamationTriangleIconSolid,
|
||
ClockIcon as ClockIconSolid,
|
||
XCircleIcon as XCircleIconSolid,
|
||
UserGroupIcon as UserGroupIconSolid,
|
||
PhoneIcon as PhoneIconSolid,
|
||
DocumentTextIcon as DocumentTextIconSolid,
|
||
CurrencyDollarIcon as CurrencyDollarIconSolid,
|
||
UsersIcon as UsersIconSolid,
|
||
} from '@heroicons/react/24/solid';
|
||
import { toast } from 'react-hot-toast';
|
||
import { statsAPI } from '../../lib/api-service';
|
||
|
||
interface DashboardStats {
|
||
totalUsers: number;
|
||
totalInterpreters: number;
|
||
totalOrders: number;
|
||
totalCalls: number;
|
||
activeUsers: number;
|
||
activeCalls: number;
|
||
recentOrders: any[];
|
||
recentCalls: any[];
|
||
}
|
||
|
||
interface RecentActivity {
|
||
id: string;
|
||
type: 'order' | 'call' | 'user' | 'interpreter';
|
||
title: string;
|
||
description: string;
|
||
time: string;
|
||
status: 'success' | 'warning' | 'error' | 'info';
|
||
icon: any;
|
||
}
|
||
|
||
export default function Dashboard() {
|
||
const router = useRouter();
|
||
const [stats, setStats] = useState<DashboardStats>({
|
||
totalUsers: 0,
|
||
totalInterpreters: 0,
|
||
totalOrders: 0,
|
||
totalCalls: 0,
|
||
activeUsers: 0,
|
||
activeCalls: 0,
|
||
recentOrders: [],
|
||
recentCalls: []
|
||
});
|
||
const [loading, setLoading] = useState(true);
|
||
const [recentActivity, setRecentActivity] = useState<RecentActivity[]>([]);
|
||
|
||
useEffect(() => {
|
||
loadDashboardData();
|
||
}, []);
|
||
|
||
const loadDashboardData = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const dashboardStats = await statsAPI.getDashboardStats();
|
||
setStats(dashboardStats);
|
||
|
||
// 生成最近活动记录
|
||
const activities: RecentActivity[] = [];
|
||
|
||
// 添加最近订单活动
|
||
dashboardStats.recentOrders.forEach((order: any) => {
|
||
activities.push({
|
||
id: order.id,
|
||
type: 'order',
|
||
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)
|
||
});
|
||
});
|
||
|
||
// 添加最近通话活动
|
||
dashboardStats.recentCalls.forEach((call: any) => {
|
||
activities.push({
|
||
id: call.id,
|
||
type: 'call',
|
||
title: `${call.service_type === 'phone' ? '电话' : '视频'}通话`,
|
||
description: `${call.users?.name || '用户'} - ${call.interpreters?.name || '翻译员'}`,
|
||
time: formatTime(call.created_at),
|
||
status: getCallStatus(call.status),
|
||
icon: call.service_type === 'phone' ? PhoneIcon : VideoCameraIcon
|
||
});
|
||
});
|
||
|
||
// 按时间排序
|
||
activities.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime());
|
||
setRecentActivity(activities.slice(0, 10));
|
||
|
||
} catch (error) {
|
||
console.error('加载仪表盘数据失败:', error);
|
||
toast.error('加载仪表盘数据失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const getOrderStatus = (status: string) => {
|
||
switch (status) {
|
||
case 'completed': return 'success';
|
||
case 'cancelled': return 'error';
|
||
case 'in_progress': return 'warning';
|
||
default: return 'info';
|
||
}
|
||
};
|
||
|
||
const getCallStatus = (status: string) => {
|
||
switch (status) {
|
||
case 'ended': return 'success';
|
||
case 'cancelled': return 'error';
|
||
case 'connected': return 'warning';
|
||
default: return 'info';
|
||
}
|
||
};
|
||
|
||
const getOrderIcon = (serviceType: string) => {
|
||
switch (serviceType) {
|
||
case 'phone': return PhoneIcon;
|
||
case 'video': return VideoCameraIcon;
|
||
case 'document': return DocumentTextIcon;
|
||
default: return LanguageIcon;
|
||
}
|
||
};
|
||
|
||
const formatTime = (dateString: string) => {
|
||
if (!dateString) return '未知时间';
|
||
const date = new Date(dateString);
|
||
const now = new Date();
|
||
const diff = now.getTime() - date.getTime();
|
||
const minutes = Math.floor(diff / 60000);
|
||
const hours = Math.floor(minutes / 60);
|
||
const days = Math.floor(hours / 24);
|
||
|
||
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 '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';
|
||
}
|
||
};
|
||
|
||
const getStatusIcon = (status: string) => {
|
||
switch (status) {
|
||
case 'success': return CheckCircleIconSolid;
|
||
case 'warning': return ExclamationTriangleIconSolid;
|
||
case 'error': return XCircleIconSolid;
|
||
default: return ClockIconSolid;
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<>
|
||
<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 (
|
||
<>
|
||
<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>
|
||
|
||
{/* 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>
|
||
<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>
|
||
</div>
|
||
</DashboardLayout>
|
||
</>
|
||
);
|
||
}
|