Преглед изворни кода

提交菜单从接口获取

pm пре 5 месеци
родитељ
комит
dff9f68027
43 измењених фајлова са 1276 додато и 165 уклоњено
  1. 1 1
      .env
  2. 2 2
      README.md
  3. 0 0
      README.txt
  4. 1 1
      build/index.html
  5. 335 0
      menu-config.json
  6. 22 22
      node_modules/.vite/deps/_metadata.json
  7. 0 0
      node_modules/.vite/deps/axios.js.map
  8. 1 1
      node_modules/.vite/deps/chunk-AGDJ642P.js
  9. 3 0
      node_modules/.vite/deps/chunk-AGDJ642P.js.map
  10. 2 2
      node_modules/.vite/deps/chunk-BANT3OPS.js
  11. 3 0
      node_modules/.vite/deps/chunk-BANT3OPS.js.map
  12. 0 3
      node_modules/.vite/deps/chunk-DRWLMN53.js.map
  13. 0 3
      node_modules/.vite/deps/chunk-HCC4GIHT.js.map
  14. 0 3
      node_modules/.vite/deps/chunk-K23GC2QC.js.map
  15. 1 1
      node_modules/.vite/deps/chunk-QRULMDK5.js
  16. 3 0
      node_modules/.vite/deps/chunk-QRULMDK5.js.map
  17. 0 0
      node_modules/.vite/deps/i18next-browser-languagedetector.js.map
  18. 1 1
      node_modules/.vite/deps/i18next.js
  19. 1 1
      node_modules/.vite/deps/lucide-react.js
  20. 0 0
      node_modules/.vite/deps/lucide-react.js.map
  21. 2 2
      node_modules/.vite/deps/react-dom_client.js
  22. 1 1
      node_modules/.vite/deps/react-dom_client.js.map
  23. 4 4
      node_modules/.vite/deps/react-i18next.js
  24. 0 0
      node_modules/.vite/deps/react-i18next.js.map
  25. 2 2
      node_modules/.vite/deps/react-router-dom.js
  26. 0 0
      node_modules/.vite/deps/react-router-dom.js.map
  27. 1 1
      node_modules/.vite/deps/react.js
  28. 1 1
      node_modules/.vite/deps/react_jsx-dev-runtime.js
  29. 0 0
      node_modules/.vite/deps/react_jsx-dev-runtime.js.map
  30. 1 1
      node_modules/.vite/deps/react_jsx-runtime.js
  31. 0 0
      node_modules/.vite/deps/react_jsx-runtime.js.map
  32. 2 2
      node_modules/.vite/deps/recharts.js
  33. 0 0
      node_modules/.vite/deps/recharts.js.map
  34. 2 2
      node_modules/.vite/deps/sonner.js
  35. 0 0
      node_modules/.vite/deps/sonner.js.map
  36. 2 2
      package-lock.json
  37. 1 1
      package.json
  38. 543 70
      src/Dashboard.tsx
  39. 44 34
      src/components/DepartmentManagement.tsx
  40. 49 0
      src/components/PermissionWrapper.tsx
  41. 7 0
      src/utils/auth.ts
  42. 231 0
      src/utils/permission.ts
  43. 7 1
      src/views/Login.tsx

+ 1 - 1
.env

@@ -2,7 +2,7 @@
 VITE_APP_TITLE=能量隔离系统
 
 # 项目本地运行端口号
-VITE_PORT=80
+VITE_PORT=81
 # 请求路径
 VITE_BASE_URL='http://120.27.232.27:48080'
 # open 运行 npm run dev 时自动打开浏览器

+ 2 - 2
README.md

@@ -1,7 +1,7 @@
 
-  # 企业SaaS登录页面设计
+  # 能量隔离系统
 
-  This is a code bundle for 企业SaaS登录页面设计. The original project is available at https://www.figma.com/design/ejgJy766OHlP3QRm4b5nee/%E4%BC%81%E4%B8%9ASaaS%E7%99%BB%E5%BD%95%E9%A1%B5%E9%9D%A2%E8%AE%BE%E8%AE%A1.
+  This is a code bundle for 能量隔离系统. The original project is available at https://www.figma.com/design/ejgJy766OHlP3QRm4b5nee/%E4%BC%81%E4%B8%9ASaaS%E7%99%BB%E5%BD%95%E9%A1%B5%E9%9D%A2%E8%AE%BE%E8%AE%A1.
 
   ## Running the code
 


+ 1 - 1
build/index.html

@@ -4,7 +4,7 @@
     <head>
       <meta charset="UTF-8" />
       <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-      <title>企业SaaS登录页面设计</title>
+      <title>能量隔离系统</title>
       <script type="module" crossorigin src="/assets/index-DYNIqP4U.js"></script>
       <link rel="stylesheet" crossorigin href="/assets/index-CgdPl1P3.css">
     </head>

+ 335 - 0
menu-config.json

@@ -0,0 +1,335 @@
+{
+  "menus": [
+    {
+      "id": 1,
+      "parentId": 0,
+      "name": "驾驶舱",
+      "path": "/cockpit",
+      "component": null,
+      "componentName": null,
+      "icon": "ep:monitor",
+      "visible": true,
+      "keepAlive": false,
+      "alwaysShow": true,
+      "order": 1,
+      "children": null,
+      "description": "驾驶舱/仪表板 - 组件文件: src/components/CockpitDashboard.tsx"
+    },
+    {
+      "id": 2,
+      "parentId": 0,
+      "name": "系统配置",
+      "path": "/system",
+      "component": null,
+      "componentName": null,
+      "icon": "ep:tools",
+      "visible": true,
+      "keepAlive": false,
+      "alwaysShow": true,
+      "order": 2,
+      "children": [
+        {
+          "id": 21,
+          "parentId": 2,
+          "name": "菜单管理",
+          "path": "/system/menu",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:menu",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 1,
+          "children": null,
+          "description": "组件文件: src/components/SystemConfig.tsx, subMenu: 菜单管理"
+        },
+        {
+          "id": 22,
+          "parentId": 2,
+          "name": "部门管理",
+          "path": "/system/dept",
+          "component": null,
+          "componentName": null,
+          "icon": "fa:address-card",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 2,
+          "children": null,
+          "description": "组件文件: src/components/DepartmentManagement.tsx, subMenu: 部门管理"
+        },
+        {
+          "id": 23,
+          "parentId": 2,
+          "name": "岗位管理",
+          "path": "/system/post",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:collection",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 3,
+          "children": null,
+          "description": "组件文件: src/components/PositionManagement.tsx, subMenu: 岗位管理"
+        },
+        {
+          "id": 24,
+          "parentId": 2,
+          "name": "角色管理",
+          "path": "/system/role",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:user",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 4,
+          "children": null,
+          "description": "组件文件: src/components/SystemConfig.tsx, subMenu: 角色管理"
+        },
+        {
+          "id": 25,
+          "parentId": 2,
+          "name": "字典管理",
+          "path": "/system/dict",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:book-open",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 5,
+          "children": null,
+          "description": "组件文件: src/components/SystemConfig.tsx, subMenu: 字典管理"
+        },
+        {
+          "id": 26,
+          "parentId": 2,
+          "name": "机柜管理",
+          "path": "/system/cabinet",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:server",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 6,
+          "children": null,
+          "description": "组件文件: src/components/SystemConfig.tsx, subMenu: 机柜管理"
+        }
+      ],
+      "description": "系统配置 - 组件文件: src/components/SystemConfig.tsx"
+    },
+    {
+      "id": 3,
+      "parentId": 0,
+      "name": "用户管理",
+      "path": "/users",
+      "component": null,
+      "componentName": null,
+      "icon": "ep:avatar",
+      "visible": true,
+      "keepAlive": false,
+      "alwaysShow": true,
+      "order": 3,
+      "children": [
+        {
+          "id": 31,
+          "parentId": 3,
+          "name": "用户列表",
+          "path": "/users/list",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:user",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 1,
+          "children": null,
+          "description": "组件文件: src/components/UserManagement.tsx, subMenu: 用户列表"
+        },
+        {
+          "id": 32,
+          "parentId": 3,
+          "name": "通知管理",
+          "path": "/users/notification",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:bell",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 2,
+          "children": null,
+          "description": "组件文件: src/components/UserManagement.tsx, subMenu: 通知管理"
+        }
+      ],
+      "description": "用户管理 - 组件文件: src/components/UserManagement.tsx"
+    },
+    {
+      "id": 4,
+      "parentId": 0,
+      "name": "硬件管理",
+      "path": "/hw",
+      "component": null,
+      "componentName": null,
+      "icon": "ep:briefcase",
+      "visible": true,
+      "keepAlive": false,
+      "alwaysShow": true,
+      "order": 4,
+      "children": [
+        {
+          "id": 41,
+          "parentId": 4,
+          "name": "机柜",
+          "path": "/hw/cabinet",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:server",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 1,
+          "children": null,
+          "description": "组件文件: src/components/HardwareManagement.tsx, subMenu: 机柜"
+        },
+        {
+          "id": 42,
+          "parentId": 4,
+          "name": "钥匙",
+          "path": "/hw/key",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:lock",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 2,
+          "children": null,
+          "description": "组件文件: src/components/HardwareManagement.tsx, subMenu: 钥匙"
+        },
+        {
+          "id": 43,
+          "parentId": 4,
+          "name": "挂锁",
+          "path": "/hw/lock",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:lock",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 3,
+          "children": null,
+          "description": "组件文件: src/components/HardwareManagement.tsx, subMenu: 挂锁"
+        },
+        {
+          "id": 44,
+          "parentId": 4,
+          "name": "便携式",
+          "path": "/hw/portable",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:radio",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 4,
+          "children": null,
+          "description": "组件文件: src/components/HardwareManagement.tsx, subMenu: 便携式"
+        }
+      ],
+      "description": "硬件管理 - 组件文件: src/components/HardwareManagement.tsx"
+    },
+    {
+      "id": 5,
+      "parentId": 0,
+      "name": "点位管理",
+      "path": "/points",
+      "component": null,
+      "componentName": null,
+      "icon": "ep:delete-location",
+      "visible": true,
+      "keepAlive": false,
+      "alwaysShow": true,
+      "order": 5,
+      "children": null,
+      "description": "点位管理 - 组件文件: src/components/LocationManagement.tsx"
+    },
+    {
+      "id": 6,
+      "parentId": 0,
+      "name": "隔离作业",
+      "path": "/jobTicket",
+      "component": null,
+      "componentName": null,
+      "icon": "ep:comment",
+      "visible": true,
+      "keepAlive": false,
+      "alwaysShow": true,
+      "order": 6,
+      "children": [
+        {
+          "id": 61,
+          "parentId": 6,
+          "name": "流程模板",
+          "path": "/jobTicket/step",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:document-copy",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 1,
+          "children": null,
+          "description": "组件文件: src/components/IsolationWork.tsx, subMenu: 流程模板"
+        },
+        {
+          "id": 62,
+          "parentId": 6,
+          "name": "SOP管理",
+          "path": "/jobTicket/sop",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:book-open",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 2,
+          "children": null,
+          "description": "组件文件: src/components/IsolationWork.tsx, subMenu: SOP管理"
+        },
+        {
+          "id": 63,
+          "parentId": 6,
+          "name": "作业管理",
+          "path": "/jobTicket/job",
+          "component": null,
+          "componentName": null,
+          "icon": "ep:activity",
+          "visible": true,
+          "keepAlive": false,
+          "alwaysShow": false,
+          "order": 3,
+          "children": null,
+          "description": "组件文件: src/components/IsolationWork.tsx, subMenu: 作业管理"
+        }
+      ],
+      "description": "隔离作业 - 组件文件: src/components/IsolationWork.tsx"
+    }
+  ],
+  "description": "React系统菜单配置 - 用于后台菜单系统导入",
+  "version": "1.0",
+  "lastUpdate": "2025-01-XX",
+  "notes": [
+    "此配置可直接导入到后台菜单系统",
+    "path字段必须与配置中的值完全一致",
+    "前端会自动识别多种路径格式(如 /hw 和 /hardware)",
+    "description字段包含组件文件路径和subMenu参数值,仅供参考",
+    "visible字段控制菜单是否显示,true表示显示,false表示隐藏",
+    "order字段控制菜单显示顺序,数字越小越靠前"
+  ]
+}
+

+ 22 - 22
node_modules/.vite/deps/_metadata.json

@@ -1,91 +1,91 @@
 {
-  "hash": "f0827c71",
-  "configHash": "552d8dd2",
-  "lockfileHash": "fb0219eb",
-  "browserHash": "8a267d70",
+  "hash": "887fe11f",
+  "configHash": "9ce89449",
+  "lockfileHash": "b58c4982",
+  "browserHash": "e6473ada",
   "optimized": {
     "react/jsx-dev-runtime": {
       "src": "../../react/jsx-dev-runtime.js",
       "file": "react_jsx-dev-runtime.js",
-      "fileHash": "4aae8993",
+      "fileHash": "5f61a5a4",
       "needsInterop": true
     },
     "axios": {
       "src": "../../axios/index.js",
       "file": "axios.js",
-      "fileHash": "37f8bcce",
+      "fileHash": "b5b9002c",
       "needsInterop": false
     },
     "i18next": {
       "src": "../../i18next/dist/esm/i18next.js",
       "file": "i18next.js",
-      "fileHash": "64db7147",
+      "fileHash": "fda51781",
       "needsInterop": false
     },
     "i18next-browser-languagedetector": {
       "src": "../../i18next-browser-languagedetector/dist/esm/i18nextBrowserLanguageDetector.js",
       "file": "i18next-browser-languagedetector.js",
-      "fileHash": "c34558f7",
+      "fileHash": "d2e684b4",
       "needsInterop": false
     },
     "lucide-react": {
       "src": "../../lucide-react/dist/esm/lucide-react.js",
       "file": "lucide-react.js",
-      "fileHash": "4e4531e4",
+      "fileHash": "a53466d9",
       "needsInterop": false
     },
     "react": {
       "src": "../../react/index.js",
       "file": "react.js",
-      "fileHash": "7b01e280",
+      "fileHash": "49c41c21",
       "needsInterop": true
     },
     "react-dom/client": {
       "src": "../../react-dom/client.js",
       "file": "react-dom_client.js",
-      "fileHash": "ecd45c05",
+      "fileHash": "05dc0e0e",
       "needsInterop": true
     },
     "react-i18next": {
       "src": "../../react-i18next/dist/es/index.js",
       "file": "react-i18next.js",
-      "fileHash": "2053041d",
+      "fileHash": "d9038141",
       "needsInterop": false
     },
     "react-router-dom": {
       "src": "../../react-router-dom/dist/index.mjs",
       "file": "react-router-dom.js",
-      "fileHash": "f9c98201",
+      "fileHash": "dfe6723e",
       "needsInterop": false
     },
     "react/jsx-runtime": {
       "src": "../../react/jsx-runtime.js",
       "file": "react_jsx-runtime.js",
-      "fileHash": "4dd57301",
+      "fileHash": "aae4bcaf",
       "needsInterop": true
     },
     "recharts": {
       "src": "../../recharts/es6/index.js",
       "file": "recharts.js",
-      "fileHash": "c04da1fe",
+      "fileHash": "44fefeea",
       "needsInterop": false
     },
     "sonner": {
       "src": "../../sonner/dist/index.mjs",
       "file": "sonner.js",
-      "fileHash": "db96482c",
+      "fileHash": "23263cf0",
       "needsInterop": false
     }
   },
   "chunks": {
-    "chunk-K23GC2QC": {
-      "file": "chunk-K23GC2QC.js"
+    "chunk-AGDJ642P": {
+      "file": "chunk-AGDJ642P.js"
     },
-    "chunk-DRWLMN53": {
-      "file": "chunk-DRWLMN53.js"
+    "chunk-BANT3OPS": {
+      "file": "chunk-BANT3OPS.js"
     },
-    "chunk-HCC4GIHT": {
-      "file": "chunk-HCC4GIHT.js"
+    "chunk-QRULMDK5": {
+      "file": "chunk-QRULMDK5.js"
     },
     "chunk-G3PMV62Z": {
       "file": "chunk-G3PMV62Z.js"

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
node_modules/.vite/deps/axios.js.map


+ 1 - 1
node_modules/.vite/deps/chunk-HCC4GIHT.js → node_modules/.vite/deps/chunk-AGDJ642P.js

@@ -2233,4 +2233,4 @@ export {
   loadNamespaces,
   loadLanguages
 };
-//# sourceMappingURL=chunk-HCC4GIHT.js.map
+//# sourceMappingURL=chunk-AGDJ642P.js.map

Разлика између датотеке није приказан због своје велике величине
+ 3 - 0
node_modules/.vite/deps/chunk-AGDJ642P.js.map


+ 2 - 2
node_modules/.vite/deps/chunk-K23GC2QC.js → node_modules/.vite/deps/chunk-BANT3OPS.js

@@ -1,6 +1,6 @@
 import {
   require_react
-} from "./chunk-DRWLMN53.js";
+} from "./chunk-QRULMDK5.js";
 import {
   __commonJS
 } from "./chunk-G3PMV62Z.js";
@@ -21684,4 +21684,4 @@ react-dom/cjs/react-dom.development.js:
    * @license Modernizr 3.0.0pre (Custom Build) | MIT
    *)
 */
-//# sourceMappingURL=chunk-K23GC2QC.js.map
+//# sourceMappingURL=chunk-BANT3OPS.js.map

Разлика између датотеке није приказан због своје велике величине
+ 3 - 0
node_modules/.vite/deps/chunk-BANT3OPS.js.map


Разлика између датотеке није приказан због своје велике величине
+ 0 - 3
node_modules/.vite/deps/chunk-DRWLMN53.js.map


Разлика између датотеке није приказан због своје велике величине
+ 0 - 3
node_modules/.vite/deps/chunk-HCC4GIHT.js.map


Разлика између датотеке није приказан због своје велике величине
+ 0 - 3
node_modules/.vite/deps/chunk-K23GC2QC.js.map


+ 1 - 1
node_modules/.vite/deps/chunk-DRWLMN53.js → node_modules/.vite/deps/chunk-QRULMDK5.js

@@ -1903,4 +1903,4 @@ react/cjs/react.development.js:
    * LICENSE file in the root directory of this source tree.
    *)
 */
-//# sourceMappingURL=chunk-DRWLMN53.js.map
+//# sourceMappingURL=chunk-QRULMDK5.js.map

Разлика између датотеке није приказан због своје велике величине
+ 3 - 0
node_modules/.vite/deps/chunk-QRULMDK5.js.map


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
node_modules/.vite/deps/i18next-browser-languagedetector.js.map


+ 1 - 1
node_modules/.vite/deps/i18next.js

@@ -15,7 +15,7 @@ import {
   setDefaultNamespace,
   t,
   use
-} from "./chunk-HCC4GIHT.js";
+} from "./chunk-AGDJ642P.js";
 import "./chunk-G3PMV62Z.js";
 export {
   changeLanguage,

+ 1 - 1
node_modules/.vite/deps/lucide-react.js

@@ -1,6 +1,6 @@
 import {
   require_react
-} from "./chunk-DRWLMN53.js";
+} from "./chunk-QRULMDK5.js";
 import {
   __export,
   __toESM

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
node_modules/.vite/deps/lucide-react.js.map


+ 2 - 2
node_modules/.vite/deps/react-dom_client.js

@@ -1,7 +1,7 @@
 import {
   require_react_dom
-} from "./chunk-K23GC2QC.js";
-import "./chunk-DRWLMN53.js";
+} from "./chunk-BANT3OPS.js";
+import "./chunk-QRULMDK5.js";
 import {
   __commonJS
 } from "./chunk-G3PMV62Z.js";

+ 1 - 1
node_modules/.vite/deps/react-dom_client.js.map

@@ -1,7 +1,7 @@
 {
   "version": 3,
   "sources": ["../../react-dom/client.js"],
-  "sourcesContent": ["'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n  exports.createRoot = m.createRoot;\n  exports.hydrateRoot = m.hydrateRoot;\n} else {\n  var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n  exports.createRoot = function(c, o) {\n    i.usingClientEntryPoint = true;\n    try {\n      return m.createRoot(c, o);\n    } finally {\n      i.usingClientEntryPoint = false;\n    }\n  };\n  exports.hydrateRoot = function(c, h, o) {\n    i.usingClientEntryPoint = true;\n    try {\n      return m.hydrateRoot(c, h, o);\n    } finally {\n      i.usingClientEntryPoint = false;\n    }\n  };\n}\n"],
+  "sourcesContent": ["'use strict';\r\n\r\nvar m = require('react-dom');\r\nif (process.env.NODE_ENV === 'production') {\r\n  exports.createRoot = m.createRoot;\r\n  exports.hydrateRoot = m.hydrateRoot;\r\n} else {\r\n  var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\r\n  exports.createRoot = function(c, o) {\r\n    i.usingClientEntryPoint = true;\r\n    try {\r\n      return m.createRoot(c, o);\r\n    } finally {\r\n      i.usingClientEntryPoint = false;\r\n    }\r\n  };\r\n  exports.hydrateRoot = function(c, h, o) {\r\n    i.usingClientEntryPoint = true;\r\n    try {\r\n      return m.hydrateRoot(c, h, o);\r\n    } finally {\r\n      i.usingClientEntryPoint = false;\r\n    }\r\n  };\r\n}\r\n"],
   "mappings": ";;;;;;;;;AAAA;AAAA;AAEA,QAAI,IAAI;AACR,QAAI,OAAuC;AACzC,cAAQ,aAAa,EAAE;AACvB,cAAQ,cAAc,EAAE;AAAA,IAC1B,OAAO;AACD,UAAI,EAAE;AACV,cAAQ,aAAa,SAAS,GAAG,GAAG;AAClC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,WAAW,GAAG,CAAC;AAAA,QAC1B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AACA,cAAQ,cAAc,SAAS,GAAG,GAAG,GAAG;AACtC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,YAAY,GAAG,GAAG,CAAC;AAAA,QAC9B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAjBM;AAAA;AAAA;",
   "names": []
 }

+ 4 - 4
node_modules/.vite/deps/react-i18next.js

@@ -1,9 +1,9 @@
-import {
-  require_react
-} from "./chunk-DRWLMN53.js";
 import {
   keysFromSelector
-} from "./chunk-HCC4GIHT.js";
+} from "./chunk-AGDJ642P.js";
+import {
+  require_react
+} from "./chunk-QRULMDK5.js";
 import {
   __commonJS,
   __toESM

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
node_modules/.vite/deps/react-i18next.js.map


+ 2 - 2
node_modules/.vite/deps/react-router-dom.js

@@ -1,9 +1,9 @@
 import {
   require_react_dom
-} from "./chunk-K23GC2QC.js";
+} from "./chunk-BANT3OPS.js";
 import {
   require_react
-} from "./chunk-DRWLMN53.js";
+} from "./chunk-QRULMDK5.js";
 import {
   __commonJS,
   __toESM

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
node_modules/.vite/deps/react-router-dom.js.map


+ 1 - 1
node_modules/.vite/deps/react.js

@@ -1,5 +1,5 @@
 import {
   require_react
-} from "./chunk-DRWLMN53.js";
+} from "./chunk-QRULMDK5.js";
 import "./chunk-G3PMV62Z.js";
 export default require_react();

+ 1 - 1
node_modules/.vite/deps/react_jsx-dev-runtime.js

@@ -1,6 +1,6 @@
 import {
   require_react
-} from "./chunk-DRWLMN53.js";
+} from "./chunk-QRULMDK5.js";
 import {
   __commonJS
 } from "./chunk-G3PMV62Z.js";

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
node_modules/.vite/deps/react_jsx-dev-runtime.js.map


+ 1 - 1
node_modules/.vite/deps/react_jsx-runtime.js

@@ -1,6 +1,6 @@
 import {
   require_react
-} from "./chunk-DRWLMN53.js";
+} from "./chunk-QRULMDK5.js";
 import {
   __commonJS
 } from "./chunk-G3PMV62Z.js";

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
node_modules/.vite/deps/react_jsx-runtime.js.map


+ 2 - 2
node_modules/.vite/deps/recharts.js

@@ -1,9 +1,9 @@
 import {
   require_react_dom
-} from "./chunk-K23GC2QC.js";
+} from "./chunk-BANT3OPS.js";
 import {
   require_react
-} from "./chunk-DRWLMN53.js";
+} from "./chunk-QRULMDK5.js";
 import {
   __commonJS,
   __export,

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
node_modules/.vite/deps/recharts.js.map


+ 2 - 2
node_modules/.vite/deps/sonner.js

@@ -1,10 +1,10 @@
 "use client";
 import {
   require_react_dom
-} from "./chunk-K23GC2QC.js";
+} from "./chunk-BANT3OPS.js";
 import {
   require_react
-} from "./chunk-DRWLMN53.js";
+} from "./chunk-QRULMDK5.js";
 import {
   __toESM
 } from "./chunk-G3PMV62Z.js";

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
node_modules/.vite/deps/sonner.js.map


+ 2 - 2
package-lock.json

@@ -1,11 +1,11 @@
 {
-      "name": "企业SaaS登录页面设计",
+      "name": "能量隔离系统",
       "version": "0.1.0",
       "lockfileVersion": 3,
       "requires": true,
       "packages": {
             "": {
-                  "name": "企业SaaS登录页面设计",
+                  "name": "能量隔离系统",
                   "version": "0.1.0",
                   "dependencies": {
                         "@radix-ui/react-accordion": "^1.2.3",

+ 1 - 1
package.json

@@ -1,5 +1,5 @@
 {
-      "name": "企业SaaS登录页面设计",
+      "name": "能量隔离系统",
       "version": "0.1.0",
       "private": true,
       "dependencies": {

+ 543 - 70
src/Dashboard.tsx

@@ -1,7 +1,7 @@
-import React, { useState } from 'react';
+import React, { useState, useMemo } from 'react';
 import { useNavigate } 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 } 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 UserManagement from './components/UserManagement';
 import HardwareManagement from './components/HardwareManagement';
@@ -12,6 +12,7 @@ import CockpitDashboard from './components/CockpitDashboard';
 import { authApi } from './api';
 import { toast } from 'sonner';
 import { env } from './config/env';
+import { getMenus, hasMenuPathPermission, mapMenuPathToKey, getPermissionUser } from './utils/permission';
 
 export default function Dashboard() {
   const navigate = useNavigate();
@@ -19,9 +20,13 @@ export default function Dashboard() {
   const [activeMenu, setActiveMenu] = useState('dashboard');
   const [activeSubMenu, setActiveSubMenu] = useState('menuManagement');
   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 [showProfileSettings, setShowProfileSettings] = useState(false);
+  const [avatarError, setAvatarError] = useState(false);
+
+  // 获取用户信息
+  const userInfo = useMemo(() => getPermissionUser(), []);
 
   // 切换语言
   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 (
     <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">
-                {mainMenus.map((item) => {
+                {filteredMainMenus.map((item) => {
                   const Icon = item.icon;
                   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 (
                       <div
                         key={item.key}
                         className="relative"
                         onMouseEnter={() => {
                           if (dropdownTimer) clearTimeout(dropdownTimer);
-                          setShowSystemConfigDropdown(true);
+                          setShowDropdownMenu(item.key);
                         }}
                         onMouseLeave={() => {
-                          const timer = setTimeout(() => setShowSystemConfigDropdown(false), 200);
+                          const timer = setTimeout(() => setShowDropdownMenu(null), 200);
                           setDropdownTimer(timer);
                         }}
                       >
@@ -133,24 +546,24 @@ export default function Dashboard() {
                           }`}
                         >
                           <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>
                         
                         {/* 下拉菜单 - 网格布局 */}
-                        {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="grid grid-cols-2 gap-2">
-                              {subMenuConfig['systemConfig'].map((subItem) => {
+                              {(filteredSubMenuConfig[item.key] || []).map((subItem) => {
                                 const IconComponent = subItem.icon;
-                                const isActive = activeMenu === 'systemConfig' && activeSubMenu === subItem.key;
+                                const isActive = activeMenu === item.key && activeSubMenu === subItem.key;
                                 return (
                                   <button
                                     key={subItem.key}
                                     onClick={() => {
-                                      setActiveMenu('systemConfig');
+                                      setActiveMenu(item.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 ${
                                       isActive
@@ -163,7 +576,7 @@ export default function Dashboard() {
                                     }`}>
                                       <IconComponent className="w-4 h-4" strokeWidth={2.5} />
                                     </div>
-                                    <span>{t(`systemConfig.${subItem.key}`)}</span>
+                                    <span>{subItem.name || t(`systemConfig.${subItem.key}`)}</span>
                                   </button>
                                 );
                               })}
@@ -174,13 +587,13 @@ export default function Dashboard() {
                     );
                   }
                   
-                  // 其他普通菜单
+                  // 其他普通菜单(用户管理、硬件管理、隔离作业等)- 它们的二级菜单在页面内用 tab 标签显示
                   return (
                     <button
                       key={item.key}
                       onClick={() => {
                         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 ${
                         isActive
@@ -189,7 +602,7 @@ export default function Dashboard() {
                       }`}
                     >
                       <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>
                   );
                 })}
@@ -207,11 +620,42 @@ export default function Dashboard() {
                 <span className="text-sm text-gray-700">{i18n.language === 'zh' ? 'EN' : '中文'}</span>
               </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">
@@ -219,12 +663,21 @@ export default function Dashboard() {
                   onClick={() => setShowUserMenu(!showUserMenu)}
                   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-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>
                   <ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${showUserMenu ? 'rotate-180' : ''}`} />
                 </button>
@@ -257,15 +710,18 @@ export default function Dashboard() {
 
       {/* 主内容区 */}
       <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="flex items-center gap-2 overflow-x-auto">
-              {subMenuConfig[activeMenu]?.map((item) => {
+              {filteredSubMenuConfig[activeMenu]?.map((item) => {
                 const isActive = activeSubMenu === item.key;
                 const menuKey = activeMenu === 'userManagement' ? 'userManagement' : 
                                activeMenu === 'hardwareManagement' ? 'hardwareManagement' : 
-                               activeMenu === 'isolationWork' ? 'isolationWork' : '';
+                               activeMenu === 'isolationWork' ? 'isolationWork' :
+                               activeMenu === 'notificationManagement' ? 'notificationManagement' : '';
                 return (
                   <button
                     key={item.key}
@@ -276,7 +732,7 @@ export default function Dashboard() {
                         : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
                     }`}
                   >
-                    {t(`${menuKey}.${item.key}`)}
+                    {item.name || t(`${menuKey}.${item.key}`)}
                   </button>
                 );
               })}
@@ -299,7 +755,24 @@ export default function Dashboard() {
           <LocationManagement />
         ) : activeMenu === 'isolationWork' ? (
           <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>
   );

+ 44 - 34
src/components/DepartmentManagement.tsx

@@ -1,5 +1,7 @@
 import React, { useState } from 'react';
 import { Plus, Search, Edit2, Trash2, MoreVertical, ChevronRight, ChevronDown, Building2 } from 'lucide-react';
+import PermissionWrapper from './PermissionWrapper';
+import { hasPermission } from '../utils/permission';
 
 interface DepartmentNode {
   id: number;
@@ -273,16 +275,18 @@ export default function DepartmentManagement() {
         <div className="bg-white rounded-2xl border border-gray-200/50 shadow-sm p-4 h-full flex flex-col">
           <div className="flex items-center justify-between mb-4 pb-3 border-b border-gray-200">
             <h3 className="text-base text-gray-900">组织架构</h3>
-            <button
-              onClick={() => {
-                setEditingItem(null);
-                setShowAddModal(true);
-              }}
-              className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
-              title="新增部门"
-            >
-              <Plus className="w-4 h-4" />
-            </button>
+            <PermissionWrapper permission="system:dept:create">
+              <button
+                onClick={() => {
+                  setEditingItem(null);
+                  setShowAddModal(true);
+                }}
+                className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
+                title="新增部门"
+              >
+                <Plus className="w-4 h-4" />
+              </button>
+            </PermissionWrapper>
           </div>
           
           <div className="flex-1 overflow-y-auto">
@@ -307,16 +311,18 @@ export default function DepartmentManagement() {
               />
             </div>
 
-            <button
-              onClick={() => {
-                setEditingItem(null);
-                setShowAddModal(true);
-              }}
-              className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
-            >
-              <Plus className="w-4 h-4" strokeWidth={2.5} />
-              <span className="text-sm">新增部门</span>
-            </button>
+            <PermissionWrapper permission="system:dept:create">
+              <button
+                onClick={() => {
+                  setEditingItem(null);
+                  setShowAddModal(true);
+                }}
+                className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:shadow-lg hover:shadow-blue-400/40 transition-all duration-300"
+              >
+                <Plus className="w-4 h-4" strokeWidth={2.5} />
+                <span className="text-sm">新增部门</span>
+              </button>
+            </PermissionWrapper>
           </div>
         </div>
 
@@ -369,20 +375,24 @@ export default function DepartmentManagement() {
                     <td className="px-6 py-4 text-sm text-gray-900">{dept.createTime}</td>
                     <td className="px-6 py-4">
                       <div className="flex items-center justify-center gap-2">
-                        <button
-                          onClick={() => handleEdit(dept)}
-                          className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
-                          title="编辑"
-                        >
-                          <Edit2 className="w-4 h-4" />
-                        </button>
-                        <button
-                          onClick={() => handleDelete(dept.id)}
-                          className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
-                          title="删除"
-                        >
-                          <Trash2 className="w-4 h-4" />
-                        </button>
+                        <PermissionWrapper permission="system:dept:update">
+                          <button
+                            onClick={() => handleEdit(dept)}
+                            className="p-2 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors"
+                            title="编辑"
+                          >
+                            <Edit2 className="w-4 h-4" />
+                          </button>
+                        </PermissionWrapper>
+                        <PermissionWrapper permission="system:dept:delete">
+                          <button
+                            onClick={() => handleDelete(dept.id)}
+                            className="p-2 text-red-600 hover:bg-red-100 rounded-lg transition-colors"
+                            title="删除"
+                          >
+                            <Trash2 className="w-4 h-4" />
+                          </button>
+                        </PermissionWrapper>
                         <button
                           className="p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
                           title="更多"

+ 49 - 0
src/components/PermissionWrapper.tsx

@@ -0,0 +1,49 @@
+import React from 'react';
+import { hasPermission, hasAnyPermission, hasAllPermissions, hasRole } from '../utils/permission';
+
+interface PermissionWrapperProps {
+  children: React.ReactNode;
+  permission?: string;
+  permissions?: string[];
+  requireAll?: boolean;
+  role?: string;
+  fallback?: React.ReactNode;
+}
+
+/**
+ * 权限包装组件
+ * 根据权限控制子组件的显示
+ */
+export default function PermissionWrapper({
+  children,
+  permission,
+  permissions,
+  requireAll = false,
+  role,
+  fallback = null,
+}: PermissionWrapperProps) {
+  // 检查角色权限
+  if (role && !hasRole(role)) {
+    return <>{fallback}</>;
+  }
+
+  // 检查单个权限
+  if (permission && !hasPermission(permission)) {
+    return <>{fallback}</>;
+  }
+
+  // 检查多个权限
+  if (permissions && permissions.length > 0) {
+    const hasAccess = requireAll
+      ? hasAllPermissions(permissions)
+      : hasAnyPermission(permissions);
+    
+    if (!hasAccess) {
+      return <>{fallback}</>;
+    }
+  }
+
+  // 如果没有指定权限检查,默认显示
+  return <>{children}</>;
+}
+

+ 7 - 0
src/utils/auth.ts

@@ -205,5 +205,12 @@ export const clearAuth = () => {
   removeUser();
   removeRoleRouters();
   removeHmLvt();
+  // 清除权限信息
+  try {
+    const { clearPermissionInfo } = require('./permission');
+    clearPermissionInfo();
+  } catch (e) {
+    // 如果权限模块未加载,忽略错误
+  }
 };
 

+ 231 - 0
src/utils/permission.ts

@@ -0,0 +1,231 @@
+// 权限管理工具函数
+
+const PERMISSION_KEY = 'permissionInfo';
+const USER_KEY = 'user';
+const ROLES_KEY = 'roles';
+const PERMISSIONS_KEY = 'permissions';
+const MENUS_KEY = 'menus';
+
+// 权限信息接口
+export interface PermissionInfo {
+  user: {
+    id: number;
+    nickname: string;
+    avatar?: string;
+    deptId?: number;
+    username: string;
+    email?: string;
+  };
+  roles: string[];
+  permissions: string[];
+  menus: MenuItem[];
+  uiComponentList?: any[];
+}
+
+// 菜单项接口
+export interface MenuItem {
+  id: number;
+  parentId: number;
+  name: string;
+  path: string;
+  component?: string | null;
+  componentName?: string | null;
+  icon?: string;
+  visible: boolean;
+  keepAlive?: boolean;
+  alwaysShow?: boolean;
+  children?: MenuItem[] | null;
+}
+
+// 设置权限信息
+export const setPermissionInfo = (info: PermissionInfo) => {
+  localStorage.setItem(PERMISSION_KEY, JSON.stringify(info));
+  if (info.user) {
+    localStorage.setItem(USER_KEY, JSON.stringify(info.user));
+  }
+  if (info.roles) {
+    localStorage.setItem(ROLES_KEY, JSON.stringify(info.roles));
+  }
+  if (info.permissions) {
+    localStorage.setItem(PERMISSIONS_KEY, JSON.stringify(info.permissions));
+  }
+  if (info.menus) {
+    localStorage.setItem(MENUS_KEY, JSON.stringify(info.menus));
+  }
+};
+
+// 获取权限信息
+export const getPermissionInfo = (): PermissionInfo | null => {
+  const info = localStorage.getItem(PERMISSION_KEY);
+  return info ? JSON.parse(info) : null;
+};
+
+// 获取用户信息
+export const getPermissionUser = () => {
+  const info = getPermissionInfo();
+  return info?.user || null;
+};
+
+// 获取角色列表
+export const getRoles = (): string[] => {
+  const info = getPermissionInfo();
+  return info?.roles || [];
+};
+
+// 获取权限列表
+export const getPermissions = (): string[] => {
+  const info = getPermissionInfo();
+  return info?.permissions || [];
+};
+
+// 获取菜单列表
+export const getMenus = (): MenuItem[] => {
+  const info = getPermissionInfo();
+  return info?.menus || [];
+};
+
+// 检查是否有某个权限
+export const hasPermission = (permission: string): boolean => {
+  const permissions = getPermissions();
+  // 如果权限列表为空或包含空字符串,表示有所有权限(超级管理员)
+  if (permissions.length === 0 || permissions.includes('')) {
+    return true;
+  }
+  return permissions.includes(permission);
+};
+
+// 检查是否有某个角色
+export const hasRole = (role: string): boolean => {
+  const roles = getRoles();
+  return roles.includes(role);
+};
+
+// 检查是否有任一权限
+export const hasAnyPermission = (permissions: string[]): boolean => {
+  if (permissions.length === 0) return true;
+  return permissions.some(permission => hasPermission(permission));
+};
+
+// 检查是否有所有权限
+export const hasAllPermissions = (permissions: string[]): boolean => {
+  if (permissions.length === 0) return true;
+  return permissions.every(permission => hasPermission(permission));
+};
+
+// 根据菜单ID检查是否有菜单权限
+export const hasMenuPermission = (menuId: number): boolean => {
+  const menus = getMenus();
+  // 递归查找菜单
+  const findMenu = (menuList: MenuItem[], id: number): boolean => {
+    for (const menu of menuList) {
+      if (menu.id === id) {
+        return menu.visible !== false;
+      }
+      if (menu.children && menu.children.length > 0) {
+        if (findMenu(menu.children, id)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  };
+  return findMenu(menus, menuId);
+};
+
+// 根据菜单路径检查是否有菜单权限
+export const hasMenuPathPermission = (path: string): boolean => {
+  const menus = getMenus();
+  // 递归查找菜单
+  const findMenuByPath = (menuList: MenuItem[], menuPath: string): boolean => {
+    for (const menu of menuList) {
+      // 匹配完整路径或部分路径
+      if (menu.path === menuPath || menu.path?.endsWith(menuPath)) {
+        return menu.visible !== false;
+      }
+      if (menu.children && menu.children.length > 0) {
+        if (findMenuByPath(menu.children, menuPath)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  };
+  return findMenuByPath(menus, path);
+};
+
+// 过滤菜单(根据权限)
+export const filterMenusByPermission = (menus: MenuItem[]): MenuItem[] => {
+  return menus
+    .filter(menu => menu.visible !== false)
+    .map(menu => {
+      const filteredMenu = { ...menu };
+      if (menu.children && menu.children.length > 0) {
+        filteredMenu.children = filterMenusByPermission(menu.children);
+        // 如果子菜单都被过滤掉了,但菜单本身有component,则保留
+        if (filteredMenu.children.length === 0 && !menu.component) {
+          return null;
+        }
+      }
+      return filteredMenu;
+    })
+    .filter((menu): menu is MenuItem => menu !== null);
+};
+
+// 清除权限信息
+export const clearPermissionInfo = () => {
+  localStorage.removeItem(PERMISSION_KEY);
+  localStorage.removeItem(USER_KEY);
+  localStorage.removeItem(ROLES_KEY);
+  localStorage.removeItem(PERMISSIONS_KEY);
+  localStorage.removeItem(MENUS_KEY);
+};
+
+// 根据菜单路径映射到前端菜单key
+export const mapMenuPathToKey = (path: string): string | null => {
+  // 系统管理相关
+  if (path === '/system' || path.startsWith('/system/')) {
+    if (path.includes('/system/menu') || path === '/system/menu') return 'menuManagement';
+    if (path.includes('/system/dept') || path === '/system/dept') return 'departmentManagement';
+    if (path.includes('/system/post') || path === '/system/post' || path.includes('/system/marsdept')) return 'positionManagement';
+    if (path.includes('/system/role') || path === '/system/role') return 'roleManagement';
+    if (path.includes('/system/dict') || path === '/system/dict') return 'dictionaryManagement';
+    return 'systemConfig';
+  }
+  
+  // 用户管理相关
+  if (path === '/users' || path.startsWith('/users/')) {
+    if (path.includes('/users/list')) return 'userList';
+    if (path.includes('/users/notification')) return 'notificationManagement';
+    return 'userManagement';
+  }
+  
+  // 硬件管理相关
+  if (path === '/hardware' || path.startsWith('/hardware/')) {
+    if (path.includes('/hardware/cabinet')) return 'cabinet';
+    if (path.includes('/hardware/key')) return 'key';
+    if (path.includes('/hardware/lock')) return 'padlock';
+    if (path.includes('/hardware/portable')) return 'portable';
+    return 'hardwareManagement';
+  }
+  
+  // 隔离作业相关
+  if (path === '/isolation' || path.startsWith('/isolation/')) {
+    if (path.includes('/isolation/list')) return 'workManagement';
+    if (path.includes('/isolation/approval')) return 'processTemplate';
+    if (path.includes('/isolation/record')) return 'sopManagement';
+    return 'isolationWork';
+  }
+  
+  // 点位管理
+  if (path === '/points' || path.startsWith('/points/')) {
+    return 'locationManagement';
+  }
+  
+  // 驾驶舱
+  if (path === '/cockpit' || path === '/dashboard') {
+    return 'dashboard';
+  }
+  
+  return null;
+};
+

+ 7 - 1
src/views/Login.tsx

@@ -7,6 +7,7 @@ import type { LoginFormParams } from '../api/Login';
 import { toast } from 'sonner';
 import { env } from '../config/env';
 import * as authUtil from '../utils/auth';
+import { setPermissionInfo } from '../utils/permission';
 
 export default function Login() {
   const navigate = useNavigate();
@@ -269,12 +270,17 @@ export default function Login() {
           loginApi.getDictDataSimpleList()
         ]);
 
-        // 存储权限信息(包含roleRouters)
+        // 存储权限信息(包含roleRouters和完整权限数据
         if (permissionInfo.status === 'fulfilled' && permissionInfo.value) {
           const permissionData = permissionInfo.value as any;
           if (permissionData.roleRouters) {
             authUtil.setRoleRouters(permissionData.roleRouters);
           }
+          // 存储完整的权限信息(user, roles, permissions, menus)
+          // 注意:axios 拦截器已经提取了 data 字段,所以 permissionData 就是 data 的内容
+          if (permissionData.user || permissionData.roles || permissionData.permissions || permissionData.menus) {
+            setPermissionInfo(permissionData);
+          }
         }
 
         // 存储其他数据(如果需要)

Неке датотеке нису приказане због велике количине промена