- 更新 .env.local 配置为真实的 Supabase 项目连接 - 创建完整的 API 服务层 (lib/api-service.ts) - 创建数据库类型定义 (types/database.ts) - 更新仪表盘页面使用真实数据替代演示数据 - 添加数据库连接测试和错误处理 - 创建测试数据验证系统功能 - 修复图标导入和语法错误 系统现在已连接到真实的 Supabase 数据库,可以正常显示统计数据和最近活动。
527 lines
19 KiB
TypeScript
527 lines
19 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import { useRouter } from 'next/router';
|
||
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,
|
||
} 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,
|
||
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,
|
||
} 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}`,
|
||
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) => {
|
||
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}天前`;
|
||
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);
|
||
};
|
||
|
||
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';
|
||
}
|
||
};
|
||
|
||
const getStatusIcon = (status: string) => {
|
||
switch (status) {
|
||
case 'success': return CheckCircleIconSolid;
|
||
case 'warning': return ExclamationTriangleIconSolid;
|
||
case 'error': return XCircleIconSolid;
|
||
default: return ClockIconSolid;
|
||
}
|
||
};
|
||
|
||
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>
|
||
);
|
||
}
|
||
|
||
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>
|
||
</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>
|
||
</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>
|
||
</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>
|
||
);
|
||
}
|