Browse Source

更新权限

王飞 1 year ago
parent
commit
9bf7b66a09
100 changed files with 4738 additions and 1827 deletions
  1. 2 2
      .env.base
  2. 2 2
      .env.dev
  3. 2 2
      .env.gitee
  4. 2 2
      .env.pro
  5. 2 2
      .env.test
  6. 1 0
      .eslintrc.js
  7. 103 0
      CHANGELOG.md
  8. 15 3
      README.md
  9. 15 3
      README.zh-CN.md
  10. 1 1
      index.html
  11. 49 41
      mock/analysis/index.ts
  12. 233 0
      mock/department/index.ts
  13. 24 20
      mock/dict/index.ts
  14. 263 0
      mock/menu/index.ts
  15. 490 204
      mock/role/index.ts
  16. 148 17
      mock/table/index.ts
  17. 17 11
      mock/user/index.ts
  18. 124 114
      mock/workplace/index.ts
  19. 56 54
      package.json
  20. 8 8
      src/App.vue
  21. 2 2
      src/api/common/index.ts
  22. 30 0
      src/api/department/index.ts
  23. 32 0
      src/api/department/types.ts
  24. 5 0
      src/api/menu/index.ts
  25. 5 0
      src/api/role/index.ts
  26. 5 1
      src/api/table/index.ts
  27. 0 0
      src/assets/svgs/login-box-bg.svg
  28. 2 0
      src/components/ConfigGlobal/index.ts
  29. 2 3
      src/components/ConfigGlobal/src/ConfigGlobal.vue
  30. 5 0
      src/components/ConfigGlobal/src/types/index.ts
  31. 7 41
      src/components/ContentDetailWrap/src/ContentDetailWrap.vue
  32. 2 0
      src/components/ContextMenu/index.ts
  33. 3 3
      src/components/ContextMenu/src/ContextMenu.vue
  34. 7 0
      src/components/ContextMenu/src/types/index.ts
  35. 2 0
      src/components/Descriptions/index.ts
  36. 118 108
      src/components/Descriptions/src/Descriptions.vue
  37. 4 0
      src/components/Descriptions/src/types/index.ts
  38. 41 28
      src/components/Dialog/src/Dialog.vue
  39. 2 3
      src/components/Editor/src/Editor.vue
  40. 37 4
      src/components/Form/index.ts
  41. 200 92
      src/components/Form/src/Form.vue
  42. 14 8
      src/components/Form/src/components/useRenderCheckbox.tsx
  43. 14 8
      src/components/Form/src/components/useRenderRadio.tsx
  44. 27 26
      src/components/Form/src/components/useRenderSelect.tsx
  45. 0 150
      src/components/Form/src/helper.ts
  46. 10 6
      src/components/Form/src/helper/componentMap.ts
  47. 162 0
      src/components/Form/src/helper/index.ts
  48. 0 17
      src/components/Form/src/types.ts
  49. 663 0
      src/components/Form/src/types/index.ts
  50. 2 0
      src/components/Icon/index.ts
  51. 19 38
      src/components/Icon/src/Icon.vue
  52. 1 0
      src/components/Icon/src/types/index.ts
  53. 2 2
      src/components/ImageViewer/index.ts
  54. 1 1
      src/components/ImageViewer/src/ImageViewer.vue
  55. 1 1
      src/components/ImageViewer/src/types/index.ts
  56. 2 0
      src/components/Infotip/index.ts
  57. 2 2
      src/components/Infotip/src/Infotip.vue
  58. 1 1
      src/components/Infotip/src/types/index.ts
  59. 4 14
      src/components/InputPassword/src/InputPassword.vue
  60. 2 0
      src/components/LocaleDropdown/index.ts
  61. 1 1
      src/components/LocaleDropdown/src/LocaleDropdown.vue
  62. 0 0
      src/components/LocaleDropdown/src/types/index.ts
  63. 1 2
      src/components/Logo/src/Logo.vue
  64. 36 36
      src/components/Menu/src/Menu.vue
  65. 4 0
      src/components/Permission/index.ts
  66. 29 0
      src/components/Permission/src/Permission.vue
  67. 14 0
      src/components/Permission/src/utils.ts
  68. 2 0
      src/components/Qrcode/index.ts
  69. 1 1
      src/components/Qrcode/src/Qrcode.vue
  70. 0 0
      src/components/Qrcode/src/types/index.ts
  71. 12 0
      src/components/Search/index.ts
  72. 186 69
      src/components/Search/src/Search.vue
  73. 59 0
      src/components/Search/src/components/ActionButton.vue
  74. 16 0
      src/components/Search/src/types/index.ts
  75. 7 11
      src/components/Setting/src/Setting.vue
  76. 2 3
      src/components/SizeDropdown/src/SizeDropdown.vue
  77. 0 3
      src/components/Sticky/index.ts
  78. 0 141
      src/components/Sticky/src/Sticky.vue
  79. 2 13
      src/components/TabMenu/src/TabMenu.vue
  80. 12 3
      src/components/Table/index.ts
  81. 371 129
      src/components/Table/src/Table.vue
  82. 151 0
      src/components/Table/src/components/TableActions.vue
  83. 0 0
      src/components/Table/src/helper/index.ts
  84. 0 26
      src/components/Table/src/types.ts
  85. 97 0
      src/components/Table/src/types/index.ts
  86. 12 15
      src/components/TableSetting/src/TableSetting.vue
  87. 57 60
      src/components/TagsView/src/TagsView.vue
  88. 50 6
      src/components/UserInfo/src/UserInfo.vue
  89. 101 0
      src/components/UserInfo/src/components/LockDialog.vue
  90. 270 0
      src/components/UserInfo/src/components/LockPage.vue
  91. 2 0
      src/components/index.ts
  92. 71 16
      src/config/axios/config.ts
  93. 21 14
      src/config/axios/index.ts
  94. 55 63
      src/config/axios/service.ts
  95. 44 0
      src/config/axios/types/index.ts
  96. 5 15
      src/directives/permission/hasPermi.ts
  97. 0 0
      src/hooks/event/useEmitt.ts
  98. 0 17
      src/hooks/web/useCache.ts
  99. 1 1
      src/hooks/web/useConfigGlobal.ts
  100. 51 137
      src/hooks/web/useCrudSchemas.ts

+ 2 - 2
.env.base

@@ -2,10 +2,10 @@
 NODE_ENV=development
 
 # 接口前缀
-VITE_API_BASEPATH=base
+VITE_API_BASE_PATH=base
 
 # 打包路径
 VITE_BASE_PATH=/
 
 # 标题
-VITE_APP_TITLE=EAdmin
+VITE_APP_TITLE=ElementAdmin

+ 2 - 2
.env.dev

@@ -2,7 +2,7 @@
 NODE_ENV=production
 
 # 接口前缀
-VITE_API_BASEPATH=dev
+VITE_API_BASE_PATH=dev
 
 # 打包路径
 VITE_BASE_PATH=/dist-dev/
@@ -20,4 +20,4 @@ VITE_SOURCEMAP=true
 VITE_OUT_DIR=dist-dev
 
 # 标题
-VITE_APP_TITLE=EAdmin
+VITE_APP_TITLE=ElementAdmin

+ 2 - 2
.env.gitee

@@ -2,7 +2,7 @@
 NODE_ENV=production
 
 # 接口前缀
-VITE_API_BASEPATH=pro
+VITE_API_BASE_PATH=pro
 
 # 打包路径
 VITE_BASE_PATH=/vue-element-plus-admin/
@@ -20,4 +20,4 @@ VITE_SOURCEMAP=false
 VITE_OUT_DIR=dist-pro
 
 # 标题
-VITE_APP_TITLE=EAdmin
+VITE_APP_TITLE=ElementAdmin

+ 2 - 2
.env.pro

@@ -2,7 +2,7 @@
 NODE_ENV=production
 
 # 接口前缀
-VITE_API_BASEPATH=pro
+VITE_API_BASE_PATH=pro
 
 # 打包路径
 VITE_BASE_PATH=/
@@ -20,4 +20,4 @@ VITE_SOURCEMAP=false
 VITE_OUT_DIR=dist-pro
 
 # 标题
-VITE_APP_TITLE=EAdmin
+VITE_APP_TITLE=ElementAdmin

+ 2 - 2
.env.test

@@ -2,7 +2,7 @@
 NODE_ENV=production
 
 # 接口前缀
-VITE_API_BASEPATH=test
+VITE_API_BASE_PATH=test
 
 # 打包路径
 VITE_BASE_PATH=/dist-test/
@@ -20,4 +20,4 @@ VITE_SOURCEMAP=true
 VITE_OUT_DIR=dist-test
 
 # 标题
-VITE_APP_TITLE=EAdmin
+VITE_APP_TITLE=ElementAdmin

+ 1 - 0
.eslintrc.js

@@ -24,6 +24,7 @@ module.exports = defineConfig({
     'plugin:prettier/recommended'
   ],
   rules: {
+    'vue/no-setup-props-destructure': 'off',
     'vue/script-setup-uses-vars': 'error',
     'vue/no-reserved-component-names': 'off',
     '@typescript-eslint/ban-ts-ignore': 'off',

+ 103 - 0
CHANGELOG.md

@@ -2,6 +2,109 @@
 
 All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
 
+## [2.0.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.10.0...v2.0.0) (2023-08-06)
+
+
+### ⚠ BREAKING CHANGES
+
+* 重构完成
+
+### Features
+
+* 重构完成 ([76e971e](https://github.com/kailong321200875/vue-element-plus-admin/commit/76e971ef96ad4f5cc7df58abd0559898ce70207d))
+
+
+### Code Refactoring
+
+* 重构完成 ([85f8cda](https://github.com/kailong321200875/vue-element-plus-admin/commit/85f8cda19d8cafb951f211b845aad970a661dd1e))
+* 重构完成 ([5d55597](https://github.com/kailong321200875/vue-element-plus-admin/commit/5d55597cca6c9d2bc6cb6211a01c161fa5f086ba))
+
+## [1.10.0](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.9.9...v1.10.0) (2023-08-06)
+
+
+### Types
+
+* Form类型调整 ([a0f4aeb](https://github.com/kailong321200875/vue-element-plus-admin/commit/a0f4aebc5a685366cd56b1a7bb39fa614976e3bb))
+* Form类型调整 ([674d760](https://github.com/kailong321200875/vue-element-plus-admin/commit/674d760029b451c0c6fc23a2aeac5c83992a0b27))
+* 修改类型 ([c3ac191](https://github.com/kailong321200875/vue-element-plus-admin/commit/c3ac1915045d4d59bca09ec6d19151bc5da342f1))
+* 修改类型 ([7d0476f](https://github.com/kailong321200875/vue-element-plus-admin/commit/7d0476f47c5858019db871cff2bdd19f0210f0d4))
+* 类型优化 ([283bc58](https://github.com/kailong321200875/vue-element-plus-admin/commit/283bc58d46151a8954bb81ee6bf8f499177b15fc))
+* 调整类型 ([24c8af9](https://github.com/kailong321200875/vue-element-plus-admin/commit/24c8af91835fb2c8c00e7c2673fff01f098c9944))
+* 迁移types ([ccbec86](https://github.com/kailong321200875/vue-element-plus-admin/commit/ccbec865568b1c9b3c3321d7071c164fdc350a0f))
+* 迁移types ([46b35e4](https://github.com/kailong321200875/vue-element-plus-admin/commit/46b35e48b3e7876c74159625b5149ef663396f5c))
+
+
+### Features
+
+* axios 改造 ([3238140](https://github.com/kailong321200875/vue-element-plus-admin/commit/32381408bbe418eeaca2a975305bac80ddaa03f5))
+* axios 改造 ([5807db1](https://github.com/kailong321200875/vue-element-plus-admin/commit/5807db1dc12a7ff2dbf66801a742a78974ad8f9c))
+* Descriptions组件重构 ([49e415d](https://github.com/kailong321200875/vue-element-plus-admin/commit/49e415d27788cb468c96f2a828f1df7ae65b7a3c))
+* Dialog组件重构 ([3701a04](https://github.com/kailong321200875/vue-element-plus-admin/commit/3701a04231af02ec7f7ef73533f3a22e707380fb))
+* Form useForm 完成 ([3e4e27c](https://github.com/kailong321200875/vue-element-plus-admin/commit/3e4e27c21fd59c944229856bee929f005d2ee140))
+* Form改造 ([9c724dc](https://github.com/kailong321200875/vue-element-plus-admin/commit/9c724dc9aad18397d5ecd00e53c3c24e142a34b5))
+* Icon改版 ([882f162](https://github.com/kailong321200875/vue-element-plus-admin/commit/882f162ff21c74239b638f284f52161e5791722d))
+* Radio改造 ([deeee73](https://github.com/kailong321200875/vue-element-plus-admin/commit/deeee73bcb3ad912844fddee62b1155d95d4b42b))
+* Radio改造 ([83513d5](https://github.com/kailong321200875/vue-element-plus-admin/commit/83513d519d4b6b8fbfd48db266b9bd7b3a998d63))
+* Search组件重构 ([a7f3702](https://github.com/kailong321200875/vue-element-plus-admin/commit/a7f370214481577ab82bf2871191dda717c7978a))
+* SelectV2改造完成 ([4d04734](https://github.com/kailong321200875/vue-element-plus-admin/commit/4d04734e13f6926c16aeee421feecb0d339534f0))
+* Table重构 ([94800b0](https://github.com/kailong321200875/vue-element-plus-admin/commit/94800b0120ee05ca7d534dda3e59653f38d7fda0))
+* 完善search组件demo ([cdf44a4](https://github.com/kailong321200875/vue-element-plus-admin/commit/cdf44a43a05010dbcba3a3ec0cb7c8251f16fce3))
+* 拖拽表格 ([b69b8ed](https://github.com/kailong321200875/vue-element-plus-admin/commit/b69b8ed1bde36100fc86e51fcc63805d4ea21210))
+* 新增TreeSelect表单项 ([de0cb43](https://github.com/kailong321200875/vue-element-plus-admin/commit/de0cb43566b9065250abbc71548ffeca4c8e8bf1))
+* 新增Uload ([c181887](https://github.com/kailong321200875/vue-element-plus-admin/commit/c181887f7f0c5eecc9584edfe99e9065440bdc56))
+* 新增useStorage ([dfea91c](https://github.com/kailong321200875/vue-element-plus-admin/commit/dfea91c7e1d18fa299067c62557cac61723ea861))
+* 新增权限测试页 ([3fe40ba](https://github.com/kailong321200875/vue-element-plus-admin/commit/3fe40ba62df29c2ffea9adfd65fc559489481e24))
+* 新增锁屏功能 ([e2fd349](https://github.com/kailong321200875/vue-element-plus-admin/commit/e2fd349070147c57f9400fa9a413260b7707bda2))
+* 用户列表重构 ([755cea0](https://github.com/kailong321200875/vue-element-plus-admin/commit/755cea0990d9e3b64c936f29c02e4053393a1a19))
+* 登录页改造 ([5312951](https://github.com/kailong321200875/vue-element-plus-admin/commit/5312951359b5d919b6c1a03783aa6bbaf8ec0044))
+* 综合示例重构 ([9a0259d](https://github.com/kailong321200875/vue-element-plus-admin/commit/9a0259de5c47970502db95f4dda24998ad5d9efe))
+* 菜单管理 ([c72b3a3](https://github.com/kailong321200875/vue-element-plus-admin/commit/c72b3a33aab7d3605770a64d23b8a84ef4ad68d2))
+* 角色管理 ([47016a5](https://github.com/kailong321200875/vue-element-plus-admin/commit/47016a535f2b7a22ab498bee197bc30a983f507d))
+* 部门管理 ([28d0785](https://github.com/kailong321200875/vue-element-plus-admin/commit/28d0785be842022cae7808c23e1f19eaab5fb996))
+* 重构Dialog组件示例 ([9a78ac9](https://github.com/kailong321200875/vue-element-plus-admin/commit/9a78ac977eb0cfb3bd6c2a9b96e69d9f010017f4))
+
+
+### Bug Fixes
+
+* mock数据 ([8bdac71](https://github.com/kailong321200875/vue-element-plus-admin/commit/8bdac7152f463cd98c50c9893a46bb6c111fd428))
+* 修复Form已知问题 ([097b32e](https://github.com/kailong321200875/vue-element-plus-admin/commit/097b32e1a9d92a609a66179d68b3dabe12f96b66))
+* 修复Table组件已知问题 ([b1a83f6](https://github.com/kailong321200875/vue-element-plus-admin/commit/b1a83f601838cb82fb29c036654a4cdc729997cd))
+* 修复类型错误 ([26dc886](https://github.com/kailong321200875/vue-element-plus-admin/commit/26dc886f8ccb5cf1ffc10e1d9601c827a1f960c2))
+* 样式问题修复 ([cdc7c76](https://github.com/kailong321200875/vue-element-plus-admin/commit/cdc7c76eb5ac3ccb79f5f55ff5b7ce6b8c4955e1))
+* 解决类型检测报错 ([9d93496](https://github.com/kailong321200875/vue-element-plus-admin/commit/9d9349600b3d2008e4216d49c9fa1c1b9995fa79))
+* 解决类型检测报错 ([513108c](https://github.com/kailong321200875/vue-element-plus-admin/commit/513108c00e622812e2e70dfe833435f6b5462d6e))
+* 解决类型检测报错 ([28bf8be](https://github.com/kailong321200875/vue-element-plus-admin/commit/28bf8bee45e3cc8575a356623abdbe56e30991f8))
+
+
+### Styling
+
+* Table样式修改 ([5fc57bd](https://github.com/kailong321200875/vue-element-plus-admin/commit/5fc57bdb08488f6898eafd6f28289b0567d6d9e2))
+* Table样式修改 ([411c0f7](https://github.com/kailong321200875/vue-element-plus-admin/commit/411c0f792ae8359c49e81974d8193f049120985b))
+* Table样式修改 ([d487c6a](https://github.com/kailong321200875/vue-element-plus-admin/commit/d487c6a93ec0281d76a3938e6e23ea2a4a7940c1))
+* Table样式修改 ([c7d21e3](https://github.com/kailong321200875/vue-element-plus-admin/commit/c7d21e36d012377ba863ac848d77abb5db4f475a))
+* Table样式修改 ([7f5078a](https://github.com/kailong321200875/vue-element-plus-admin/commit/7f5078a436c4d5abcaf7a420df35d2be9b3680c5))
+* 修改Dialog样式 ([e451bfc](https://github.com/kailong321200875/vue-element-plus-admin/commit/e451bfcde6e5a47d4b3022e240ffcc0576ebb9a8))
+* 修改样式 ([207c5b3](https://github.com/kailong321200875/vue-element-plus-admin/commit/207c5b3fc4e52bb06baa36cd4b659e14893785ba))
+* 完善角色管理 ([c4576bd](https://github.com/kailong321200875/vue-element-plus-admin/commit/c4576bd57bcf504733f20188202ea7d33ab1c184))
+* 布局样式优化 ([962689a](https://github.com/kailong321200875/vue-element-plus-admin/commit/962689a8bd0ed5eb17d946b8a21dec4a197f13a7))
+* 样式布局调整完成 ([7193176](https://github.com/kailong321200875/vue-element-plus-admin/commit/719317694f71e22692256bb557070343f034ffe5))
+* 用户管理样式修改 ([57a5fa7](https://github.com/kailong321200875/vue-element-plus-admin/commit/57a5fa7b82ae9f3d7a1f8ec5391f14b1d1cd32e8))
+* 移除不必要样式 ([7ef1d1e](https://github.com/kailong321200875/vue-element-plus-admin/commit/7ef1d1e3013cc5bf7fc574e67c2004f50792e66d))
+* 移除不必要样式 ([366db45](https://github.com/kailong321200875/vue-element-plus-admin/commit/366db4528254d18659e6a922817702b5b92a57b0))
+* 调整Icon悬停样式 ([64c7e48](https://github.com/kailong321200875/vue-element-plus-admin/commit/64c7e48bd18ba83e605daccbc4c2f4cc6b58695d))
+* 调整工作台样式错乱 ([cc18f29](https://github.com/kailong321200875/vue-element-plus-admin/commit/cc18f297ef50655d5773d01fcfddabc365dc53e7))
+
+
+### Performance Improvements
+
+* Dialog默认高度修改 ([0e04fce](https://github.com/kailong321200875/vue-element-plus-admin/commit/0e04fce4367d6829e8de97a249318b0309e06fd5))
+* Form Table Search Descriptions 支持嵌套赋值 ([46ddf62](https://github.com/kailong321200875/vue-element-plus-admin/commit/46ddf62d2d4ce1a653f47695cb0bb3475aa16bd8))
+* ImageViewer组件优化 ([3b9c3d8](https://github.com/kailong321200875/vue-element-plus-admin/commit/3b9c3d8b757646eaf74625403112a969bfd15e55))
+* 优化Form事件传递 ([69cafb3](https://github.com/kailong321200875/vue-element-plus-admin/commit/69cafb3b7b2ce7ecbd9f2e8ef09e250817e9a55c))
+* 优化Search组件 ([e548668](https://github.com/kailong321200875/vue-element-plus-admin/commit/e548668ccef8c41d9ac7d9fe39ffe66471d160d2))
+* 优化表单组件 ([77a3866](https://github.com/kailong321200875/vue-element-plus-admin/commit/77a38662488ab9ff4cbe5ff3cf9b65eea34abca1))
+* 优化锁屏组件 ([4f8330a](https://github.com/kailong321200875/vue-element-plus-admin/commit/4f8330a4faf6cc98a9bac17bd3e1719ae1b30c81))
+
 ## [1.9.9](https://github.com/kailong321200875/vue-element-plus-admin/compare/v1.9.8...v1.9.9) (2023-04-13)
 
 

+ 15 - 3
README.md

@@ -1,6 +1,6 @@
 <div align="center"> <a href="https://github.com/kailong321200875/vue-element-plus-admin"> <img width="100" src="./public/logo.png"> </a> <br> <br>
 
-[![license](https://img.shields.io/github/license/kailong321200875/vue-element-plus-admin.svg)](LICENSE)
+[![license](https://img.shields.io/github/license/kailong321200875/vue-element-plus-admin.svg)](LICENSE) [![repo-size](https://img.shields.io/github/repo-size/kailong321200875/vue-element-plus-admin.svg)](repo-size) [![last-commit](https://img.shields.io/github/last-commit/kailong321200875/vue-element-plus-admin.svg)](last-commit) [![stars](https://img.shields.io/github/stars/kailong321200875/vue-element-plus-admin.svg)](stars) [![forks](https://img.shields.io/github/forks/kailong321200875/vue-element-plus-admin.svg)](forks) [![release](https://img.shields.io/github/release/kailong321200875/vue-element-plus-admin.svg)](release) [![downloads](https://img.shields.io/github/downloads/kailong321200875/vue-element-plus-admin/total.svg)](downloads) [![watchers](https://img.shields.io/github/watchers/kailong321200875/vue-element-plus-admin.svg)](watchers)
 
 <h1>vue-element-plus-admin</h1>
 </div>
@@ -9,11 +9,11 @@
 
 ## Introduction
 
-vue-element-plus-admin is a free and open source middle and background template based on `element-plus`. Developed using the latest mainstream technologies such as `vue3`, `vite4` and `typescript`, the out of the box middle and background front-end solution can be used as the starting template of the project and learning reference. And always pay attention to the latest technological trends and update them as soon as possible.
+vue-element-plus-admin is a free and open source middle and background template based on `element-plus`. Developed using the latest mainstream technologies such as `vue3`, `vite` and `typescript`, the out of the box middle and background front-end solution can be used as the starting template of the project and learning reference. And always pay attention to the latest technological trends and update them as soon as possible.
 
 vue-element-plus-admin is positioned as a background integration scheme, which is not suitable for secondary development as a basic template. Because it integrates many functions that you may not use, it will cause a lot of code redundancy. If your project doesn't pay attention to this problem, you can also directly carry out secondary development based on it.
 
-If you need a basic template, please switch to the `tempalte` branch. `Tempalte` simply integrates some common layout functions such as layout and dynamic menu, which is more suitable for developers to carry out secondary development.
+If you need a basic template, please switch to the `mini` branch. `mini` simply integrates some common layout functions such as layout and dynamic menu, which is more suitable for developers to carry out secondary development.
 
 ## Feature
 
@@ -37,6 +37,8 @@ account: **admin/admin test/test**
 
 `test` account is used to simulate the front-end control authority. The server only returns the menu key to be displayed, and the front-end performs matching rendering
 
+Online examples do not apply to menu filtering by default, but directly use Static routing
+
 ## Documentation
 
 [Document Github](https://element-plus-admin-doc.cn/)
@@ -127,6 +129,16 @@ Support modern browsers, not IE
 | :-: | :-: | :-: | :-: | :-: |
 | not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
 
+## Donate
+
+If you find this project helpful, welcome sponsorship to show your support~
+
+<img src="https://github.com/kailong321200875/my-image/raw/master/pay.jpg" />
+
+## Group
+
+<img src="https://github.com/kailong321200875/my-image/raw/master/chat.jpg" />
+
 ## License
 
 [MIT](./LICENSE)

+ 15 - 3
README.zh-CN.md

@@ -1,6 +1,6 @@
 <div align="center"> <a href="https://github.com/kailong321200875/vue-element-plus-admin"> <img width="100" src="./public/logo.png"> </a> <br> <br>
 
-[![license](https://img.shields.io/github/license/kailong321200875/vue-element-plus-admin.svg)](LICENSE)
+[![license](https://img.shields.io/github/license/kailong321200875/vue-element-plus-admin.svg)](LICENSE) [![repo-size](https://img.shields.io/github/repo-size/kailong321200875/vue-element-plus-admin.svg)](repo-size) [![last-commit](https://img.shields.io/github/last-commit/kailong321200875/vue-element-plus-admin.svg)](last-commit) [![stars](https://img.shields.io/github/stars/kailong321200875/vue-element-plus-admin.svg)](stars) [![forks](https://img.shields.io/github/forks/kailong321200875/vue-element-plus-admin.svg)](forks) [![release](https://img.shields.io/github/release/kailong321200875/vue-element-plus-admin.svg)](release) [![downloads](https://img.shields.io/github/downloads/kailong321200875/vue-element-plus-admin/total.svg)](downloads) [![watchers](https://img.shields.io/github/watchers/kailong321200875/vue-element-plus-admin.svg)](watchers)
 
 <h1>vue-element-plus-admin</h1>
 </div>
@@ -9,11 +9,11 @@
 
 ## 介绍
 
-vue-element-plus-admin 是一个基于 `element-plus` 免费开源的中后台模版。使用了最新的`vue3`,`vite4`,`TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,可以用来作为项目的启动模版,也可用于学习参考。并且时刻关注着最新技术动向,尽可能的第一时间更新。
+vue-element-plus-admin 是一个基于 `element-plus` 免费开源的中后台模版。使用了最新的`vue3`,`vite`,`TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,可以用来作为项目的启动模版,也可用于学习参考。并且时刻关注着最新技术动向,尽可能的第一时间更新。
 
 vue-element-plus-admin 的定位是后台集成方案,不太适合当基础模板来进行二次开发。因为集成了很多你可能用不到的功能,会造成不少的代码冗余。如果你的项目不关注这方面的问题,也可以直接基于它进行二次开发。
 
-如需要基础模版,请切换到 `tempalte` 分支,`tempalte` 只简单集成了一些如:布局、动态菜单等常用布局功能,更适合开发者进行二次开发。
+如需要基础模版,请切换到 `mini` 分支,`mini` 只简单集成了一些如:布局、动态菜单等常用布局功能,更适合开发者进行二次开发。
 
 ## 特性
 
@@ -37,6 +37,8 @@ vue-element-plus-admin 的定位是后台集成方案,不太适合当基础模
 
 `test` 帐号用于模拟前端控制权限,服务端只返回需要显示的菜单 key,前端进行匹配渲染
 
+在线例子默认不适用菜单过滤,而是直接使用静态路由表
+
 ## 文档
 
 [文档地址 Github](https://element-plus-admin-doc.cn/)
@@ -127,6 +129,16 @@ pnpm run build:pro
 | :-: | :-: | :-: | :-: | :-: |
 | not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
 
+## Donate
+
+如果你觉得这个项目有帮助,欢迎赞助以示支持~
+
+<img src="https://gitee.com/kailong110120130/my-image/raw/master/pay.jpg" />
+
+## 交流群
+
+<img src="https://gitee.com/kailong110120130/my-image/raw/master/chat.jpg" />
+
 ## 许可证
 
 [MIT](./LICENSE)

+ 1 - 1
index.html

@@ -60,7 +60,7 @@
           border-bottom: 0;
           border-left-color: transparent;
           border-radius: 50%;
-          animation: loader-outter 1s cubic-bezier(0.42, 0.61, 0.58, 0.41) infinite;
+          animation: loader-outter 1.5s cubic-bezier(0.42, 0.61, 0.58, 0.41) infinite;
         }
 
         .app-loading .app-loading-inner {

+ 49 - 41
mock/analysis/index.ts

@@ -1,9 +1,9 @@
-import { config } from '@/config/axios/config'
+import config from '@/config/axios/config'
 import { MockMethod } from 'vite-plugin-mock'
 
-const { result_code } = config
+const { code } = config
 
-const timeout = 0
+const timeout = 1
 
 export default [
   // 分析页统计接口
@@ -13,12 +13,14 @@ export default [
     timeout,
     response: () => {
       return {
-        code: result_code,
         data: {
-          users: 102400,
-          messages: 81212,
-          moneys: 9280,
-          shoppings: 13600
+          code: code,
+          data: {
+            users: 102400,
+            messages: 81212,
+            moneys: 9280,
+            shoppings: 13600
+          }
         }
       }
     }
@@ -30,14 +32,16 @@ export default [
     timeout,
     response: () => {
       return {
-        code: result_code,
-        data: [
-          { value: 10, name: 'analysis.directAccess' },
-          { value: 310, name: 'analysis.mailMarketing' },
-          { value: 234, name: 'analysis.allianceAdvertising' },
-          { value: 135, name: 'analysis.videoAdvertising' },
-          { value: 1548, name: 'analysis.searchEngines' }
-        ]
+        data: {
+          code: code,
+          data: [
+            { value: 1000, name: 'analysis.directAccess' },
+            { value: 310, name: 'analysis.mailMarketing' },
+            { value: 234, name: 'analysis.allianceAdvertising' },
+            { value: 135, name: 'analysis.videoAdvertising' },
+            { value: 1548, name: 'analysis.searchEngines' }
+          ]
+        }
       }
     }
   },
@@ -48,16 +52,18 @@ export default [
     timeout,
     response: () => {
       return {
-        code: result_code,
-        data: [
-          { value: 13253, name: 'analysis.monday' },
-          { value: 34235, name: 'analysis.tuesday' },
-          { value: 26321, name: 'analysis.wednesday' },
-          { value: 12340, name: 'analysis.thursday' },
-          { value: 24643, name: 'analysis.friday' },
-          { value: 1322, name: 'analysis.saturday' },
-          { value: 1324, name: 'analysis.sunday' }
-        ]
+        data: {
+          code: code,
+          data: [
+            { value: 13253, name: 'analysis.monday' },
+            { value: 34235, name: 'analysis.tuesday' },
+            { value: 26321, name: 'analysis.wednesday' },
+            { value: 12340, name: 'analysis.thursday' },
+            { value: 24643, name: 'analysis.friday' },
+            { value: 1322, name: 'analysis.saturday' },
+            { value: 1324, name: 'analysis.sunday' }
+          ]
+        }
       }
     }
   },
@@ -68,21 +74,23 @@ export default [
     timeout,
     response: () => {
       return {
-        code: result_code,
-        data: [
-          { estimate: 100, actual: 120, name: 'analysis.january' },
-          { estimate: 120, actual: 82, name: 'analysis.february' },
-          { estimate: 161, actual: 91, name: 'analysis.march' },
-          { estimate: 134, actual: 154, name: 'analysis.april' },
-          { estimate: 105, actual: 162, name: 'analysis.may' },
-          { estimate: 160, actual: 140, name: 'analysis.june' },
-          { estimate: 165, actual: 145, name: 'analysis.july' },
-          { estimate: 114, actual: 250, name: 'analysis.august' },
-          { estimate: 163, actual: 134, name: 'analysis.september' },
-          { estimate: 185, actual: 56, name: 'analysis.october' },
-          { estimate: 118, actual: 99, name: 'analysis.november' },
-          { estimate: 123, actual: 123, name: 'analysis.december' }
-        ]
+        data: {
+          code: code,
+          data: [
+            { estimate: 100, actual: 120, name: 'analysis.january' },
+            { estimate: 120, actual: 82, name: 'analysis.february' },
+            { estimate: 161, actual: 91, name: 'analysis.march' },
+            { estimate: 134, actual: 154, name: 'analysis.april' },
+            { estimate: 105, actual: 162, name: 'analysis.may' },
+            { estimate: 160, actual: 140, name: 'analysis.june' },
+            { estimate: 165, actual: 145, name: 'analysis.july' },
+            { estimate: 114, actual: 250, name: 'analysis.august' },
+            { estimate: 163, actual: 134, name: 'analysis.september' },
+            { estimate: 185, actual: 56, name: 'analysis.october' },
+            { estimate: 118, actual: 99, name: 'analysis.november' },
+            { estimate: 123, actual: 123, name: 'analysis.december' }
+          ]
+        }
       }
     }
   }

+ 233 - 0
mock/department/index.ts

@@ -0,0 +1,233 @@
+import config from '@/config/axios/config'
+import { MockMethod } from 'vite-plugin-mock'
+import { toAnyString } from '@/utils'
+import Mock from 'mockjs'
+
+const { code } = config
+
+const departmentList: any = []
+
+const citys = ['厦门总公司', '北京分公司', '上海分公司', '福州分公司', '深圳分公司', '杭州分公司']
+
+for (let i = 0; i < 5; i++) {
+  let departmentId = toAnyString()
+  departmentList.push({
+    // 部门名称
+    departmentName: citys[i],
+    id: departmentId,
+    createTime: '@datetime',
+    superiorDepartment: '',
+    // 状态
+    status: Mock.Random.integer(0, 1),
+    // 备注
+    remark: '@cword(10, 15)',
+    children: [
+      {
+        // 部门名称
+        departmentName: '研发部',
+        createTime: '@datetime',
+        superiorDepartment: departmentId,
+        superiorDepartmentName: citys[i],
+        // 状态
+        status: Mock.Random.integer(0, 1),
+        id: toAnyString(),
+        remark: '@cword(10, 15)'
+      },
+      {
+        // 部门名称
+        departmentName: '产品部',
+        createTime: '@datetime',
+        superiorDepartment: departmentId,
+        superiorDepartmentName: citys[i],
+        // 状态
+        status: Mock.Random.integer(0, 1),
+        id: toAnyString(),
+        remark: '@cword(10, 15)'
+      },
+      {
+        // 部门名称
+        departmentName: '运营部',
+        createTime: '@datetime',
+        superiorDepartment: departmentId,
+        superiorDepartmentName: citys[i],
+        // 状态
+        status: Mock.Random.integer(0, 1),
+        id: toAnyString(),
+        remark: '@cword(10, 15)'
+      },
+      {
+        // 部门名称
+        departmentName: '市场部',
+        createTime: '@datetime',
+        superiorDepartment: departmentId,
+        superiorDepartmentName: citys[i],
+        // 状态
+        status: Mock.Random.integer(0, 1),
+        id: toAnyString(),
+        remark: '@cword(10, 15)'
+      },
+      {
+        // 部门名称
+        departmentName: '销售部',
+        createTime: '@datetime',
+        superiorDepartment: departmentId,
+        superiorDepartmentName: citys[i],
+        // 状态
+        status: Mock.Random.integer(0, 1),
+        id: toAnyString(),
+        remark: '@cword(10, 15)'
+      },
+      {
+        // 部门名称
+        departmentName: '客服部',
+        createTime: '@datetime',
+        superiorDepartment: departmentId,
+        superiorDepartmentName: citys[i],
+        // 状态
+        status: Mock.Random.integer(0, 1),
+        id: toAnyString(),
+        remark: '@cword(10, 15)'
+      }
+    ]
+  })
+}
+
+export default [
+  // 列表接口
+  {
+    url: '/department/list',
+    method: 'get',
+    response: () => {
+      return {
+        data: {
+          code: code,
+          data: {
+            list: departmentList
+          }
+        }
+      }
+    }
+  },
+  {
+    url: '/department/table/list',
+    method: 'get',
+    response: () => {
+      return {
+        data: {
+          code: code,
+          data: {
+            list: departmentList,
+            total: 5
+          }
+        }
+      }
+    }
+  },
+  {
+    url: '/department/users',
+    method: 'get',
+    timeout: 1000,
+    response: ({ query }) => {
+      const { pageSize } = query
+      // 根据pageSize来创建数据
+      const mockList: any = []
+      for (let i = 0; i < pageSize; i++) {
+        mockList.push(
+          Mock.mock({
+            // 用户名
+            username: '@cname',
+            // 账号
+            account: '@first',
+            // 邮箱
+            email: '@EMAIL',
+            // 创建时间
+            createTime: '@datetime',
+            // 角色
+            role: '@first',
+            // 用户id
+            id: toAnyString()
+          })
+        )
+      }
+      return {
+        data: {
+          code: code,
+          data: {
+            total: 100,
+            list: mockList
+          }
+        }
+      }
+    }
+  },
+  // 保存接口
+  {
+    url: '/department/user/save',
+    method: 'post',
+    timeout: 1000,
+    response: () => {
+      return {
+        data: {
+          code: code,
+          data: 'success'
+        }
+      }
+    }
+  },
+  // 删除接口
+  {
+    url: '/department/user/delete',
+    method: 'post',
+    response: ({ body }) => {
+      const ids = body.ids
+      if (!ids) {
+        return {
+          code: '500',
+          message: '请选择需要删除的数据'
+        }
+      } else {
+        return {
+          data: {
+            code: code,
+            data: 'success'
+          }
+        }
+      }
+    }
+  },
+  // 保存接口
+  {
+    url: '/department/save',
+    method: 'post',
+    timeout: 1000,
+    response: () => {
+      return {
+        data: {
+          code: code,
+          data: 'success'
+        }
+      }
+    }
+  },
+  // 删除接口
+  {
+    url: '/department/delete',
+    method: 'post',
+    response: ({ body }) => {
+      const ids = body.ids
+      if (!ids) {
+        return {
+          code: '500',
+          message: '请选择需要删除的数据'
+        }
+      } else {
+        return {
+          data: {
+            code: code,
+            data: 'success'
+          }
+        }
+      }
+    }
+  }
+] as MockMethod[]

+ 24 - 20
mock/dict/index.ts

@@ -1,9 +1,9 @@
-import { config } from '@/config/axios/config'
+import config from '@/config/axios/config'
 import { MockMethod } from 'vite-plugin-mock'
 
-const { result_code } = config
+const { code } = config
 
-const timeout = 0
+const timeout = 1
 
 const dictObj: Recordable = {
   importance: [
@@ -30,8 +30,10 @@ export default [
     timeout,
     response: () => {
       return {
-        code: result_code,
-        data: dictObj
+        data: {
+          code: code,
+          data: dictObj
+        }
       }
     }
   },
@@ -42,21 +44,23 @@ export default [
     timeout,
     response: () => {
       return {
-        code: result_code,
-        data: [
-          {
-            label: 'test1',
-            value: 0
-          },
-          {
-            label: 'test2',
-            value: 1
-          },
-          {
-            label: 'test3',
-            value: 2
-          }
-        ]
+        data: {
+          code: code,
+          data: [
+            {
+              label: 'test1',
+              value: 0
+            },
+            {
+              label: 'test2',
+              value: 1
+            },
+            {
+              label: 'test3',
+              value: 2
+            }
+          ]
+        }
       }
     }
   }

+ 263 - 0
mock/menu/index.ts

@@ -0,0 +1,263 @@
+import config from '@/config/axios/config'
+import { MockMethod } from 'vite-plugin-mock'
+import Mock from 'mockjs'
+
+const { code } = config
+
+const timeout = 1
+
+export default [
+  // 列表接口
+  {
+    url: '/menu/list',
+    method: 'get',
+    timeout,
+    response: () => {
+      return {
+        data: {
+          code: code,
+          data: {
+            list: [
+              {
+                path: '/dashboard',
+                component: '#',
+                redirect: '/dashboard/analysis',
+                name: 'Dashboard',
+                status: Mock.Random.integer(0, 1),
+                id: 1,
+                preciousMenu: '',
+                meta: {
+                  title: '首页',
+                  icon: 'ant-design:dashboard-filled',
+                  alwaysShow: true
+                },
+                children: [
+                  {
+                    path: 'analysis',
+                    component: 'views/Dashboard/Analysis',
+                    name: 'Analysis',
+                    preciousMenu: 'Dashboard',
+                    status: Mock.Random.integer(0, 1),
+                    id: 2,
+                    meta: {
+                      title: '分析页',
+                      noCache: true
+                    }
+                  },
+                  {
+                    path: 'workplace',
+                    component: 'views/Dashboard/Workplace',
+                    name: 'Workplace',
+                    preciousMenu: 'Dashboard',
+                    status: Mock.Random.integer(0, 1),
+                    id: 3,
+                    meta: {
+                      title: '工作台',
+                      noCache: true
+                    }
+                  }
+                ]
+              },
+              {
+                path: '/external-link',
+                component: '#',
+                meta: {
+                  title: '文档',
+                  icon: 'clarity:document-solid'
+                },
+                name: 'ExternalLink',
+                status: Mock.Random.integer(0, 1),
+                id: 4,
+                children: [
+                  {
+                    path: 'https://element-plus-admin-doc.cn/',
+                    name: 'DocumentLink',
+                    status: Mock.Random.integer(0, 1),
+                    id: 5,
+                    meta: {
+                      title: '文档'
+                    }
+                  }
+                ]
+              },
+              {
+                path: '/level',
+                component: '#',
+                redirect: '/level/menu1/menu1-1/menu1-1-1',
+                name: 'Level',
+                status: Mock.Random.integer(0, 1),
+                id: 6,
+                meta: {
+                  title: '菜单',
+                  icon: 'carbon:skill-level-advanced'
+                },
+                children: [
+                  {
+                    path: 'menu1',
+                    name: 'Menu1',
+                    component: '##',
+                    preciousMenu: 'Level',
+                    status: Mock.Random.integer(0, 1),
+                    id: 7,
+                    redirect: '/level/menu1/menu1-1/menu1-1-1',
+                    meta: {
+                      title: '菜单1'
+                    },
+                    children: [
+                      {
+                        path: 'menu1-1',
+                        name: 'Menu11',
+                        preciousMenu: 'Menu1',
+                        component: '##',
+                        status: Mock.Random.integer(0, 1),
+                        id: 8,
+                        redirect: '/level/menu1/menu1-1/menu1-1-1',
+                        meta: {
+                          title: '菜单1-1',
+                          alwaysShow: true
+                        },
+                        children: [
+                          {
+                            path: 'menu1-1-1',
+                            name: 'Menu111',
+                            preciousMenu: 'Menu11',
+                            component: 'views/Level/Menu111',
+                            status: Mock.Random.integer(0, 1),
+                            id: 9,
+                            permission: ['edit', 'add', 'delete'],
+                            meta: {
+                              title: '菜单1-1-1'
+                            }
+                          }
+                        ]
+                      },
+                      {
+                        path: 'menu1-2',
+                        name: 'Menu12',
+                        preciousMenu: 'Menu1',
+                        component: 'views/Level/Menu12',
+                        status: Mock.Random.integer(0, 1),
+                        id: 10,
+                        permission: ['edit', 'add', 'delete'],
+                        meta: {
+                          title: '菜单1-2'
+                        }
+                      }
+                    ]
+                  },
+                  {
+                    path: 'menu2',
+                    name: 'Menu2Demo',
+                    preciousMenu: 'Level',
+                    component: 'views/Level/Menu2',
+                    status: Mock.Random.integer(0, 1),
+                    id: 11,
+                    permission: ['edit', 'add', 'delete'],
+                    meta: {
+                      title: '菜单2'
+                    }
+                  }
+                ]
+              },
+              {
+                path: '/example',
+                component: '#',
+                redirect: '/example/example-dialog',
+                name: 'Example',
+                status: Mock.Random.integer(0, 1),
+                id: 12,
+                meta: {
+                  title: '综合示例',
+                  icon: 'ep:management',
+                  alwaysShow: true
+                },
+                children: [
+                  {
+                    path: 'example-dialog',
+                    component: 'views/Example/Dialog/ExampleDialog',
+                    name: 'ExampleDialog',
+                    preciousMenu: 'Example',
+                    status: Mock.Random.integer(0, 1),
+                    id: 13,
+                    permission: ['edit', 'add', 'delete'],
+                    meta: {
+                      title: '综合示例-弹窗',
+                      permission: ['edit', 'add']
+                    }
+                  },
+                  {
+                    path: 'example-page',
+                    component: 'views/Example/Page/ExamplePage',
+                    name: 'ExamplePage',
+                    preciousMenu: 'Example',
+                    status: Mock.Random.integer(0, 1),
+                    id: 14,
+                    permission: ['edit', 'add', 'delete'],
+                    meta: {
+                      title: '综合示例-页面',
+                      permission: ['edit', 'add']
+                    }
+                  },
+                  {
+                    path: 'example-add',
+                    component: 'views/Example/Page/ExampleAdd',
+                    name: 'ExampleAdd',
+                    preciousMenu: 'Example',
+                    status: Mock.Random.integer(0, 1),
+                    id: 15,
+                    permission: ['edit', 'add', 'delete'],
+                    meta: {
+                      title: '综合示例-新增',
+                      noTagsView: true,
+                      noCache: true,
+                      hidden: true,
+                      showMainRoute: true,
+                      activeMenu: '/example/example-page',
+                      permission: ['delete', 'add']
+                    }
+                  },
+                  {
+                    path: 'example-edit',
+                    component: 'views/Example/Page/ExampleEdit',
+                    name: 'ExampleEdit',
+                    preciousMenu: 'Example',
+                    status: Mock.Random.integer(0, 1),
+                    id: 16,
+                    permission: ['edit', 'add', 'delete'],
+                    meta: {
+                      title: '综合示例-编辑',
+                      noTagsView: true,
+                      noCache: true,
+                      hidden: true,
+                      showMainRoute: true,
+                      activeMenu: '/example/example-page',
+                      permission: ['delete', 'add']
+                    }
+                  },
+                  {
+                    path: 'example-detail',
+                    component: 'views/Example/Page/ExampleDetail',
+                    name: 'ExampleDetail',
+                    preciousMenu: 'Example',
+                    status: Mock.Random.integer(0, 1),
+                    id: 17,
+                    permission: ['edit', 'add', 'delete'],
+                    meta: {
+                      title: '综合示例-详情',
+                      noTagsView: true,
+                      noCache: true,
+                      hidden: true,
+                      showMainRoute: true,
+                      activeMenu: '/example/example-page',
+                      permission: ['delete', 'edit']
+                    }
+                  }
+                ]
+              }
+            ]
+          }
+        }
+      }
+    }
+  }
+] as MockMethod[]

+ 490 - 204
mock/role/index.ts

@@ -1,9 +1,11 @@
-import { config } from '@/config/axios/config'
+import config from '@/config/axios/config'
 import { MockMethod } from 'vite-plugin-mock'
+import Mock from 'mockjs'
+import { toAnyString } from '@/utils'
 
-const { result_code } = config
+const { code } = config
 
-const timeout = 0
+const timeout = 1
 
 const adminList = [
   {
@@ -12,7 +14,7 @@ const adminList = [
     redirect: '/dashboard/analysis',
     name: 'Dashboard',
     meta: {
-      title: '首页',
+      title: 'router.dashboard',
       icon: 'ant-design:dashboard-filled',
       alwaysShow: true
     },
@@ -37,20 +39,17 @@ const adminList = [
       }
     ]
   },
-
-
   {
     path: '/example',
     component: '#',
     redirect: '/example/example-page',
     name: 'Example',
     meta: {
-      title: '示例',
+      title: 'router.example',
       icon: 'ep:management',
       alwaysShow: true
     },
     children: [
-
       {
         path: 'example-page',
         component: 'views/Example/Page/ExamplePage',
@@ -68,7 +67,7 @@ const adminList = [
           noTagsView: true,
           noCache: true,
           hidden: true,
-          canTo: true,
+          showMainRoute: true,
           activeMenu: '/example/example-page'
         }
       },
@@ -81,7 +80,7 @@ const adminList = [
           noTagsView: true,
           noCache: true,
           hidden: true,
-          canTo: true,
+          showMainRoute: true,
           activeMenu: '/example/example-page'
         }
       },
@@ -94,229 +93,66 @@ const adminList = [
           noTagsView: true,
           noCache: true,
           hidden: true,
-          canTo: true,
+          showMainRoute: true,
           activeMenu: '/example/example-page'
         }
       }
     ]
   },
   {
-    path: '/manage',
+    path: '/authorization',
     component: '#',
-    redirect: '/manage/news',
-    name: 'Manage',
+    redirect: '/authorization/user',
+    name: 'Authorization',
     meta: {
-      title: '模块管理',
-      icon: 'ep:menu',
+      title: 'router.authorization',
+      icon: 'eos-icons:role-binding',
       alwaysShow: true
     },
     children: [
       {
-        path: 'news-page',
-        component: 'views/Manage/News/NewsPage',
-        name: 'NewsPage',
-        meta: {
-          title: '新闻管理'
-        }
-      },
-      {
-        path: 'news-add',
-        component: 'views/Manage/News/NewsAdd',
-        name: 'NewsAdd',
-        meta: {
-          title: '新闻新增',
-          noTagsView: true,
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          activeMenu: '/manage/news-page'
-        }
-      },
-      {
-        path: 'news-edit',
-        component: 'views/Manage/News/NewsEdit',
-        name: 'NewsEdit',
-        meta: {
-          title: '新闻编辑',
-          noTagsView: true,
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          activeMenu: '/manage/news-page'
-        }
-      },
-      {
-        path: 'news-detail',
-        component: 'views/Manage/News/NewsDetail',
-        name: 'NewsDetail',
-        meta: {
-          title: '新闻详情',
-          noTagsView: true,
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          activeMenu: '/manage/news-page'
-        }
-      },
-      {
-        path: 'product-page',
-        component: 'views/Manage/Product/ProductPage',
-        name: 'ProductPage',
-        meta: {
-          title: '产品管理',
-          activeMenu: '/manage/product-page'
-        }
-      },
-      {
-        path: 'product-add',
-        component: 'views/Manage/Product/ProductAdd',
-        name: 'ProductAdd',
-        meta: {
-          title: '产品新增',
-          noTagsView: true,
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          activeMenu: '/manage/product-page'
-        }
-      },
-      {
-        path: 'product-edit',
-        component: 'views/Manage/Product/ProductEdit',
-        name: 'ProductEdit',
+        path: 'department',
+        component: 'views/Authorization/Department/Department',
+        name: 'Department',
         meta: {
-          title: '产品编辑',
-          noTagsView: true,
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          activeMenu: '/manage/product-page'
+          title: 'router.department'
         }
       },
       {
-        path: 'product-detail',
-        component: 'views/Manage/Product/ProductDetail',
-        name: 'ProductDetail',
+        path: 'user',
+        component: 'views/Authorization/User/User',
+        name: 'User',
         meta: {
-          title: '产品详情',
-          noTagsView: true,
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          activeMenu: '/manage/product-page'
-        }
-      },
-    ]
-  },
-  {
-    path: '/setting',
-    component: '#',
-    redirect: '/setting/menu',
-    name: 'Setting',
-    meta: {
-      title: '系统设置',
-      icon: 'ep:setting',
-      alwaysShow: true
-    },
-    children: [
-      {
-        path: 'menu-page',
-        component: 'views/Setting/Menu/MenuPage',
-        name: 'MenuPage',
-        meta: {
-          title: '菜单管理'
+          title: 'router.user'
         }
       },
       {
-        path: 'menu-add',
-        component: 'views/Setting/Menu/MenuAdd',
-        name: 'MenuAdd',
+        path: 'menu',
+        component: 'views/Authorization/Menu/Menu',
+        name: 'Menu',
         meta: {
-          title: '菜单新增',
-          noTagsView: true,
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          activeMenu: '/setting/menu-page'
+          title: 'router.menuManagement'
         }
       },
       {
-        path: 'menu-edit',
-        component: 'views/Setting/Menu/MenuEdit',
-        name: 'MenuEdit',
+        path: 'role',
+        component: 'views/Authorization/Role/Role',
+        name: 'Role',
         meta: {
-          title: '菜单编辑',
-          noTagsView: true,
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          activeMenu: '/setting/menu-page'
+          title: 'router.role'
         }
       },
       {
-        path: 'menu-detail',
-        component: 'views/Setting/Menu/MenuDetail',
-        name: 'MenuDetail',
+        path: 'test',
+        component: 'views/Authorization/Test/Test',
+        name: 'Test',
         meta: {
-          title: '菜单详情',
-          noTagsView: true,
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          activeMenu: '/setting/menu-page'
+          title: 'router.permission',
+          permission: ['add', 'edit', 'delete']
         }
-      },
-      {
-        path: 'user-page',
-        component: 'views/Setting/User/UserPage',
-        name: 'UserPage',
-        meta: {
-          title: '用户管理',
-          activeMenu: '/setting/user-page'
-        }
-      },
-      {
-        path: 'user-add',
-        component: 'views/Setting/User/UserAdd',
-        name: 'UserAdd',
-        meta: {
-          title: '用户新增',
-          noTagsView: true,
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          activeMenu: '/setting/user-page'
-        }
-      },
-      {
-        path: 'user-edit',
-        component: 'views/Setting/User/UserEdit',
-        name: 'UserEdit',
-        meta: {
-          title: '用户编辑',
-          noTagsView: true,
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          activeMenu: '/setting/user-page'
-        }
-      },
-      {
-        path: 'user-detail',
-        component: 'views/Setting/User/UserDetail',
-        name: 'UserDetail',
-        meta: {
-          title: '用户详情',
-          noTagsView: true,
-          noCache: true,
-          hidden: true,
-          canTo: true,
-          activeMenu: '/setting/user-page'
-        }
-      },
+      }
     ]
   }
-
 ]
 
 const testList: string[] = [
@@ -335,6 +171,8 @@ const testList: string[] = [
   '/components/table',
   '/components/table/default-table',
   '/components/table/use-table',
+  '/components/table/tree-table',
+  '/components/table/table-image-preview',
   '/components/table/ref-table',
   '/components/editor-demo',
   '/components/editor-demo/editor',
@@ -352,7 +190,8 @@ const testList: string[] = [
   '/Components/Sticky',
   '/hooks',
   '/hooks/useWatermark',
-  '/hooks/useCrudSchemas',
+  '/hooks/useOpenTab',
+  // '/hooks/useCrudSchemas',
   '/level',
   '/level/menu1',
   '/level/menu1/menu1-1',
@@ -365,12 +204,441 @@ const testList: string[] = [
   '/example/example-add',
   '/example/example-edit',
   '/example/example-detail',
+  '/authorization',
+  '/authorization/department',
+  '/authorization/user',
+  '/authorization/role',
+  '/authorization/menu',
+  '/authorization/test',
   '/error',
   '/error/404-demo',
   '/error/403-demo',
   '/error/500-demo'
 ]
 
+const List: any[] = []
+
+const roleNames = ['超级管理员', '管理员', '普通用户', '游客']
+const menus = [
+  [
+    {
+      path: '/dashboard',
+      component: '#',
+      redirect: '/dashboard/analysis',
+      name: 'Dashboard',
+      status: Mock.Random.integer(0, 1),
+      id: 1,
+      meta: {
+        title: '首页',
+        icon: 'ant-design:dashboard-filled',
+        alwaysShow: true
+      },
+      children: [
+        {
+          path: 'analysis',
+          component: 'views/Dashboard/Analysis',
+          name: 'Analysis',
+          status: Mock.Random.integer(0, 1),
+          id: 2,
+          meta: {
+            title: '分析页',
+            noCache: true
+          }
+        },
+        {
+          path: 'workplace',
+          component: 'views/Dashboard/Workplace',
+          name: 'Workplace',
+          status: Mock.Random.integer(0, 1),
+          id: 3,
+          meta: {
+            title: '工作台',
+            noCache: true
+          }
+        }
+      ]
+    },
+    {
+      path: '/external-link',
+      component: '#',
+      meta: {
+        title: '文档',
+        icon: 'clarity:document-solid'
+      },
+      name: 'ExternalLink',
+      status: Mock.Random.integer(0, 1),
+      id: 4,
+      children: [
+        {
+          path: 'https://element-plus-admin-doc.cn/',
+          name: 'DocumentLink',
+          status: Mock.Random.integer(0, 1),
+          id: 5,
+          meta: {
+            title: '文档'
+          }
+        }
+      ]
+    },
+    {
+      path: '/level',
+      component: '#',
+      redirect: '/level/menu1/menu1-1/menu1-1-1',
+      name: 'Level',
+      status: Mock.Random.integer(0, 1),
+      id: 6,
+      meta: {
+        title: '菜单',
+        icon: 'carbon:skill-level-advanced'
+      },
+      children: [
+        {
+          path: 'menu1',
+          name: 'Menu1',
+          component: '##',
+          status: Mock.Random.integer(0, 1),
+          id: 7,
+          redirect: '/level/menu1/menu1-1/menu1-1-1',
+          meta: {
+            title: '菜单1'
+          },
+          children: [
+            {
+              path: 'menu1-1',
+              name: 'Menu11',
+              component: '##',
+              status: Mock.Random.integer(0, 1),
+              id: 8,
+              redirect: '/level/menu1/menu1-1/menu1-1-1',
+              meta: {
+                title: '菜单1-1',
+                alwaysShow: true
+              },
+              children: [
+                {
+                  path: 'menu1-1-1',
+                  name: 'Menu111',
+                  component: 'views/Level/Menu111',
+                  status: Mock.Random.integer(0, 1),
+                  id: 9,
+                  permission: ['edit', 'add', 'delete'],
+                  meta: {
+                    title: '菜单1-1-1',
+                    permission: ['edit', 'add', 'delete']
+                  }
+                }
+              ]
+            },
+            {
+              path: 'menu1-2',
+              name: 'Menu12',
+              component: 'views/Level/Menu12',
+              status: Mock.Random.integer(0, 1),
+              id: 10,
+              permission: ['edit', 'add', 'delete'],
+              meta: {
+                title: '菜单1-2',
+                permission: ['edit', 'add', 'delete']
+              }
+            }
+          ]
+        },
+        {
+          path: 'menu2',
+          name: 'Menu2Demo',
+          component: 'views/Level/Menu2',
+          status: Mock.Random.integer(0, 1),
+          id: 11,
+          permission: ['edit', 'add', 'delete'],
+          meta: {
+            title: '菜单2',
+            permission: ['edit', 'add', 'delete']
+          }
+        }
+      ]
+    },
+    {
+      path: '/example',
+      component: '#',
+      redirect: '/example/example-dialog',
+      name: 'Example',
+      status: Mock.Random.integer(0, 1),
+      id: 12,
+      meta: {
+        title: '综合示例',
+        icon: 'ep:management',
+        alwaysShow: true
+      },
+      children: [
+        {
+          path: 'example-dialog',
+          component: 'views/Example/Dialog/ExampleDialog',
+          name: 'ExampleDialog',
+          status: Mock.Random.integer(0, 1),
+          id: 13,
+          permission: ['edit', 'add', 'delete'],
+          meta: {
+            title: '综合示例-弹窗',
+            permission: ['edit', 'add', 'delete']
+          }
+        },
+        {
+          path: 'example-page',
+          component: 'views/Example/Page/ExamplePage',
+          name: 'ExamplePage',
+          status: Mock.Random.integer(0, 1),
+          id: 14,
+          permission: ['edit', 'add', 'delete'],
+          meta: {
+            title: '综合示例-页面',
+            permission: ['edit', 'add', 'delete']
+          }
+        },
+        {
+          path: 'example-add',
+          component: 'views/Example/Page/ExampleAdd',
+          name: 'ExampleAdd',
+          status: Mock.Random.integer(0, 1),
+          id: 15,
+          permission: ['edit', 'add', 'delete'],
+          meta: {
+            title: '综合示例-新增',
+            noTagsView: true,
+            noCache: true,
+            hidden: true,
+            showMainRoute: true,
+            activeMenu: '/example/example-page',
+            permission: ['edit', 'add', 'delete']
+          }
+        },
+        {
+          path: 'example-edit',
+          component: 'views/Example/Page/ExampleEdit',
+          name: 'ExampleEdit',
+          status: Mock.Random.integer(0, 1),
+          id: 16,
+          permission: ['edit', 'add', 'delete'],
+          meta: {
+            title: '综合示例-编辑',
+            noTagsView: true,
+            noCache: true,
+            hidden: true,
+            showMainRoute: true,
+            activeMenu: '/example/example-page',
+            permission: ['edit', 'add', 'delete']
+          }
+        },
+        {
+          path: 'example-detail',
+          component: 'views/Example/Page/ExampleDetail',
+          name: 'ExampleDetail',
+          status: Mock.Random.integer(0, 1),
+          id: 17,
+          permission: ['edit', 'add', 'delete'],
+          meta: {
+            title: '综合示例-详情',
+            noTagsView: true,
+            noCache: true,
+            hidden: true,
+            showMainRoute: true,
+            activeMenu: '/example/example-page',
+            permission: ['edit', 'add', 'delete']
+          }
+        }
+      ]
+    }
+  ],
+  [
+    {
+      path: '/dashboard',
+      component: '#',
+      redirect: '/dashboard/analysis',
+      name: 'Dashboard',
+      status: Mock.Random.integer(0, 1),
+      id: 1,
+      meta: {
+        title: '首页',
+        icon: 'ant-design:dashboard-filled',
+        alwaysShow: true
+      },
+      children: [
+        {
+          path: 'analysis',
+          component: 'views/Dashboard/Analysis',
+          name: 'Analysis',
+          status: Mock.Random.integer(0, 1),
+          id: 2,
+          meta: {
+            title: '分析页',
+            noCache: true
+          }
+        },
+        {
+          path: 'workplace',
+          component: 'views/Dashboard/Workplace',
+          name: 'Workplace',
+          status: Mock.Random.integer(0, 1),
+          id: 3,
+          meta: {
+            title: '工作台',
+            noCache: true
+          }
+        }
+      ]
+    }
+  ],
+  [
+    {
+      path: '/external-link',
+      component: '#',
+      meta: {
+        title: '文档',
+        icon: 'clarity:document-solid'
+      },
+      name: 'ExternalLink',
+      status: Mock.Random.integer(0, 1),
+      id: 4,
+      children: [
+        {
+          path: 'https://element-plus-admin-doc.cn/',
+          name: 'DocumentLink',
+          status: Mock.Random.integer(0, 1),
+          id: 5,
+          meta: {
+            title: '文档'
+          }
+        }
+      ]
+    },
+    {
+      path: '/level',
+      component: '#',
+      redirect: '/level/menu1/menu1-1/menu1-1-1',
+      name: 'Level',
+      status: Mock.Random.integer(0, 1),
+      id: 6,
+      meta: {
+        title: '菜单',
+        icon: 'carbon:skill-level-advanced'
+      },
+      children: [
+        {
+          path: 'menu1',
+          name: 'Menu1',
+          component: '##',
+          status: Mock.Random.integer(0, 1),
+          id: 7,
+          redirect: '/level/menu1/menu1-1/menu1-1-1',
+          meta: {
+            title: '菜单1'
+          },
+          children: [
+            {
+              path: 'menu1-1',
+              name: 'Menu11',
+              component: '##',
+              status: Mock.Random.integer(0, 1),
+              id: 8,
+              redirect: '/level/menu1/menu1-1/menu1-1-1',
+              meta: {
+                title: '菜单1-1',
+                alwaysShow: true
+              },
+              children: [
+                {
+                  path: 'menu1-1-1',
+                  name: 'Menu111',
+                  component: 'views/Level/Menu111',
+                  status: Mock.Random.integer(0, 1),
+                  id: 9,
+                  permission: ['edit', 'add', 'delete'],
+                  meta: {
+                    title: '菜单1-1-1',
+                    permission: ['edit', 'add', 'delete']
+                  }
+                }
+              ]
+            },
+            {
+              path: 'menu1-2',
+              name: 'Menu12',
+              component: 'views/Level/Menu12',
+              status: Mock.Random.integer(0, 1),
+              id: 10,
+              permission: ['edit', 'add', 'delete'],
+              meta: {
+                title: '菜单1-2',
+                permission: ['edit', 'add', 'delete']
+              }
+            }
+          ]
+        },
+        {
+          path: 'menu2',
+          name: 'Menu2Demo',
+          component: 'views/Level/Menu2',
+          status: Mock.Random.integer(0, 1),
+          id: 11,
+          permission: ['edit', 'add', 'delete'],
+          meta: {
+            title: '菜单2',
+            permission: ['edit', 'add', 'delete']
+          }
+        }
+      ]
+    }
+  ],
+  [
+    {
+      path: '/example',
+      component: '#',
+      redirect: '/example/example-dialog',
+      name: 'Example',
+      status: Mock.Random.integer(0, 1),
+      id: 12,
+      meta: {
+        title: '综合示例',
+        icon: 'ep:management',
+        alwaysShow: true
+      },
+      children: [
+        {
+          path: 'example-detail',
+          component: 'views/Example/Page/ExampleDetail',
+          name: 'ExampleDetail',
+          status: Mock.Random.integer(0, 1),
+          id: 17,
+          permission: ['edit', 'add', 'delete'],
+          meta: {
+            title: '综合示例-详情',
+            noTagsView: true,
+            noCache: true,
+            hidden: true,
+            showMainRoute: true,
+            activeMenu: '/example/example-page',
+            permission: ['edit', 'add', 'delete']
+          }
+        }
+      ]
+    }
+  ]
+]
+
+for (let i = 0; i < 4; i++) {
+  List.push(
+    Mock.mock({
+      id: toAnyString(),
+      // timestamp: +Mock.Random.date('T'),
+      roleName: roleNames[i],
+      role: '@first',
+      status: Mock.Random.integer(0, 1),
+      createTime: '@datetime',
+      remark: '@cword(10, 15)',
+      menu: menus[i]
+    })
+  )
+}
+
 export default [
   // 列表接口
   {
@@ -380,8 +648,26 @@ export default [
     response: ({ query }) => {
       const { roleName } = query
       return {
-        code: result_code,
-        data: roleName === 'admin' ? adminList : testList
+        data: {
+          code: code,
+          data: roleName === 'admin' ? adminList : testList
+        }
+      }
+    }
+  },
+  {
+    url: '/role/table',
+    method: 'get',
+    timeout,
+    response: () => {
+      return {
+        data: {
+          code: code,
+          data: {
+            list: List,
+            total: 4
+          }
+        }
       }
     }
   }

+ 148 - 17
mock/table/index.ts

@@ -1,18 +1,18 @@
-import { config } from '@/config/axios/config'
+import config from '@/config/axios/config'
 import { MockMethod } from 'vite-plugin-mock'
 import { toAnyString } from '@/utils'
 import Mock from 'mockjs'
 
-const { result_code } = config
+const { code } = config
 
-const timeout = 0
+const timeout = 1
 
 const count = 100
 
 const baseContent =
   '<p>I am testing data, I am testing data.</p><p><img src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943"></p>'
 
-let List: {
+interface ListProps {
   id: string
   author: string
   title: string
@@ -20,7 +20,21 @@ let List: {
   importance: number
   display_time: string
   pageviews: number
-}[] = []
+  image_uri: string
+}
+
+interface TreeListProps {
+  id: string
+  author: string
+  title: string
+  content: string
+  importance: number
+  display_time: string
+  pageviews: number
+  children: TreeListProps[]
+}
+
+let List: ListProps[] = []
 
 for (let i = 0; i < count; i++) {
   List.push(
@@ -32,13 +46,120 @@ for (let i = 0; i < count; i++) {
       content: baseContent,
       importance: '@integer(1, 3)',
       display_time: '@datetime',
-      pageviews: '@integer(300, 5000)'
+      pageviews: '@integer(300, 5000)',
+      image_uri: Mock.Random.image('@integer(300, 5000)x@integer(300, 5000)')
+    })
+  )
+}
+
+const treeList: TreeListProps[] = []
+
+for (let i = 0; i < count; i++) {
+  treeList.push(
+    Mock.mock({
+      id: toAnyString(),
+      // timestamp: +Mock.Random.date('T'),
+      author: '@first',
+      title: '@title(5, 10)',
+      content: baseContent,
+      importance: '@integer(1, 3)',
+      display_time: '@datetime',
+      pageviews: '@integer(300, 5000)',
+      children: [
+        {
+          id: toAnyString(),
+          // timestamp: +Mock.Random.date('T'),
+          author: '@first',
+          title: '@title(5, 10)',
+          content: baseContent,
+          importance: '@integer(1, 3)',
+          display_time: '@datetime',
+          pageviews: '@integer(300, 5000)',
+          children: [
+            {
+              id: toAnyString(),
+              // timestamp: +Mock.Random.date('T'),
+              author: '@first',
+              title: '@title(5, 10)',
+              content: baseContent,
+              importance: '@integer(1, 3)',
+              display_time: '@datetime',
+              pageviews: '@integer(300, 5000)'
+            },
+            {
+              id: toAnyString(),
+              // timestamp: +Mock.Random.date('T'),
+              author: '@first',
+              title: '@title(5, 10)',
+              content: baseContent,
+              importance: '@integer(1, 3)',
+              display_time: '@datetime',
+              pageviews: '@integer(300, 5000)'
+            }
+          ]
+        },
+        {
+          id: toAnyString(),
+          // timestamp: +Mock.Random.date('T'),
+          author: '@first',
+          title: '@title(5, 10)',
+          content: baseContent,
+          importance: '@integer(1, 3)',
+          display_time: '@datetime',
+          pageviews: '@integer(300, 5000)'
+        },
+        {
+          id: toAnyString(),
+          // timestamp: +Mock.Random.date('T'),
+          author: '@first',
+          title: '@title(5, 10)',
+          content: baseContent,
+          importance: '@integer(1, 3)',
+          display_time: '@datetime',
+          pageviews: '@integer(300, 5000)'
+        },
+        {
+          id: toAnyString(),
+          // timestamp: +Mock.Random.date('T'),
+          author: '@first',
+          title: '@title(5, 10)',
+          content: baseContent,
+          importance: '@integer(1, 3)',
+          display_time: '@datetime',
+          pageviews: '@integer(300, 5000)'
+        }
+      ]
       // image_uri
     })
   )
 }
 
 export default [
+  // 树形列表接口
+  {
+    url: '/example/treeList',
+    method: 'get',
+    timeout,
+    response: ({ query }) => {
+      const { title, pageIndex, pageSize } = query
+      const mockList = treeList.filter((item) => {
+        if (title && item.title.indexOf(title) < 0) return false
+        return true
+      })
+      const pageList = mockList.filter(
+        (_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1)
+      )
+      return {
+        data: {
+          code: code,
+          data: {
+            total: mockList.length,
+            list: pageList
+          }
+        }
+      }
+    }
+  },
   // 列表接口
   {
     url: '/example/list',
@@ -54,10 +175,12 @@ export default [
         (_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1)
       )
       return {
-        code: result_code,
         data: {
-          total: mockList.length,
-          list: pageList
+          code: code,
+          data: {
+            total: mockList.length,
+            list: pageList
+          }
         }
       }
     }
@@ -75,8 +198,10 @@ export default [
           })
         ].concat(List)
         return {
-          code: result_code,
-          data: 'success'
+          data: {
+            code: code,
+            data: 'success'
+          }
         }
       } else {
         List.map((item) => {
@@ -87,8 +212,10 @@ export default [
           }
         })
         return {
-          code: result_code,
-          data: 'success'
+          data: {
+            code: code,
+            data: 'success'
+          }
         }
       }
     }
@@ -102,8 +229,10 @@ export default [
       for (const example of List) {
         if (example.id === id) {
           return {
-            code: result_code,
-            data: example
+            data: {
+              code: code,
+              data: example
+            }
           }
         }
       }
@@ -128,8 +257,10 @@ export default [
           }
         }
         return {
-          code: result_code,
-          data: 'success'
+          data: {
+            code: code,
+            data: 'success'
+          }
         }
       }
     }

+ 17 - 11
mock/user/index.ts

@@ -1,9 +1,9 @@
-import { config } from '@/config/axios/config'
+import config from '@/config/axios/config'
 import { MockMethod } from 'vite-plugin-mock'
 
-const { result_code } = config
+const { code } = config
 
-const timeout = 0
+const timeout = 1
 
 const List: {
   username: string
@@ -45,10 +45,12 @@ export default [
       )
 
       return {
-        code: result_code,
         data: {
-          total: mockList.length,
-          list: pageList
+          code: code,
+          data: {
+            total: mockList.length,
+            list: pageList
+          }
         }
       }
     }
@@ -65,14 +67,16 @@ export default [
         if (user.username === data.username && user.password === data.password) {
           hasUser = true
           return {
-            code: result_code,
-            data: user
+            data: {
+              code: code,
+              data: user
+            }
           }
         }
       }
       if (!hasUser) {
         return {
-          code: '500',
+          code: 500,
           message: '账号或密码错误'
         }
       }
@@ -85,8 +89,10 @@ export default [
     timeout,
     response: () => {
       return {
-        code: result_code,
-        data: null
+        data: {
+          code: code,
+          data: null
+        }
       }
     }
   }

+ 124 - 114
mock/workplace/index.ts

@@ -1,9 +1,9 @@
-import { config } from '@/config/axios/config'
+import config from '@/config/axios/config'
 import { MockMethod } from 'vite-plugin-mock'
 
-const { result_code } = config
+const { code } = config
 
-const timeout = 0
+const timeout = 1
 
 export default [
   // 获取统计
@@ -13,11 +13,13 @@ export default [
     timeout,
     response: () => {
       return {
-        code: result_code,
         data: {
-          project: 40,
-          access: 2340,
-          todo: 10
+          code: code,
+          data: {
+            project: 40,
+            access: 2340,
+            todo: 10
+          }
         }
       }
     }
@@ -29,51 +31,53 @@ export default [
     timeout,
     response: () => {
       return {
-        code: result_code,
-        data: [
-          {
-            name: 'Github',
-            icon: 'akar-icons:github-fill',
-            message: 'workplace.introduction',
-            personal: 'Archer',
-            time: new Date()
-          },
-          {
-            name: 'Vue',
-            icon: 'logos:vue',
-            message: 'workplace.introduction',
-            personal: 'Archer',
-            time: new Date()
-          },
-          {
-            name: 'Angular',
-            icon: 'logos:angular-icon',
-            message: 'workplace.introduction',
-            personal: 'Archer',
-            time: new Date()
-          },
-          {
-            name: 'React',
-            icon: 'logos:react',
-            message: 'workplace.introduction',
-            personal: 'Archer',
-            time: new Date()
-          },
-          {
-            name: 'Webpack',
-            icon: 'logos:webpack',
-            message: 'workplace.introduction',
-            personal: 'Archer',
-            time: new Date()
-          },
-          {
-            name: 'Vite',
-            icon: 'vscode-icons:file-type-vite',
-            message: 'workplace.introduction',
-            personal: 'Archer',
-            time: new Date()
-          }
-        ]
+        data: {
+          code: code,
+          data: [
+            {
+              name: 'Github',
+              icon: 'akar-icons:github-fill',
+              message: 'workplace.introduction',
+              personal: 'Archer',
+              time: new Date()
+            },
+            {
+              name: 'Vue',
+              icon: 'logos:vue',
+              message: 'workplace.introduction',
+              personal: 'Archer',
+              time: new Date()
+            },
+            {
+              name: 'Angular',
+              icon: 'logos:angular-icon',
+              message: 'workplace.introduction',
+              personal: 'Archer',
+              time: new Date()
+            },
+            {
+              name: 'React',
+              icon: 'logos:react',
+              message: 'workplace.introduction',
+              personal: 'Archer',
+              time: new Date()
+            },
+            {
+              name: 'Webpack',
+              icon: 'logos:webpack',
+              message: 'workplace.introduction',
+              personal: 'Archer',
+              time: new Date()
+            },
+            {
+              name: 'Vite',
+              icon: 'vscode-icons:file-type-vite',
+              message: 'workplace.introduction',
+              personal: 'Archer',
+              time: new Date()
+            }
+          ]
+        }
       }
     }
   },
@@ -84,33 +88,35 @@ export default [
     timeout,
     response: () => {
       return {
-        code: result_code,
-        data: [
-          {
-            keys: ['workplace.push', 'Github'],
-            time: new Date()
-          },
-          {
-            keys: ['workplace.push', 'Github'],
-            time: new Date()
-          },
-          {
-            keys: ['workplace.push', 'Github'],
-            time: new Date()
-          },
-          {
-            keys: ['workplace.push', 'Github'],
-            time: new Date()
-          },
-          {
-            keys: ['workplace.push', 'Github'],
-            time: new Date()
-          },
-          {
-            keys: ['workplace.push', 'Github'],
-            time: new Date()
-          }
-        ]
+        data: {
+          code: code,
+          data: [
+            {
+              keys: ['workplace.push', 'Github'],
+              time: new Date()
+            },
+            {
+              keys: ['workplace.push', 'Github'],
+              time: new Date()
+            },
+            {
+              keys: ['workplace.push', 'Github'],
+              time: new Date()
+            },
+            {
+              keys: ['workplace.push', 'Github'],
+              time: new Date()
+            },
+            {
+              keys: ['workplace.push', 'Github'],
+              time: new Date()
+            },
+            {
+              keys: ['workplace.push', 'Github'],
+              time: new Date()
+            }
+          ]
+        }
       }
     }
   },
@@ -121,33 +127,35 @@ export default [
     timeout,
     response: () => {
       return {
-        code: result_code,
-        data: [
-          {
-            name: 'Github',
-            icon: 'akar-icons:github-fill'
-          },
-          {
-            name: 'Vue',
-            icon: 'logos:vue'
-          },
-          {
-            name: 'Angular',
-            icon: 'logos:angular-icon'
-          },
-          {
-            name: 'React',
-            icon: 'logos:react'
-          },
-          {
-            name: 'Webpack',
-            icon: 'logos:webpack'
-          },
-          {
-            name: 'Vite',
-            icon: 'vscode-icons:file-type-vite'
-          }
-        ]
+        data: {
+          code: code,
+          data: [
+            {
+              name: 'Github',
+              icon: 'akar-icons:github-fill'
+            },
+            {
+              name: 'Vue',
+              icon: 'logos:vue'
+            },
+            {
+              name: 'Angular',
+              icon: 'logos:angular-icon'
+            },
+            {
+              name: 'React',
+              icon: 'logos:react'
+            },
+            {
+              name: 'Webpack',
+              icon: 'logos:webpack'
+            },
+            {
+              name: 'Vite',
+              icon: 'vscode-icons:file-type-vite'
+            }
+          ]
+        }
       }
     }
   },
@@ -158,14 +166,16 @@ export default [
     timeout,
     response: () => {
       return {
-        code: result_code,
-        data: [
-          { name: 'workplace.quote', max: 65, personal: 42, team: 50 },
-          { name: 'workplace.contribution', max: 160, personal: 30, team: 140 },
-          { name: 'workplace.hot', max: 300, personal: 20, team: 28 },
-          { name: 'workplace.yield', max: 130, personal: 35, team: 35 },
-          { name: 'workplace.follow', max: 100, personal: 80, team: 90 }
-        ]
+        data: {
+          code: code,
+          data: [
+            { name: 'workplace.quote', max: 65, personal: 42, team: 50 },
+            { name: 'workplace.contribution', max: 160, personal: 30, team: 140 },
+            { name: 'workplace.hot', max: 300, personal: 20, team: 28 },
+            { name: 'workplace.yield', max: 130, personal: 35, team: 35 },
+            { name: 'workplace.follow', max: 100, personal: 80, team: 90 }
+          ]
+        }
       }
     }
   }

+ 56 - 54
package.json

@@ -1,16 +1,16 @@
 {
   "name": "vue-element-plus-admin",
-  "version": "1.9.9",
+  "version": "2.0.0",
   "description": "一套基于vue3、element-plus、typesScript、vite4的后台集成方案。",
   "author": "Archer <502431556@qq.com>",
   "private": false,
   "scripts": {
     "i": "pnpm install",
     "dev": "vite --mode base",
-    "ts:check": "vue-tsc --noEmit",
+    "ts:check": "vue-tsc --noEmit --skipLibCheck",
     "build:pro": "vite build --mode pro",
     "build:gitee": "vite build --mode gitee",
-    "build:dev": "npm run ts:check && vite build --mode dev",
+    "build:dev": "vite build --mode dev",
     "build:test": "npm run ts:check && vite build --mode test",
     "serve:pro": "vite preview --mode pro",
     "serve:dev": "vite preview --mode dev",
@@ -23,90 +23,92 @@
     "lint:style": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
     "lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
     "prepare": "husky install",
-    "p": "plop",
-    "analysis": "windicss-analysis"
+    "p": "plop"
   },
   "dependencies": {
-    "@iconify/iconify": "^3.1.0",
-    "@vueuse/core": "^9.13.0",
+    "@iconify/iconify": "^3.1.1",
+    "@iconify/vue": "^4.1.1",
+    "@vueuse/core": "^10.2.1",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.10",
-    "@zxcvbn-ts/core": "^2.2.1",
+    "@zxcvbn-ts/core": "^3.0.3",
     "animate.css": "^4.1.1",
-    "axios": "^1.3.5",
-    "echarts": "^5.4.2",
+    "axios": "^1.4.0",
+    "dayjs": "^1.11.9",
+    "echarts": "^5.4.3",
     "echarts-wordcloud": "^2.1.0",
-    "element-plus": "2.3.3",
+    "element-plus": "^2.3.8",
     "intro.js": "^7.0.1",
     "lodash-es": "^4.17.21",
-    "mitt": "^3.0.0",
+    "mitt": "^3.0.1",
     "mockjs": "^1.1.0",
     "nprogress": "^0.2.0",
-    "pinia": "^2.0.34",
-    "qrcode": "^1.5.1",
-    "qs": "^6.11.1",
-    "url": "^0.11.0",
-    "vue": "3.2.47",
+    "pinia": "^2.1.4",
+    "pinia-plugin-persist": "^1.0.0",
+    "qrcode": "^1.5.3",
+    "qs": "^6.11.2",
+    "sortablejs": "^1.15.0",
+    "url": "^0.11.1",
+    "vue": "3.3.4",
     "vue-i18n": "9.2.2",
-    "vue-router": "^4.1.6",
-    "vue-types": "^5.0.2",
-    "web-storage-cache": "^1.1.1"
+    "vue-router": "^4.2.4",
+    "vue-types": "^5.1.0"
   },
   "devDependencies": {
-    "@commitlint/cli": "^17.5.1",
-    "@commitlint/config-conventional": "^17.4.4",
-    "@iconify/json": "^2.2.48",
-    "@intlify/unplugin-vue-i18n": "^0.10.0",
+    "@commitlint/cli": "^17.6.7",
+    "@commitlint/config-conventional": "^17.6.7",
+    "@iconify/json": "^2.2.92",
+    "@intlify/unplugin-vue-i18n": "^0.12.2",
     "@purge-icons/generated": "^0.9.0",
     "@types/intro.js": "^5.1.1",
-    "@types/lodash-es": "^4.17.7",
-    "@types/node": "^18.15.11",
+    "@types/lodash-es": "^4.17.8",
+    "@types/node": "^20.4.2",
     "@types/nprogress": "^0.2.0",
-    "@types/qrcode": "^1.5.0",
+    "@types/qrcode": "^1.5.1",
     "@types/qs": "^6.9.7",
-    "@typescript-eslint/eslint-plugin": "^5.58.0",
-    "@typescript-eslint/parser": "^5.58.0",
-    "@vitejs/plugin-legacy": "^4.0.2",
-    "@vitejs/plugin-vue": "^4.1.0",
+    "@types/sortablejs": "^1.15.1",
+    "@typescript-eslint/eslint-plugin": "^6.1.0",
+    "@typescript-eslint/parser": "^6.1.0",
+    "@unocss/transformer-variant-group": "^0.53.5",
+    "@vitejs/plugin-legacy": "^4.1.0",
+    "@vitejs/plugin-vue": "^4.2.3",
     "@vitejs/plugin-vue-jsx": "^3.0.1",
+    "@vue-macros/volar": "^0.12.2",
     "autoprefixer": "^10.4.14",
-    "consola": "^3.0.1",
-    "eslint": "^8.38.0",
+    "consola": "^3.2.3",
+    "eslint": "^8.45.0",
     "eslint-config-prettier": "^8.8.0",
-    "eslint-define-config": "^1.17.0",
-    "eslint-plugin-prettier": "^4.2.1",
-    "eslint-plugin-vue": "^9.10.0",
+    "eslint-define-config": "^1.21.0",
+    "eslint-plugin-prettier": "^5.0.0",
+    "eslint-plugin-vue": "^9.15.1",
     "husky": "^8.0.3",
     "less": "^4.1.3",
-    "lint-staged": "^13.2.1",
+    "lint-staged": "^13.2.3",
     "plop": "^3.1.2",
-    "postcss": "^8.4.21",
+    "postcss": "^8.4.26",
     "postcss-html": "^1.5.0",
     "postcss-less": "^6.0.0",
-    "prettier": "^2.8.7",
-    "rimraf": "^5.0.0",
-    "rollup": "^3.20.2",
-    "stylelint": "^15.4.0",
+    "prettier": "^3.0.0",
+    "rimraf": "^5.0.1",
+    "rollup": "^3.26.3",
+    "stylelint": "^15.10.1",
     "stylelint-config-html": "^1.1.0",
-    "stylelint-config-prettier": "^9.0.5",
-    "stylelint-config-recommended": "^11.0.0",
-    "stylelint-config-standard": "^32.0.0",
+    "stylelint-config-recommended": "^13.0.0",
+    "stylelint-config-standard": "^34.0.0",
     "stylelint-order": "^6.0.3",
-    "terser": "^5.16.9",
-    "typescript": "5.0.4",
-    "unplugin-vue-define-options": "^1.3.3",
-    "vite": "4.2.1",
+    "terser": "^5.19.1",
+    "typescript": "5.1.6",
+    "unocss": "^0.53.5",
+    "unplugin-vue-define-options": "^1.3.11",
+    "vite": "4.4.4",
     "vite-plugin-ejs": "^1.6.4",
     "vite-plugin-eslint": "^1.8.1",
-    "vite-plugin-mock": "^2.9.6",
+    "vite-plugin-mock": "2.9.6",
     "vite-plugin-progress": "^0.0.7",
     "vite-plugin-purge-icons": "^0.9.2",
     "vite-plugin-style-import": "2.0.0",
     "vite-plugin-svg-icons": "^2.0.1",
-    "vite-plugin-windicss": "^1.8.10",
-    "vue-tsc": "^1.2.0",
-    "windicss": "^3.5.6",
-    "windicss-analysis": "^0.3.5"
+    "vue-tsc": "^1.8.5"
   },
   "engines": {
     "node": ">= 14.18.0"

+ 8 - 8
src/App.vue

@@ -5,7 +5,7 @@ import { usePageStore } from '@/store/modules/page'
 import { ConfigGlobal } from '@/components/ConfigGlobal'
 import { isDark } from '@/utils/is'
 import { useDesign } from '@/hooks/web/useDesign'
-import { useCache } from '@/hooks/web/useCache'
+import { useStorage } from '@/hooks/web/useStorage'
 
 const { getPrefixCls } = useDesign()
 
@@ -20,19 +20,19 @@ const greyMode = computed(() => appStore.getGreyMode)
 
 // 根据浏览器当前主题设置系统主题色
 const setDefaultTheme = () => {
-  let { wsCache } = useCache()
-  if (wsCache.get('isDark') !== null) {
-    appStore.setIsDark(wsCache.get('isDark'))
+  const { getStorage } = useStorage()
+  if (getStorage('isDark') !== null) {
+    appStore.setIsDark(getStorage('isDark'))
     return
   }
   const isDarkTheme = isDark()
   appStore.setIsDark(isDarkTheme)
 }
-// 获取浏览器存储的页面数据
+
 const setPageData = () => {
-  let { wsCache } = useCache('localStorage')
-  if (wsCache.get('pageData') !== null) {
-    pageStore.setAllPageData(wsCache.get('pageData'))
+  const { getStorage } = useStorage('localStorage')
+  if (getStorage('pageData') !== null) {
+    pageStore.setAllPageData(getStorage('pageData'))
     return
   }
 }

+ 2 - 2
src/api/common/index.ts

@@ -1,11 +1,11 @@
 import request from '@/config/axios'
 
 // 获取所有字典
-export const getDictApi = (): Promise<IResponse> => {
+export const getDictApi = () => {
   return request.get({ url: '/dict/list' })
 }
 
 // 模拟获取某个字典
-export const getDictOneApi = async (): Promise<IResponse> => {
+export const getDictOneApi = async () => {
   return request.get({ url: '/dict/one' })
 }

+ 30 - 0
src/api/department/index.ts

@@ -0,0 +1,30 @@
+import request from '@/config/axios'
+import { DepartmentListResponse, DepartmentUserParams, DepartmentUserResponse } from './types'
+
+export const getDepartmentApi = () => {
+  return request.get<DepartmentListResponse>({ url: '/department/list' })
+}
+
+export const getUserByIdApi = (params: DepartmentUserParams) => {
+  return request.get<DepartmentUserResponse>({ url: '/department/users', params })
+}
+
+export const deleteUserByIdApi = (ids: string[] | number[]) => {
+  return request.post({ url: '/department/user/delete', data: { ids } })
+}
+
+export const saveUserApi = (data: any) => {
+  return request.post({ url: '/department/user/save', data })
+}
+
+export const saveDepartmentApi = (data: any) => {
+  return request.post({ url: '/department/save', data })
+}
+
+export const deleteDepartmentApi = (ids: string[] | number[]) => {
+  return request.post({ url: '/department/delete', data: { ids } })
+}
+
+export const getDepartmentTableApi = (params: any) => {
+  return request.get({ url: '/department/table/list', params })
+}

+ 32 - 0
src/api/department/types.ts

@@ -0,0 +1,32 @@
+export interface DepartmentItem {
+  id: string
+  departmentName: string
+  children?: DepartmentItem[]
+}
+
+export interface DepartmentListResponse {
+  list: DepartmentItem[]
+}
+
+export interface DepartmentUserParams {
+  pageSize: number
+  pageIndex: number
+  id: string
+  username?: string
+  account?: string
+}
+
+export interface DepartmentUserItem {
+  id: string
+  username: string
+  account: string
+  email: string
+  createTime: string
+  role: string
+  department: DepartmentItem
+}
+
+export interface DepartmentUserResponse {
+  list: DepartmentUserItem[]
+  total: number
+}

+ 5 - 0
src/api/menu/index.ts

@@ -0,0 +1,5 @@
+import request from '@/config/axios'
+
+export const getMenuListApi = () => {
+  return request.get({ url: '/menu/list' })
+}

+ 5 - 0
src/api/role/index.ts

@@ -0,0 +1,5 @@
+import request from '@/config/axios'
+
+export const getRoleListApi = () => {
+  return request.get({ url: '/role/table' })
+}

+ 5 - 1
src/api/table/index.ts

@@ -1,10 +1,14 @@
 import request from '@/config/axios'
 import type { TableData } from './types'
 
-export const getTableListApi = (params: any): Promise<IResponse> => {
+export const getTableListApi = (params: any) => {
   return request.get({ url: '/example/list', params })
 }
 
+export const getTreeTableListApi = (params: any) => {
+  return request.get({ url: '/example/treeList', params })
+}
+
 export const saveTableApi = (data: Partial<TableData>): Promise<IResponse> => {
   return request.post({ url: '/example/save', data })
 }

File diff suppressed because it is too large
+ 0 - 0
src/assets/svgs/login-box-bg.svg


+ 2 - 0
src/components/ConfigGlobal/index.ts

@@ -1,3 +1,5 @@
 import ConfigGlobal from './src/ConfigGlobal.vue'
 
+export type { ConfigGlobalTypes } from './src/types'
+
 export { ConfigGlobal }

+ 2 - 3
src/components/ConfigGlobal/src/ConfigGlobal.vue

@@ -1,20 +1,19 @@
 <script setup lang="ts">
 import { provide, computed, watch, onMounted } from 'vue'
 import { propTypes } from '@/utils/propTypes'
-import { ElConfigProvider } from 'element-plus'
+import { ComponentSize, ElConfigProvider } from 'element-plus'
 import { useLocaleStore } from '@/store/modules/locale'
 import { useWindowSize } from '@vueuse/core'
 import { useAppStore } from '@/store/modules/app'
 import { setCssVar } from '@/utils'
 import { useDesign } from '@/hooks/web/useDesign'
-import { ElementPlusSize } from '@/types/elementPlus'
 
 const { variables } = useDesign()
 
 const appStore = useAppStore()
 
 const props = defineProps({
-  size: propTypes.oneOf<ElementPlusSize>(['default', 'small', 'large']).def('default')
+  size: propTypes.oneOf<ComponentSize>(['default', 'small', 'large']).def('default')
 })
 
 provide('configGlobal', props)

+ 5 - 0
src/components/ConfigGlobal/src/types/index.ts

@@ -0,0 +1,5 @@
+import { ComponentSize } from 'element-plus'
+
+export interface ConfigGlobalTypes {
+  size?: ComponentSize
+}

+ 7 - 41
src/components/ContentDetailWrap/src/ContentDetailWrap.vue

@@ -1,11 +1,7 @@
 <script setup lang="ts">
-import { ElCard, ElButton } from 'element-plus'
+import { ElCard } from 'element-plus'
 import { propTypes } from '@/utils/propTypes'
 import { useDesign } from '@/hooks/web/useDesign'
-import { ref, onMounted, defineEmits } from 'vue'
-import { Sticky } from '@/components/Sticky'
-import { useI18n } from '@/hooks/web/useI18n'
-const { t } = useI18n()
 
 const { getPrefixCls } = useDesign()
 
@@ -15,45 +11,15 @@ defineProps({
   title: propTypes.string.def(''),
   message: propTypes.string.def('')
 })
-const emit = defineEmits(['back'])
-const offset = ref(85)
-const contentDetailWrap = ref()
-onMounted(() => {
-  offset.value = contentDetailWrap.value.getBoundingClientRect().top
-})
 </script>
 
 <template>
-  <div :class="[`${prefixCls}-container`, 'relative bg-[#fff]']" ref="contentDetailWrap">
-    <Sticky :offset="offset">
-      <div
-        :class="[
-          `${prefixCls}-header`,
-          'flex border-bottom-1 h-50px items-center text-center bg-white pr-10px'
-        ]"
-      >
-        <div :class="[`${prefixCls}-header__back`, 'flex pl-10px pr-10px ']">
-          <el-button @click="emit('back')">
-            <Icon icon="ep:arrow-left" class="mr-5px" />
-            {{ t('common.back') }}
-          </el-button>
-        </div>
-        <div :class="[`${prefixCls}-header__title`, 'flex flex-1  justify-center']">
-          <slot name="title">
-            <label class="text-16px font-700">{{ title }}</label>
-          </slot>
-        </div>
-        <div :class="[`${prefixCls}-header__right`, 'flex  pl-10px pr-10px']">
-          <slot name="right"></slot>
-        </div>
+  <div :class="[`${prefixCls}-container`, 'relative']">
+    <ElCard :class="[`${prefixCls}-body`, 'mb-20px']" shadow="never">
+      <div class="mb-20px pb-20px" style="border-bottom: 1px solid var(--el-border-color)">
+        <slot name="header"></slot>
       </div>
-    </Sticky>
-    <div style="padding: var(--app-content-padding)">
-      <ElCard :class="[`${prefixCls}-body`, 'mb-20px']" shadow="never">
-        <div>
-          <slot></slot>
-        </div>
-      </ElCard>
-    </div>
+      <slot></slot>
+    </ElCard>
   </div>
 </template>

+ 2 - 0
src/components/ContextMenu/index.ts

@@ -2,6 +2,8 @@ import ContextMenu from './src/ContextMenu.vue'
 import { ElDropdown } from 'element-plus'
 import type { RouteLocationNormalizedLoaded } from 'vue-router'
 
+export type { ContextMenuSchema } from './src/types'
+
 export interface ContextMenuExpose {
   elDropdownMenuRef: ComponentRef<typeof ElDropdown>
   tagItem: RouteLocationNormalizedLoaded

+ 3 - 3
src/components/ContextMenu/src/ContextMenu.vue

@@ -4,7 +4,7 @@ import { PropType, ref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useDesign } from '@/hooks/web/useDesign'
 import type { RouteLocationNormalizedLoaded } from 'vue-router'
-import { contextMenuSchema } from '../../../types/contextMenu'
+import { ContextMenuSchema } from './types'
 const { getPrefixCls } = useDesign()
 
 const prefixCls = getPrefixCls('context-menu')
@@ -15,7 +15,7 @@ const emit = defineEmits(['visibleChange'])
 
 const props = defineProps({
   schema: {
-    type: Array as PropType<contextMenuSchema[]>,
+    type: Array as PropType<ContextMenuSchema[]>,
     default: () => []
   },
   trigger: {
@@ -28,7 +28,7 @@ const props = defineProps({
   }
 })
 
-const command = (item: contextMenuSchema) => {
+const command = (item: ContextMenuSchema) => {
   item.command && item.command(item)
 }
 

+ 7 - 0
src/components/ContextMenu/src/types/index.ts

@@ -0,0 +1,7 @@
+export interface ContextMenuSchema {
+  disabled?: boolean
+  divided?: boolean
+  icon?: string
+  label: string
+  command?: (item: ContextMenuSchema) => void
+}

+ 2 - 0
src/components/Descriptions/index.ts

@@ -1,3 +1,5 @@
 import Descriptions from './src/Descriptions.vue'
 
+export type { DescriptionsSchema } from './src/types'
+
 export { Descriptions }

+ 118 - 108
src/components/Descriptions/src/Descriptions.vue

@@ -1,134 +1,144 @@
-<script setup lang="ts">
+<script lang="tsx">
 import { ElCollapseTransition, ElDescriptions, ElDescriptionsItem, ElTooltip } from 'element-plus'
 import { useDesign } from '@/hooks/web/useDesign'
 import { propTypes } from '@/utils/propTypes'
-import { ref, unref, PropType, computed, useAttrs, useSlots } from 'vue'
+import { ref, unref, PropType, computed, defineComponent } from 'vue'
 import { useAppStore } from '@/store/modules/app'
-import { DescriptionsSchema } from '@/types/descriptions'
+import { DescriptionsSchema } from './types'
+import { Icon } from '@/components/Icon'
+import { get } from 'lodash-es'
 
 const appStore = useAppStore()
 
 const mobile = computed(() => appStore.getMobile)
 
-const attrs = useAttrs()
-
-const slots = useSlots()
-
-const props = defineProps({
-  title: propTypes.string.def(''),
-  message: propTypes.string.def(''),
-  collapse: propTypes.bool.def(true),
-  schema: {
-    type: Array as PropType<DescriptionsSchema[]>,
-    default: () => []
-  },
-  data: {
-    type: Object as PropType<any>,
-    default: () => ({})
-  }
-})
-
 const { getPrefixCls } = useDesign()
 
 const prefixCls = getPrefixCls('descriptions')
 
-const getBindValue = computed(() => {
-  const delArr: string[] = ['title', 'message', 'collapse', 'schema', 'data', 'class']
-  const obj = { ...attrs, ...props }
-  for (const key in obj) {
-    if (delArr.indexOf(key) !== -1) {
-      delete obj[key]
+export default defineComponent({
+  name: 'Descriptions',
+  props: {
+    title: propTypes.string.def(''),
+    message: propTypes.string.def(''),
+    collapse: propTypes.bool.def(true),
+    border: propTypes.bool.def(true),
+    column: propTypes.number.def(2),
+    size: propTypes.oneOf(['large', 'default', 'small']).def('default'),
+    direction: propTypes.oneOf(['horizontal', 'vertical']).def('horizontal'),
+    extra: propTypes.string.def(''),
+    schema: {
+      type: Array as PropType<DescriptionsSchema[]>,
+      default: () => []
+    },
+    data: {
+      type: Object as PropType<any>,
+      default: () => ({})
     }
-  }
-  return obj
-})
-
-const getBindItemValue = (item: DescriptionsSchema) => {
-  const delArr: string[] = ['field']
-  const obj = { ...item }
-  for (const key in obj) {
-    if (delArr.indexOf(key) !== -1) {
-      delete obj[key]
+  },
+  setup(props, { slots, attrs }) {
+    const getBindValue = computed((): any => {
+      const delArr: string[] = ['title', 'message', 'collapse', 'schema', 'data', 'class']
+      const obj = { ...attrs, ...props }
+      for (const key in obj) {
+        if (delArr.indexOf(key) !== -1) {
+          delete obj[key]
+        }
+      }
+      if (unref(mobile)) {
+        obj.direction = 'vertical'
+      }
+      return obj
+    })
+
+    const getBindItemValue = (item: DescriptionsSchema) => {
+      const delArr: string[] = ['field']
+      const obj = { ...item }
+      for (const key in obj) {
+        if (delArr.indexOf(key) !== -1) {
+          delete obj[key]
+        }
+      }
+      return obj
     }
-  }
-  return obj
-}
-
-// 折叠
-const show = ref(true)
 
-const toggleClick = () => {
-  if (props.collapse) {
-    show.value = !unref(show)
-  }
-}
-</script>
+    // 折叠
+    const show = ref(true)
 
-<template>
-  <div
-    :class="[
-      prefixCls,
-      'bg-[var(--el-color-white)] dark:(bg-[var(--el-bg-color)] border-[var(--el-border-color)] border-1px)'
-    ]"
-  >
-    <div
-      v-if="title"
-      :class="[
-        `${prefixCls}-header`,
-        'h-50px flex justify-between items-center border-bottom-1 border-solid border-[var(--tags-view-border-color)] px-10px cursor-pointer dark:border-[var(--el-border-color)]'
-      ]"
-      @click="toggleClick"
-    >
-      <div :class="[`${prefixCls}-header__title`, 'relative font-18px font-bold ml-10px']">
-        <div class="flex items-center">
-          {{ title }}
-          <ElTooltip v-if="message" :content="message" placement="right">
-            <Icon icon="ep:warning" class="ml-5px" />
-          </ElTooltip>
-        </div>
-      </div>
-      <Icon v-if="collapse" :icon="show ? 'ep:arrow-down' : 'ep:arrow-up'" />
-    </div>
+    const toggleClick = () => {
+      if (props.collapse) {
+        show.value = !unref(show)
+      }
+    }
 
-    <ElCollapseTransition>
-      <div v-show="show" :class="[`${prefixCls}-content`, 'p-10px']">
-        <ElDescriptions
-          :column="2"
-          border
-          :direction="mobile ? 'vertical' : 'horizontal'"
-          v-bind="getBindValue"
+    return () => {
+      return (
+        <div
+          class={[
+            prefixCls,
+            'bg-[var(--el-color-white)] dark:bg-[var(--el-bg-color)] dark:border-[var(--el-border-color)] dark:border-1px'
+          ]}
         >
-          <template v-if="slots['extra']" #extra>
-            <slot name="extra"></slot>
-          </template>
-          <ElDescriptionsItem
-            v-for="item in schema"
-            :key="item.field"
-            v-bind="getBindItemValue(item)"
-          >
-            <template #label>
-              <slot
-                :name="`${item.field}-label`"
-                :row="{
-                  label: item.label
-                }"
-                >{{ item.label }}</slot
-              >
-            </template>
-
-            <template #default>
-              <slot :name="item.field" :row="data">{{ data[item.field] }}</slot>
-            </template>
-          </ElDescriptionsItem>
-        </ElDescriptions>
-      </div>
-    </ElCollapseTransition>
-  </div>
-</template>
+          {props.title ? (
+            <div
+              class={[
+                `${prefixCls}-header`,
+                'relative h-50px flex justify-between items-center layout-border__bottom px-10px cursor-pointer'
+              ]}
+              onClick={toggleClick}
+            >
+              <div class={[`${prefixCls}-header__title`, 'relative font-18px font-bold ml-10px']}>
+                <div class="flex items-center">
+                  {props.title}
+                  {props.message ? (
+                    <ElTooltip content={props.message} placement="right">
+                      <Icon icon="bi:question-circle-fill" class="ml-5px" size={14} />
+                    </ElTooltip>
+                  ) : null}
+                </div>
+              </div>
+              {props.collapse ? <Icon icon={show.value ? 'ep:arrow-down' : 'ep:arrow-up'} /> : null}
+            </div>
+          ) : null}
+
+          <ElCollapseTransition>
+            <div v-show={unref(show)} class={[`${prefixCls}-content`]}>
+              <ElDescriptions {...unref(getBindValue)}>
+                {{
+                  extra: () => (slots['extra'] ? slots['extra']() : props.extra),
+                  default: () => {
+                    return props.schema.map((item) => {
+                      return (
+                        <ElDescriptionsItem key={item.field} {...getBindItemValue(item)}>
+                          {{
+                            label: () => (item.slots?.label ? item.slots?.label(item) : item.label),
+                            default: () =>
+                              item.slots?.default
+                                ? item.slots?.default(props.data)
+                                : get(props.data, item.field)
+                          }}
+                        </ElDescriptionsItem>
+                      )
+                    })
+                  }
+                }}
+              </ElDescriptions>
+            </div>
+          </ElCollapseTransition>
+        </div>
+      )
+    }
+  }
+})
+</script>
 
 <style lang="less" scoped>
 @prefix-cls: ~'@{namespace}-descriptions';
 
+:deep(.@{elNamespace}-descriptions__header) {
+  display: none !important;
+}
+
 .@{prefix-cls}-header {
   &__title {
     &::after {

+ 4 - 0
src/types/descriptions.d.ts → src/components/Descriptions/src/types/index.ts

@@ -8,4 +8,8 @@ export interface DescriptionsSchema {
   labelAlign?: 'left' | 'center' | 'right'
   className?: string
   labelClassName?: string
+  slots?: {
+    default?: (...args: any[]) => JSX.Element | null
+    label?: (...args: any[]) => JSX.Element | null
+  }
 }

+ 41 - 28
src/components/Dialog/src/Dialog.vue

@@ -10,7 +10,7 @@ const props = defineProps({
   modelValue: propTypes.bool.def(false),
   title: propTypes.string.def('Dialog'),
   fullscreen: propTypes.bool.def(true),
-  maxHeight: propTypes.oneOfType([String, Number]).def('500px')
+  maxHeight: propTypes.oneOfType([String, Number]).def('400px')
 })
 
 const getBindValue = computed(() => {
@@ -50,7 +50,6 @@ watch(
 )
 
 const dialogStyle = computed(() => {
-  console.log(unref(dialogHeight))
   return {
     height: unref(dialogHeight)
   }
@@ -64,20 +63,34 @@ const dialogStyle = computed(() => {
     destroy-on-close
     lock-scroll
     draggable
+    top="0"
     :close-on-click-modal="false"
+    :show-close="false"
   >
-    <template #header>
-      <div class="flex justify-between">
+    <template #header="{ close }">
+      <div class="flex justify-between items-center h-54px pl-15px pr-15px relative">
         <slot name="title">
           {{ title }}
         </slot>
-        <Icon
-          v-if="fullscreen"
-          class="mr-18px cursor-pointer is-hover mt-2px z-10"
-          :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
-          color="var(--el-color-info)"
-          @click="toggleFull"
-        />
+        <div
+          class="h-54px flex justify-between items-center absolute top-[50%] right-15px translate-y-[-50%]"
+        >
+          <Icon
+            v-if="fullscreen"
+            class="cursor-pointer is-hover !h-54px mr-10px"
+            :icon="isFullscreen ? 'radix-icons:exit-full-screen' : 'radix-icons:enter-full-screen'"
+            color="var(--el-color-info)"
+            hover-color="var(--el-color-primary)"
+            @click="toggleFull"
+          />
+          <Icon
+            class="cursor-pointer is-hover !h-54px"
+            icon="ep:close"
+            hover-color="var(--el-color-primary)"
+            color="var(--el-color-info)"
+            @click="close"
+          />
+        </div>
       </div>
     </template>
 
@@ -92,28 +105,28 @@ const dialogStyle = computed(() => {
 </template>
 
 <style lang="less">
-.@{elNamespace}-dialog__header {
-  margin-right: 0 !important;
-  border-bottom: 1px solid var(--tags-view-border-color);
+.@{elNamespace}-overlay-dialog {
+  display: flex;
+  justify-content: center;
+  align-items: center;
 }
 
-.@{elNamespace}-dialog__footer {
-  border-top: 1px solid var(--tags-view-border-color);
-}
-
-.is-hover {
-  &:hover {
-    color: var(--el-color-primary) !important;
-  }
-}
-
-.dark {
-  .@{elNamespace}-dialog__header {
+.@{elNamespace}-dialog {
+  margin: 0 !important;
+  &__header {
+    margin-right: 0 !important;
     border-bottom: 1px solid var(--el-border-color);
+    padding: 0;
+    height: 54px;
   }
-
-  .@{elNamespace}-dialog__footer {
+  &__body {
+    padding: 15px !important;
+  }
+  &__footer {
     border-top: 1px solid var(--el-border-color);
   }
+  &__headerbtn {
+    top: 0;
+  }
 }
 </style>

+ 2 - 3
src/components/Editor/src/Editor.vue

@@ -99,7 +99,6 @@ const handleChange = (editor: IDomEditor) => {
 // 组件销毁时,及时销毁编辑器
 onBeforeUnmount(() => {
   const editor = unref(editorRef.value)
-  if (editor === null) return
 
   // 销毁,并移除 editor
   editor?.destroy()
@@ -116,12 +115,12 @@ defineExpose({
 </script>
 
 <template>
-  <div class="border-1 border-solid border-[var(--tags-view-border-color)] z-99">
+  <div class="border-1 border-solid border-[var(--el-border-color)] z-10">
     <!-- 工具栏 -->
     <Toolbar
       :editor="editorRef"
       :editorId="editorId"
-      class="border-bottom-1 border-solid border-[var(--tags-view-border-color)]"
+      class="border-0 b-b-1 border-solid border-[var(--el-border-color)]"
     />
     <!-- 编辑器 -->
     <Editor

+ 37 - 4
src/components/Form/index.ts

@@ -1,15 +1,48 @@
 import Form from './src/Form.vue'
-import { ElForm } from 'element-plus'
-import { FormSchema, FormSetPropsType } from '@/types/form'
+import type { FormSchema, FormSetProps } from './src/types'
+export type {
+  ComponentNameEnum,
+  ComponentName,
+  InputComponentProps,
+  AutocompleteComponentProps,
+  InputNumberComponentProps,
+  SelectOption,
+  SelectComponentProps,
+  SelectV2ComponentProps,
+  CascaderComponentProps,
+  SwitchComponentProps,
+  RateComponentProps,
+  ColorPickerComponentProps,
+  TransferComponentProps,
+  RadioOption,
+  RadioGroupComponentProps,
+  RadioButtonComponentProps,
+  CheckboxOption,
+  CheckboxGroupComponentProps,
+  DividerComponentProps,
+  DatePickerComponentProps,
+  DateTimePickerComponentProps,
+  TimePickerComponentProps,
+  TimeSelectComponentProps,
+  ColProps,
+  FormSetProps,
+  FormItemProps,
+  FormSchema,
+  FormProps,
+  PlaceholderModel,
+  InputPasswordComponentProps,
+  TreeSelectComponentProps
+} from './src/types'
 
 export interface FormExpose {
   setValues: (data: Recordable) => void
   setProps: (props: Recordable) => void
   delSchema: (field: string) => void
   addSchema: (formSchema: FormSchema, index?: number) => void
-  setSchema: (schemaProps: FormSetPropsType[]) => void
+  setSchema: (schemaProps: FormSetProps[]) => void
   formModel: Recordable
-  getElFormRef: () => ComponentRef<typeof ElForm>
+  getComponentExpose: (field: string) => any
+  getFormItemExpose: (field: string) => any
 }
 
 export { Form }

+ 200 - 92
src/components/Form/src/Form.vue

@@ -1,7 +1,15 @@
 <script lang="tsx">
 import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
-import { ElForm, ElFormItem, ElRow, ElCol, ElTooltip } from 'element-plus'
-import { componentMap } from './componentMap'
+import {
+  ElForm,
+  ElFormItem,
+  ElRow,
+  ElCol,
+  FormRules,
+  ComponentSize
+  // FormItemProp
+} from 'element-plus'
+import { componentMap } from './helper/componentMap'
 import { propTypes } from '@/utils/propTypes'
 import { getSlot } from '@/utils/tsxHelper'
 import {
@@ -9,18 +17,27 @@ import {
   setGridProp,
   setComponentProps,
   setItemComponentSlots,
-  initModel,
-  setFormItemSlots
+  initModel
 } from './helper'
 import { useRenderSelect } from './components/useRenderSelect'
 import { useRenderRadio } from './components/useRenderRadio'
 import { useRenderCheckbox } from './components/useRenderCheckbox'
 import { useDesign } from '@/hooks/web/useDesign'
 import { findIndex } from '@/utils'
-import { set } from 'lodash-es'
+import { get, set } from 'lodash-es'
 import { FormProps } from './types'
-import { Icon } from '@/components/Icon'
-import { FormSchema, FormSetPropsType } from '@/types/form'
+import {
+  FormSchema,
+  FormSetProps,
+  ComponentNameEnum,
+  SelectComponentProps,
+  RadioGroupComponentProps,
+  CheckboxGroupComponentProps
+} from './types'
+
+const { renderSelectOptions } = useRenderSelect()
+const { renderRadioOptions } = useRenderRadio()
+const { renderCheckboxOptions } = useRenderCheckbox()
 
 const { getPrefixCls } = useDesign()
 
@@ -38,7 +55,7 @@ export default defineComponent({
     isCol: propTypes.bool.def(true),
     // 表单数据对象
     model: {
-      type: Object as PropType<Recordable>,
+      type: Object as PropType<any>,
       default: () => ({})
     },
     // 是否自动设置placeholder
@@ -46,7 +63,30 @@ export default defineComponent({
     // 是否自定义内容
     isCustom: propTypes.bool.def(false),
     // 表单label宽度
-    labelWidth: propTypes.oneOfType([String, Number]).def('auto')
+    labelWidth: propTypes.oneOfType([String, Number]).def('auto'),
+    rules: {
+      type: Object as PropType<FormRules>,
+      default: () => ({})
+    },
+    labelPosition: propTypes.oneOf(['left', 'right', 'top']).def('right'),
+    labelSuffix: propTypes.string.def(''),
+    hideRequiredAsterisk: propTypes.bool.def(false),
+    requireAsteriskPosition: propTypes.oneOf(['left', 'right']).def('left'),
+    showMessage: propTypes.bool.def(true),
+    inlineMessage: propTypes.bool.def(false),
+    statusIcon: propTypes.bool.def(false),
+    validateOnRuleChange: propTypes.bool.def(true),
+    size: {
+      type: String as PropType<ComponentSize>,
+      default: undefined
+    },
+    disabled: propTypes.bool.def(false),
+    scrollToError: propTypes.bool.def(false),
+    scrollToErrorOffset: propTypes.oneOfType([Boolean, Object]).def(undefined)
+    // onValidate: {
+    //   type: Function as PropType<(prop: FormItemProp, isValid: boolean, message: string) => void>,
+    //   default: () => {}
+    // }
   },
   emits: ['register'],
   setup(props, { slots, expose, emit }) {
@@ -64,8 +104,14 @@ export default defineComponent({
       return propsObj
     })
 
+    // 存储表单实例
+    const formComponents = ref({})
+
+    // 存储form-item实例
+    const formItemComponents = ref({})
+
     // 表单数据
-    const formModel = ref<Recordable>({})
+    const formModel = ref<Recordable>(props.model)
 
     onMounted(() => {
       emit('register', unref(elFormRef)?.$parent, unref(elFormRef))
@@ -78,6 +124,7 @@ export default defineComponent({
 
     const setProps = (props: FormProps = {}) => {
       mergeProps.value = Object.assign(unref(mergeProps), props)
+      // @ts-ignore
       outsideProps.value = props
     }
 
@@ -99,7 +146,7 @@ export default defineComponent({
       schema.push(formSchema)
     }
 
-    const setSchema = (schemaProps: FormSetPropsType[]) => {
+    const setSchema = (schemaProps: FormSetProps[]) => {
       const { schema } = unref(getProps)
       for (const v of schema) {
         for (const item of schemaProps) {
@@ -110,8 +157,42 @@ export default defineComponent({
       }
     }
 
-    const getElFormRef = (): ComponentRef<typeof ElForm> => {
-      return unref(elFormRef) as ComponentRef<typeof ElForm>
+    const getOptions = async (fn: Function, item: FormSchema) => {
+      const options = await fn()
+      setSchema([
+        {
+          field: item.field,
+          path:
+            item.component === ComponentNameEnum.TREE_SELECT
+              ? 'componentProps.data'
+              : 'componentProps.options',
+          value: options
+        }
+      ])
+    }
+
+    /**
+     * @description: 获取表单组件实例
+     * @param filed 表单字段
+     */
+    const getComponentExpose = (filed: string) => {
+      return unref(formComponents)[filed]
+    }
+
+    /**
+     * @description: 获取formItem实例
+     * @param filed 表单字段
+     */
+    const getFormItemExpose = (filed: string) => {
+      return unref(formItemComponents)[filed]
+    }
+
+    const setComponentRefMap = (ref: any, filed: string) => {
+      formComponents.value[filed] = ref
+    }
+
+    const setFormItemRefMap = (ref: any, filed: string) => {
+      formItemComponents.value[filed] = ref
     }
 
     expose({
@@ -121,7 +202,8 @@ export default defineComponent({
       delSchema,
       addSchema,
       setSchema,
-      getElFormRef
+      getComponentExpose,
+      getFormItemExpose
     })
 
     // 监听表单结构化数组,重新生成formModel
@@ -153,7 +235,7 @@ export default defineComponent({
       const { schema = [], isCol } = unref(getProps)
 
       return schema
-        .filter((v) => !v.hidden)
+        .filter((v) => !v.remove)
         .map((item) => {
           // 如果是 Divider 组件,需要自己占用一行
           const isDivider = item.component === 'Divider'
@@ -171,93 +253,119 @@ export default defineComponent({
 
     // 渲染formItem
     const renderFormItem = (item: FormSchema) => {
-      // 单独给只有options属性的组件做判断
-      const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
-      const slotsMap: Recordable = {
-        ...setItemComponentSlots(slots, item?.componentProps?.slots, item.field)
-      }
-      if (
-        item?.component !== 'SelectV2' &&
-        item?.component !== 'Cascader' &&
-        item?.componentProps?.options
-      ) {
-        slotsMap.default = () => renderOptions(item)
+      // 如果有optionApi,优先使用optionApi
+      if (item.optionApi) {
+        // 内部自动调用接口,不影响其它渲染
+        getOptions(item.optionApi, item)
       }
+      const formItemSlots: Recordable = {
+        default: () => {
+          if (item?.formItemProps?.slots?.default) {
+            return item?.formItemProps?.slots?.default(formModel.value)
+          } else {
+            const Com = componentMap[item.component as string] as ReturnType<typeof defineComponent>
 
-      const formItemSlots: Recordable = setFormItemSlots(slots, item.field)
-      // 如果有 labelMessage,自动使用插槽渲染
-      if (item?.labelMessage) {
-        formItemSlots.label = () => {
-          return (
-            <>
-              <span>{item.label}</span>
-              <ElTooltip placement="right" raw-content>
-                {{
-                  content: () => <span v-html={item.labelMessage}></span>,
-                  default: () => (
-                    <Icon
-                      icon="ep:warning"
-                      size={16}
-                      color="var(--el-color-primary)"
-                      class="ml-2px relative top-1px"
-                    ></Icon>
-                  )
-                }}
-              </ElTooltip>
-            </>
-          )
-        }
-      }
-      return (
-        <ElFormItem {...(item.formItemProps || {})} prop={item.field} label={item.label || ''}>
-          {{
-            ...formItemSlots,
-            default: () => {
-              const Com = componentMap[item.component as string] as ReturnType<
-                typeof defineComponent
-              >
-
-              const { autoSetPlaceholder } = unref(getProps)
-
-              return slots[item.field] ? (
-                getSlot(slots, item.field, formModel.value)
-              ) : (
+            const { autoSetPlaceholder } = unref(getProps)
+
+            const componentSlots = (item?.componentProps as any)?.slots || {}
+            const slotsMap: Recordable = {
+              ...setItemComponentSlots(componentSlots)
+            }
+            // // 如果是select组件,并且没有自定义模板,自动渲染options
+            if (item.component === ComponentNameEnum.SELECT) {
+              slotsMap.default = !componentSlots.default
+                ? () => renderSelectOptions(item)
+                : () => {
+                    return componentSlots.default(
+                      unref((item?.componentProps as SelectComponentProps)?.options)
+                    )
+                  }
+            }
+
+            // 虚拟列表
+            if (item.component === ComponentNameEnum.SELECT_V2 && componentSlots.default) {
+              slotsMap.default = ({ item }) => {
+                return componentSlots.default(item)
+              }
+            }
+
+            // 单选框组和按钮样式
+            if (
+              item.component === ComponentNameEnum.RADIO_GROUP ||
+              item.component === ComponentNameEnum.RADIO_BUTTON
+            ) {
+              slotsMap.default = !componentSlots.default
+                ? () => renderRadioOptions(item)
+                : () => {
+                    return componentSlots.default(
+                      unref((item?.componentProps as CheckboxGroupComponentProps)?.options)
+                    )
+                  }
+            }
+
+            // 多选框组和按钮样式
+            if (
+              item.component === ComponentNameEnum.CHECKBOX_GROUP ||
+              item.component === ComponentNameEnum.CHECKBOX_BUTTON
+            ) {
+              slotsMap.default = !componentSlots.default
+                ? () => renderCheckboxOptions(item)
+                : () => {
+                    return componentSlots.default(
+                      unref((item?.componentProps as RadioGroupComponentProps)?.options)
+                    )
+                  }
+            }
+
+            const Comp = () => {
+              // 如果field是多层路径,需要转换成对象
+              const itemVal = computed({
+                get: () => {
+                  return get(formModel.value, item.field)
+                },
+                set: (val) => {
+                  set(formModel.value, item.field, val)
+                }
+              })
+
+              return (
                 <Com
-                  vModel={formModel.value[item.field]}
+                  vModel={itemVal.value}
+                  ref={(el: any) => setComponentRefMap(el, item.field)}
                   {...(autoSetPlaceholder && setTextPlaceholder(item))}
                   {...setComponentProps(item)}
-                  style={item.componentProps?.style}
-                  {...(notRenderOptions.includes(item?.component as string) &&
-                  item?.componentProps?.options
-                    ? { options: item?.componentProps?.options || [] }
-                    : {})}
+                  style={item.componentProps?.style || {}}
                 >
                   {{ ...slotsMap }}
                 </Com>
               )
             }
-          }}
-        </ElFormItem>
-      )
-    }
 
-    // 渲染options
-    const renderOptions = (item: FormSchema) => {
-      switch (item.component) {
-        case 'Select':
-          const { renderSelectOptions } = useRenderSelect(slots)
-          return renderSelectOptions(item)
-        case 'Radio':
-        case 'RadioButton':
-          const { renderRadioOptions } = useRenderRadio()
-          return renderRadioOptions(item)
-        case 'Checkbox':
-        case 'CheckboxButton':
-          const { renderCheckboxOptions } = useRenderCheckbox()
-          return renderCheckboxOptions(item)
-        default:
-          break
+            return <>{Comp()}</>
+          }
+        }
+      }
+      if (item?.formItemProps?.slots?.label) {
+        formItemSlots.label = (...args: any[]) => {
+          return (item?.formItemProps?.slots as any)?.label(...args)
+        }
       }
+      if (item?.formItemProps?.slots?.error) {
+        formItemSlots.error = (...args: any[]) => {
+          return (item?.formItemProps?.slots as any)?.error(...args)
+        }
+      }
+      return (
+        <ElFormItem
+          v-show={!item.hidden}
+          ref={(el: any) => setFormItemRefMap(el, item.field)}
+          {...(item.formItemProps || {})}
+          prop={item.field}
+          label={item.label || ''}
+        >
+          {formItemSlots}
+        </ElFormItem>
+      )
     }
 
     // 过滤传入Form组件的属性
@@ -270,14 +378,14 @@ export default defineComponent({
           delete props[key]
         }
       }
-      return props
+      return props as FormProps
     }
 
     return () => (
       <ElForm
         ref={elFormRef}
         {...getFormBindValue()}
-        model={props.isCustom ? props.model : formModel}
+        model={unref(getProps).isCustom ? unref(getProps).model : formModel}
         class={prefixCls}
       >
         {{

+ 14 - 8
src/components/Form/src/components/useRenderCheckbox.tsx

@@ -1,19 +1,25 @@
-import { FormSchema } from '@/types/form'
+import { FormSchema, ComponentNameEnum, CheckboxGroupComponentProps } from '../types'
 import { ElCheckbox, ElCheckboxButton } from 'element-plus'
 import { defineComponent } from 'vue'
 
 export const useRenderCheckbox = () => {
   const renderCheckboxOptions = (item: FormSchema) => {
     // 如果有别名,就取别名
-    const labelAlias = item?.componentProps?.optionsAlias?.labelField
-    const valueAlias = item?.componentProps?.optionsAlias?.valueField
-    const Com = (item.component === 'Checkbox' ? ElCheckbox : ElCheckboxButton) as ReturnType<
-      typeof defineComponent
-    >
-    return item?.componentProps?.options?.map((option) => {
+    const componentProps = item?.componentProps as CheckboxGroupComponentProps
+    const valueAlias = componentProps?.props?.value || 'value'
+    const labelAlias = componentProps?.props?.label || 'label'
+    const disabledAlias = componentProps?.props?.disabled || 'disabled'
+    const Com = (
+      item.component === ComponentNameEnum.CHECKBOX_GROUP ? ElCheckbox : ElCheckboxButton
+    ) as ReturnType<typeof defineComponent>
+    return componentProps?.options?.map((option) => {
       const { value, ...other } = option
       return (
-        <Com {...other} label={option[valueAlias || 'value']}>
+        <Com
+          {...other}
+          disabled={option[disabledAlias || 'disabled']}
+          label={option[valueAlias || 'value']}
+        >
           {option[labelAlias || 'label']}
         </Com>
       )

+ 14 - 8
src/components/Form/src/components/useRenderRadio.tsx

@@ -1,19 +1,25 @@
-import { FormSchema } from '@/types/form'
+import { FormSchema, ComponentNameEnum, RadioGroupComponentProps } from '../types'
 import { ElRadio, ElRadioButton } from 'element-plus'
 import { defineComponent } from 'vue'
 
 export const useRenderRadio = () => {
   const renderRadioOptions = (item: FormSchema) => {
     // 如果有别名,就取别名
-    const labelAlias = item?.componentProps?.optionsAlias?.labelField
-    const valueAlias = item?.componentProps?.optionsAlias?.valueField
-    const Com = (item.component === 'Radio' ? ElRadio : ElRadioButton) as ReturnType<
-      typeof defineComponent
-    >
-    return item?.componentProps?.options?.map((option) => {
+    const componentProps = item?.componentProps as RadioGroupComponentProps
+    const valueAlias = componentProps?.props?.value || 'value'
+    const labelAlias = componentProps?.props?.label || 'label'
+    const disabledAlias = componentProps?.props?.disabled || 'disabled'
+    const Com = (
+      item.component === ComponentNameEnum.RADIO_GROUP ? ElRadio : ElRadioButton
+    ) as ReturnType<typeof defineComponent>
+    return componentProps?.options?.map((option) => {
       const { value, ...other } = option
       return (
-        <Com {...other} label={option[valueAlias || 'value']}>
+        <Com
+          {...other}
+          disabled={option[disabledAlias || 'disabled']}
+          label={option[valueAlias || 'value']}
+        >
           {option[labelAlias || 'label']}
         </Com>
       )

+ 27 - 26
src/components/Form/src/components/useRenderSelect.tsx

@@ -1,22 +1,25 @@
 import { ElOption, ElOptionGroup } from 'element-plus'
-import { getSlot } from '@/utils/tsxHelper'
-import { Slots } from 'vue'
-import { FormSchema } from '@/types/form'
-import { ComponentOptions } from '@/types/components'
+import { FormSchema, SelectComponentProps, SelectOption } from '../types'
 
-export const useRenderSelect = (slots: Slots) => {
+export const useRenderSelect = () => {
   // 渲染 select options
   const renderSelectOptions = (item: FormSchema) => {
+    const componentsProps = item?.componentProps as SelectComponentProps
+    const optionGroupDefaultSlot = componentsProps?.slots?.optionGroupDefault
     // 如果有别名,就取别名
-    const labelAlias = item?.componentProps?.optionsAlias?.labelField
-    return item?.componentProps?.options?.map((option) => {
+    const labelAlias = componentsProps?.props?.label
+    const keyAlias = componentsProps?.props?.key
+    return componentsProps?.options?.map((option) => {
       if (option?.options?.length) {
-        return (
-          <ElOptionGroup label={option[labelAlias || 'label']}>
-            {() => {
-              return option?.options?.map((v) => {
-                return renderSelectOptionItem(item, v)
-              })
+        return optionGroupDefaultSlot ? (
+          optionGroupDefaultSlot(option)
+        ) : (
+          <ElOptionGroup label={option[labelAlias || 'label']} key={option[keyAlias || 'key']}>
+            {{
+              default: () =>
+                option?.options?.map((v) => {
+                  return renderSelectOptionItem(item, v)
+                })
             }}
           </ElOptionGroup>
         )
@@ -27,25 +30,23 @@ export const useRenderSelect = (slots: Slots) => {
   }
 
   // 渲染 select option item
-  const renderSelectOptionItem = (item: FormSchema, option: ComponentOptions) => {
+  const renderSelectOptionItem = (item: FormSchema, option: SelectOption) => {
     // 如果有别名,就取别名
-    const labelAlias = item?.componentProps?.optionsAlias?.labelField
-    const valueAlias = item?.componentProps?.optionsAlias?.valueField
-
-    const { label, value, ...other } = option
+    const componentsProps = item.componentProps as SelectComponentProps
+    const labelAlias = componentsProps?.props?.label
+    const valueAlias = componentsProps?.props?.value
+    const keyAlias = componentsProps?.props?.key
+    const optionDefaultSlot = componentsProps.slots?.optionDefault
 
     return (
       <ElOption
-        {...other}
-        label={labelAlias ? option[labelAlias] : label}
-        value={valueAlias ? option[valueAlias] : value}
+        {...option}
+        key={option[keyAlias || 'key']}
+        label={option[labelAlias || 'label']}
+        value={option[valueAlias || 'value']}
       >
         {{
-          default: () =>
-            // option 插槽名规则,{field}-option
-            item?.componentProps?.optionsSlot
-              ? getSlot(slots, `${item.field}-option`, { item: option })
-              : undefined
+          default: () => (optionDefaultSlot ? optionDefaultSlot(option) : undefined)
         }}
       </ElOption>
     )

+ 0 - 150
src/components/Form/src/helper.ts

@@ -1,150 +0,0 @@
-import { useI18n } from '@/hooks/web/useI18n'
-import type { Slots } from 'vue'
-import { getSlot } from '@/utils/tsxHelper'
-import { PlaceholderMoel } from './types'
-import { FormSchema } from '@/types/form'
-import { ColProps } from '@/types/components'
-
-const { t } = useI18n()
-
-/**
- *
- * @param schema 对应组件数据
- * @returns 返回提示信息对象
- * @description 用于自动设置placeholder
- */
-export const setTextPlaceholder = (schema: FormSchema): PlaceholderMoel => {
-  const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword']
-  const selectMap = ['Select', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect']
-  if (textMap.includes(schema?.component as string)) {
-    return {
-      placeholder: t('common.inputText')
-    }
-  }
-  if (selectMap.includes(schema?.component as string)) {
-    // 一些范围选择器
-    const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange']
-    if (
-      twoTextMap.includes(
-        (schema?.componentProps?.type || schema?.componentProps?.isRange) as string
-      )
-    ) {
-      return {
-        startPlaceholder: t('common.startTimeText'),
-        endPlaceholder: t('common.endTimeText'),
-        rangeSeparator: '-'
-      }
-    } else {
-      return {
-        placeholder: t('common.selectText')
-      }
-    }
-  }
-  return {}
-}
-
-/**
- *
- * @param col 内置栅格
- * @returns 返回栅格属性
- * @description 合并传入进来的栅格属性
- */
-export const setGridProp = (col: ColProps = {}): ColProps => {
-  const colProps: ColProps = {
-    // 如果有span,代表用户优先级更高,所以不需要默认栅格
-    ...(col.span
-      ? {}
-      : {
-          xs: 24,
-          sm: 12,
-          md: 12,
-          lg: 12,
-          xl: 12
-        }),
-    ...col
-  }
-  return colProps
-}
-
-/**
- *
- * @param item 传入的组件属性
- * @returns 默认添加 clearable 属性
- */
-export const setComponentProps = (item: FormSchema): Recordable => {
-  const notNeedClearable = ['ColorPicker']
-  const componentProps: Recordable = notNeedClearable.includes(item.component as string)
-    ? { ...item.componentProps }
-    : {
-        clearable: true,
-        ...item.componentProps
-      }
-  // 需要删除额外的属性
-  delete componentProps?.slots
-  return componentProps
-}
-
-/**
- *
- * @param slots 插槽
- * @param slotsProps 插槽属性
- * @param field 字段名
- */
-export const setItemComponentSlots = (
-  slots: Slots,
-  slotsProps: Recordable = {},
-  field: string
-): Recordable => {
-  const slotObj: Recordable = {}
-  for (const key in slotsProps) {
-    if (slotsProps[key]) {
-      // 由于组件有可能重复,需要有一个唯一的前缀
-      slotObj[key] = (data: Recordable) => {
-        return getSlot(slots, `${field}-${key}`, data)
-      }
-    }
-  }
-  return slotObj
-}
-
-/**
- *
- * @param schema Form表单结构化数组
- * @param formModel FormMoel
- * @returns FormMoel
- * @description 生成对应的formModel
- */
-export const initModel = (schema: FormSchema[], formModel: Recordable) => {
-  const model: Recordable = { ...formModel }
-  schema.map((v) => {
-    // 如果是hidden,就删除对应的值
-    if (v.hidden) {
-      delete model[v.field]
-    } else if (v.component && v.component !== 'Divider') {
-      const hasField = Reflect.has(model, v.field)
-      // 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值
-      model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : ''
-    }
-  })
-  return model
-}
-
-/**
- * @param slots 插槽
- * @param field 字段名
- * @returns 返回FormIiem插槽
- */
-export const setFormItemSlots = (slots: Slots, field: string): Recordable => {
-  const slotObj: Recordable = {}
-  if (slots[`${field}-error`]) {
-    slotObj['error'] = (data: Recordable) => {
-      return getSlot(slots, `${field}-error`, data)
-    }
-  }
-  if (slots[`${field}-label`]) {
-    slotObj['label'] = (data: Recordable) => {
-      return getSlot(slots, `${field}-label`, data)
-    }
-  }
-  return slotObj
-}

+ 10 - 6
src/components/Form/src/componentMap.ts → src/components/Form/src/helper/componentMap.ts

@@ -16,15 +16,18 @@ import {
   ElTimeSelect,
   ElTransfer,
   ElAutocomplete,
-  ElDivider
+  ElDivider,
+  ElTreeSelect,
+  ElUpload
 } from 'element-plus'
 import { InputPassword } from '@/components/InputPassword'
 import { Editor } from '@/components/Editor'
-import { ComponentName } from '@/types/components'
+import { ComponentName } from '../types'
 
 const componentMap: Recordable<Component, ComponentName> = {
-  Radio: ElRadioGroup,
-  Checkbox: ElCheckboxGroup,
+  RadioGroup: ElRadioGroup,
+  RadioButton: ElRadioGroup,
+  CheckboxGroup: ElCheckboxGroup,
   CheckboxButton: ElCheckboxGroup,
   Input: ElInput,
   Autocomplete: ElAutocomplete,
@@ -41,9 +44,10 @@ const componentMap: Recordable<Component, ComponentName> = {
   Divider: ElDivider,
   TimeSelect: ElTimeSelect,
   SelectV2: ElSelectV2,
-  RadioButton: ElRadioGroup,
   InputPassword: InputPassword,
-  Editor: Editor
+  Editor: Editor,
+  TreeSelect: ElTreeSelect,
+  Upload: ElUpload
 }
 
 export { componentMap }

+ 162 - 0
src/components/Form/src/helper/index.ts

@@ -0,0 +1,162 @@
+import { useI18n } from '@/hooks/web/useI18n'
+import { PlaceholderModel, FormSchema, ComponentNameEnum, ColProps } from '../types'
+import { isFunction } from '@/utils/is'
+import { firstUpperCase, humpToDash } from '@/utils'
+import { set, get } from 'lodash-es'
+
+const { t } = useI18n()
+
+/**
+ *
+ * @param schema 对应组件数据
+ * @returns 返回提示信息对象
+ * @description 用于自动设置placeholder
+ */
+export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => {
+  const textMap = [
+    ComponentNameEnum.INPUT,
+    ComponentNameEnum.AUTOCOMPLETE,
+    ComponentNameEnum.INPUT_NUMBER,
+    ComponentNameEnum.INPUT_PASSWORD
+  ]
+  const selectMap = [
+    ComponentNameEnum.SELECT,
+    ComponentNameEnum.TIME_PICKER,
+    ComponentNameEnum.DATE_PICKER,
+    ComponentNameEnum.TIME_SELECT,
+    ComponentNameEnum.SELECT_V2
+  ]
+  if (textMap.includes(schema?.component as ComponentNameEnum)) {
+    return {
+      placeholder: t('common.inputText')
+    }
+  }
+  if (selectMap.includes(schema?.component as ComponentNameEnum)) {
+    // 一些范围选择器
+    const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange']
+    if (
+      twoTextMap.includes(
+        ((schema?.componentProps as any)?.type ||
+          (schema?.componentProps as any)?.isRange) as string
+      )
+    ) {
+      return {
+        startPlaceholder: t('common.startTimeText'),
+        endPlaceholder: t('common.endTimeText'),
+        rangeSeparator: '-'
+      }
+    } else {
+      return {
+        placeholder: t('common.selectText')
+      }
+    }
+  }
+  return {}
+}
+
+/**
+ *
+ * @param col 内置栅格
+ * @returns 返回栅格属性
+ * @description 合并传入进来的栅格属性
+ */
+export const setGridProp = (col: ColProps = {}): ColProps => {
+  const colProps: ColProps = {
+    // 如果有span,代表用户优先级更高,所以不需要默认栅格
+    ...(col.span
+      ? {}
+      : {
+          xs: 24,
+          sm: 12,
+          md: 12,
+          lg: 12,
+          xl: 12
+        }),
+    ...col
+  }
+  return colProps
+}
+
+/**
+ *
+ * @param item 传入的组件属性
+ * @returns 默认添加 clearable 属性
+ */
+export const setComponentProps = (item: FormSchema): Recordable => {
+  // const notNeedClearable = ['ColorPicker']
+  // 拆分事件并组合
+  const onEvents = (item?.componentProps as any)?.on || {}
+  const newOnEvents: Recordable = {}
+
+  for (const key in onEvents) {
+    if (onEvents[key]) {
+      newOnEvents[`on${firstUpperCase(key)}`] = (...args: any[]) => {
+        onEvents[key](...args)
+      }
+    }
+  }
+
+  const componentProps: Recordable = {
+    clearable: true,
+    ...item.componentProps,
+    ...newOnEvents
+  }
+  // 需要删除额外的属性
+  if (componentProps.slots) {
+    delete componentProps.slots
+  }
+  if (componentProps.on) {
+    delete componentProps.on
+  }
+  return componentProps
+}
+
+/**
+ *
+ * @param formModel 表单数据
+ * @param slotsProps 插槽属性
+ */
+export const setItemComponentSlots = (slotsProps: Recordable = {}): Recordable => {
+  const slotObj: Recordable = {}
+  for (const key in slotsProps) {
+    if (slotsProps[key]) {
+      if (isFunction(slotsProps[key])) {
+        slotObj[humpToDash(key)] = (...args: any[]) => {
+          return slotsProps[key]?.(...args)
+        }
+      } else {
+        slotObj[humpToDash(key)] = () => {
+          return slotsProps[key]
+        }
+      }
+    }
+  }
+  return slotObj
+}
+
+/**
+ *
+ * @param schema Form表单结构化数组
+ * @param formModel FormMoel
+ * @returns FormMoel
+ * @description 生成对应的formModel
+ */
+export const initModel = (schema: FormSchema[], formModel: Recordable) => {
+  const model: Recordable = { ...formModel }
+  schema.map((v) => {
+    if (v.remove) {
+      delete model[v.field]
+    } else if (v.component !== 'Divider') {
+      // const hasField = Reflect.has(model, v.field)
+      const hasField = get(model, v.field)
+      // 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值
+      set(
+        model,
+        v.field,
+        hasField !== void 0 ? get(model, v.field) : v.value !== void 0 ? v.value : undefined
+      )
+      // model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : undefined
+    }
+  })
+  return model
+}

+ 0 - 17
src/components/Form/src/types.ts

@@ -1,17 +0,0 @@
-import { FormSchema } from '@/types/form'
-
-export interface PlaceholderMoel {
-  placeholder?: string
-  startPlaceholder?: string
-  endPlaceholder?: string
-  rangeSeparator?: string
-}
-
-export type FormProps = {
-  schema?: FormSchema[]
-  isCol?: boolean
-  model?: Recordable
-  autoSetPlaceholder?: boolean
-  isCustom?: boolean
-  labelWidth?: string | number
-} & Recordable

+ 663 - 0
src/components/Form/src/types/index.ts

@@ -0,0 +1,663 @@
+import {
+  AutocompleteProps,
+  InputNumberProps,
+  CascaderProps,
+  CascaderNode,
+  CascaderValue,
+  SwitchProps,
+  ComponentSize,
+  InputProps,
+  RateProps,
+  ColorPickerProps,
+  TransferProps,
+  RadioGroupProps,
+  RadioButtonProps,
+  CheckboxGroupProps,
+  DividerProps,
+  DatePickerProps,
+  FormItemProps as ElFormItemProps,
+  FormProps as ElFormProps,
+  ISelectProps,
+  UploadProps
+} from 'element-plus'
+import { IEditorConfig } from '@wangeditor/editor'
+import { CSSProperties } from 'vue'
+
+export interface PlaceholderModel {
+  placeholder?: string
+  startPlaceholder?: string
+  endPlaceholder?: string
+  rangeSeparator?: string
+}
+
+export enum ComponentNameEnum {
+  RADIO_GROUP = 'RadioGroup',
+  RADIO_BUTTON = 'RadioButton',
+  CHECKBOX_GROUP = 'CheckboxGroup',
+  CHECKBOX_BUTTON = 'CheckboxButton',
+  INPUT = 'Input',
+  AUTOCOMPLETE = 'Autocomplete',
+  INPUT_NUMBER = 'InputNumber',
+  SELECT = 'Select',
+  CASCADER = 'Cascader',
+  SWITCH = 'Switch',
+  SLIDER = 'Slider',
+  TIME_PICKER = 'TimePicker',
+  DATE_PICKER = 'DatePicker',
+  RATE = 'Rate',
+  COLOR_PICKER = 'ColorPicker',
+  TRANSFER = 'Transfer',
+  DIVIDER = 'Divider',
+  TIME_SELECT = 'TimeSelect',
+  SELECT_V2 = 'SelectV2',
+  INPUT_PASSWORD = 'InputPassword',
+  EDITOR = 'Editor',
+  TREE_SELECT = 'TreeSelect',
+  UPLOAD = 'Upload'
+}
+
+type CamelCaseComponentName = keyof typeof ComponentNameEnum extends infer K
+  ? K extends string
+    ? K extends `${infer A}_${infer B}`
+      ? `${Capitalize<Lowercase<A>>}${Capitalize<Lowercase<B>>}`
+      : Capitalize<Lowercase<K>>
+    : never
+  : never
+
+export type ComponentName = CamelCaseComponentName
+
+export interface InputPasswordComponentProps {
+  strength?: boolean
+  style?: CSSProperties
+}
+
+export interface InputComponentProps extends Partial<InputProps> {
+  rows?: number
+  on?: {
+    blur?: (event: FocusEvent) => void
+    focus?: (event: FocusEvent) => void
+    change?: (value: string | number) => void
+    clear?: () => void
+    input?: (value: string | number) => void
+  }
+  slots?: {
+    prefix?: (...args: any[]) => JSX.Element | null
+    suffix?: (...args: any[]) => JSX.Element | null
+    prepend?: (...args: any[]) => JSX.Element | null
+    append?: (...args: any[]) => JSX.Element | null
+  }
+  style?: CSSProperties
+}
+
+export interface AutocompleteComponentProps extends Partial<AutocompleteProps> {
+  on?: {
+    select?: (item: any) => void
+    change?: (value: string | number) => void
+  }
+  slots?: {
+    default?: (...args: any[]) => JSX.Element | null
+    prefix?: (...args: any[]) => JSX.Element | null
+    suffix?: (...args: any[]) => JSX.Element | null
+    prepend?: (...args: any[]) => JSX.Element | null
+    append?: (...args: any[]) => JSX.Element | null
+  }
+  style?: CSSProperties
+}
+
+export interface InputNumberComponentProps extends Partial<InputNumberProps> {
+  on?: {
+    change?: (currentValue: number | undefined, oldValue: number | undefined) => void
+    blur?: (event: FocusEvent) => void
+    focus?: (event: FocusEvent) => void
+  }
+  style?: CSSProperties
+}
+
+export interface SelectOption {
+  label?: string
+  disabled?: boolean
+  value?: any
+  key?: string | number
+  options?: SelectOption[]
+  [key: string]: any
+}
+
+export interface SelectComponentProps extends Omit<Partial<ISelectProps>, 'options'> {
+  /**
+   * 数据源的字段别名
+   */
+  props?: {
+    key?: string
+    value?: string
+    label?: string
+    children?: string
+  }
+  on?: {
+    change?: (value: string | number | boolean | Object) => void
+    visibleChange?: (visible: boolean) => void
+    removeTag?: (tag: any) => void
+    clear?: () => void
+    blur?: (event: FocusEvent) => void
+    focus?: (event: FocusEvent) => void
+  }
+  slots?: {
+    default?: (options: SelectOption[]) => JSX.Element[] | null
+    optionGroupDefault?: (item: SelectOption) => JSX.Element
+    optionDefault?: (option: SelectOption) => JSX.Element | null
+    prefix?: (...args: any[]) => JSX.Element | null
+    empty?: (...args: any[]) => JSX.Element | null
+  }
+  options?: SelectOption[]
+  style?: CSSProperties
+}
+
+export interface SelectV2ComponentProps {
+  multiple?: boolean
+  disabled?: boolean
+  valueKey?: string
+  size?: ComponentSize
+  clearable?: boolean
+  clearIcon?: string | JSX.Element | null
+  collapseTags?: boolean
+  multipleLimit?: number
+  name?: string
+  effect?: string
+  autocomplete?: string
+  placeholder?: string
+  filterable?: boolean
+  allowCreate?: boolean
+  reserveKeyword?: boolean
+  noDataText?: string
+  popperClass?: string
+  teleported?: boolean
+  persistent?: boolean
+  popperOptions?: any
+  automaticDropdown?: boolean
+  height?: number
+  scrollbarAlwaysOn?: boolean
+  remote?: boolean
+  remoteMethod?: (query: string) => void
+  validateEvent?: boolean
+  placement?: AutocompleteProps['placement']
+  collapseTagsTooltip?: boolean
+  on?: {
+    change?: (value: string | number | boolean | Object) => void
+    visibleChange?: (visible: boolean) => void
+    removeTag?: (tag: any) => void
+    clear?: () => void
+    blur?: (event: FocusEvent) => void
+    focus?: (event: FocusEvent) => void
+  }
+  options?: SelectOption[]
+  slots?: {
+    default?: (option: SelectOption) => JSX.Element | null
+  }
+  style?: CSSProperties
+}
+
+export interface CascaderComponentProps {
+  options?: Record<string, unknown>[]
+  props?: CascaderProps
+  size?: ComponentSize
+  placeholder?: string
+  disabled?: boolean
+  clearable?: boolean
+  showAllLevels?: boolean
+  collapseTags?: boolean
+  collapseTagsTooltip?: boolean
+  separator?: string
+  filterable?: boolean
+  filterMethod?: (node: CascaderNode, keyword: string) => boolean
+  debounce?: number
+  beforeFilter?: (value: string) => boolean
+  popperClass?: string
+  teleported?: boolean
+  tagType?: ElementPlusInfoType
+  validateEvent?: boolean
+  on?: {
+    change?: (value: CascaderValue) => void
+    expandChange?: (value: CascaderValue) => void
+    blur?: (event: FocusEvent) => void
+    focus?: (event: FocusEvent) => void
+    visibleChange?: (value: boolean) => void
+    removeTag?: (value: CascaderNode['valueByOption']) => void
+  }
+  slots?: {
+    default?: (...args: any[]) => JSX.Element | null
+    empty?: (...args: any[]) => JSX.Element | null
+  }
+  style?: CSSProperties
+}
+
+export interface SwitchComponentProps extends Partial<SwitchProps> {
+  on?: {
+    change?: (value: boolean | string | number) => void
+  }
+  style?: CSSProperties
+}
+
+export interface RateComponentProps extends Partial<RateProps> {
+  on?: {
+    change?: (value: number) => void
+  }
+  style?: CSSProperties
+}
+
+export interface ColorPickerComponentProps extends Partial<ColorPickerProps> {
+  on?: {
+    change?: (value: string) => void
+    activeChange?: (value: string) => void
+  }
+  style?: CSSProperties
+}
+
+export interface TransferComponentProps extends Partial<TransferProps> {
+  on?: {
+    change?: (
+      value: number | string,
+      direction: 'left' | 'right',
+      movedKeys: string[] | number[]
+    ) => void
+    leftCheckChange?: (value: any[]) => void
+    rightCheckChange?: (value: any[]) => void
+  }
+  slots?: {
+    default?: (...args: any[]) => JSX.Element | null
+    leftFooter?: (...args: any[]) => JSX.Element | null
+    rightFooter?: (...args: any[]) => JSX.Element | null
+  }
+  style?: CSSProperties
+}
+
+export interface RadioOption {
+  label?: string
+  value?: string | number | boolean
+  disabled?: boolean
+  border?: boolean
+  size?: ComponentSize
+  name?: string
+  [key: string]: any
+}
+export interface RadioGroupComponentProps extends Partial<RadioGroupProps> {
+  options?: RadioOption[]
+  /**
+   * 数据源的字段别名
+   */
+  props?: {
+    label?: string
+    value?: string
+    disabled?: string
+  }
+  on?: {
+    change?: (value: string | number | boolean) => void
+  }
+  slots?: {
+    default?: (...args: any[]) => JSX.Element[] | null
+  }
+  style?: CSSProperties
+}
+
+export interface RadioButtonComponentProps extends Partial<RadioButtonProps> {
+  options?: RadioOption[]
+  /**
+   * 数据源的字段别名
+   */
+  props?: {
+    label?: string
+    value?: string
+    disabled?: string
+  }
+  on?: {
+    change?: (value: string | number | boolean) => void
+  }
+  slots?: {
+    default?: (...args: any[]) => JSX.Element[] | null
+  }
+  style?: CSSProperties
+}
+
+export interface CheckboxOption {
+  label?: string
+  value?: string | number | boolean
+  disabled?: boolean
+  trueLabel?: string | number
+  falseLabel?: string | number
+  border?: boolean
+  size?: ComponentSize
+  name?: string
+  checked?: boolean
+  indeterminate?: boolean
+  validateEvent?: boolean
+  tabindex?: number | string
+  id?: string
+  controls?: boolean
+  [key: string]: any
+}
+
+export interface CheckboxGroupComponentProps extends Partial<CheckboxGroupProps> {
+  options?: CheckboxOption[]
+  /**
+   * 数据源的字段别名
+   */
+  props?: {
+    label?: string
+    value?: string
+    disabled?: string
+  }
+  on?: {
+    change?: (value: string | number | boolean) => void
+  }
+  slots?: {
+    default?: (...args: any[]) => JSX.Element[] | null
+  }
+  style?: CSSProperties
+}
+
+export interface DividerComponentProps extends Partial<DividerProps> {
+  on?: {
+    change?: (value: number) => void
+    input?: (value: number) => void
+  }
+  style?: CSSProperties
+}
+
+export interface DatePickerComponentProps extends Partial<DatePickerProps> {
+  on?: {
+    change?: (value: string | Date | number | string[]) => void
+    blur?: (event: FocusEvent) => void
+    focus?: (event: FocusEvent) => void
+    calendarChange?: (val: [Date, Date]) => void
+    panelChange?: (date, mode, view) => void
+    visibleChange?: (visibility: boolean) => void
+  }
+  slots?: {
+    default?: (...args: any[]) => JSX.Element | null
+    rangeSeparator?: (...args: any[]) => JSX.Element | null
+  }
+  style?: CSSProperties
+}
+
+export interface DateTimePickerComponentProps {
+  readonly?: boolean
+  disabled?: boolean
+  editable?: boolean
+  clearable?: boolean
+  size?: ComponentSize
+  placeholder?: string
+  startPlaceholder?: string
+  endPlaceholder?: string
+  timeArrowControl?: boolean
+  type?: 'year' | 'month' | 'date' | 'datetime' | 'datetimerange' | 'daterange' | 'week'
+  format?: string
+  popperClass?: string
+  rangeSeparator?: string
+  defaultValue?: Date | [Date, Date]
+  defaultTime?: Date | [Date, Date]
+  valueFormat?: string
+  id?: string
+  name?: string
+  unlinkPanels?: boolean
+  prefixIcon?: string | JSX.Element
+  clearIcon?: string | JSX.Element
+  shortcuts?: Array<{ text: string; value: Date | Function }>
+  disabledDate?: (date: Date) => boolean
+  cellClassName?: string | ((date: Date) => string | undefined)
+  teleported?: boolean
+  on?: {
+    change?: (value: string | Date | number | string[]) => void
+    blur?: (event: FocusEvent) => void
+    focus?: (event: FocusEvent) => void
+    calendarChange?: (val: [Date, Date]) => void
+    visibleChange?: (visibility: boolean) => void
+  }
+  slots?: {
+    default?: (...args: any[]) => JSX.Element | null
+    rangeSeparator?: (...args: any[]) => JSX.Element | null
+  }
+  style?: CSSProperties
+}
+
+export interface TimePickerComponentProps {
+  readonly?: boolean
+  disabled?: boolean
+  editable?: boolean
+  clearable?: boolean
+  size?: ComponentSize
+  placeholder?: string
+  startPlaceholder?: string
+  endPlaceholder?: string
+  isRange?: boolean
+  arrowControl?: boolean
+  popperClass?: string
+  rangeSeparator?: string
+  format?: string
+  defaultValue?: Date | [Date, Date]
+  id?: string
+  name?: string
+  label?: string
+  prefixIcon?: string | JSX.Element
+  clearIcon?: string | JSX.Element
+  disabledHours?: (role: string, comparingDate?: any) => number[]
+  disabledMinutes?: (hour: number, role: string, comparingDate?: any) => number[]
+  disabledSeconds?: (hour: number, minute: number, role: string, comparingDate?: any) => number[]
+  teleported?: boolean
+  tabindex?: number | string
+  on?: {
+    change: (
+      val: number | string | Date | [number, number] | [string, string] | [Date, Date]
+    ) => void
+    blur?: (event: FocusEvent) => void
+    focus?: (event: FocusEvent) => void
+    visibleChange?: (visibility: boolean) => void
+  }
+  style?: CSSProperties
+}
+
+export interface TimeSelectComponentProps {
+  disabled?: boolean
+  editable?: boolean
+  clearable?: boolean
+  size?: ComponentSize
+  placeholder?: string
+  name?: string
+  effect?: string
+  prefixIcon?: string | JSX.Element
+  clearIcon?: string | JSX.Element
+  start?: string
+  end?: string
+  step?: string
+  minTime?: string
+  maxTime?: string
+  format?: string
+  on?: {
+    change?: (val: string) => void
+    blur?: (event: FocusEvent) => void
+    focus?: (event: FocusEvent) => void
+  }
+  style?: CSSProperties
+}
+
+export interface EditorComponentProps {
+  editorConfig?: IEditorConfig
+  style?: CSSProperties
+}
+
+export interface ColProps {
+  span?: number
+  xs?: number
+  sm?: number
+  md?: number
+  lg?: number
+  xl?: number
+  tag?: string
+}
+
+export interface FormSetProps {
+  field: string
+  path: string
+  value: any
+}
+
+export interface FormItemProps extends Partial<ElFormItemProps> {
+  style?: CSSProperties
+  slots?: {
+    default?: (...args: any[]) => JSX.Element | null
+    label?: (...args: any[]) => JSX.Element | null
+    error?: (...args: any[]) => JSX.Element | null
+  }
+}
+
+export interface UploadComponentProps extends Partial<UploadProps> {
+  slots?: {
+    default?: (...args: any[]) => JSX.Element | null
+    trigger?: (...args: any[]) => JSX.Element | null
+    tip?: (...args: any[]) => JSX.Element | null
+    file?: (...args: any[]) => JSX.Element | null
+  }
+  style?: CSSProperties
+}
+
+export interface TreeSelectComponentProps
+  extends Omit<Partial<SelectComponentProps>, 'props' | 'on' | 'slots'> {
+  data?: any[]
+  emptyText?: string
+  nodeKey?: string
+  props?: {
+    children?: string
+    label?: string | ((...args: any[]) => string)
+    disabled?: string | ((...args: any[]) => string)
+    isLeaf?: string | ((...args: any[]) => string)
+    class?: string | ((...args: any[]) => string)
+  }
+  renderAfterExpand?: boolean
+  load?: (...args: any[]) => Promise<any>
+  renderContent?: (...args: any[]) => JSX.Element | null
+  highlightCurrent?: boolean
+  defaultExpandAll?: boolean
+  expandOnClickNode?: boolean
+  checkOnClickNode?: boolean
+  autoExpandParent?: boolean
+  defaultExpandedKeys?: any[]
+  showCheckbox?: boolean
+  checkStrictly?: boolean
+  defaultCheckedKeys?: any[]
+  currentNodeKey?: string | number
+  filterNodeMethod?: (...args: any[]) => boolean
+  accordion?: boolean
+  indent?: number
+  icon?: string | ((...args: any[]) => JSX.Element | null)
+  lazy?: boolean
+  draggable?: boolean
+  allowDrag?: (...args: any[]) => boolean
+  allowDrop?: (...args: any[]) => boolean
+  on?: {
+    change?: (value: string | number | boolean | Object) => void
+    visibleChange?: (visible: boolean) => void
+    removeTag?: (tag: any) => void
+    clear?: () => void
+    blur?: (event: FocusEvent) => void
+    focus?: (event: FocusEvent) => void
+    nodeClick?: (...args: any[]) => void
+    nodeContextMenu?: (...args: any[]) => void
+    checkChange?: (...args: any[]) => void
+    check?: (...args: any[]) => void
+    currentChange?: (...args: any[]) => void
+    nodeExpand?: (...args: any[]) => void
+    nodeCollapse?: (...args: any[]) => void
+    nodeDragStart?: (...args: any[]) => void
+    nodeDragEnter?: (...args: any[]) => void
+    nodeDragLeave?: (...args: any[]) => void
+    nodeDragOver?: (...args: any[]) => void
+    nodeDragEnd?: (...args: any[]) => void
+    nodeDrop?: (...args: any[]) => void
+  }
+  slots?: {
+    default?: (...args: any[]) => JSX.Element | null
+    optionGroupDefault?: (item: SelectOption) => JSX.Element
+    optionDefault?: (option: SelectOption) => JSX.Element | null
+    prefix?: (...args: any[]) => JSX.Element | null
+    empty?: (...args: any[]) => JSX.Element | null
+  }
+  style?: CSSProperties
+}
+
+export interface FormSchema {
+  /**
+   * 唯一标识
+   */
+  field: string
+
+  /**
+   * 标题
+   */
+  label?: string
+
+  /**
+   * col组件属性
+   */
+  colProps?: ColProps
+
+  /**
+   * 表单组件属性,具体可以查看element-plus文档
+   */
+  componentProps?:
+    | InputComponentProps
+    | AutocompleteComponentProps
+    | InputNumberComponentProps
+    | SelectComponentProps
+    | SelectV2ComponentProps
+    | CascaderComponentProps
+    | SwitchComponentProps
+    | RateComponentProps
+    | ColorPickerComponentProps
+    | TransferComponentProps
+    | RadioGroupComponentProps
+    | RadioButtonComponentProps
+    | DividerComponentProps
+    | DatePickerComponentProps
+    | DateTimePickerComponentProps
+    | TimePickerComponentProps
+    | InputPasswordComponentProps
+    | TreeSelectComponentProps
+    | UploadComponentProps
+    | any
+
+  /**
+   * formItem组件属性,具体可以查看element-plus文档
+   */
+  formItemProps?: FormItemProps
+
+  /**
+   * 渲染的组件名称
+   */
+  component?: ComponentName
+
+  /**
+   * 初始值
+   */
+  value?: any
+
+  /**
+   * 是否隐藏,如果为true,会连同值一同删除,类似v-if
+   */
+  remove?: boolean
+
+  /**
+   * 样式隐藏,不会把值一同删掉,类似v-show
+   */
+  hidden?: boolean
+
+  /**
+   * @returns 远程加载下拉项
+   */
+  optionApi?: any
+}
+
+export interface FormProps extends Partial<ElFormProps> {
+  schema?: FormSchema[]
+  isCol?: boolean
+  model?: Recordable
+  autoSetPlaceholder?: boolean
+  isCustom?: boolean
+  [key: string]: any
+}

+ 2 - 0
src/components/Icon/index.ts

@@ -1,3 +1,5 @@
 import Icon from './src/Icon.vue'
 
+export type { IconTypes } from './src/types'
+
 export { Icon }

+ 19 - 38
src/components/Icon/src/Icon.vue

@@ -1,9 +1,9 @@
 <script setup lang="ts">
-import { computed, unref, ref, watch, nextTick } from 'vue'
+import { computed, unref } from 'vue'
 import { ElIcon } from 'element-plus'
 import { propTypes } from '@/utils/propTypes'
-import Iconify from '@purge-icons/generated'
 import { useDesign } from '@/hooks/web/useDesign'
+import { Icon } from '@iconify/vue'
 
 const { getPrefixCls } = useDesign()
 
@@ -15,11 +15,10 @@ const props = defineProps({
   // icon color
   color: propTypes.string,
   // icon size
-  size: propTypes.number.def(16)
+  size: propTypes.number.def(16),
+  hoverColor: propTypes.string
 })
 
-const elRef = ref<ElRef>(null)
-
 const isLocal = computed(() => props.icon.startsWith('svg-icon:'))
 
 const symbolId = computed(() => {
@@ -33,36 +32,6 @@ const getIconifyStyle = computed(() => {
     color
   }
 })
-
-const updateIcon = async (icon: string) => {
-  if (unref(isLocal)) return
-
-  const el = unref(elRef)
-  if (!el) return
-
-  await nextTick()
-
-  if (!icon) return
-
-  const svg = Iconify.renderSVG(icon, {})
-  if (svg) {
-    el.textContent = ''
-    el.appendChild(svg)
-  } else {
-    const span = document.createElement('span')
-    span.className = 'iconify'
-    span.dataset.icon = icon
-    el.textContent = ''
-    el.appendChild(span)
-  }
-}
-
-watch(
-  () => props.icon,
-  (icon: string) => {
-    updateIcon(icon)
-  }
-)
 </script>
 
 <template>
@@ -71,8 +40,20 @@ watch(
       <use :xlink:href="symbolId" />
     </svg>
 
-    <span v-else ref="elRef" :class="$attrs.class" :style="getIconifyStyle">
-      <span class="iconify" :data-icon="symbolId"></span>
-    </span>
+    <Icon v-else :icon="icon" :style="getIconifyStyle" />
   </ElIcon>
 </template>
+
+<style lang="less" scoped>
+@prefix-cls: ~'@{namespace}-icon';
+
+.@{prefix-cls},
+.iconify {
+  &:hover {
+    :deep(svg) {
+      // stylelint-disable-next-line
+      color: v-bind(hoverColor) !important;
+    }
+  }
+}
+</style>

+ 1 - 0
src/types/icon.d.ts → src/components/Icon/src/types/index.ts

@@ -2,4 +2,5 @@ export interface IconTypes {
   size?: number
   color?: string
   icon: string
+  hoverColor?: string
 }

+ 2 - 2
src/components/ImageViewer/index.ts

@@ -12,7 +12,7 @@ export function createImageViewer(options: ImageViewerProps) {
     initialIndex = 0,
     infinite = true,
     hideOnClickModal = false,
-    appendToBody = false,
+    teleported = false,
     zIndex = 2000,
     show = true
   } = options
@@ -23,7 +23,7 @@ export function createImageViewer(options: ImageViewerProps) {
   propsData.initialIndex = initialIndex
   propsData.infinite = infinite
   propsData.hideOnClickModal = hideOnClickModal
-  propsData.appendToBody = appendToBody
+  propsData.teleported = teleported
   propsData.zIndex = zIndex
   propsData.show = show
 

+ 1 - 1
src/components/ImageViewer/src/ImageViewer.vue

@@ -12,7 +12,7 @@ const props = defineProps({
   initialIndex: propTypes.number.def(0),
   infinite: propTypes.bool.def(true),
   hideOnClickModal: propTypes.bool.def(false),
-  appendToBody: propTypes.bool.def(false),
+  teleported: propTypes.bool.def(false),
   show: propTypes.bool.def(false)
 })
 

+ 1 - 1
src/components/ImageViewer/src/types.ts → src/components/ImageViewer/src/types/index.ts

@@ -4,6 +4,6 @@ export interface ImageViewerProps {
   initialIndex?: number
   infinite?: boolean
   hideOnClickModal?: boolean
-  appendToBody?: boolean
+  teleported?: boolean
   show?: boolean
 }

+ 2 - 0
src/components/Infotip/index.ts

@@ -1,3 +1,5 @@
 import Infotip from './src/Infotip.vue'
 
+export type { InfoTipSchema } from './src/types'
+
 export { Infotip }

+ 2 - 2
src/components/Infotip/src/Infotip.vue

@@ -3,7 +3,7 @@ import { PropType } from 'vue'
 import { Highlight } from '@/components/Highlight'
 import { useDesign } from '@/hooks/web/useDesign'
 import { propTypes } from '@/utils/propTypes'
-import { TipSchema } from '@/types/infoTip'
+import { InfoTipSchema } from './types'
 
 const { getPrefixCls } = useDesign()
 
@@ -12,7 +12,7 @@ const prefixCls = getPrefixCls('infotip')
 defineProps({
   title: propTypes.string.def(''),
   schema: {
-    type: Array as PropType<Array<string | TipSchema>>,
+    type: Array as PropType<Array<string | InfoTipSchema>>,
     required: true,
     default: () => []
   },

+ 1 - 1
src/types/infoTip.d.ts → src/components/Infotip/src/types/index.ts

@@ -1,4 +1,4 @@
-export interface TipSchema {
+export interface InfoTipSchema {
   label: string
   keys?: string[]
 }

+ 4 - 14
src/components/InputPassword/src/InputPassword.vue

@@ -32,10 +32,6 @@ const emit = defineEmits(['update:modelValue'])
 // 设置input的type属性
 const textType = ref<'password' | 'text'>('password')
 
-const changeTextType = () => {
-  textType.value = unref(textType) === 'text' ? 'password' : 'text'
-}
-
 // 输入框的值
 const valueRef = ref(props.modelValue)
 
@@ -53,19 +49,11 @@ const getPasswordStrength = computed(() => {
   const zxcvbnRef = zxcvbn(unref(valueRef)) as ZxcvbnResult
   return value ? zxcvbnRef.score : -1
 })
-
-const getIconName = computed(() =>
-  unref(textType) === 'password' ? 'ant-design:eye-invisible-outlined' : 'ant-design:eye-outlined'
-)
 </script>
 
 <template>
   <div :class="[prefixCls, `${prefixCls}--${configGlobal?.size}`]">
-    <ElInput v-bind="$attrs" v-model="valueRef" :type="textType">
-      <template #suffix>
-        <Icon class="el-input__icon cursor-pointer" :icon="getIconName" @click="changeTextType" />
-      </template>
-    </ElInput>
+    <ElInput v-bind="$attrs" v-model="valueRef" showPassword :type="textType" />
     <div
       v-if="strength"
       :class="`${prefixCls}__bar`"
@@ -116,7 +104,9 @@ const getIconName = computed(() =>
       height: inherit;
       background-color: transparent;
       border-radius: inherit;
-      transition: width 0.5s ease-in-out, background 0.25s;
+      transition:
+        width 0.5s ease-in-out,
+        background 0.25s;
 
       &[data-score='0'] {
         width: 20%;

+ 2 - 0
src/components/LocaleDropdown/index.ts

@@ -1,3 +1,5 @@
 import LocaleDropdown from './src/LocaleDropdown.vue'
 
+export type { Language, LocaleDropdownType } from './src/types'
+
 export { LocaleDropdown }

+ 1 - 1
src/components/LocaleDropdown/src/LocaleDropdown.vue

@@ -37,7 +37,7 @@ const setLang = (lang: LocaleType) => {
     <Icon
       :size="18"
       icon="ion:language-sharp"
-      class="cursor-pointer"
+      class="cursor-pointer !p-0"
       :class="$attrs.class"
       :color="color"
     />

+ 0 - 0
src/types/localeDropdown.d.ts → src/components/LocaleDropdown/src/types/index.ts


+ 1 - 2
src/components/Logo/src/Logo.vue

@@ -60,8 +60,7 @@ watch(
       :class="[
         prefixCls,
         layout !== 'classic' ? `${prefixCls}__Top` : '',
-        'flex !h-[var(--logo-height)]  items-center justify-center cursor-pointer relative',
-        'dark:bg-[var(--el-bg-color)]'
+        'flex !h-[var(--logo-height)] items-center cursor-pointer pl-8px relative decoration-none overflow-hidden'
       ]"
       to="/"
     >

+ 36 - 36
src/components/Menu/src/Menu.vue

@@ -7,7 +7,6 @@ import { useRenderMenuItem } from './components/useRenderMenuItem'
 import { useRouter } from 'vue-router'
 import { isUrl } from '@/utils/is'
 import { useDesign } from '@/hooks/web/useDesign'
-import { LayoutType } from '@/types/layout'
 
 const { getPrefixCls } = useDesign()
 
@@ -124,28 +123,29 @@ export default defineComponent({
 <style lang="less" scoped>
 @prefix-cls: ~'@{namespace}-menu';
 
-.is-active--after {
-  position: absolute;
-  top: 0;
-  right: 0;
-  width: 4px;
-  height: 100%;
-  background-color: var(--el-color-primary);
-  content: '';
-}
+// .is-active--after {
+//   position: absolute;
+//   top: 0;
+//   right: 0;
+//   width: 4px;
+//   height: 100%;
+//   background-color: var(--el-color-primary);
+//   content: '';
+// }
 
 .@{prefix-cls} {
   position: relative;
   transition: width var(--transition-time-02);
 
-  &:after {
-    position: absolute;
-    top: 0;
-    right: 0;
-    height: 100%;
-    border-left: 1px solid var(--left-menu-border-color);
-    content: '';
-  }
+  // &:after {
+  //   position: absolute;
+  //   top: 0;
+  //   right: 0;
+  //   height: 100%;
+  //   width: 1px;
+  //   background-color: var(--el-border-color);
+  //   content: '';
+  // }
 
   :deep(.@{elNamespace}-menu) {
     width: 100% !important;
@@ -181,9 +181,9 @@ export default defineComponent({
     .@{elNamespace}-menu-item.is-active {
       position: relative;
 
-      &:after {
-        .is-active--after;
-      }
+      // &:after {
+      //   .is-active--after;
+      // }
     }
 
     // 设置子菜单的背景颜色
@@ -204,9 +204,9 @@ export default defineComponent({
       position: relative;
       background-color: var(--left-menu-collapse-bg-active-color) !important;
 
-      &:after {
-        .is-active--after;
-      }
+      // &:after {
+      //   .is-active--after;
+      // }
     }
   }
 
@@ -254,15 +254,15 @@ export default defineComponent({
 <style lang="less">
 @prefix-cls: ~'@{namespace}-menu-popper';
 
-.is-active--after {
-  position: absolute;
-  top: 0;
-  right: 0;
-  width: 4px;
-  height: 100%;
-  background-color: var(--el-color-primary);
-  content: '';
-}
+// .is-active--after {
+//   position: absolute;
+//   top: 0;
+//   right: 0;
+//   width: 4px;
+//   height: 100%;
+//   background-color: var(--el-color-primary);
+//   content: '';
+// }
 
 .@{prefix-cls}--vertical,
 .@{prefix-cls}--horizontal {
@@ -291,9 +291,9 @@ export default defineComponent({
       background-color: var(--left-menu-bg-active-color) !important;
     }
 
-    &:after {
-      .is-active--after;
-    }
+    // &:after {
+    //   .is-active--after;
+    // }
   }
 }
 </style>

+ 4 - 0
src/components/Permission/index.ts

@@ -0,0 +1,4 @@
+import Permission from './src/Permission.vue'
+import { hasPermi } from './src/utils'
+
+export { Permission, hasPermi }

+ 29 - 0
src/components/Permission/src/Permission.vue

@@ -0,0 +1,29 @@
+<script setup lang="ts">
+import { propTypes } from '@/utils/propTypes'
+import { computed, unref } from 'vue'
+import { useRouter } from 'vue-router'
+
+const { currentRoute } = useRouter()
+
+const props = defineProps({
+  permission: propTypes.string.def()
+})
+
+const currentPermission = computed(() => {
+  return unref(currentRoute)?.meta?.permission || []
+})
+
+const hasPermission = computed(() => {
+  const permission = unref(props.permission)
+  if (!permission) {
+    return true
+  }
+  return unref(currentPermission).includes(permission)
+})
+</script>
+
+<template>
+  <template v-if="hasPermission">
+    <slot></slot>
+  </template>
+</template>

+ 14 - 0
src/components/Permission/src/utils.ts

@@ -0,0 +1,14 @@
+import { useI18n } from '@/hooks/web/useI18n'
+import router from '@/router'
+
+export const hasPermi = (value: string) => {
+  const { t } = useI18n()
+  const permission = (router.currentRoute.value.meta.permission || []) as string[]
+  if (!value) {
+    throw new Error(t('permission.hasPermission'))
+  }
+  if (permission.includes(value)) {
+    return true
+  }
+  return false
+}

+ 2 - 0
src/components/Qrcode/index.ts

@@ -1,3 +1,5 @@
 import Qrcode from './src/Qrcode.vue'
 
+export type { QrcodeLogo } from './src/types'
+
 export { Qrcode }

+ 1 - 1
src/components/Qrcode/src/Qrcode.vue

@@ -6,7 +6,7 @@ import { cloneDeep } from 'lodash-es'
 import { propTypes } from '@/utils/propTypes'
 import { useDesign } from '@/hooks/web/useDesign'
 import { isString } from '@/utils/is'
-import { QrcodeLogo } from '@/types/qrcode'
+import { QrcodeLogo } from '@/components/Qrcode'
 
 const props = defineProps({
   // img 或者 canvas,img不支持logo嵌套

+ 0 - 0
src/types/qrcode.d.ts → src/components/Qrcode/src/types/index.ts


+ 12 - 0
src/components/Search/index.ts

@@ -1,3 +1,15 @@
+import { FormSchema, FormSetProps } from '../Form'
 import Search from './src/Search.vue'
 
+export type { SearchProps } from './src/types'
+
+export interface SearchExpose {
+  setValues: (data: Recordable) => void
+  setProps: (props: Recordable) => void
+  delSchema: (field: string) => void
+  addSchema: (formSchema: FormSchema, index?: number) => void
+  setSchema: (schemaProps: FormSetProps[]) => void
+  formModel: Recordable
+}
+
 export { Search }

+ 186 - 69
src/components/Search/src/Search.vue

@@ -1,15 +1,15 @@
-<script setup lang="ts">
-import { Form } from '@/components/Form'
-import { PropType, computed, unref, ref } from 'vue'
+<script setup lang="tsx">
+import { Form, FormSchema, FormSetProps } from '@/components/Form'
+import { PropType, computed, unref, ref, watch, onMounted } from 'vue'
 import { propTypes } from '@/utils/propTypes'
-import { ElButton } from 'element-plus'
-import { useI18n } from '@/hooks/web/useI18n'
 import { useForm } from '@/hooks/web/useForm'
 import { findIndex } from '@/utils'
-import { cloneDeep } from 'lodash-es'
-import { FormSchema } from '@/types/form'
-
-const { t } = useI18n()
+import { cloneDeep, set } from 'lodash-es'
+import { initModel } from '@/components/Form/src/helper'
+import ActionButton from './components/ActionButton.vue'
+import { SearchProps } from './types'
+import { FormItemProp } from 'element-plus'
+import { isObject, isEmptyVal } from '@/utils/is'
 
 const props = defineProps({
   // 生成Form的布局结构数组
@@ -24,41 +24,72 @@ const props = defineProps({
   // 操作按钮风格位置
   layout: propTypes.string.validate((v: string) => ['inline', 'bottom'].includes(v)).def('inline'),
   // 底部按钮的对齐方式
-  buttomPosition: propTypes.string
+  buttonPosition: propTypes.string
     .validate((v: string) => ['left', 'center', 'right'].includes(v))
     .def('center'),
   showSearch: propTypes.bool.def(true),
   showReset: propTypes.bool.def(true),
   // 是否显示伸缩
-  expand: propTypes.bool.def(false),
+  showExpand: propTypes.bool.def(false),
   // 伸缩的界限字段
   expandField: propTypes.string.def(''),
   inline: propTypes.bool.def(true),
+  // 是否去除空值项
+  removeNoValueItem: propTypes.bool.def(true),
   model: {
     type: Object as PropType<Recordable>,
     default: () => ({})
-  }
+  },
+  searchLoading: propTypes.bool.def(false),
+  resetLoading: propTypes.bool.def(false)
 })
 
-const emit = defineEmits(['search', 'reset'])
+const emit = defineEmits(['search', 'reset', 'register', 'validate'])
 
 const visible = ref(true)
 
+// 表单数据
+const formModel = ref<Recordable>(props.model)
+
 const newSchema = computed(() => {
-  let schema: FormSchema[] = cloneDeep(props.schema)
-  if (props.expand && props.expandField && !unref(visible)) {
-    const index = findIndex(schema, (v: FormSchema) => v.field === props.expandField)
-    if (index > -1) {
-      const length = schema.length
-      schema.splice(index + 1, length)
-    }
+  const propsComputed = unref(getProps)
+  let schema: FormSchema[] = cloneDeep(propsComputed.schema)
+  if (propsComputed.showExpand && propsComputed.expandField && !unref(visible)) {
+    const index = findIndex(schema, (v: FormSchema) => v.field === propsComputed.expandField)
+    schema.map((v, i) => {
+      if (i >= index) {
+        v.hidden = true
+      } else {
+        v.hidden = false
+      }
+      return v
+    })
   }
-  if (props.layout === 'inline') {
+  if (propsComputed.layout === 'inline') {
     schema = schema.concat([
       {
         field: 'action',
         formItemProps: {
-          labelWidth: '0px'
+          labelWidth: '0px',
+          slots: {
+            default: () => {
+              return (
+                <div>
+                  <ActionButton
+                    showSearch={propsComputed.showSearch}
+                    showReset={propsComputed.showReset}
+                    showExpand={propsComputed.showExpand}
+                    searchLoading={propsComputed.searchLoading}
+                    resetLoading={propsComputed.resetLoading}
+                    visible={visible.value}
+                    onExpand={setVisible}
+                    onReset={reset}
+                    onSearch={search}
+                  />
+                </div>
+              )
+            }
+          }
         }
       }
     ])
@@ -66,81 +97,167 @@ const newSchema = computed(() => {
   return schema
 })
 
-const { register, elFormRef, methods } = useForm({
-  model: props.model || {}
+const { formRegister, formMethods } = useForm()
+const { getElFormExpose, getFormData, getFormExpose } = formMethods
+
+// useSearch传入的props
+const outsideProps = ref<SearchProps>({})
+
+const mergeProps = ref<SearchProps>({})
+
+const getProps = computed(() => {
+  const propsObj = { ...props }
+  Object.assign(propsObj, unref(mergeProps))
+  return propsObj
 })
 
+const setProps = (props: SearchProps = {}) => {
+  mergeProps.value = Object.assign(unref(mergeProps), props)
+  // @ts-ignore
+  outsideProps.value = props
+}
+
+// 监听表单结构化数组,重新生成formModel
+watch(
+  () => unref(newSchema),
+  async (schema = []) => {
+    formModel.value = initModel(schema, unref(formModel))
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
+
+const filterModel = async () => {
+  const model = await getFormData()
+  if (unref(getProps).removeNoValueItem) {
+    // 使用reduce过滤空值,并返回一个新对象
+    return Object.keys(model).reduce((prev, next) => {
+      const value = model[next]
+      if (!isEmptyVal(value)) {
+        if (isObject(value)) {
+          if (Object.keys(value).length > 0) {
+            prev[next] = value
+          }
+        } else {
+          prev[next] = value
+        }
+      }
+      return prev
+    }, {})
+  }
+  return model
+}
+
 const search = async () => {
-  await unref(elFormRef)?.validate(async (isValid) => {
+  const elFormExpose = await getElFormExpose()
+  await elFormExpose?.validate(async (isValid) => {
     if (isValid) {
-      const { getFormData } = methods
-      const model = await getFormData()
+      const model = await filterModel()
       emit('search', model)
     }
   })
 }
 
 const reset = async () => {
-  unref(elFormRef)?.resetFields()
-  const { getFormData } = methods
-  const model = await getFormData()
+  const elFormExpose = await getElFormExpose()
+  elFormExpose?.resetFields()
+  const model = await filterModel()
   emit('reset', model)
 }
 
-const bottonButtonStyle = computed(() => {
+const bottomButtonStyle = computed(() => {
   return {
-    textAlign: props.buttomPosition as unknown as 'left' | 'center' | 'right'
+    textAlign: unref(getProps).buttonPosition as unknown as 'left' | 'center' | 'right'
   }
 })
 
-const setVisible = () => {
-  unref(elFormRef)?.resetFields()
+const setVisible = async () => {
   visible.value = !unref(visible)
 }
+
+const setSchema = (schemaProps: FormSetProps[]) => {
+  const { schema } = unref(getProps)
+  for (const v of schema) {
+    for (const item of schemaProps) {
+      if (v.field === item.field) {
+        set(v, item.path, item.value)
+      }
+    }
+  }
+}
+
+// 对表单赋值
+const setValues = async (data: Recordable = {}) => {
+  formModel.value = Object.assign(props.model, unref(formModel), data)
+  const formExpose = await getFormExpose()
+  formExpose?.setValues(data)
+}
+
+const delSchema = (field: string) => {
+  const { schema } = unref(getProps)
+
+  const index = findIndex(schema, (v: FormSchema) => v.field === field)
+  if (index > -1) {
+    schema.splice(index, 1)
+  }
+}
+
+const addSchema = (formSchema: FormSchema, index?: number) => {
+  const { schema } = unref(getProps)
+  if (index !== void 0) {
+    schema.splice(index, 0, formSchema)
+    return
+  }
+  schema.push(formSchema)
+}
+
+const defaultExpose = {
+  getElFormExpose,
+  setProps,
+  setSchema,
+  setValues,
+  delSchema,
+  addSchema
+}
+
+onMounted(() => {
+  emit('register', defaultExpose)
+})
+
+defineExpose(defaultExpose)
+
+const onFormValidate = (prop: FormItemProp, isValid: boolean, message: string) => {
+  emit('validate', prop, isValid, message)
+}
 </script>
 
 <template>
   <Form
+    :model="formModel"
     :is-custom="false"
-    :label-width="labelWidth"
+    :label-width="getProps.labelWidth"
     hide-required-asterisk
-    :inline="inline"
-    :is-col="isCol"
+    :inline="getProps.inline"
+    :is-col="getProps.isCol"
     :schema="newSchema"
-    @register="register"
-  >
-    <template #action>
-      <div v-if="layout === 'inline'">
-        <ElButton v-if="showSearch" type="primary" @click="search">
-          <Icon icon="ep:search" class="mr-5px" />
-          {{ t('common.query') }}
-        </ElButton>
-        <ElButton v-if="showReset" @click="reset">
-          <Icon icon="ep:refresh-right" class="mr-5px" />
-          {{ t('common.reset') }}
-        </ElButton>
-        <ElButton v-if="expand" text @click="setVisible">
-          {{ t(visible ? 'common.shrink' : 'common.expand') }}
-          <Icon :icon="visible ? 'ant-design:up-outlined' : 'ant-design:down-outlined'" />
-        </ElButton>
-      </div>
-    </template>
-  </Form>
+    @register="formRegister"
+    @validate="onFormValidate"
+  />
 
   <template v-if="layout === 'bottom'">
-    <div :style="bottonButtonStyle">
-      <ElButton v-if="showSearch" type="primary" @click="search">
-        <Icon icon="ep:search" class="mr-5px" />
-        {{ t('common.query') }}
-      </ElButton>
-      <ElButton v-if="showReset" @click="reset">
-        <Icon icon="ep:refresh-right" class="mr-5px" />
-        {{ t('common.reset') }}
-      </ElButton>
-      <ElButton v-if="expand" text @click="setVisible">
-        {{ t(visible ? 'common.shrink' : 'common.expand') }}
-        <Icon :icon="visible ? 'ant-design:up-outlined' : 'ant-design:down-outlined'" />
-      </ElButton>
+    <div :style="bottomButtonStyle">
+      <ActionButton
+        :show-reset="getProps.showReset"
+        :show-search="getProps.showSearch"
+        :show-expand="getProps.showExpand"
+        :search-loading="getProps.searchLoading"
+        :reset-loading="getProps.resetLoading"
+        @expand="setVisible"
+        @reset="reset"
+        @search="search"
+      />
     </div>
   </template>
 </template>

+ 59 - 0
src/components/Search/src/components/ActionButton.vue

@@ -0,0 +1,59 @@
+<script setup lang="ts">
+import { ElButton } from 'element-plus'
+import { useIcon } from '@/hooks/web/useIcon'
+import { propTypes } from '@/utils/propTypes'
+import { useI18n } from '@/hooks/web/useI18n'
+
+const emit = defineEmits(['search', 'reset', 'expand'])
+
+const { t } = useI18n()
+
+defineProps({
+  showSearch: propTypes.bool.def(true),
+  showReset: propTypes.bool.def(true),
+  showExpand: propTypes.bool.def(false),
+  visible: propTypes.bool.def(true),
+  searchLoading: propTypes.bool.def(false),
+  resetLoading: propTypes.bool.def(false)
+})
+
+const onSearch = () => {
+  emit('search')
+}
+
+const onReset = () => {
+  emit('reset')
+}
+
+const onExpand = () => {
+  emit('expand')
+}
+</script>
+
+<template>
+  <ElButton
+    v-if="showSearch"
+    type="primary"
+    :loading="searchLoading"
+    :icon="useIcon({ icon: 'ep:search' })"
+    @click="onSearch"
+  >
+    {{ t('common.query') }}
+  </ElButton>
+  <ElButton
+    v-if="showReset"
+    :loading="resetLoading"
+    :icon="useIcon({ icon: 'ep:refresh-right' })"
+    @click="onReset"
+  >
+    {{ t('common.reset') }}
+  </ElButton>
+  <ElButton
+    v-if="showExpand"
+    :icon="useIcon({ icon: visible ? 'ep:arrow-down' : 'ep:arrow-up' })"
+    text
+    @click="onExpand"
+  >
+    {{ t(visible ? 'common.shrink' : 'common.expand') }}
+  </ElButton>
+</template>

+ 16 - 0
src/components/Search/src/types/index.ts

@@ -0,0 +1,16 @@
+import { FormSchema } from '@/components/Form'
+
+export interface SearchProps {
+  schema?: FormSchema[]
+  isCol?: boolean
+  labelWidth?: string | number
+  layout?: 'inline' | 'bottom'
+  buttonPosition?: 'left' | 'right' | 'center'
+  showSearch?: boolean
+  showReset?: boolean
+  showExpand?: boolean
+  expandField?: string
+  inline?: boolean
+  removeNoValueItem?: boolean
+  model?: Recordable
+}

+ 7 - 11
src/components/Setting/src/Setting.vue

@@ -10,10 +10,12 @@ import { trim, setCssVar } from '@/utils'
 import ColorRadioPicker from './components/ColorRadioPicker.vue'
 import InterfaceDisplay from './components/InterfaceDisplay.vue'
 import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
-import { useCache } from '@/hooks/web/useCache'
+import { useStorage } from '@/hooks/web/useStorage'
 import { useClipboard } from '@vueuse/core'
 import { useDesign } from '@/hooks/web/useDesign'
 
+const { removeStorage } = useStorage()
+
 const { getPrefixCls } = useDesign()
 
 const prefixCls = getPrefixCls('setting')
@@ -47,7 +49,6 @@ const setHeaderTheme = (color: string) => {
   setCssVar('--top-header-bg-color', color)
   setCssVar('--top-header-text-color', textColor)
   setCssVar('--top-header-hover-color', textHoverColor)
-  setCssVar('--top-tool-border-color', topToolBorderColor)
   appStore.setTheme({
     topHeaderBgColor: color,
     topHeaderTextColor: textColor,
@@ -92,10 +93,6 @@ const setMenuTheme = (color: string) => {
   appStore.setTheme(theme)
   appStore.setCssVarTheme()
 }
-if (layout.value === 'top' && !appStore.getIsDark) {
-  headerTheme.value = '#fff'
-  setHeaderTheme('#fff')
-}
 
 // 监听layout变化,重置一些主题色
 watch(
@@ -191,10 +188,9 @@ const copyConfig = async () => {
 
 // 清空缓存
 const clear = () => {
-  const { wsCache } = useCache()
-  wsCache.delete('layout')
-  wsCache.delete('theme')
-  wsCache.delete('isDark')
+  removeStorage('layout')
+  removeStorage('theme')
+  removeStorage('isDark')
   window.location.reload()
 }
 </script>
@@ -202,7 +198,7 @@ const clear = () => {
 <template>
   <div
     :class="prefixCls"
-    class="fixed top-[45%] right-0 w-40px h-40px text-center leading-40px bg-[var(--el-color-primary)] cursor-pointer"
+    class="fixed top-[45%] right-0 w-40px h-40px flex items-center justify-center bg-[var(--el-color-primary)] cursor-pointer z-10"
     @click="drawer = true"
   >
     <Icon icon="ant-design:setting-outlined" color="#fff" />

+ 2 - 3
src/components/SizeDropdown/src/SizeDropdown.vue

@@ -1,11 +1,10 @@
 <script setup lang="ts">
 import { computed } from 'vue'
-import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
+import { ElDropdown, ElDropdownMenu, ElDropdownItem, ComponentSize } from 'element-plus'
 import { useAppStore } from '@/store/modules/app'
 import { useI18n } from '@/hooks/web/useI18n'
 import { propTypes } from '@/utils/propTypes'
 import { useDesign } from '@/hooks/web/useDesign'
-import { ElementPlusSize } from '@/types/elementPlus'
 
 const { getPrefixCls } = useDesign()
 
@@ -21,7 +20,7 @@ const appStore = useAppStore()
 
 const sizeMap = computed(() => appStore.sizeMap)
 
-const setCurrentSize = (size: ElementPlusSize) => {
+const setCurrentSize = (size: ComponentSize) => {
   appStore.setCurrentSize(size)
 }
 </script>

+ 0 - 3
src/components/Sticky/index.ts

@@ -1,3 +0,0 @@
-import Sticky from './src/Sticky.vue'
-
-export { Sticky }

+ 0 - 141
src/components/Sticky/src/Sticky.vue

@@ -1,141 +0,0 @@
-<script setup lang="ts">
-import { propTypes } from '@/utils/propTypes'
-import { ref, onMounted, onActivated, shallowRef } from 'vue'
-import { useEventListener, useWindowSize, isClient } from '@vueuse/core'
-import type { CSSProperties } from 'vue'
-const props = defineProps({
-  // 距离顶部或者底部的距离(单位px)
-  offset: propTypes.number.def(0),
-  // 设置元素的堆叠顺序
-  zIndex: propTypes.number.def(999),
-  // 设置指定的class
-  className: propTypes.string.def(''),
-  // 定位方式,默认为(top),表示距离顶部位置,可以设置为top或者bottom
-  position: {
-    type: String,
-    validator: function (value: string) {
-      return ['top', 'bottom'].indexOf(value) !== -1
-    },
-    default: 'top'
-  }
-})
-const width = ref('auto' as string)
-const height = ref('auto' as string)
-const isSticky = ref(false)
-const refSticky = shallowRef<HTMLElement>()
-const scrollContainer = shallowRef<HTMLElement | Window>()
-const { height: windowHeight } = useWindowSize()
-onMounted(() => {
-  height.value = refSticky.value?.getBoundingClientRect().height + 'px'
-
-  scrollContainer.value = getScrollContainer(refSticky.value!, true)
-  useEventListener(scrollContainer, 'scroll', handleScroll)
-  useEventListener('resize', handleReize)
-  handleScroll()
-})
-onActivated(() => {
-  handleScroll()
-})
-
-const camelize = (str: string): string => {
-  return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
-}
-
-const getStyle = (element: HTMLElement, styleName: keyof CSSProperties): string => {
-  if (!isClient || !element || !styleName) return ''
-
-  let key = camelize(styleName)
-  if (key === 'float') key = 'cssFloat'
-  try {
-    const style = element.style[styleName]
-    if (style) return style
-    const computed = document.defaultView?.getComputedStyle(element, '')
-    return computed ? computed[styleName] : ''
-  } catch {
-    return element.style[styleName]
-  }
-}
-const isScroll = (el: HTMLElement, isVertical?: boolean): boolean => {
-  if (!isClient) return false
-  const key = (
-    {
-      undefined: 'overflow',
-      true: 'overflow-y',
-      false: 'overflow-x'
-    } as const
-  )[String(isVertical)]!
-  const overflow = getStyle(el, key)
-  return ['scroll', 'auto', 'overlay'].some((s) => overflow.includes(s))
-}
-
-const getScrollContainer = (
-  el: HTMLElement,
-  isVertical: boolean
-): Window | HTMLElement | undefined => {
-  if (!isClient) return
-  let parent = el
-  while (parent) {
-    if ([window, document, document.documentElement].includes(parent)) return window
-    if (isScroll(parent, isVertical)) return parent
-    parent = parent.parentNode as HTMLElement
-  }
-  return parent
-}
-
-const handleScroll = () => {
-  width.value = refSticky.value!.getBoundingClientRect().width! + 'px'
-  if (props.position === 'top') {
-    const offsetTop = refSticky.value?.getBoundingClientRect().top
-    if (offsetTop !== undefined && offsetTop < props.offset) {
-      sticky()
-      return
-    }
-    reset()
-  } else {
-    const offsetBottom = refSticky.value?.getBoundingClientRect().bottom
-
-    if (offsetBottom !== undefined && offsetBottom > windowHeight.value - props.offset) {
-      sticky()
-      return
-    }
-    reset()
-  }
-}
-const handleReize = () => {
-  if (isSticky.value && refSticky.value) {
-    width.value = refSticky.value.getBoundingClientRect().width + 'px'
-  }
-}
-const sticky = () => {
-  if (isSticky.value) {
-    return
-  }
-  isSticky.value = true
-}
-const reset = () => {
-  if (!isSticky.value) {
-    return
-  }
-  width.value = 'auto'
-  isSticky.value = false
-}
-</script>
-<template>
-  <div :style="{ height: height, zIndex: zIndex }" ref="refSticky">
-    <div
-      :class="className"
-      :style="{
-        top: position === 'top' ? offset + 'px' : '',
-        bottom: position !== 'top' ? offset + 'px' : '',
-        zIndex: zIndex,
-        position: isSticky ? 'fixed' : 'static',
-        width: width,
-        height: height
-      }"
-    >
-      <slot>
-        <div>sticky</div>
-      </slot>
-    </div>
-  </div>
-</template>

+ 2 - 13
src/components/TabMenu/src/TabMenu.vue

@@ -141,7 +141,7 @@ export default defineComponent({
         id={`${variables.namespace}-menu`}
         class={[
           prefixCls,
-          'relative bg-[var(--left-menu-bg-color)] top-1px z-3000',
+          'relative bg-[var(--left-menu-bg-color)] top-1px z-3000 layout-border__right',
           {
             'w-[var(--tab-menu-max-width)]': !unref(collapse),
             'w-[var(--tab-menu-min-width)]': unref(collapse)
@@ -197,7 +197,7 @@ export default defineComponent({
         </div>
         <Menu
           class={[
-            '!absolute top-0 border-left-1 border-solid border-[var(--left-menu-bg-light-color)]',
+            '!absolute top-0',
             {
               '!left-[var(--tab-menu-min-width)]': unref(collapse),
               '!left-[var(--tab-menu-max-width)]': !unref(collapse),
@@ -219,16 +219,6 @@ export default defineComponent({
 .@{prefix-cls} {
   transition: all var(--transition-time-02);
 
-  &:after {
-    position: absolute;
-    top: 0;
-    right: 0;
-    width: 1px;
-    height: 100%;
-    border-left: 1px solid var(--left-menu-border-color);
-    content: '';
-  }
-
   &__item {
     color: var(--left-menu-text-color);
     transition: all var(--transition-time-02);
@@ -242,7 +232,6 @@ export default defineComponent({
   &--collapse {
     color: var(--left-menu-text-color);
     background-color: var(--left-menu-bg-light-color);
-    border-top: 1px solid var(--left-menu-border-color);
   }
 
   .is-active {

+ 12 - 3
src/components/Table/index.ts

@@ -1,11 +1,20 @@
 import Table from './src/Table.vue'
 import { ElTable } from 'element-plus'
-import { TableSetPropsType } from '@/types/table'
+import { TableColumn, TableSetProps } from './src/types'
+
+export type {
+  TableColumn,
+  TableSlotDefault,
+  Pagination,
+  TableSetProps,
+  TableProps
+} from './src/types'
 
 export interface TableExpose {
   setProps: (props: Recordable) => void
-  setColumn: (columnProps: TableSetPropsType[]) => void
-  selections: Recordable[]
+  setColumn: (columnProps: TableSetProps[]) => void
+  addColumn: (column: TableColumn, index?: number) => void
+  delColumn: (field: string) => void
   elTableRef: ComponentRef<typeof ElTable>
 }
 

+ 371 - 129
src/components/Table/src/Table.vue

@@ -1,20 +1,30 @@
 <script lang="tsx">
-import { ElTable, ElTableColumn, ElPagination } from 'element-plus'
+import {
+  ElTable,
+  ElTableColumn,
+  ElPagination,
+  ComponentSize,
+  ElTooltipProps,
+  ElImage
+} from 'element-plus'
 import { defineComponent, PropType, ref, computed, unref, watch, onMounted } from 'vue'
 import { propTypes } from '@/utils/propTypes'
 import { setIndex } from './helper'
+import type { TableProps, TableColumn, Pagination, TableSetProps } from './types'
+import { set, get } from 'lodash-es'
+import { CSSProperties } from 'vue'
 import { getSlot } from '@/utils/tsxHelper'
-import type { TableProps } from './types'
-import { set } from 'lodash-es'
-import { TableColumn, TableSlotDefault, Pagination, TableSetPropsType } from '../../../types/table'
+import TableActions from './components/TableActions.vue'
+// import Sortable from 'sortablejs'
+// import { Icon } from '@/components/Icon'
 
 export default defineComponent({
   name: 'Table',
   props: {
     pageSize: propTypes.number.def(10),
     currentPage: propTypes.number.def(1),
-    // 是否多选
-    selection: propTypes.bool.def(true),
+    // 是否展示表格的工具栏
+    showAction: propTypes.bool.def(false),
     // 是否所有的超出隐藏,优先级低于schema中的showOverflowTooltip,
     showOverflowTooltip: propTypes.bool.def(true),
     // 表头
@@ -23,7 +33,7 @@ export default defineComponent({
       default: () => []
     },
     // 展开行
-    expand: propTypes.bool.def(false),
+    // expand: propTypes.bool.def(false),
     // 是否展示分页
     pagination: {
       type: Object as PropType<Pagination>,
@@ -46,10 +56,140 @@ export default defineComponent({
     data: {
       type: Array as PropType<Recordable[]>,
       default: () => []
-    }
+    },
+    // 是否自动预览
+    preview: {
+      type: Array as PropType<string[]>,
+      default: () => []
+    },
+    // sortable: propTypes.bool.def(false),
+    height: propTypes.oneOfType([Number, String]),
+    maxHeight: propTypes.oneOfType([Number, String]),
+    stripe: propTypes.bool.def(false),
+    border: propTypes.bool.def(true),
+    size: {
+      type: String as PropType<ComponentSize>,
+      validator: (v: ComponentSize) => ['medium', 'small', 'mini'].includes(v)
+    },
+    fit: propTypes.bool.def(true),
+    showHeader: propTypes.bool.def(true),
+    highlightCurrentRow: propTypes.bool.def(false),
+    currentRowKey: propTypes.oneOfType([Number, String]),
+    // row-class-name, 类型为 (row: Recordable, rowIndex: number) => string | string
+    rowClassName: {
+      type: [Function, String] as PropType<(row: Recordable, rowIndex: number) => string | string>,
+      default: ''
+    },
+    rowStyle: {
+      type: [Function, Object] as PropType<
+        (row: Recordable, rowIndex: number) => Recordable | CSSProperties
+      >,
+      default: () => undefined
+    },
+    cellClassName: {
+      type: [Function, String] as PropType<
+        (row: Recordable, column: any, rowIndex: number) => string | string
+      >,
+      default: ''
+    },
+    cellStyle: {
+      type: [Function, Object] as PropType<
+        (row: Recordable, column: any, rowIndex: number) => Recordable | CSSProperties
+      >,
+      default: () => undefined
+    },
+    headerRowClassName: {
+      type: [Function, String] as PropType<(row: Recordable, rowIndex: number) => string | string>,
+      default: ''
+    },
+    headerRowStyle: {
+      type: [Function, Object] as PropType<
+        (row: Recordable, rowIndex: number) => Recordable | CSSProperties
+      >,
+      default: () => undefined
+    },
+    headerCellClassName: {
+      type: [Function, String] as PropType<
+        (row: Recordable, column: any, rowIndex: number) => string | string
+      >,
+      default: ''
+    },
+    headerCellStyle: {
+      type: [Function, Object] as PropType<
+        (row: Recordable, column: any, rowIndex: number) => Recordable | CSSProperties
+      >,
+      default: () => undefined
+    },
+    rowKey: propTypes.string.def('id'),
+    emptyText: propTypes.string.def('No Data'),
+    defaultExpandAll: propTypes.bool.def(false),
+    expandRowKeys: {
+      type: Array as PropType<string[]>,
+      default: () => []
+    },
+    defaultSort: {
+      type: Object as PropType<{ prop: string; order: string }>,
+      default: () => ({})
+    },
+    tooltipEffect: {
+      type: String as PropType<'dark' | 'light'>,
+      default: 'dark'
+    },
+    tooltipOptions: {
+      type: Object as PropType<
+        Pick<
+          ElTooltipProps,
+          | 'effect'
+          | 'enterable'
+          | 'hideAfter'
+          | 'offset'
+          | 'placement'
+          | 'popperClass'
+          | 'popperOptions'
+          | 'showAfter'
+          | 'showArrow'
+        >
+      >,
+      default: () => ({
+        enterable: true,
+        placement: 'top',
+        showArrow: true,
+        hideAfter: 200,
+        popperOptions: { strategy: 'fixed' }
+      })
+    },
+    showSummary: propTypes.bool.def(false),
+    sumText: propTypes.string.def('Sum'),
+    summaryMethod: {
+      type: Function as PropType<(param: { columns: any[]; data: any[] }) => any[]>,
+      default: () => undefined
+    },
+    spanMethod: {
+      type: Function as PropType<
+        (param: { row: any; column: any; rowIndex: number; columnIndex: number }) => any[]
+      >,
+      default: () => undefined
+    },
+    selectOnIndeterminate: propTypes.bool.def(true),
+    indent: propTypes.number.def(16),
+    lazy: propTypes.bool.def(false),
+    load: {
+      type: Function as PropType<(row: Recordable, treeNode: any, resolve: Function) => void>,
+      default: () => undefined
+    },
+    treeProps: {
+      type: Object as PropType<{ hasChildren?: string; children?: string; label?: string }>,
+      default: () => ({ hasChildren: 'hasChildren', children: 'children', label: 'label' })
+    },
+    tableLayout: {
+      type: String as PropType<'auto' | 'fixed'>,
+      default: 'fixed'
+    },
+    scrollbarAlwaysOn: propTypes.bool.def(false),
+    flexible: propTypes.bool.def(false)
   },
-  emits: ['update:pageSize', 'update:currentPage', 'register'],
-  setup(props, { attrs, slots, emit, expose }) {
+  emits: ['update:pageSize', 'update:currentPage', 'register', 'refresh', 'sortable-change'],
+  setup(props, { attrs, emit, slots, expose }) {
     const elTableRef = ref<ComponentRef<typeof ElTable>>()
 
     // 注册
@@ -73,12 +213,39 @@ export default defineComponent({
       return propsObj
     })
 
+    // const sortableEl = ref()
+    // 初始化拖拽
+    // const initDropTable = () => {
+    //   const el = unref(elTableRef)?.$el.querySelector('.el-table__body tbody')
+    //   if (!el) return
+    //   if (unref(sortableEl)) unref(sortableEl).destroy()
+
+    //   sortableEl.value = Sortable.create(el, {
+    //     handle: '.table-move',
+    //     animation: 180,
+    //     onEnd(e: any) {
+    //       emit('sortable-change', e)
+    //     }
+    //   })
+    // }
+
+    // watch(
+    //   () => getProps.value.sortable,
+    //   async (v) => {
+    //     await nextTick()
+    //     v && initDropTable()
+    //   },
+    //   {
+    //     immediate: true
+    //   }
+    // )
+
     const setProps = (props: TableProps = {}) => {
       mergeProps.value = Object.assign(unref(mergeProps), props)
-      outsideProps.value = props
+      outsideProps.value = { ...props } as any
     }
 
-    const setColumn = (columnProps: TableSetPropsType[], columnsChildren?: TableColumn[]) => {
+    const setColumn = (columnProps: TableSetProps[], columnsChildren?: TableColumn[]) => {
       const { columns } = unref(getProps)
       for (const v of columnsChildren || columns) {
         for (const item of columnProps) {
@@ -91,16 +258,36 @@ export default defineComponent({
       }
     }
 
-    const selections = ref<Recordable[]>([])
+    const addColumn = (column: TableColumn, index?: number) => {
+      const { columns } = unref(getProps)
+      if (index) {
+        columns.splice(index, 0, column)
+      } else {
+        columns.push(column)
+      }
+    }
+
+    const delColumn = (field: string) => {
+      const { columns } = unref(getProps)
+      const index = columns.findIndex((item) => item.field === field)
+      if (index > -1) {
+        columns.splice(index, 1)
+      }
+    }
+
+    const refresh = () => {
+      emit('refresh')
+    }
 
-    const selectionChange = (selection: Recordable[]) => {
-      selections.value = selection
+    const changSize = (size: ComponentSize) => {
+      setProps({ size })
     }
 
     expose({
       setProps,
       setColumn,
-      selections,
+      delColumn,
+      addColumn,
       elTableRef
     })
 
@@ -149,44 +336,44 @@ export default defineComponent({
     )
 
     const getBindValue = computed(() => {
-      const bindValue: Recordable = { ...attrs, ...props }
+      const bindValue: Recordable = { ...attrs, ...unref(getProps) }
       delete bindValue.columns
       delete bindValue.data
       return bindValue
     })
 
-    const renderTableSelection = () => {
-      const { selection, reserveSelection, align, headerAlign } = unref(getProps)
-      // 渲染多选
-      return selection ? (
-        <ElTableColumn
-          type="selection"
-          reserveSelection={reserveSelection}
-          align={align}
-          headerAlign={headerAlign}
-          width="50"
-        ></ElTableColumn>
-      ) : undefined
-    }
-
-    const renderTableExpand = () => {
-      const { align, headerAlign, expand } = unref(getProps)
-      // 渲染展开行
-      return expand ? (
-        <ElTableColumn type="expand" align={align} headerAlign={headerAlign}>
-          {{
-            // @ts-ignore
-            default: (data: TableSlotDefault) => getSlot(slots, 'expand', data)
-          }}
-        </ElTableColumn>
-      ) : undefined
-    }
-
-    const rnderTreeTableColumn = (columnsChildren: TableColumn[]) => {
-      const { align, headerAlign, showOverflowTooltip } = unref(getProps)
+    const renderTreeTableColumn = (columnsChildren: TableColumn[]) => {
+      const { align, headerAlign, showOverflowTooltip, preview } = unref(getProps)
       return columnsChildren.map((v) => {
-        const props = { ...v }
+        if (v.hidden) return null
+        const props = { ...v } as any
         if (props.children) delete props.children
+
+        const children = v.children
+
+        const slots = {
+          default: (...args: any[]) => {
+            const data = args[0]
+            let isImageUrl = false
+            if (preview.length) {
+              isImageUrl = preview.some((item) => (item as string) === v.field)
+            }
+
+            return children && children.length
+              ? renderTreeTableColumn(children)
+              : props?.slots?.default
+              ? props.slots.default(args)
+              : v?.formatter
+              ? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
+              : isImageUrl
+              ? renderPreview(get(data.row, v.field))
+              : get(data.row, v.field)
+          }
+        }
+        if (props?.slots?.header) {
+          slots['header'] = (...args: any[]) => props.slots.header(args)
+        }
+
         return (
           <ElTableColumn
             showOverflowTooltip={showOverflowTooltip}
@@ -195,23 +382,28 @@ export default defineComponent({
             {...props}
             prop={v.field}
           >
-            {{
-              default: (data: TableSlotDefault) =>
-                v.children && v.children.length
-                  ? rnderTableColumn(v.children)
-                  : // @ts-ignore
-                    getSlot(slots, v.field, data) ||
-                    v?.formatter?.(data.row, data.column, data.row[v.field], data.$index) ||
-                    data.row[v.field],
-              // @ts-ignore
-              header: getSlot(slots, `${v.field}-header`)
-            }}
+            {slots}
           </ElTableColumn>
         )
       })
     }
 
-    const rnderTableColumn = (columnsChildren?: TableColumn[]) => {
+    const renderPreview = (url: string) => {
+      return (
+        <div class="flex items-center">
+          <ElImage
+            src={url}
+            fit="cover"
+            class="w-[100%] h-100px"
+            lazy
+            preview-src-list={[url]}
+            preview-teleported
+          />
+        </div>
+      )
+    }
+
+    const renderTableColumn = (columnsChildren?: TableColumn[]) => {
       const {
         columns,
         reserveIndex,
@@ -219,80 +411,130 @@ export default defineComponent({
         currentPage,
         align,
         headerAlign,
-        showOverflowTooltip
+        showOverflowTooltip,
+        reserveSelection,
+        preview
       } = unref(getProps)
-      return [...[renderTableExpand()], ...[renderTableSelection()]].concat(
-        (columnsChildren || columns).map((v) => {
-          // 自定生成序号
-          if (v.type === 'index') {
-            return (
-              <ElTableColumn
-                type="index"
-                index={
-                  v.index
-                    ? v.index
-                    : (index) => setIndex(reserveIndex, index, pageSize, currentPage)
-                }
-                align={v.align || align}
-                headerAlign={v.headerAlign || headerAlign}
-                label={v.label}
-                width="65px"
-              ></ElTableColumn>
-            )
-          } else {
-            const props = { ...v }
-            if (props.children) delete props.children
-            return (
-              <ElTableColumn
-                showOverflowTooltip={showOverflowTooltip}
-                align={align}
-                headerAlign={headerAlign}
-                {...props}
-                prop={v.field}
-              >
-                {{
-                  default: (data: TableSlotDefault) =>
-                    v.children && v.children.length
-                      ? rnderTreeTableColumn(v.children)
-                      : // @ts-ignore
-                        getSlot(slots, v.field, data) ||
-                        v?.formatter?.(data.row, data.column, data.row[v.field], data.$index) ||
-                        data.row[v.field],
-                  // @ts-ignore
-                  header: () => getSlot(slots, `${v.field}-header`) || v.label
-                }}
-              </ElTableColumn>
-            )
+
+      return (columnsChildren || columns).map((v) => {
+        if (v.hidden) return null
+        if (v.type === 'index') {
+          return (
+            <ElTableColumn
+              type="index"
+              index={
+                v.index ? v.index : (index) => setIndex(reserveIndex, index, pageSize, currentPage)
+              }
+              align={v.align || align}
+              headerAlign={v.headerAlign || headerAlign}
+              label={v.label}
+              width="65px"
+            ></ElTableColumn>
+          )
+        } else if (v.type === 'selection') {
+          return (
+            <ElTableColumn
+              type="selection"
+              reserveSelection={reserveSelection}
+              align={align}
+              headerAlign={headerAlign}
+              width="50"
+            ></ElTableColumn>
+          )
+        } else {
+          const props = { ...v } as any
+          if (props.children) delete props.children
+
+          const children = v.children
+
+          const slots = {
+            default: (...args: any[]) => {
+              const data = args[0]
+
+              let isImageUrl = false
+              if (preview.length) {
+                isImageUrl = preview.some((item) => (item as string) === v.field)
+              }
+
+              return children && children.length
+                ? renderTreeTableColumn(children)
+                : props?.slots?.default
+                ? props.slots.default(args)
+                : v?.formatter
+                ? v?.formatter?.(data.row, data.column, get(data.row, v.field), data.$index)
+                : isImageUrl
+                ? renderPreview(get(data.row, v.field))
+                : get(data.row, v.field)
+            }
           }
-        })
-      )
+          if (props?.slots?.header) {
+            slots['header'] = (...args: any[]) => props.slots.header(args)
+          }
+
+          return (
+            <ElTableColumn
+              showOverflowTooltip={showOverflowTooltip}
+              align={align}
+              headerAlign={headerAlign}
+              {...props}
+              prop={v.field}
+            >
+              {slots}
+            </ElTableColumn>
+          )
+        }
+      })
     }
 
-    return () => (
-      <div v-loading={unref(getProps).loading}>
-        <ElTable
-          // @ts-ignore
-          ref={elTableRef}
-          data={unref(getProps).data}
-          onSelection-change={selectionChange}
-          {...unref(getBindValue)}
-        >
-          {{
-            default: () => rnderTableColumn(),
-            // @ts-ignore
-            append: () => getSlot(slots, 'append')
-          }}
-        </ElTable>
-        {unref(getProps).pagination ? (
-          <ElPagination
-            v-model:pageSize={pageSizeRef.value}
-            v-model:currentPage={currentPageRef.value}
-            class="mt-10px"
-            {...unref(pagination)}
-          ></ElPagination>
-        ) : undefined}
-      </div>
-    )
+    return () => {
+      const tableSlots = {}
+      if (getSlot(slots, 'empty')) {
+        tableSlots['empty'] = (...args: any[]) => getSlot(slots, 'empty', args)
+      }
+      if (getSlot(slots, 'append')) {
+        tableSlots['append'] = (...args: any[]) => getSlot(slots, 'append', args)
+      }
+
+      // const { sortable } = unref(getProps)
+
+      // const sortableEl = sortable ? (
+      //   <ElTableColumn
+      //     className="table-move cursor-move"
+      //     type="sortable"
+      //     prop="sortable"
+      //     width="60px"
+      //     align="center"
+      //   >
+      //     <Icon icon="ant-design:drag-outlined" />
+      //   </ElTableColumn>
+      // ) : null
+
+      return (
+        <div v-loading={unref(getProps).loading}>
+          {unref(getProps).showAction ? (
+            <TableActions
+              columns={unref(getProps).columns}
+              onChangSize={changSize}
+              onRefresh={refresh}
+            />
+          ) : null}
+          <ElTable ref={elTableRef} data={unref(getProps).data} {...unref(getBindValue)}>
+            {{
+              default: () => renderTableColumn(),
+              ...tableSlots
+            }}
+          </ElTable>
+          {unref(getProps).pagination ? (
+            <ElPagination
+              v-model:pageSize={pageSizeRef.value}
+              v-model:currentPage={currentPageRef.value}
+              class="mt-10px"
+              {...unref(pagination)}
+            ></ElPagination>
+          ) : undefined}
+        </div>
+      )
+    }
   }
 })
 </script>

+ 151 - 0
src/components/Table/src/components/TableActions.vue

@@ -0,0 +1,151 @@
+<script lang="tsx">
+import { defineComponent, unref, computed, PropType, watch } from 'vue'
+import {
+  ElTooltip,
+  ElDropdown,
+  ElDropdownMenu,
+  ElDropdownItem,
+  ComponentSize
+  // ElPopover,
+  // ElTree
+} from 'element-plus'
+import { Icon } from '@/components/Icon'
+import { useI18n } from '@/hooks/web/useI18n'
+import { useAppStore } from '@/store/modules/app'
+import { TableColumn } from '../types'
+import { cloneDeep } from 'lodash-es'
+// import { eachTree } from '@/utils/tree'
+
+const appStore = useAppStore()
+const sizeMap = computed(() => appStore.sizeMap)
+
+const { t } = useI18n()
+
+export default defineComponent({
+  name: 'TableActions',
+  props: {
+    columns: {
+      type: Array as PropType<TableColumn[]>,
+      default: () => []
+    }
+  },
+  emits: ['refresh', 'changSize'],
+  setup(props, { emit }) {
+    const refresh = () => {
+      emit('refresh')
+    }
+
+    const changSize = (size: ComponentSize) => {
+      emit('changSize', size)
+    }
+
+    const columns = computed(() => {
+      return cloneDeep(props.columns).filter((v) => {
+        // 去掉type为selection的列和expand的列
+        if (v.type !== 'selection' && v.type !== 'expand') {
+          return v
+        }
+      })
+    })
+
+    watch(
+      () => columns.value,
+      (newColumns) => {
+        console.log('columns change:', newColumns)
+      },
+      {
+        deep: true
+      }
+    )
+
+    return () => (
+      <>
+        <div class="text-right h-28px flex items-center justify-end">
+          <ElTooltip content={t('common.refresh')} placement="top">
+            <span onClick={refresh}>
+              <Icon
+                icon="ant-design:sync-outlined"
+                class="cursor-pointer"
+                hover-color="var(--el-color-primary)"
+              />
+            </span>
+          </ElTooltip>
+
+          <ElTooltip content={t('common.size')} placement="top">
+            <ElDropdown trigger="click" onCommand={changSize}>
+              {{
+                default: () => {
+                  return (
+                    <span>
+                      <Icon
+                        icon="ant-design:column-height-outlined"
+                        class="cursor-pointer mr-8px ml-8px"
+                        hover-color="var(--el-color-primary)"
+                      />
+                    </span>
+                  )
+                },
+                dropdown: () => {
+                  return (
+                    <ElDropdownMenu>
+                      {{
+                        default: () => {
+                          return unref(sizeMap).map((v) => {
+                            return (
+                              <ElDropdownItem key={v} command={v}>
+                                {t(`size.${v}`)}
+                              </ElDropdownItem>
+                            )
+                          })
+                        }
+                      }}
+                    </ElDropdownMenu>
+                  )
+                }
+              }}
+            </ElDropdown>
+          </ElTooltip>
+
+          {/* <ElTooltip content={t('common.columnSetting')} placement="top"> */}
+          {/* <ElPopover trigger="click" placement="left">
+            {{
+              default: () => {
+                return (
+                  <div>
+                    <ElTree
+                      data={unref(columns)}
+                      show-checkbox
+                      default-checked-keys={unref(defaultCheckeds)}
+                      draggable
+                      node-key="field"
+                      allow-drop={(_draggingNode: any, _dropNode: any, type: string) => {
+                        if (type === 'inner') {
+                          return false
+                        } else {
+                          return true
+                        }
+                      }}
+                      onNode-drag-end={onNodeDragEnd}
+                      onCheck-change={onCheckChange}
+                    />
+                  </div>
+                )
+              },
+              reference: () => {
+                return (
+                  <Icon
+                    icon="ant-design:setting-outlined"
+                    class="cursor-pointer"
+                    hoverColor="var(--el-color-primary)"
+                  />
+                )
+              }
+            }}
+          </ElPopover> */}
+          {/* </ElTooltip> */}
+        </div>
+      </>
+    )
+  }
+})
+</script>

+ 0 - 0
src/components/Table/src/helper.ts → src/components/Table/src/helper/index.ts


+ 0 - 26
src/components/Table/src/types.ts

@@ -1,26 +0,0 @@
-import { Pagination, TableColumn } from '@/types/table'
-
-export type TableProps = {
-  pageSize?: number
-  currentPage?: number
-  // 是否多选
-  selection?: boolean
-  // 是否所有的超出隐藏,优先级低于schema中的showOverflowTooltip,
-  showOverflowTooltip?: boolean
-  // 表头
-  columns?: TableColumn[]
-  // 是否展示分页
-  pagination?: Pagination | undefined
-  // 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key)
-  reserveSelection?: boolean
-  // 加载状态
-  loading?: boolean
-  // 是否叠加索引
-  reserveIndex?: boolean
-  // 对齐方式
-  align?: 'left' | 'center' | 'right'
-  // 表头对齐方式
-  headerAlign?: 'left' | 'center' | 'right'
-  data?: Recordable
-  expand?: boolean
-} & Recordable

+ 97 - 0
src/components/Table/src/types/index.ts

@@ -0,0 +1,97 @@
+import { TableProps as ElTableProps } from 'element-plus'
+export interface TableColumn {
+  field: string
+  label?: string
+  type?: string
+  /**
+   * 是否隐藏
+   */
+  hidden?: boolean
+  children?: TableColumn[]
+  slots?: {
+    default?: (...args: any[]) => JSX.Element | JSX.Element[] | null
+    header?: (...args: any[]) => JSX.Element | null
+  }
+  index?: number | ((index: number) => number)
+  columnKey?: string
+  width?: string | number
+  minWidth?: string | number
+  fixed?: boolean | 'left' | 'right'
+  renderHeader?: (...args: any[]) => JSX.Element | null
+  // sortable?: boolean
+  sortMethod?: (...args: any[]) => number
+  sortBy?: string | string[] | ((...args: any[]) => string | string[])
+  sortOrders?: (string | null)[]
+  resizable?: boolean
+  formatter?: (...args: any[]) => any
+  showOverflowTooltip?: boolean
+  align?: 'left' | 'center' | 'right'
+  headerAlign?: 'left' | 'center' | 'right'
+  className?: string
+  labelClassName?: string
+  selectable?: (...args: any[]) => boolean
+  reserveSelection?: boolean
+  filters?: Array<{ text: string; value: string }>
+  filterPlacement?: string
+  filterMultiple?: boolean
+  filterMethod?: (...args: any[]) => boolean
+  filteredValue?: string[]
+  [key: string]: any
+}
+
+export interface TableSlotDefault {
+  row: Recordable
+  column: TableColumn
+  $index: number
+  [key: string]: any
+}
+
+export interface Pagination {
+  small?: boolean
+  background?: boolean
+  pageSize?: number
+  defaultPageSize?: number
+  total?: number
+  pageCount?: number
+  pagerCount?: number
+  currentPage?: number
+  defaultCurrentPage?: number
+  layout?: string
+  pageSizes?: number[]
+  popperClass?: string
+  prevText?: string
+  nextText?: string
+  disabled?: boolean
+  hideOnSinglePage?: boolean
+}
+
+export interface TableSetProps {
+  field: string
+  path: string
+  value: any
+}
+
+export interface TableProps extends Omit<Partial<ElTableProps<any[]>>, 'data'> {
+  pageSize?: number
+  currentPage?: number
+  showAction?: boolean
+  // 是否所有的超出隐藏,优先级低于schema中的showOverflowTooltip,
+  showOverflowTooltip?: boolean
+  // 表头
+  columns?: TableColumn[]
+  // 是否展示分页
+  pagination?: Pagination | undefined
+  // 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key)
+  reserveSelection?: boolean
+  // 加载状态
+  loading?: boolean
+  // 是否叠加索引
+  reserveIndex?: boolean
+  // 对齐方式
+  align?: 'left' | 'center' | 'right'
+  // 表头对齐方式
+  headerAlign?: 'left' | 'center' | 'right'
+  preview?: string[]
+  sortable?: boolean
+  data?: Recordable
+}

+ 12 - 15
src/components/TableSetting/src/TableSetting.vue

@@ -1,9 +1,8 @@
-<script setup lang="ts">
+<script setup lang="tsx">
 import { ElDrawer, ElButton, ElMessage, ElSwitch } from 'element-plus'
-import { ref, defineEmits } from 'vue'
-import { TableColumn } from '@/types/table'
+import { ref } from 'vue'
 import { CrudSchema } from '@/hooks/web/useCrudSchemas'
-import { Table } from '@/components/Table'
+import { Table, TableColumn } from '@/components/Table'
 import { usePageStore } from '@/store/modules/page'
 import { PropType } from 'vue'
 
@@ -26,8 +25,13 @@ const columns: TableColumn[] = [
     label: '列名'
   },
   {
-    field: 'tableShow',
-    label: '列表展示'
+    field: 'table.hidden',
+    label: '隐藏',
+    slots: {
+      default: (data: any) => {
+        return <ElSwitch v-model={data[0].row.table.hidden}></ElSwitch>
+      }
+    }
   }
 ]
 
@@ -38,7 +42,6 @@ let tableDataList = ref<CrudSchema[]>(props.data)
 let storeData = appStore.getPageData[props.page]
 
 if (storeData) {
-  console.log(storeData)
   for (let i = 0; i < tableDataList.value.length; i++) {
     let dataItem = storeData.schemas.find((e: CrudSchema) => {
       return e.field == tableDataList.value[i].field
@@ -54,6 +57,7 @@ if (storeData) {
 
 const save = () => {
   appStore.setPageData(props.page, { ...storeData, schemas: tableDataList.value })
+  console.log(appStore.getPageData[props.page])
   emit('setSchemas', tableDataList.value)
   drawer.value = false
   ElMessage.success('保存成功')
@@ -73,14 +77,7 @@ const save = () => {
         height="calc(100vh - 88px - 74px)"
         :data="tableDataList"
         :selection="false"
-      >
-        <template #tableShow="{ row }">
-          <ElSwitch v-model="row.table.show" />
-        </template>
-        <template #searchShow="{ row }">
-          <ElSwitch v-model="row.search.show" />
-        </template>
-      </Table>
+      />
       <div class="footer">
         <ElButton @click="drawer = false">取消</ElButton>
         <ElButton type="primary" @click="save">保存</ElButton>

+ 57 - 60
src/components/TagsView/src/TagsView.vue

@@ -35,6 +35,8 @@ const appStore = useAppStore()
 
 const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
 
+const isDark = computed(() => appStore.getIsDark)
+
 // 初始化tag
 const initTags = () => {
   affixTagArr.value = filterAffixTags(unref(routers))
@@ -73,7 +75,7 @@ const closeAllTags = () => {
   toLastView()
 }
 
-// 关闭其
+// 关闭其
 const closeOthersTags = () => {
   tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
 }
@@ -264,13 +266,14 @@ watch(
     class="flex w-full relative bg-[#fff] dark:bg-[var(--el-bg-color)]"
   >
     <span
-      :class="`${prefixCls}__tool`"
-      class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] flex justify-center items-center cursor-pointer"
+      :class="`${prefixCls}__tool ${prefixCls}__tool--first`"
+      class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] flex items-center justify-center cursor-pointer"
       @click="move(-200)"
     >
       <Icon
         icon="ep:d-arrow-left"
-        :color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'"
+        color="var(--el-text-color-placeholder)"
+        :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
       />
     </span>
     <div class="overflow-hidden flex-1">
@@ -280,7 +283,7 @@ watch(
             :ref="itemRefs.set"
             :schema="[
               {
-                icon: 'ep:refresh',
+                icon: 'ant-design:sync-outlined',
                 label: t('common.reload'),
                 disabled: selectedTag?.fullPath !== item.fullPath,
                 command: () => {
@@ -288,7 +291,7 @@ watch(
                 }
               },
               {
-                icon: 'ep:close',
+                icon: 'ant-design:close-outlined',
                 label: t('common.closeTab'),
                 disabled: !!visitedViews?.length && selectedTag?.meta.affix,
                 command: () => {
@@ -351,7 +354,7 @@ watch(
               <router-link :ref="tagLinksRefs.set" :to="{ ...item }" custom v-slot="{ navigate }">
                 <div
                   @click="navigate"
-                  class="h-full flex justify-center items-center whitespace-nowrap pl-15px pr-5px"
+                  class="h-full flex justify-center items-center whitespace-nowrap pl-15px"
                 >
                   <Icon
                     v-if="
@@ -365,15 +368,13 @@ watch(
                     class="mr-5px"
                   />
                   {{ t(item?.meta?.title as string) }}
-                  <div :class="`${prefixCls}__item--close`">
-                    <Icon
-                      :class="`${prefixCls}__item--close-icon`"
-                      color="#333"
-                      icon="ep:close"
-                      :size="12"
-                      @click.prevent.stop="closeSelectedTag(item)"
-                    />
-                  </div>
+                  <Icon
+                    :class="`${prefixCls}__item--close`"
+                    color="#333"
+                    icon="ant-design:close-outlined"
+                    :size="12"
+                    @click.prevent.stop="closeSelectedTag(item)"
+                  />
                 </div>
               </router-link>
             </div>
@@ -383,22 +384,24 @@ watch(
     </div>
     <span
       :class="`${prefixCls}__tool`"
-      class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] flex justify-center items-center cursor-pointer"
+      class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] flex items-center justify-center cursor-pointer"
       @click="move(200)"
     >
       <Icon
         icon="ep:d-arrow-right"
-        :color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'"
+        color="var(--el-text-color-placeholder)"
+        :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
       />
     </span>
     <span
       :class="`${prefixCls}__tool`"
-      class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] flex justify-center items-center cursor-pointer"
+      class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] flex items-center justify-center cursor-pointer"
       @click="refreshSelectedTag(selectedTag)"
     >
       <Icon
         icon="ant-design:reload-outlined"
-        :color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'"
+        color="var(--el-text-color-placeholder)"
+        :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
       />
     </span>
     <ContextMenu
@@ -457,11 +460,12 @@ watch(
     >
       <span
         :class="`${prefixCls}__tool`"
-        class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] flex justify-center items-center cursor-pointer block"
+        class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] flex items-center justify-center cursor-pointer block"
       >
         <Icon
           icon="ant-design:setting-outlined"
-          :color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'"
+          color="var(--el-text-color-placeholder)"
+          :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
         />
       </span>
     </ContextMenu>
@@ -479,44 +483,47 @@ watch(
   &__tool {
     position: relative;
 
-    &:hover {
-      :deep(span) {
-        color: var(--el-color-black) !important;
-      }
-    }
-
-    &:after {
+    &::before {
       position: absolute;
       top: 1px;
       left: 0;
       width: 100%;
       height: calc(~'100% - 1px');
-      border-right: 1px solid var(--tags-view-border-color);
-      border-left: 1px solid var(--tags-view-border-color);
+      border-left: 1px solid var(--el-border-color);
       content: '';
     }
+
+    &--first {
+      &::before {
+        position: absolute;
+        top: 1px;
+        left: 0;
+        width: 100%;
+        height: calc(~'100% - 1px');
+        border-right: 1px solid var(--el-border-color);
+        border-left: none;
+        content: '';
+      }
+    }
   }
 
   &__item {
     position: relative;
     top: 2px;
-    height: calc(~'100% - 4px');
+    height: calc(~'100% - 6px');
+    padding-right: 25px;
     margin-left: 4px;
     font-size: 12px;
     cursor: pointer;
     border: 1px solid #d9d9d9;
-    border-radius: 4px;
+    border-radius: 2px;
+
     &--close {
-      // display: none;
-      // transform: translate(0, -50%);
-      margin-left: 3px;
-      line-height: 0;
-      padding: 3px;
-      &:hover {
-        background: rgba(0, 0, 0, 0.2);
-        border-radius: 50%;
-        color: #fff;
-      }
+      position: absolute;
+      top: 50%;
+      right: 5px;
+      display: none;
+      transform: translate(0, -50%);
     }
     &:not(.@{prefix-cls}__item--affix):hover {
       .@{prefix-cls}__item--close {
@@ -527,7 +534,7 @@ watch(
 
   &__item:not(.is-active) {
     &:hover {
-      // color: var(--el-color-primary);
+      color: var(--el-color-primary);
     }
   }
 
@@ -536,7 +543,7 @@ watch(
     background-color: var(--el-color-primary);
     border: 1px solid var(--el-color-primary);
     .@{prefix-cls}__item--close {
-      :deep(span) {
+      :deep(svg) {
         color: var(--el-color-white) !important;
       }
     }
@@ -546,25 +553,14 @@ watch(
 .dark {
   .@{prefix-cls} {
     &__tool {
-      &:hover {
-        :deep(span) {
-          color: #fff !important;
+      &--first {
+        &::after {
+          display: none;
         }
       }
-
-      &:after {
-        border-right: 1px solid var(--el-border-color);
-        border-left: 1px solid var(--el-border-color);
-      }
     }
 
     &__item {
-      position: relative;
-      top: 2px;
-      height: calc(~'100% - 4px');
-      padding-right: 25px;
-      font-size: 12px;
-      cursor: pointer;
       border: 1px solid var(--el-border-color);
     }
 
@@ -577,8 +573,9 @@ watch(
     &__item.is-active {
       color: var(--el-color-white);
       background-color: var(--el-color-primary);
+      border: 1px solid var(--el-color-primary);
       .@{prefix-cls}__item--close {
-        :deep(span) {
+        :deep(svg) {
           color: var(--el-color-white) !important;
         }
       }

+ 50 - 6
src/components/UserInfo/src/UserInfo.vue

@@ -1,12 +1,20 @@
 <script setup lang="ts">
 import { ElDropdown, ElDropdownMenu, ElDropdownItem, ElMessageBox } from 'element-plus'
 import { useI18n } from '@/hooks/web/useI18n'
-import { useCache } from '@/hooks/web/useCache'
+import { useStorage } from '@/hooks/web/useStorage'
 import { resetRouter } from '@/router'
 import { useRouter } from 'vue-router'
 import { loginOutApi } from '@/api/login'
 import { useDesign } from '@/hooks/web/useDesign'
 import { useTagsViewStore } from '@/store/modules/tagsView'
+import LockDialog from './components/LockDialog.vue'
+import { ref, computed } from 'vue'
+import LockPage from './components/LockPage.vue'
+import { useLockStore } from '@/store/modules/lock'
+
+const lockStore = useLockStore()
+
+const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
 
 const tagsViewStore = useTagsViewStore()
 
@@ -16,7 +24,7 @@ const prefixCls = getPrefixCls('user-info')
 
 const { t } = useI18n()
 
-const { wsCache } = useCache()
+const { clear } = useStorage()
 
 const { replace } = useRouter()
 
@@ -29,7 +37,7 @@ const loginOut = () => {
     .then(async () => {
       const res = await loginOutApi().catch(() => {})
       if (res) {
-        wsCache.clear()
+        clear()
         tagsViewStore.delAllViews()
         resetRouter() // 重置静态路由表
         replace('/login')
@@ -38,13 +46,20 @@ const loginOut = () => {
     .catch(() => {})
 }
 
+const dialogVisible = ref<boolean>(false)
+
+// 锁定屏幕
+const lockScreen = () => {
+  dialogVisible.value = true
+}
+
 const toDocument = () => {
   window.open('https://element-plus-admin-doc.cn/')
 }
 </script>
 
 <template>
-  <ElDropdown :class="prefixCls" trigger="click">
+  <ElDropdown class="custom-hover" :class="prefixCls" trigger="click">
     <div class="flex items-center">
       <img
         src="@/assets/imgs/avatar.jpg"
@@ -55,13 +70,42 @@ const toDocument = () => {
     </div>
     <template #dropdown>
       <ElDropdownMenu>
-        <!-- <ElDropdownItem>
+        <ElDropdownItem>
           <div @click="toDocument">{{ t('common.document') }}</div>
-        </ElDropdownItem> -->
+        </ElDropdownItem>
         <ElDropdownItem divided>
+          <div @click="lockScreen">{{ t('lock.lockScreen') }}</div>
+        </ElDropdownItem>
+        <ElDropdownItem>
           <div @click="loginOut">{{ t('common.loginOut') }}</div>
         </ElDropdownItem>
       </ElDropdownMenu>
     </template>
   </ElDropdown>
+
+  <LockDialog v-if="dialogVisible" v-model="dialogVisible" />
+  <teleport to="body">
+    <transition name="fade-bottom" mode="out-in">
+      <LockPage v-if="getIsLock" />
+    </transition>
+  </teleport>
 </template>
+
+<style scoped lang="less">
+.fade-bottom-enter-active,
+.fade-bottom-leave-active {
+  transition:
+    opacity 0.25s,
+    transform 0.3s;
+}
+
+.fade-bottom-enter-from {
+  opacity: 0;
+  transform: translateY(-10%);
+}
+
+.fade-bottom-leave-to {
+  opacity: 0;
+  transform: translateY(10%);
+}
+</style>

+ 101 - 0
src/components/UserInfo/src/components/LockDialog.vue

@@ -0,0 +1,101 @@
+<script setup lang="ts">
+import { useI18n } from '@/hooks/web/useI18n'
+import { ref } from 'vue'
+import { Dialog } from '@/components/Dialog'
+import { Form } from '@/components/Form'
+import { useForm } from '@/hooks/web/useForm'
+import { reactive, computed } from 'vue'
+import { useValidator } from '@/hooks/web/useValidator'
+import { FormSchema } from '@/components/Form'
+import { ElButton } from 'element-plus'
+import { useDesign } from '@/hooks/web/useDesign'
+import { useLockStore } from '@/store/modules/lock'
+
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('lock-dialog')
+
+const { required } = useValidator()
+
+const { t } = useI18n()
+
+const lockStore = useLockStore()
+
+const props = defineProps({
+  modelValue: {
+    type: Boolean
+  }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (val) => {
+    console.log('set: ', val)
+    emit('update:modelValue', val)
+  }
+})
+
+const dialogTitle = ref(t('lock.lockScreen'))
+
+const rules = reactive({
+  password: [required()]
+})
+
+const schema: FormSchema[] = reactive([
+  {
+    label: t('lock.lockPassword'),
+    field: 'password',
+    component: 'Input',
+    componentProps: {
+      type: 'password',
+      showPassword: true
+    }
+  }
+])
+
+const { formRegister, formMethods } = useForm()
+
+const { getFormData, getElFormExpose } = formMethods
+
+const handleLock = async () => {
+  const formExpose = await getElFormExpose()
+  formExpose?.validate(async (valid) => {
+    if (valid) {
+      dialogVisible.value = false
+      const formData = await getFormData()
+      lockStore.setLockInfo({
+        isLock: true,
+        ...formData
+      })
+    }
+  })
+}
+</script>
+
+<template>
+  <Dialog
+    v-model="dialogVisible"
+    width="500px"
+    max-height="170px"
+    :class="prefixCls"
+    :title="dialogTitle"
+  >
+    <div class="flex flex-col items-center">
+      <img src="@/assets/imgs/avatar.jpg" alt="" class="w-70px h-70px rounded-[50%]" />
+      <span class="text-14px my-10px text-[var(--top-header-text-color)]">Archer</span>
+    </div>
+    <Form :is-col="false" :schema="schema" :rules="rules" @register="formRegister" />
+    <template #footer>
+      <ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton>
+    </template>
+  </Dialog>
+</template>
+
+<style lang="less" scoped>
+:global(.v-lock-dialog) {
+  @media (max-width: 767px) {
+    max-width: calc(100vw - 16px);
+  }
+}
+</style>

+ 270 - 0
src/components/UserInfo/src/components/LockPage.vue

@@ -0,0 +1,270 @@
+<script lang="ts" setup>
+import { ref } from 'vue'
+import { ElInput, ElButton } from 'element-plus'
+import { resetRouter } from '@/router'
+import { useRouter } from 'vue-router'
+import { useStorage } from '@/hooks/web/useStorage'
+import { useLockStore } from '@/store/modules/lock'
+import { useI18n } from '@/hooks/web/useI18n'
+import { useNow } from '@/hooks/web/useNow'
+import { useDesign } from '@/hooks/web/useDesign'
+import { Icon } from '@/components/Icon'
+import { loginOutApi } from '@/api/login'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+const tagsViewStore = useTagsViewStore()
+
+const { clear } = useStorage()
+
+const { replace } = useRouter()
+
+const password = ref('')
+const loading = ref(false)
+const errMsg = ref(false)
+const showDate = ref(true)
+
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('lock-page')
+
+const lockStore = useLockStore()
+
+const { hour, month, minute, meridiem, year, day, week } = useNow(true)
+
+const { t } = useI18n()
+
+// 解锁
+async function unLock() {
+  if (!password.value) {
+    return
+  }
+  let pwd = password.value
+  try {
+    loading.value = true
+    const res = await lockStore.unLock(pwd)
+    errMsg.value = !res
+  } finally {
+    loading.value = false
+  }
+}
+
+// 返回登录
+async function goLogin() {
+  const res = await loginOutApi().catch(() => {})
+  if (res) {
+    clear()
+    tagsViewStore.delAllViews()
+    resetRouter() // 重置静态路由表
+    lockStore.resetLockInfo()
+    replace('/login')
+  }
+}
+
+function handleShowForm(show = false) {
+  showDate.value = show
+}
+</script>
+
+<template>
+  <div
+    :class="prefixCls"
+    class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
+  >
+    <div
+      :class="`${prefixCls}__unlock`"
+      class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
+      @click="handleShowForm(false)"
+      v-show="showDate"
+    >
+      <Icon icon="ep:lock" />
+      <span>{{ t('lock.unlock') }}</span>
+    </div>
+
+    <div class="flex w-screen h-screen justify-center items-center">
+      <div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
+        <span>{{ hour }}</span>
+        <span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
+          {{ meridiem }}
+        </span>
+      </div>
+      <div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
+        <span> {{ minute }}</span>
+      </div>
+    </div>
+    <transition name="fade-slide">
+      <div :class="`${prefixCls}-entry`" v-show="!showDate">
+        <div :class="`${prefixCls}-entry-content`">
+          <div class="flex flex-col items-center">
+            <img src="@/assets/imgs/avatar.jpg" alt="" class="w-70px h-70px rounded-[50%]" />
+            <span class="text-14px my-10px text-[var(--logo-title-text-color)]">Archer</span>
+          </div>
+          <ElInput
+            type="password"
+            :placeholder="t('lock.placeholder')"
+            class="enter-x"
+            v-model="password"
+          />
+          <span :class="`text-14px ${prefixCls}-entry__err-msg enter-x`" v-if="errMsg">
+            {{ t('lock.message') }}
+          </span>
+          <div :class="`${prefixCls}-entry__footer enter-x`">
+            <ElButton
+              type="primary"
+              size="small"
+              class="mt-2 mr-2 enter-x"
+              link
+              :disabled="loading"
+              @click="handleShowForm(true)"
+            >
+              {{ t('common.back') }}
+            </ElButton>
+            <ElButton
+              type="primary"
+              size="small"
+              class="mt-2 mr-2 enter-x"
+              link
+              :disabled="loading"
+              @click="goLogin"
+            >
+              {{ t('lock.backToLogin') }}
+            </ElButton>
+            <ElButton
+              type="primary"
+              class="mt-2"
+              size="small"
+              link
+              @click="unLock()"
+              :disabled="loading"
+            >
+              {{ t('lock.entrySystem') }}
+            </ElButton>
+          </div>
+        </div>
+      </div>
+    </transition>
+
+    <div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
+      <div class="text-5xl mb-4 enter-x" v-show="!showDate">
+        {{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
+      </div>
+      <div class="text-2xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div>
+    </div>
+  </div>
+</template>
+
+<style lang="less" scoped>
+@prefix-cls: ~'@{namespace}-lock-page';
+
+// Small screen / tablet
+@screen-sm: 576px;
+
+// Medium screen / desktop
+@screen-md: 768px;
+
+// Large screen / wide desktop
+@screen-lg: 992px;
+
+// Extra large screen / full hd
+@screen-xl: 1200px;
+
+// Extra extra large screen / large desktop
+@screen-2xl: 1600px;
+
+@error-color: #ed6f6f;
+
+.@{prefix-cls} {
+  z-index: 3000;
+
+  &__unlock {
+    transform: translate(-50%, 0);
+  }
+
+  &__hour,
+  &__minute {
+    display: flex;
+    font-weight: 700;
+    color: #bababa;
+    background-color: #141313;
+    border-radius: 30px;
+    justify-content: center;
+    align-items: center;
+
+    @media screen and (max-width: @screen-md) {
+      span:not(.meridiem) {
+        font-size: 160px;
+      }
+    }
+
+    @media screen and (min-width: @screen-md) {
+      span:not(.meridiem) {
+        font-size: 160px;
+      }
+    }
+
+    @media screen and (max-width: @screen-sm) {
+      span:not(.meridiem) {
+        font-size: 90px;
+      }
+    }
+    @media screen and (min-width: @screen-lg) {
+      span:not(.meridiem) {
+        font-size: 220px;
+      }
+    }
+
+    @media screen and (min-width: @screen-xl) {
+      span:not(.meridiem) {
+        font-size: 260px;
+      }
+    }
+    @media screen and (min-width: @screen-2xl) {
+      span:not(.meridiem) {
+        font-size: 320px;
+      }
+    }
+  }
+
+  &-entry {
+    position: absolute;
+    top: 0;
+    left: 0;
+    display: flex;
+    width: 100%;
+    height: 100%;
+    background-color: rgb(0 0 0 / 50%);
+    backdrop-filter: blur(8px);
+    justify-content: center;
+    align-items: center;
+
+    &-content {
+      width: 260px;
+    }
+
+    &__header {
+      text-align: center;
+
+      &-img {
+        width: 70px;
+        margin: 0 auto;
+        border-radius: 50%;
+      }
+
+      &-name {
+        margin-top: 5px;
+        font-weight: 500;
+        color: #bababa;
+      }
+    }
+
+    &__err-msg {
+      display: inline-block;
+      margin-top: 10px;
+      color: @error-color;
+    }
+
+    &__footer {
+      display: flex;
+      justify-content: space-between;
+    }
+  }
+}
+</style>

+ 2 - 0
src/components/index.ts

@@ -1,6 +1,8 @@
 import type { App } from 'vue'
 import { Icon } from './Icon'
+import { Permission } from './Permission'
 
 export const setupGlobCom = (app: App<Element>): void => {
   app.component('Icon', Icon)
+  app.component('Permission', Permission)
 }

+ 71 - 16
src/config/axios/config.ts

@@ -1,18 +1,18 @@
-const config: {
-  base_url: {
-    base: string
-    dev: string
-    pro: string
-    test: string
-  }
-  result_code: number | string
-  default_headers: AxiosHeaders
-  request_timeout: number
-} = {
+import {
+  AxiosConfig,
+  AxiosResponse,
+  AxiosRequestHeaders,
+  AxiosError,
+  InternalAxiosRequestConfig
+} from './types'
+import { ElMessage } from 'element-plus'
+import qs from 'qs'
+
+const config: AxiosConfig = {
   /**
    * api请求基础路径
    */
-  base_url: {
+  baseUrl: {
     // 开发环境接口前缀
     base: '',
 
@@ -29,18 +29,73 @@ const config: {
   /**
    * 接口成功返回状态码
    */
-  result_code: '0000',
+  code: 0,
 
   /**
    * 接口请求超时时间
    */
-  request_timeout: 60000,
+  timeout: 60000,
 
   /**
    * 默认接口请求类型
    * 可选值:application/x-www-form-urlencoded multipart/form-data
    */
-  default_headers: 'application/json'
+  defaultHeaders: 'application/json',
+
+  interceptors: {
+    //请求拦截
+    // requestInterceptors: (config) => {
+    //   return config
+    // },
+    // 响应拦截器
+    // responseInterceptors: (result: AxiosResponse) => {
+    //   return result
+    // }
+  }
+}
+
+const defaultRequestInterceptors = (config: InternalAxiosRequestConfig) => {
+  if (
+    config.method === 'post' &&
+    (config.headers as AxiosRequestHeaders)['Content-Type'] === 'application/x-www-form-urlencoded'
+  ) {
+    config.data = qs.stringify(config.data)
+  }
+  if (config.method === 'get' && config.params) {
+    let url = config.url as string
+    url += '?'
+    const keys = Object.keys(config.params)
+    for (const key of keys) {
+      if (config.params[key] !== void 0 && config.params[key] !== null) {
+        url += `${key}=${encodeURIComponent(config.params[key])}&`
+      }
+    }
+    url = url.substring(0, url.length - 1)
+    config.params = {}
+    config.url = url
+  }
+  return config
+}
+;(error: AxiosError) => {
+  console.log(error)
+  Promise.reject(error)
+}
+
+const defaultResponseInterceptors = (response: AxiosResponse<any>) => {
+  if (response?.config?.responseType === 'blob') {
+    // 如果是文件流,直接过
+    return response
+  } else if (response.data.code === config.code) {
+    return response.data
+  } else {
+    ElMessage.error(response.data.message)
+  }
+}
+;(error: AxiosError) => {
+  console.log('err' + error) // for debug
+  ElMessage.error(error.message)
+  return Promise.reject(error)
 }
 
-export { config }
+export { defaultResponseInterceptors, defaultRequestInterceptors }
+export default config

+ 21 - 14
src/config/axios/index.ts

@@ -1,33 +1,40 @@
-import { service } from './service'
+import service from './service'
 
-import { config } from './config'
+import config from './config'
 
-const { default_headers } = config
+const { defaultHeaders } = config
 
-const request = (option: any) => {
+const request = (option: AxiosConfig) => {
   const { url, method, params, data, headersType, responseType } = option
-  return service({
+  return service.request({
     url: url,
     method,
     params,
     data,
     responseType: responseType,
     headers: {
-      'Content-Type': headersType || default_headers
+      'Content-Type': headersType || defaultHeaders
     }
   })
 }
+
 export default {
-  get: <T = any>(option: any) => {
-    return request({ method: 'get', ...option }) as unknown as T
+  get: <T = any>(option: AxiosConfig) => {
+    return request({ method: 'get', ...option }) as Promise<IResponse<T>>
+  },
+  post: <T = any>(option: AxiosConfig) => {
+    return request({ method: 'post', ...option }) as Promise<IResponse<T>>
+  },
+  delete: <T = any>(option: AxiosConfig) => {
+    return request({ method: 'delete', ...option }) as Promise<IResponse<T>>
   },
-  post: <T = any>(option: any) => {
-    return request({ method: 'post', ...option }) as unknown as T
+  put: <T = any>(option: AxiosConfig) => {
+    return request({ method: 'put', ...option }) as Promise<IResponse<T>>
   },
-  delete: <T = any>(option: any) => {
-    return request({ method: 'delete', ...option }) as unknown as T
+  cancelRequest: (url: string | string[]) => {
+    return service.cancelRequest(url)
   },
-  put: <T = any>(option: any) => {
-    return request({ method: 'put', ...option }) as unknown as T
+  cancelAllRequest: () => {
+    return service.cancelAllRequest()
   }
 }

+ 55 - 63
src/config/axios/service.ts

@@ -1,78 +1,70 @@
-import axios, {
-  AxiosInstance,
-  InternalAxiosRequestConfig,
-  AxiosRequestHeaders,
-  AxiosResponse,
-  AxiosError
-} from 'axios'
+import axios from 'axios'
+import config, { defaultRequestInterceptors, defaultResponseInterceptors } from './config'
 
-import qs from 'qs'
+import { AxiosInstance, InternalAxiosRequestConfig, RequestConfig, AxiosResponse } from './types'
 
-import { config } from './config'
+const { interceptors, baseUrl } = config
+export const PATH_URL = baseUrl[import.meta.env.VITE_API_BASE_PATH]
 
-import { ElMessage } from 'element-plus'
+const { requestInterceptors, responseInterceptors } = interceptors
 
-const { result_code, base_url } = config
+const abortControllerMap: Map<string, AbortController> = new Map()
 
-export const PATH_URL = base_url[import.meta.env.VITE_API_BASEPATH]
+const axiosInstance: AxiosInstance = axios.create({
+  ...config,
+  baseURL: PATH_URL
+})
 
-// 创建axios实例
-const service: AxiosInstance = axios.create({
-  baseURL: PATH_URL, // api 的 base_url
-  timeout: config.request_timeout // 请求超时时间
+axiosInstance.interceptors.request.use((res: InternalAxiosRequestConfig) => {
+  const controller = new AbortController()
+  const url = res.url || ''
+  res.signal = controller.signal
+  abortControllerMap.set(url, controller)
+  return res
 })
 
-// request拦截器
-service.interceptors.request.use(
-  (config: InternalAxiosRequestConfig) => {
-    if (
-      config.method === 'post' &&
-      (config.headers as AxiosRequestHeaders)['Content-Type'] ===
-        'application/x-www-form-urlencoded'
-    ) {
-      config.data = qs.stringify(config.data)
-    }
-    // ;(config.headers as AxiosRequestHeaders)['Token'] = 'test test'
-    // get参数编码
-    if (config.method === 'get' && config.params) {
-      let url = config.url as string
-      url += '?'
-      const keys = Object.keys(config.params)
-      for (const key of keys) {
-        if (config.params[key] !== void 0 && config.params[key] !== null) {
-          url += `${key}=${encodeURIComponent(config.params[key])}&`
-        }
-      }
-      url = url.substring(0, url.length - 1)
-      config.params = {}
-      config.url = url
-    }
-    return config
+axiosInstance.interceptors.response.use(
+  (res: AxiosResponse) => {
+    const url = res.config.url || ''
+    abortControllerMap.delete(url)
+    return res.data
   },
-  (error: AxiosError) => {
-    // Do something with request error
-    console.log(error) // for debug
-    Promise.reject(error)
-  }
+  (err: any) => err
 )
 
-// response 拦截器
-service.interceptors.response.use(
-  (response: AxiosResponse<any>) => {
-    if (response.config.responseType === 'blob') {
-      // 如果是文件流,直接过
-      return response
-    } else if (response.data.code === result_code) {
-      return response.data
-    } else {
-      ElMessage.error(response.data.message)
+axiosInstance.interceptors.request.use(requestInterceptors || defaultRequestInterceptors)
+axiosInstance.interceptors.response.use(responseInterceptors || defaultResponseInterceptors)
+
+const service = {
+  request: (config: RequestConfig) => {
+    return new Promise((resolve, reject) => {
+      if (config.interceptors?.requestInterceptors) {
+        config = config.interceptors.requestInterceptors(config as any)
+      }
+
+      axiosInstance
+        .request(config)
+        .then((res) => {
+          resolve(res)
+        })
+        .catch((err: any) => {
+          reject(err)
+        })
+    })
+  },
+  cancelRequest: (url: string | string[]) => {
+    const urlList = Array.isArray(url) ? url : [url]
+    for (const _url of urlList) {
+      abortControllerMap.get(_url)?.abort()
+      abortControllerMap.delete(_url)
     }
   },
-  (error: AxiosError) => {
-    console.log('err' + error) // for debug
-    ElMessage.error(error.message)
-    return Promise.reject(error)
+  cancelAllRequest() {
+    for (const [_, controller] of abortControllerMap) {
+      controller.abort()
+    }
+    abortControllerMap.clear()
   }
-)
+}
 
-export { service }
+export default service

+ 44 - 0
src/config/axios/types/index.ts

@@ -0,0 +1,44 @@
+import type {
+  InternalAxiosRequestConfig,
+  AxiosResponse,
+  AxiosRequestConfig,
+  AxiosInstance,
+  AxiosRequestHeaders,
+  AxiosError
+} from 'axios'
+
+interface RequestInterceptors<T> {
+  // 请求拦截
+  requestInterceptors?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
+  requestInterceptorsCatch?: (err: any) => any
+  // 响应拦截
+  responseInterceptors?: (config: T) => T
+  responseInterceptorsCatch?: (err: any) => any
+}
+interface AxiosConfig<T = AxiosResponse> {
+  baseUrl: {
+    base: string
+    dev: string
+    pro: string
+    test: string
+  }
+  code: number
+  defaultHeaders: AxiosHeaders
+  timeout: number
+  interceptors: RequestInterceptors<T>
+}
+
+interface RequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
+  interceptors?: RequestInterceptors<T>
+}
+
+export {
+  AxiosResponse,
+  RequestInterceptors,
+  RequestConfig,
+  AxiosConfig,
+  AxiosInstance,
+  InternalAxiosRequestConfig,
+  AxiosRequestHeaders,
+  AxiosError
+}

+ 5 - 15
src/directives/permission/hasPermi.ts

@@ -1,28 +1,18 @@
 import type { App, Directive, DirectiveBinding } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
-import { useCache } from '@/hooks/web/useCache'
-import { intersection } from 'lodash-es'
-import { isArray } from '@/utils/is'
-import { useAppStoreWithOut } from '@/store/modules/app'
+import router from '@/router'
 
 const { t } = useI18n()
-const { wsCache } = useCache()
-const appStore = useAppStoreWithOut()
 
-// 全部权限
-const all_permission = ['*.*.*']
-const hasPermission = (value: string | string[]): boolean => {
-  const permissions = wsCache.get(appStore.getUserInfo).permissions as string[]
+const hasPermission = (value: string): boolean => {
+  const permission = (router.currentRoute.value.meta.permission || []) as string[]
   if (!value) {
     throw new Error(t('permission.hasPermission'))
   }
-  if (!isArray(value)) {
-    return permissions?.includes(value as string)
-  }
-  if (all_permission[0] === permissions[0]) {
+  if (permission.includes(value)) {
     return true
   }
-  return (intersection(value, permissions) as string[]).length > 0
+  return false
 }
 function hasPermi(el: Element, binding: DirectiveBinding) {
   const value = binding.value

+ 0 - 0
src/hooks/web/useEmitt.ts → src/hooks/event/useEmitt.ts


+ 0 - 17
src/hooks/web/useCache.ts

@@ -1,17 +0,0 @@
-/**
- * 配置浏览器本地存储的方式,可直接存储对象数组。
- */
-
-import WebStorageCache from 'web-storage-cache'
-
-type CacheType = 'sessionStorage' | 'localStorage'
-
-export const useCache = (type: CacheType = 'sessionStorage') => {
-  const wsCache: WebStorageCache = new WebStorageCache({
-    storage: type
-  })
-
-  return {
-    wsCache
-  }
-}

+ 1 - 1
src/hooks/web/useConfigGlobal.ts

@@ -1,4 +1,4 @@
-import { ConfigGlobalTypes } from '@/types/configGlobal'
+import { ConfigGlobalTypes } from '@/components/ConfigGlobal'
 import { inject } from 'vue'
 
 export const useConfigGlobal = () => {

+ 51 - 137
src/hooks/web/useCrudSchemas.ts

@@ -1,12 +1,8 @@
 import { reactive } from 'vue'
 import { eachTree, treeMap, filter } from '@/utils/tree'
-import { findIndex } from '@/utils'
-import { useDictStoreWithOut } from '@/store/modules/dict'
-import { useI18n } from '@/hooks/web/useI18n'
-import type { AxiosPromise } from 'axios'
-import { FormSchema } from '@/types/form'
-import { TableColumn } from '@/types/table'
-import { DescriptionsSchema } from '@/types/descriptions'
+import { FormSchema } from '@/components/Form'
+import { TableColumn } from '@/components/Table'
+import { DescriptionsSchema } from '@/components/Descriptions'
 
 export type CrudSchema = Omit<TableColumn, 'children'> & {
   search?: CrudSearchParams
@@ -16,39 +12,25 @@ export type CrudSchema = Omit<TableColumn, 'children'> & {
   children?: CrudSchema[]
 }
 
-type CrudSearchParams = {
-  // 是否显示在查询项
-  show?: boolean
-  // 字典名称,会去取全局的字典
-  dictName?: string
-  // 接口
-  api?: () => Promise<any>
-  // 搜索字段
-  field?: string
-} & Omit<FormSchema, 'field'>
-
-type CrudTableParams = {
-  // 是否显示表头
-  show?: boolean
-} & Omit<FormSchema, 'field'>
-
-type CrudFormParams = {
-  // 字典名称,会去取全局的字典
-  dictName?: string
-  // 接口
-  api?: () => Promise<any>
-  // 是否显示表单项
-  show?: boolean
-} & Omit<FormSchema, 'field'>
-
-type CrudDescriptionsParams = {
-  // 是否显示表单项
-  show?: boolean
-} & Omit<DescriptionsSchema, 'field'>
-
-const dictStore = useDictStoreWithOut()
-
-const { t } = useI18n()
+interface CrudSearchParams extends Omit<FormSchema, 'field'> {
+  // 是否隐藏在查询项
+  hidden?: boolean
+}
+
+interface CrudTableParams extends Omit<TableColumn, 'field'> {
+  // 是否隐藏表头
+  hidden?: boolean
+}
+
+interface CrudFormParams extends Omit<FormSchema, 'field'> {
+  // 是否隐藏表单项
+  hidden?: boolean
+}
+
+interface CrudDescriptionsParams extends Omit<DescriptionsSchema, 'field'> {
+  // 是否隐藏表单项
+  hidden?: boolean
+}
 
 interface AllSchemas {
   searchSchema: FormSchema[]
@@ -71,13 +53,14 @@ export const useCrudSchemas = (
     detailSchema: []
   })
 
-  const searchSchema = filterSearchSchema(crudSchema, allSchemas)
+  const searchSchema = filterSearchSchema(crudSchema)
+  // @ts-ignore
   allSchemas.searchSchema = searchSchema || []
 
   const tableColumns = filterTableSchema(crudSchema)
   allSchemas.tableColumns = tableColumns || []
 
-  const formSchema = filterFormSchema(crudSchema, allSchemas)
+  const formSchema = filterFormSchema(crudSchema)
   allSchemas.formSchema = formSchema
 
   const detailSchema = filterDescriptionsSchema(crudSchema)
@@ -89,55 +72,26 @@ export const useCrudSchemas = (
 }
 
 // 过滤 Search 结构
-const filterSearchSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => {
+const filterSearchSchema = (crudSchema: CrudSchema[]): FormSchema[] => {
   const searchSchema: FormSchema[] = []
+  const length = crudSchema.length
 
-  // 获取字典列表队列
-  const searchRequestTask: Array<() => Promise<void>> = []
-
-  eachTree(crudSchema, (schemaItem: CrudSchema) => {
-    // 判断是否显示
-    if (schemaItem?.search?.show) {
+  for (let i = 0; i < length; i++) {
+    const schemaItem = crudSchema[i]
+    // 判断是否隐藏
+    if (!schemaItem?.search?.hidden) {
       const searchSchemaItem = {
-        // 默认为 input
-        component: schemaItem.search.component || 'Input',
-        componentProps: {},
+        component: schemaItem?.search?.component || 'Input',
         ...schemaItem.search,
-        field: schemaItem?.search?.field || schemaItem.field,
-        label: schemaItem.search?.label || schemaItem.label
-      }
-
-      if (searchSchemaItem.dictName) {
-        // 如果有 dictName 则证明是从字典中获取数据
-        const dictArr = dictStore.getDictObj[searchSchemaItem.dictName]
-        searchSchemaItem.componentProps!.options = filterOptions(dictArr)
-      } else if (searchSchemaItem.api) {
-        searchRequestTask.push(async () => {
-          const res = await (searchSchemaItem.api as () => AxiosPromise)()
-          if (res) {
-            const index = findIndex(allSchemas.searchSchema, (v: FormSchema) => {
-              return v.field === searchSchemaItem.field
-            })
-            if (index !== -1) {
-              allSchemas.searchSchema[index]!.componentProps!.options = filterOptions(
-                res,
-                searchSchemaItem.componentProps.optionsAlias?.labelField
-              )
-            }
-          }
-        })
+        field: schemaItem.field,
+        label: schemaItem.label
       }
 
       // 删除不必要的字段
-      delete searchSchemaItem.show
-      delete searchSchemaItem.dictName
+      delete searchSchemaItem.hidden
 
       searchSchema.push(searchSchemaItem)
     }
-  })
-
-  for (const task of searchRequestTask) {
-    task()
   }
 
   return searchSchema
@@ -147,7 +101,7 @@ const filterSearchSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): F
 const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => {
   const tableColumns = treeMap<CrudSchema>(crudSchema, {
     conversion: (schema: CrudSchema) => {
-      if (schema?.table?.show !== false) {
+      if (!schema?.table?.hidden) {
         return {
           ...schema.table,
           ...schema
@@ -166,56 +120,28 @@ const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => {
 }
 
 // 过滤 form 结构
-const filterFormSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => {
+const filterFormSchema = (crudSchema: CrudSchema[]): FormSchema[] => {
   const formSchema: FormSchema[] = []
+  const length = crudSchema.length
 
-  // 获取字典列表队列
-  const formRequestTask: Array<() => Promise<void>> = []
-
-  eachTree(crudSchema, (schemaItem: CrudSchema) => {
-    // 判断是否显示
-    if (schemaItem?.form?.show !== false) {
+  for (let i = 0; i < length; i++) {
+    const formItem = crudSchema[i]
+    // 判断是否隐藏
+    if (!formItem?.form?.hidden) {
       const formSchemaItem = {
-        // 默认为 input
-        component: schemaItem?.form?.component || 'Input',
-        componentProps: {},
-        ...schemaItem.form,
-        field: schemaItem.field,
-        label: schemaItem.search?.label || schemaItem.label
-      }
-
-      if (formSchemaItem.dictName) {
-        // 如果有 dictName 则证明是从字典中获取数据
-        const dictArr = dictStore.getDictObj[formSchemaItem.dictName]
-        formSchemaItem.componentProps!.options = filterOptions(dictArr)
-      } else if (formSchemaItem.api) {
-        formRequestTask.push(async () => {
-          const res = await (formSchemaItem.api as () => AxiosPromise)()
-          if (res) {
-            const index = findIndex(allSchemas.formSchema, (v: FormSchema) => {
-              return v.field === formSchemaItem.field
-            })
-            if (index !== -1) {
-              allSchemas.formSchema[index]!.componentProps!.options = filterOptions(
-                res,
-                formSchemaItem.componentProps.optionsAlias?.labelField
-              )
-            }
-          }
-        })
+        component: formItem?.form?.component || 'Input',
+        ...formItem.form,
+        field: formItem.field,
+        label: formItem.label
       }
 
       // 删除不必要的字段
-      delete formSchemaItem.show
-      delete formSchemaItem.dictName
+      delete formSchemaItem.hidden
 
       formSchema.push(formSchemaItem)
     }
-  })
-
-  for (const task of formRequestTask) {
-    task()
   }
+
   return formSchema
 }
 
@@ -224,8 +150,8 @@ const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[
   const descriptionsSchema: FormSchema[] = []
 
   eachTree(crudSchema, (schemaItem: CrudSchema) => {
-    // 判断是否显示
-    if (schemaItem?.detail?.show !== false) {
+    // 判断是否隐藏
+    if (!schemaItem?.detail?.hidden) {
       const descriptionsSchemaItem = {
         ...schemaItem.detail,
         field: schemaItem.field,
@@ -233,7 +159,7 @@ const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[
       }
 
       // 删除不必要的字段
-      delete descriptionsSchemaItem.show
+      delete descriptionsSchemaItem.hidden
 
       descriptionsSchema.push(descriptionsSchemaItem)
     }
@@ -241,15 +167,3 @@ const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[
 
   return descriptionsSchema
 }
-
-// 给options添加国际化
-const filterOptions = (options: Recordable, labelField?: string) => {
-  return options?.map((v: Recordable) => {
-    if (labelField) {
-      v['labelField'] = t(v.labelField)
-    } else {
-      v['label'] = t(v.label)
-    }
-    return v
-  })
-}

Some files were not shown because too many files changed in this diff