664 lines
25 KiB
TypeScript
664 lines
25 KiB
TypeScript
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;
|
||
}
|
||
|
||
export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: SidebarProps) {
|
||
const location = useLocation();
|
||
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({});
|
||
const [menuItems, setMenuItems] = useState<MenuItem[]>([]); // 动态菜单项
|
||
const [isLoadingRoutes, setIsLoadingRoutes] = useState<boolean>(true); // 路由加载状态
|
||
const [isMobile, setIsMobile] = useState<boolean>(false); // 移动端检测
|
||
const [selectedModuleName, setSelectedModuleName] = useState<string>(''); // 当前选中的模块名称
|
||
const [selectedModulePicPath, setSelectedModulePicPath] = useState<string>(''); // 当前选中的模块图片路径
|
||
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(() => {
|
||
// console.log('🔍 [Sidebar] 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 未找到,props.frontendJWT:', frontendJWT, 'localStorage.access_token:', localStorage.getItem('access_token'));
|
||
setMenuItems([]);
|
||
setIsLoadingRoutes(false);
|
||
return;
|
||
}
|
||
|
||
// console.log('🔍 [Sidebar] 当前用户角色:', userRole, 'JWT前20字符:', jwt.substring(0, 20));
|
||
// console.log('🔍 [Sidebar] 映射后的角色key:', roleKey);
|
||
const result = await getUserRoutesByRole(userRole, 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]);
|
||
|
||
// 🔑 检查是否处于系统设置模式或交叉评查模式
|
||
const [isSettingsMode, setIsSettingsMode] = useState<boolean>(false);
|
||
const [isCrossCheckingMode, setIsCrossCheckingMode] = useState<boolean>(false);
|
||
|
||
// 🔒 检测当前端口,用于控制交叉评查入口的显示
|
||
const [currentPort, setCurrentPort] = useState<string>('');
|
||
|
||
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<string, boolean> = {};
|
||
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,
|
||
children: [
|
||
{
|
||
id: 'rules-admin-penalty',
|
||
title: '行政处罚',
|
||
path: buildRulesTestListPath('行政处罚'),
|
||
icon: 'ri-list-check-3',
|
||
order: 1
|
||
},
|
||
{
|
||
id: 'rules-admin-license',
|
||
title: '行政许可',
|
||
path: buildRulesTestListPath('行政许可'),
|
||
icon: 'ri-list-check-3',
|
||
order: 2
|
||
}
|
||
]
|
||
};
|
||
}
|
||
|
||
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<string>();
|
||
|
||
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 && (
|
||
<div
|
||
className="sidebar-overlay"
|
||
onClick={onToggle}
|
||
role="button"
|
||
tabIndex={0}
|
||
aria-label="关闭侧边栏"
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
onToggle();
|
||
}
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
<div className={`sidebar ${collapsed ? 'collapsed' : ''} ${isMobile ? 'sidebar-mobile' : ''} flex flex-col`}>
|
||
<div className="py-6 px-4 border-b border-gray-100 flex justify-between items-center">
|
||
<div className="flex items-center"
|
||
onClick={() => {
|
||
navigateWithFallback('/');
|
||
}}
|
||
role="button"
|
||
tabIndex={0}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
navigateWithFallback('/');
|
||
}
|
||
}}
|
||
>
|
||
<img src="/logo.svg" alt="智慧法务" className="w-12 h-12 mr-2" />
|
||
{!collapsed && <h2 className="text-lg font-medium">智慧法务</h2>}
|
||
</div>
|
||
<button
|
||
className="sidebar-toggle"
|
||
onClick={handleToggleSidebar}
|
||
aria-label={collapsed ? "展开侧边栏" : "折叠侧边栏"}
|
||
type="button"
|
||
>
|
||
<i className={`${collapsed ? 'ri-menu-unfold-line' : 'ri-menu-fold-line'}`}></i>
|
||
</button>
|
||
</div>
|
||
|
||
{/* 显示入口模块的名称 */}
|
||
{effectiveSelectedModuleName && (
|
||
<div className="py-3 px-4 border-b border-gray-100">
|
||
<div className={`flex items-center ${collapsed ? 'justify-center' : ''}`}>
|
||
{effectiveSelectedModulePicPath && (
|
||
<img
|
||
src={effectiveSelectedModuleName === '智慧法务助手' || effectiveSelectedModuleName === '交叉评查' ? effectiveSelectedModulePicPath : `${DOCUMENT_URL}${effectiveSelectedModulePicPath}`}
|
||
alt={effectiveSelectedModuleName}
|
||
className={`${collapsed ? 'w-8 h-8' : 'w-6 h-6 mr-3'}`}
|
||
/>
|
||
)}
|
||
{!collapsed && (
|
||
<span className="text-base font-medium text-green-700">{effectiveSelectedModuleName}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="py-4 px-[10px] flex-1 overflow-y-auto sidebar-scroll-area">
|
||
{isLoadingRoutes ? (
|
||
// 加载中状态显示,保留菜单布局结构
|
||
<div className="py-2">
|
||
{Array(5).fill(0).map((_, index) => (
|
||
<div key={index} className="sidebar-menu-item-skeleton mb-2">
|
||
<div className="flex items-center">
|
||
<div className="bg-gray-200 rounded-md h-5 w-5 mr-3 animate-pulse"></div>
|
||
{!collapsed && <div className="bg-gray-200 rounded-md h-4 w-24 animate-pulse"></div>}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
// 数据加载完成后显示菜单
|
||
processedMenuItems.map((item) => (
|
||
<div key={item.id} className={`${collapsed ? 'px-0' : ''}`}>
|
||
{!item.children ? (
|
||
<Link
|
||
to={item.path}
|
||
reloadDocument={shouldForceDocumentNavigation}
|
||
className={`sidebar-menu-item ${isActive(item.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
|
||
onClick={(e) => {
|
||
// 只阻止冒泡,不阻止默认行为
|
||
e.stopPropagation();
|
||
// console.log('单级菜单点击:', item.title, '路径:', item.path);
|
||
}}
|
||
>
|
||
<i className={`${item.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
|
||
{!collapsed && <span>{item.title}</span>}
|
||
</Link>
|
||
) : (
|
||
<>
|
||
<div
|
||
className={`sidebar-menu-item flex items-center ${collapsed ? 'justify-center' : 'justify-between'} cursor-pointer z-10`}
|
||
onClick={(e) => {
|
||
// 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);
|
||
}
|
||
}}
|
||
>
|
||
<div className="flex items-center">
|
||
<i className={`${item.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
|
||
{!collapsed && <span>{item.title}</span>}
|
||
</div>
|
||
{!collapsed && (
|
||
<i className={`ri-arrow-${expandedMenus[item.id] ? 'down' : 'right'}-s-line`}></i>
|
||
)}
|
||
</div>
|
||
|
||
{(expandedMenus[item.id] || collapsed) && (
|
||
<div
|
||
className={`submenu-container ${collapsed ? 'border-l-0 pl-0' : 'border-l border-gray-100 ml-4 pl-3'} z-20`}
|
||
id={`submenu-${item.id}`}
|
||
>
|
||
{item.children.map((child) => (
|
||
<Link
|
||
key={child.id}
|
||
to={child.path}
|
||
reloadDocument={shouldForceDocumentNavigation}
|
||
className={`sidebar-menu-item ${isActive(child.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
|
||
onClick={(e) => handleSubMenuClick(child, e)}
|
||
>
|
||
<i className={`${child.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
|
||
{!collapsed && <span>{child.title}</span>}
|
||
</Link>
|
||
))}
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
{/* 操作手册下载按钮 */}
|
||
<div className="mt-auto px-4 py-1 border-t border-gray-100">
|
||
<a
|
||
href="/智慧法务平台操作手册.pdf"
|
||
download="智慧法务平台操作手册.pdf"
|
||
className={`flex items-center ${collapsed ? 'justify-center' : ''} text-gray-600 hover:text-green-700 transition-colors duration-200`}
|
||
title="下载操作手册"
|
||
>
|
||
<i className={`ri-question-line ${collapsed ? 'text-base' : 'text-base mr-3'}`}></i>
|
||
{!collapsed && <span className="text-base">操作手册</span>}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|