Prechádzať zdrojové kódy

feat: 添加系统管理-菜单管理 (#929)

* feat: 添加系统管理-菜单管理

* chore: update

* chore: add Copyright in login page

* chore: 将页脚放在一屏可视区

* chore: 依赖更新

* chore: update

* chore: update

* chore: 更新依赖

* chore: update `husky v9`

* style: 适配`el-dialog`样式的更新

* style: update `src/layout/components/search/components/SearchResult.vue`

* chore: update

* style: update

* fix: 修复`ReDialog`中点击取消和确定按钮会触发两次关闭回调

* chore: update

* chore: update src/views/system/menu/README.md

* chore: update

* chore: update

* chore: done

* chore: update
xiaoming 1 rok pred
rodič
commit
c314b0cd1c
45 zmenil súbory, kde vykonal 1946 pridanie a 376 odobranie
  1. 1 1
      .nvmrc
  2. 1 0
      .prettierignore
  3. 1 0
      locales/en.yaml
  4. 1 0
      locales/zh-CN.yaml
  5. 21 8
      mock/asyncRoutes.ts
  6. 614 0
      mock/system.ts
  7. 37 37
      package.json
  8. 237 236
      pnpm-lock.yaml
  9. 1 1
      public/platform-config.json
  10. 5 0
      src/api/system.ts
  11. 0 0
      src/assets/login/illustration.svg
  12. 9 2
      src/components/ReAnimateSelector/src/index.vue
  13. 1 1
      src/components/ReDialog/index.vue
  14. 3 3
      src/components/ReDialog/type.ts
  15. 1 1
      src/components/ReIcon/src/Select.vue
  16. 8 8
      src/components/ReIcon/src/offlineIcon.ts
  17. 5 1
      src/components/RePureTableBar/src/bar.tsx
  18. 1 1
      src/components/ReSegmented/src/type.ts
  19. 7 5
      src/layout/components/footer/index.vue
  20. 1 1
      src/layout/components/notice/data.ts
  21. 1 1
      src/layout/components/search/components/SearchResult.vue
  22. 14 17
      src/layout/components/sidebar/leftCollapse.vue
  23. 2 4
      src/layout/components/sidebar/sidebarItem.vue
  24. 1 1
      src/router/enums.ts
  25. 1 1
      src/router/modules/components.ts
  26. 15 10
      src/style/element-plus.scss
  27. 1 0
      src/style/login.css
  28. 4 4
      src/views/components/animatecss.vue
  29. 4 2
      src/views/components/dialog/index.vue
  30. 2 2
      src/views/components/tag.vue
  31. 12 0
      src/views/login/index.vue
  32. 37 18
      src/views/permission/button/index.vue
  33. 4 1
      src/views/pure-table/base/filters.vue
  34. 15 7
      src/views/system/dept/index.vue
  35. 27 0
      src/views/system/menu/README.md
  36. 326 0
      src/views/system/menu/form.vue
  37. 157 0
      src/views/system/menu/index.vue
  38. 94 0
      src/views/system/menu/utils/enums.ts
  39. 223 0
      src/views/system/menu/utils/hook.tsx
  40. 10 0
      src/views/system/menu/utils/rule.ts
  41. 29 0
      src/views/system/menu/utils/types.ts
  42. 5 0
      src/views/system/role/index.vue
  43. 5 0
      src/views/system/user/index.vue
  44. 1 1
      src/views/system/user/tree.vue
  45. 1 1
      src/views/system/user/utils/hook.tsx

+ 1 - 1
.nvmrc

@@ -1 +1 @@
-v20.11.0
+v20.11.1

+ 1 - 0
.prettierignore

@@ -0,0 +1 @@
+src/views/system/menu/README.md

+ 1 - 0
locales/en.yaml

@@ -29,6 +29,7 @@ menus:
   hssysManagement: System Manage
   hsUser: User Manage
   hsRole: Role Manage
+  hsSystemMenu: Menu Manage
   hsDept: Dept Manage
   hseditor: Editor
   hsabnormal: Abnormal Page

+ 1 - 0
locales/zh-CN.yaml

@@ -29,6 +29,7 @@ menus:
   hssysManagement: 系统管理
   hsUser: 用户管理
   hsRole: 角色管理
+  hsSystemMenu: 菜单管理
   hsDept: 部门管理
   hseditor: 编辑器
   hsabnormal: 异常页面

+ 21 - 8
mock/asyncRoutes.ts

@@ -11,7 +11,7 @@ import { system, permission, frame, tabs } from "@/router/enums";
 const systemRouter = {
   path: "/system",
   meta: {
-    icon: "setting",
+    icon: "ri:settings-3-line",
     title: "menus.hssysManagement",
     rank: system
   },
@@ -20,7 +20,7 @@ const systemRouter = {
       path: "/system/user/index",
       name: "SystemUser",
       meta: {
-        icon: "flUser",
+        icon: "ri:admin-line",
         title: "menus.hsUser",
         roles: ["admin"]
       }
@@ -29,16 +29,25 @@ const systemRouter = {
       path: "/system/role/index",
       name: "SystemRole",
       meta: {
-        icon: "role",
+        icon: "ri:admin-fill",
         title: "menus.hsRole",
         roles: ["admin"]
       }
     },
+    {
+      path: "/system/menu/index",
+      name: "SystemMenu",
+      meta: {
+        icon: "ep:menu",
+        title: "menus.hsSystemMenu",
+        roles: ["admin"]
+      }
+    },
     {
       path: "/system/dept/index",
       name: "SystemDept",
       meta: {
-        icon: "dept",
+        icon: "ri:git-branch-line",
         title: "menus.hsDept",
         roles: ["admin"]
       }
@@ -50,7 +59,7 @@ const permissionRouter = {
   path: "/permission",
   meta: {
     title: "menus.permission",
-    icon: "lollipop",
+    icon: "ep:lollipop",
     rank: permission
   },
   children: [
@@ -68,7 +77,11 @@ const permissionRouter = {
       meta: {
         title: "menus.permissionButton",
         roles: ["admin", "common"],
-        auths: ["btn_add", "btn_edit", "btn_delete"]
+        auths: [
+          "permission:btn:add",
+          "permission:btn:edit",
+          "permission:btn:delete"
+        ]
       }
     }
   ]
@@ -77,7 +90,7 @@ const permissionRouter = {
 const frameRouter = {
   path: "/iframe",
   meta: {
-    icon: "monitor",
+    icon: "ep:monitor",
     title: "menus.hsExternalPage",
     rank: frame
   },
@@ -180,7 +193,7 @@ const frameRouter = {
 const tabsRouter = {
   path: "/tabs",
   meta: {
-    icon: "tag",
+    icon: "ri:bookmark-2-line",
     title: "menus.hstabs",
     rank: tabs
   },

+ 614 - 0
mock/system.ts

@@ -140,6 +140,620 @@ export default defineFakeRoute([
       };
     }
   },
+  // 菜单管理
+  {
+    url: "/menu",
+    method: "post",
+    response: () => {
+      return {
+        success: true,
+        data: [
+          // 外部页面
+          {
+            parentId: 0,
+            id: 100,
+            menuType: 0, // 菜单类型(0代表菜单、1代表iframe、2代表外链、3代表按钮)
+            title: "menus.hsExternalPage",
+            name: "PureIframe",
+            path: "/iframe",
+            component: "",
+            rank: 7,
+            redirect: "",
+            icon: "ep:monitor",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 100,
+            id: 101,
+            menuType: 0,
+            title: "menus.hsExternalDoc",
+            name: "PureIframeExternal",
+            path: "/iframe/external",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 101,
+            id: 102,
+            menuType: 2,
+            title: "menus.externalLink",
+            name: "https://yiming_chang.gitee.io/pure-admin-doc",
+            path: "/external",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 101,
+            id: 103,
+            menuType: 2,
+            title: "menus.pureutilsLink",
+            name: "https://pure-admin-utils.netlify.app/",
+            path: "/pureutilsLink",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 100,
+            id: 104,
+            menuType: 1,
+            title: "menus.hsEmbeddedDoc",
+            name: "PureIframeEmbedded",
+            path: "/iframe/embedded",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 104,
+            id: 105,
+            menuType: 1,
+            title: "menus.hsEpDocument",
+            name: "FrameEp",
+            path: "/iframe/ep",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "https://element-plus.org/zh-CN/",
+            frameLoading: true,
+            keepAlive: true,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 104,
+            id: 106,
+            menuType: 1,
+            title: "menus.hsTailwindcssDocument",
+            name: "FrameTailwindcss",
+            path: "/iframe/tailwindcss",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "https://tailwindcss.com/docs/installation",
+            frameLoading: true,
+            keepAlive: true,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 104,
+            id: 107,
+            menuType: 1,
+            title: "menus.hsVueDocument",
+            name: "FrameVue",
+            path: "/iframe/vue3",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "https://cn.vuejs.org/",
+            frameLoading: true,
+            keepAlive: true,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 104,
+            id: 108,
+            menuType: 1,
+            title: "menus.hsViteDocument",
+            name: "FrameVite",
+            path: "/iframe/vite",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "https://cn.vitejs.dev/",
+            frameLoading: true,
+            keepAlive: true,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 104,
+            id: 109,
+            menuType: 1,
+            title: "menus.hsPiniaDocument",
+            name: "FramePinia",
+            path: "/iframe/pinia",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "https://pinia.vuejs.org/zh/index.html",
+            frameLoading: true,
+            keepAlive: true,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 104,
+            id: 110,
+            menuType: 1,
+            title: "menus.hsRouterDocument",
+            name: "FrameRouter",
+            path: "/iframe/vue-router",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "https://router.vuejs.org/zh/",
+            frameLoading: true,
+            keepAlive: true,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          // 权限管理
+          {
+            parentId: 0,
+            id: 200,
+            menuType: 0,
+            title: "menus.permission",
+            name: "PurePermission",
+            path: "/permission",
+            component: "",
+            rank: 9,
+            redirect: "",
+            icon: "ep:lollipop",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 200,
+            id: 201,
+            menuType: 0,
+            title: "menus.permissionPage",
+            name: "PermissionPage",
+            path: "/permission/page/index",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 200,
+            id: 202,
+            menuType: 0,
+            title: "menus.permissionButton",
+            name: "PermissionButton",
+            path: "/permission/button/index",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 202,
+            id: 203,
+            menuType: 3,
+            title: "添加",
+            name: "",
+            path: "",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "permission:btn:add",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 202,
+            id: 204,
+            menuType: 3,
+            title: "修改",
+            name: "",
+            path: "",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "permission:btn:edit",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 202,
+            id: 205,
+            menuType: 3,
+            title: "删除",
+            name: "",
+            path: "",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "permission:btn:delete",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          // 系统管理
+          {
+            parentId: 0,
+            id: 300,
+            menuType: 0,
+            title: "menus.hssysManagement",
+            name: "PureSystem",
+            path: "/system",
+            component: "",
+            rank: 10,
+            redirect: "",
+            icon: "ri:settings-3-line",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 300,
+            id: 301,
+            menuType: 0,
+            title: "menus.hsUser",
+            name: "SystemUser",
+            path: "/system/user/index",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "ri:admin-line",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 300,
+            id: 302,
+            menuType: 0,
+            title: "menus.hsRole",
+            name: "SystemRole",
+            path: "/system/role/index",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "ri:admin-fill",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 300,
+            id: 303,
+            menuType: 0,
+            title: "menus.hsSystemMenu",
+            name: "SystemMenu",
+            path: "/system/menu/index",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "ep:menu",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 300,
+            id: 304,
+            menuType: 0,
+            title: "menus.hsDept",
+            name: "SystemDept",
+            path: "/system/dept/index",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "ri:git-branch-line",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          // 标签页操作
+          {
+            parentId: 0,
+            id: 400,
+            menuType: 0,
+            title: "menus.hstabs",
+            name: "PureTabs",
+            path: "/tabs",
+            component: "",
+            rank: 11,
+            redirect: "",
+            icon: "ri:bookmark-2-line",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 400,
+            id: 401,
+            menuType: 0,
+            title: "menus.hstabs",
+            name: "Tabs",
+            path: "/tabs/index",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: true,
+            showParent: false
+          },
+          {
+            parentId: 400,
+            id: 402,
+            menuType: 0,
+            title: "query传参模式",
+            name: "TabQueryDetail",
+            path: "/tabs/query-detail",
+            component: "",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "/tabs/index",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: false,
+            showParent: false
+          },
+          {
+            parentId: 400,
+            id: 403,
+            menuType: 0,
+            title: "params传参模式",
+            name: "TabParamsDetail",
+            path: "/tabs/params-detail/:id",
+            component: "params-detail",
+            rank: null,
+            redirect: "",
+            icon: "",
+            extraIcon: "",
+            enterTransition: "",
+            leaveTransition: "",
+            activePath: "/tabs/index",
+            auths: "",
+            frameSrc: "",
+            frameLoading: true,
+            keepAlive: false,
+            hiddenTag: false,
+            showLink: false,
+            showParent: false
+          }
+        ]
+      };
+    }
+  },
   // 部门管理
   {
     url: "/dept",

+ 37 - 37
package.json

@@ -19,7 +19,7 @@
     "lint:prettier": "prettier --write  \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
     "lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache-location node_modules/.cache/stylelint/",
     "lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
-    "prepare": "husky install",
+    "prepare": "husky",
     "preinstall": "npx only-allow pnpm"
   },
   "keywords": [
@@ -50,13 +50,13 @@
   "dependencies": {
     "@amap/amap-jsapi-loader": "^1.0.1",
     "@howdyjs/mouse-menu": "2.0.9",
-    "@logicflow/core": "^1.2.18",
-    "@logicflow/extension": "^1.2.19",
+    "@logicflow/core": "^1.2.22",
+    "@logicflow/extension": "^1.2.22",
     "@pureadmin/descriptions": "^1.2.0",
     "@pureadmin/table": "^3.0.2",
     "@pureadmin/utils": "^2.4.4",
-    "@vueuse/core": "^10.7.2",
-    "@vueuse/motion": "^2.0.0",
+    "@vueuse/core": "^10.8.0",
+    "@vueuse/motion": "^2.1.0",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.12",
     "@zxcvbn-ts/core": "^3.0.4",
@@ -65,9 +65,9 @@
     "china-area-data": "^5.0.1",
     "cropperjs": "^1.6.1",
     "dayjs": "^1.11.10",
-    "echarts": "^5.4.3",
+    "echarts": "^5.5.0",
     "el-table-infinite-scroll": "^3.0.3",
-    "element-plus": "^2.5.3",
+    "element-plus": "^2.5.6",
     "intro.js": "^7.2.0",
     "js-cookie": "^3.0.5",
     "jsbarcode": "^3.11.6",
@@ -77,37 +77,37 @@
     "nprogress": "^0.2.0",
     "path": "^0.12.7",
     "pinia": "^2.1.7",
-    "pinyin-pro": "^3.19.3",
+    "pinyin-pro": "^3.19.6",
     "qrcode": "^1.5.3",
     "qs": "^6.11.2",
     "responsive-storage": "^2.2.0",
     "sortablejs": "^1.15.2",
-    "swiper": "^11.0.5",
+    "swiper": "^11.0.6",
     "typeit": "8.7.1",
-    "v-contextmenu": "3.0.0",
+    "v-contextmenu": "^3.2.0",
     "v3-infinite-loading": "^1.3.1",
     "version-rocket": "^1.7.1",
     "vue": "3.4.14",
-    "vue-i18n": "^9.9.0",
+    "vue-i18n": "^9.9.1",
     "vue-json-pretty": "^2.3.0",
     "vue-pdf-embed": "1.2.1",
-    "vue-router": "^4.2.5",
+    "vue-router": "^4.3.0",
     "vue-tippy": "^6.4.1",
     "vue-types": "^5.1.1",
     "vue-virtual-scroller": "2.0.0-beta.8",
     "vue-waterfall-plugin-next": "^2.3.1",
     "vue3-danmaku": "^1.6.0",
     "vuedraggable": "^4.1.0",
-    "wavesurfer.js": "^7.7.1",
-    "xgplayer": "^3.0.11",
+    "wavesurfer.js": "^7.7.3",
+    "xgplayer": "^3.0.13",
     "xlsx": "^0.18.5"
   },
   "devDependencies": {
-    "@commitlint/cli": "^18.6.0",
-    "@commitlint/config-conventional": "^18.6.0",
-    "@commitlint/types": "^18.6.0",
-    "@eslint/js": "^8.56.0",
-    "@faker-js/faker": "^8.4.0",
+    "@commitlint/cli": "^18.6.1",
+    "@commitlint/config-conventional": "^18.6.2",
+    "@commitlint/types": "^18.6.1",
+    "@eslint/js": "^8.57.0",
+    "@faker-js/faker": "^8.4.1",
     "@iconify-icons/ep": "^1.2.12",
     "@iconify-icons/ri": "^1.2.10",
     "@iconify/vue": "^4.1.1",
@@ -116,44 +116,44 @@
     "@types/gradient-string": "^1.1.5",
     "@types/intro.js": "^5.1.5",
     "@types/js-cookie": "^3.0.6",
-    "@types/node": "^20.11.7",
+    "@types/node": "^20.11.20",
     "@types/nprogress": "^0.2.3",
     "@types/qrcode": "^1.5.5",
     "@types/qs": "^6.9.11",
-    "@types/sortablejs": "^1.15.7",
-    "@typescript-eslint/eslint-plugin": "^6.19.1",
-    "@typescript-eslint/parser": "^6.19.1",
-    "@vitejs/plugin-vue": "^5.0.3",
+    "@types/sortablejs": "^1.15.8",
+    "@typescript-eslint/eslint-plugin": "^7.0.2",
+    "@typescript-eslint/parser": "^7.0.2",
+    "@vitejs/plugin-vue": "^5.0.4",
     "@vitejs/plugin-vue-jsx": "^3.1.0",
     "autoprefixer": "^10.4.17",
     "boxen": "^7.1.1",
     "cloc": "^2.11.0",
-    "cssnano": "^6.0.3",
-    "eslint": "^8.56.0",
+    "cssnano": "^6.0.5",
+    "eslint": "^8.57.0",
     "eslint-config-prettier": "^9.1.0",
     "eslint-define-config": "^2.1.0",
     "eslint-plugin-prettier": "^5.1.3",
-    "eslint-plugin-vue": "^9.20.1",
+    "eslint-plugin-vue": "^9.22.0",
     "gradient-string": "^2.0.2",
-    "husky": "^8.0.3",
-    "lint-staged": "^15.2.0",
-    "postcss": "^8.4.33",
+    "husky": "^9.0.11",
+    "lint-staged": "^15.2.2",
+    "postcss": "^8.4.35",
     "postcss-html": "^1.6.0",
-    "postcss-import": "^15.1.0",
+    "postcss-import": "^16.0.1",
     "postcss-scss": "^4.0.9",
-    "prettier": "^3.2.4",
+    "prettier": "^3.2.5",
     "rimraf": "^5.0.5",
     "rollup-plugin-visualizer": "^5.12.0",
-    "sass": "^1.70.0",
-    "stylelint": "^16.2.0",
-    "stylelint-config-recess-order": "^4.4.0",
+    "sass": "^1.71.1",
+    "stylelint": "^16.2.1",
+    "stylelint-config-recess-order": "^4.6.0",
     "stylelint-config-recommended-vue": "^1.5.0",
-    "stylelint-config-standard-scss": "^12.0.0",
+    "stylelint-config-standard-scss": "^13.0.0",
     "stylelint-prettier": "^5.0.0",
     "svgo": "^3.2.0",
     "tailwindcss": "^3.4.1",
     "typescript": "^5.3.3",
-    "vite": "^5.0.12",
+    "vite": "^5.1.4",
     "vite-plugin-cdn-import": "^0.3.5",
     "vite-plugin-compression": "^0.5.1",
     "vite-plugin-fake-server": "^2.1.1",

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 237 - 236
pnpm-lock.yaml


+ 1 - 1
public/platform-config.json

@@ -13,7 +13,7 @@
   "Grey": false,
   "Weak": false,
   "HideTabs": false,
-  "HideFooter": true,
+  "HideFooter": false,
   "SidebarStatus": true,
   "EpThemeColor": "#409EFF",
   "ShowLogo": true,

+ 5 - 0
src/api/system.ts

@@ -43,3 +43,8 @@ export const getRoleList = (data?: object) => {
 export const getDeptList = (data?: object) => {
   return http.request<Result>("post", "/dept", { data });
 };
+
+/** 获取菜单管理列表 */
+export const getMenuList = (data?: object) => {
+  return http.request<Result>("post", "/menu", { data });
+};

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
src/assets/login/illustration.svg


+ 9 - 2
src/components/ReAnimateSelector/src/index.vue

@@ -1,12 +1,19 @@
 <script setup lang="ts">
+import { ref, computed } from "vue";
 import { animates } from "./animate";
-import { ref, computed, toRef } from "vue";
 import { cloneDeep } from "@pureadmin/utils";
 
 defineOptions({
   name: "ReAnimateSelector"
 });
 
+const props = defineProps({
+  placeholder: {
+    type: String,
+    default: "请选择动画"
+  }
+});
+
 const inputValue = defineModel({ type: String });
 
 const searchVal = ref();
@@ -74,7 +81,7 @@ function onMouseleave() {
   <el-select
     clearable
     filterable
-    placeholder="请选择动画"
+    :placeholder="props.placeholder"
     popper-class="pure-animate-popper"
     :model-value="inputValue"
     :filter-method="filterMethod"

+ 1 - 1
src/components/ReDialog/index.vue

@@ -90,7 +90,7 @@ function handleClose(
     v-model="options.visible"
     class="pure-dialog"
     :fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
-    @close="handleClose(options, index)"
+    @closed="handleClose(options, index)"
     @opened="eventsCallBack('open', options, index)"
     @openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
     @closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"

+ 3 - 3
src/components/ReDialog/type.ts

@@ -3,7 +3,7 @@ import type { CSSProperties, VNode, Component } from "vue";
 type DoneFn = (cancel?: boolean) => void;
 type EventType = "open" | "close" | "openAutoFocus" | "closeAutoFocus";
 type ArgsType = {
-  /** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页 */
+  /** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键 */
   command: "cancel" | "sure" | "close";
 };
 
@@ -157,7 +157,7 @@ interface DialogOptions extends DialogProps {
     options: DialogOptions;
     index: number;
   }) => void;
-  /** `Dialog` 关闭后的回调(只有点击右上角关闭按钮或空白页关闭页面时才会触发) */
+  /** `Dialog` 关闭后的回调(只有点击右上角关闭按钮或空白页或按下了esc键关闭页面时才会触发) */
   close?: ({
     options,
     index
@@ -165,7 +165,7 @@ interface DialogOptions extends DialogProps {
     options: DialogOptions;
     index: number;
   }) => void;
-  /** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页  */
+  /** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或空白页或按下了esc键  */
   closeCallBack?: ({
     options,
     index,

+ 1 - 1
src/components/ReIcon/src/Select.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import { IconJson } from "@/components/ReIcon/data";
 import { cloneDeep, isAllEmpty } from "@pureadmin/utils";
-import { ref, computed, CSSProperties, toRef, watch } from "vue";
+import { ref, computed, CSSProperties, watch } from "vue";
 import Search from "@iconify-icons/ri/search-eye-line";
 
 type ParameterCSSProperties = (item?: string) => CSSProperties | undefined;

+ 8 - 8
src/components/ReIcon/src/offlineIcon.ts

@@ -30,7 +30,7 @@ import Table from "@iconify-icons/ri/table-line";
 import Info from "@iconify-icons/ri/file-info-line";
 import Artboard from "@iconify-icons/ri/artboard-line";
 addIcon("ubuntuFill", UbuntuFill);
-addIcon("menu", Menu);
+addIcon("ep:menu", Menu);
 addIcon("edit", Edit);
 addIcon("informationLine", InformationLine);
 addIcon("setUp", SetUp);
@@ -42,14 +42,14 @@ addIcon("listCheck", ListCheck);
 addIcon("histogram", Histogram);
 addIcon("ppt", Ppt);
 addIcon("checkboxCircleLine", CheckboxCircleLine);
-addIcon("flUser", FlUser);
-addIcon("role", Role);
-addIcon("setting", Setting);
-addIcon("dept", Dept);
+addIcon("ri:admin-line", FlUser);
+addIcon("ri:admin-fill", Role);
+addIcon("ri:settings-3-line", Setting);
+addIcon("ri:git-branch-line", Dept);
 addIcon("search", Search);
-addIcon("lollipop", Lollipop);
-addIcon("monitor", Monitor);
-addIcon("tag", Tag);
+addIcon("ep:lollipop", Lollipop);
+addIcon("ep:monitor", Monitor);
+addIcon("ri:bookmark-2-line", Tag);
 addIcon("table", Table);
 addIcon("info", Info);
 addIcon("artboard", Artboard);

+ 5 - 1
src/components/RePureTableBar/src/bar.tsx

@@ -29,6 +29,10 @@ const props = {
   columns: {
     type: Array as PropType<TableColumnList>,
     default: () => []
+  },
+  isExpandAll: {
+    type: Boolean,
+    default: true
   }
 };
 
@@ -38,10 +42,10 @@ export default defineComponent({
   emits: ["refresh"],
   setup(props, { emit, slots, attrs }) {
     const size = ref("default");
-    const isExpandAll = ref(true);
     const loading = ref(false);
     const checkAll = ref(true);
     const isIndeterminate = ref(false);
+    const isExpandAll = ref(props.isExpandAll);
     const filterColumns = cloneDeep(props?.columns).filter(column =>
       isBoolean(column?.hide)
         ? !column.hide

+ 1 - 1
src/components/ReSegmented/src/type.ts

@@ -12,7 +12,7 @@ export interface OptionsType {
   /** 图标属性、样式配置 */
   iconAttrs?: iconType;
   /** 值 */
-  value?: string | number;
+  value?: any;
   /** 是否禁用 */
   disabled?: boolean;
   /** `tooltip` 提示 */

+ 7 - 5
src/layout/components/footer/index.vue

@@ -5,14 +5,16 @@ const TITLE = getConfig("Title");
 </script>
 
 <template>
-  <footer class="layout-footer">
-    MIT © 2020-PRESENT
+  <footer
+    class="layout-footer text-[rgba(0,0,0,0.6)] dark:text-[rgba(220,220,242,0.8)]"
+  >
+    Copyright © 2020-present
     <a
-      class="ml-1 hover:text-primary"
+      class="hover:text-primary"
       href="https://github.com/pure-admin"
       target="_blank"
     >
-      {{ TITLE }}
+      &nbsp;{{ TITLE }}
     </a>
   </footer>
 </template>
@@ -24,6 +26,6 @@ const TITLE = getConfig("Title");
   justify-content: center;
   width: 100%;
   padding: 0 0 8px;
-  color: #c0c4cc;
+  font-size: 14px;
 }
 </style>

+ 1 - 1
src/layout/components/notice/data.ts

@@ -4,7 +4,7 @@ export interface ListItem {
   datetime: string;
   type: string;
   description: string;
-  status?: "" | "success" | "warning" | "info" | "danger";
+  status?: "primary" | "success" | "warning" | "info" | "danger";
   extra?: string;
 }
 

+ 1 - 1
src/layout/components/search/components/SearchResult.vue

@@ -115,7 +115,7 @@ defineExpose({ handleScroll });
     cursor: pointer;
     border: 0.1px solid #ccc;
     border-radius: 4px;
-    transition: all 0.3s;
+    transition: font-size 0.16s;
 
     &-title {
       display: flex;

+ 14 - 17
src/layout/components/sidebar/leftCollapse.vue

@@ -1,7 +1,8 @@
 <script setup lang="ts">
-import { ref, computed } from "vue";
+import { computed } from "vue";
 import { useGlobal } from "@pureadmin/utils";
 import { useNav } from "@/layout/hooks/useNav";
+
 import MenuFold from "@iconify-icons/ri/menu-fold-fill";
 
 interface Props {
@@ -12,7 +13,6 @@ const props = withDefaults(defineProps<Props>(), {
   isActive: false
 });
 
-const visible = ref(false);
 const { tooltipEffect } = useNav();
 
 const iconClass = computed(() => {
@@ -42,21 +42,18 @@ const toggleClick = () => {
 
 <template>
   <div class="collapse-container">
-    <el-tooltip
-      placement="right"
-      :visible="visible"
-      :effect="tooltipEffect"
-      :content="props.isActive ? '点击折叠' : '点击展开'"
-    >
-      <IconifyIconOffline
-        :icon="MenuFold"
-        :class="[iconClass, themeColor === 'light' ? '' : 'text-primary']"
-        :style="{ transform: props.isActive ? 'none' : 'rotateY(180deg)' }"
-        @click="toggleClick"
-        @mouseenter="visible = true"
-        @mouseleave="visible = false"
-      />
-    </el-tooltip>
+    <IconifyIconOffline
+      v-tippy="{
+        content: props.isActive ? '点击折叠' : '点击展开',
+        theme: tooltipEffect,
+        hideOnClick: 'toggle',
+        placement: 'right'
+      }"
+      :icon="MenuFold"
+      :class="[iconClass, themeColor === 'light' ? '' : 'text-primary']"
+      :style="{ transform: props.isActive ? 'none' : 'rotateY(180deg)' }"
+      @click="toggleClick"
+    />
   </div>
 </template>
 

+ 2 - 4
src/layout/components/sidebar/sidebarItem.vue

@@ -3,7 +3,6 @@ import path from "path";
 import { getConfig } from "@/config";
 import { menuType } from "../../types";
 import extraIcon from "./extraIcon.vue";
-import { useDark } from "@pureadmin/utils";
 import { ReText } from "@/components/ReText";
 import { useNav } from "@/layout/hooks/useNav";
 import { transformI18n } from "@/plugins/i18n";
@@ -16,7 +15,6 @@ import ArrowLeft from "@iconify-icons/ep/arrow-left-bold";
 import ArrowRight from "@iconify-icons/ep/arrow-right-bold";
 
 const { layout, isCollapse, tooltipEffect, getDivStyle } = useNav();
-const { isDark } = useDark();
 
 const props = defineProps({
   item: {
@@ -143,7 +141,7 @@ function resolvePath(routePath) {
         <ReText
           :tippyProps="{
             offset: [0, -10],
-            theme: !isDark ? tooltipEffect : undefined
+            theme: tooltipEffect
           }"
           class="!text-inherit"
         >
@@ -181,7 +179,7 @@ function resolvePath(routePath) {
         "
         :tippyProps="{
           offset: [0, -10],
-          theme: !isDark ? tooltipEffect : undefined
+          theme: tooltipEffect
         }"
         :class="{
           '!text-inherit': true,

+ 1 - 1
src/router/enums.ts

@@ -1,6 +1,6 @@
 // 完整版菜单比较多,将 rank 抽离出来,在此方便维护
 
-const home = 0, // 平台规定只有 home 路由的 rank 才能为 0 ,所以后端在返回 rank 的时候需要从 1 开始哦
+const home = 0, // 平台规定只有 home 路由的 rank 才能为 0 ,所以后端在返回 rank 的时候需要从非 0 开始
   components = 1,
   able = 2,
   table = 3,

+ 1 - 1
src/router/modules/components.ts

@@ -5,7 +5,7 @@ export default {
   path: "/components",
   redirect: "/components/dialog",
   meta: {
-    icon: "menu",
+    icon: "ep:menu",
     title: $t("menus.hscomponents"),
     rank: components
   },

+ 15 - 10
src/style/element-plus.scss

@@ -44,16 +44,24 @@
 }
 
 .pure-dialog {
-  .pure-dialog-svg {
-    color: var(--el-color-info);
+  .el-dialog__header.show-close {
+    padding-right: 16px;
   }
 
   .el-dialog__headerbtn {
-    top: 20px;
-    right: 14px;
+    top: 16px;
+    right: 12px;
     width: 24px;
     height: 24px;
   }
+
+  .pure-dialog-svg {
+    color: var(--el-color-info);
+  }
+
+  .el-dialog__footer {
+    padding-top: 0;
+  }
 }
 
 /* 全局覆盖element-plus的el-dialog、el-drawer、el-message-box、el-notification组件右上角关闭图标和el-upload上传文件列表右侧关闭图标的样式,表现更鲜明 */
@@ -148,17 +156,14 @@
     display: none;
   }
 
-  .el-dialog__body {
-    padding-top: 12px;
-    padding-bottom: 0;
-  }
-
   .el-input__inner {
     font-size: 1.2em;
   }
 
   .el-dialog__footer {
-    padding-bottom: 10px;
+    width: calc(100% + 32px);
+    padding: 10px 20px;
+    margin: auto -16px -16px;
     box-shadow:
       0 -1px 0 0 #e0e3e8,
       0 -3px 6px 0 rgb(69 98 155 / 12%);

+ 1 - 0
src/style/login.css

@@ -1,6 +1,7 @@
 .wave {
   position: fixed;
   height: 100%;
+  width: 80%;
   left: 0;
   bottom: 0;
   z-index: -1;

+ 4 - 4
src/views/components/animatecss.vue

@@ -6,10 +6,10 @@ defineOptions({
   name: "AnimateCss"
 });
 
-const icon = ref("");
+const animate = ref("");
 
-watch(icon, () => {
-  console.log("icon", icon.value);
+watch(animate, () => {
+  console.log("animate", animate.value);
 });
 </script>
 
@@ -29,6 +29,6 @@ watch(icon, () => {
         </span>
       </div>
     </template>
-    <ReAnimateSelector v-model="icon" class="!w-[200px]" />
+    <ReAnimateSelector v-model="animate" class="!w-[200px]" />
   </el-card>
 </template>

+ 4 - 2
src/views/components/dialog/index.vue

@@ -203,7 +203,7 @@ function onCloseCallBackClick() {
       } else if (args?.command === "sure") {
         text = "您点击了确定按钮";
       } else {
-        text = "您点击了右上角关闭按钮或空白页";
+        text = "您点击了右上角关闭按钮或空白页或按下了esc键";
       }
       message(text);
     },
@@ -301,7 +301,9 @@ function onFormOneClick() {
       } else if (args?.command === "sure") {
         message(`您点击了确定按钮,当前表单数据为 ${text}`);
       } else {
-        message(`您点击了右上角关闭按钮或者空白页,当前表单数据为 ${text}`);
+        message(
+          `您点击了右上角关闭按钮或空白页或按下了esc键,当前表单数据为 ${text}`
+        );
       }
     }
   });

+ 2 - 2
src/views/components/tag.vue

@@ -12,8 +12,8 @@ const checked2 = ref(false);
 const baseTag = ref("dark");
 const tagList = ref([
   {
-    type: "",
-    text: "Default"
+    type: "primary",
+    text: "Primary"
   },
   {
     type: "success",

+ 12 - 0
src/views/login/index.vue

@@ -323,6 +323,18 @@ watch(loginDay, value => {
         </div>
       </div>
     </div>
+    <div
+      class="w-full flex-c absolute bottom-3 text-sm text-[rgba(0,0,0,0.6)] dark:text-[rgba(220,220,242,0.8)]"
+    >
+      Copyright © 2020-present
+      <a
+        class="hover:text-primary"
+        href="https://github.com/pure-admin"
+        target="_blank"
+      >
+        &nbsp;{{ title }}
+      </a>
+    </div>
   </div>
 </template>
 

+ 37 - 18
src/views/permission/button/index.vue

@@ -15,19 +15,26 @@ defineOptions({
         <div class="card-header">组件方式判断权限</div>
       </template>
       <el-space wrap>
-        <Auth value="btn_add">
+        <Auth value="permission:btn:add">
           <el-button plain type="warning">
-            拥有code:'btn_add' 权限可见
+            拥有code:'permission:btn:add' 权限可见
           </el-button>
         </Auth>
-        <Auth :value="['btn_edit']">
+        <Auth :value="['permission:btn:edit']">
           <el-button plain type="primary">
-            拥有code:['btn_edit'] 权限可见
+            拥有code:['permission:btn:edit'] 权限可见
           </el-button>
         </Auth>
-        <Auth :value="['btn_add', 'btn_edit', 'btn_delete']">
+        <Auth
+          :value="[
+            'permission:btn:add',
+            'permission:btn:edit',
+            'permission:btn:delete'
+          ]"
+        >
           <el-button plain type="danger">
-            拥有code:['btn_add', 'btn_edit', 'btn_delete'] 权限可见
+            拥有code:['permission:btn:add', 'permission:btn:edit',
+            'permission:btn:delete'] 权限可见
           </el-button>
         </Auth>
       </el-space>
@@ -38,18 +45,25 @@ defineOptions({
         <div class="card-header">函数方式判断权限</div>
       </template>
       <el-space wrap>
-        <el-button v-if="hasAuth('btn_add')" plain type="warning">
-          拥有code:'btn_add' 权限可见
+        <el-button v-if="hasAuth('permission:btn:add')" plain type="warning">
+          拥有code:'permission:btn:add' 权限可见
         </el-button>
-        <el-button v-if="hasAuth(['btn_edit'])" plain type="primary">
-          拥有code:['btn_edit'] 权限可见
+        <el-button v-if="hasAuth(['permission:btn:edit'])" plain type="primary">
+          拥有code:['permission:btn:edit'] 权限可见
         </el-button>
         <el-button
-          v-if="hasAuth(['btn_add', 'btn_edit', 'btn_delete'])"
+          v-if="
+            hasAuth([
+              'permission:btn:add',
+              'permission:btn:edit',
+              'permission:btn:delete'
+            ])
+          "
           plain
           type="danger"
         >
-          拥有code:['btn_add', 'btn_edit', 'btn_delete'] 权限可见
+          拥有code:['permission:btn:add', 'permission:btn:edit',
+          'permission:btn:delete'] 权限可见
         </el-button>
       </el-space>
     </el-card>
@@ -61,18 +75,23 @@ defineOptions({
         </div>
       </template>
       <el-space wrap>
-        <el-button v-auth="'btn_add'" plain type="warning">
-          拥有code:'btn_add' 权限可见
+        <el-button v-auth="'permission:btn:add'" plain type="warning">
+          拥有code:'permission:btn:add' 权限可见
         </el-button>
-        <el-button v-auth="['btn_edit']" plain type="primary">
-          拥有code:['btn_edit'] 权限可见
+        <el-button v-auth="['permission:btn:edit']" plain type="primary">
+          拥有code:['permission:btn:edit'] 权限可见
         </el-button>
         <el-button
-          v-auth="['btn_add', 'btn_edit', 'btn_delete']"
+          v-auth="[
+            'permission:btn:add',
+            'permission:btn:edit',
+            'permission:btn:delete'
+          ]"
           plain
           type="danger"
         >
-          拥有code:['btn_add', 'btn_edit', 'btn_delete'] 权限可见
+          拥有code:['permission:btn:add', 'permission:btn:edit',
+          'permission:btn:delete'] 权限可见
         </el-button>
       </el-space>
     </el-card>

+ 4 - 1
src/views/pure-table/base/filters.vue

@@ -95,7 +95,10 @@ const tableData = [
       :columns="columns"
     >
       <template #tag="{ row }">
-        <el-tag :type="row.tag === 'Home' ? '' : 'success'" disable-transitions>
+        <el-tag
+          :type="row.tag === 'Home' ? null : 'success'"
+          disable-transitions
+        >
           {{ row.tag }}
         </el-tag>
       </template>

+ 15 - 7
src/views/system/dept/index.vue

@@ -89,7 +89,7 @@ const {
         <pure-table
           ref="tableRef"
           adaptive
-          :adaptiveConfig="{ offsetBottom: 32 }"
+          :adaptiveConfig="{ offsetBottom: 45 }"
           align-whole="center"
           row-key="id"
           showOverflowTooltip
@@ -111,20 +111,20 @@ const {
               link
               type="primary"
               :size="size"
-              :icon="useRenderIcon(AddFill)"
-              @click="openDialog('新增', { parentId: row.id } as any)"
+              :icon="useRenderIcon(EditPen)"
+              @click="openDialog('修改', row)"
             >
-              新增
+              修改
             </el-button>
             <el-button
               class="reset-margin"
               link
               type="primary"
               :size="size"
-              :icon="useRenderIcon(EditPen)"
-              @click="openDialog('修改', row)"
+              :icon="useRenderIcon(AddFill)"
+              @click="openDialog('新增', { parentId: row.id } as any)"
             >
-              修改
+              新增
             </el-button>
             <el-popconfirm
               :title="`是否确认删除部门名称为${row.name}的这条数据`"
@@ -150,6 +150,14 @@ const {
 </template>
 
 <style lang="scss" scoped>
+:deep(.el-table__inner-wrapper::before) {
+  height: 0;
+}
+
+.main-content {
+  margin: 24px 24px 0 !important;
+}
+
 .search-form {
   :deep(.el-form-item) {
     margin-bottom: 12px;

+ 27 - 0
src/views/system/menu/README.md

@@ -0,0 +1,27 @@
+<!-- 初版,持续完善中 -->
+
+## 字段含义
+
+| 字段              | 说明                                                         |
+| :---------------- | :----------------------------------------------------------- |
+| `menuType`        | 菜单类型(`0`代表菜单、`1`代表`iframe`、`2`代表外链、`3`代表按钮) |
+| `parentId`        |                                                              |
+| `title`           | 菜单名称(兼容国际化、非国际化,如果用国际化的写法就必须在根目录的`locales`文件夹下对应添加) |
+| `name`            | 路由名称(必须唯一并且和当前路由`component`字段对应的页面里用`defineOptions`包起来的`name`保持一致) |
+| `path`            | 路由路径                                                     |
+| `component`       | 组件路径(传`component`组件路径,那么`path`可以随便写,如果不传,`component`组件路径会跟`path`保持一致) |
+| `rank`            | 菜单排序(平台规定只有`home`路由的`rank`才能为`0`,所以后端在返回`rank`的时候需要从非`0`开始 [点击查看更多](https://yiming_chang.gitee.io/pure-admin-doc/pages/routerMenu/#%E8%8F%9C%E5%8D%95%E6%8E%92%E5%BA%8F-rank)) |
+| `redirect`        | 路由重定向                                                   |
+| `icon`            | 菜单图标                                                     |
+| `extraIcon`       | 右侧图标                                                     |
+| `enterTransition` | 进场动画(页面加载动画)                                     |
+| `leaveTransition` | 离场动画(页面加载动画)                                     |
+| `activePath`      | 菜单激活(将某个菜单激活,主要用于通过`query`或`params`传参的路由,当它们通过配置`showLink: false`后不在菜单中显示,就不会有任何菜单高亮,而通过设置`activePath`指定激活菜单即可获得高亮,`activePath`为指定激活菜单的`path`) |
+| `auths`           | 权限标识(按钮级别权限设置)                                 |
+| `frameSrc`        | 链接地址(需要内嵌的`iframe`链接地址)                       |
+| `frameLoading`    | 加载动画(内嵌的`iframe`页面是否开启首次加载动画)           |
+| `keepAlive`       | 缓存页面(是否缓存该路由页面,开启后会保存该页面的整体状态,刷新后会清空状态) |
+| `hiddenTag`       | 标签页(当前菜单名称或自定义信息禁止添加到标签页)           |
+| `showLink`        | 菜单(是否显示该菜单)                                       |
+| `showParent`      | 父级菜单(是否显示父级菜单 [点击查看更多](https://yiming_chang.gitee.io/pure-admin-doc/pages/routerMenu/#%E7%AC%AC%E4%B8%80%E7%A7%8D-%E8%AF%A5%E6%A8%A1%E5%BC%8F%E9%92%88%E5%AF%B9%E7%88%B6%E7%BA%A7%E8%8F%9C%E5%8D%95%E4%B8%8B%E5%8F%AA%E6%9C%89%E4%B8%80%E4%B8%AA%E5%AD%90%E8%8F%9C%E5%8D%95%E7%9A%84%E6%83%85%E5%86%B5-%E5%9C%A8%E5%AD%90%E8%8F%9C%E5%8D%95%E7%9A%84-meta-%E5%B1%9E%E6%80%A7%E4%B8%AD%E5%8A%A0%E4%B8%8A-showparent-true-%E5%8D%B3%E5%8F%AF)) |
+

+ 326 - 0
src/views/system/menu/form.vue

@@ -0,0 +1,326 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import ReCol from "@/components/ReCol";
+import { formRules } from "./utils/rule";
+import { FormProps } from "./utils/types";
+import { transformI18n } from "@/plugins/i18n";
+import { IconSelect } from "@/components/ReIcon";
+import Segmented from "@/components/ReSegmented";
+import ReAnimateSelector from "@/components/ReAnimateSelector";
+import {
+  menuTypeOptions,
+  showLinkOptions,
+  keepAliveOptions,
+  hiddenTagOptions,
+  showParentOptions,
+  frameLoadingOptions
+} from "./utils/enums";
+
+const props = withDefaults(defineProps<FormProps>(), {
+  formInline: () => ({
+    menuType: 0,
+    higherMenuOptions: [],
+    parentId: 0,
+    title: "",
+    name: "",
+    path: "",
+    component: "",
+    rank: 99,
+    redirect: " ",
+    icon: "",
+    extraIcon: "",
+    enterTransition: "",
+    leaveTransition: "",
+    activePath: "",
+    auths: "",
+    frameSrc: "",
+    frameLoading: true,
+    keepAlive: false,
+    hiddenTag: false,
+    showLink: true,
+    showParent: false
+  })
+});
+
+const ruleFormRef = ref();
+const newFormInline = ref(props.formInline);
+
+function getRef() {
+  return ruleFormRef.value;
+}
+
+defineExpose({ getRef });
+</script>
+
+<template>
+  <el-form
+    ref="ruleFormRef"
+    :model="newFormInline"
+    :rules="formRules"
+    label-width="82px"
+  >
+    <el-row :gutter="30">
+      <re-col>
+        <el-form-item label="菜单类型">
+          <Segmented
+            v-model="newFormInline.menuType"
+            :options="menuTypeOptions"
+          />
+        </el-form-item>
+      </re-col>
+
+      <re-col>
+        <el-form-item label="上级菜单">
+          <el-cascader
+            v-model="newFormInline.parentId"
+            class="w-full"
+            :options="newFormInline.higherMenuOptions"
+            :props="{
+              value: 'id',
+              label: 'title',
+              emitPath: false,
+              checkStrictly: true
+            }"
+            clearable
+            filterable
+            placeholder="请选择上级菜单"
+          >
+            <template #default="{ node, data }">
+              <span>{{ transformI18n(data.title) }}</span>
+              <span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
+            </template>
+          </el-cascader>
+        </el-form-item>
+      </re-col>
+
+      <re-col :value="12" :xs="24" :sm="24">
+        <el-form-item label="菜单名称" prop="title">
+          <el-input
+            v-model="newFormInline.title"
+            clearable
+            placeholder="请输入菜单名称"
+          />
+        </el-form-item>
+      </re-col>
+      <re-col v-if="newFormInline.menuType !== 3" :value="12" :xs="24" :sm="24">
+        <el-form-item label="路由名称" prop="name">
+          <el-input
+            v-model="newFormInline.name"
+            clearable
+            placeholder="请输入路由名称"
+          />
+        </el-form-item>
+      </re-col>
+
+      <re-col v-if="newFormInline.menuType !== 3" :value="12" :xs="24" :sm="24">
+        <el-form-item label="路由路径" prop="path">
+          <el-input
+            v-model="newFormInline.path"
+            clearable
+            placeholder="请输入路由路径"
+          />
+        </el-form-item>
+      </re-col>
+      <re-col
+        v-show="newFormInline.menuType === 0"
+        :value="12"
+        :xs="24"
+        :sm="24"
+      >
+        <el-form-item label="组件路径">
+          <el-input
+            v-model="newFormInline.component"
+            clearable
+            placeholder="请输入组件路径"
+          />
+        </el-form-item>
+      </re-col>
+
+      <re-col :value="12" :xs="24" :sm="24">
+        <el-form-item label="菜单排序">
+          <el-input-number
+            v-model="newFormInline.rank"
+            class="!w-full"
+            :min="1"
+            :max="9999"
+            controls-position="right"
+          />
+        </el-form-item>
+      </re-col>
+      <re-col
+        v-show="newFormInline.menuType === 0"
+        :value="12"
+        :xs="24"
+        :sm="24"
+      >
+        <el-form-item label="路由重定向">
+          <el-input
+            v-model="newFormInline.redirect"
+            clearable
+            placeholder="请输入默认跳转地址"
+          />
+        </el-form-item>
+      </re-col>
+
+      <re-col
+        v-show="newFormInline.menuType !== 3"
+        :value="12"
+        :xs="24"
+        :sm="24"
+      >
+        <el-form-item label="菜单图标">
+          <IconSelect v-model="newFormInline.icon" class="w-full" />
+        </el-form-item>
+      </re-col>
+      <re-col
+        v-show="newFormInline.menuType !== 3"
+        :value="12"
+        :xs="24"
+        :sm="24"
+      >
+        <el-form-item label="右侧图标">
+          <el-input
+            v-model="newFormInline.extraIcon"
+            clearable
+            placeholder="菜单名称右侧的额外图标"
+          />
+        </el-form-item>
+      </re-col>
+
+      <re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
+        <el-form-item label="进场动画">
+          <ReAnimateSelector
+            v-model="newFormInline.enterTransition"
+            placeholder="请选择页面进场加载动画"
+          />
+        </el-form-item>
+      </re-col>
+      <re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
+        <el-form-item label="离场动画">
+          <ReAnimateSelector
+            v-model="newFormInline.leaveTransition"
+            placeholder="请选择页面离场加载动画"
+          />
+        </el-form-item>
+      </re-col>
+
+      <re-col
+        v-show="newFormInline.menuType === 0"
+        :value="12"
+        :xs="24"
+        :sm="24"
+      >
+        <el-form-item label="菜单激活">
+          <el-input
+            v-model="newFormInline.activePath"
+            clearable
+            placeholder="请输入需要激活的菜单"
+          />
+        </el-form-item>
+      </re-col>
+      <re-col v-if="newFormInline.menuType === 3" :value="12" :xs="24" :sm="24">
+        <!-- 按钮级别权限设置 -->
+        <el-form-item label="权限标识" prop="auths">
+          <el-input
+            v-model="newFormInline.auths"
+            clearable
+            placeholder="请输入权限标识"
+          />
+        </el-form-item>
+      </re-col>
+
+      <re-col
+        v-show="newFormInline.menuType === 1"
+        :value="12"
+        :xs="24"
+        :sm="24"
+      >
+        <!-- iframe -->
+        <el-form-item label="链接地址">
+          <el-input
+            v-model="newFormInline.frameSrc"
+            clearable
+            placeholder="请输入 iframe 链接地址"
+          />
+        </el-form-item>
+      </re-col>
+      <re-col v-if="newFormInline.menuType === 1" :value="12" :xs="24" :sm="24">
+        <el-form-item label="加载动画">
+          <Segmented
+            :modelValue="newFormInline.frameLoading ? 0 : 1"
+            :options="frameLoadingOptions"
+            @change="
+              ({ option: { value } }) => {
+                newFormInline.frameLoading = value;
+              }
+            "
+          />
+        </el-form-item>
+      </re-col>
+
+      <re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
+        <el-form-item label="缓存页面">
+          <Segmented
+            :modelValue="newFormInline.keepAlive ? 0 : 1"
+            :options="keepAliveOptions"
+            @change="
+              ({ option: { value } }) => {
+                newFormInline.keepAlive = value;
+              }
+            "
+          />
+        </el-form-item>
+      </re-col>
+      <re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
+        <el-form-item label="标签页">
+          <Segmented
+            :modelValue="newFormInline.hiddenTag ? 1 : 0"
+            :options="hiddenTagOptions"
+            @change="
+              ({ option: { value } }) => {
+                newFormInline.hiddenTag = value;
+              }
+            "
+          />
+        </el-form-item>
+      </re-col>
+
+      <re-col
+        v-show="newFormInline.menuType !== 3"
+        :value="12"
+        :xs="24"
+        :sm="24"
+      >
+        <el-form-item label="菜单">
+          <Segmented
+            :modelValue="newFormInline.showLink ? 0 : 1"
+            :options="showLinkOptions"
+            @change="
+              ({ option: { value } }) => {
+                newFormInline.showLink = value;
+              }
+            "
+          />
+        </el-form-item>
+      </re-col>
+      <re-col
+        v-show="newFormInline.menuType !== 3"
+        :value="8"
+        :xs="24"
+        :sm="24"
+      >
+        <el-form-item label="父级菜单">
+          <Segmented
+            :modelValue="newFormInline.showParent ? 0 : 1"
+            :options="showParentOptions"
+            @change="
+              ({ option: { value } }) => {
+                newFormInline.showParent = value;
+              }
+            "
+          />
+        </el-form-item>
+      </re-col>
+    </el-row>
+  </el-form>
+</template>

+ 157 - 0
src/views/system/menu/index.vue

@@ -0,0 +1,157 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { useMenu } from "./utils/hook";
+import { transformI18n } from "@/plugins/i18n";
+import { PureTableBar } from "@/components/RePureTableBar";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+
+import Delete from "@iconify-icons/ep/delete";
+import EditPen from "@iconify-icons/ep/edit-pen";
+import Refresh from "@iconify-icons/ep/refresh";
+import AddFill from "@iconify-icons/ri/add-circle-line";
+
+defineOptions({
+  name: "SystemMenu"
+});
+
+const formRef = ref();
+const tableRef = ref();
+const {
+  form,
+  loading,
+  columns,
+  dataList,
+  onSearch,
+  resetForm,
+  openDialog,
+  handleDelete,
+  handleSelectionChange
+} = useMenu();
+</script>
+
+<template>
+  <div class="main">
+    <el-form
+      ref="formRef"
+      :inline="true"
+      :model="form"
+      class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
+    >
+      <el-form-item label="菜单名称:" prop="title">
+        <el-input
+          v-model="form.title"
+          placeholder="请输入菜单名称"
+          clearable
+          class="!w-[180px]"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon('search')"
+          :loading="loading"
+          @click="onSearch"
+        >
+          搜索
+        </el-button>
+        <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <PureTableBar
+      title="菜单管理(初版,持续完善中)"
+      :columns="columns"
+      :isExpandAll="false"
+      :tableRef="tableRef?.getTableRef()"
+      @refresh="onSearch"
+    >
+      <template #buttons>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon(AddFill)"
+          @click="openDialog()"
+        >
+          新增菜单
+        </el-button>
+      </template>
+      <template v-slot="{ size, dynamicColumns }">
+        <pure-table
+          ref="tableRef"
+          adaptive
+          :adaptiveConfig="{ offsetBottom: 45 }"
+          align-whole="center"
+          row-key="id"
+          showOverflowTooltip
+          table-layout="auto"
+          :loading="loading"
+          :size="size"
+          :data="dataList"
+          :columns="dynamicColumns"
+          :header-cell-style="{
+            background: 'var(--el-fill-color-light)',
+            color: 'var(--el-text-color-primary)'
+          }"
+          @selection-change="handleSelectionChange"
+        >
+          <template #operation="{ row }">
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(EditPen)"
+              @click="openDialog('修改', row)"
+            >
+              修改
+            </el-button>
+            <el-button
+              v-show="row.menuType !== 3"
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(AddFill)"
+              @click="openDialog('新增', { parentId: row.id } as any)"
+            >
+              新增
+            </el-button>
+            <el-popconfirm
+              :title="`是否确认删除菜单名称为${transformI18n(row.title)}的这条数据${row?.children?.length > 0 ? '。注意下级菜单也会一并删除,请谨慎操作' : ''}`"
+              @confirm="handleDelete(row)"
+            >
+              <template #reference>
+                <el-button
+                  class="reset-margin"
+                  link
+                  type="primary"
+                  :size="size"
+                  :icon="useRenderIcon(Delete)"
+                >
+                  删除
+                </el-button>
+              </template>
+            </el-popconfirm>
+          </template>
+        </pure-table>
+      </template>
+    </PureTableBar>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-table__inner-wrapper::before) {
+  height: 0;
+}
+
+.main-content {
+  margin: 24px 24px 0 !important;
+}
+
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+</style>

+ 94 - 0
src/views/system/menu/utils/enums.ts

@@ -0,0 +1,94 @@
+import type { OptionsType } from "@/components/ReSegmented";
+
+const menuTypeOptions: Array<OptionsType> = [
+  {
+    label: "菜单",
+    value: 0
+  },
+  {
+    label: "iframe",
+    value: 1
+  },
+  {
+    label: "外链",
+    value: 2
+  },
+  {
+    label: "按钮",
+    value: 3
+  }
+];
+
+const showLinkOptions: Array<OptionsType> = [
+  {
+    label: "显示",
+    tip: "会在菜单中显示",
+    value: true
+  },
+  {
+    label: "隐藏",
+    tip: "不会在菜单中显示",
+    value: false
+  }
+];
+
+const keepAliveOptions: Array<OptionsType> = [
+  {
+    label: "缓存",
+    tip: "会保存该页面的整体状态,刷新后会清空状态",
+    value: true
+  },
+  {
+    label: "不缓存",
+    tip: "不会保存该页面的整体状态",
+    value: false
+  }
+];
+
+const hiddenTagOptions: Array<OptionsType> = [
+  {
+    label: "允许",
+    tip: "当前菜单名称或自定义信息允许添加到标签页",
+    value: false
+  },
+  {
+    label: "禁止",
+    tip: "当前菜单名称或自定义信息禁止添加到标签页",
+    value: true
+  }
+];
+
+const showParentOptions: Array<OptionsType> = [
+  {
+    label: "显示",
+    tip: "会显示父级菜单",
+    value: true
+  },
+  {
+    label: "隐藏",
+    tip: "不会显示父级菜单",
+    value: false
+  }
+];
+
+const frameLoadingOptions: Array<OptionsType> = [
+  {
+    label: "开启",
+    tip: "有首次加载动画",
+    value: true
+  },
+  {
+    label: "关闭",
+    tip: "无首次加载动画",
+    value: false
+  }
+];
+
+export {
+  menuTypeOptions,
+  showLinkOptions,
+  keepAliveOptions,
+  hiddenTagOptions,
+  showParentOptions,
+  frameLoadingOptions
+};

+ 223 - 0
src/views/system/menu/utils/hook.tsx

@@ -0,0 +1,223 @@
+import editForm from "../form.vue";
+import { handleTree } from "@/utils/tree";
+import { message } from "@/utils/message";
+import { getMenuList } from "@/api/system";
+import { transformI18n } from "@/plugins/i18n";
+import { addDialog } from "@/components/ReDialog";
+import { reactive, ref, onMounted, h } from "vue";
+import type { FormItemProps } from "../utils/types";
+import { cloneDeep, isAllEmpty } from "@pureadmin/utils";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+
+export function useMenu() {
+  const form = reactive({
+    title: ""
+  });
+
+  const formRef = ref();
+  const dataList = ref([]);
+  const loading = ref(true);
+
+  const getMenuType = (type, text = false) => {
+    switch (type) {
+      case 0:
+        return text ? "菜单" : "primary";
+      case 1:
+        return text ? "iframe" : "warning";
+      case 2:
+        return text ? "外链" : "danger";
+      case 3:
+        return text ? "按钮" : "info";
+    }
+  };
+
+  const columns: TableColumnList = [
+    {
+      label: "菜单名称",
+      prop: "title",
+      align: "left",
+      cellRenderer: ({ row }) => (
+        <>
+          <span class="inline-block mr-1">
+            {h(useRenderIcon(row.icon), {
+              style: { paddingTop: "1px" }
+            })}
+          </span>
+          <span>{transformI18n(row.title)}</span>
+        </>
+      )
+    },
+    {
+      label: "菜单类型",
+      prop: "menuType",
+      width: 100,
+      cellRenderer: ({ row, props }) => (
+        <el-tag
+          size={props.size}
+          type={getMenuType(row.menuType)}
+          effect="plain"
+        >
+          {getMenuType(row.menuType, true)}
+        </el-tag>
+      )
+    },
+    {
+      label: "路由路径",
+      prop: "path"
+    },
+    {
+      label: "组件路径",
+      prop: "component",
+      formatter: ({ path, component }) =>
+        isAllEmpty(component) ? path : component
+    },
+    {
+      label: "权限标识",
+      prop: "auths"
+    },
+    {
+      label: "排序",
+      prop: "rank",
+      width: 100
+    },
+    {
+      label: "隐藏",
+      prop: "showLink",
+      formatter: ({ showLink }) => (showLink ? "否" : "是"),
+      width: 100
+    },
+    {
+      label: "操作",
+      fixed: "right",
+      width: 210,
+      slot: "operation"
+    }
+  ];
+
+  function handleSelectionChange(val) {
+    console.log("handleSelectionChange", val);
+  }
+
+  function resetForm(formEl) {
+    if (!formEl) return;
+    formEl.resetFields();
+    onSearch();
+  }
+
+  async function onSearch() {
+    loading.value = true;
+    const { data } = await getMenuList(); // 这里是返回一维数组结构,前端自行处理成树结构,返回格式要求:唯一id加父节点parentId,parentId取父节点id
+    let newData = data;
+    if (!isAllEmpty(form.title)) {
+      // 前端搜索菜单名称
+      newData = newData.filter(item =>
+        transformI18n(item.title).includes(form.title)
+      );
+    }
+    dataList.value = handleTree(newData); // 处理成树结构
+    setTimeout(() => {
+      loading.value = false;
+    }, 500);
+  }
+
+  function formatHigherMenuOptions(treeList) {
+    if (!treeList || !treeList.length) return;
+    const newTreeList = [];
+    for (let i = 0; i < treeList.length; i++) {
+      treeList[i].title = transformI18n(treeList[i].title);
+      formatHigherMenuOptions(treeList[i].children);
+      newTreeList.push(treeList[i]);
+    }
+    return newTreeList;
+  }
+
+  function openDialog(title = "新增", row?: FormItemProps) {
+    addDialog({
+      title: `${title}菜单`,
+      props: {
+        formInline: {
+          menuType: row?.menuType ?? 0,
+          higherMenuOptions: formatHigherMenuOptions(cloneDeep(dataList.value)),
+          parentId: row?.parentId ?? 0,
+          title: row?.title ?? "",
+          name: row?.name ?? "",
+          path: row?.path ?? "",
+          component: row?.component ?? "",
+          rank: row?.rank ?? 99,
+          redirect: row?.redirect ?? "",
+          icon: row?.icon ?? "",
+          extraIcon: row?.extraIcon ?? "",
+          enterTransition: row?.enterTransition ?? "",
+          leaveTransition: row?.leaveTransition ?? "",
+          activePath: row?.activePath ?? "",
+          auths: row?.auths ?? "",
+          frameSrc: row?.frameSrc ?? "",
+          frameLoading: row?.frameLoading ?? true,
+          keepAlive: row?.keepAlive ?? false,
+          hiddenTag: row?.hiddenTag ?? false,
+          showLink: row?.showLink ?? true,
+          showParent: row?.showParent ?? false
+        }
+      },
+      width: "45%",
+      draggable: true,
+      fullscreenIcon: true,
+      closeOnClickModal: false,
+      contentRenderer: () => h(editForm, { ref: formRef }),
+      beforeSure: (done, { options }) => {
+        const FormRef = formRef.value.getRef();
+        const curData = options.props.formInline as FormItemProps;
+        function chores() {
+          message(
+            `您${title}了菜单名称为${transformI18n(curData.title)}的这条数据`,
+            {
+              type: "success"
+            }
+          );
+          done(); // 关闭弹框
+          onSearch(); // 刷新表格数据
+        }
+        FormRef.validate(valid => {
+          if (valid) {
+            console.log("curData", curData);
+            // 表单规则校验通过
+            if (title === "新增") {
+              // 实际开发先调用新增接口,再进行下面操作
+              chores();
+            } else {
+              // 实际开发先调用修改接口,再进行下面操作
+              chores();
+            }
+          }
+        });
+      }
+    });
+  }
+
+  function handleDelete(row) {
+    message(`您删除了菜单名称为${transformI18n(row.title)}的这条数据`, {
+      type: "success"
+    });
+    onSearch();
+  }
+
+  onMounted(() => {
+    onSearch();
+  });
+
+  return {
+    form,
+    loading,
+    columns,
+    dataList,
+    /** 搜索 */
+    onSearch,
+    /** 重置 */
+    resetForm,
+    /** 新增、修改菜单 */
+    openDialog,
+    /** 删除菜单 */
+    handleDelete,
+    handleSelectionChange
+  };
+}

+ 10 - 0
src/views/system/menu/utils/rule.ts

@@ -0,0 +1,10 @@
+import { reactive } from "vue";
+import type { FormRules } from "element-plus";
+
+/** 自定义表单规则校验 */
+export const formRules = reactive(<FormRules>{
+  title: [{ required: true, message: "菜单名称为必填项", trigger: "blur" }],
+  name: [{ required: true, message: "路由名称为必填项", trigger: "blur" }],
+  path: [{ required: true, message: "路由路径为必填项", trigger: "blur" }],
+  auths: [{ required: true, message: "权限标识为必填项", trigger: "blur" }]
+});

+ 29 - 0
src/views/system/menu/utils/types.ts

@@ -0,0 +1,29 @@
+interface FormItemProps {
+  /** 菜单类型(0代表菜单、1代表iframe、2代表外链、3代表按钮)*/
+  menuType: number;
+  higherMenuOptions: Record<string, unknown>[];
+  parentId: number;
+  title: string;
+  name: string;
+  path: string;
+  component: string;
+  rank: number;
+  redirect: string;
+  icon: string;
+  extraIcon: string;
+  enterTransition: string;
+  leaveTransition: string;
+  activePath: string;
+  auths: string;
+  frameSrc: string;
+  frameLoading: boolean;
+  keepAlive: boolean;
+  hiddenTag: boolean;
+  showLink: boolean;
+  showParent: boolean;
+}
+interface FormProps {
+  formInline: FormItemProps;
+}
+
+export type { FormItemProps, FormProps };

+ 5 - 0
src/views/system/role/index.vue

@@ -108,6 +108,7 @@ const {
           :loading="loading"
           :size="size"
           adaptive
+          :adaptiveConfig="{ offsetBottom: 108 }"
           :data="dataList"
           :columns="dynamicColumns"
           :pagination="pagination"
@@ -206,6 +207,10 @@ const {
   margin: 0;
 }
 
+.main-content {
+  margin: 24px 24px 0 !important;
+}
+
 .search-form {
   :deep(.el-form-item) {
     margin-bottom: 12px;

+ 5 - 0
src/views/system/user/index.vue

@@ -150,6 +150,7 @@ const {
             ref="tableRef"
             row-key="id"
             adaptive
+            :adaptiveConfig="{ offsetBottom: 108 }"
             align-whole="center"
             table-layout="auto"
             :loading="loading"
@@ -260,6 +261,10 @@ const {
   outline: none;
 }
 
+.main-content {
+  margin: 24px 24px 0 !important;
+}
+
 .search-form {
   :deep(.el-form-item) {
     margin-bottom: 12px;

+ 1 - 1
src/views/system/user/tree.vue

@@ -96,7 +96,7 @@ defineExpose({ onTreeReset });
   <div
     v-loading="props.treeLoading"
     class="h-full bg-bg_color overflow-auto"
-    :style="{ minHeight: `calc(100vh - 133px)` }"
+    :style="{ minHeight: `calc(100vh - 145px)` }"
   >
     <div class="flex items-center h-[34px]">
       <el-input

+ 1 - 1
src/views/system/user/utils/hook.tsx

@@ -104,7 +104,7 @@ export function useUser(tableRef: Ref, treeRef: Ref) {
       cellRenderer: ({ row, props }) => (
         <el-tag
           size={props.size}
-          type={row.sex === 1 ? "danger" : ""}
+          type={row.sex === 1 ? "danger" : null}
           effect="plain"
         >
           {row.sex === 1 ? "女" : "男"}

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov