Dashboard.tsx 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947
  1. import React, { useState, useMemo, useEffect } 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 CockpitDashboard from './components/CockpitDashboard';
  12. import LockCabinetDetail from './components/lockCabinet/LockCabinetDetail';
  13. import NotificationManagement from './components/NotificationManagement';
  14. import { authApi } from './api';
  15. import { toast } from 'sonner';
  16. import { Toaster } from 'sonner';
  17. import { env } from './utils/env';
  18. import { getMenus, hasMenuPathPermission, mapMenuPathToKey, getPermissionUser } from './utils/permission';
  19. export default function Dashboard() {
  20. const navigate = useNavigate();
  21. const location = useLocation();
  22. const { t, i18n } = useTranslation();
  23. const [activeMenu, setActiveMenu] = useState('dashboard');
  24. const [activeSubMenu, setActiveSubMenu] = useState('menuManagement');
  25. const [showUserMenu, setShowUserMenu] = useState(false);
  26. const [showDropdownMenu, setShowDropdownMenu] = useState<string | null>(null); // 当前显示下拉菜单的菜单key
  27. const [dropdownTimer, setDropdownTimer] = useState<NodeJS.Timeout | null>(null);
  28. const [showProfileSettings, setShowProfileSettings] = useState(false);
  29. const [avatarError, setAvatarError] = useState(false);
  30. // 获取用户信息
  31. const userInfo = useMemo(() => getPermissionUser(), []);
  32. // 切换语言
  33. const toggleLanguage = () => {
  34. const newLang = i18n.language === 'zh' ? 'en' : 'zh';
  35. i18n.changeLanguage(newLang);
  36. };
  37. // 登出处理
  38. const handleLogout = async () => {
  39. try {
  40. await authApi.logout();
  41. localStorage.removeItem('token');
  42. localStorage.removeItem('tenant');
  43. toast.success(t('common.success'));
  44. navigate('/login');
  45. } catch (error: any) {
  46. // 即使API失败也清除本地数据并跳转
  47. localStorage.removeItem('token');
  48. localStorage.removeItem('tenant');
  49. navigate('/login');
  50. }
  51. };
  52. // 后端菜单路径到前端菜单key的映射函数
  53. const mapBackendPathToFrontendKey = (path: string): string | null => {
  54. // 客户端系统路径(支持 /clientSystem/xxx 格式)
  55. if (path.startsWith('/clientSystem')) {
  56. if (path.includes('SystemConfig') || path.endsWith('/SystemConfig')) return 'systemConfig';
  57. if (path.includes('UserManagement') || path.endsWith('/UserManagement')) return 'userManagement';
  58. if (path.includes('HardwareManagement') || path.endsWith('/HardwareManagement')) return 'hardwareManagement';
  59. if (path.includes('LocationManagement') || path.endsWith('/LocationManagement')) return 'locationManagement';
  60. if (path.includes('IsolationWork') || path.endsWith('/IsolationWork')) return 'isolationWork';
  61. if (path.includes('notification') || path.endsWith('/notification')) return 'notificationManagement';
  62. }
  63. // 系统管理相关
  64. if (path === '/system' || path.startsWith('/system')) {
  65. if (path.includes('menu') || path.endsWith('/menu')) return 'menuManagement';
  66. if (path.includes('dept') || path.endsWith('/dept')) return 'departmentManagement';
  67. if (path.includes('post') || path.includes('marsdept') || path.endsWith('/post') || path.endsWith('/marsdept')) return 'positionManagement';
  68. if (path.includes('role') || path.endsWith('/role')) return 'roleManagement';
  69. if (path.includes('dict') || path.endsWith('/dict')) return 'dictionaryManagement';
  70. if (path.includes('cabinet') || path.endsWith('/cabinet')) return 'cabinetManagement';
  71. return 'systemConfig';
  72. }
  73. // 用户管理相关
  74. if (path === '/users' || path.startsWith('/users') || path === '/user' || path.startsWith('/user')) {
  75. if (path.includes('list') || path.endsWith('/list')) return 'userList';
  76. if (path.includes('notification') || path.endsWith('/notification')) return 'notificationManagement';
  77. return 'userManagement';
  78. }
  79. // 硬件管理相关
  80. if (path === '/hw' || path.startsWith('/hw') || path === '/hardware' || path.startsWith('/hardware')) {
  81. if (path.includes('cabinet') || path.includes('work')) return 'cabinet';
  82. if (path.includes('key') || path.includes('keys')) return 'key';
  83. if (path.includes('lock') || path.includes('lk') || path.includes('padlock')) return 'padlock';
  84. if (path.includes('portable') || path.includes('rfid')) return 'portable';
  85. return 'hardwareManagement';
  86. }
  87. // 隔离作业相关
  88. if (path === '/jobTicket' || path.startsWith('/jobTicket') || path === '/isolation' || path.startsWith('/isolation') || path === '/CustomWorkflow' || path.startsWith('/CustomWorkflow') || path === '/sopm' || path.startsWith('/sopm')) {
  89. if (path.includes('job') || path.endsWith('/job')) return 'workManagement';
  90. if (path.includes('sop') || path.endsWith('/sop')) return 'sopManagement';
  91. if (path.includes('step') || path.includes('template') || path.includes('process') || path.includes('CW') || path.includes('CS')) return 'processTemplate';
  92. return 'isolationWork';
  93. }
  94. // 点位管理
  95. if (path === '/points' || path.startsWith('/points') || path === '/Basicdata' || path.startsWith('/Basicdata')) {
  96. return 'locationManagement';
  97. }
  98. // 驾驶舱
  99. if (path === '/cockpit' || path === '/dashboard' || path === '/') {
  100. return 'dashboard';
  101. }
  102. return null;
  103. };
  104. // 图标名称到组件的映射(用于备用)
  105. const getIconByPath = (path: string): any => {
  106. if (path.includes('system')) return Settings;
  107. if (path.includes('user')) return Users;
  108. if (path.includes('hw') || path.includes('hardware')) return Cpu;
  109. if (path.includes('point') || path.includes('Basicdata')) return MapPin;
  110. if (path.includes('job') || path.includes('isolation') || path.includes('sop') || path.includes('CustomWorkflow')) return Layers;
  111. if (path.includes('cockpit') || path === '/') return Gauge;
  112. if (path.includes('infra')) return Settings;
  113. return Settings; // 默认
  114. };
  115. // 从后端菜单动态生成前端菜单配置
  116. const { mainMenus, subMenuConfig } = useMemo(() => {
  117. const backendMenus = getMenus();
  118. // 调试:打印后端菜单数据
  119. // console.log('后端菜单数据:', backendMenus);
  120. // 如果没有后端菜单数据,使用默认菜单(兼容性处理)
  121. if (!backendMenus || backendMenus.length === 0) {
  122. // console.log('没有后端菜单数据,使用默认菜单');
  123. return {
  124. mainMenus: [
  125. { key: 'dashboard', icon: Gauge, path: '/dashboard', name: '驾驶舱' },
  126. { key: 'systemConfig', icon: Settings, path: '/system', name: '系统配置' },
  127. { key: 'userManagement', icon: Users, path: '/users', name: '用户管理' },
  128. { key: 'hardwareManagement', icon: Cpu, path: '/hardware', name: '硬件管理' },
  129. { key: 'locationManagement', icon: MapPin, path: '/points', name: '点位管理' },
  130. { key: 'isolationWork', icon: Layers, path: '/isolation', name: '隔离作业' },
  131. ],
  132. subMenuConfig: {
  133. systemConfig: [
  134. { key: 'menuManagement', icon: Menu, path: '/system/menu', name: '菜单管理' },
  135. { key: 'departmentManagement', icon: Building2, path: '/system/dept', name: '部门管理' },
  136. { key: 'positionManagement', icon: Briefcase, path: '/system/post', name: '岗位管理' },
  137. { key: 'roleManagement', icon: UserCog, path: '/system/role', name: '角色管理' },
  138. { key: 'dictionaryManagement', icon: BookOpen, path: '/system/dict', name: '字典管理' },
  139. { key: 'cabinetManagement', icon: Server, path: '/system/cabinet', name: '机柜管理' },
  140. ],
  141. userManagement: [
  142. { key: 'userList', icon: User, path: '/users/list', name: '用户列表' },
  143. { key: 'notificationManagement', icon: Bell, path: '/users/notification', name: '通知管理' },
  144. ],
  145. hardwareManagement: [
  146. { key: 'cabinet', icon: Cpu, path: '/hardware/cabinet', name: '机柜' },
  147. { key: 'key', icon: Lock, path: '/hardware/key', name: '钥匙' },
  148. { key: 'padlock', icon: Lock, path: '/hardware/lock', name: '挂锁' },
  149. { key: 'portable', icon: Radio, path: '/hardware/portable', name: '便携式' },
  150. ],
  151. locationManagement: [],
  152. isolationWork: [
  153. { key: 'processTemplate', icon: Layers, path: '/isolation/approval', name: '流程模板' },
  154. { key: 'sopManagement', icon: BookOpen, path: '/isolation/record', name: 'SOP管理' },
  155. { key: 'workManagement', icon: Activity, path: '/isolation/list', name: '作业管理' },
  156. ],
  157. }
  158. };
  159. }
  160. // 从后端菜单动态生成
  161. const mainMenuMap: { [key: string]: { key: string; icon: any; path: string; name: string } } = {};
  162. const subMenuMap: { [key: string]: { key: string; icon: any; path: string; name: string }[] } = {};
  163. const processedSubMenuKeys = new Set<string>(); // 用于去重,避免重复添加子菜单
  164. // 根据前端key或菜单名称获取对应的图标
  165. const getIconByKey = (key: string, menuName?: string): any => {
  166. // 首先尝试根据 key 精确匹配
  167. const iconMap: { [key: string]: any } = {
  168. // 主菜单
  169. 'dashboard': Gauge,
  170. 'systemConfig': Settings,
  171. 'userManagement': Users,
  172. 'hardwareManagement': Cpu,
  173. 'locationManagement': MapPin,
  174. 'isolationWork': Layers,
  175. // 系统配置子菜单
  176. 'menuManagement': Menu,
  177. 'departmentManagement': Building2,
  178. 'positionManagement': Briefcase,
  179. 'roleManagement': UserCog,
  180. 'dictionaryManagement': BookOpen,
  181. 'cabinetManagement': Server,
  182. // 用户管理子菜单
  183. 'userList': User,
  184. 'notificationManagement': Bell,
  185. // 硬件管理子菜单
  186. 'cabinet': Server,
  187. 'key': KeyRound,
  188. 'padlock': Lock,
  189. 'portable': Radio,
  190. // 隔离作业子菜单
  191. 'processTemplate': Workflow,
  192. 'sopManagement': ClipboardList,
  193. 'workManagement': Activity,
  194. };
  195. if (iconMap[key]) {
  196. return iconMap[key];
  197. }
  198. // 如果 key 无法匹配,尝试根据菜单名称推断
  199. if (menuName) {
  200. const name = menuName.toLowerCase();
  201. // 根据名称关键词匹配图标
  202. if (name.includes('菜单') || name.includes('menu')) return Menu;
  203. if (name.includes('部门') || name.includes('dept')) return Building2;
  204. if (name.includes('岗位') || name.includes('position') || name.includes('post')) return Briefcase;
  205. if (name.includes('角色') || name.includes('role')) return UserCog;
  206. if (name.includes('字典') || name.includes('dict')) return BookOpen;
  207. if (name.includes('机柜') || name.includes('cabinet')) return Server;
  208. if (name.includes('用户') || name.includes('user')) return User;
  209. if (name.includes('通知') || name.includes('notification')) return Bell;
  210. if (name.includes('钥匙') || name.includes('key')) return KeyRound;
  211. if (name.includes('挂锁') || name.includes('padlock') || name.includes('lock')) return Lock;
  212. if (name.includes('便携') || name.includes('portable')) return Radio;
  213. if (name.includes('流程') || name.includes('process') || name.includes('template')) return Workflow;
  214. if (name.includes('sop') || name.includes('标准')) return ClipboardList;
  215. if (name.includes('作业') || name.includes('work') || name.includes('job')) return Activity;
  216. if (name.includes('列表') || name.includes('list')) return List;
  217. if (name.includes('管理') || name.includes('manage')) return Settings;
  218. if (name.includes('配置') || name.includes('config')) return Settings;
  219. if (name.includes('数据') || name.includes('data')) return Database;
  220. if (name.includes('文件') || name.includes('file')) return FileText;
  221. if (name.includes('文件夹') || name.includes('folder')) return FolderOpen;
  222. if (name.includes('网络') || name.includes('network')) return Network;
  223. if (name.includes('硬件') || name.includes('hardware')) return Cpu;
  224. if (name.includes('设备') || name.includes('device')) return HardDrive;
  225. if (name.includes('位置') || name.includes('location') || name.includes('point')) return MapPin;
  226. if (name.includes('隔离') || name.includes('isolation')) return Layers;
  227. }
  228. // 最后尝试根据 key 中的关键词推断
  229. const keyLower = key.toLowerCase();
  230. if (keyLower.includes('menu')) return Menu;
  231. if (keyLower.includes('dept') || keyLower.includes('department')) return Building2;
  232. if (keyLower.includes('position') || keyLower.includes('post')) return Briefcase;
  233. if (keyLower.includes('role')) return UserCog;
  234. if (keyLower.includes('dict')) return BookOpen;
  235. if (keyLower.includes('cabinet')) return Server;
  236. if (keyLower.includes('user')) return User;
  237. if (keyLower.includes('notification')) return Bell;
  238. if (keyLower.includes('key')) return KeyRound;
  239. if (keyLower.includes('lock') || keyLower.includes('padlock')) return Lock;
  240. if (keyLower.includes('portable')) return Radio;
  241. if (keyLower.includes('process') || keyLower.includes('template')) return Workflow;
  242. if (keyLower.includes('sop')) return ClipboardList;
  243. if (keyLower.includes('work') || keyLower.includes('job')) return Activity;
  244. if (keyLower.includes('list')) return List;
  245. if (keyLower.includes('hardware') || keyLower.includes('hw')) return Cpu;
  246. if (keyLower.includes('location') || keyLower.includes('point')) return MapPin;
  247. if (keyLower.includes('isolation')) return Layers;
  248. // 默认使用更合适的图标而不是 Settings
  249. return Layers; // 使用 Layers 作为默认图标,比 Settings 更通用
  250. };
  251. // 递归处理菜单
  252. const processMenu = (menu: any, parentKey: string | null = null, isClientParent: boolean = false) => {
  253. // 检查是否是客户端菜单(当前菜单包含"客户端"或者是客户端菜单的子菜单)
  254. const isClientMenu = menu.name && menu.name.includes('客户端');
  255. const shouldProcess = isClientMenu || isClientParent;
  256. // 对于客户端菜单,即使 visible 为 false 也显示;对于非客户端菜单,如果 visible 为 false 则跳过
  257. if (!shouldProcess && menu.visible === false) {
  258. // 如果不是客户端菜单且不可见,跳过
  259. if (menu.children && menu.children.length > 0) {
  260. menu.children.forEach((child: any) => {
  261. processMenu(child, parentKey, false);
  262. });
  263. }
  264. return;
  265. }
  266. // 调试:打印菜单处理信息
  267. // console.log('处理菜单:', {
  268. // name: menu.name,
  269. // path: menu.path,
  270. // parentId: menu.parentId,
  271. // isClientMenu,
  272. // isClientParent,
  273. // shouldProcess
  274. // });
  275. // 只处理客户端菜单或其子菜单
  276. if (!shouldProcess) {
  277. // 如果不是客户端菜单,但可能有子菜单是客户端菜单,继续递归处理子菜单
  278. if (menu.children && menu.children.length > 0) {
  279. menu.children.forEach((child: any) => {
  280. processMenu(child, parentKey, false);
  281. });
  282. }
  283. return;
  284. }
  285. // 尝试映射到已知的key,如果无法映射则使用path作为key(支持客户端菜单)
  286. let frontendKey: string = mapBackendPathToFrontendKey(menu.path) || '';
  287. if (!frontendKey) {
  288. // 如果无法映射,使用path作为key(去除特殊字符)
  289. frontendKey = menu.path.replace(/[^a-zA-Z0-9]/g, '_') || `menu_${menu.id}`;
  290. }
  291. // 获取图标组件 - 使用原来的图标配置,根据key来映射
  292. const IconComponent = getIconByKey(frontendKey);
  293. // 如果是顶级菜单(parentId === 0)
  294. // 排除通知管理,它应该显示在右侧功能区
  295. if (menu.parentId === 0 && frontendKey !== 'notificationManagement') {
  296. if (!mainMenuMap[frontendKey]) {
  297. // 去掉"客户端-"前缀
  298. const displayName = menu.name.replace(/^客户端[-_]\s*/i, '');
  299. mainMenuMap[frontendKey] = {
  300. key: frontendKey,
  301. icon: IconComponent,
  302. path: menu.path,
  303. name: displayName
  304. };
  305. }
  306. // 处理子菜单 - 只在这里处理,避免在递归时重复添加
  307. if (menu.children && menu.children.length > 0) {
  308. if (!subMenuMap[frontendKey]) {
  309. subMenuMap[frontendKey] = [];
  310. }
  311. // console.log(`处理父菜单 "${menu.name}" 的子菜单,共 ${menu.children.length} 个`);
  312. menu.children.forEach((child: any) => {
  313. // 子菜单也需要检查是否包含"客户端",或者父菜单是客户端菜单
  314. const isChildClientMenu = child.name && child.name.includes('客户端');
  315. const isChildShouldProcess = isChildClientMenu || isClientMenu;
  316. // console.log(` 子菜单: ${child.name}, visible: ${child.visible}, isChildShouldProcess: ${isChildShouldProcess}`);
  317. // 对于客户端菜单的子菜单,即使 visible 为 false 也显示
  318. if (!isChildShouldProcess && child.visible === false) {
  319. // console.log(` 跳过: 不是客户端菜单且不可见`);
  320. return;
  321. }
  322. if (!isChildShouldProcess) {
  323. // 如果子菜单不是客户端菜单,且父菜单也不是客户端菜单,跳过
  324. // console.log(` 跳过: 不是客户端菜单的子菜单`);
  325. return;
  326. }
  327. let childKey: string = mapBackendPathToFrontendKey(child.path) || '';
  328. if (!childKey) {
  329. // 如果无法映射,尝试根据路径推断
  330. if (child.path.includes('/dept')) {
  331. childKey = 'departmentManagement';
  332. } else if (child.path.includes('/menu')) {
  333. childKey = 'menuManagement';
  334. } else if (child.path.includes('/post') || child.path.includes('/marsdept')) {
  335. childKey = 'positionManagement';
  336. } else if (child.path.includes('/role')) {
  337. childKey = 'roleManagement';
  338. } else if (child.path.includes('/dict')) {
  339. childKey = 'dictionaryManagement';
  340. } else if (child.path.includes('/cabinet')) {
  341. childKey = 'cabinetManagement';
  342. } else {
  343. // 如果无法推断,使用path作为key(去除特殊字符)
  344. childKey = child.path.replace(/[^a-zA-Z0-9]/g, '_') || `child_${child.id}`;
  345. }
  346. }
  347. // 检查是否已经添加过(去重)
  348. const subMenuKey = `${frontendKey}_${childKey}`;
  349. if (processedSubMenuKeys.has(subMenuKey)) {
  350. // console.log(` 跳过: 子菜单 ${childKey} 已存在`);
  351. return;
  352. }
  353. processedSubMenuKeys.add(subMenuKey);
  354. // 去掉"客户端-"前缀
  355. const childDisplayName = child.name ? child.name.replace(/^客户端[-_]\s*/i, '') : child.name;
  356. // 使用原来的图标配置,根据key和名称来映射(传入名称以便智能推断)
  357. const ChildIcon = getIconByKey(childKey, childDisplayName || child.name);
  358. const subMenuItem = {
  359. key: childKey,
  360. icon: ChildIcon,
  361. path: child.path,
  362. name: childDisplayName
  363. };
  364. // console.log(` 添加子菜单到 ${frontendKey}:`, subMenuItem);
  365. subMenuMap[frontendKey].push(subMenuItem);
  366. });
  367. // console.log(`父菜单 "${menu.name}" 的子菜单数量: ${subMenuMap[frontendKey].length}`);
  368. }
  369. }
  370. // 注意:不再在这里处理子菜单,避免重复添加
  371. // 子菜单已经在 parentId === 0 的分支中处理了
  372. // 递归处理子菜单(传递是否是客户端父菜单的标志)
  373. if (menu.children && menu.children.length > 0) {
  374. menu.children.forEach((child: any) => {
  375. processMenu(child, frontendKey, isClientMenu);
  376. });
  377. }
  378. };
  379. backendMenus.forEach(menu => {
  380. processMenu(menu);
  381. });
  382. const result = {
  383. mainMenus: Object.values(mainMenuMap),
  384. subMenuConfig: subMenuMap
  385. };
  386. // 调试:打印最终生成的菜单
  387. // console.log('=== 菜单生成结果 ===');
  388. // console.log('主菜单数量:', result.mainMenus.length);
  389. // console.log('主菜单列表:', result.mainMenus.map(m => ({ key: m.key, name: m.name })));
  390. // console.log('子菜单配置:', result.subMenuConfig);
  391. // console.log('==================');
  392. return result;
  393. }, []);
  394. // 获取通知管理菜单信息(从后端菜单中查找)
  395. const notificationMenu = useMemo(() => {
  396. const backendMenus = getMenus();
  397. const findNotificationMenu = (menus: any[]): any => {
  398. for (const menu of menus) {
  399. if (menu.name && menu.name.includes('客户端') && menu.name.includes('通知')) {
  400. return menu;
  401. }
  402. if (menu.children && menu.children.length > 0) {
  403. const found = findNotificationMenu(menu.children);
  404. if (found) return found;
  405. }
  406. }
  407. return null;
  408. };
  409. return findNotificationMenu(backendMenus);
  410. }, []);
  411. // 将通知管理的子菜单也加入到 subMenuConfig 中,以便在 tab 标签中显示
  412. const enhancedSubMenuConfig = useMemo(() => {
  413. const config = { ...subMenuConfig };
  414. // 如果找到通知管理菜单,将其子菜单添加到配置中
  415. if (notificationMenu && notificationMenu.children) {
  416. const notificationSubMenus = notificationMenu.children
  417. .filter((child: any) => child.visible !== false || (child.name && child.name.includes('客户端')))
  418. .map((child: any) => {
  419. const childKey = mapBackendPathToFrontendKey(child.path) || child.path.replace(/[^a-zA-Z0-9]/g, '_') || `child_${child.id}`;
  420. const childDisplayName = child.name ? child.name.replace(/^客户端[-_]\s*/i, '') : child.name;
  421. // 获取图标 - 需要在 useMemo 外部定义 getIconByKey,或者在这里重新定义
  422. // 由于 getIconByKey 在 useMemo 内部,我们需要在这里重新实现图标映射逻辑
  423. let ChildIcon = Bell; // 默认图标
  424. const iconMap: { [key: string]: any } = {
  425. 'userList': User,
  426. 'notificationManagement': Bell,
  427. 'menuManagement': Menu,
  428. 'departmentManagement': Building2,
  429. 'positionManagement': Briefcase,
  430. 'roleManagement': UserCog,
  431. 'dictionaryManagement': BookOpen,
  432. 'cabinetManagement': Server,
  433. 'cabinet': Server,
  434. 'key': KeyRound,
  435. 'padlock': Lock,
  436. 'portable': Radio,
  437. 'processTemplate': Workflow,
  438. 'sopManagement': ClipboardList,
  439. 'workManagement': Activity,
  440. };
  441. if (iconMap[childKey]) {
  442. ChildIcon = iconMap[childKey];
  443. } else if (childDisplayName) {
  444. const name = childDisplayName.toLowerCase();
  445. if (name.includes('用户') || name.includes('user')) ChildIcon = User;
  446. else if (name.includes('通知') || name.includes('notification')) ChildIcon = Bell;
  447. else if (name.includes('列表') || name.includes('list')) ChildIcon = List;
  448. else if (name.includes('流程') || name.includes('process')) ChildIcon = Workflow;
  449. else if (name.includes('作业') || name.includes('work')) ChildIcon = Activity;
  450. }
  451. return {
  452. key: childKey,
  453. icon: ChildIcon,
  454. name: childDisplayName,
  455. path: child.path
  456. };
  457. });
  458. if (notificationSubMenus.length > 0) {
  459. config['notificationManagement'] = notificationSubMenus;
  460. }
  461. }
  462. return config;
  463. }, [subMenuConfig, notificationMenu]);
  464. // 过滤后的主菜单(已经根据后端菜单过滤了)
  465. const filteredMainMenus = mainMenus;
  466. // 过滤后的二级菜单配置(已经根据后端菜单过滤了,包含通知管理)
  467. const filteredSubMenuConfig = enhancedSubMenuConfig;
  468. // 监听路径变化,处理详情页显示和菜单状态更新
  469. useEffect(() => {
  470. // 如果是详情页,设置 activeMenu 为空,直接显示详情页
  471. if (location.pathname.startsWith('/lock-cabinet/detail')) {
  472. setActiveMenu('');
  473. return;
  474. }
  475. // 如果路径是 /dashboard,不处理(使用当前状态)
  476. if (location.pathname === '/dashboard') {
  477. return;
  478. }
  479. // 根据路径更新菜单状态
  480. const path = location.pathname;
  481. const menuKey = mapBackendPathToFrontendKey(path);
  482. if (menuKey) {
  483. console.log('根据路径更新菜单:', { path, menuKey });
  484. // 如果是系统配置的子菜单,需要设置对应的子菜单key
  485. if (menuKey === 'systemConfig' || menuKey === 'departmentManagement' || menuKey === 'menuManagement' ||
  486. menuKey === 'positionManagement' || menuKey === 'roleManagement' || menuKey === 'dictionaryManagement' ||
  487. menuKey === 'cabinetManagement') {
  488. // 确定是哪个子菜单
  489. let subMenuKey = 'menuManagement'; // 默认值
  490. if (path.includes('/dept') || menuKey === 'departmentManagement') {
  491. subMenuKey = 'departmentManagement';
  492. } else if (path.includes('/menu') || menuKey === 'menuManagement') {
  493. subMenuKey = 'menuManagement';
  494. } else if (path.includes('/post') || menuKey === 'positionManagement') {
  495. subMenuKey = 'positionManagement';
  496. } else if (path.includes('/role') || menuKey === 'roleManagement') {
  497. subMenuKey = 'roleManagement';
  498. } else if (path.includes('/dict') || menuKey === 'dictionaryManagement') {
  499. subMenuKey = 'dictionaryManagement';
  500. } else if (path.includes('/cabinet') || menuKey === 'cabinetManagement') {
  501. subMenuKey = 'cabinetManagement';
  502. }
  503. console.log('设置系统配置子菜单:', { menuKey: 'systemConfig', subMenuKey });
  504. setActiveMenu('systemConfig');
  505. setActiveSubMenu(subMenuKey);
  506. } else if (menuKey === 'cabinet' || menuKey === 'key' || menuKey === 'padlock' || menuKey === 'portable') {
  507. // 硬件管理的子菜单
  508. setActiveMenu('hardwareManagement');
  509. setActiveSubMenu(menuKey);
  510. } else if (filteredSubMenuConfig[menuKey] && filteredSubMenuConfig[menuKey].length > 0) {
  511. // 其他菜单,设置第一个子菜单
  512. setActiveMenu(menuKey);
  513. setActiveSubMenu(filteredSubMenuConfig[menuKey][0].key);
  514. }
  515. }
  516. // eslint-disable-next-line react-hooks/exhaustive-deps
  517. }, [location.pathname, filteredSubMenuConfig]);
  518. // 根据URL路径初始化菜单状态(仅在首次加载时执行)
  519. useEffect(() => {
  520. // 如果是详情页,设置 activeMenu 为空,直接显示详情页
  521. if (location.pathname.startsWith('/lock-cabinet/detail')) {
  522. setActiveMenu('');
  523. return;
  524. }
  525. // 只在路径不是 /dashboard 时才根据路径初始化(因为应用使用状态管理而非路由)
  526. // 如果路径是 /dashboard,则使用默认状态
  527. if (location.pathname === '/dashboard') {
  528. return; // 使用默认状态
  529. }
  530. const path = location.pathname;
  531. const menuKey = mapBackendPathToFrontendKey(path);
  532. if (menuKey) {
  533. console.log('根据路径初始化菜单:', { path, menuKey });
  534. // 如果是系统配置的子菜单,需要设置对应的子菜单key
  535. if (menuKey === 'systemConfig' || menuKey === 'departmentManagement' || menuKey === 'menuManagement' ||
  536. menuKey === 'positionManagement' || menuKey === 'roleManagement' || menuKey === 'dictionaryManagement' ||
  537. menuKey === 'cabinetManagement') {
  538. // 确定是哪个子菜单
  539. let subMenuKey = 'menuManagement'; // 默认值
  540. if (path.includes('/dept') || menuKey === 'departmentManagement') {
  541. subMenuKey = 'departmentManagement';
  542. } else if (path.includes('/menu') || menuKey === 'menuManagement') {
  543. subMenuKey = 'menuManagement';
  544. } else if (path.includes('/post') || menuKey === 'positionManagement') {
  545. subMenuKey = 'positionManagement';
  546. } else if (path.includes('/role') || menuKey === 'roleManagement') {
  547. subMenuKey = 'roleManagement';
  548. } else if (path.includes('/dict') || menuKey === 'dictionaryManagement') {
  549. subMenuKey = 'dictionaryManagement';
  550. } else if (path.includes('/cabinet') || menuKey === 'cabinetManagement') {
  551. subMenuKey = 'cabinetManagement';
  552. }
  553. console.log('设置系统配置子菜单:', { menuKey: 'systemConfig', subMenuKey });
  554. setActiveMenu('systemConfig');
  555. setActiveSubMenu(subMenuKey);
  556. } else if (menuKey === 'cabinet' || menuKey === 'key' || menuKey === 'padlock' || menuKey === 'portable') {
  557. // 硬件管理的子菜单
  558. setActiveMenu('hardwareManagement');
  559. setActiveSubMenu(menuKey);
  560. } else if (filteredSubMenuConfig[menuKey] && filteredSubMenuConfig[menuKey].length > 0) {
  561. // 其他菜单,设置第一个子菜单
  562. setActiveMenu(menuKey);
  563. setActiveSubMenu(filteredSubMenuConfig[menuKey][0].key);
  564. }
  565. }
  566. // eslint-disable-next-line react-hooks/exhaustive-deps
  567. }, []); // 只在组件挂载时执行一次
  568. // 监听从用户管理跳转到岗位管理的事件
  569. useEffect(() => {
  570. const handleSwitchToPostManagement = (event: CustomEvent) => {
  571. const userId = event.detail?.userId;
  572. if (userId) {
  573. // 将用户ID存储到 sessionStorage
  574. sessionStorage.setItem('selectedUserId', String(userId));
  575. }
  576. // 切换到岗位管理页面
  577. setActiveMenu('systemConfig');
  578. setActiveSubMenu('positionManagement');
  579. };
  580. window.addEventListener('switchToPostManagement', handleSwitchToPostManagement as EventListener);
  581. return () => {
  582. window.removeEventListener('switchToPostManagement', handleSwitchToPostManagement as EventListener);
  583. };
  584. }, []);
  585. return (
  586. <div className="min-h-screen bg-gradient-to-br from-gray-50 via-blue-50/30 to-gray-50">
  587. {/* 顶部导航栏 */}
  588. <nav className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 sticky top-0 z-50 shadow-sm">
  589. <div className="px-6 py-4">
  590. <div className="flex items-center justify-between">
  591. {/* Logo区域 */}
  592. <div className="flex items-center gap-8">
  593. <div className="flex items-center gap-3">
  594. <div className="relative">
  595. <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%)' }}>
  596. <Shield className="w-6 h-6 text-white" strokeWidth={2.5} />
  597. </div>
  598. <div className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-green-400 rounded-full border-2 border-white"></div>
  599. </div>
  600. <div>
  601. <h1 className="text-lg text-gray-900">{env.appTitle}</h1>
  602. <p className="text-xs text-gray-500">{t('login.subtitle')}</p>
  603. </div>
  604. </div>
  605. {/* 主菜单 */}
  606. <div className="hidden lg:flex items-center gap-2 ml-4">
  607. {filteredMainMenus.map((item) => {
  608. const Icon = item.icon;
  609. const isActive = activeMenu === item.key;
  610. // 只有系统配置显示下拉菜单,其他菜单的二级菜单在页面内用 tab 标签显示
  611. if (item.key === 'systemConfig' && filteredSubMenuConfig[item.key] && filteredSubMenuConfig[item.key].length > 0) {
  612. const isDropdownOpen = showDropdownMenu === item.key;
  613. return (
  614. <div
  615. key={item.key}
  616. className="relative"
  617. onMouseEnter={() => {
  618. if (dropdownTimer) clearTimeout(dropdownTimer);
  619. setShowDropdownMenu(item.key);
  620. }}
  621. onMouseLeave={() => {
  622. const timer = setTimeout(() => setShowDropdownMenu(null), 200);
  623. setDropdownTimer(timer);
  624. }}
  625. >
  626. <button
  627. className={`flex items-center gap-2 px-4 py-2.5 rounded-xl transition-all duration-300 ${
  628. isActive
  629. ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
  630. : 'text-gray-600 hover:bg-blue-50 hover:text-blue-600'
  631. }`}
  632. >
  633. <Icon className={`w-4 h-4 ${isActive ? 'text-white' : ''}`} strokeWidth={2.5} />
  634. <span className={`text-sm ${isActive ? 'text-white' : ''}`}>{item.name || t(`nav.${item.key}`)}</span>
  635. <ChevronDown className={`w-3 h-3 transition-transform ${isDropdownOpen ? 'rotate-180' : ''} ${isActive ? 'text-white' : ''}`} />
  636. </button>
  637. {/* 下拉菜单 - 网格布局 */}
  638. {isDropdownOpen && (
  639. <div className="absolute top-full left-0 mt-2 w-[340px] 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-50">
  640. <div className="grid grid-cols-2 gap-2">
  641. {(filteredSubMenuConfig[item.key] || []).map((subItem) => {
  642. const IconComponent = subItem.icon;
  643. const isActive = activeMenu === item.key && activeSubMenu === subItem.key;
  644. return (
  645. <button
  646. key={subItem.key}
  647. onClick={() => {
  648. console.log('点击子菜单:', { menuKey: item.key, subMenuKey: subItem.key });
  649. // 如果当前在详情页,导航回对应的菜单页面
  650. if (location.pathname.startsWith('/lock-cabinet/detail')) {
  651. navigate('/dashboard');
  652. }
  653. setActiveMenu(item.key);
  654. setActiveSubMenu(subItem.key);
  655. setShowDropdownMenu(null);
  656. }}
  657. className={`flex items-center gap-2.5 px-3 py-3 rounded-lg text-sm transition-all ${
  658. isActive
  659. ? 'bg-blue-50 text-blue-600 shadow-sm'
  660. : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
  661. }`}
  662. >
  663. <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
  664. isActive ? 'bg-blue-100' : 'bg-gray-100'
  665. }`}>
  666. <IconComponent className="w-4 h-4" strokeWidth={2.5} />
  667. </div>
  668. <span>{subItem.name || t(`systemConfig.${subItem.key}`)}</span>
  669. </button>
  670. );
  671. })}
  672. </div>
  673. </div>
  674. )}
  675. </div>
  676. );
  677. }
  678. // 其他普通菜单(用户管理、硬件管理、隔离作业等)- 它们的二级菜单在页面内用 tab 标签显示
  679. return (
  680. <button
  681. key={item.key}
  682. onClick={() => {
  683. // 如果当前在详情页,导航回对应的菜单页面
  684. if (location.pathname.startsWith('/lock-cabinet/detail')) {
  685. navigate('/dashboard');
  686. }
  687. setActiveMenu(item.key);
  688. setActiveSubMenu(filteredSubMenuConfig[item.key]?.[0]?.key || '');
  689. }}
  690. className={`flex items-center gap-2 px-4 py-2.5 rounded-xl transition-all duration-300 ${
  691. isActive
  692. ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
  693. : 'text-gray-600 hover:bg-blue-50 hover:text-blue-600'
  694. }`}
  695. >
  696. <Icon className={`w-4 h-4 ${isActive ? 'text-white' : ''}`} strokeWidth={2.5} />
  697. <span className={`text-sm ${isActive ? 'text-white' : ''}`}>{item.name || t(`nav.${item.key}`)}</span>
  698. </button>
  699. );
  700. })}
  701. </div>
  702. </div>
  703. {/* 右侧功能区 */}
  704. <div className="flex items-center gap-4">
  705. {/* 语言切换 */}
  706. <button
  707. onClick={toggleLanguage}
  708. className="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 rounded-xl transition-colors group"
  709. >
  710. <Globe className="w-4 h-4 text-gray-600 group-hover:rotate-12 transition-transform" />
  711. <span className="text-sm text-gray-700">{i18n.language === 'zh' ? 'EN' : '中文'}</span>
  712. </button>
  713. {/* 消息通知 */}
  714. <div className="relative group">
  715. <button
  716. className="relative p-2.5 hover:bg-gray-100 rounded-xl transition-colors"
  717. title="消息通知"
  718. >
  719. <Bell className="w-5 h-5 text-gray-600" />
  720. <span className="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full"></span>
  721. </button>
  722. {/* Tooltip */}
  723. <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">
  724. {/* 消息通知 */}
  725. </div>
  726. </div>
  727. {/* 通知管理 - 点击后进入页面,二级菜单在 tab 标签中显示 */}
  728. {notificationMenu && (
  729. <div className="relative group">
  730. <button
  731. className="relative p-2.5 hover:bg-gray-100 rounded-xl transition-colors"
  732. title={notificationMenu.name ? notificationMenu.name.replace(/^客户端[-_]\s*/i, '') : '通知管理'}
  733. // title=""
  734. onClick={() => {
  735. const firstSubMenu = filteredSubMenuConfig['notificationManagement']?.[0]?.key || '';
  736. setActiveMenu('notificationManagement');
  737. setActiveSubMenu(firstSubMenu);
  738. }}
  739. >
  740. <MessageSquare className="w-5 h-5 text-gray-600" />
  741. </button>
  742. {/* Tooltip - 显示菜单名称 */}
  743. <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">
  744. </div>
  745. </div>
  746. )}
  747. {/* 用户菜单 */}
  748. <div className="relative">
  749. <button
  750. onClick={() => setShowUserMenu(!showUserMenu)}
  751. className="flex items-center gap-3 px-3 py-2 hover:bg-gray-100 rounded-xl transition-colors"
  752. >
  753. {userInfo?.avatar && !avatarError ? (
  754. <img
  755. src={userInfo.avatar}
  756. alt={userInfo.nickname || userInfo.username || '用户'}
  757. className="w-9 h-9 rounded-lg object-cover shadow-lg border-2 border-white"
  758. onError={() => setAvatarError(true)}
  759. />
  760. ) : (
  761. <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">
  762. <User className="w-5 h-5 text-white" strokeWidth={2.5} />
  763. </div>
  764. )}
  765. <div className="text-left hidden md:block">
  766. <div className="text-sm text-gray-900">{userInfo?.nickname || userInfo?.username || '用户'}</div>
  767. <div className="text-xs text-gray-500">{userInfo?.username || 'User'}</div>
  768. </div>
  769. <ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${showUserMenu ? 'rotate-180' : ''}`} />
  770. </button>
  771. {/* 下拉菜单 */}
  772. {showUserMenu && (
  773. <div 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">
  774. <button
  775. onClick={() => {
  776. setShowProfileSettings(true);
  777. setShowUserMenu(false);
  778. }}
  779. 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"
  780. >
  781. <User className="w-4 h-4" />
  782. {t('nav.profile')}
  783. </button>
  784. <hr className="my-2 border-gray-200" />
  785. <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}>
  786. <LogOut className="w-4 h-4" />
  787. {t('nav.logout')}
  788. </button>
  789. </div>
  790. )}
  791. </div>
  792. </div>
  793. </div>
  794. </div>
  795. </nav>
  796. {/* 主内容区 */}
  797. <div className="px-6 pt-6 pb-6 h-[calc(100vh-88px)] overflow-auto">
  798. {/* 二级菜单 Tab - 只在有多个子菜单(>1)且不是系统配置时显示,并且不在个人资料页面时显示 */}
  799. {!showProfileSettings &&
  800. filteredSubMenuConfig[activeMenu]?.length > 1 &&
  801. activeMenu !== 'systemConfig' && (
  802. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-2 mb-6">
  803. <div className="flex items-center gap-2 overflow-x-auto">
  804. {filteredSubMenuConfig[activeMenu]?.map((item) => {
  805. const isActive = activeSubMenu === item.key;
  806. const menuKey = activeMenu === 'userManagement' ? 'userManagement' :
  807. activeMenu === 'hardwareManagement' ? 'hardwareManagement' :
  808. activeMenu === 'isolationWork' ? 'isolationWork' :
  809. activeMenu === 'notificationManagement' ? 'notificationManagement' : '';
  810. return (
  811. <button
  812. key={item.key}
  813. onClick={() => setActiveSubMenu(item.key)}
  814. className={`px-4 py-2.5 rounded-xl text-sm transition-all duration-200 whitespace-nowrap ${
  815. isActive
  816. ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
  817. : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
  818. }`}
  819. >
  820. <span className={isActive ? 'text-white' : ''}>
  821. {item.name || t(`${menuKey}.${item.key}`)}
  822. </span>
  823. </button>
  824. );
  825. })}
  826. </div>
  827. </div>
  828. )}
  829. {/* 主内容区域 */}
  830. {showProfileSettings ? (
  831. <ProfileSettings onBack={() => setShowProfileSettings(false)} />
  832. ) : location.pathname.startsWith('/lock-cabinet/detail') && (!activeMenu || activeMenu === '') ? (
  833. <LockCabinetDetail />
  834. ) : activeMenu === 'dashboard' ? (
  835. <CockpitDashboard />
  836. ) : activeMenu === 'systemConfig' ? (
  837. <SystemConfig subMenu={activeSubMenu} />
  838. ) : activeMenu === 'userManagement' ? (
  839. <UserManagement subMenu={activeSubMenu} />
  840. ) : activeMenu === 'hardwareManagement' ? (
  841. <HardwareManagement subMenu={activeSubMenu} />
  842. ) : activeMenu === 'locationManagement' ? (
  843. <SegregationPointManagement />
  844. ) : activeMenu === 'isolationWork' ? (
  845. <IsolationWork subMenu={activeSubMenu} />
  846. ) : activeMenu === 'notificationManagement' ? (
  847. <NotificationManagement />
  848. ) : (
  849. // 无法映射的菜单(如客户端菜单)显示占位内容
  850. <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-8">
  851. <div className="text-center">
  852. <div className="text-gray-400 mb-4">
  853. <Settings className="w-16 h-16 mx-auto" />
  854. </div>
  855. <h3 className="text-lg text-gray-900 mb-2">
  856. {mainMenus.find(m => m.key === activeMenu)?.name || '功能开发中'}
  857. </h3>
  858. <p className="text-sm text-gray-500">
  859. 该功能正在开发中,敬请期待
  860. </p>
  861. </div>
  862. </div>
  863. )}
  864. </div>
  865. <Toaster position="top-center" richColors />
  866. </div>
  867. );
  868. }