|
@@ -1,7 +1,7 @@
|
|
|
-import React, { useState } from 'react';
|
|
|
|
|
|
|
+import React, { useState, useMemo } from 'react';
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
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 } from 'lucide-react';
|
|
|
|
|
|
|
+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 SystemConfig from './components/SystemConfig';
|
|
|
import UserManagement from './components/UserManagement';
|
|
import UserManagement from './components/UserManagement';
|
|
|
import HardwareManagement from './components/HardwareManagement';
|
|
import HardwareManagement from './components/HardwareManagement';
|
|
@@ -12,6 +12,7 @@ import CockpitDashboard from './components/CockpitDashboard';
|
|
|
import { authApi } from './api';
|
|
import { authApi } from './api';
|
|
|
import { toast } from 'sonner';
|
|
import { toast } from 'sonner';
|
|
|
import { env } from './config/env';
|
|
import { env } from './config/env';
|
|
|
|
|
+import { getMenus, hasMenuPathPermission, mapMenuPathToKey, getPermissionUser } from './utils/permission';
|
|
|
|
|
|
|
|
export default function Dashboard() {
|
|
export default function Dashboard() {
|
|
|
const navigate = useNavigate();
|
|
const navigate = useNavigate();
|
|
@@ -19,9 +20,13 @@ export default function Dashboard() {
|
|
|
const [activeMenu, setActiveMenu] = useState('dashboard');
|
|
const [activeMenu, setActiveMenu] = useState('dashboard');
|
|
|
const [activeSubMenu, setActiveSubMenu] = useState('menuManagement');
|
|
const [activeSubMenu, setActiveSubMenu] = useState('menuManagement');
|
|
|
const [showUserMenu, setShowUserMenu] = useState(false);
|
|
const [showUserMenu, setShowUserMenu] = useState(false);
|
|
|
- const [showSystemConfigDropdown, setShowSystemConfigDropdown] = useState(false);
|
|
|
|
|
|
|
+ const [showDropdownMenu, setShowDropdownMenu] = useState<string | null>(null); // 当前显示下拉菜单的菜单key
|
|
|
const [dropdownTimer, setDropdownTimer] = useState<NodeJS.Timeout | null>(null);
|
|
const [dropdownTimer, setDropdownTimer] = useState<NodeJS.Timeout | null>(null);
|
|
|
const [showProfileSettings, setShowProfileSettings] = useState(false);
|
|
const [showProfileSettings, setShowProfileSettings] = useState(false);
|
|
|
|
|
+ const [avatarError, setAvatarError] = useState(false);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取用户信息
|
|
|
|
|
+ const userInfo = useMemo(() => getPermissionUser(), []);
|
|
|
|
|
|
|
|
// 切换语言
|
|
// 切换语言
|
|
|
const toggleLanguage = () => {
|
|
const toggleLanguage = () => {
|
|
@@ -45,44 +50,451 @@ export default function Dashboard() {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 主菜单配置
|
|
|
|
|
- const mainMenus = [
|
|
|
|
|
- { key: 'dashboard', icon: Gauge },
|
|
|
|
|
- { key: 'systemConfig', icon: Settings },
|
|
|
|
|
- { key: 'userManagement', icon: Users },
|
|
|
|
|
- { key: 'hardwareManagement', icon: Cpu },
|
|
|
|
|
- { key: 'locationManagement', icon: MapPin },
|
|
|
|
|
- { key: 'isolationWork', icon: Layers },
|
|
|
|
|
- ];
|
|
|
|
|
|
|
+ // 后端菜单路径到前端菜单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 subMenuConfig: { [key: string]: { key: string; icon: any }[] } = {
|
|
|
|
|
- systemConfig: [
|
|
|
|
|
- { key: 'menuManagement', icon: Menu },
|
|
|
|
|
- { key: 'departmentManagement', icon: Building2 },
|
|
|
|
|
- { key: 'positionManagement', icon: Briefcase },
|
|
|
|
|
- { key: 'roleManagement', icon: UserCog },
|
|
|
|
|
- { key: 'dictionaryManagement', icon: BookOpen },
|
|
|
|
|
- { key: 'cabinetManagement', icon: Server },
|
|
|
|
|
- ],
|
|
|
|
|
- userManagement: [
|
|
|
|
|
- { key: 'userList', icon: User },
|
|
|
|
|
- { key: 'notificationManagement', icon: Bell },
|
|
|
|
|
- ],
|
|
|
|
|
- hardwareManagement: [
|
|
|
|
|
- { key: 'cabinet', icon: Cpu },
|
|
|
|
|
- { key: 'key', icon: Lock },
|
|
|
|
|
- { key: 'padlock', icon: Lock },
|
|
|
|
|
- { key: 'portable', icon: Radio },
|
|
|
|
|
- ],
|
|
|
|
|
- locationManagement: [],
|
|
|
|
|
- isolationWork: [
|
|
|
|
|
- { key: 'processTemplate', icon: Layers },
|
|
|
|
|
- { key: 'sopManagement', icon: BookOpen },
|
|
|
|
|
- { key: 'workManagement', icon: Activity },
|
|
|
|
|
- ],
|
|
|
|
|
|
|
+ // 图标名称到组件的映射(用于备用)
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ // 如果无法映射,使用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;
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-blue-50/30 to-gray-50">
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-blue-50/30 to-gray-50">
|
|
|
{/* 顶部导航栏 */}
|
|
{/* 顶部导航栏 */}
|
|
@@ -106,22 +518,23 @@ export default function Dashboard() {
|
|
|
|
|
|
|
|
{/* 主菜单 */}
|
|
{/* 主菜单 */}
|
|
|
<div className="hidden lg:flex items-center gap-2 ml-4">
|
|
<div className="hidden lg:flex items-center gap-2 ml-4">
|
|
|
- {mainMenus.map((item) => {
|
|
|
|
|
|
|
+ {filteredMainMenus.map((item) => {
|
|
|
const Icon = item.icon;
|
|
const Icon = item.icon;
|
|
|
const isActive = activeMenu === item.key;
|
|
const isActive = activeMenu === item.key;
|
|
|
|
|
|
|
|
- // 系统配置带下拉菜单
|
|
|
|
|
- if (item.key === 'systemConfig') {
|
|
|
|
|
|
|
+ // 只有系统配置显示下拉菜单,其他菜单的二级菜单在页面内用 tab 标签显示
|
|
|
|
|
+ if (item.key === 'systemConfig' && filteredSubMenuConfig[item.key] && filteredSubMenuConfig[item.key].length > 0) {
|
|
|
|
|
+ const isDropdownOpen = showDropdownMenu === item.key;
|
|
|
return (
|
|
return (
|
|
|
<div
|
|
<div
|
|
|
key={item.key}
|
|
key={item.key}
|
|
|
className="relative"
|
|
className="relative"
|
|
|
onMouseEnter={() => {
|
|
onMouseEnter={() => {
|
|
|
if (dropdownTimer) clearTimeout(dropdownTimer);
|
|
if (dropdownTimer) clearTimeout(dropdownTimer);
|
|
|
- setShowSystemConfigDropdown(true);
|
|
|
|
|
|
|
+ setShowDropdownMenu(item.key);
|
|
|
}}
|
|
}}
|
|
|
onMouseLeave={() => {
|
|
onMouseLeave={() => {
|
|
|
- const timer = setTimeout(() => setShowSystemConfigDropdown(false), 200);
|
|
|
|
|
|
|
+ const timer = setTimeout(() => setShowDropdownMenu(null), 200);
|
|
|
setDropdownTimer(timer);
|
|
setDropdownTimer(timer);
|
|
|
}}
|
|
}}
|
|
|
>
|
|
>
|
|
@@ -133,24 +546,24 @@ export default function Dashboard() {
|
|
|
}`}
|
|
}`}
|
|
|
>
|
|
>
|
|
|
<Icon className="w-4 h-4" strokeWidth={2.5} />
|
|
<Icon className="w-4 h-4" strokeWidth={2.5} />
|
|
|
- <span className="text-sm">{t(`nav.${item.key}`)}</span>
|
|
|
|
|
- <ChevronDown className={`w-3 h-3 transition-transform ${showSystemConfigDropdown ? 'rotate-180' : ''}`} />
|
|
|
|
|
|
|
+ <span className="text-sm">{item.name || t(`nav.${item.key}`)}</span>
|
|
|
|
|
+ <ChevronDown className={`w-3 h-3 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`} />
|
|
|
</button>
|
|
</button>
|
|
|
|
|
|
|
|
{/* 下拉菜单 - 网格布局 */}
|
|
{/* 下拉菜单 - 网格布局 */}
|
|
|
- {showSystemConfigDropdown && (
|
|
|
|
|
|
|
+ {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="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">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
- {subMenuConfig['systemConfig'].map((subItem) => {
|
|
|
|
|
|
|
+ {(filteredSubMenuConfig[item.key] || []).map((subItem) => {
|
|
|
const IconComponent = subItem.icon;
|
|
const IconComponent = subItem.icon;
|
|
|
- const isActive = activeMenu === 'systemConfig' && activeSubMenu === subItem.key;
|
|
|
|
|
|
|
+ const isActive = activeMenu === item.key && activeSubMenu === subItem.key;
|
|
|
return (
|
|
return (
|
|
|
<button
|
|
<button
|
|
|
key={subItem.key}
|
|
key={subItem.key}
|
|
|
onClick={() => {
|
|
onClick={() => {
|
|
|
- setActiveMenu('systemConfig');
|
|
|
|
|
|
|
+ setActiveMenu(item.key);
|
|
|
setActiveSubMenu(subItem.key);
|
|
setActiveSubMenu(subItem.key);
|
|
|
- setShowSystemConfigDropdown(false);
|
|
|
|
|
|
|
+ setShowDropdownMenu(null);
|
|
|
}}
|
|
}}
|
|
|
className={`flex items-center gap-2.5 px-3 py-3 rounded-lg text-sm transition-all ${
|
|
className={`flex items-center gap-2.5 px-3 py-3 rounded-lg text-sm transition-all ${
|
|
|
isActive
|
|
isActive
|
|
@@ -163,7 +576,7 @@ export default function Dashboard() {
|
|
|
}`}>
|
|
}`}>
|
|
|
<IconComponent className="w-4 h-4" strokeWidth={2.5} />
|
|
<IconComponent className="w-4 h-4" strokeWidth={2.5} />
|
|
|
</div>
|
|
</div>
|
|
|
- <span>{t(`systemConfig.${subItem.key}`)}</span>
|
|
|
|
|
|
|
+ <span>{subItem.name || t(`systemConfig.${subItem.key}`)}</span>
|
|
|
</button>
|
|
</button>
|
|
|
);
|
|
);
|
|
|
})}
|
|
})}
|
|
@@ -174,13 +587,13 @@ export default function Dashboard() {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 其他普通菜单
|
|
|
|
|
|
|
+ // 其他普通菜单(用户管理、硬件管理、隔离作业等)- 它们的二级菜单在页面内用 tab 标签显示
|
|
|
return (
|
|
return (
|
|
|
<button
|
|
<button
|
|
|
key={item.key}
|
|
key={item.key}
|
|
|
onClick={() => {
|
|
onClick={() => {
|
|
|
setActiveMenu(item.key);
|
|
setActiveMenu(item.key);
|
|
|
- setActiveSubMenu(subMenuConfig[item.key]?.[0]?.key || '');
|
|
|
|
|
|
|
+ setActiveSubMenu(filteredSubMenuConfig[item.key]?.[0]?.key || '');
|
|
|
}}
|
|
}}
|
|
|
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl transition-all duration-300 ${
|
|
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl transition-all duration-300 ${
|
|
|
isActive
|
|
isActive
|
|
@@ -189,7 +602,7 @@ export default function Dashboard() {
|
|
|
}`}
|
|
}`}
|
|
|
>
|
|
>
|
|
|
<Icon className="w-4 h-4" strokeWidth={2.5} />
|
|
<Icon className="w-4 h-4" strokeWidth={2.5} />
|
|
|
- <span className="text-sm">{t(`nav.${item.key}`)}</span>
|
|
|
|
|
|
|
+ <span className="text-sm">{item.name || t(`nav.${item.key}`)}</span>
|
|
|
</button>
|
|
</button>
|
|
|
);
|
|
);
|
|
|
})}
|
|
})}
|
|
@@ -207,11 +620,42 @@ export default function Dashboard() {
|
|
|
<span className="text-sm text-gray-700">{i18n.language === 'zh' ? 'EN' : '中文'}</span>
|
|
<span className="text-sm text-gray-700">{i18n.language === 'zh' ? 'EN' : '中文'}</span>
|
|
|
</button>
|
|
</button>
|
|
|
|
|
|
|
|
- {/* 通知 */}
|
|
|
|
|
- <button className="relative p-2.5 hover:bg-gray-100 rounded-xl transition-colors">
|
|
|
|
|
- <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>
|
|
|
|
|
|
|
+ {/* 消息通知 */}
|
|
|
|
|
+ <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, '') : '通知管理'}
|
|
|
|
|
+ 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">
|
|
|
|
|
+ {notificationMenu.name ? notificationMenu.name.replace(/^客户端[-_]\s*/i, '') : '通知管理'}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
|
|
|
{/* 用户菜单 */}
|
|
{/* 用户菜单 */}
|
|
|
<div className="relative">
|
|
<div className="relative">
|
|
@@ -219,12 +663,21 @@ export default function Dashboard() {
|
|
|
onClick={() => setShowUserMenu(!showUserMenu)}
|
|
onClick={() => setShowUserMenu(!showUserMenu)}
|
|
|
className="flex items-center gap-3 px-3 py-2 hover:bg-gray-100 rounded-xl transition-colors"
|
|
className="flex items-center gap-3 px-3 py-2 hover:bg-gray-100 rounded-xl transition-colors"
|
|
|
>
|
|
>
|
|
|
- <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>
|
|
|
|
|
|
|
+ {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-left hidden md:block">
|
|
|
- <div className="text-sm text-gray-900">管理员</div>
|
|
|
|
|
- <div className="text-xs text-gray-500">Admin</div>
|
|
|
|
|
|
|
+ <div className="text-sm text-gray-900">{userInfo?.nickname || userInfo?.username || '用户'}</div>
|
|
|
|
|
+ <div className="text-xs text-gray-500">{userInfo?.username || 'User'}</div>
|
|
|
</div>
|
|
</div>
|
|
|
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${showUserMenu ? 'rotate-180' : ''}`} />
|
|
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${showUserMenu ? 'rotate-180' : ''}`} />
|
|
|
</button>
|
|
</button>
|
|
@@ -257,15 +710,18 @@ export default function Dashboard() {
|
|
|
|
|
|
|
|
{/* 主内容区 */}
|
|
{/* 主内容区 */}
|
|
|
<div className="px-6 pt-6 pb-6 h-[calc(100vh-88px)] overflow-auto">
|
|
<div className="px-6 pt-6 pb-6 h-[calc(100vh-88px)] overflow-auto">
|
|
|
- {/* 二级菜单 Tab - 只在有子菜单且不是系统配置时显示,并且不在个人资料页面时显示 */}
|
|
|
|
|
- {!showProfileSettings && subMenuConfig[activeMenu]?.length > 0 && activeMenu !== 'systemConfig' && (
|
|
|
|
|
|
|
+ {/* 二级菜单 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="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">
|
|
<div className="flex items-center gap-2 overflow-x-auto">
|
|
|
- {subMenuConfig[activeMenu]?.map((item) => {
|
|
|
|
|
|
|
+ {filteredSubMenuConfig[activeMenu]?.map((item) => {
|
|
|
const isActive = activeSubMenu === item.key;
|
|
const isActive = activeSubMenu === item.key;
|
|
|
const menuKey = activeMenu === 'userManagement' ? 'userManagement' :
|
|
const menuKey = activeMenu === 'userManagement' ? 'userManagement' :
|
|
|
activeMenu === 'hardwareManagement' ? 'hardwareManagement' :
|
|
activeMenu === 'hardwareManagement' ? 'hardwareManagement' :
|
|
|
- activeMenu === 'isolationWork' ? 'isolationWork' : '';
|
|
|
|
|
|
|
+ activeMenu === 'isolationWork' ? 'isolationWork' :
|
|
|
|
|
+ activeMenu === 'notificationManagement' ? 'notificationManagement' : '';
|
|
|
return (
|
|
return (
|
|
|
<button
|
|
<button
|
|
|
key={item.key}
|
|
key={item.key}
|
|
@@ -276,7 +732,7 @@ export default function Dashboard() {
|
|
|
: 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
|
|
: 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
|
|
|
}`}
|
|
}`}
|
|
|
>
|
|
>
|
|
|
- {t(`${menuKey}.${item.key}`)}
|
|
|
|
|
|
|
+ {item.name || t(`${menuKey}.${item.key}`)}
|
|
|
</button>
|
|
</button>
|
|
|
);
|
|
);
|
|
|
})}
|
|
})}
|
|
@@ -299,7 +755,24 @@ export default function Dashboard() {
|
|
|
<LocationManagement />
|
|
<LocationManagement />
|
|
|
) : activeMenu === 'isolationWork' ? (
|
|
) : activeMenu === 'isolationWork' ? (
|
|
|
<IsolationWork subMenu={activeSubMenu} />
|
|
<IsolationWork subMenu={activeSubMenu} />
|
|
|
- ) : null}
|
|
|
|
|
|
|
+ ) : activeMenu === 'notificationManagement' ? (
|
|
|
|
|
+ <UserManagement subMenu={activeSubMenu} />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ // 无法映射的菜单(如客户端菜单)显示占位内容
|
|
|
|
|
+ <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>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|