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

308 lines
11 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 Link from 'next/link';
import Head from 'next/head';
import {
HomeIcon,
UsersIcon,
PhoneIcon,
CalendarIcon,
DocumentTextIcon,
CurrencyDollarIcon,
ChartBarIcon,
CogIcon,
BellIcon,
UserGroupIcon,
ClipboardDocumentListIcon,
Bars3Icon,
XMarkIcon,
BuildingOfficeIcon,
FolderIcon,
DocumentIcon,
ReceiptPercentIcon,
ChevronDownIcon,
ChevronRightIcon,
LanguageIcon,
ArrowRightOnRectangleIcon
} from '@heroicons/react/24/outline';
interface DashboardLayoutProps {
children: React.ReactNode;
title?: string;
}
const navigation = [
{ name: '仪表盘', href: '/dashboard', icon: HomeIcon },
{ name: '用户管理', href: '/dashboard/users', icon: UsersIcon },
{ name: '翻译员管理', href: '/dashboard/interpreters', icon: LanguageIcon },
{
name: '订单管理',
icon: DocumentTextIcon,
children: [
{ name: '订单列表', href: '/dashboard/orders' },
{ name: '发票管理', href: '/dashboard/invoices' }
]
},
{ name: '通话记录', href: '/dashboard/calls', icon: PhoneIcon },
{ name: '企业服务', href: '/dashboard/enterprise', icon: BuildingOfficeIcon },
{ name: '文档管理', href: '/dashboard/documents', icon: FolderIcon },
{ name: '系统设置', href: '/dashboard/settings', icon: CogIcon },
];
export default function DashboardLayout({ children, title = '管理后台' }: DashboardLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [isDemoMode, setIsDemoMode] = useState(true); // 始终启用演示模式
const [expandedItems, setExpandedItems] = useState<string[]>([]);
const router = useRouter();
useEffect(() => {
// 演示模式始终启用
setIsDemoMode(true);
}, []);
const handleLogout = async () => {
try {
// 清除所有相关的本地存储
localStorage.removeItem('access_token');
localStorage.removeItem('adminToken');
localStorage.removeItem('user');
// 调用后端登出API如果需要
try {
await fetch('/api/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
} catch (apiError) {
console.log('API logout error (non-critical):', apiError);
}
// 重定向到登录页面
window.location.href = '/auth/login';
} catch (error) {
console.error('Logout error:', error);
// 即使出错也要重定向到登录页面
window.location.href = '/auth/login';
}
};
const toggleExpanded = (itemName: string) => {
setExpandedItems(prev =>
prev.includes(itemName)
? prev.filter(name => name !== itemName)
: [...prev, itemName]
);
};
const isItemActive = (item: any) => {
if (item.href) {
return router.pathname === item.href;
}
if (item.children) {
return item.children.some((child: any) => router.pathname === child.href);
}
return false;
};
const renderNavItem = (item: any) => {
const hasChildren = item.children && item.children.length > 0;
const isActive = isItemActive(item);
const isExpanded = expandedItems.includes(item.name);
if (hasChildren) {
return (
<div key={item.name}>
<button
onClick={() => toggleExpanded(item.name)}
className={`${
isActive
? 'bg-blue-100 text-blue-900'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
} group flex items-center w-full px-2 py-2 text-sm font-medium rounded-md`}
>
<item.icon
className={`${
isActive ? 'text-blue-500' : 'text-gray-400 group-hover:text-gray-500'
} mr-3 flex-shrink-0 h-6 w-6`}
/>
{item.name}
{isExpanded ? (
<ChevronDownIcon className="ml-auto h-4 w-4" />
) : (
<ChevronRightIcon className="ml-auto h-4 w-4" />
)}
</button>
{isExpanded && (
<div className="ml-8 mt-1 space-y-1">
{item.children.map((child: any) => (
<Link
key={child.name}
href={child.href}
className={`${
router.pathname === child.href
? 'bg-blue-50 text-blue-700 border-r-2 border-blue-500'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
} group flex items-center px-2 py-2 text-sm font-medium rounded-md`}
>
{child.name}
</Link>
))}
</div>
)}
</div>
);
}
return (
<Link
key={item.name}
href={item.href}
className={`${
isActive
? 'bg-blue-100 text-blue-900'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
} group flex items-center px-2 py-2 text-sm font-medium rounded-md`}
>
<item.icon
className={`${
isActive ? 'text-blue-500' : 'text-gray-400 group-hover:text-gray-500'
} mr-3 flex-shrink-0 h-6 w-6`}
/>
{item.name}
</Link>
);
};
return (
<>
<Head>
<title>{title} - </title>
<meta name="description" content="口译服务管理平台管理后台" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="h-screen flex overflow-hidden bg-gray-100">
{/* 移动端侧边栏 */}
<div className={`fixed inset-0 flex z-40 md:hidden ${sidebarOpen ? '' : 'hidden'}`}>
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" onClick={() => setSidebarOpen(false)} />
<div className="relative flex-1 flex flex-col max-w-xs w-full pt-5 pb-4 bg-white">
<div className="absolute top-0 right-0 -mr-12 pt-2">
<button
type="button"
className="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
onClick={() => setSidebarOpen(false)}
>
<XMarkIcon className="h-6 w-6 text-white" />
</button>
</div>
<div className="flex-shrink-0 flex items-center px-4">
<h1 className="text-xl font-bold text-gray-900"></h1>
{isDemoMode && (
<span className="ml-2 px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
</span>
)}
</div>
<div className="mt-5 flex-1 h-0 overflow-y-auto">
<nav className="px-2 space-y-1">
{navigation.map(renderNavItem)}
</nav>
</div>
<div className="flex-shrink-0 p-4 border-t border-gray-200">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="h-8 w-8 rounded-full bg-blue-500 flex items-center justify-center">
<span className="text-sm font-medium text-white"></span>
</div>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-gray-700"></p>
<p className="text-xs text-gray-500">admin@demo.com</p>
</div>
</div>
<button
onClick={handleLogout}
className="mt-3 w-full bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-2 rounded-md text-sm font-medium flex items-center justify-center"
>
<ArrowRightOnRectangleIcon className="h-4 w-4 mr-2" />
退
</button>
</div>
</div>
</div>
{/* 桌面端侧边栏 */}
<div className="hidden md:flex md:flex-shrink-0">
<div className="flex flex-col w-64">
<div className="flex flex-col h-0 flex-1">
<div className="flex items-center h-16 flex-shrink-0 px-4 bg-white border-b border-gray-200">
<h1 className="text-xl font-bold text-gray-900"></h1>
{isDemoMode && (
<span className="ml-2 px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
</span>
)}
</div>
<div className="flex-1 flex flex-col overflow-y-auto bg-white border-r border-gray-200">
<nav className="flex-1 px-2 py-4 space-y-1">
{navigation.map(renderNavItem)}
</nav>
<div className="flex-shrink-0 p-4 border-t border-gray-200">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="h-8 w-8 rounded-full bg-blue-500 flex items-center justify-center">
<span className="text-sm font-medium text-white"></span>
</div>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-gray-700"></p>
<p className="text-xs text-gray-500">admin@demo.com</p>
</div>
</div>
<button
onClick={handleLogout}
className="mt-3 w-full bg-gray-100 text-gray-700 hover:bg-gray-200 px-3 py-2 rounded-md text-sm font-medium flex items-center justify-center"
>
<ArrowRightOnRectangleIcon className="h-4 w-4 mr-2" />
退
</button>
</div>
</div>
</div>
</div>
</div>
{/* 主内容区域 */}
<div className="flex flex-col w-0 flex-1 overflow-hidden">
<div className="relative z-10 flex-shrink-0 flex h-16 bg-white shadow md:hidden">
<button
type="button"
className="px-4 border-r border-gray-200 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 md:hidden"
onClick={() => setSidebarOpen(true)}
>
<Bars3Icon className="h-6 w-6" />
</button>
<div className="flex-1 px-4 flex justify-between items-center">
<h1 className="text-lg font-semibold text-gray-900">{title}</h1>
{isDemoMode && (
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
</span>
)}
</div>
</div>
<main className="flex-1 relative overflow-y-auto focus:outline-none">
<div className="py-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
{children}
</div>
</div>
</main>
</div>
</div>
</>
);
}