| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947 |
- import React, { useState, useMemo, useEffect } from 'react';
- import { useNavigate, useLocation } from 'react-router-dom';
- import { useTranslation } from 'react-i18next';
- 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';
- import SystemConfig from './components/SystemConfig';
- import UserManagement from './components/UserManagement';
- import HardwareManagement from './components/HardwareManagement';
- import SegregationPointManagement from './components/SegregationPointManagement';
- import IsolationWork from './components/IsolationWork';
- import ProfileSettings from './components/ProfileSettings';
- import CockpitDashboard from './components/CockpitDashboard';
- import LockCabinetDetail from './components/lockCabinet/LockCabinetDetail';
- import NotificationManagement from './components/NotificationManagement';
- import { authApi } from './api';
- import { toast } from 'sonner';
- import { Toaster } from 'sonner';
- import { env } from './utils/env';
- import { getMenus, hasMenuPathPermission, mapMenuPathToKey, getPermissionUser } from './utils/permission';
- export default function Dashboard() {
- const navigate = useNavigate();
- const location = useLocation();
- const { t, i18n } = useTranslation();
- const [activeMenu, setActiveMenu] = useState('dashboard');
- const [activeSubMenu, setActiveSubMenu] = useState('menuManagement');
- const [showUserMenu, setShowUserMenu] = useState(false);
- const [showDropdownMenu, setShowDropdownMenu] = useState<string | null>(null); // 当前显示下拉菜单的菜单key
- const [dropdownTimer, setDropdownTimer] = useState<NodeJS.Timeout | null>(null);
- const [showProfileSettings, setShowProfileSettings] = useState(false);
- const [avatarError, setAvatarError] = useState(false);
- // 获取用户信息
- const userInfo = useMemo(() => getPermissionUser(), []);
- // 切换语言
- const toggleLanguage = () => {
- const newLang = i18n.language === 'zh' ? 'en' : 'zh';
- i18n.changeLanguage(newLang);
- };
- // 登出处理
- const handleLogout = async () => {
- try {
- await authApi.logout();
- localStorage.removeItem('token');
- localStorage.removeItem('tenant');
- toast.success(t('common.success'));
- navigate('/login');
- } catch (error: any) {
- // 即使API失败也清除本地数据并跳转
- localStorage.removeItem('token');
- localStorage.removeItem('tenant');
- navigate('/login');
- }
- };
- // 后端菜单路径到前端菜单key的映射函数
- const mapBackendPathToFrontendKey = (path: string): string | null => {
- // 客户端系统路径(支持 /clientSystem/xxx 格式)
- if (path.startsWith('/clientSystem')) {
- if (path.includes('SystemConfig') || path.endsWith('/SystemConfig')) return 'systemConfig';
- if (path.includes('UserManagement') || path.endsWith('/UserManagement')) return 'userManagement';
- if (path.includes('HardwareManagement') || path.endsWith('/HardwareManagement')) return 'hardwareManagement';
- if (path.includes('LocationManagement') || path.endsWith('/LocationManagement')) return 'locationManagement';
- if (path.includes('IsolationWork') || path.endsWith('/IsolationWork')) return 'isolationWork';
- if (path.includes('notification') || path.endsWith('/notification')) return 'notificationManagement';
- }
-
- // 系统管理相关
- if (path === '/system' || path.startsWith('/system')) {
- if (path.includes('menu') || path.endsWith('/menu')) return 'menuManagement';
- if (path.includes('dept') || path.endsWith('/dept')) return 'departmentManagement';
- if (path.includes('post') || path.includes('marsdept') || path.endsWith('/post') || path.endsWith('/marsdept')) return 'positionManagement';
- if (path.includes('role') || path.endsWith('/role')) return 'roleManagement';
- if (path.includes('dict') || path.endsWith('/dict')) return 'dictionaryManagement';
- if (path.includes('cabinet') || path.endsWith('/cabinet')) return 'cabinetManagement';
- return 'systemConfig';
- }
-
- // 用户管理相关
- if (path === '/users' || path.startsWith('/users') || path === '/user' || path.startsWith('/user')) {
- if (path.includes('list') || path.endsWith('/list')) return 'userList';
- if (path.includes('notification') || path.endsWith('/notification')) return 'notificationManagement';
- return 'userManagement';
- }
-
- // 硬件管理相关
- if (path === '/hw' || path.startsWith('/hw') || path === '/hardware' || path.startsWith('/hardware')) {
- if (path.includes('cabinet') || path.includes('work')) return 'cabinet';
- if (path.includes('key') || path.includes('keys')) return 'key';
- if (path.includes('lock') || path.includes('lk') || path.includes('padlock')) return 'padlock';
- if (path.includes('portable') || path.includes('rfid')) return 'portable';
- return 'hardwareManagement';
- }
-
- // 隔离作业相关
- if (path === '/jobTicket' || path.startsWith('/jobTicket') || path === '/isolation' || path.startsWith('/isolation') || path === '/CustomWorkflow' || path.startsWith('/CustomWorkflow') || path === '/sopm' || path.startsWith('/sopm')) {
- if (path.includes('job') || path.endsWith('/job')) return 'workManagement';
- if (path.includes('sop') || path.endsWith('/sop')) return 'sopManagement';
- if (path.includes('step') || path.includes('template') || path.includes('process') || path.includes('CW') || path.includes('CS')) return 'processTemplate';
- return 'isolationWork';
- }
-
- // 点位管理
- if (path === '/points' || path.startsWith('/points') || path === '/Basicdata' || path.startsWith('/Basicdata')) {
- return 'locationManagement';
- }
-
- // 驾驶舱
- if (path === '/cockpit' || path === '/dashboard' || path === '/') {
- return 'dashboard';
- }
-
- return null;
- };
- // 图标名称到组件的映射(用于备用)
- const getIconByPath = (path: string): any => {
- if (path.includes('system')) return Settings;
- if (path.includes('user')) return Users;
- if (path.includes('hw') || path.includes('hardware')) return Cpu;
- if (path.includes('point') || path.includes('Basicdata')) return MapPin;
- if (path.includes('job') || path.includes('isolation') || path.includes('sop') || path.includes('CustomWorkflow')) return Layers;
- if (path.includes('cockpit') || path === '/') return Gauge;
- if (path.includes('infra')) return Settings;
- return Settings; // 默认
- };
- // 从后端菜单动态生成前端菜单配置
- const { mainMenus, subMenuConfig } = useMemo(() => {
- const backendMenus = getMenus();
-
- // 调试:打印后端菜单数据
- // console.log('后端菜单数据:', backendMenus);
-
- // 如果没有后端菜单数据,使用默认菜单(兼容性处理)
- if (!backendMenus || backendMenus.length === 0) {
- // console.log('没有后端菜单数据,使用默认菜单');
- return {
- mainMenus: [
- { key: 'dashboard', icon: Gauge, path: '/dashboard', name: '驾驶舱' },
- { key: 'systemConfig', icon: Settings, path: '/system', name: '系统配置' },
- { key: 'userManagement', icon: Users, path: '/users', name: '用户管理' },
- { key: 'hardwareManagement', icon: Cpu, path: '/hardware', name: '硬件管理' },
- { key: 'locationManagement', icon: MapPin, path: '/points', name: '点位管理' },
- { key: 'isolationWork', icon: Layers, path: '/isolation', name: '隔离作业' },
- ],
- subMenuConfig: {
- systemConfig: [
- { key: 'menuManagement', icon: Menu, path: '/system/menu', name: '菜单管理' },
- { key: 'departmentManagement', icon: Building2, path: '/system/dept', name: '部门管理' },
- { key: 'positionManagement', icon: Briefcase, path: '/system/post', name: '岗位管理' },
- { key: 'roleManagement', icon: UserCog, path: '/system/role', name: '角色管理' },
- { key: 'dictionaryManagement', icon: BookOpen, path: '/system/dict', name: '字典管理' },
- { key: 'cabinetManagement', icon: Server, path: '/system/cabinet', name: '机柜管理' },
- ],
- userManagement: [
- { key: 'userList', icon: User, path: '/users/list', name: '用户列表' },
- { key: 'notificationManagement', icon: Bell, path: '/users/notification', name: '通知管理' },
- ],
- hardwareManagement: [
- { key: 'cabinet', icon: Cpu, path: '/hardware/cabinet', name: '机柜' },
- { key: 'key', icon: Lock, path: '/hardware/key', name: '钥匙' },
- { key: 'padlock', icon: Lock, path: '/hardware/lock', name: '挂锁' },
- { key: 'portable', icon: Radio, path: '/hardware/portable', name: '便携式' },
- ],
- locationManagement: [],
- isolationWork: [
- { key: 'processTemplate', icon: Layers, path: '/isolation/approval', name: '流程模板' },
- { key: 'sopManagement', icon: BookOpen, path: '/isolation/record', name: 'SOP管理' },
- { key: 'workManagement', icon: Activity, path: '/isolation/list', name: '作业管理' },
- ],
- }
- };
- }
- // 从后端菜单动态生成
- const mainMenuMap: { [key: string]: { key: string; icon: any; path: string; name: string } } = {};
- const subMenuMap: { [key: string]: { key: string; icon: any; path: string; name: string }[] } = {};
- const processedSubMenuKeys = new Set<string>(); // 用于去重,避免重复添加子菜单
- // 根据前端key或菜单名称获取对应的图标
- const getIconByKey = (key: string, menuName?: string): any => {
- // 首先尝试根据 key 精确匹配
- const iconMap: { [key: string]: any } = {
- // 主菜单
- 'dashboard': Gauge,
- 'systemConfig': Settings,
- 'userManagement': Users,
- 'hardwareManagement': Cpu,
- 'locationManagement': MapPin,
- 'isolationWork': Layers,
- // 系统配置子菜单
- 'menuManagement': Menu,
- 'departmentManagement': Building2,
- 'positionManagement': Briefcase,
- 'roleManagement': UserCog,
- 'dictionaryManagement': BookOpen,
- 'cabinetManagement': Server,
- // 用户管理子菜单
- 'userList': User,
- 'notificationManagement': Bell,
- // 硬件管理子菜单
- 'cabinet': Server,
- 'key': KeyRound,
- 'padlock': Lock,
- 'portable': Radio,
- // 隔离作业子菜单
- 'processTemplate': Workflow,
- 'sopManagement': ClipboardList,
- 'workManagement': Activity,
- };
-
- if (iconMap[key]) {
- return iconMap[key];
- }
-
- // 如果 key 无法匹配,尝试根据菜单名称推断
- if (menuName) {
- const name = menuName.toLowerCase();
- // 根据名称关键词匹配图标
- if (name.includes('菜单') || name.includes('menu')) return Menu;
- if (name.includes('部门') || name.includes('dept')) return Building2;
- if (name.includes('岗位') || name.includes('position') || name.includes('post')) return Briefcase;
- if (name.includes('角色') || name.includes('role')) return UserCog;
- if (name.includes('字典') || name.includes('dict')) return BookOpen;
- if (name.includes('机柜') || name.includes('cabinet')) return Server;
- if (name.includes('用户') || name.includes('user')) return User;
- if (name.includes('通知') || name.includes('notification')) return Bell;
- if (name.includes('钥匙') || name.includes('key')) return KeyRound;
- if (name.includes('挂锁') || name.includes('padlock') || name.includes('lock')) return Lock;
- if (name.includes('便携') || name.includes('portable')) return Radio;
- if (name.includes('流程') || name.includes('process') || name.includes('template')) return Workflow;
- if (name.includes('sop') || name.includes('标准')) return ClipboardList;
- if (name.includes('作业') || name.includes('work') || name.includes('job')) return Activity;
- if (name.includes('列表') || name.includes('list')) return List;
- if (name.includes('管理') || name.includes('manage')) return Settings;
- if (name.includes('配置') || name.includes('config')) return Settings;
- if (name.includes('数据') || name.includes('data')) return Database;
- if (name.includes('文件') || name.includes('file')) return FileText;
- if (name.includes('文件夹') || name.includes('folder')) return FolderOpen;
- if (name.includes('网络') || name.includes('network')) return Network;
- if (name.includes('硬件') || name.includes('hardware')) return Cpu;
- if (name.includes('设备') || name.includes('device')) return HardDrive;
- if (name.includes('位置') || name.includes('location') || name.includes('point')) return MapPin;
- if (name.includes('隔离') || name.includes('isolation')) return Layers;
- }
-
- // 最后尝试根据 key 中的关键词推断
- const keyLower = key.toLowerCase();
- if (keyLower.includes('menu')) return Menu;
- if (keyLower.includes('dept') || keyLower.includes('department')) return Building2;
- if (keyLower.includes('position') || keyLower.includes('post')) return Briefcase;
- if (keyLower.includes('role')) return UserCog;
- if (keyLower.includes('dict')) return BookOpen;
- if (keyLower.includes('cabinet')) return Server;
- if (keyLower.includes('user')) return User;
- if (keyLower.includes('notification')) return Bell;
- if (keyLower.includes('key')) return KeyRound;
- if (keyLower.includes('lock') || keyLower.includes('padlock')) return Lock;
- if (keyLower.includes('portable')) return Radio;
- if (keyLower.includes('process') || keyLower.includes('template')) return Workflow;
- if (keyLower.includes('sop')) return ClipboardList;
- if (keyLower.includes('work') || keyLower.includes('job')) return Activity;
- if (keyLower.includes('list')) return List;
- if (keyLower.includes('hardware') || keyLower.includes('hw')) return Cpu;
- if (keyLower.includes('location') || keyLower.includes('point')) return MapPin;
- if (keyLower.includes('isolation')) return Layers;
-
- // 默认使用更合适的图标而不是 Settings
- return Layers; // 使用 Layers 作为默认图标,比 Settings 更通用
- };
- // 递归处理菜单
- const processMenu = (menu: any, parentKey: string | null = null, isClientParent: boolean = false) => {
- // 检查是否是客户端菜单(当前菜单包含"客户端"或者是客户端菜单的子菜单)
- const isClientMenu = menu.name && menu.name.includes('客户端');
- const shouldProcess = isClientMenu || isClientParent;
-
- // 对于客户端菜单,即使 visible 为 false 也显示;对于非客户端菜单,如果 visible 为 false 则跳过
- if (!shouldProcess && menu.visible === false) {
- // 如果不是客户端菜单且不可见,跳过
- if (menu.children && menu.children.length > 0) {
- menu.children.forEach((child: any) => {
- processMenu(child, parentKey, false);
- });
- }
- return;
- }
-
- // 调试:打印菜单处理信息
- // console.log('处理菜单:', {
- // name: menu.name,
- // path: menu.path,
- // parentId: menu.parentId,
- // isClientMenu,
- // isClientParent,
- // shouldProcess
- // });
-
- // 只处理客户端菜单或其子菜单
- if (!shouldProcess) {
- // 如果不是客户端菜单,但可能有子菜单是客户端菜单,继续递归处理子菜单
- if (menu.children && menu.children.length > 0) {
- menu.children.forEach((child: any) => {
- processMenu(child, parentKey, false);
- });
- }
- return;
- }
-
- // 尝试映射到已知的key,如果无法映射则使用path作为key(支持客户端菜单)
- let frontendKey: string = mapBackendPathToFrontendKey(menu.path) || '';
- if (!frontendKey) {
- // 如果无法映射,使用path作为key(去除特殊字符)
- frontendKey = menu.path.replace(/[^a-zA-Z0-9]/g, '_') || `menu_${menu.id}`;
- }
-
- // 获取图标组件 - 使用原来的图标配置,根据key来映射
- const IconComponent = getIconByKey(frontendKey);
-
- // 如果是顶级菜单(parentId === 0)
- // 排除通知管理,它应该显示在右侧功能区
- if (menu.parentId === 0 && frontendKey !== 'notificationManagement') {
- if (!mainMenuMap[frontendKey]) {
- // 去掉"客户端-"前缀
- const displayName = menu.name.replace(/^客户端[-_]\s*/i, '');
- mainMenuMap[frontendKey] = {
- key: frontendKey,
- icon: IconComponent,
- path: menu.path,
- name: displayName
- };
- }
-
- // 处理子菜单 - 只在这里处理,避免在递归时重复添加
- if (menu.children && menu.children.length > 0) {
- if (!subMenuMap[frontendKey]) {
- subMenuMap[frontendKey] = [];
- }
- // console.log(`处理父菜单 "${menu.name}" 的子菜单,共 ${menu.children.length} 个`);
- menu.children.forEach((child: any) => {
- // 子菜单也需要检查是否包含"客户端",或者父菜单是客户端菜单
- const isChildClientMenu = child.name && child.name.includes('客户端');
- const isChildShouldProcess = isChildClientMenu || isClientMenu;
-
- // console.log(` 子菜单: ${child.name}, visible: ${child.visible}, isChildShouldProcess: ${isChildShouldProcess}`);
-
- // 对于客户端菜单的子菜单,即使 visible 为 false 也显示
- if (!isChildShouldProcess && child.visible === false) {
- // console.log(` 跳过: 不是客户端菜单且不可见`);
- return;
- }
-
- if (!isChildShouldProcess) {
- // 如果子菜单不是客户端菜单,且父菜单也不是客户端菜单,跳过
- // console.log(` 跳过: 不是客户端菜单的子菜单`);
- return;
- }
- let childKey: string = mapBackendPathToFrontendKey(child.path) || '';
- if (!childKey) {
- // 如果无法映射,尝试根据路径推断
- if (child.path.includes('/dept')) {
- childKey = 'departmentManagement';
- } else if (child.path.includes('/menu')) {
- childKey = 'menuManagement';
- } else if (child.path.includes('/post') || child.path.includes('/marsdept')) {
- childKey = 'positionManagement';
- } else if (child.path.includes('/role')) {
- childKey = 'roleManagement';
- } else if (child.path.includes('/dict')) {
- childKey = 'dictionaryManagement';
- } else if (child.path.includes('/cabinet')) {
- childKey = 'cabinetManagement';
- } else {
- // 如果无法推断,使用path作为key(去除特殊字符)
- childKey = child.path.replace(/[^a-zA-Z0-9]/g, '_') || `child_${child.id}`;
- }
- }
-
- // 检查是否已经添加过(去重)
- const subMenuKey = `${frontendKey}_${childKey}`;
- if (processedSubMenuKeys.has(subMenuKey)) {
- // console.log(` 跳过: 子菜单 ${childKey} 已存在`);
- return;
- }
- processedSubMenuKeys.add(subMenuKey);
-
- // 去掉"客户端-"前缀
- const childDisplayName = child.name ? child.name.replace(/^客户端[-_]\s*/i, '') : child.name;
-
- // 使用原来的图标配置,根据key和名称来映射(传入名称以便智能推断)
- const ChildIcon = getIconByKey(childKey, childDisplayName || child.name);
- const subMenuItem = {
- key: childKey,
- icon: ChildIcon,
- path: child.path,
- name: childDisplayName
- };
- // console.log(` 添加子菜单到 ${frontendKey}:`, subMenuItem);
- subMenuMap[frontendKey].push(subMenuItem);
- });
- // console.log(`父菜单 "${menu.name}" 的子菜单数量: ${subMenuMap[frontendKey].length}`);
- }
- }
- // 注意:不再在这里处理子菜单,避免重复添加
- // 子菜单已经在 parentId === 0 的分支中处理了
-
- // 递归处理子菜单(传递是否是客户端父菜单的标志)
- if (menu.children && menu.children.length > 0) {
- menu.children.forEach((child: any) => {
- processMenu(child, frontendKey, isClientMenu);
- });
- }
- };
-
- backendMenus.forEach(menu => {
- processMenu(menu);
- });
-
- const result = {
- mainMenus: Object.values(mainMenuMap),
- subMenuConfig: subMenuMap
- };
-
- // 调试:打印最终生成的菜单
- // console.log('=== 菜单生成结果 ===');
- // console.log('主菜单数量:', result.mainMenus.length);
- // console.log('主菜单列表:', result.mainMenus.map(m => ({ key: m.key, name: m.name })));
- // console.log('子菜单配置:', result.subMenuConfig);
- // console.log('==================');
-
- return result;
- }, []);
- // 获取通知管理菜单信息(从后端菜单中查找)
- const notificationMenu = useMemo(() => {
- const backendMenus = getMenus();
- const findNotificationMenu = (menus: any[]): any => {
- for (const menu of menus) {
- if (menu.name && menu.name.includes('客户端') && menu.name.includes('通知')) {
- return menu;
- }
- if (menu.children && menu.children.length > 0) {
- const found = findNotificationMenu(menu.children);
- if (found) return found;
- }
- }
- return null;
- };
- return findNotificationMenu(backendMenus);
- }, []);
- // 将通知管理的子菜单也加入到 subMenuConfig 中,以便在 tab 标签中显示
- const enhancedSubMenuConfig = useMemo(() => {
- const config = { ...subMenuConfig };
-
- // 如果找到通知管理菜单,将其子菜单添加到配置中
- if (notificationMenu && notificationMenu.children) {
- const notificationSubMenus = notificationMenu.children
- .filter((child: any) => child.visible !== false || (child.name && child.name.includes('客户端')))
- .map((child: any) => {
- const childKey = mapBackendPathToFrontendKey(child.path) || child.path.replace(/[^a-zA-Z0-9]/g, '_') || `child_${child.id}`;
- const childDisplayName = child.name ? child.name.replace(/^客户端[-_]\s*/i, '') : child.name;
- // 获取图标 - 需要在 useMemo 外部定义 getIconByKey,或者在这里重新定义
- // 由于 getIconByKey 在 useMemo 内部,我们需要在这里重新实现图标映射逻辑
- let ChildIcon = Bell; // 默认图标
- const iconMap: { [key: string]: any } = {
- 'userList': User,
- 'notificationManagement': Bell,
- 'menuManagement': Menu,
- 'departmentManagement': Building2,
- 'positionManagement': Briefcase,
- 'roleManagement': UserCog,
- 'dictionaryManagement': BookOpen,
- 'cabinetManagement': Server,
- 'cabinet': Server,
- 'key': KeyRound,
- 'padlock': Lock,
- 'portable': Radio,
- 'processTemplate': Workflow,
- 'sopManagement': ClipboardList,
- 'workManagement': Activity,
- };
- if (iconMap[childKey]) {
- ChildIcon = iconMap[childKey];
- } else if (childDisplayName) {
- const name = childDisplayName.toLowerCase();
- if (name.includes('用户') || name.includes('user')) ChildIcon = User;
- else if (name.includes('通知') || name.includes('notification')) ChildIcon = Bell;
- else if (name.includes('列表') || name.includes('list')) ChildIcon = List;
- else if (name.includes('流程') || name.includes('process')) ChildIcon = Workflow;
- else if (name.includes('作业') || name.includes('work')) ChildIcon = Activity;
- }
- return {
- key: childKey,
- icon: ChildIcon,
- name: childDisplayName,
- path: child.path
- };
- });
-
- if (notificationSubMenus.length > 0) {
- config['notificationManagement'] = notificationSubMenus;
- }
- }
-
- return config;
- }, [subMenuConfig, notificationMenu]);
- // 过滤后的主菜单(已经根据后端菜单过滤了)
- const filteredMainMenus = mainMenus;
- // 过滤后的二级菜单配置(已经根据后端菜单过滤了,包含通知管理)
- const filteredSubMenuConfig = enhancedSubMenuConfig;
- // 监听路径变化,处理详情页显示和菜单状态更新
- useEffect(() => {
- // 如果是详情页,设置 activeMenu 为空,直接显示详情页
- if (location.pathname.startsWith('/lock-cabinet/detail')) {
- setActiveMenu('');
- return;
- }
-
- // 如果路径是 /dashboard,不处理(使用当前状态)
- if (location.pathname === '/dashboard') {
- return;
- }
-
- // 根据路径更新菜单状态
- const path = location.pathname;
- const menuKey = mapBackendPathToFrontendKey(path);
-
- if (menuKey) {
- console.log('根据路径更新菜单:', { path, menuKey });
-
- // 如果是系统配置的子菜单,需要设置对应的子菜单key
- if (menuKey === 'systemConfig' || menuKey === 'departmentManagement' || menuKey === 'menuManagement' ||
- menuKey === 'positionManagement' || menuKey === 'roleManagement' || menuKey === 'dictionaryManagement' ||
- menuKey === 'cabinetManagement') {
- // 确定是哪个子菜单
- let subMenuKey = 'menuManagement'; // 默认值
-
- if (path.includes('/dept') || menuKey === 'departmentManagement') {
- subMenuKey = 'departmentManagement';
- } else if (path.includes('/menu') || menuKey === 'menuManagement') {
- subMenuKey = 'menuManagement';
- } else if (path.includes('/post') || menuKey === 'positionManagement') {
- subMenuKey = 'positionManagement';
- } else if (path.includes('/role') || menuKey === 'roleManagement') {
- subMenuKey = 'roleManagement';
- } else if (path.includes('/dict') || menuKey === 'dictionaryManagement') {
- subMenuKey = 'dictionaryManagement';
- } else if (path.includes('/cabinet') || menuKey === 'cabinetManagement') {
- subMenuKey = 'cabinetManagement';
- }
-
- console.log('设置系统配置子菜单:', { menuKey: 'systemConfig', subMenuKey });
- setActiveMenu('systemConfig');
- setActiveSubMenu(subMenuKey);
- } else if (menuKey === 'cabinet' || menuKey === 'key' || menuKey === 'padlock' || menuKey === 'portable') {
- // 硬件管理的子菜单
- setActiveMenu('hardwareManagement');
- setActiveSubMenu(menuKey);
- } else if (filteredSubMenuConfig[menuKey] && filteredSubMenuConfig[menuKey].length > 0) {
- // 其他菜单,设置第一个子菜单
- setActiveMenu(menuKey);
- setActiveSubMenu(filteredSubMenuConfig[menuKey][0].key);
- }
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [location.pathname, filteredSubMenuConfig]);
- // 根据URL路径初始化菜单状态(仅在首次加载时执行)
- useEffect(() => {
- // 如果是详情页,设置 activeMenu 为空,直接显示详情页
- if (location.pathname.startsWith('/lock-cabinet/detail')) {
- setActiveMenu('');
- return;
- }
-
- // 只在路径不是 /dashboard 时才根据路径初始化(因为应用使用状态管理而非路由)
- // 如果路径是 /dashboard,则使用默认状态
- if (location.pathname === '/dashboard') {
- return; // 使用默认状态
- }
-
- const path = location.pathname;
- const menuKey = mapBackendPathToFrontendKey(path);
-
- if (menuKey) {
- console.log('根据路径初始化菜单:', { path, menuKey });
-
- // 如果是系统配置的子菜单,需要设置对应的子菜单key
- if (menuKey === 'systemConfig' || menuKey === 'departmentManagement' || menuKey === 'menuManagement' ||
- menuKey === 'positionManagement' || menuKey === 'roleManagement' || menuKey === 'dictionaryManagement' ||
- menuKey === 'cabinetManagement') {
- // 确定是哪个子菜单
- let subMenuKey = 'menuManagement'; // 默认值
-
- if (path.includes('/dept') || menuKey === 'departmentManagement') {
- subMenuKey = 'departmentManagement';
- } else if (path.includes('/menu') || menuKey === 'menuManagement') {
- subMenuKey = 'menuManagement';
- } else if (path.includes('/post') || menuKey === 'positionManagement') {
- subMenuKey = 'positionManagement';
- } else if (path.includes('/role') || menuKey === 'roleManagement') {
- subMenuKey = 'roleManagement';
- } else if (path.includes('/dict') || menuKey === 'dictionaryManagement') {
- subMenuKey = 'dictionaryManagement';
- } else if (path.includes('/cabinet') || menuKey === 'cabinetManagement') {
- subMenuKey = 'cabinetManagement';
- }
-
- console.log('设置系统配置子菜单:', { menuKey: 'systemConfig', subMenuKey });
- setActiveMenu('systemConfig');
- setActiveSubMenu(subMenuKey);
- } else if (menuKey === 'cabinet' || menuKey === 'key' || menuKey === 'padlock' || menuKey === 'portable') {
- // 硬件管理的子菜单
- setActiveMenu('hardwareManagement');
- setActiveSubMenu(menuKey);
- } else if (filteredSubMenuConfig[menuKey] && filteredSubMenuConfig[menuKey].length > 0) {
- // 其他菜单,设置第一个子菜单
- setActiveMenu(menuKey);
- setActiveSubMenu(filteredSubMenuConfig[menuKey][0].key);
- }
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []); // 只在组件挂载时执行一次
- // 监听从用户管理跳转到岗位管理的事件
- useEffect(() => {
- const handleSwitchToPostManagement = (event: CustomEvent) => {
- const userId = event.detail?.userId;
- if (userId) {
- // 将用户ID存储到 sessionStorage
- sessionStorage.setItem('selectedUserId', String(userId));
- }
- // 切换到岗位管理页面
- setActiveMenu('systemConfig');
- setActiveSubMenu('positionManagement');
- };
- window.addEventListener('switchToPostManagement', handleSwitchToPostManagement as EventListener);
- return () => {
- window.removeEventListener('switchToPostManagement', handleSwitchToPostManagement as EventListener);
- };
- }, []);
- return (
- <div className="min-h-screen bg-gradient-to-br from-gray-50 via-blue-50/30 to-gray-50">
- {/* 顶部导航栏 */}
- <nav className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 sticky top-0 z-50 shadow-sm">
- <div className="px-6 py-4">
- <div className="flex items-center justify-between">
- {/* Logo区域 */}
- <div className="flex items-center gap-8">
- <div className="flex items-center gap-3">
- <div className="relative">
- <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%)' }}>
- <Shield className="w-6 h-6 text-white" strokeWidth={2.5} />
- </div>
- <div className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-green-400 rounded-full border-2 border-white"></div>
- </div>
- <div>
- <h1 className="text-lg text-gray-900">{env.appTitle}</h1>
- <p className="text-xs text-gray-500">{t('login.subtitle')}</p>
- </div>
- </div>
- {/* 主菜单 */}
- <div className="hidden lg:flex items-center gap-2 ml-4">
- {filteredMainMenus.map((item) => {
- const Icon = item.icon;
- const isActive = activeMenu === item.key;
-
- // 只有系统配置显示下拉菜单,其他菜单的二级菜单在页面内用 tab 标签显示
- if (item.key === 'systemConfig' && filteredSubMenuConfig[item.key] && filteredSubMenuConfig[item.key].length > 0) {
- const isDropdownOpen = showDropdownMenu === item.key;
- return (
- <div
- key={item.key}
- className="relative"
- onMouseEnter={() => {
- if (dropdownTimer) clearTimeout(dropdownTimer);
- setShowDropdownMenu(item.key);
- }}
- onMouseLeave={() => {
- const timer = setTimeout(() => setShowDropdownMenu(null), 200);
- setDropdownTimer(timer);
- }}
- >
- <button
- className={`flex items-center gap-2 px-4 py-2.5 rounded-xl transition-all duration-300 ${
- isActive
- ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
- : 'text-gray-600 hover:bg-blue-50 hover:text-blue-600'
- }`}
- >
- <Icon className={`w-4 h-4 ${isActive ? 'text-white' : ''}`} strokeWidth={2.5} />
- <span className={`text-sm ${isActive ? 'text-white' : ''}`}>{item.name || t(`nav.${item.key}`)}</span>
- <ChevronDown className={`w-3 h-3 transition-transform ${isDropdownOpen ? 'rotate-180' : ''} ${isActive ? 'text-white' : ''}`} />
- </button>
-
- {/* 下拉菜单 - 网格布局 */}
- {isDropdownOpen && (
- <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">
- <div className="grid grid-cols-2 gap-2">
- {(filteredSubMenuConfig[item.key] || []).map((subItem) => {
- const IconComponent = subItem.icon;
- const isActive = activeMenu === item.key && activeSubMenu === subItem.key;
- return (
- <button
- key={subItem.key}
- onClick={() => {
- console.log('点击子菜单:', { menuKey: item.key, subMenuKey: subItem.key });
- // 如果当前在详情页,导航回对应的菜单页面
- if (location.pathname.startsWith('/lock-cabinet/detail')) {
- navigate('/dashboard');
- }
- setActiveMenu(item.key);
- setActiveSubMenu(subItem.key);
- setShowDropdownMenu(null);
- }}
- className={`flex items-center gap-2.5 px-3 py-3 rounded-lg text-sm transition-all ${
- isActive
- ? 'bg-blue-50 text-blue-600 shadow-sm'
- : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
- }`}
- >
- <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
- isActive ? 'bg-blue-100' : 'bg-gray-100'
- }`}>
- <IconComponent className="w-4 h-4" strokeWidth={2.5} />
- </div>
- <span>{subItem.name || t(`systemConfig.${subItem.key}`)}</span>
- </button>
- );
- })}
- </div>
- </div>
- )}
- </div>
- );
- }
-
- // 其他普通菜单(用户管理、硬件管理、隔离作业等)- 它们的二级菜单在页面内用 tab 标签显示
- return (
- <button
- key={item.key}
- onClick={() => {
- // 如果当前在详情页,导航回对应的菜单页面
- if (location.pathname.startsWith('/lock-cabinet/detail')) {
- navigate('/dashboard');
- }
- setActiveMenu(item.key);
- setActiveSubMenu(filteredSubMenuConfig[item.key]?.[0]?.key || '');
- }}
- className={`flex items-center gap-2 px-4 py-2.5 rounded-xl transition-all duration-300 ${
- isActive
- ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
- : 'text-gray-600 hover:bg-blue-50 hover:text-blue-600'
- }`}
- >
- <Icon className={`w-4 h-4 ${isActive ? 'text-white' : ''}`} strokeWidth={2.5} />
- <span className={`text-sm ${isActive ? 'text-white' : ''}`}>{item.name || t(`nav.${item.key}`)}</span>
- </button>
- );
- })}
- </div>
- </div>
- {/* 右侧功能区 */}
- <div className="flex items-center gap-4">
- {/* 语言切换 */}
- <button
- onClick={toggleLanguage}
- className="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 rounded-xl transition-colors group"
- >
- <Globe className="w-4 h-4 text-gray-600 group-hover:rotate-12 transition-transform" />
- <span className="text-sm text-gray-700">{i18n.language === 'zh' ? 'EN' : '中文'}</span>
- </button>
-
- {/* 消息通知 */}
- <div className="relative group">
- <button
- className="relative p-2.5 hover:bg-gray-100 rounded-xl transition-colors"
- title="消息通知"
- >
- <Bell className="w-5 h-5 text-gray-600" />
- <span className="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full"></span>
- </button>
- {/* Tooltip */}
- <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">
- {/* 消息通知 */}
- </div>
- </div>
- {/* 通知管理 - 点击后进入页面,二级菜单在 tab 标签中显示 */}
- {notificationMenu && (
- <div className="relative group">
- <button
- className="relative p-2.5 hover:bg-gray-100 rounded-xl transition-colors"
- title={notificationMenu.name ? notificationMenu.name.replace(/^客户端[-_]\s*/i, '') : '通知管理'}
- // title=""
- onClick={() => {
- const firstSubMenu = filteredSubMenuConfig['notificationManagement']?.[0]?.key || '';
- setActiveMenu('notificationManagement');
- setActiveSubMenu(firstSubMenu);
- }}
- >
- <MessageSquare className="w-5 h-5 text-gray-600" />
- </button>
- {/* Tooltip - 显示菜单名称 */}
- <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">
-
- </div>
- </div>
- )}
- {/* 用户菜单 */}
- <div className="relative">
- <button
- onClick={() => setShowUserMenu(!showUserMenu)}
- className="flex items-center gap-3 px-3 py-2 hover:bg-gray-100 rounded-xl transition-colors"
- >
- {userInfo?.avatar && !avatarError ? (
- <img
- src={userInfo.avatar}
- alt={userInfo.nickname || userInfo.username || '用户'}
- className="w-9 h-9 rounded-lg object-cover shadow-lg border-2 border-white"
- onError={() => setAvatarError(true)}
- />
- ) : (
- <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">
- <User className="w-5 h-5 text-white" strokeWidth={2.5} />
- </div>
- )}
- <div className="text-left hidden md:block">
- <div className="text-sm text-gray-900">{userInfo?.nickname || userInfo?.username || '用户'}</div>
- <div className="text-xs text-gray-500">{userInfo?.username || 'User'}</div>
- </div>
- <ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${showUserMenu ? 'rotate-180' : ''}`} />
- </button>
- {/* 下拉菜单 */}
- {showUserMenu && (
- <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">
- <button
- onClick={() => {
- setShowProfileSettings(true);
- setShowUserMenu(false);
- }}
- 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"
- >
- <User className="w-4 h-4" />
- {t('nav.profile')}
- </button>
- <hr className="my-2 border-gray-200" />
- <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}>
- <LogOut className="w-4 h-4" />
- {t('nav.logout')}
- </button>
- </div>
- )}
- </div>
- </div>
- </div>
- </div>
- </nav>
- {/* 主内容区 */}
- <div className="px-6 pt-6 pb-6 h-[calc(100vh-88px)] overflow-auto">
- {/* 二级菜单 Tab - 只在有多个子菜单(>1)且不是系统配置时显示,并且不在个人资料页面时显示 */}
- {!showProfileSettings &&
- filteredSubMenuConfig[activeMenu]?.length > 1 &&
- activeMenu !== 'systemConfig' && (
- <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-2 mb-6">
- <div className="flex items-center gap-2 overflow-x-auto">
- {filteredSubMenuConfig[activeMenu]?.map((item) => {
- const isActive = activeSubMenu === item.key;
- const menuKey = activeMenu === 'userManagement' ? 'userManagement' :
- activeMenu === 'hardwareManagement' ? 'hardwareManagement' :
- activeMenu === 'isolationWork' ? 'isolationWork' :
- activeMenu === 'notificationManagement' ? 'notificationManagement' : '';
- return (
- <button
- key={item.key}
- onClick={() => setActiveSubMenu(item.key)}
- className={`px-4 py-2.5 rounded-xl text-sm transition-all duration-200 whitespace-nowrap ${
- isActive
- ? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-400/40'
- : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
- }`}
- >
- <span className={isActive ? 'text-white' : ''}>
- {item.name || t(`${menuKey}.${item.key}`)}
- </span>
- </button>
- );
- })}
- </div>
- </div>
- )}
- {/* 主内容区域 */}
- {showProfileSettings ? (
- <ProfileSettings onBack={() => setShowProfileSettings(false)} />
- ) : location.pathname.startsWith('/lock-cabinet/detail') && (!activeMenu || activeMenu === '') ? (
- <LockCabinetDetail />
- ) : activeMenu === 'dashboard' ? (
- <CockpitDashboard />
- ) : activeMenu === 'systemConfig' ? (
- <SystemConfig subMenu={activeSubMenu} />
- ) : activeMenu === 'userManagement' ? (
- <UserManagement subMenu={activeSubMenu} />
- ) : activeMenu === 'hardwareManagement' ? (
- <HardwareManagement subMenu={activeSubMenu} />
- ) : activeMenu === 'locationManagement' ? (
- <SegregationPointManagement />
- ) : activeMenu === 'isolationWork' ? (
- <IsolationWork subMenu={activeSubMenu} />
- ) : activeMenu === 'notificationManagement' ? (
- <NotificationManagement />
- ) : (
- // 无法映射的菜单(如客户端菜单)显示占位内容
- <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-8">
- <div className="text-center">
- <div className="text-gray-400 mb-4">
- <Settings className="w-16 h-16 mx-auto" />
- </div>
- <h3 className="text-lg text-gray-900 mb-2">
- {mainMenus.find(m => m.key === activeMenu)?.name || '功能开发中'}
- </h3>
- <p className="text-sm text-gray-500">
- 该功能正在开发中,敬请期待
- </p>
- </div>
- </div>
- )}
- </div>
- <Toaster position="top-center" richColors />
- </div>
- );
- }
|