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

384 lines
13 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 {
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>
</>
);
}