Dashboard.tsx 83 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607
  1. import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
  2. import { useNavigate, useLocation } from 'react-router-dom';
  3. import { useTranslation } from 'react-i18next';
  4. import { Shield, Settings, Users, Cpu, MapPin, Layers, Bell, User, LogOut, ChevronDown, Activity, Radio, Lock, AlertCircle, CheckCircle, Clock, Menu, Building2, UserCog, BookOpen, Server, Globe, Gauge, Briefcase, MessageSquare, FileText, FolderTree, KeyRound, HardDrive, Database, Network, Workflow, ClipboardList, Wrench, Archive, Package, Box, Grid3x3, List, LayoutGrid, FolderOpen, FileCheck, FileEdit, FileSearch } from 'lucide-react';
  5. import SystemConfig from './components/SystemConfig';
  6. import UserManagement from './components/UserManagement';
  7. import HardwareManagement from './components/HardwareManagement';
  8. import SegregationPointManagement from './components/SegregationPointManagement';
  9. import IsolationWork from './components/IsolationWork';
  10. import ProfileSettings from './components/ProfileSettings';
  11. import DashboardComponent from './components/Dashboard';
  12. import ExecutorDashboard from './components/ExecutorDashboard';
  13. import NotificationManagement from './components/NotificationManagement';
  14. import FormManagement from './components/FormManagement';
  15. import MyTask from './components/MyTask';
  16. import TaskManagement from './components/TaskManagement';
  17. import MessageNotification from './components/MessageNotification';
  18. import { authApi } from './api';
  19. import { toast } from 'sonner';
  20. import { Toaster } from 'sonner';
  21. import { env } from './utils/env';
  22. import { getMenus, hasMenuPathPermission, mapMenuPathToKey, getPermissionUser, hasRole } from './utils/permission';
  23. import { Dropdown } from 'antd';
  24. import type { MenuProps } from 'antd';
  25. export default function Dashboard() {
  26. const navigate = useNavigate();
  27. const location = useLocation();
  28. const { t, i18n } = useTranslation();
  29. // 监听语言变化,强制组件重新渲染
  30. useEffect(() => {
  31. // 当语言切换时,这个 effect 会重新执行,从而触发组件更新
  32. }, [i18n.language]);
  33. // 从 URL hash 解析菜单状态
  34. const parseHashToMenuState = (hash: string): { menu: string; subMenu: string } | null => {
  35. if (!hash || hash === '#') return null;
  36. const cleanHash = hash.replace(/^#/, '');
  37. const parts = cleanHash.split('-');
  38. if (parts.length >= 2) {
  39. return { menu: parts[0], subMenu: parts.slice(1).join('-') };
  40. } else if (parts.length === 1 && parts[0]) {
  41. return { menu: parts[0], subMenu: '' };
  42. }
  43. return null;
  44. };
  45. // 初始化时从 hash 读取菜单状态
  46. const initialMenuState = parseHashToMenuState(window.location.hash);
  47. const [activeMenu, setActiveMenu] = useState(initialMenuState?.menu || 'dashboard');
  48. const [activeSubMenu, setActiveSubMenu] = useState(initialMenuState?.subMenu || 'menuManagement');
  49. // 更新 URL hash 的函数(不触发页面刷新)
  50. const updateUrlHash = useCallback((menu: string, subMenu: string) => {
  51. const newHash = subMenu ? `#${menu}-${subMenu}` : `#${menu}`;
  52. // 使用 pushState 添加历史记录,这样浏览器回退按钮可以工作
  53. if (window.location.hash !== newHash) {
  54. window.history.pushState(null, '', newHash);
  55. }
  56. }, []);
  57. // 封装 setActiveMenu 和 setActiveSubMenu,同时更新 URL hash
  58. const handleMenuChange = useCallback((menu: string, subMenu: string) => {
  59. setActiveMenu(menu);
  60. setActiveSubMenu(subMenu);
  61. updateUrlHash(menu, subMenu);
  62. }, [updateUrlHash]);
  63. // 监听浏览器回退/前进按钮(popstate 事件)
  64. useEffect(() => {
  65. const handlePopState = () => {
  66. const menuState = parseHashToMenuState(window.location.hash);
  67. if (menuState) {
  68. console.log('浏览器回退/前进,恢复菜单状态:', menuState);
  69. setActiveMenu(menuState.menu);
  70. setActiveSubMenu(menuState.subMenu);
  71. } else {
  72. // 如果没有 hash,恢复到默认状态(dashboard)
  73. setActiveMenu('dashboard');
  74. setActiveSubMenu('');
  75. }
  76. };
  77. window.addEventListener('popstate', handlePopState);
  78. return () => {
  79. window.removeEventListener('popstate', handlePopState);
  80. };
  81. }, []);
  82. const [showUserMenu, setShowUserMenu] = useState(false);
  83. const [showDropdownMenu, setShowDropdownMenu] = useState<string | null>(null); // 当前显示下拉菜单的菜单key
  84. const [dropdownTimer, setDropdownTimer] = useState<NodeJS.Timeout | null>(null);
  85. const [showProfileSettings, setShowProfileSettings] = useState(false);
  86. const [avatarError, setAvatarError] = useState(false);
  87. // 获取用户信息
  88. const userInfo = useMemo(() => getPermissionUser(), []);
  89. // 切换语言
  90. const changeLanguage = (lang: 'zh' | 'en') => {
  91. i18n.changeLanguage(lang).then(() => {
  92. // 语言切换后,强制组件更新以确保菜单重新渲染
  93. window.dispatchEvent(new Event('languagechange'));
  94. });
  95. };
  96. // 语言下拉菜单项
  97. const languageMenuItems: MenuProps['items'] = [
  98. {
  99. key: 'zh',
  100. label: '中文',
  101. onClick: () => changeLanguage('zh'),
  102. },
  103. {
  104. key: 'en',
  105. label: 'English',
  106. onClick: () => changeLanguage('en'),
  107. },
  108. ];
  109. // 登出处理
  110. const handleLogout = async () => {
  111. try {
  112. await authApi.logout();
  113. localStorage.removeItem('token');
  114. localStorage.removeItem('tenant');
  115. // 清除所有菜单相关的 sessionStorage
  116. sessionStorage.removeItem('lastActiveMenu');
  117. sessionStorage.removeItem('navigateToMenu');
  118. sessionStorage.removeItem('cabinetDetailSource');
  119. // 退出登录不再弹出全局成功提示(避免频繁/停留过久影响体验)
  120. navigate('/login');
  121. } catch (error: any) {
  122. // 即使API失败也清除本地数据并跳转
  123. localStorage.removeItem('token');
  124. localStorage.removeItem('tenant');
  125. // 清除所有菜单相关的 sessionStorage
  126. sessionStorage.removeItem('lastActiveMenu');
  127. sessionStorage.removeItem('navigateToMenu');
  128. sessionStorage.removeItem('cabinetDetailSource');
  129. navigate('/login');
  130. }
  131. };
  132. // 后端菜单路径到前端菜单key的映射函数
  133. const mapBackendPathToFrontendKey = (path: string): string | null => {
  134. // ===== 通知管理子菜单:优先兜底映射(有些后端路径不包含 /notification)=====
  135. // App 通知
  136. if (path && (path.includes('app-notify-message') || path.includes('appNotify') || path.includes('app_notify'))) {
  137. return 'app';
  138. }
  139. // 站内信
  140. if (path && (path.includes('notify-message') || path.includes('in_site') || path.includes('in-site'))) {
  141. return 'webmail';
  142. }
  143. // 短信日志
  144. if (path && (path.includes('sms-log') || path.includes('smsLog') || path.includes('sms_log'))) {
  145. return 'sms';
  146. }
  147. // 邮件提醒/邮件模板(归到邮件 Tab)- 只匹配明确的通知相关邮件路径
  148. if (path && (
  149. path.includes('mail-notify') ||
  150. path.includes('mailNotify') ||
  151. path.includes('mail_notify') ||
  152. path.includes('email-notify') ||
  153. path.includes('emailNotify') ||
  154. path.includes('email_notify') ||
  155. path.includes('mail-template') ||
  156. path.includes('mailTemplate') ||
  157. path.includes('mail_template')
  158. )) {
  159. return 'email';
  160. }
  161. // 处理简短路径(不带斜杠的路径,如 "menuManagement", "dept", "post" 等)
  162. // 这些通常是系统配置下的子菜单路径
  163. if (!path.startsWith('/')) {
  164. // 系统配置子菜单路径
  165. if (path === 'menuManagement' || path === 'menu') return 'menuManagement';
  166. if (path === 'dept' || path === 'department') return 'departmentManagement';
  167. if (path === 'post' || path === 'marsdept' || path === 'position') return 'positionManagement';
  168. if (path === 'role') return 'roleManagement';
  169. if (path === 'dict' || path === 'dictionary') return 'dictionaryManagement';
  170. if (path === 'cabinet') return 'cabinetManagement';
  171. // 硬件管理子菜单路径
  172. if (path === 'hwcabinet' || path === 'cabinet') return 'cabinet';
  173. if (path === 'key') return 'key';
  174. if (path === 'lock' || path === 'padlock') return 'padlock';
  175. if (path === 'portability' || path === 'portable') return 'portable';
  176. // 隔离作业子菜单路径
  177. if (path === 'processTemplate' || path === 'processTemplate') return 'processTemplate';
  178. if (path === 'job') return 'workManagement';
  179. if (path === 'form') return 'formManagement';
  180. if (path === 'taskmanagement' || path === 'taskManagement') return 'taskManagement';
  181. if (path === 'mytask' || path === 'myTask') return 'myTask';
  182. if (path === 'sop') return 'sopManagement';
  183. }
  184. // 客户端系统路径(支持 /clientSystem/xxx 格式)
  185. if (path.startsWith('/clientSystem')) {
  186. // 隔离作业子菜单路径(优先处理)
  187. if (path.startsWith('/clientSystem/IsolationWork/')) {
  188. if (path.includes('/processDesign') || path.endsWith('/processDesign')) return 'processDesign';
  189. if (path.includes('/sop') || path.endsWith('/sop')) return 'sopManagement';
  190. if (path.includes('/job') || path.endsWith('/job')) return 'workManagement';
  191. if (path.includes('/form') || path.endsWith('/form')) return 'formManagement';
  192. return 'isolationWork';
  193. }
  194. // 其他客户端系统路径
  195. if (path.includes('SystemConfig') || path.endsWith('/SystemConfig')) return 'systemConfig';
  196. if (path.includes('UserManagement') || path.endsWith('/UserManagement')) return 'userManagement';
  197. if (path.includes('HardwareManagement') || path.endsWith('/HardwareManagement')) return 'hardwareManagement';
  198. if (path.includes('LocationManagement') || path.endsWith('/LocationManagement')) return 'locationManagement';
  199. if (path.includes('IsolationWork') || path.endsWith('/IsolationWork')) return 'isolationWork';
  200. if (path.includes('notification') || path.endsWith('/notification')) return 'notificationManagement';
  201. if (path.includes('TaskManagement') || path.endsWith('/TaskManagement') || path.includes('taskManagement') || path.endsWith('/taskManagement')) return 'taskManagement';
  202. }
  203. // 系统管理相关
  204. if (path === '/system' || path.startsWith('/system')) {
  205. if (path.includes('menu') || path.endsWith('/menu') || path.includes('/menuManagement')) return 'menuManagement';
  206. if (path.includes('dept') || path.endsWith('/dept') || path.includes('/department')) return 'departmentManagement';
  207. if (path.includes('post') || path.includes('marsdept') || path.endsWith('/post') || path.endsWith('/marsdept') || path.includes('/position')) return 'positionManagement';
  208. if (path.includes('role') || path.endsWith('/role')) return 'roleManagement';
  209. if (path.includes('dict') || path.endsWith('/dict') || path.includes('/dictionary')) return 'dictionaryManagement';
  210. if (path.includes('cabinet') || path.endsWith('/cabinet')) return 'cabinetManagement';
  211. return 'systemConfig';
  212. }
  213. // 用户管理相关
  214. if (path === '/users' || path.startsWith('/users') || path === '/user' || path.startsWith('/user')) {
  215. if (path.includes('list') || path.endsWith('/list')) return 'userList';
  216. if (path.includes('notification') || path.endsWith('/notification')) return 'notificationManagement';
  217. return 'userManagement';
  218. }
  219. // 通知管理相关(clientSystem 路径)
  220. if (path.includes('/notification') || path.includes('/clientSystem/notification')) {
  221. const p = path.toLowerCase();
  222. // 通知管理二级菜单(优先返回稳定 key,便于 i18n)
  223. if (p.includes('webmail') || p.includes('in_site') || p.includes('in-site') || p.includes('站内信')) return 'webmail';
  224. if (p.includes('sms') || p.includes('短信')) return 'sms';
  225. if (p.includes('email') || p.includes('邮件') || p.includes('mail')) return 'email';
  226. if (p.includes('app') || p.includes('app-notify') || p.includes('app_notify') || p.includes('app通知')) return 'app';
  227. return 'notificationManagement';
  228. }
  229. // 硬件管理相关
  230. if (path === '/hw' || path.startsWith('/hw') || path === '/hardware' || path.startsWith('/hardware')) {
  231. if (path.includes('hwcabinet') || (path.includes('cabinet') && !path.includes('system')) || path.includes('work')) return 'cabinet';
  232. if (path.includes('key') || path.includes('keys')) return 'key';
  233. if (path.includes('lock') || path.includes('lk') || path.includes('padlock')) return 'padlock';
  234. if (path.includes('portability') || path.includes('portable') || path.includes('rfid')) return 'portable';
  235. return 'hardwareManagement';
  236. }
  237. // 任务管理相关(优先处理,避免被隔离作业匹配)
  238. if (path.includes('/task-management') || path.includes('/taskManagement') || path.includes('/admin-work') || path.includes('/adminWork') || path.includes('TaskManagement')) {
  239. return 'taskManagement';
  240. }
  241. // 我的任务相关(优先处理,避免被隔离作业匹配)
  242. if (path.includes('/my-task') || path.includes('/myTask') || path.includes('/my-work') || path.includes('/myWork')) {
  243. return 'myTask';
  244. }
  245. // 隔离作业相关
  246. if (path === '/jobTicket' || path.startsWith('/jobTicket') || path === '/isolation' || path.startsWith('/isolation') || path === '/CustomWorkflow' || path.startsWith('/CustomWorkflow') || path === '/sopm' || path.startsWith('/sopm')) {
  247. if (path.includes('/form') || path.endsWith('/form')) return 'formManagement';
  248. if (path.includes('job') || path.endsWith('/job')) return 'workManagement';
  249. if (path.includes('sop') || path.endsWith('/sop')) return 'sopManagement';
  250. if (path.includes('design') || path.endsWith('/design')) return 'processDesign';
  251. if (path.includes('step') || path.includes('template') || path.includes('process') || path.includes('CW') || path.includes('CS')) return 'processTemplate';
  252. return 'isolationWork';
  253. }
  254. // 我的任务相关
  255. if (path.includes('/my-task') || path.includes('/myTask') || path.endsWith('/my-task') || path.endsWith('/myTask')) {
  256. return 'myTask';
  257. }
  258. // 点位管理
  259. if (path === '/points' || path.startsWith('/points') || path === '/Basicdata' || path.startsWith('/Basicdata')) {
  260. return 'locationManagement';
  261. }
  262. // 驾驶舱
  263. if (path === '/cockpit' || path === '/dashboard' || path === '/') {
  264. return 'dashboard';
  265. }
  266. return null;
  267. };
  268. // 图标名称到组件的映射(用于备用)
  269. const getIconByPath = (path: string): any => {
  270. if (path.includes('system')) return Settings;
  271. if (path.includes('user')) return Users;
  272. if (path.includes('hw') || path.includes('hardware')) return Cpu;
  273. if (path.includes('point') || path.includes('Basicdata')) return MapPin;
  274. if (path.includes('job') || path.includes('isolation') || path.includes('sop') || path.includes('CustomWorkflow')) return Layers;
  275. if (path.includes('cockpit') || path === '/') return Gauge;
  276. if (path.includes('infra')) return Settings;
  277. return Settings; // 默认
  278. };
  279. // 从后端菜单动态生成前端菜单配置
  280. const { mainMenus, subMenuConfig } = useMemo(() => {
  281. const backendMenus = getMenus();
  282. // 调试:打印后端菜单数据
  283. // console.log('后端菜单数据:', backendMenus);
  284. // 如果menus为空数组(接口明确返回空数组),不渲染任何菜单
  285. if (backendMenus !== null && Array.isArray(backendMenus) && backendMenus.length === 0) {
  286. // console.log('menus为空数组,不渲染任何菜单');
  287. return {
  288. mainMenus: [],
  289. subMenuConfig: {}
  290. };
  291. }
  292. // 如果权限信息不存在或menus字段不存在,使用默认菜单(兼容性处理)
  293. if (!backendMenus) {
  294. // console.log('权限信息不存在,使用默认菜单');
  295. return {
  296. mainMenus: [
  297. { key: 'dashboard', icon: Gauge, path: '/dashboard', name: '驾驶舱' },
  298. { key: 'systemConfig', icon: Settings, path: '/system', name: '系统配置' },
  299. { key: 'userManagement', icon: Users, path: '/users', name: '用户管理' },
  300. { key: 'hardwareManagement', icon: Cpu, path: '/hardware', name: '硬件管理' },
  301. { key: 'locationManagement', icon: MapPin, path: '/points', name: '点位管理' },
  302. { key: 'isolationWork', icon: Layers, path: '/isolation', name: '隔离作业' },
  303. ],
  304. subMenuConfig: {
  305. systemConfig: [
  306. { key: 'menuManagement', icon: Menu, path: '/system/menu', name: '菜单管理' },
  307. { key: 'departmentManagement', icon: Building2, path: '/system/dept', name: '部门管理' },
  308. { key: 'positionManagement', icon: Briefcase, path: '/system/post', name: '岗位管理' },
  309. { key: 'roleManagement', icon: UserCog, path: '/system/role', name: '角色管理' },
  310. { key: 'dictionaryManagement', icon: BookOpen, path: '/system/dict', name: '字典管理' },
  311. { key: 'cabinetManagement', icon: Server, path: '/system/cabinet', name: '机柜管理' },
  312. ],
  313. userManagement: [
  314. { key: 'userList', icon: User, path: '/users/list', name: '用户列表' },
  315. { key: 'notificationManagement', icon: Bell, path: '/users/notification', name: '通知管理' },
  316. ],
  317. hardwareManagement: [
  318. { key: 'cabinet', icon: Cpu, path: '/hardware/cabinet', name: '机柜' },
  319. { key: 'key', icon: Lock, path: '/hardware/key', name: '钥匙' },
  320. { key: 'padlock', icon: Lock, path: '/hardware/lock', name: '挂锁' },
  321. { key: 'portable', icon: Radio, path: '/hardware/portable', name: '便携式' },
  322. ],
  323. locationManagement: [],
  324. isolationWork: [
  325. { key: 'processTemplate', icon: Layers, path: '/isolation/approval', name: '流程模板' },
  326. { key: 'sopManagement', icon: BookOpen, path: '/isolation/record', name: 'SOP管理' },
  327. { key: 'workManagement', icon: Activity, path: '/isolation/list', name: '作业管理' },
  328. { key: 'processDesign', icon: Workflow, path: '/jobTicket/design', name: '流程设计' },
  329. { key: 'formManagement', icon: FileText, path: '/jobTicket/form', name: '表单管理' },
  330. ],
  331. }
  332. };
  333. }
  334. // 从后端菜单动态生成
  335. const mainMenuMap: { [key: string]: { key: string; icon: any; path: string; name: string } } = {};
  336. const subMenuMap: { [key: string]: { key: string; icon: any; path: string; name: string }[] } = {};
  337. const processedSubMenuKeys = new Set<string>(); // 用于去重,避免重复添加子菜单
  338. // 根据前端key或菜单名称获取对应的图标
  339. const getIconByKey = (key: string, menuName?: string): any => {
  340. // 首先尝试根据 key 精确匹配
  341. const iconMap: { [key: string]: any } = {
  342. // 主菜单
  343. 'dashboard': Gauge,
  344. 'systemConfig': Settings,
  345. 'userManagement': Users,
  346. 'hardwareManagement': Cpu,
  347. 'locationManagement': MapPin,
  348. 'isolationWork': Layers,
  349. // 系统配置子菜单
  350. 'menuManagement': Menu,
  351. 'departmentManagement': Building2,
  352. 'positionManagement': Briefcase,
  353. 'roleManagement': UserCog,
  354. 'dictionaryManagement': BookOpen,
  355. 'cabinetManagement': Server,
  356. // 用户管理子菜单
  357. 'userList': User,
  358. 'notificationManagement': Bell,
  359. // 硬件管理子菜单
  360. 'cabinet': Server,
  361. 'key': KeyRound,
  362. 'padlock': Lock,
  363. 'portable': Radio,
  364. // 隔离作业子菜单
  365. 'processTemplate': Workflow,
  366. 'sopManagement': ClipboardList,
  367. 'workManagement': Activity,
  368. 'processDesign': Workflow,
  369. 'formManagement': FileText,
  370. };
  371. if (iconMap[key]) {
  372. return iconMap[key];
  373. }
  374. // 如果 key 无法匹配,尝试根据菜单名称推断
  375. if (menuName) {
  376. const name = menuName.toLowerCase();
  377. // 根据名称关键词匹配图标
  378. if (name.includes('菜单') || name.includes('menu')) return Menu;
  379. if (name.includes('部门') || name.includes('dept')) return Building2;
  380. if (name.includes('岗位') || name.includes('position') || name.includes('post')) return Briefcase;
  381. if (name.includes('角色') || name.includes('role')) return UserCog;
  382. if (name.includes('字典') || name.includes('dict')) return BookOpen;
  383. if (name.includes('机柜') || name.includes('cabinet')) return Server;
  384. if (name.includes('用户') || name.includes('user')) return User;
  385. if (name.includes('通知') || name.includes('notification')) return Bell;
  386. if (name.includes('钥匙') || name.includes('key')) return KeyRound;
  387. if (name.includes('挂锁') || name.includes('padlock') || name.includes('lock')) return Lock;
  388. if (name.includes('便携') || name.includes('portable')) return Radio;
  389. if (name.includes('流程') || name.includes('process') || name.includes('template')) return Workflow;
  390. if (name.includes('sop') || name.includes('标准')) return ClipboardList;
  391. if (name.includes('作业') || name.includes('work') || name.includes('job')) return Activity;
  392. if (name.includes('列表') || name.includes('list')) return List;
  393. if (name.includes('管理') || name.includes('manage')) return Settings;
  394. if (name.includes('配置') || name.includes('config')) return Settings;
  395. if (name.includes('数据') || name.includes('data')) return Database;
  396. if (name.includes('文件') || name.includes('file')) return FileText;
  397. if (name.includes('文件夹') || name.includes('folder')) return FolderOpen;
  398. if (name.includes('网络') || name.includes('network')) return Network;
  399. if (name.includes('硬件') || name.includes('hardware')) return Cpu;
  400. if (name.includes('设备') || name.includes('device')) return HardDrive;
  401. if (name.includes('位置') || name.includes('location') || name.includes('point')) return MapPin;
  402. if (name.includes('隔离') || name.includes('isolation')) return Layers;
  403. }
  404. // 最后尝试根据 key 中的关键词推断
  405. const keyLower = key.toLowerCase();
  406. if (keyLower.includes('menu')) return Menu;
  407. if (keyLower.includes('dept') || keyLower.includes('department')) return Building2;
  408. if (keyLower.includes('position') || keyLower.includes('post')) return Briefcase;
  409. if (keyLower.includes('role')) return UserCog;
  410. if (keyLower.includes('dict')) return BookOpen;
  411. if (keyLower.includes('cabinet')) return Server;
  412. if (keyLower.includes('user')) return User;
  413. if (keyLower.includes('notification')) return Bell;
  414. if (keyLower.includes('key')) return KeyRound;
  415. if (keyLower.includes('lock') || keyLower.includes('padlock')) return Lock;
  416. if (keyLower.includes('portable')) return Radio;
  417. if (keyLower.includes('process') || keyLower.includes('template')) return Workflow;
  418. if (keyLower.includes('sop')) return ClipboardList;
  419. if (keyLower.includes('work') || keyLower.includes('job')) return Activity;
  420. if (keyLower.includes('list')) return List;
  421. if (keyLower.includes('hardware') || keyLower.includes('hw')) return Cpu;
  422. if (keyLower.includes('location') || keyLower.includes('point')) return MapPin;
  423. if (keyLower.includes('isolation')) return Layers;
  424. // 默认使用更合适的图标而不是 Settings
  425. return Layers; // 使用 Layers 作为默认图标,比 Settings 更通用
  426. };
  427. // 递归处理菜单
  428. const processMenu = (menu: any, parentKey: string | null = null, isClientParent: boolean = false) => {
  429. // 检查是否是客户端菜单(当前菜单包含"客户端"或者是客户端菜单的子菜单)
  430. const isClientMenu = menu.name && menu.name.includes('客户端');
  431. const shouldProcess = isClientMenu || isClientParent;
  432. // 对于客户端菜单,即使 visible 为 false 也显示;对于非客户端菜单,如果 visible 为 false 则跳过
  433. if (!shouldProcess && menu.visible === false) {
  434. // 如果不是客户端菜单且不可见,跳过
  435. if (menu.children && menu.children.length > 0) {
  436. menu.children.forEach((child: any) => {
  437. processMenu(child, parentKey, false);
  438. });
  439. }
  440. return;
  441. }
  442. // 调试:打印菜单处理信息
  443. // console.log('处理菜单:', {
  444. // name: menu.name,
  445. // path: menu.path,
  446. // parentId: menu.parentId,
  447. // isClientMenu,
  448. // isClientParent,
  449. // shouldProcess
  450. // });
  451. // 只处理客户端菜单或其子菜单
  452. if (!shouldProcess) {
  453. // 如果不是客户端菜单,但可能有子菜单是客户端菜单,继续递归处理子菜单
  454. if (menu.children && menu.children.length > 0) {
  455. menu.children.forEach((child: any) => {
  456. processMenu(child, parentKey, false);
  457. });
  458. }
  459. return;
  460. }
  461. // 尝试映射到已知的key,如果无法映射则使用path作为key(支持客户端菜单)
  462. let frontendKey: string = mapBackendPathToFrontendKey(menu.path) || '';
  463. if (!frontendKey) {
  464. // 如果无法映射,使用path作为key(去除特殊字符)
  465. frontendKey = menu.path.replace(/[^a-zA-Z0-9]/g, '_') || `menu_${menu.id}`;
  466. }
  467. // 获取图标组件 - 使用原来的图标配置,根据key来映射
  468. const IconComponent = getIconByKey(frontendKey);
  469. // 如果是顶级菜单(parentId === 0)
  470. // 排除通知管理,它应该显示在右侧功能区
  471. if (menu.parentId === 0 && frontendKey !== 'notificationManagement') {
  472. if (!mainMenuMap[frontendKey]) {
  473. // 去掉"客户端-"前缀
  474. const displayName = menu.name.replace(/^客户端[-_]\s*/i, '');
  475. mainMenuMap[frontendKey] = {
  476. key: frontendKey,
  477. icon: IconComponent,
  478. path: menu.path,
  479. name: displayName
  480. };
  481. }
  482. // 处理子菜单 - 只在这里处理,避免在递归时重复添加
  483. if (menu.children && menu.children.length > 0) {
  484. if (!subMenuMap[frontendKey]) {
  485. subMenuMap[frontendKey] = [];
  486. }
  487. // console.log(`处理父菜单 "${menu.name}" 的子菜单,共 ${menu.children.length} 个`);
  488. menu.children.forEach((child: any) => {
  489. // 子菜单也需要检查是否包含"客户端",或者父菜单是客户端菜单
  490. const isChildClientMenu = child.name && child.name.includes('客户端');
  491. const isChildShouldProcess = isChildClientMenu || isClientMenu;
  492. // console.log(` 子菜单: ${child.name}, visible: ${child.visible}, isChildShouldProcess: ${isChildShouldProcess}`);
  493. // 对于客户端菜单的子菜单,即使 visible 为 false 也显示
  494. if (!isChildShouldProcess && child.visible === false) {
  495. // console.log(` 跳过: 不是客户端菜单且不可见`);
  496. return;
  497. }
  498. if (!isChildShouldProcess) {
  499. // 如果子菜单不是客户端菜单,且父菜单也不是客户端菜单,跳过
  500. // console.log(` 跳过: 不是客户端菜单的子菜单`);
  501. return;
  502. }
  503. let childKey: string = mapBackendPathToFrontendKey(child.path) || '';
  504. console.log('处理子菜单:', { path: child.path, mappedKey: childKey });
  505. if (!childKey) {
  506. // 如果无法映射,尝试根据路径推断
  507. // 优先处理 clientSystem/IsolationWork 路径
  508. if (child.path.startsWith('/clientSystem/IsolationWork/')) {
  509. if (child.path.includes('/processDesign') || child.path.endsWith('/processDesign')) {
  510. childKey = 'processDesign';
  511. } else if (child.path.includes('/sop') || child.path.endsWith('/sop')) {
  512. childKey = 'sopManagement';
  513. } else if (child.path.includes('/job') || child.path.endsWith('/job')) {
  514. childKey = 'workManagement';
  515. } else if (child.path.includes('/form') || child.path.endsWith('/form')) {
  516. childKey = 'formManagement';
  517. } else {
  518. childKey = 'isolationWork';
  519. }
  520. } else if (child.path.includes('/jobTicket') || child.path.startsWith('/jobTicket')) {
  521. // 处理 /jobTicket 路径
  522. if (child.path.includes('/form') || child.path.endsWith('/form')) {
  523. childKey = 'formManagement';
  524. } else if (child.path.includes('/processDesign') || child.path.endsWith('/design')) {
  525. childKey = 'processDesign';
  526. } else if (child.path.includes('/sop') || child.path.endsWith('/sop')) {
  527. childKey = 'sopManagement';
  528. } else if (child.path.includes('/job') || child.path.endsWith('/job')) {
  529. childKey = 'workManagement';
  530. } else if (child.path.includes('/step') || child.path.includes('/template')) {
  531. childKey = 'processTemplate';
  532. } else {
  533. childKey = 'isolationWork';
  534. }
  535. } else if (child.path.includes('/dept') || child.path === 'dept' || child.path === 'department') {
  536. childKey = 'departmentManagement';
  537. } else if (child.path.includes('/menu') || child.path === 'menuManagement' || child.path === 'menu') {
  538. childKey = 'menuManagement';
  539. } else if (child.path.includes('/post') || child.path.includes('/marsdept') || child.path === 'post' || child.path === 'marsdept' || child.path === 'position') {
  540. childKey = 'positionManagement';
  541. } else if (child.path.includes('/role') || child.path === 'role') {
  542. childKey = 'roleManagement';
  543. } else if (child.path.includes('/dict') || child.path === 'dict' || child.path === 'dictionary') {
  544. childKey = 'dictionaryManagement';
  545. } else if (child.path.includes('/cabinet') || child.path === 'cabinet') {
  546. childKey = 'cabinetManagement';
  547. } else if (child.path.includes('/hw') || child.path.includes('/hardware') || child.path === 'hwcabinet' || child.path === 'cabinet') {
  548. // 硬件管理子菜单
  549. if (child.path.includes('hwcabinet') || child.path === 'hwcabinet' || (child.path.includes('cabinet') && !child.path.includes('system'))) {
  550. childKey = 'cabinet';
  551. } else if (child.path.includes('key') || child.path === 'key') {
  552. childKey = 'key';
  553. } else if (child.path.includes('lock') || child.path === 'lock' || child.path.includes('padlock') || child.path === 'padlock') {
  554. childKey = 'padlock';
  555. } else if (child.path.includes('portability') || child.path === 'portability' || child.path.includes('portable') || child.path === 'portable') {
  556. childKey = 'portable';
  557. } else {
  558. childKey = 'cabinet'; // 默认
  559. }
  560. } else if (child.path === 'processTemplate' || child.path === 'processTemplate' || child.path.includes('processTemplate')) {
  561. // 隔离作业子菜单路径推断
  562. childKey = 'processTemplate';
  563. } else if (child.path === 'job' || child.path.includes('/job') || child.path.endsWith('/job')) {
  564. childKey = 'workManagement';
  565. } else if (child.path === 'form' || child.path.includes('/form') || child.path.endsWith('/form')) {
  566. childKey = 'formManagement';
  567. } else if (child.path === 'taskmanagement' || child.path === 'taskManagement' || child.path.includes('taskmanagement') || child.path.includes('taskManagement')) {
  568. childKey = 'taskManagement';
  569. } else if (child.path === 'mytask' || child.path === 'myTask' || child.path.includes('mytask') || child.path.includes('myTask')) {
  570. childKey = 'myTask';
  571. } else if (child.path === 'sop' || child.path.includes('/sop') || child.path.endsWith('/sop')) {
  572. childKey = 'sopManagement';
  573. } else if (child.path.includes('/notification') || child.path.startsWith('/clientSystem/notification')) {
  574. // 通知管理子菜单
  575. if (child.path.includes('appNotification') || child.path.includes('app-notify') || child.path.includes('app_notify')) {
  576. childKey = 'appNotification';
  577. } else if (child.path.includes('webmail') || child.path.includes('站内信')) {
  578. childKey = 'webmail';
  579. } else if (child.path.includes('sms') || child.path.includes('短信')) {
  580. childKey = 'sms';
  581. } else if (child.path.includes('email') || child.path.includes('邮件')) {
  582. childKey = 'email';
  583. } else {
  584. childKey = 'email'; // 默认
  585. }
  586. } else {
  587. // 如果无法推断,使用path作为key(去除特殊字符)
  588. childKey = child.path.replace(/[^a-zA-Z0-9]/g, '_') || `child_${child.id}`;
  589. }
  590. }
  591. console.log('最终 childKey:', childKey);
  592. // 检查是否已经添加过(去重)
  593. const subMenuKey = `${frontendKey}_${childKey}`;
  594. if (processedSubMenuKeys.has(subMenuKey)) {
  595. // console.log(` 跳过: 子菜单 ${childKey} 已存在`);
  596. return;
  597. }
  598. processedSubMenuKeys.add(subMenuKey);
  599. // 去掉"客户端-"前缀
  600. const childDisplayName = child.name ? child.name.replace(/^客户端[-_]\s*/i, '') : child.name;
  601. // 使用原来的图标配置,根据key和名称来映射(传入名称以便智能推断)
  602. const ChildIcon = getIconByKey(childKey, childDisplayName || child.name);
  603. const subMenuItem = {
  604. key: childKey,
  605. icon: ChildIcon,
  606. path: child.path,
  607. name: childDisplayName
  608. };
  609. // console.log(` 添加子菜单到 ${frontendKey}:`, subMenuItem);
  610. subMenuMap[frontendKey].push(subMenuItem);
  611. });
  612. // console.log(`父菜单 "${menu.name}" 的子菜单数量: ${subMenuMap[frontendKey].length}`);
  613. }
  614. }
  615. // 注意:不再在这里处理子菜单,避免重复添加
  616. // 子菜单已经在 parentId === 0 的分支中处理了
  617. // 递归处理子菜单(传递是否是客户端父菜单的标志)
  618. if (menu.children && menu.children.length > 0) {
  619. menu.children.forEach((child: any) => {
  620. processMenu(child, frontendKey, isClientMenu);
  621. });
  622. }
  623. };
  624. backendMenus.forEach(menu => {
  625. processMenu(menu);
  626. });
  627. const result = {
  628. mainMenus: Object.values(mainMenuMap),
  629. subMenuConfig: subMenuMap
  630. };
  631. // 调试:打印最终生成的菜单
  632. // console.log('=== 菜单生成结果 ===');
  633. // console.log('主菜单数量:', result.mainMenus.length);
  634. // console.log('主菜单列表:', result.mainMenus.map(m => ({ key: m.key, name: m.name })));
  635. // console.log('子菜单配置:', result.subMenuConfig);
  636. // console.log('==================');
  637. return result;
  638. }, [i18n.language]);
  639. // 获取通知管理菜单信息(从后端菜单中查找)
  640. const notificationMenu = useMemo(() => {
  641. const backendMenus = getMenus();
  642. // 如果menus为null或空数组,返回null
  643. if (!backendMenus || backendMenus.length === 0) {
  644. return null;
  645. }
  646. const findNotificationMenu = (menus: any[]): any => {
  647. for (const menu of menus) {
  648. if (menu.name && menu.name.includes('客户端') && menu.name.includes('通知')) {
  649. return menu;
  650. }
  651. if (menu.children && menu.children.length > 0) {
  652. const found = findNotificationMenu(menu.children);
  653. if (found) return found;
  654. }
  655. }
  656. return null;
  657. };
  658. return findNotificationMenu(backendMenus);
  659. }, []);
  660. // 将通知管理的子菜单也加入到 subMenuConfig 中,以便在 tab 标签中显示
  661. const enhancedSubMenuConfig = useMemo(() => {
  662. const config = { ...subMenuConfig };
  663. // 如果找到通知管理菜单,将其子菜单添加到配置中
  664. if (notificationMenu && notificationMenu.children) {
  665. const notificationSubMenus = notificationMenu.children
  666. // 放宽过滤:即使后端标记不可见,只要是通知子项(尤其 APP 通知)也加入 Tab
  667. .filter((child: any) => {
  668. const name: string = child?.name || '';
  669. const path: string = child?.path || '';
  670. const lower = `${name} ${path}`.toLowerCase();
  671. const isAppNotify = lower.includes('app') || lower.includes('app-notify') || lower.includes('app_notify') || lower.includes('app通知');
  672. return child.visible !== false || (name && name.includes('客户端')) || isAppNotify;
  673. })
  674. .map((child: any) => {
  675. const rawKey = mapBackendPathToFrontendKey(child.path) || child.path.replace(/[^a-zA-Z0-9]/g, '_') || `child_${child.id}`;
  676. const childDisplayName = child.name ? child.name.replace(/^客户端[-_]\s*/i, '') : child.name;
  677. // 通知管理二级菜单 key 归一化:确保 i18n 命中(英文切换时不会回退到中文 name)
  678. const lower = `${childDisplayName || ''} ${child.path || ''}`.toLowerCase();
  679. let childKey = rawKey;
  680. if (lower.includes('app') || lower.includes('app-notify') || lower.includes('app_notify') || lower.includes('app通知')) {
  681. childKey = 'app';
  682. } else if (lower.includes('webmail') || lower.includes('in_site') || lower.includes('in-site') || lower.includes('站内信')) {
  683. childKey = 'webmail';
  684. } else if (lower.includes('sms') || lower.includes('sms-log') || lower.includes('短信')) {
  685. childKey = 'sms';
  686. } else if (lower.includes('email') || lower.includes('mail') || lower.includes('邮件')) {
  687. childKey = 'email';
  688. }
  689. // 获取图标 - 需要在 useMemo 外部定义 getIconByKey,或者在这里重新定义
  690. // 由于 getIconByKey 在 useMemo 内部,我们需要在这里重新实现图标映射逻辑
  691. let ChildIcon = Bell; // 默认图标
  692. const iconMap: { [key: string]: any } = {
  693. 'userList': User,
  694. 'notificationManagement': Bell,
  695. 'menuManagement': Menu,
  696. 'departmentManagement': Building2,
  697. 'positionManagement': Briefcase,
  698. 'roleManagement': UserCog,
  699. 'dictionaryManagement': BookOpen,
  700. 'cabinetManagement': Server,
  701. 'cabinet': Server,
  702. 'key': KeyRound,
  703. 'padlock': Lock,
  704. 'portable': Radio,
  705. 'processTemplate': Workflow,
  706. 'sopManagement': ClipboardList,
  707. 'workManagement': Activity,
  708. 'processDesign': Workflow,
  709. 'formManagement': FileText,
  710. };
  711. if (iconMap[childKey]) {
  712. ChildIcon = iconMap[childKey];
  713. } else if (childDisplayName) {
  714. const name = childDisplayName.toLowerCase();
  715. if (name.includes('用户') || name.includes('user')) ChildIcon = User;
  716. else if (name.includes('通知') || name.includes('notification')) ChildIcon = Bell;
  717. else if (name.includes('列表') || name.includes('list')) ChildIcon = List;
  718. else if (name.includes('流程') || name.includes('process')) ChildIcon = Workflow;
  719. else if (name.includes('作业') || name.includes('work')) ChildIcon = Activity;
  720. }
  721. return {
  722. key: childKey,
  723. icon: ChildIcon,
  724. name: childDisplayName,
  725. path: child.path
  726. };
  727. });
  728. if (notificationSubMenus.length > 0) {
  729. // 去重:后端可能存在多个“邮件/APP”等子菜单,归一化后会出现重复 key(例如两个 Email)
  730. const uniqueNotificationSubMenus = Array.from(
  731. new Map(notificationSubMenus.map((item) => [item.key, item])).values()
  732. );
  733. config['notificationManagement'] = uniqueNotificationSubMenus;
  734. }
  735. }
  736. return config;
  737. }, [subMenuConfig, notificationMenu]);
  738. // 过滤后的主菜单(已经根据后端菜单过滤了)
  739. const filteredMainMenus = mainMenus;
  740. // 过滤后的二级菜单配置(已经根据后端菜单过滤了,包含通知管理)
  741. // 隐藏隔离作业模块下的SOP管理功能tab
  742. const filteredSubMenuConfig = useMemo(() => {
  743. const config = { ...enhancedSubMenuConfig };
  744. if (config.isolationWork) {
  745. config.isolationWork = config.isolationWork.filter(item => item.key !== 'sopManagement');
  746. }
  747. return config;
  748. }, [enhancedSubMenuConfig]);
  749. // 监听路径变化,处理详情页显示和菜单状态更新
  750. useEffect(() => {
  751. // 如果路径是 /dashboard,检查是否有从其他页面跳转过来的菜单信息(navigateToMenu)
  752. // 注意:只有在有 navigateToMenu 时才恢复菜单状态,避免新登录用户读取到旧的 lastActiveMenu
  753. if (location.pathname === '/dashboard') {
  754. // 首先检查 URL hash,如果有 hash,说明用户是直接访问带 hash 的 URL 或浏览器回退
  755. const hashState = parseHashToMenuState(window.location.hash);
  756. if (hashState && hashState.menu) {
  757. console.log('从 URL hash 恢复菜单状态:', hashState);
  758. // 不需要重新设置状态,因为 useState 初始化时已经从 hash 读取了
  759. // 但如果是 useEffect 触发的(比如路由变化),需要设置
  760. if (activeMenu !== hashState.menu || activeSubMenu !== hashState.subMenu) {
  761. setActiveMenu(hashState.menu);
  762. setActiveSubMenu(hashState.subMenu);
  763. }
  764. return;
  765. }
  766. // 优先检查 navigateToMenu(登录后跳转首菜单或从其他页面跳转)
  767. const navigateToMenu = sessionStorage.getItem('navigateToMenu');
  768. if (navigateToMenu) {
  769. try {
  770. const menuInfo = JSON.parse(navigateToMenu);
  771. if (menuInfo.menu && menuInfo.subMenu) {
  772. console.log('从其他页面跳转,恢复菜单状态:', menuInfo);
  773. setActiveMenu(menuInfo.menu);
  774. setActiveSubMenu(menuInfo.subMenu);
  775. // 同时更新 URL hash
  776. updateUrlHash(menuInfo.menu, menuInfo.subMenu);
  777. sessionStorage.removeItem('navigateToMenu');
  778. return;
  779. }
  780. } catch (e) {
  781. console.error('解析菜单信息失败:', e);
  782. sessionStorage.removeItem('navigateToMenu');
  783. }
  784. }
  785. // 若接口未返回 /dashboard 路由,则不强制展示驾驶舱:切到接口返回的第一个菜单
  786. const hasDashboardMenu = filteredMainMenus.some((m: { key: string }) => m.key === 'dashboard');
  787. if (!hasDashboardMenu && filteredMainMenus.length > 0 && activeMenu === 'dashboard') {
  788. const first = filteredMainMenus[0];
  789. const subs = filteredSubMenuConfig[first.key];
  790. const sub = (subs && subs.length > 0) ? subs[0].key : first.key;
  791. setActiveMenu(first.key);
  792. setActiveSubMenu(sub);
  793. // 同时更新 URL hash
  794. updateUrlHash(first.key, sub);
  795. return;
  796. }
  797. return;
  798. }
  799. // 根据路径更新菜单状态
  800. const path = location.pathname;
  801. const menuKey = mapBackendPathToFrontendKey(path);
  802. console.log('路径变化处理:', { path, menuKey, filteredSubMenuConfig: Object.keys(filteredSubMenuConfig) });
  803. if (menuKey) {
  804. console.log('根据路径更新菜单:', { path, menuKey });
  805. // 如果是系统配置的子菜单,需要设置对应的子菜单key
  806. if (menuKey === 'systemConfig' || menuKey === 'departmentManagement' || menuKey === 'menuManagement' ||
  807. menuKey === 'positionManagement' || menuKey === 'roleManagement' || menuKey === 'dictionaryManagement' ||
  808. menuKey === 'cabinetManagement') {
  809. // 确定是哪个子菜单
  810. let subMenuKey = 'menuManagement'; // 默认值
  811. if (path.includes('/dept') || menuKey === 'departmentManagement') {
  812. subMenuKey = 'departmentManagement';
  813. } else if (path.includes('/menu') || menuKey === 'menuManagement') {
  814. subMenuKey = 'menuManagement';
  815. } else if (path.includes('/post') || menuKey === 'positionManagement') {
  816. subMenuKey = 'positionManagement';
  817. } else if (path.includes('/role') || menuKey === 'roleManagement') {
  818. subMenuKey = 'roleManagement';
  819. } else if (path.includes('/dict') || menuKey === 'dictionaryManagement') {
  820. subMenuKey = 'dictionaryManagement';
  821. } else if (path.includes('/cabinet') || menuKey === 'cabinetManagement') {
  822. subMenuKey = 'cabinetManagement';
  823. }
  824. console.log('设置系统配置子菜单:', { menuKey: 'systemConfig', subMenuKey });
  825. setActiveMenu('systemConfig');
  826. setActiveSubMenu(subMenuKey);
  827. } else if (menuKey === 'cabinet' || menuKey === 'key' || menuKey === 'padlock' || menuKey === 'portable') {
  828. // 硬件管理的子菜单
  829. setActiveMenu('hardwareManagement');
  830. setActiveSubMenu(menuKey);
  831. } else if (menuKey === 'myTask') {
  832. // 我的任务
  833. console.log('设置我的任务菜单:', { menuKey: 'myTask' });
  834. setActiveMenu('isolationWork');
  835. setActiveSubMenu('myTask');
  836. } else if (menuKey === 'taskManagement' || menuKey === 'task-management' || menuKey === 'taskmanagement') {
  837. // 任务管理
  838. console.log('设置任务管理菜单:', { menuKey });
  839. setActiveMenu('taskManagement');
  840. setActiveSubMenu('taskManagement');
  841. } else if (menuKey === 'formManagement' || menuKey === 'processDesign' || menuKey === 'sopManagement' ||
  842. menuKey === 'workManagement' || menuKey === 'processTemplate') {
  843. // 隔离作业的子菜单
  844. setActiveMenu('isolationWork');
  845. setActiveSubMenu(menuKey);
  846. } else if (menuKey === 'processDesign' || menuKey === 'sopManagement' || menuKey === 'workManagement' || menuKey === 'formManagement') {
  847. // 隔离作业的子菜单
  848. console.log('设置隔离作业子菜单:', { menuKey: 'isolationWork', subMenuKey: menuKey });
  849. setActiveMenu('isolationWork');
  850. setActiveSubMenu(menuKey);
  851. } else if (menuKey === 'notificationManagement') {
  852. // 通知管理的子菜单
  853. let subMenuKey = '';
  854. // 根据路径判断是哪个子菜单
  855. if (path.includes('appNotification') || path.includes('app-notify') || path.includes('app_notify')) {
  856. // 查找 appNotification 相关的子菜单
  857. const appSubMenu = filteredSubMenuConfig[menuKey]?.find(item =>
  858. item.key.toLowerCase().includes('app') ||
  859. item.path.toLowerCase().includes('appnotification') ||
  860. item.name?.toLowerCase().includes('app')
  861. );
  862. subMenuKey = appSubMenu?.key || 'appNotification';
  863. } else if (path.includes('webmail') || path.includes('站内信')) {
  864. const webmailSubMenu = filteredSubMenuConfig[menuKey]?.find(item =>
  865. item.key.toLowerCase().includes('webmail') ||
  866. item.name?.toLowerCase().includes('站内信')
  867. );
  868. subMenuKey = webmailSubMenu?.key || 'webmail';
  869. } else if (path.includes('sms') || path.includes('短信')) {
  870. const smsSubMenu = filteredSubMenuConfig[menuKey]?.find(item =>
  871. item.key.toLowerCase().includes('sms') ||
  872. item.name?.toLowerCase().includes('短信')
  873. );
  874. subMenuKey = smsSubMenu?.key || 'sms';
  875. } else if (path.includes('email') || path.includes('邮件')) {
  876. const emailSubMenu = filteredSubMenuConfig[menuKey]?.find(item =>
  877. item.key.toLowerCase().includes('email') ||
  878. item.name?.toLowerCase().includes('邮件')
  879. );
  880. subMenuKey = emailSubMenu?.key || 'email';
  881. }
  882. // 如果找到了子菜单,设置它;否则设置第一个子菜单
  883. if (subMenuKey && filteredSubMenuConfig[menuKey]?.find(item => item.key === subMenuKey)) {
  884. setActiveMenu(menuKey);
  885. setActiveSubMenu(subMenuKey);
  886. } else if (filteredSubMenuConfig[menuKey] && filteredSubMenuConfig[menuKey].length > 0) {
  887. setActiveMenu(menuKey);
  888. setActiveSubMenu(filteredSubMenuConfig[menuKey][0].key);
  889. }
  890. } else if (filteredSubMenuConfig[menuKey] && filteredSubMenuConfig[menuKey].length > 0) {
  891. // 其他菜单,设置第一个子菜单
  892. setActiveMenu(menuKey);
  893. setActiveSubMenu(filteredSubMenuConfig[menuKey][0].key);
  894. } else if (menuKey === 'isolationWork') {
  895. // 如果只是隔离作业主菜单,设置第一个子菜单
  896. if (filteredSubMenuConfig.isolationWork && filteredSubMenuConfig.isolationWork.length > 0) {
  897. setActiveMenu('isolationWork');
  898. setActiveSubMenu(filteredSubMenuConfig.isolationWork[0].key);
  899. }
  900. }
  901. }
  902. // eslint-disable-next-line react-hooks/exhaustive-deps
  903. }, [location.pathname, filteredSubMenuConfig, filteredMainMenus, activeMenu]);
  904. // 监听自定义事件,用于在同一页面切换菜单
  905. useEffect(() => {
  906. const handleSwitchToMenu = (event: CustomEvent) => {
  907. const { menu, subMenu } = event.detail;
  908. if (menu && subMenu) {
  909. console.log('收到切换菜单事件:', { menu, subMenu });
  910. // 使用 handleMenuChange 更新菜单状态并同步更新 URL hash
  911. handleMenuChange(menu, subMenu);
  912. }
  913. };
  914. window.addEventListener('switchToMenu', handleSwitchToMenu as EventListener);
  915. return () => {
  916. window.removeEventListener('switchToMenu', handleSwitchToMenu as EventListener);
  917. };
  918. }, [handleMenuChange]);
  919. // 根据URL路径初始化菜单状态(仅在首次加载时执行)
  920. useEffect(() => {
  921. // 检查是否有从其他页面跳转过来的菜单信息
  922. const navigateToMenu = sessionStorage.getItem('navigateToMenu');
  923. if (navigateToMenu) {
  924. try {
  925. const menuInfo = JSON.parse(navigateToMenu);
  926. if (menuInfo.menu && menuInfo.subMenu) {
  927. setActiveMenu(menuInfo.menu);
  928. setActiveSubMenu(menuInfo.subMenu);
  929. sessionStorage.removeItem('navigateToMenu');
  930. return;
  931. }
  932. } catch (e) {
  933. console.error('解析菜单信息失败:', e);
  934. sessionStorage.removeItem('navigateToMenu');
  935. }
  936. }
  937. // 只在路径不是 /dashboard 时才根据路径初始化(因为应用使用状态管理而非路由)
  938. // 如果路径是 /dashboard,则使用默认状态
  939. if (location.pathname === '/dashboard') {
  940. return; // 使用默认状态
  941. }
  942. const path = location.pathname;
  943. const menuKey = mapBackendPathToFrontendKey(path);
  944. if (menuKey) {
  945. console.log('根据路径初始化菜单:', { path, menuKey });
  946. // 如果是系统配置的子菜单,需要设置对应的子菜单key
  947. if (menuKey === 'systemConfig' || menuKey === 'departmentManagement' || menuKey === 'menuManagement' ||
  948. menuKey === 'positionManagement' || menuKey === 'roleManagement' || menuKey === 'dictionaryManagement' ||
  949. menuKey === 'cabinetManagement') {
  950. // 确定是哪个子菜单
  951. let subMenuKey = 'menuManagement'; // 默认值
  952. if (path.includes('/dept') || menuKey === 'departmentManagement') {
  953. subMenuKey = 'departmentManagement';
  954. } else if (path.includes('/menu') || menuKey === 'menuManagement') {
  955. subMenuKey = 'menuManagement';
  956. } else if (path.includes('/post') || menuKey === 'positionManagement') {
  957. subMenuKey = 'positionManagement';
  958. } else if (path.includes('/role') || menuKey === 'roleManagement') {
  959. subMenuKey = 'roleManagement';
  960. } else if (path.includes('/dict') || menuKey === 'dictionaryManagement') {
  961. subMenuKey = 'dictionaryManagement';
  962. } else if (path.includes('/cabinet') || menuKey === 'cabinetManagement') {
  963. subMenuKey = 'cabinetManagement';
  964. }
  965. console.log('设置系统配置子菜单:', { menuKey: 'systemConfig', subMenuKey });
  966. setActiveMenu('systemConfig');
  967. setActiveSubMenu(subMenuKey);
  968. } else if (menuKey === 'cabinet' || menuKey === 'key' || menuKey === 'padlock' || menuKey === 'portable') {
  969. // 硬件管理的子菜单
  970. setActiveMenu('hardwareManagement');
  971. setActiveSubMenu(menuKey);
  972. } else if (menuKey === 'processDesign' || menuKey === 'sopManagement' || menuKey === 'workManagement' || menuKey === 'formManagement') {
  973. // 隔离作业的子菜单
  974. console.log('设置隔离作业子菜单:', { menuKey: 'isolationWork', subMenuKey: menuKey });
  975. setActiveMenu('isolationWork');
  976. setActiveSubMenu(menuKey);
  977. } else if (menuKey === 'formManagement' || menuKey === 'processDesign' || menuKey === 'sopManagement' ||
  978. menuKey === 'workManagement' || menuKey === 'processTemplate') {
  979. // 隔离作业的子菜单
  980. setActiveMenu('isolationWork');
  981. setActiveSubMenu(menuKey);
  982. } else if (filteredSubMenuConfig[menuKey] && filteredSubMenuConfig[menuKey].length > 0) {
  983. // 其他菜单,设置第一个子菜单
  984. setActiveMenu(menuKey);
  985. setActiveSubMenu(filteredSubMenuConfig[menuKey][0].key);
  986. }
  987. }
  988. // eslint-disable-next-line react-hooks/exhaustive-deps
  989. }, []); // 只在组件挂载时执行一次
  990. // 监听从用户管理跳转到岗位管理的事件
  991. useEffect(() => {
  992. const handleSwitchToPostManagement = (event: CustomEvent) => {
  993. const userId = event.detail?.userId;
  994. if (userId) {
  995. // 将用户ID存储到 sessionStorage
  996. sessionStorage.setItem('selectedUserId', String(userId));
  997. }
  998. // 切换到岗位管理页面,使用 handleMenuChange 更新菜单状态并同步更新 URL hash
  999. handleMenuChange('systemConfig', 'positionManagement');
  1000. };
  1001. window.addEventListener('switchToPostManagement', handleSwitchToPostManagement as EventListener);
  1002. return () => {
  1003. window.removeEventListener('switchToPostManagement', handleSwitchToPostManagement as EventListener);
  1004. };
  1005. }, [handleMenuChange]);
  1006. return (
  1007. <div className="h-screen flex flex-col bg-gradient-to-br from-gray-50 via-blue-50/30 to-gray-50 overflow-hidden">
  1008. {/* 顶部导航栏 */}
  1009. <nav className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 flex-shrink-0 z-50 shadow-sm">
  1010. <div className="px-6 py-4 min-w-0">
  1011. <div className="flex items-center justify-between gap-4 min-w-0">
  1012. {/* Logo区域 */}
  1013. <div className="flex items-center gap-4 lg:gap-8 flex-shrink-0 min-w-0">
  1014. <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
  1015. <div className="relative flex-shrink-0">
  1016. <div className="w-11 h-11 rounded-xl flex items-center justify-center shadow-lg shadow-blue-400/40" style={{ background: 'linear-gradient(135deg, #4d79f8 0%, #6b8ffb 100%)' }}>
  1017. <Shield className="w-6 h-6 text-white" strokeWidth={2.5} />
  1018. </div>
  1019. <div className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-green-400 rounded-full border-2 border-white"></div>
  1020. </div>
  1021. <div className="min-w-0">
  1022. <h1 className="text-lg text-gray-900 truncate">{env.appTitle}</h1>
  1023. <p className="text-xs text-gray-500 truncate">{t('login.subtitle')}</p>
  1024. </div>
  1025. </div>
  1026. {/* 主菜单 */}
  1027. <div className="hidden lg:flex items-center gap-2 ml-2 lg:ml-4 flex-shrink min-w-0">
  1028. {filteredMainMenus.map((item) => {
  1029. const Icon = item.icon;
  1030. const isActive = activeMenu === item.key;
  1031. // 只有系统配置显示下拉菜单,其他菜单的二级菜单在页面内用 tab 标签显示
  1032. if (item.key === 'systemConfig' && filteredSubMenuConfig[item.key] && filteredSubMenuConfig[item.key].length > 0) {
  1033. const isDropdownOpen = showDropdownMenu === item.key;
  1034. const buttonRef = useRef<HTMLButtonElement>(null);
  1035. const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number } | null>(null);
  1036. // 计算下拉菜单位置
  1037. useEffect(() => {
  1038. if (isDropdownOpen && buttonRef.current) {
  1039. const updatePosition = () => {
  1040. if (buttonRef.current) {
  1041. const rect = buttonRef.current.getBoundingClientRect();
  1042. setDropdownPosition({
  1043. top: rect.bottom + 8,
  1044. left: rect.left,
  1045. });
  1046. }
  1047. };
  1048. updatePosition();
  1049. window.addEventListener('scroll', updatePosition, true);
  1050. window.addEventListener('resize', updatePosition);
  1051. return () => {
  1052. window.removeEventListener('scroll', updatePosition, true);
  1053. window.removeEventListener('resize', updatePosition);
  1054. };
  1055. } else {
  1056. setDropdownPosition(null);
  1057. }
  1058. }, [isDropdownOpen]);
  1059. return (
  1060. <>
  1061. <div
  1062. key={item.key}
  1063. className="relative"
  1064. onMouseEnter={() => {
  1065. if (dropdownTimer) clearTimeout(dropdownTimer);
  1066. setShowDropdownMenu(item.key);
  1067. }}
  1068. onMouseLeave={() => {
  1069. // 鼠标离开按钮时立即隐藏(如果鼠标进入悬浮框,悬浮框的onMouseEnter会保持显示)
  1070. if (dropdownTimer) clearTimeout(dropdownTimer);
  1071. setShowDropdownMenu(null);
  1072. }}
  1073. >
  1074. <button
  1075. ref={buttonRef}
  1076. className={`flex items-center gap-2 px-4 py-2.5 rounded-xl transition-all duration-300 ${
  1077. isActive
  1078. ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
  1079. : 'text-gray-600 hover:bg-blue-50 hover:text-blue-600'
  1080. }`}
  1081. >
  1082. <Icon className={`w-4 h-4 ${isActive ? 'text-white' : ''}`} strokeWidth={2.5} />
  1083. <span className={`text-sm ${isActive ? 'text-white' : ''}`}>
  1084. {t(`nav.${item.key}`, item.name || item.key)}
  1085. </span>
  1086. <ChevronDown className={`w-3 h-3 transition-transform ${isDropdownOpen ? 'rotate-180' : ''} ${isActive ? 'text-white' : ''}`} />
  1087. </button>
  1088. </div>
  1089. {/* 下拉菜单 - 使用 fixed 定位,避免被父容器裁剪 */}
  1090. {isDropdownOpen && dropdownPosition && (
  1091. <>
  1092. {/* 连接区域 - 填充按钮和下拉菜单之间的间隙 */}
  1093. <div
  1094. className="fixed z-[99]"
  1095. style={{
  1096. top: `${dropdownPosition.top - 8}px`,
  1097. left: `${dropdownPosition.left}px`,
  1098. width: '340px',
  1099. height: '8px',
  1100. }}
  1101. onMouseEnter={() => {
  1102. // 鼠标进入连接区域时保持显示
  1103. if (dropdownTimer) clearTimeout(dropdownTimer);
  1104. setShowDropdownMenu(item.key);
  1105. }}
  1106. onMouseLeave={() => {
  1107. // 鼠标离开连接区域时立即隐藏
  1108. if (dropdownTimer) clearTimeout(dropdownTimer);
  1109. setShowDropdownMenu(null);
  1110. }}
  1111. />
  1112. <div
  1113. className="fixed bg-white rounded-xl shadow-xl border border-gray-200/50 p-3 animate-in fade-in slide-in-from-top-2 duration-200 z-[100]"
  1114. style={{
  1115. top: `${dropdownPosition.top}px`,
  1116. left: `${dropdownPosition.left}px`,
  1117. width: '340px',
  1118. }}
  1119. onMouseEnter={() => {
  1120. // 鼠标进入悬浮框时保持显示
  1121. if (dropdownTimer) clearTimeout(dropdownTimer);
  1122. setShowDropdownMenu(item.key);
  1123. }}
  1124. onMouseLeave={() => {
  1125. // 鼠标离开悬浮框时立即隐藏
  1126. if (dropdownTimer) clearTimeout(dropdownTimer);
  1127. setShowDropdownMenu(null);
  1128. }}
  1129. >
  1130. <div className="grid grid-cols-2 gap-2">
  1131. {(filteredSubMenuConfig[item.key] || []).map((subItem) => {
  1132. const IconComponent = subItem.icon;
  1133. const isActive = activeMenu === item.key && activeSubMenu === subItem.key;
  1134. return (
  1135. <button
  1136. key={subItem.key}
  1137. onClick={() => {
  1138. console.log('点击子菜单:', { menuKey: item.key, subMenuKey: subItem.key });
  1139. // 如果当前在个人中心页面,先关闭个人中心
  1140. if (showProfileSettings) {
  1141. setShowProfileSettings(false);
  1142. }
  1143. // 如果当前在详情页,导航回对应的菜单页面
  1144. if (location.pathname.startsWith('/lock-cabinet/detail')) {
  1145. navigate('/dashboard');
  1146. }
  1147. // 保存菜单状态到 sessionStorage
  1148. sessionStorage.setItem('lastActiveMenu', JSON.stringify({
  1149. menu: item.key,
  1150. subMenu: subItem.key
  1151. }));
  1152. // 使用 handleMenuChange 更新菜单状态并同步更新 URL hash
  1153. handleMenuChange(item.key, subItem.key);
  1154. setShowDropdownMenu(null);
  1155. }}
  1156. className={`flex items-center gap-2.5 px-3 py-3 rounded-lg text-sm transition-all ${
  1157. isActive
  1158. ? 'bg-blue-50 text-blue-600 shadow-sm'
  1159. : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
  1160. }`}
  1161. >
  1162. <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
  1163. isActive ? 'bg-blue-100' : 'bg-gray-100'
  1164. }`}>
  1165. <IconComponent className="w-4 h-4" strokeWidth={2.5} />
  1166. </div>
  1167. <span>
  1168. {t(`systemConfig.${subItem.key}`, subItem.name || subItem.key)}
  1169. </span>
  1170. </button>
  1171. );
  1172. })}
  1173. </div>
  1174. </div>
  1175. </>
  1176. )}
  1177. </>
  1178. );
  1179. }
  1180. // 其他普通菜单(用户管理、硬件管理、隔离作业等)- 它们的二级菜单在页面内用 tab 标签显示
  1181. return (
  1182. <button
  1183. key={item.key}
  1184. onClick={() => {
  1185. console.log('点击主菜单:', { menuKey: item.key, hasSubMenus: !!filteredSubMenuConfig[item.key] });
  1186. // 如果当前在个人中心页面,先关闭个人中心
  1187. if (showProfileSettings) {
  1188. setShowProfileSettings(false);
  1189. }
  1190. // 保存菜单状态到 sessionStorage
  1191. const firstSubMenu = filteredSubMenuConfig[item.key]?.[0];
  1192. if (firstSubMenu) {
  1193. sessionStorage.setItem('lastActiveMenu', JSON.stringify({
  1194. menu: item.key,
  1195. subMenu: firstSubMenu.key
  1196. }));
  1197. }
  1198. // 如果当前在详情页,导航回对应的菜单页面
  1199. if (location.pathname.startsWith('/lock-cabinet/detail')) {
  1200. navigate('/dashboard');
  1201. }
  1202. // 任务管理菜单如果没有子菜单,设置 activeSubMenu 为 'taskManagement'
  1203. // 使用 handleMenuChange 更新菜单状态并同步更新 URL hash
  1204. if (item.key === 'taskManagement' && !firstSubMenu) {
  1205. handleMenuChange('taskManagement', 'taskManagement');
  1206. } else {
  1207. handleMenuChange(item.key, firstSubMenu?.key || '');
  1208. }
  1209. }}
  1210. className={`flex items-center gap-2 px-4 py-2.5 rounded-xl transition-all duration-300 ${
  1211. isActive
  1212. ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
  1213. : 'text-gray-600 hover:bg-blue-50 hover:text-blue-600'
  1214. }`}
  1215. >
  1216. <Icon className={`w-4 h-4 ${isActive ? 'text-white' : ''}`} strokeWidth={2.5} />
  1217. <span className={`text-sm ${isActive ? 'text-white' : ''}`}>
  1218. {t(`nav.${item.key}`, item.name || item.key)}
  1219. </span>
  1220. </button>
  1221. );
  1222. })}
  1223. </div>
  1224. </div>
  1225. {/* 右侧功能区 */}
  1226. <div className="flex items-center gap-2 lg:gap-4 flex-shrink-0">
  1227. {/* 语言切换 - 鼠标悬停显示,移开自动收起 */}
  1228. <Dropdown
  1229. menu={{ items: languageMenuItems }}
  1230. trigger={['hover']}
  1231. placement="bottomRight"
  1232. mouseEnterDelay={0}
  1233. mouseLeaveDelay={0.15}
  1234. >
  1235. <button
  1236. className="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 rounded-xl transition-colors group"
  1237. >
  1238. {/* <Globe className="w-4 h-4 text-gray-600 group-hover:rotate-12 transition-transform" /> */}
  1239. {/* <span className="text-sm text-gray-700">{i18n.language === 'zh' ? '中文' : 'English'}</span> */}
  1240. {/* <span className="text-sm text-gray-700">语言切换</span> */}
  1241. {/* <ChevronDown className="w-3 h-3 text-gray-500" /> */}
  1242. <svg t="1770183289750" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5073" width="24" height="24" >
  1243. <path d="M789.312 426.688L977.088 896H885.12l-51.2-128h-174.528l-51.2 128H516.352L704 426.688h85.312zM426.688 85.312v85.376h256V256H598.72A777.472 777.472 0 0 1 444.16 524.8c30.784 27.52 64.192 51.84 99.712 72.896l-32 80.128A728.96 728.96 0 0 1 384 585.664a711.68 711.68 0 0 1-264.512 151.296l-22.976-82.304A627.2 627.2 0 0 0 323.84 524.928a771.328 771.328 0 0 1-120.448-183.616H298.88c23.808 43.968 52.352 85.12 85.056 122.816C437.312 402.56 479.36 332.16 508.16 256H85.312V170.688h256V85.312h85.376z m320 464.448l-53.184 132.928h106.24l-53.12-132.928z" fill="#515151" p-id="5074"></path>
  1244. </svg>
  1245. </button>
  1246. </Dropdown>
  1247. {/* 消息通知 - 显示未读消息红点和消息列表 */}
  1248. <MessageNotification />
  1249. {/* 通知管理 - 点击后进入页面,二级菜单在 tab 标签中显示 */}
  1250. {notificationMenu && (
  1251. <div className="relative group">
  1252. <button
  1253. className="relative p-2.5 hover:bg-gray-100 rounded-xl transition-colors"
  1254. title={t('nav.notificationManagement', '通知管理')}
  1255. // title=""
  1256. onClick={() => {
  1257. // 如果当前在个人中心页面,先关闭个人中心
  1258. if (showProfileSettings) {
  1259. setShowProfileSettings(false);
  1260. }
  1261. const firstSubMenu = filteredSubMenuConfig['notificationManagement']?.[0]?.key || '';
  1262. // 使用 handleMenuChange 更新菜单状态并同步更新 URL hash
  1263. handleMenuChange('notificationManagement', firstSubMenu);
  1264. }}
  1265. >
  1266. <MessageSquare className="w-5 h-5 text-gray-600" />
  1267. </button>
  1268. {/* Tooltip - 显示菜单名称 */}
  1269. <div className="absolute right-0 top-full mt-2 px-3 py-1.5 bg-gray-900 text-white text-xs rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-50">
  1270. {t('nav.notificationManagement', '通知管理')}
  1271. </div>
  1272. </div>
  1273. )}
  1274. {/* 用户菜单 */}
  1275. <div
  1276. className="relative"
  1277. onMouseEnter={() => {
  1278. // 清除可能存在的隐藏定时器
  1279. if (dropdownTimer) {
  1280. clearTimeout(dropdownTimer);
  1281. setDropdownTimer(null);
  1282. }
  1283. setShowUserMenu(true);
  1284. }}
  1285. onMouseLeave={() => {
  1286. // 延迟隐藏,给用户时间移动到下拉菜单
  1287. const timer = setTimeout(() => {
  1288. setShowUserMenu(false);
  1289. }, 200);
  1290. setDropdownTimer(timer);
  1291. }}
  1292. >
  1293. <button
  1294. onClick={() => setShowUserMenu(!showUserMenu)}
  1295. className="flex items-center gap-3 px-3 py-2 hover:bg-gray-100 rounded-xl transition-colors"
  1296. >
  1297. {userInfo?.avatar && !avatarError ? (
  1298. <img
  1299. src={userInfo.avatar}
  1300. alt={userInfo.nickname || userInfo.username || '用户'}
  1301. className="w-9 h-9 rounded-lg object-cover shadow-lg border-2 border-white"
  1302. onError={() => setAvatarError(true)}
  1303. />
  1304. ) : (
  1305. <div className="w-9 h-9 bg-gradient-to-br from-blue-400 to-blue-500 rounded-lg flex items-center justify-center shadow-lg shadow-blue-400/30">
  1306. <User className="w-5 h-5 text-white" strokeWidth={2.5} />
  1307. </div>
  1308. )}
  1309. <div className="text-left hidden md:block">
  1310. <div className="text-sm text-gray-900">{userInfo?.nickname || userInfo?.username || '用户'}</div>
  1311. <div className="text-xs text-gray-500">{userInfo?.username || 'User'}</div>
  1312. </div>
  1313. <ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${showUserMenu ? 'rotate-180' : ''}`} />
  1314. </button>
  1315. {/* 下拉菜单 */}
  1316. {showUserMenu && (
  1317. <div
  1318. className="absolute right-0 mt-2 w-48 bg-white rounded-xl shadow-xl border border-gray-200/50 py-2 animate-in fade-in slide-in-from-top-2 duration-200 z-50"
  1319. onMouseEnter={() => {
  1320. // 鼠标进入下拉菜单时,清除隐藏定时器
  1321. if (dropdownTimer) {
  1322. clearTimeout(dropdownTimer);
  1323. setDropdownTimer(null);
  1324. }
  1325. setShowUserMenu(true);
  1326. }}
  1327. onMouseLeave={() => {
  1328. // 鼠标离开下拉菜单时,延迟隐藏
  1329. const timer = setTimeout(() => {
  1330. setShowUserMenu(false);
  1331. }, 200);
  1332. setDropdownTimer(timer);
  1333. }}
  1334. >
  1335. <button
  1336. onClick={() => {
  1337. setShowProfileSettings(true);
  1338. setShowUserMenu(false);
  1339. }}
  1340. className="w-full px-4 py-2.5 text-left text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600 flex items-center gap-2 transition-colors"
  1341. >
  1342. <User className="w-4 h-4" />
  1343. {t('nav.profile')}
  1344. </button>
  1345. <hr className="my-2 border-gray-200" />
  1346. <button className="w-full px-4 py-2.5 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2 transition-colors" onClick={handleLogout}>
  1347. <LogOut className="w-4 h-4" />
  1348. {t('nav.logout')}
  1349. </button>
  1350. </div>
  1351. )}
  1352. </div>
  1353. </div>
  1354. </div>
  1355. </div>
  1356. </nav>
  1357. {/* 主内容区 */}
  1358. <div className="flex-1 flex flex-col overflow-hidden">
  1359. <div className="px-6 pt-6 flex-shrink-0">
  1360. {/* 二级菜单 Tab - 只要有子菜单(>=1)且不是系统配置时显示,并且不在个人资料页面时显示 */}
  1361. {!showProfileSettings &&
  1362. filteredSubMenuConfig[activeMenu]?.length >= 1 &&
  1363. activeMenu !== 'systemConfig' &&
  1364. activeMenu !== 'userManagement' && (
  1365. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-2 mb-6">
  1366. <div className="flex items-center gap-2 overflow-x-auto min-w-0">
  1367. {filteredSubMenuConfig[activeMenu]?.map((item) => {
  1368. const isActive = activeSubMenu === item.key;
  1369. // 根据主菜单key确定翻译键前缀
  1370. let menuKey = '';
  1371. if (activeMenu === 'userManagement') {
  1372. menuKey = 'userManagement';
  1373. } else if (activeMenu === 'hardwareManagement') {
  1374. menuKey = 'hardwareManagement';
  1375. } else if (activeMenu === 'isolationWork' || item.key === 'processTemplate' || item.key === 'sopManagement' || item.key === 'workManagement' || item.key === 'processDesign' || item.key === 'formManagement' || item.key === 'myTask' || item.key === 'taskManagement') {
  1376. menuKey = 'isolationWork';
  1377. } else if (activeMenu === 'notificationManagement') {
  1378. menuKey = 'notificationManagement';
  1379. } else if (activeMenu === 'systemConfig') {
  1380. menuKey = 'systemConfig';
  1381. } else {
  1382. // 默认尝试使用 activeMenu 作为翻译键前缀
  1383. menuKey = activeMenu;
  1384. }
  1385. return (
  1386. <button
  1387. key={item.key}
  1388. onClick={() => {
  1389. // 保存菜单状态到 sessionStorage
  1390. sessionStorage.setItem('lastActiveMenu', JSON.stringify({
  1391. menu: activeMenu,
  1392. subMenu: item.key
  1393. }));
  1394. // 使用 handleMenuChange 更新菜单状态并同步更新 URL hash
  1395. handleMenuChange(activeMenu, item.key);
  1396. }}
  1397. className={`px-3 lg:px-4 py-2 lg:py-2.5 rounded-xl text-xs lg:text-sm transition-all duration-200 whitespace-nowrap flex-shrink-0 ${
  1398. isActive
  1399. ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
  1400. : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
  1401. }`}
  1402. >
  1403. <span className={isActive ? 'text-white' : ''}>
  1404. {(() => {
  1405. // 优先尝试使用对应的翻译键
  1406. let translationKey = '';
  1407. if (menuKey) {
  1408. translationKey = `${menuKey}.${item.key}`;
  1409. } else if (activeMenu === 'isolationWork') {
  1410. // 如果是隔离作业的子菜单,使用 isolationWork
  1411. translationKey = `isolationWork.${item.key}`;
  1412. } else if (activeMenu === 'notificationManagement') {
  1413. // 如果是通知管理的子菜单,尝试使用 userManagement(因为通知管理通常在用户管理下)
  1414. translationKey = `userManagement.${item.key}`;
  1415. } else {
  1416. // 默认尝试使用 activeMenu 作为前缀
  1417. translationKey = `${activeMenu}.${item.key}`;
  1418. }
  1419. // 尝试翻译,如果翻译不存在则使用 item.name
  1420. const translation = t(translationKey, item.name || item.key);
  1421. // 如果翻译结果等于键本身(说明翻译不存在),使用后备值
  1422. return translation === translationKey ? (item.name || item.key) : translation;
  1423. })()}
  1424. </span>
  1425. </button>
  1426. );
  1427. })}
  1428. </div>
  1429. </div>
  1430. )}
  1431. </div>
  1432. {/* 主内容区域 - 可滚动 */}
  1433. <div className="flex-1 px-6 pb-6 overflow-auto">
  1434. {showProfileSettings ? (
  1435. <ProfileSettings onBack={() => setShowProfileSettings(false)} />
  1436. ) : activeMenu === 'dashboard' ? (
  1437. hasRole('super_admin') ? <DashboardComponent /> : <ExecutorDashboard />
  1438. ) : activeMenu === 'systemConfig' ? (
  1439. <SystemConfig subMenu={activeSubMenu} />
  1440. ) : activeMenu === 'userManagement' ? (
  1441. <UserManagement subMenu={activeSubMenu} />
  1442. ) : activeMenu === 'hardwareManagement' ? (
  1443. <HardwareManagement subMenu={activeSubMenu} />
  1444. ) : activeMenu === 'locationManagement' ? (
  1445. <SegregationPointManagement />
  1446. ) : activeMenu === 'isolationWork' ? (
  1447. (() => {
  1448. // 调试信息
  1449. console.log('检查隔离作业子菜单:', { activeMenu, activeSubMenu, location: location.pathname, filteredSubMenuConfig: filteredSubMenuConfig[activeMenu] });
  1450. // 支持 'formManagement' 和 'form' 两种 key
  1451. if (activeSubMenu === 'formManagement' || activeSubMenu === 'form' || location.pathname.includes('/form')) {
  1452. console.log('✅ 渲染 FormManagement 组件', { activeMenu, activeSubMenu });
  1453. return <FormManagement />;
  1454. } else if (activeSubMenu === 'myTask' || activeSubMenu === 'mytask' || location.pathname.includes('/my-task') || location.pathname.includes('/myTask')) {
  1455. console.log('✅ 渲染 MyTask 组件', { activeMenu, activeSubMenu, pathname: location.pathname });
  1456. return <MyTask />;
  1457. } else if (activeSubMenu === 'taskManagement' || activeSubMenu === 'taskmanagement' || activeSubMenu === 'task-management' || location.pathname.includes('/task-management') || location.pathname.includes('/taskManagement')) {
  1458. console.log('✅ 渲染 TaskManagement 组件(从隔离作业子菜单)', { activeMenu, activeSubMenu, pathname: location.pathname });
  1459. return <TaskManagement />;
  1460. } else {
  1461. const subMenuName = filteredSubMenuConfig[activeMenu]?.find(item => item.key === activeSubMenu)?.name || activeSubMenu;
  1462. console.log('⚠️ 渲染 IsolationWork 组件', { activeMenu, activeSubMenu, subMenuName });
  1463. return <IsolationWork subMenu={subMenuName} />;
  1464. }
  1465. })()
  1466. ) : activeMenu === 'myTask' || location.pathname.includes('/my-task') ? (
  1467. <MyTask />
  1468. ) : activeMenu === 'taskManagement' || activeSubMenu === 'taskManagement' || location.pathname.includes('/task-management') || location.pathname.includes('/taskManagement') ? (
  1469. (() => {
  1470. console.log('✅ 渲染 TaskManagement 组件', { activeMenu, activeSubMenu, pathname: location.pathname });
  1471. return <TaskManagement />;
  1472. })()
  1473. ) : activeMenu === 'notificationManagement' ? (
  1474. (() => {
  1475. const subMenuName = filteredSubMenuConfig[activeMenu]?.find(item => item.key === activeSubMenu)?.name || activeSubMenu;
  1476. return <NotificationManagement subMenu={subMenuName} />;
  1477. })()
  1478. ) : (
  1479. // 无法映射的菜单(如客户端菜单)显示占位内容
  1480. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-8">
  1481. <div className="text-center">
  1482. <div className="text-gray-400 mb-4">
  1483. <Settings className="w-16 h-16 mx-auto" />
  1484. </div>
  1485. <h3 className="text-lg text-gray-900 mb-2">
  1486. {mainMenus.find(m => m.key === activeMenu)?.name || '功能开发中'}
  1487. </h3>
  1488. <p className="text-sm text-gray-500">
  1489. 该功能正在开发中,敬请期待
  1490. </p>
  1491. </div>
  1492. </div>
  1493. )}
  1494. </div>
  1495. </div>
  1496. <Toaster position="top-center" richColors />
  1497. </div>
  1498. );
  1499. }