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'; import { DOCUMENT_URL, CROSS_CHECKING_ONLY_PORT, CROSS_CHECKING_ONLY_MODE } from '~/config/api-config'; import { normalizeRoutePathForPermission } from '~/utils/route-alias'; interface SidebarProps { onToggle: () => void; collapsed: boolean; userRole: UserRole; frontendJWT?: string; menuItems?: MenuItem[]; } export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', menuItems: initialMenuItems = [] }: SidebarProps) { const location = useLocation(); const [expandedMenus, setExpandedMenus] = useState>({}); const [menuItems, setMenuItems] = useState(initialMenuItems); // 动态菜单项 const [isLoadingRoutes, setIsLoadingRoutes] = useState(initialMenuItems.length === 0); // 路由加载状态 const [isMobile, setIsMobile] = useState(false); // 移动端检测 const [selectedModuleName, setSelectedModuleName] = useState(''); // 当前选中的模块名称 const [selectedModulePicPath, setSelectedModulePicPath] = useState(''); // 当前选中的模块图片路径 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(() => { if (initialMenuItems.length > 0) { setMenuItems(initialMenuItems); setIsLoadingRoutes(false); return; } const fetchUserRoutes = async () => { setIsLoadingRoutes(true); try { 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 未找到,props.frontendJWT:', frontendJWT, 'localStorage.access_token:', localStorage.getItem('access_token')); setMenuItems([]); setIsLoadingRoutes(false); return; } const result = await getUserRoutesByRole(userRole, jwt); if (result.success && result.data) { setMenuItems(result.data); } else { console.error('❌ [Sidebar] 获取用户路由权限失败:', result.error); if (result.shouldRedirectToHome) { navigate('/'); return; } setMenuItems([]); } } catch (error) { console.error('❌ [Sidebar] 获取用户路由权限时发生错误:', error); navigate('/'); return; } finally { setIsLoadingRoutes(false); } }; fetchUserRoutes(); }, [userRole, frontendJWT, navigate, initialMenuItems]); // 🔑 检查是否处于系统设置模式或交叉评查模式 const [isSettingsMode, setIsSettingsMode] = useState(false); const [isCrossCheckingMode, setIsCrossCheckingMode] = useState(false); // 🔒 检测当前端口,用于控制交叉评查入口的显示 const [currentPort, setCurrentPort] = useState(''); const isPathInSection = (prefix: string) => location.pathname === prefix || location.pathname.startsWith(`${prefix}/`); const isSettingsPath = isPathInSection('/settings') || isPathInSection('/entry-modules') || isPathInSection('/role-permissions') || isPathInSection('/document-types') || isPathInSection('/rule-groups'); const isCrossCheckingPath = isPathInSection('/cross-checking'); const isChatPath = isPathInSection('/chat-with-llm'); const isContractTemplatePath = isPathInSection('/contract-template') || isPathInSection('/contract-draft'); const shouldForceDocumentNavigation = isPathInSection('/reviewsTest'); const navigateWithFallback = (path: string) => { if (shouldForceDocumentNavigation && typeof window !== 'undefined') { window.location.assign(path); return; } navigate(path); }; useEffect(() => { if (typeof window !== 'undefined') { setCurrentPort(window.location.port || ''); } }, []); // 从 sessionStorage 读取当前选中的模块名称和图片路径,以及各种模式标志 useEffect(() => { if (typeof window !== 'undefined') { try { const moduleName = sessionStorage.getItem('selectedModuleName'); const modulePicPath = sessionStorage.getItem('selectedModulePicPath'); const settingsMode = sessionStorage.getItem('settingsMode'); const crossCheckingMode = sessionStorage.getItem('crossCheckingMode'); const hasStaleSpecialModule = (moduleName === '智慧法务助手' || moduleName === '交叉评查') && !isChatPath && !isCrossCheckingPath && !isSettingsPath && location.pathname !== '/'; if (hasStaleSpecialModule) { sessionStorage.removeItem('selectedModuleName'); sessionStorage.removeItem('selectedModulePicPath'); setSelectedModuleName(''); setSelectedModulePicPath(''); console.log('🧹 [Sidebar] 已清理残留的特殊入口模块状态:', moduleName); } else { setSelectedModuleName(moduleName || ''); setSelectedModulePicPath(modulePicPath || ''); if (moduleName) { console.log('📌 [Sidebar] 当前选中模块:', moduleName); } if (modulePicPath) { console.log('🖼️ [Sidebar] 模块图片路径:', modulePicPath); } } // 当前路由优先,避免历史 sessionStorage 状态把普通业务页侧边栏劫持成设置页/交叉评查页 if (isSettingsPath) { setIsSettingsMode(true); setIsCrossCheckingMode(false); console.log('⚙️ [Sidebar] 根据当前路由进入系统设置模式'); } else if (isCrossCheckingPath) { setIsCrossCheckingMode(true); setIsSettingsMode(false); console.log('🔀 [Sidebar] 根据当前路由进入交叉评查模式'); } else if (settingsMode === 'true' && location.pathname === '/') { setIsSettingsMode(true); setIsCrossCheckingMode(false); console.log('⚙️ [Sidebar] 首页保留系统设置模式'); } else if (crossCheckingMode === 'true' && location.pathname === '/') { setIsCrossCheckingMode(true); setIsSettingsMode(false); console.log('🔀 [Sidebar] 首页保留交叉评查模式'); } else { setIsSettingsMode(false); setIsCrossCheckingMode(false); // 进入普通业务页时同步清理模式标志,避免后续刷新再次读到脏状态 sessionStorage.removeItem('settingsMode'); sessionStorage.removeItem('crossCheckingMode'); } // 兼容用户直接刷新/直达子页时没有 sessionStorage 场景 if (!moduleName) { if (isChatPath) { setSelectedModuleName('智慧法务助手'); setSelectedModulePicPath('/images/icon_assistant.png'); } else if (isCrossCheckingPath) { setSelectedModuleName('交叉评查'); setSelectedModulePicPath('/images/icon_cross@2x.png'); } else if (isContractTemplatePath) { setSelectedModuleName('合同管理'); } } } catch (error) { console.error('❌ [Sidebar] 读取 sessionStorage 失败:', error); } } }, [isChatPath, isContractTemplatePath, isCrossCheckingPath, location.pathname]); // 路由变化时重新读取 // 初始化展开状态,默认全部展开 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) => { const target = new URL(path, 'http://sidebar.local'); if (target.search) { const currentParams = new URLSearchParams(location.search); return location.pathname === target.pathname && Array.from(target.searchParams.entries()).every( ([key, value]) => currentParams.get(key) === value ); } return location.pathname === target.pathname || location.pathname.startsWith(`${target.pathname}/`); }; // 处理侧边栏切换事件 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); }; const isRuleManagementMenu = (item: MenuItem) => item.id === 'rule-management' || item.path === '/rules' || item.title === '规则管理' || !!item.children?.some(child => child.id === 'rules-list' || child.path === '/rules/list'); const specialModeModuleNames = new Set(['智慧法务助手', '交叉评查']); const shouldHideModuleBanner = isSettingsPath; const effectiveSelectedModuleName = (() => { if (shouldHideModuleBanner) { return ''; } if (isChatPath) { return '智慧法务助手'; } if (isCrossCheckingPath) { return '交叉评查'; } if (isContractTemplatePath && !selectedModuleName) { return '合同管理'; } if (!isChatPath && !isCrossCheckingPath && specialModeModuleNames.has(selectedModuleName)) { return ''; } return selectedModuleName; })(); const effectiveSelectedModulePicPath = (() => { if (shouldHideModuleBanner) { return ''; } if (isChatPath) { return '/images/icon_assistant.png'; } if (isCrossCheckingPath) { return '/images/icon_cross@2x.png'; } if (!isChatPath && !isCrossCheckingPath && specialModeModuleNames.has(selectedModuleName)) { return ''; } return selectedModulePicPath; })(); const isCaseFileModule = effectiveSelectedModuleName.includes('案卷') || effectiveSelectedModuleName.includes('卷宗'); const isAssistantModule = effectiveSelectedModuleName === '智慧法务助手' || isChatPath; const isContractModule = effectiveSelectedModuleName.includes('合同') || isContractTemplatePath; const effectiveSettingsMode = isSettingsMode || isSettingsPath; const effectiveCrossCheckingMode = isCrossCheckingMode || isCrossCheckingPath; const buildRulesTestListPath = (mainType?: string) => { const params = new URLSearchParams(); if (isCaseFileModule) { params.set('documentType', '案卷'); if (mainType) params.set('mainType', mainType); } else if (isContractModule) { params.set('documentType', '合同'); if (mainType) { params.set('mainType', mainType); } } else if (effectiveSelectedModuleName.includes('公文')) { params.set('documentType', '内部公文'); if (mainType) { params.set('mainType', mainType); } } else if (effectiveSelectedModuleName) { params.set('documentType', effectiveSelectedModuleName); params.set('mainType', effectiveSelectedModuleName); } const query = params.toString(); return query ? `/rulesTest/list?${query}` : '/rulesTest/list'; }; const normalizeRuleManagementMenu = (item: MenuItem): MenuItem => { if (!isRuleManagementMenu(item)) { return item; } if (isCaseFileModule) { return { ...item, path: buildRulesTestListPath(), children: undefined }; } return { ...item, children: item.children?.map(child => ( child.id === 'rules-list' || child.path === '/rules' || child.path === '/rules/list' ? { ...child, path: buildRulesTestListPath() } : child )) }; }; // const isPort51707 = typeof window !== 'undefined' && window.location.port === '51707' const dedupeMenuItems = (items: MenuItem[]): MenuItem[] => { const descendantKeys = new Set(); const buildMenuIdentity = (item: MenuItem) => { const normalizedPath = normalizeRoutePathForPermission(item.path || ''); return `${normalizedPath}::${item.title}`; }; const collectDescendantPaths = (children?: MenuItem[]) => { if (!children || children.length === 0) { return; } children.forEach((child) => { descendantKeys.add(buildMenuIdentity(child)); collectDescendantPaths(child.children); }); }; items.forEach((item) => collectDescendantPaths(item.children)); return items.filter((item) => !descendantKeys.has(buildMenuIdentity(item))); }; // 处理菜单项:清理子菜单结构 const processedMenuItems: MenuItem[] = dedupeMenuItems(menuItems.filter(item =>{ // console.log('菜单项:', item.title, 'Icon:', item.icon) // 🔑 优先检查:如果处于系统设置模式,只显示 /settings 及其子路由 if (effectiveSettingsMode) { return item.path === '/settings' || item.path?.startsWith('/settings/'); } // 🔑 优先检查:如果处于交叉评查模式,只显示 /cross-checking 及其子路由 if (effectiveCrossCheckingMode) { return item.path === '/cross-checking' || item.path?.startsWith('/cross-checking/'); } // 🔑 重要:非系统设置模式下,隐藏所有 /settings 相关菜单 if (item.path === '/settings' || item.path?.startsWith('/settings/')) { return false; } // 🔒 交叉评查访问控制: // - CROSS_CHECKING_ONLY_MODE=false 时,所有端口都可访问(根据后端权限) // - CROSS_CHECKING_ONLY_MODE=true 时,只有 51707 端口可访问 if (CROSS_CHECKING_ONLY_MODE && currentPort && currentPort !== CROSS_CHECKING_ONLY_PORT) { if (item.path === '/cross-checking' || item.path?.startsWith('/cross-checking/')) { return false; } } // 🔑 重要:非交叉评查模式下,隐藏所有 /cross-checking 相关菜单 if (item.path === '/cross-checking' || item.path?.startsWith('/cross-checking/')) { return false; } // 如果是省局访问 // if(isPort51707){ // if (selectedModuleName === '智慧法务助手'){ // return item.path && item.path.startsWith('/chat-with-llm') // } // return item.path && item.path.startsWith('/cross-checking') // } // 🔑 如果选择了"智慧法务助手",显示 /chat-with-llm 和 /dataset-manager 相关菜单 if (isAssistantModule) { return item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/') } // 🔑 如果选择了包含"合同"的模块 if (isContractModule) { // 排除智慧法务助手专属菜单 if (item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')) { return false; } // 保留其他所有菜单(包括 /contract-template) return true; } // 🔑 其他模块:排除特殊菜单 // 排除智慧法务助手专属菜单 if (item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')) { return false; } // 排除合同专属菜单 if (item.path === '/contract-template' || item.path?.startsWith('/contract-template/')) { return false; } // 保留其他菜单 return true; }).map(normalizeRuleManagementMenu).map((item): MenuItem => { // 处理子菜单:过滤隐藏的子菜单 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 as MenuItem; } // 如果还有可见的子菜单,返回带过滤后子菜单的项 return { ...item, children: visibleChildren }; } // 处理空 children 数组或 undefined 的情况 if (item.children !== undefined) { // 如果 children 存在但为空数组,移除它(让父级变成可点击的单级菜单) const { children, ...itemWithoutChildren } = item; return itemWithoutChildren as MenuItem; } // 没有子菜单的项直接返回 return item; })); return ( <> {/* 移动端遮罩层 */} {isMobile && !collapsed && (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggle(); } }} /> )}
{ navigateWithFallback('/'); }} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigateWithFallback('/'); } }} > 智慧法务 {!collapsed &&

智慧法务

}
{/* 显示入口模块的名称 */} {effectiveSelectedModuleName && (
{effectiveSelectedModulePicPath && ( {effectiveSelectedModuleName} )} {!collapsed && ( {effectiveSelectedModuleName} )}
)}
{isLoadingRoutes ? ( // 加载中状态显示,保留菜单布局结构
{Array(5).fill(0).map((_, index) => (
{!collapsed &&
}
))}
) : ( // 数据加载完成后显示菜单 processedMenuItems.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}} ))}
)} )}
)) )}
{/* 操作手册下载按钮 */}
); }