import { useState, useEffect } from 'react'; import { Link, useLocation, useNavigate } from '@remix-run/react'; import type { UserRole } from '~/root'; import { getUserRoutesByRole, mapUserRoleToRoleKey, type MenuItem } from '~/api/auth/user-routes'; interface SidebarProps { onToggle: () => void; collapsed: boolean; userRole: UserRole; frontendJWT?: string; selectedApp?: string; // 添加所选应用模块参数 } // 已移除 APP_MENU_MAP:路由的显示/隐藏由后端 is_hidden 字段控制 // 只保留特殊规则: // - /chat-with-llm 只在 model 模块中显示 // - /contract-template 只在 contract 模块中显示 // 应用模块名称映射 const APP_NAME_MAP: Record = { 'contract': '合同管理', 'record': '案卷智能评查', 'model': '智慧法务大模型' }; // 应用模块图标映射 const APP_ICON_MAP: Record = { 'contract': '/images/icon_hetong.png', 'record': '/images/icon_anjuan.png', 'model': '/images/icon_assistant.png' }; export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', selectedApp = '' }: SidebarProps) { const location = useLocation(); const [expandedMenus, setExpandedMenus] = useState>({}); const [currentApp, setCurrentApp] = useState(''); // 初始设置为空字符串而不是selectedApp const [isLoading, setIsLoading] = useState(true); // 添加加载状态 const [menuItems, setMenuItems] = useState([]); // 动态菜单项 const [isLoadingRoutes, setIsLoadingRoutes] = useState(true); // 路由加载状态 const [isMobile, setIsMobile] = useState(false); // 移动端检测 const navigate = useNavigate(); // 移动端检测 useEffect(() => { const checkMobile = () => { const mobile = window.innerWidth <= 768; // 768px以下视为移动端 setIsMobile(mobile); }; // 初始检测 checkMobile(); // 监听窗口大小变化 window.addEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile); }, []); // 获取用户路由权限 useEffect(() => { const fetchUserRoutes = async () => { setIsLoadingRoutes(true); try { // 优先使用传入的 frontendJWT,否则从 localStorage 读取 let jwt = frontendJWT; if (!jwt && typeof window !== 'undefined') { jwt = localStorage.getItem('access_token') || ''; console.log('📖 [Sidebar] 从 localStorage 读取 JWT'); } if (!jwt) { console.error('❌ [Sidebar] JWT token 未找到'); setMenuItems([]); setIsLoadingRoutes(false); return; } console.log('🔍 [Sidebar] 当前用户角色:', userRole); const roleKey = mapUserRoleToRoleKey(userRole); const result = await getUserRoutesByRole(roleKey, jwt); if (result.success && result.data) { setMenuItems(result.data); console.log('✅ [Sidebar] 用户路由权限加载成功:', result.data); } else { console.error('❌ [Sidebar] 获取用户路由权限失败:', result.error); // 如果需要重定向到首页 if (result.shouldRedirectToHome) { console.log('🔄 [Sidebar] 重定向到首页'); navigate('/'); return; } // 其他错误情况,使用空数组 setMenuItems([]); } } catch (error) { console.error('❌ [Sidebar] 获取用户路由权限时发生错误:', error); // 发生异常时也重定向到首页 navigate('/'); return; } finally { setIsLoadingRoutes(false); } }; fetchUserRoutes(); }, [userRole, frontendJWT, navigate]); // 组件挂载后从 sessionStorage 读取初始 reviewType useEffect(() => { let mounted = true; setIsLoading(true); // 设置加载状态 try { const reviewType = sessionStorage.getItem('reviewType'); // console.log('初始 reviewType:', reviewType); if (reviewType && mounted) { setCurrentApp(reviewType); } else if (selectedApp && mounted) { // 如果没有reviewType,但有selectedApp,使用selectedApp setCurrentApp(selectedApp); } } catch (error) { console.error('读取 reviewType 失败:', error); } finally { // 使用setTimeout确保状态在DOM更新后再变化,避免闪烁 setTimeout(() => { if (mounted) { setIsLoading(false); // 数据加载完成 } }, 0); } return () => { mounted = false; }; }, [selectedApp]); // 从 sessionStorage 获取 reviewType 并设置当前应用模块 useEffect(() => { // 监听 sessionStorage 变化(主要用于多标签页情况) const handleStorageChange = (e: StorageEvent) => { if (e.key === 'reviewType' && e.newValue) { setCurrentApp(e.newValue); } }; // 添加事件监听器 window.addEventListener('storage', handleStorageChange); return () => { window.removeEventListener('storage', handleStorageChange); }; }, []); // 监听路由变化,重新检查 reviewType useEffect(() => { let mounted = true; try { const reviewType = sessionStorage.getItem('reviewType'); // 只有当reviewType变化时才设置加载状态和更新currentApp if (reviewType && reviewType !== currentApp && mounted) { setIsLoading(true); // 路由变化时设置加载状态 setCurrentApp(reviewType); // 使用setTimeout确保状态在DOM更新后再变化,避免闪烁 setTimeout(() => { if (mounted) { setIsLoading(false); } }, 0); } } catch (error) { console.error('路由变化时读取 reviewType 失败:', error); if (mounted) { setIsLoading(false); } } return () => { mounted = false; }; }, [location.pathname, currentApp]); // 监听 selectedApp 属性变化 useEffect(() => { if (selectedApp && selectedApp !== currentApp) { setIsLoading(true); // 设置加载状态 setCurrentApp(selectedApp); // 使用setTimeout确保状态在DOM更新后再变化,避免闪烁 setTimeout(() => { setIsLoading(false); // 数据加载完成 }, 0); } }, [selectedApp, currentApp]); // 初始化展开状态,默认全部展开 useEffect(() => { const initialExpandedState: Record = {}; menuItems.forEach(item => { if (item.children) { initialExpandedState[item.id] = true; } }); setExpandedMenus(initialExpandedState); }, [menuItems]); const toggleMenu = (id: string, e: React.MouseEvent) => { // 我们只防止事件冒泡,不阻止默认行为 e.stopPropagation(); // console.log('父菜单展开/折叠:', id); setExpandedMenus(prev => ({ ...prev, [id]: !prev[id] })); }; const isActive = (path: string) => { return location.pathname === path || location.pathname.startsWith(`${path}/`); }; // 处理侧边栏切换事件 const handleToggleSidebar = (e: React.MouseEvent) => { // console.log('侧边栏折叠/展开'); // 只防止事件冒泡,不阻止默认行为 e.stopPropagation(); onToggle(); }; // 处理子菜单项点击事件 const handleSubMenuClick = (child: MenuItem, e: React.MouseEvent) => { // 只需要阻止冒泡,不阻止默认行为 e.stopPropagation(); // console.log('子菜单点击:', child.title, '路径:', child.path); }; // 检查是否通过51707端口访问(省局) const isPort51707 = typeof window !== 'undefined' && window.location.port === '51707'; // 根据当前应用模式过滤菜单项 const filteredMenuItems = menuItems .filter(item => { // 如果是51707端口,只显示交叉评查相关菜单 if (isPort51707) { // 如果当前应用是智慧法务大模型,只显示AI对话菜单 if (currentApp === 'model') { return item.path && item.path.startsWith('/chat-with-llm'); } else { return item.path && item.path.startsWith('/cross-checking'); } } // 特殊规则1:/chat-with-llm 只在 model 模块中显示 if (item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')) { return currentApp === 'model'; } // 特殊规则2:/contract-template 只在 contract 模块中显示 if (item.path === '/contract-template' || item.path?.startsWith('/contract-template/')) { return currentApp === 'contract'; } // 其他路由:后端已通过 is_hidden 控制显示/隐藏,这里全部保留 return true; }) .map(item => { // 处理子菜单:过滤隐藏的子菜单 if (item.children && item.children.length > 0) { // 过滤掉 hideBreadcrumb=true 的子菜单(这些通常是隐藏菜单) const visibleChildren = item.children.filter(child => !child.hideBreadcrumb); // 如果过滤后没有可见的子菜单,返回不带子菜单的父级(变成可点击的单级菜单) if (visibleChildren.length === 0) { const { children, ...itemWithoutChildren } = item; return itemWithoutChildren; } // 如果还有可见的子菜单,返回带过滤后子菜单的项 return { ...item, children: visibleChildren }; } // 处理空 children 数组或 undefined 的情况 if (item.children !== undefined) { // 如果 children 存在但为空数组,移除它(让父级变成可点击的单级菜单) const { children, ...itemWithoutChildren } = item; return itemWithoutChildren; } // 没有子菜单的项直接返回 return item; }); // filteredMenuItems = filteredMenuItems.map(item => { // if(item.children && item.children.length > 0){ // const children = item.children.filter(child => { // const isUploadByPath = child.path === '/files/upload' || child.path?.startsWith('/files/upload') // const isUploadByTitle = child.title === '文件上传' // return !(isUploadByPath || isUploadByTitle) // }) // return { ...item, children} // } // return item // }) return ( <> {/* 移动端遮罩层 */} {isMobile && !collapsed && (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggle(); } }} /> )}
{ navigate('/'); }} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigate('/'); } }} > 智慧法务 {!collapsed &&

智慧法务

}
{!collapsed && (
{isLoading ? ( // 加载中状态,只显示加载图标,保留布局
加载中...
) : ( <> {APP_NAME_MAP[currentApp] {APP_NAME_MAP[currentApp] || ''} )}
)}
{isLoading || isLoadingRoutes ? ( // 加载中状态显示,保留菜单布局结构
{Array(5).fill(0).map((_, index) => (
{!collapsed &&
}
))}
) : ( // 数据加载完成后显示菜单 filteredMenuItems.map((item) => (
{!item.children ? ( { // 只阻止冒泡,不阻止默认行为 e.stopPropagation(); // console.log('单级菜单点击:', item.title, '路径:', item.path); }} > {!collapsed && {item.title}} ) : ( <>
{ // console.log('%c父菜单点击 ===> ', 'background: #722ed1; color: white; padding: 2px 4px; border-radius: 2px;', item.title); toggleMenu(item.id, e); }} role="button" tabIndex={0} aria-expanded={expandedMenus[item.id] || false} aria-controls={`submenu-${item.id}`} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleMenu(item.id, (e as unknown) as React.MouseEvent); } }} >
{!collapsed && {item.title}}
{!collapsed && ( )}
{(expandedMenus[item.id] || collapsed) && (
{item.children.map((child) => ( handleSubMenuClick(child, e)} > {!collapsed && {child.title}} ))}
)} )}
)) )}
{/* 操作手册下载按钮 */}
); }