Files
leaudit-platform-frontend/app/components/layout/Layout.tsx
T
2025-11-18 11:06:24 +08:00

181 lines
6.1 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Sidebar } from './Sidebar';
// import { Header } from './Header';
import { Breadcrumb } from './Breadcrumb';
import { useMatches, useLocation } from '@remix-run/react';
import type { UserRole } from '~/root';
// 定义应用模块类型
type AppModule = 'contract' | 'record' | 'model' | '';
// 应用模块与reviewType的映射
const REVIEW_TYPE_TO_APP: Record<string, AppModule> = {
'contract': 'contract', // 合同管理
'record': 'record', // 案卷智能评查
'model': 'model' // 智慧法务大模型
};
interface LayoutProps {
children: React.ReactNode;
userRole?: UserRole;
frontendJWT?: string;
}
// 添加一个接口表示路由handle可能包含的属性
interface RouteHandle {
hideBreadcrumb?: boolean;
[key: string]: unknown;
}
interface Match {
handle?: RouteHandle;
pathname: string;
data: unknown;
}
export function Layout({ children, userRole = 'developer' as UserRole, frontendJWT = '' }: LayoutProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [selectedApp, setSelectedApp] = useState<AppModule>('');
const [effectiveUserRole, setEffectiveUserRole] = useState<UserRole>(userRole);
const [effectiveFrontendJWT, setEffectiveFrontendJWT] = useState<string>(frontendJWT);
const matches = useMatches() as Match[];
const location = useLocation();
// 检查当前路径是否应该隐藏侧边栏
const noLayoutPaths = ['/login', '/'];
const shouldHideSidebar = noLayoutPaths.includes(location.pathname);
// 检查当前路由是否应该隐藏默认面包屑
const shouldHideBreadcrumb = shouldHideSidebar || matches.some(match =>
match.handle && match.handle.hideBreadcrumb === true
);
// 从 localStorage 读取用户信息和 JWT 作为备用方案
useEffect(() => {
if (typeof window === 'undefined') return;
try {
// 如果服务端没有传递 userRole,从 localStorage 读取
if (!userRole || userRole === '') {
const storedUserInfoStr = localStorage.getItem('user_info');
if (storedUserInfoStr) {
const storedUserInfo = JSON.parse(storedUserInfoStr);
const storedUserRole = storedUserInfo.user_role || 'common';
console.log('📖 [Layout] 从 localStorage 读取用户角色:', storedUserRole);
setEffectiveUserRole(storedUserRole as UserRole);
}
} else {
setEffectiveUserRole(userRole);
}
// 如果服务端没有传递 frontendJWT,从 localStorage 读取
if (!frontendJWT || frontendJWT === '') {
const storedToken = localStorage.getItem('access_token');
if (storedToken) {
console.log('📖 [Layout] 从 localStorage 读取 JWT token');
setEffectiveFrontendJWT(storedToken);
}
} else {
setEffectiveFrontendJWT(frontendJWT);
}
} catch (error) {
console.error('❌ [Layout] 读取 localStorage 失败:', error);
}
}, [userRole, frontendJWT]);
// 从sessionStorage中获取侧边栏状态和reviewType
useEffect(() => {
// 检查是否为移动端
const isMobile = window.innerWidth <= 768;
// 从localStorage获取侧边栏状态
const savedState = localStorage.getItem('sidebarCollapsed');
// 移动端默认收起,桌面端使用保存的状态
if (isMobile) {
setSidebarCollapsed(true);
} else if (savedState) {
setSidebarCollapsed(savedState === 'true');
}
// 从sessionStorage获取reviewType并设置对应的应用模块
if (typeof window !== 'undefined') {
try {
const reviewType = sessionStorage.getItem('reviewType');
if (reviewType && REVIEW_TYPE_TO_APP[reviewType]) {
setSelectedApp(REVIEW_TYPE_TO_APP[reviewType]);
}
} catch (error) {
console.error('获取reviewType失败:', error);
}
}
}, []);
// 路由变化时,检查并更新应用模块
useEffect(() => {
if (typeof window !== 'undefined') {
try {
const reviewType = sessionStorage.getItem('reviewType');
// console.log('Layout 路由变化, reviewType:', reviewType, '路径:', location.pathname);
if (reviewType && REVIEW_TYPE_TO_APP[reviewType]) {
setSelectedApp(REVIEW_TYPE_TO_APP[reviewType]);
}
} catch (error) {
console.error('路由变化时获取reviewType失败:', error);
}
}
}, [location.pathname]);
const toggleSidebar = () => {
const newState = !sidebarCollapsed;
setSidebarCollapsed(newState);
localStorage.setItem('sidebarCollapsed', String(newState));
};
// 切换应用模块
// const changeAppModule = (appId: AppModule) => {
// setSelectedApp(appId);
// localStorage.setItem('selectedApp', appId);
// };
// 如果是无布局页面,只渲染内容
if (shouldHideSidebar) {
return <>{children}</>;
}
return (
<div className="layout-container">
{/* 侧边栏始终保留,不再使用条件渲染 */}
<Sidebar
collapsed={sidebarCollapsed}
onToggle={toggleSidebar}
userRole={effectiveUserRole}
selectedApp={selectedApp}
frontendJWT={effectiveFrontendJWT}
/>
<div className={`main-content ${sidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
{/* 应用模块选择器 */}
{/* <div className="app-module-selector py-2 px-4 border-b border-gray-100 flex items-center">
{APP_MODULES.map(app => (
<button
key={app.id}
onClick={() => changeAppModule(app.id as AppModule)}
className={`app-module-btn mr-4 py-2 px-4 rounded-md flex items-center ${
selectedApp === app.id ? 'bg-green-50 text-green-700 border border-green-200' : 'hover:bg-gray-50'
}`}
>
<i className={`${app.icon} mr-2`}></i>
<span>{app.name}</span>
</button>
))}
</div> */}
<div className="content-container">
{!shouldHideBreadcrumb && <Breadcrumb />}
{children}
</div>
</div>
</div>
);
}