Browse Source

Merge pull request #301 from kailong321200875/master

Archer 1 năm trước cách đây
mục cha
commit
95bb076c83
100 tập tin đã thay đổi với 4544 bổ sung2052 xóa
  1. 1 1
      .env.base
  2. 1 1
      .env.dev
  3. 1 1
      .env.gitee
  4. 1 1
      .env.pro
  5. 1 1
      .env.test
  6. 1 0
      .github/workflows/auto-merge.yml
  7. 1 1
      .github/workflows/release.yml
  8. 219 0
      .husky/commit-msg
  9. 250 0
      mock/menu/index.ts
  10. 536 25
      mock/role/index.ts
  11. 124 3
      mock/table/index.ts
  12. 52 50
      package.json
  13. 4 4
      src/App.vue
  14. 2 2
      src/api/common/index.ts
  15. 30 0
      src/api/department/index.ts
  16. 32 0
      src/api/department/types.ts
  17. 5 0
      src/api/menu/index.ts
  18. 5 0
      src/api/role/index.ts
  19. 5 1
      src/api/table/index.ts
  20. 2 0
      src/components/ConfigGlobal/index.ts
  21. 2 3
      src/components/ConfigGlobal/src/ConfigGlobal.vue
  22. 5 0
      src/components/ConfigGlobal/src/types/index.ts
  23. 7 41
      src/components/ContentDetailWrap/src/ContentDetailWrap.vue
  24. 2 0
      src/components/ContextMenu/index.ts
  25. 3 3
      src/components/ContextMenu/src/ContextMenu.vue
  26. 7 0
      src/components/ContextMenu/src/types/index.ts
  27. 2 0
      src/components/Descriptions/index.ts
  28. 118 108
      src/components/Descriptions/src/Descriptions.vue
  29. 4 0
      src/components/Descriptions/src/types/index.ts
  30. 41 28
      src/components/Dialog/src/Dialog.vue
  31. 2 3
      src/components/Editor/src/Editor.vue
  32. 37 4
      src/components/Form/index.ts
  33. 0 302
      src/components/Form/src/Form copy.vue
  34. 200 99
      src/components/Form/src/Form.vue
  35. 14 8
      src/components/Form/src/components/useRenderCheckbox.tsx
  36. 14 8
      src/components/Form/src/components/useRenderRadio.tsx
  37. 14 16
      src/components/Form/src/components/useRenderSelect.tsx
  38. 10 6
      src/components/Form/src/helper/componentMap.ts
  39. 52 41
      src/components/Form/src/helper/index.ts
  40. 0 17
      src/components/Form/src/types.ts
  41. 663 0
      src/components/Form/src/types/index.ts
  42. 2 0
      src/components/Icon/index.ts
  43. 19 38
      src/components/Icon/src/Icon.vue
  44. 1 0
      src/components/Icon/src/types/index.ts
  45. 2 2
      src/components/ImageViewer/index.ts
  46. 1 1
      src/components/ImageViewer/src/ImageViewer.vue
  47. 1 1
      src/components/ImageViewer/src/types/index.ts
  48. 2 0
      src/components/Infotip/index.ts
  49. 2 2
      src/components/Infotip/src/Infotip.vue
  50. 1 1
      src/components/Infotip/src/types/index.ts
  51. 4 14
      src/components/InputPassword/src/InputPassword.vue
  52. 2 0
      src/components/LocaleDropdown/index.ts
  53. 0 0
      src/components/LocaleDropdown/src/types/index.ts
  54. 27 28
      src/components/Menu/src/Menu.vue
  55. 4 0
      src/components/Permission/index.ts
  56. 29 0
      src/components/Permission/src/Permission.vue
  57. 14 0
      src/components/Permission/src/utils.ts
  58. 2 0
      src/components/Qrcode/index.ts
  59. 1 1
      src/components/Qrcode/src/Qrcode.vue
  60. 0 0
      src/components/Qrcode/src/types/index.ts
  61. 12 0
      src/components/Search/index.ts
  62. 186 69
      src/components/Search/src/Search.vue
  63. 59 0
      src/components/Search/src/components/ActionButton.vue
  64. 16 0
      src/components/Search/src/types/index.ts
  65. 7 11
      src/components/Setting/src/Setting.vue
  66. 2 3
      src/components/SizeDropdown/src/SizeDropdown.vue
  67. 0 3
      src/components/Sticky/index.ts
  68. 0 141
      src/components/Sticky/src/Sticky.vue
  69. 12 3
      src/components/Table/index.ts
  70. 371 129
      src/components/Table/src/Table.vue
  71. 151 0
      src/components/Table/src/components/TableActions.vue
  72. 0 0
      src/components/Table/src/helper/index.ts
  73. 0 26
      src/components/Table/src/types.ts
  74. 97 0
      src/components/Table/src/types/index.ts
  75. 20 27
      src/components/TagsView/src/TagsView.vue
  76. 6 4
      src/components/UserInfo/src/UserInfo.vue
  77. 8 9
      src/components/UserInfo/src/components/LockDialog.vue
  78. 4 4
      src/components/UserInfo/src/components/LockPage.vue
  79. 2 0
      src/components/index.ts
  80. 1 1
      src/config/axios/config.ts
  81. 9 9
      src/config/axios/index.ts
  82. 2 2
      src/config/axios/service.ts
  83. 0 0
      src/config/axios/types/index.ts
  84. 5 15
      src/directives/permission/hasPermi.ts
  85. 0 0
      src/hooks/event/useEmitt.ts
  86. 0 17
      src/hooks/web/useCache.ts
  87. 1 1
      src/hooks/web/useConfigGlobal.ts
  88. 51 137
      src/hooks/web/useCrudSchemas.ts
  89. 52 10
      src/hooks/web/useForm.ts
  90. 1 2
      src/hooks/web/useIcon.ts
  91. 1 1
      src/hooks/web/useNow.ts
  92. 91 0
      src/hooks/web/useSearch.ts
  93. 31 0
      src/hooks/web/useStorage.ts
  94. 126 129
      src/hooks/web/useTable.ts
  95. 89 6
      src/locales/en.ts
  96. 87 8
      src/locales/zh-CN.ts
  97. 42 42
      src/permission.ts
  98. 390 346
      src/router/index.ts
  99. 22 25
      src/store/modules/app.ts
  100. 6 6
      src/store/modules/locale.ts

+ 1 - 1
.env.base

@@ -2,7 +2,7 @@
 NODE_ENV=development
 
 # 接口前缀
-VITE_API_BASEPATH=base
+VITE_API_BASE_PATH=base
 
 # 打包路径
 VITE_BASE_PATH=/

+ 1 - 1
.env.dev

@@ -2,7 +2,7 @@
 NODE_ENV=production
 
 # 接口前缀
-VITE_API_BASEPATH=dev
+VITE_API_BASE_PATH=dev
 
 # 打包路径
 VITE_BASE_PATH=/dist-dev/

+ 1 - 1
.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/

+ 1 - 1
.env.pro

@@ -2,7 +2,7 @@
 NODE_ENV=production
 
 # 接口前缀
-VITE_API_BASEPATH=pro
+VITE_API_BASE_PATH=pro
 
 # 打包路径
 VITE_BASE_PATH=/

+ 1 - 1
.env.test

@@ -2,7 +2,7 @@
 NODE_ENV=production
 
 # 接口前缀
-VITE_API_BASEPATH=test
+VITE_API_BASE_PATH=test
 
 # 打包路径
 VITE_BASE_PATH=/dist-test/

+ 1 - 0
.github/workflows/auto-merge.yml

@@ -24,6 +24,7 @@ jobs:
       - name: Automerge
         uses: 'pascalgn/automerge-action@v0.14.3'
         env:
+          BASE_BRANCHES: 'release'
           GITHUB_TOKEN: '${{ secrets.TOKEN }}'
           MERGE_LABELS: ''
           MERGE_FILTER_AUTHOR: 'kailong321200875'

+ 1 - 1
.github/workflows/release.yml

@@ -1,7 +1,7 @@
 on:
   push:
     branches:
-      - master
+      - release
 
 name: Release
 

+ 219 - 0
.husky/commit-msg

@@ -0,0 +1,219 @@
+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++) {
+  departmentList.push({
+    // 部门名称
+    departmentName: citys[i],
+    id: toAnyString(),
+    createTime: '@datetime',
+    // 状态
+    status: Mock.Random.integer(0, 1),
+    // 备注
+    remark: '@cword(10, 15)',
+    children: [
+      {
+        // 部门名称
+        departmentName: '研发部',
+        createTime: '@datetime',
+        // 状态
+        status: Mock.Random.integer(0, 1),
+        id: toAnyString(),
+        remark: '@cword(10, 15)'
+      },
+      {
+        // 部门名称
+        departmentName: '产品部',
+        createTime: '@datetime',
+        // 状态
+        status: Mock.Random.integer(0, 1),
+        id: toAnyString(),
+        remark: '@cword(10, 15)'
+      },
+      {
+        // 部门名称
+        departmentName: '运营部',
+        createTime: '@datetime',
+        // 状态
+        status: Mock.Random.integer(0, 1),
+        id: toAnyString(),
+        remark: '@cword(10, 15)'
+      },
+      {
+        // 部门名称
+        departmentName: '市场部',
+        createTime: '@datetime',
+        // 状态
+        status: Mock.Random.integer(0, 1),
+        id: toAnyString(),
+        remark: '@cword(10, 15)'
+      },
+      {
+        // 部门名称
+        departmentName: '销售部',
+        createTime: '@datetime',
+        // 状态
+        status: Mock.Random.integer(0, 1),
+        id: toAnyString(),
+        remark: '@cword(10, 15)'
+      },
+      {
+        // 部门名称
+        departmentName: '客服部',
+        createTime: '@datetime',
+        // 状态
+        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[]

+ 250 - 0
mock/menu/index.ts

@@ -0,0 +1,250 @@
+import config from '@/config/axios/config'
+import { MockMethod } from 'vite-plugin-mock'
+import Mock from 'mockjs'
+
+const { code } = config
+
+const timeout = 1000
+
+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,
+                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'
+                            }
+                          }
+                        ]
+                      },
+                      {
+                        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'
+                        }
+                      }
+                    ]
+                  },
+                  {
+                    path: 'menu2',
+                    name: 'Menu2Demo',
+                    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',
+                    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',
+                    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',
+                    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',
+                    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',
+                    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[]

+ 536 - 25
mock/role/index.ts

@@ -1,5 +1,7 @@
 import config from '@/config/axios/config'
 import { MockMethod } from 'vite-plugin-mock'
+import Mock from 'mockjs'
+import { toAnyString } from '@/utils'
 
 const { code } = config
 
@@ -105,14 +107,6 @@ const adminList = [
             meta: {
               title: 'UseForm'
             }
-          },
-          {
-            path: 'ref-form',
-            component: 'views/Components/Form/RefForm',
-            name: 'RefForm',
-            meta: {
-              title: 'RefForm'
-            }
           }
         ]
       },
@@ -143,13 +137,29 @@ const adminList = [
             }
           },
           {
-            path: 'ref-table',
-            component: 'views/Components/Table/RefTable',
-            name: 'RefTable',
+            path: 'tree-table',
+            component: 'views/Components/Table/TreeTable',
+            name: 'TreeTable',
+            meta: {
+              title: 'TreeTable'
+            }
+          },
+          {
+            path: 'table-image-preview',
+            component: 'views/Components/Table/TableImagePreview',
+            name: 'TableImagePreview',
             meta: {
-              title: 'RefTable'
+              title: 'router.PicturePreview'
             }
           }
+          // {
+          //   path: 'ref-table',
+          //   component: 'views/Components/Table/RefTable',
+          //   name: 'RefTable',
+          //   meta: {
+          //     title: 'RefTable'
+          //   }
+          // }
         ]
       },
       {
@@ -259,14 +269,6 @@ const adminList = [
         meta: {
           title: 'router.inputPassword'
         }
-      },
-      {
-        path: 'sticky',
-        component: 'views/Components/Sticky',
-        name: 'Sticky',
-        meta: {
-          title: 'router.sticky'
-        }
       }
     ]
   },
@@ -290,13 +292,21 @@ const adminList = [
         }
       },
       {
-        path: 'useCrudSchemas',
-        component: 'views/hooks/useCrudSchemas',
-        name: 'UseCrudSchemas',
+        path: 'useOpenTab',
+        component: 'views/hooks/useOpenTab',
+        name: 'UseOpenTab',
         meta: {
-          title: 'useCrudSchemas'
+          title: 'useOpenTab'
         }
       }
+      // {
+      //   path: 'useCrudSchemas',
+      //   component: 'views/hooks/useCrudSchemas',
+      //   name: 'UseCrudSchemas',
+      //   meta: {
+      //     title: 'useCrudSchemas'
+      //   }
+      // }
     ]
   },
   {
@@ -462,6 +472,59 @@ const adminList = [
         }
       }
     ]
+  },
+  {
+    path: '/authorization',
+    component: '#',
+    redirect: '/authorization/user',
+    name: 'Authorization',
+    meta: {
+      title: 'router.authorization',
+      icon: 'eos-icons:role-binding',
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: 'department',
+        component: 'views/Authorization/Department/Department',
+        name: 'Department',
+        meta: {
+          title: 'router.department'
+        }
+      },
+      {
+        path: 'user',
+        component: 'views/Authorization/User/User',
+        name: 'User',
+        meta: {
+          title: 'router.user'
+        }
+      },
+      {
+        path: 'menu',
+        component: 'views/Authorization/Menu/Menu',
+        name: 'Menu',
+        meta: {
+          title: 'router.menuManagement'
+        }
+      },
+      {
+        path: 'role',
+        component: 'views/Authorization/Role/Role',
+        name: 'Role',
+        meta: {
+          title: 'router.role'
+        }
+      },
+      {
+        path: 'test',
+        component: 'views/Authorization/Test/Test',
+        name: 'Test',
+        meta: {
+          title: 'router.permission'
+        }
+      }
+    ]
   }
 ]
 
@@ -481,6 +544,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',
@@ -498,7 +563,8 @@ const testList: string[] = [
   '/Components/Sticky',
   '/hooks',
   '/hooks/useWatermark',
-  '/hooks/useCrudSchemas',
+  '/hooks/useOpenTab',
+  // '/hooks/useCrudSchemas',
   '/level',
   '/level/menu1',
   '/level/menu1/menu1-1',
@@ -511,12 +577,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 [
   // 列表接口
   {
@@ -532,5 +1027,21 @@ export default [
         }
       }
     }
+  },
+  {
+    url: '/role/table',
+    method: 'get',
+    timeout,
+    response: () => {
+      return {
+        data: {
+          code: code,
+          data: {
+            list: List,
+            total: 4
+          }
+        }
+      }
+    }
   }
 ] as MockMethod[]

+ 124 - 3
mock/table/index.ts

@@ -12,7 +12,7 @@ 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',

+ 52 - 50
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",
@@ -26,88 +26,90 @@
     "p": "plop"
   },
   "dependencies": {
-    "@iconify/iconify": "^3.1.0",
-    "@vueuse/core": "^10.1.2",
+    "@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": "^3.0.0",
+    "@zxcvbn-ts/core": "^3.0.3",
     "animate.css": "^4.1.1",
     "axios": "^1.4.0",
-    "dayjs": "^1.11.7",
-    "echarts": "^5.4.2",
+    "dayjs": "^1.11.9",
+    "echarts": "^5.4.3",
     "echarts-wordcloud": "^2.1.0",
-    "element-plus": "2.3.4",
+    "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.36",
+    "pinia": "^2.1.4",
     "pinia-plugin-persist": "^1.0.0",
     "qrcode": "^1.5.3",
-    "qs": "^6.11.1",
-    "url": "^0.11.0",
-    "vue": "3.2.47",
+    "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.6.3",
-    "@commitlint/config-conventional": "^17.6.3",
-    "@iconify/json": "^2.2.62",
-    "@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": "^20.1.1",
+    "@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.59.5",
-    "@typescript-eslint/parser": "^5.59.5",
-    "@unocss/transformer-variant-group": "^0.51.12",
-    "@vitejs/plugin-legacy": "^4.0.3",
-    "@vitejs/plugin-vue": "^4.2.1",
+    "@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.9.8",
+    "@vue-macros/volar": "^0.12.2",
     "autoprefixer": "^10.4.14",
-    "consola": "^3.1.0",
-    "eslint": "^8.40.0",
+    "consola": "^3.2.3",
+    "eslint": "^8.45.0",
     "eslint-config-prettier": "^8.8.0",
-    "eslint-define-config": "^1.20.0",
-    "eslint-plugin-prettier": "^4.2.1",
-    "eslint-plugin-vue": "^9.11.1",
+    "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.2",
+    "lint-staged": "^13.2.3",
     "plop": "^3.1.2",
-    "postcss": "^8.4.23",
+    "postcss": "^8.4.26",
     "postcss-html": "^1.5.0",
     "postcss-less": "^6.0.0",
-    "prettier": "^2.8.8",
-    "rimraf": "^5.0.0",
-    "rollup": "^3.21.5",
-    "stylelint": "^15.6.1",
+    "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": "^12.0.0",
-    "stylelint-config-standard": "^33.0.0",
+    "stylelint-config-recommended": "^13.0.0",
+    "stylelint-config-standard": "^34.0.0",
     "stylelint-order": "^6.0.3",
-    "terser": "^5.17.2",
-    "typescript": "5.0.4",
-    "unocss": "^0.51.12",
-    "unplugin-vue-define-options": "^1.3.5",
-    "vite": "4.3.5",
+    "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": "^3.0.0",
+    "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",
-    "vue-tsc": "^1.6.4"
+    "vue-tsc": "^1.8.5"
   },
   "engines": {
     "node": ">= 14.18.0"

+ 4 - 4
src/App.vue

@@ -4,7 +4,7 @@ import { useAppStore } from '@/store/modules/app'
 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()
 
@@ -16,12 +16,12 @@ const currentSize = computed(() => appStore.getCurrentSize)
 
 const greyMode = computed(() => appStore.getGreyMode)
 
-const { wsCache } = useCache()
+const { getStorage } = useStorage()
 
 // 根据浏览器当前主题设置系统主题色
 const setDefaultTheme = () => {
-  if (wsCache.get('isDark') !== null) {
-    appStore.setIsDark(wsCache.get('isDark'))
+  if (getStorage('isDark') !== null) {
+    appStore.setIsDark(getStorage('isDark'))
     return
   }
   const isDarkTheme = isDark()

+ 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 })
 }

+ 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 b-b-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)] dark:border-[var(--el-border-color)] dark:border-1px'
-    ]"
-  >
-    <div
-      v-if="title"
-      :class="[
-        `${prefixCls}-header`,
-        'h-50px flex justify-between items-center b-b-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="b-b-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 }

+ 0 - 302
src/components/Form/src/Form copy.vue

@@ -1,302 +0,0 @@
-<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 { propTypes } from '@/utils/propTypes'
-import { getSlot } from '@/utils/tsxHelper'
-import {
-  setTextPlaceholder,
-  setGridProp,
-  setComponentProps,
-  setItemComponentSlots,
-  initModel,
-  setFormItemSlots
-} 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 { FormProps } from './types'
-import { Icon } from '@/components/Icon'
-import { FormSchema, FormSetPropsType } from '@/types/form'
-
-const { getPrefixCls } = useDesign()
-
-const prefixCls = getPrefixCls('form')
-
-export default defineComponent({
-  name: 'Form',
-  props: {
-    // 生成Form的布局结构数组
-    schema: {
-      type: Array as PropType<FormSchema[]>,
-      default: () => []
-    },
-    // 是否需要栅格布局
-    isCol: propTypes.bool.def(true),
-    // 表单数据对象
-    model: {
-      type: Object as PropType<Recordable>,
-      default: () => ({})
-    },
-    // 是否自动设置placeholder
-    autoSetPlaceholder: propTypes.bool.def(true),
-    // 是否自定义内容
-    isCustom: propTypes.bool.def(false),
-    // 表单label宽度
-    labelWidth: propTypes.oneOfType([String, Number]).def('auto')
-  },
-  emits: ['register'],
-  setup(props, { slots, expose, emit }) {
-    // element form 实例
-    const elFormRef = ref<ComponentRef<typeof ElForm>>()
-
-    // useForm传入的props
-    const outsideProps = ref<FormProps>({})
-
-    const mergeProps = ref<FormProps>({})
-
-    const getProps = computed(() => {
-      const propsObj = { ...props }
-      Object.assign(propsObj, unref(mergeProps))
-      return propsObj
-    })
-
-    // 表单数据
-    const formModel = ref<Recordable>({})
-
-    onMounted(() => {
-      emit('register', unref(elFormRef)?.$parent, unref(elFormRef))
-    })
-
-    // 对表单赋值
-    const setValues = (data: Recordable = {}) => {
-      formModel.value = Object.assign(unref(formModel), data)
-    }
-
-    const setProps = (props: FormProps = {}) => {
-      mergeProps.value = Object.assign(unref(mergeProps), props)
-      outsideProps.value = props
-    }
-
-    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 setSchema = (schemaProps: FormSetPropsType[]) => {
-      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 getElFormRef = (): ComponentRef<typeof ElForm> => {
-      return unref(elFormRef) as ComponentRef<typeof ElForm>
-    }
-
-    expose({
-      setValues,
-      formModel,
-      setProps,
-      delSchema,
-      addSchema,
-      setSchema,
-      getElFormRef
-    })
-
-    // 监听表单结构化数组,重新生成formModel
-    watch(
-      () => unref(getProps).schema,
-      (schema = []) => {
-        formModel.value = initModel(schema, unref(formModel))
-      },
-      {
-        immediate: true,
-        deep: true
-      }
-    )
-
-    // 渲染包裹标签,是否使用栅格布局
-    const renderWrap = () => {
-      const { isCol } = unref(getProps)
-      const content = isCol ? (
-        <ElRow gutter={20}>{renderFormItemWrap()}</ElRow>
-      ) : (
-        renderFormItemWrap()
-      )
-      return content
-    }
-
-    // 是否要渲染el-col
-    const renderFormItemWrap = () => {
-      // hidden属性表示隐藏,不做渲染
-      const { schema = [], isCol } = unref(getProps)
-
-      return schema
-        .filter((v) => !v.hidden)
-        .map((item) => {
-          // 如果是 Divider 组件,需要自己占用一行
-          const isDivider = item.component === 'Divider'
-          const Com = componentMap['Divider'] as ReturnType<typeof defineComponent>
-          return isDivider ? (
-            <Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com>
-          ) : isCol ? (
-            // 如果需要栅格,需要包裹 ElCol
-            <ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol>
-          ) : (
-            renderFormItem(item)
-          )
-        })
-    }
-
-    // 渲染formItem
-    const renderFormItem = (item: FormSchema) => {
-      // 单独给只有options属性的组件做判断
-      const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
-      const componentSlots = (item?.componentProps as any)?.slots || {}
-      const slotsMap: Recordable = {
-        ...setItemComponentSlots(unref(formModel), componentSlots)
-      }
-      if (
-        item?.component !== 'SelectV2' &&
-        item?.component !== 'Cascader' &&
-        item?.componentProps?.options
-      ) {
-        slotsMap.default = () => renderOptions(item)
-      }
-
-      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)
-              ) : (
-                <Com
-                  vModel={formModel.value[item.field]}
-                  {...(autoSetPlaceholder && setTextPlaceholder(item))}
-                  {...setComponentProps(item)}
-                  style={item.componentProps?.style}
-                  {...(notRenderOptions.includes(item?.component as string) &&
-                  item?.componentProps?.options
-                    ? { options: item?.componentProps?.options || [] }
-                    : {})}
-                >
-                  {{ ...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
-      }
-    }
-
-    // 过滤传入Form组件的属性
-    const getFormBindValue = () => {
-      // 避免在标签上出现多余的属性
-      const delKeys = ['schema', 'isCol', 'autoSetPlaceholder', 'isCustom', 'model']
-      const props = { ...unref(getProps) }
-      for (const key in props) {
-        if (delKeys.indexOf(key) !== -1) {
-          delete props[key]
-        }
-      }
-      return props
-    }
-
-    return () => (
-      <ElForm
-        ref={elFormRef}
-        {...getFormBindValue()}
-        model={props.isCustom ? props.model : formModel}
-        class={prefixCls}
-      >
-        {{
-          // 如果需要自定义,就什么都不渲染,而是提供默认插槽
-          default: () => {
-            const { isCustom } = unref(getProps)
-            return isCustom ? getSlot(slots, 'default') : renderWrap()
-          }
-        }}
-      </ElForm>
-    )
-  }
-})
-</script>
-
-<style lang="less" scoped>
-.@{elNamespace}-form.@{namespace}-form .@{elNamespace}-row {
-  margin-right: 0 !important;
-  margin-left: 0 !important;
-}
-</style>

+ 200 - 99
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,19 +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 { ComponentNameEnum, SelectComponentProps } from '@/types/components.d'
+import {
+  FormSchema,
+  FormSetProps,
+  ComponentNameEnum,
+  SelectComponentProps,
+  RadioGroupComponentProps,
+  CheckboxGroupComponentProps
+} from './types'
+
+const { renderSelectOptions } = useRenderSelect()
+const { renderRadioOptions } = useRenderRadio()
+const { renderCheckboxOptions } = useRenderCheckbox()
 
 const { getPrefixCls } = useDesign()
 
@@ -39,7 +55,7 @@ export default defineComponent({
     isCol: propTypes.bool.def(true),
     // 表单数据对象
     model: {
-      type: Object as PropType<Recordable>,
+      type: Object as PropType<any>,
       default: () => ({})
     },
     // 是否自动设置placeholder
@@ -47,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 }) {
@@ -65,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))
@@ -79,6 +124,7 @@ export default defineComponent({
 
     const setProps = (props: FormProps = {}) => {
       mergeProps.value = Object.assign(unref(mergeProps), props)
+      // @ts-ignore
       outsideProps.value = props
     }
 
@@ -100,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) {
@@ -111,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({
@@ -122,7 +202,8 @@ export default defineComponent({
       delSchema,
       addSchema,
       setSchema,
-      getElFormRef
+      getComponentExpose,
+      getFormItemExpose
     })
 
     // 监听表单结构化数组,重新生成formModel
@@ -154,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'
@@ -172,99 +253,119 @@ export default defineComponent({
 
     // 渲染formItem
     const renderFormItem = (item: FormSchema) => {
-      // 单独给只有options属性的组件做判断
-      // const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
-      const componentSlots = (item?.componentProps as any)?.slots || {}
-      const slotsMap: Recordable = {
-        ...setItemComponentSlots(unref(formModel), componentSlots)
+      // 如果有optionApi,优先使用optionApi
+      if (item.optionApi) {
+        // 内部自动调用接口,不影响其它渲染
+        getOptions(item.optionApi, item)
       }
-      // 如果是select组件,并且没有自定义模板,自动渲染options
-      if (item.component === ComponentNameEnum.SELECT) {
-        slotsMap.default = !componentSlots.default
-          ? () => renderOptions(item)
-          : () => {
-              return componentSlots.default(
-                unref((item?.componentProps as SelectComponentProps)?.options)
-              )
+      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 { autoSetPlaceholder } = unref(getProps)
+
+            const componentSlots = (item?.componentProps as any)?.slots || {}
+            const slotsMap: Recordable = {
+              ...setItemComponentSlots(componentSlots)
             }
-      }
-      // if (
-      //   item?.component !== 'SelectV2' &&
-      //   item?.component !== 'Cascader' &&
-      //   item?.componentProps?.options
-      // ) {
-      //   slotsMap.default = () => renderOptions(item)
-      // }
-
-      // 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 || ''}>
-          {{
-            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)
-              ) : (
+            // // 如果是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}
+                  style={item.componentProps?.style || {}}
                 >
                   {{ ...slotsMap }}
                 </Com>
               )
             }
-          }}
-        </ElFormItem>
-      )
-    }
 
-    // 渲染options
-    const renderOptions = (item: FormSchema) => {
-      switch (item.component) {
-        case ComponentNameEnum.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组件的属性
@@ -277,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>
       )

+ 14 - 16
src/components/Form/src/components/useRenderSelect.tsx

@@ -1,22 +1,20 @@
 import { ElOption, ElOptionGroup } from 'element-plus'
-import { getSlot } from '@/utils/tsxHelper'
-import { Slots } from 'vue'
-import { FormSchema } from '@/types/form'
-import { SelectComponentProps, SelectOption } 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 componentsProps = item?.componentProps as SelectComponentProps
+    const optionGroupDefaultSlot = componentsProps?.slots?.optionGroupDefault
     // 如果有别名,就取别名
-    const labelAlias = componentsProps?.labelAlias
+    const labelAlias = componentsProps?.props?.label
+    const keyAlias = componentsProps?.props?.key
     return componentsProps?.options?.map((option) => {
       if (option?.options?.length) {
         return optionGroupDefaultSlot ? (
           optionGroupDefaultSlot(option)
         ) : (
-          <ElOptionGroup label={option[labelAlias || 'label']}>
+          <ElOptionGroup label={option[labelAlias || 'label']} key={option[keyAlias || 'key']}>
             {{
               default: () =>
                 option?.options?.map((v) => {
@@ -35,17 +33,17 @@ export const useRenderSelect = (slots: Slots) => {
   const renderSelectOptionItem = (item: FormSchema, option: SelectOption) => {
     // 如果有别名,就取别名
     const componentsProps = item.componentProps as SelectComponentProps
-    const labelAlias = componentsProps?.labelAlias
-    const valueAlias = componentsProps?.valueAlias
+    const labelAlias = componentsProps?.props?.label
+    const valueAlias = componentsProps?.props?.value
+    const keyAlias = componentsProps?.props?.key
     const optionDefaultSlot = componentsProps.slots?.optionDefault
 
-    const { label, value, ...other } = option
-
     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: () => (optionDefaultSlot ? optionDefaultSlot(option) : undefined)

+ 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 }

+ 52 - 41
src/components/Form/src/helper.ts → src/components/Form/src/helper/index.ts

@@ -1,10 +1,8 @@
 import { useI18n } from '@/hooks/web/useI18n'
-import { unref, type Slots } from 'vue'
-import { getSlot } from '@/utils/tsxHelper'
-import { PlaceholderMoel } from './types'
-import { FormSchema } from '@/types/form'
-import { ColProps } from '@/types/components'
+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()
 
@@ -14,20 +12,32 @@ const { t } = useI18n()
  * @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)) {
+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 string)) {
+  if (selectMap.includes(schema?.component as ComponentNameEnum)) {
     // 一些范围选择器
     const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange']
     if (
       twoTextMap.includes(
-        (schema?.componentProps?.type || schema?.componentProps?.isRange) as string
+        ((schema?.componentProps as any)?.type ||
+          (schema?.componentProps as any)?.isRange) as string
       )
     ) {
       return {
@@ -74,14 +84,30 @@ export const setGridProp = (col: ColProps = {}): ColProps => {
  */
 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
+    ...item.componentProps,
+    ...newOnEvents
   }
   // 需要删除额外的属性
   if (componentProps.slots) {
     delete componentProps.slots
   }
+  if (componentProps.on) {
+    delete componentProps.on
+  }
   return componentProps
 }
 
@@ -90,16 +116,16 @@ export const setComponentProps = (item: FormSchema): Recordable => {
  * @param formModel 表单数据
  * @param slotsProps 插槽属性
  */
-export const setItemComponentSlots = (formModel: any, slotsProps: Recordable = {}): Recordable => {
+export const setItemComponentSlots = (slotsProps: Recordable = {}): Recordable => {
   const slotObj: Recordable = {}
   for (const key in slotsProps) {
     if (slotsProps[key]) {
       if (isFunction(slotsProps[key])) {
-        slotObj[key] = () => {
-          return slotsProps[key]?.(formModel)
+        slotObj[humpToDash(key)] = (...args: any[]) => {
+          return slotsProps[key]?.(...args)
         }
       } else {
-        slotObj[key] = () => {
+        slotObj[humpToDash(key)] = () => {
           return slotsProps[key]
         }
       }
@@ -118,34 +144,19 @@ export const setItemComponentSlots = (formModel: any, slotsProps: Recordable = {
 export const initModel = (schema: FormSchema[], formModel: Recordable) => {
   const model: Recordable = { ...formModel }
   schema.map((v) => {
-    // 如果是hidden,就删除对应的值
-    if (v.hidden) {
+    if (v.remove) {
       delete model[v.field]
-    } else if (v.component && v.component !== 'Divider') {
-      const hasField = Reflect.has(model, v.field)
+    } else if (v.component !== 'Divider') {
+      // const hasField = Reflect.has(model, v.field)
+      const hasField = get(model, v.field)
       // 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值
-      model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : ''
+      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
 }
-
-/**
- * @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
-}

+ 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 }

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


+ 27 - 28
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,15 +123,15 @@ 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;
@@ -182,9 +181,9 @@ export default defineComponent({
     .@{elNamespace}-menu-item.is-active {
       position: relative;
 
-      &:after {
-        .is-active--after;
-      }
+      // &:after {
+      //   .is-active--after;
+      // }
     }
 
     // 设置子菜单的背景颜色
@@ -205,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;
+      // }
     }
   }
 
@@ -255,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 {
@@ -292,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('--layout-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 flex items-center justify-center 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>

+ 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
+}

+ 20 - 27
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)
 }
@@ -270,7 +272,8 @@ watch(
     >
       <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">
@@ -386,7 +389,8 @@ watch(
     >
       <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
@@ -396,7 +400,8 @@ watch(
     >
       <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
@@ -459,7 +464,8 @@ watch(
       >
         <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>
@@ -476,36 +482,29 @@ watch(
 
   &__tool {
     position: relative;
-    &:after {
+
+    &::before {
       position: absolute;
       top: 1px;
       left: 0;
       width: 100%;
       height: calc(~'100% - 1px');
-      border-left: 1px solid var(--layout-border-color);
+      border-left: 1px solid var(--el-border-color);
       content: '';
     }
 
     &--first {
-      &:after {
-        display: none;
-      }
-      &:before {
+      &::before {
         position: absolute;
         top: 1px;
         left: 0;
         width: 100%;
         height: calc(~'100% - 1px');
-        border-right: 1px solid var(--layout-border-color);
+        border-right: 1px solid var(--el-border-color);
+        border-left: none;
         content: '';
       }
     }
-
-    &:hover {
-      :deep(span) {
-        color: var(--el-color-black) !important;
-      }
-    }
   }
 
   &__item {
@@ -544,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;
       }
     }
@@ -554,14 +553,8 @@ watch(
 .dark {
   .@{prefix-cls} {
     &__tool {
-      &:hover {
-        :deep(span) {
-          color: #fff !important;
-        }
-      }
-
       &--first {
-        &:after {
+        &::after {
           display: none;
         }
       }
@@ -582,7 +575,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;
         }
       }

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

@@ -1,7 +1,7 @@
 <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'
@@ -24,7 +24,7 @@ const prefixCls = getPrefixCls('user-info')
 
 const { t } = useI18n()
 
-const { wsCache } = useCache()
+const { clear } = useStorage()
 
 const { replace } = useRouter()
 
@@ -37,7 +37,7 @@ const loginOut = () => {
     .then(async () => {
       const res = await loginOutApi().catch(() => {})
       if (res) {
-        wsCache.clear()
+        clear()
         tagsViewStore.delAllViews()
         resetRouter() // 重置静态路由表
         replace('/login')
@@ -94,7 +94,9 @@ const toDocument = () => {
 <style scoped lang="less">
 .fade-bottom-enter-active,
 .fade-bottom-leave-active {
-  transition: opacity 0.25s, transform 0.3s;
+  transition:
+    opacity 0.25s,
+    transform 0.3s;
 }
 
 .fade-bottom-enter-from {

+ 8 - 9
src/components/UserInfo/src/components/LockDialog.vue

@@ -1,12 +1,12 @@
 <script setup lang="ts">
 import { useI18n } from '@/hooks/web/useI18n'
-import { ref, unref } from 'vue'
+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 '@/types/form'
+import { FormSchema } from '@/components/Form'
 import { ElButton } from 'element-plus'
 import { useDesign } from '@/hooks/web/useDesign'
 import { useLockStore } from '@/store/modules/lock'
@@ -54,14 +54,13 @@ const schema: FormSchema[] = reactive([
   }
 ])
 
-const { register, formRef, methods } = useForm({
-  schema
-})
+const { formRegister, formMethods } = useForm()
 
-const { getFormData } = methods
+const { getFormData, getElFormExpose } = formMethods
 
-const handleLock = () => {
-  unref(formRef)?.validate(async (valid) => {
+const handleLock = async () => {
+  const formExpose = await getElFormExpose()
+  formExpose?.validate(async (valid) => {
     if (valid) {
       dialogVisible.value = false
       const formData = await getFormData()
@@ -86,7 +85,7 @@ const handleLock = () => {
       <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" :rules="rules" @register="register" />
+    <Form :is-col="false" :schema="schema" :rules="rules" @register="formRegister" />
     <template #footer>
       <ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton>
     </template>

+ 4 - 4
src/components/UserInfo/src/components/LockPage.vue

@@ -3,10 +3,10 @@ import { ref } from 'vue'
 import { ElInput, ElButton } from 'element-plus'
 import { resetRouter } from '@/router'
 import { useRouter } from 'vue-router'
-import { useCache } from '@/hooks/web/useCache'
+import { useStorage } from '@/hooks/web/useStorage'
 import { useLockStore } from '@/store/modules/lock'
 import { useI18n } from '@/hooks/web/useI18n'
-import { useNow } from './useNow'
+import { useNow } from '@/hooks/web/useNow'
 import { useDesign } from '@/hooks/web/useDesign'
 import { Icon } from '@/components/Icon'
 import { loginOutApi } from '@/api/login'
@@ -14,7 +14,7 @@ import { useTagsViewStore } from '@/store/modules/tagsView'
 
 const tagsViewStore = useTagsViewStore()
 
-const { wsCache } = useCache()
+const { clear } = useStorage()
 
 const { replace } = useRouter()
 
@@ -51,7 +51,7 @@ async function unLock() {
 async function goLogin() {
   const res = await loginOutApi().catch(() => {})
   if (res) {
-    wsCache.clear()
+    clear()
     tagsViewStore.delAllViews()
     resetRouter() // 重置静态路由表
     lockStore.resetLockInfo()

+ 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)
 }

+ 1 - 1
src/config/axios/config.ts

@@ -4,7 +4,7 @@ import {
   AxiosRequestHeaders,
   AxiosError,
   InternalAxiosRequestConfig
-} from './type'
+} from './types'
 import { ElMessage } from 'element-plus'
 import qs from 'qs'
 

+ 9 - 9
src/config/axios/index.ts

@@ -4,7 +4,7 @@ import config from './config'
 
 const { defaultHeaders } = config
 
-const request = (option: any) => {
+const request = (option: AxiosConfig) => {
   const { url, method, params, data, headersType, responseType } = option
   return service.request({
     url: url,
@@ -19,17 +19,17 @@ const request = (option: any) => {
 }
 
 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: any) => {
-    return request({ method: 'post', ...option }) as unknown as T
+  post: <T = any>(option: AxiosConfig) => {
+    return request({ method: 'post', ...option }) as Promise<IResponse<T>>
   },
-  delete: <T = any>(option: any) => {
-    return request({ method: 'delete', ...option }) as unknown as T
+  delete: <T = any>(option: AxiosConfig) => {
+    return request({ method: 'delete', ...option }) as Promise<IResponse<T>>
   },
-  put: <T = any>(option: any) => {
-    return request({ method: 'put', ...option }) as unknown as T
+  put: <T = any>(option: AxiosConfig) => {
+    return request({ method: 'put', ...option }) as Promise<IResponse<T>>
   },
   cancelRequest: (url: string | string[]) => {
     return service.cancelRequest(url)

+ 2 - 2
src/config/axios/service.ts

@@ -1,10 +1,10 @@
 import axios from 'axios'
 import config, { defaultRequestInterceptors, defaultResponseInterceptors } from './config'
 
-import { AxiosInstance, InternalAxiosRequestConfig, RequestConfig, AxiosResponse } from './type'
+import { AxiosInstance, InternalAxiosRequestConfig, RequestConfig, AxiosResponse } from './types'
 
 const { interceptors, baseUrl } = config
-export const PATH_URL = baseUrl[import.meta.env.VITE_API_BASEPATH]
+export const PATH_URL = baseUrl[import.meta.env.VITE_API_BASE_PATH]
 
 const { requestInterceptors, responseInterceptors } = interceptors
 

+ 0 - 0
src/config/axios/type.ts → src/config/axios/types/index.ts


+ 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
-  })
-}

+ 52 - 10
src/hooks/web/useForm.ts

@@ -1,10 +1,9 @@
 import type { Form, FormExpose } from '@/components/Form'
-import type { ElForm } from 'element-plus'
+import type { ElForm, ElFormItem } from 'element-plus'
 import { ref, unref, nextTick } from 'vue'
-import type { FormProps } from '@/components/Form/src/types'
-import { FormSchema, FormSetPropsType } from '@/types/form'
+import { FormSchema, FormSetProps, FormProps } from '@/components/Form'
 
-export const useForm = (props?: FormProps) => {
+export const useForm = () => {
   // From实例
   const formRef = ref<typeof Form & FormExpose>()
 
@@ -31,6 +30,10 @@ export const useForm = (props?: FormProps) => {
 
   // 一些内置的方法
   const methods = {
+    /**
+     * @description 设置form组件的props
+     * @param props form组件的props
+     */
     setProps: async (props: FormProps = {}) => {
       const form = await getForm()
       form?.setProps(props)
@@ -39,20 +42,26 @@ export const useForm = (props?: FormProps) => {
       }
     },
 
+    /**
+     * @description 设置form的值
+     * @param data 需要设置的数据
+     */
     setValues: async (data: Recordable) => {
       const form = await getForm()
       form?.setValues(data)
     },
 
     /**
+     * @description 设置schema
      * @param schemaProps 需要设置的schemaProps
      */
-    setSchema: async (schemaProps: FormSetPropsType[]) => {
+    setSchema: async (schemaProps: FormSetProps[]) => {
       const form = await getForm()
       form?.setSchema(schemaProps)
     },
 
     /**
+     * @description 新增schema
      * @param formSchema 需要新增数据
      * @param index 在哪里新增
      */
@@ -62,6 +71,7 @@ export const useForm = (props?: FormProps) => {
     },
 
     /**
+     * @description 删除schema
      * @param field 删除哪个数据
      */
     delSchema: async (field: string) => {
@@ -70,19 +80,51 @@ export const useForm = (props?: FormProps) => {
     },
 
     /**
+     * @description 获取表单数据
      * @returns form data
      */
     getFormData: async <T = Recordable>(): Promise<T> => {
       const form = await getForm()
       return form?.formModel as T
+    },
+
+    /**
+     * @description 获取表单组件的实例
+     * @param field 表单项唯一标识
+     * @returns component instance
+     */
+    getComponentExpose: async (field: string) => {
+      const form = await getForm()
+      return form?.getComponentExpose(field)
+    },
+
+    /**
+     * @description 获取formItem组件的实例
+     * @param field 表单项唯一标识
+     * @returns formItem instance
+     */
+    getFormItemExpose: async (field: string) => {
+      const form = await getForm()
+      return form?.getFormItemExpose(field) as ComponentRef<typeof ElFormItem>
+    },
+
+    /**
+     * @description 获取ElForm组件的实例
+     * @returns ElForm instance
+     */
+    getElFormExpose: async () => {
+      await getForm()
+      return unref(elFormRef)
+    },
+
+    getFormExpose: async () => {
+      await getForm()
+      return unref(formRef)
     }
   }
 
-  props && methods.setProps(props)
-
   return {
-    register,
-    formRef: elFormRef,
-    methods
+    formRegister: register,
+    formMethods: methods
   }
 }

+ 1 - 2
src/hooks/web/useIcon.ts

@@ -1,7 +1,6 @@
 import { h } from 'vue'
 import type { VNode } from 'vue'
-import { Icon } from '@/components/Icon'
-import { IconTypes } from '@/types/icon'
+import { Icon, IconTypes } from '@/components/Icon'
 
 export const useIcon = (props: IconTypes): VNode => {
   return h(Icon, props)

+ 1 - 1
src/components/UserInfo/src/components/useNow.ts → src/hooks/web/useNow.ts

@@ -2,7 +2,7 @@ import { dateUtil } from '@/utils/dateUtil'
 import { reactive, toRefs } from 'vue'
 import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'
 
-export function useNow(immediate = true) {
+export const useNow = (immediate = true) => {
   let timer: IntervalHandle
 
   const state = reactive({

+ 91 - 0
src/hooks/web/useSearch.ts

@@ -0,0 +1,91 @@
+import { ref, unref, nextTick } from 'vue'
+import { FormSchema, FormSetProps } from '@/components/Form'
+import { SearchExpose, SearchProps } from '@/components/Search'
+
+export const useSearch = () => {
+  // Search实例
+  const searchRef = ref<SearchExpose>()
+
+  /**
+   * @param ref Search实例
+   * @param elRef ElForm实例
+   */
+  const register = (ref: SearchExpose) => {
+    searchRef.value = ref
+  }
+
+  const getSearch = async () => {
+    await nextTick()
+    const search = unref(searchRef)
+    if (!search) {
+      console.error('The Search is not registered. Please use the register method to register')
+    }
+    return search
+  }
+
+  // 一些内置的方法
+  const methods = {
+    /**
+     * @description 设置search组件的props
+     * @param field FormItem的field
+     */
+    setProps: async (props: SearchProps = {}) => {
+      const search = await getSearch()
+      search?.setProps(props)
+      if (props.model) {
+        search?.setValues(props.model)
+      }
+    },
+
+    /**
+     * @description 设置form的值
+     * @param data 需要设置的数据
+     */
+    setValues: async (data: Recordable) => {
+      const search = await getSearch()
+      search?.setValues(data)
+    },
+
+    /**
+     * @description 设置schema
+     * @param schemaProps 需要设置的schemaProps
+     */
+    setSchema: async (schemaProps: FormSetProps[]) => {
+      const search = await getSearch()
+      search?.setSchema(schemaProps)
+    },
+
+    /**
+     * @description 新增schema
+     * @param formSchema 需要新增数据
+     * @param index 在哪里新增
+     */
+    addSchema: async (formSchema: FormSchema, index?: number) => {
+      const search = await getSearch()
+      search?.addSchema(formSchema, index)
+    },
+
+    /**
+     * @description 删除schema
+     * @param field 删除哪个数据
+     */
+    delSchema: async (field: string) => {
+      const search = await getSearch()
+      search?.delSchema(field)
+    },
+
+    /**
+     * @description 获取表单数据
+     * @returns form data
+     */
+    getFormData: async <T = Recordable>(): Promise<T> => {
+      const search = await getSearch()
+      return search?.formModel as T
+    }
+  }
+
+  return {
+    searchRegister: register,
+    searchMethods: methods
+  }
+}

+ 31 - 0
src/hooks/web/useStorage.ts

@@ -0,0 +1,31 @@
+import { isArray, isObject } from '@/utils/is'
+
+export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionStorage') => {
+  const setStorage = (key: string, value: any) => {
+    window[type].setItem(key, isArray(value) || isObject(value) ? JSON.stringify(value) : value)
+  }
+
+  const getStorage = (key: string) => {
+    const value = window[type].getItem(key)
+    try {
+      return JSON.parse(value || '')
+    } catch (error) {
+      return value
+    }
+  }
+
+  const removeStorage = (key: string) => {
+    window[type].removeItem(key)
+  }
+
+  const clear = () => {
+    window[type].clear()
+  }
+
+  return {
+    setStorage,
+    getStorage,
+    removeStorage,
+    clear
+  }
+}

+ 126 - 129
src/hooks/web/useTable.ts

@@ -1,91 +1,57 @@
-import { Table, TableExpose } from '@/components/Table'
-import { ElTable, ElMessageBox, ElMessage } from 'element-plus'
-import { ref, reactive, watch, computed, unref, nextTick } from 'vue'
-import { get } from 'lodash-es'
-import type { TableProps } from '@/components/Table/src/types'
 import { useI18n } from '@/hooks/web/useI18n'
-import { TableSetPropsType } from '@/types/table'
+import { Table, TableExpose, TableProps, TableSetProps, TableColumn } from '@/components/Table'
+import { ElTable, ElMessageBox, ElMessage } from 'element-plus'
+import { ref, watch, unref, nextTick, onMounted } from 'vue'
 
 const { t } = useI18n()
 
-interface TableResponse<T = any> {
-  total: number
-  list: T[]
-  pageNumber: number
-  pageSize: number
-}
-
-interface UseTableConfig<T = any> {
-  getListApi: (option: any) => Promise<IResponse<TableResponse<T>>>
-  delListApi?: (option: any) => Promise<IResponse>
-  // 返回数据格式配置
-  response: {
-    list: string
-    total?: string
-  }
-  // 默认传递的参数
-  defaultParams?: Recordable
-  props?: TableProps
+interface UseTableConfig {
+  /**
+   * 是否初始化的时候请求一次
+   */
+  immediate?: boolean
+  fetchDataApi: () => Promise<{
+    list: any[]
+    total?: number
+  }>
+  fetchDelApi?: () => Promise<boolean>
 }
 
-interface TableObject<T = any> {
-  pageSize: number
-  currentPage: number
-  total: number
-  tableList: T[]
-  params: any
-  loading: boolean
-  currentRow: Nullable<T>
-}
+export const useTable = (config: UseTableConfig) => {
+  const { immediate = true } = config
 
-export const useTable = <T = any>(config?: UseTableConfig<T>) => {
-  const tableObject = reactive<TableObject<T>>({
-    // 页数
-    pageSize: 10,
-    // 当前页
-    currentPage: 1,
-    // 总条数
-    total: 10,
-    // 表格数据
-    tableList: [],
-    // AxiosConfig 配置
-    params: {
-      ...(config?.defaultParams || {})
-    },
-    // 加载中
-    loading: true,
-    // 当前行的数据
-    currentRow: null
-  })
-
-  const paramsObj = computed(() => {
-    return {
-      ...tableObject.params,
-      pageSize: tableObject.pageSize,
-      pageIndex: tableObject.currentPage
-    }
-  })
+  const loading = ref(false)
+  const currentPage = ref(1)
+  const pageSize = ref(10)
+  const total = ref(0)
+  const dataList = ref<any[]>([])
 
   watch(
-    () => tableObject.currentPage,
+    () => currentPage.value,
     () => {
       methods.getList()
     }
   )
 
   watch(
-    () => tableObject.pageSize,
+    () => pageSize.value,
     () => {
       // 当前页不为1时,修改页数后会导致多次调用getList方法
-      if (tableObject.currentPage === 1) {
+      if (unref(currentPage) === 1) {
         methods.getList()
       } else {
-        tableObject.currentPage = 1
+        currentPage.value = 1
         methods.getList()
       }
     }
   )
 
+  onMounted(() => {
+    if (immediate) {
+      methods.getList()
+    }
+  })
+
   // Table实例
   const tableRef = ref<typeof Table & TableExpose>()
 
@@ -106,91 +72,122 @@ export const useTable = <T = any>(config?: UseTableConfig<T>) => {
     return table
   }
 
-  const delData = async (ids: string[] | number[]) => {
-    const res = await (config?.delListApi && config?.delListApi(ids))
-    if (res) {
-      ElMessage.success(t('common.delSuccess'))
-
-      // 计算出临界点
-      const currentPage =
-        tableObject.total % tableObject.pageSize === ids.length || tableObject.pageSize === 1
-          ? tableObject.currentPage > 1
-            ? tableObject.currentPage - 1
-            : tableObject.currentPage
-          : tableObject.currentPage
-
-      tableObject.currentPage = currentPage
-      methods.getList()
-    }
-  }
-
   const methods = {
+    /**
+     * 获取表单数据
+     */
     getList: async () => {
-      tableObject.loading = true
-      const res = await config?.getListApi(unref(paramsObj)).finally(() => {
-        tableObject.loading = false
-      })
-      if (res) {
-        tableObject.tableList = get(res.data || {}, config?.response.list as string)
-        tableObject.total = get(res.data || {}, config?.response?.total as string) || 0
+      loading.value = true
+      try {
+        const res = await config?.fetchDataApi()
+        console.log('fetchDataApi res', res)
+        if (res) {
+          dataList.value = res.list
+          total.value = res.total || 0
+        }
+      } catch (err) {
+        console.log('fetchDataApi error')
+      } finally {
+        loading.value = false
       }
     },
+
+    /**
+     * @description 设置table组件的props
+     * @param props table组件的props
+     */
     setProps: async (props: TableProps = {}) => {
       const table = await getTable()
       table?.setProps(props)
     },
-    setColumn: async (columnProps: TableSetPropsType[]) => {
+
+    /**
+     * @description 设置column
+     * @param columnProps 需要设置的列
+     */
+    setColumn: async (columnProps: TableSetProps[]) => {
       const table = await getTable()
       table?.setColumn(columnProps)
     },
-    getSelections: async () => {
+
+    /**
+     * @description 新增column
+     * @param tableColumn 需要新增数据
+     * @param index 在哪里新增
+     */
+    addColumn: async (tableColumn: TableColumn, index?: number) => {
       const table = await getTable()
-      return (table?.selections || []) as T[]
+      table?.addColumn(tableColumn, index)
     },
-    // 与Search组件结合
-    setSearchParams: (data: Recordable) => {
-      tableObject.currentPage = 1
-      tableObject.params = Object.assign(tableObject.params, {
-        pageSize: tableObject.pageSize,
-        pageIndex: tableObject.currentPage,
-        ...data
-      })
+
+    /**
+     * @description 删除column
+     * @param field 删除哪个数据
+     */
+    delColumn: async (field: string) => {
+      const table = await getTable()
+      table?.delColumn(field)
+    },
+
+    /**
+     * @description 获取ElTable组件的实例
+     * @returns ElTable instance
+     */
+    getElTableExpose: async () => {
+      await getTable()
+      return unref(elTableRef)
+    },
+
+    refresh: () => {
       methods.getList()
     },
+
+    // sortableChange: (e: any) => {
+    //   console.log('sortableChange', e)
+    //   const { oldIndex, newIndex } = e
+    //   dataList.value.splice(newIndex, 0, dataList.value.splice(oldIndex, 1)[0])
+    //   // to do something
+    // }
     // 删除数据
-    delList: async (ids: string[] | number[], multiple: boolean, message = true) => {
-      const tableRef = await getTable()
-      if (multiple) {
-        if (!tableRef?.selections.length) {
-          ElMessage.warning(t('common.delNoData'))
-          return
-        }
-      } else {
-        if (!tableObject.currentRow) {
-          ElMessage.warning(t('common.delNoData'))
-          return
-        }
-      }
-      if (message) {
-        ElMessageBox.confirm(t('common.delMessage'), t('common.delWarning'), {
-          confirmButtonText: t('common.delOk'),
-          cancelButtonText: t('common.delCancel'),
-          type: 'warning'
-        }).then(async () => {
-          await delData(ids)
-        })
-      } else {
-        await delData(ids)
+    delList: async (idsLength: number) => {
+      const { fetchDelApi } = config
+      if (!fetchDelApi) {
+        console.warn('fetchDelApi is undefined')
+        return
       }
+      ElMessageBox.confirm(t('common.delMessage'), t('common.delWarning'), {
+        confirmButtonText: t('common.delOk'),
+        cancelButtonText: t('common.delCancel'),
+        type: 'warning'
+      }).then(async () => {
+        const res = await fetchDelApi()
+        if (res) {
+          ElMessage.success(t('common.delSuccess'))
+
+          // 计算出临界点
+          const current =
+            unref(total) % unref(pageSize) === idsLength || unref(pageSize) === 1
+              ? unref(currentPage) > 1
+                ? unref(currentPage) - 1
+                : unref(currentPage)
+              : unref(currentPage)
+
+          currentPage.value = current
+          methods.getList()
+        }
+      })
     }
   }
 
-  config?.props && methods.setProps(config.props)
-
   return {
-    register,
-    elTableRef,
-    tableObject,
-    methods
+    tableRegister: register,
+    tableMethods: methods,
+    tableState: {
+      currentPage,
+      pageSize,
+      total,
+      dataList,
+      loading
+    }
   }
 }

+ 89 - 6
src/locales/en.ts

@@ -40,7 +40,11 @@ export default {
     delOk: 'OK',
     delCancel: 'Cancel',
     delNoData: 'Please select the data to delete',
-    delSuccess: 'Deleted successfully'
+    delSuccess: 'Deleted successfully',
+    refresh: 'Refresh',
+    fullscreen: 'Fullscreen',
+    size: 'Size',
+    columnSetting: 'Column setting'
   },
   lock: {
     lockScreen: 'Lock screen',
@@ -154,7 +158,13 @@ export default {
     role: 'Role management',
     document: 'Document',
     inputPassword: 'InputPassword',
-    sticky: 'Sticky'
+    sticky: 'Sticky',
+    treeTable: 'Tree table',
+    PicturePreview: 'Table Image Preview',
+    department: 'Department management',
+    menuManagement: 'Menu management',
+    // 权限测试页面
+    permission: 'Permission test page'
   },
   permission: {
     hasPermission: 'Please set the operation permission value'
@@ -241,8 +251,11 @@ export default {
     transfer: 'Transfer',
     render: 'Render',
     radio: 'Radio',
+    radioGroup: 'Radio Group',
     button: 'Button',
     checkbox: 'Checkbox',
+    checkboxButton: 'Checkbox Button',
+    checkboxGroup: 'Checkbox Group',
     slider: 'Slider',
     datePicker: 'Date Picker',
     shortcuts: 'Shortcuts',
@@ -277,7 +290,26 @@ export default {
     set: 'Set',
     subitem: 'Subitem',
     formValidation: 'Form validation',
-    verifyReset: 'Verify reset'
+    verifyReset: 'Verify reset',
+    // 富文本编辑器
+    richText: 'Rich text',
+    form: 'Form',
+    // 远程加载
+    remoteLoading: 'Remote loading',
+    // 聚焦
+    focus: 'Focus',
+    treeSelect: 'Tree Select',
+    showCheckbox: 'Show Checkbox',
+    selectAnyLevel: 'Select Any Level',
+    multiple: 'Multiple',
+    filterable: 'Filterable',
+    // 自定义节点内容
+    customContent: 'Custom content',
+    // 懒加载
+    lazyLoad: 'Lazy load',
+    upload: 'Upload',
+    // 用户头像
+    userAvatar: 'User avatar'
   },
   guideDemo: {
     guide: 'Guide',
@@ -361,7 +393,13 @@ export default {
     left: 'left',
     center: 'center',
     right: 'right',
-    dynamicOptions: 'Dynamic options'
+    dynamicOptions: 'Dynamic options',
+    // 删除单选框
+    deleteRadio: 'Delete radio',
+    // 还原单选框
+    restoreRadio: 'Restore radio',
+    loading: 'Loading',
+    reset: 'Reset'
   },
   stickyDemo: {
     sticky: 'Sticky'
@@ -392,7 +430,14 @@ export default {
     hiddenExpandedRows: 'Hidden expanded rows',
     changeTitle: 'Change title',
     header: 'Header',
-    selectAllNone: 'Select all / none'
+    selectAllNone: 'Select all / none',
+    delOrAddAction: 'Delete or add action',
+    showOrHiddenStripe: 'Show or hidden stripe',
+    showOrHiddenBorder: 'Show or hidden border',
+    fixedHeaderOrAuto: 'Fixed header or auto',
+    getSelections: 'Get selections',
+    preview: 'Preview',
+    showOrHiddenSortable: 'Show or hidden sortable'
   },
   richText: {
     richText: 'Rich text',
@@ -446,7 +491,45 @@ export default {
     role: 'Role',
     remark: 'Remark',
     remarkMessage1: 'Back end control routing permission',
-    remarkMessage2: 'Front end control routing permission'
+    remarkMessage2: 'Front end control routing permission',
+    // 部门列表
+    departmentList: 'Department list',
+    // 搜索部门
+    searchDepartment: 'Search department',
+    account: 'Account',
+    email: 'Email',
+    createTime: 'Create time',
+    // 所属部门
+    department: 'Department',
+    departmentName: 'Department name',
+    status: 'Status',
+    enable: 'Enable',
+    disable: 'Disable',
+    superiorDepartment: 'Superior department'
+  },
+  menu: {
+    menuName: 'Menu name',
+    icon: 'Icon',
+    // 权限
+    permission: 'Permission',
+    component: 'Component',
+    path: 'Path',
+    status: 'Status',
+    hidden: 'Hidden',
+    alwaysShow: 'Always show',
+    noCache: 'No cache',
+    breadcrumb: 'Breadcrumb',
+    affix: 'Affix',
+    noTagsView: 'No tags view',
+    activeMenu: 'Active menu',
+    canTo: 'Can to',
+    name: 'Name'
+  },
+  role: {
+    roleName: 'Role name',
+    role: 'Role',
+    // 菜单分配
+    menu: 'Menu allocation'
   },
   inputPasswordDemo: {
     title: 'InputPassword',

+ 87 - 8
src/locales/zh-CN.ts

@@ -17,7 +17,7 @@ export default {
     closeTab: '关闭标签页',
     closeTheLeftTab: '关闭左侧标签页',
     closeTheRightTab: '关闭右侧标签页',
-    closeOther: '关闭其标签页',
+    closeOther: '关闭其标签页',
     closeAll: '关闭全部标签页',
     prevLabel: '上一步',
     nextLabel: '下一步',
@@ -40,7 +40,11 @@ export default {
     delOk: '确定',
     delCancel: '取消',
     delNoData: '请选择需要删除的数据',
-    delSuccess: '删除成功'
+    delSuccess: '删除成功',
+    refresh: '刷新',
+    fullscreen: '全屏',
+    size: '尺寸',
+    columnSetting: '列设置'
   },
   lock: {
     lockScreen: '锁定屏幕',
@@ -102,7 +106,7 @@ export default {
     register: '注册',
     checkPassword: '确认密码',
     login: '登录',
-    otherLogin: '其登录方式',
+    otherLogin: '其登录方式',
     remember: '记住我',
     hasUser: '已有账号?去登录',
     forgetPassword: '忘记密码',
@@ -154,7 +158,12 @@ export default {
     role: '角色管理',
     document: '文档',
     inputPassword: '密码输入框',
-    sticky: '黏性'
+    sticky: '黏性',
+    treeTable: '树形表格',
+    PicturePreview: '表格图片预览',
+    department: '部门管理',
+    menuManagement: '菜单管理',
+    permission: '权限测试页'
   },
   permission: {
     hasPermission: '请设置操作权限值'
@@ -241,8 +250,11 @@ export default {
     transfer: '穿梭框',
     render: '渲染器',
     radio: '单选框',
+    radioGroup: '单选框组',
     button: '按钮',
     checkbox: '多选框',
+    checkboxButton: '多选框按钮',
+    checkboxGroup: '多选框组',
     slider: '滑块',
     datePicker: '日期选择器',
     shortcuts: '快捷选项',
@@ -276,7 +288,23 @@ export default {
     set: '设置',
     subitem: '子项',
     formValidation: '表单验证',
-    verifyReset: '验证重置'
+    verifyReset: '验证重置',
+    // 富文本编辑器
+    richText: '富文本编辑器',
+    form: '表单',
+    // 远程加载
+    remoteLoading: '远程加载',
+    // 聚焦
+    focus: '聚焦',
+    treeSelect: '树形选择器',
+    showCheckbox: '显示复选框',
+    selectAnyLevel: '选择任意级别',
+    multiple: '多选',
+    filterable: '可筛选',
+    customContent: '自定义内容',
+    lazyLoad: '懒加载',
+    upload: '上传',
+    userAvatar: '用户头像'
   },
   guideDemo: {
     guide: '引导页',
@@ -358,7 +386,13 @@ export default {
     left: '左',
     center: '中',
     right: '右',
-    dynamicOptions: '动态选项'
+    dynamicOptions: '动态选项',
+    // 删除单选框
+    deleteRadio: '删除单选框',
+    // 还原单选框
+    restoreRadio: '还原单选框',
+    loading: '加载中',
+    reset: '重置'
   },
   stickyDemo: {
     sticky: '黏性'
@@ -389,7 +423,14 @@ export default {
     hiddenExpandedRows: '隐藏展开行',
     changeTitle: '修改标题',
     header: '头部',
-    selectAllNone: '全选/全不选'
+    selectAllNone: '全选/全不选',
+    delOrAddAction: '删除/添加操作列',
+    showOrHiddenStripe: '显示/隐藏斑马纹',
+    showOrHiddenBorder: '显示/隐藏边框',
+    fixedHeaderOrAuto: '固定头部/自动',
+    getSelections: '获取多选数据',
+    preview: '封面',
+    showOrHiddenSortable: '显示/隐藏排序'
   },
   richText: {
     richText: '富文本',
@@ -442,7 +483,45 @@ export default {
     role: '角色',
     remark: '备注',
     remarkMessage1: '后端控制路由权限',
-    remarkMessage2: '前端控制路由权限'
+    remarkMessage2: '前端控制路由权限',
+    // 部门列表
+    departmentList: '部门列表',
+    searchDepartment: '搜索部门',
+    account: '账号',
+    email: '邮箱',
+    createTime: '创建时间',
+    // 所属部门
+    department: '所属部门',
+    departmentName: '部门名称',
+    status: '状态',
+    // 启用
+    enable: '启用',
+    // 禁用
+    disable: '禁用',
+    // 上级部门
+    superiorDepartment: '上级部门'
+  },
+  menu: {
+    menuName: '菜单名称',
+    icon: '图标',
+    permission: '权限标识',
+    component: '组件',
+    path: '路径',
+    status: '状态',
+    hidden: '是否隐藏',
+    alwaysShow: '是否一直显示',
+    noCache: '是否清除缓存',
+    breadcrumb: '是否显示面包屑',
+    affix: '是否固定在标签页',
+    noTagsView: '是否隐藏标签页',
+    activeMenu: '高亮菜单',
+    canTo: '是否可跳转',
+    name: '组件名称'
+  },
+  role: {
+    roleName: '角色名称',
+    role: '角色',
+    menu: '菜单分配'
   },
   inputPasswordDemo: {
     title: '密码输入框',

+ 42 - 42
src/permission.ts

@@ -1,6 +1,6 @@
 import router from './router'
 import { useAppStoreWithOut } from '@/store/modules/app'
-import { useCache } from '@/hooks/web/useCache'
+import { useStorage } from '@/hooks/web/useStorage'
 import type { RouteRecordRaw } from 'vue-router'
 import { useTitle } from '@/hooks/web/useTitle'
 import { useNProgress } from '@/hooks/web/useNProgress'
@@ -15,7 +15,7 @@ const appStore = useAppStoreWithOut()
 
 const dictStore = useDictStoreWithOut()
 
-const { wsCache } = useCache()
+const { getStorage } = useStorage()
 
 const { start, done } = useNProgress()
 
@@ -26,52 +26,52 @@ const whiteList = ['/login'] // 不重定向白名单
 router.beforeEach(async (to, from, next) => {
   start()
   loadStart()
-  // if (!wsCache.get(appStore.getUserInfo)) {
-  if (to.path === '/login') {
-    next({ path: '/' })
-  } else {
-    if (!dictStore.getIsSetDict) {
-      // 获取所有字典
-      const res = await getDictApi()
-      if (res) {
-        dictStore.setDictObj(res.data)
-        dictStore.setIsSetDict(true)
+  if (getStorage(appStore.getUserInfo)) {
+    if (to.path === '/login') {
+      next({ path: '/' })
+    } else {
+      if (!dictStore.getIsSetDict) {
+        // 获取所有字典
+        const res = await getDictApi()
+        if (res) {
+          dictStore.setDictObj(res.data)
+          dictStore.setIsSetDict(true)
+        }
+      }
+      if (permissionStore.getIsAddRouters) {
+        next()
+        return
       }
-    }
-    if (permissionStore.getIsAddRouters) {
-      next()
-      return
-    }
 
-    // 开发者可根据实际情况进行修改
-    const roleRouters = wsCache.get('roleRouters') || []
-    const userInfo = wsCache.get(appStore.getUserInfo)
+      // 开发者可根据实际情况进行修改
+      const roleRouters = getStorage('roleRouters') || []
+      const userInfo = getStorage(appStore.getUserInfo)
 
-    // 是否使用动态路由
-    if (appStore.getDynamicRouter) {
-      userInfo.role === 'admin'
-        ? await permissionStore.generateRoutes('admin', roleRouters as AppCustomRouteRecordRaw[])
-        : await permissionStore.generateRoutes('test', roleRouters as string[])
+      // 是否使用动态路由
+      if (appStore.getDynamicRouter) {
+        userInfo.role === 'admin'
+          ? await permissionStore.generateRoutes('admin', roleRouters as AppCustomRouteRecordRaw[])
+          : await permissionStore.generateRoutes('test', roleRouters as string[])
+      } else {
+        await permissionStore.generateRoutes('none')
+      }
+
+      permissionStore.getAddRouters.forEach((route) => {
+        router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
+      })
+      const redirectPath = from.query.redirect || to.path
+      const redirect = decodeURIComponent(redirectPath as string)
+      const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
+      permissionStore.setIsAddRouters(true)
+      next(nextData)
+    }
+  } else {
+    if (whiteList.indexOf(to.path) !== -1) {
+      next()
     } else {
-      await permissionStore.generateRoutes('none')
+      next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
     }
-
-    permissionStore.getAddRouters.forEach((route) => {
-      router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
-    })
-    const redirectPath = from.query.redirect || to.path
-    const redirect = decodeURIComponent(redirectPath as string)
-    const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
-    permissionStore.setIsAddRouters(true)
-    next(nextData)
   }
-  // } else {
-  //   if (whiteList.indexOf(to.path) !== -1) {
-  //     next()
-  //   } else {
-  //     next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
-  //   }
-  // }
 })
 
 router.afterEach((to) => {

+ 390 - 346
src/router/index.ts

@@ -148,335 +148,335 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
             meta: {
               title: t('router.defaultForm')
             }
+          },
+          {
+            path: 'use-form',
+            component: () => import('@/views/Components/Form/UseFormDemo.vue'),
+            name: 'UseForm',
+            meta: {
+              title: 'UseForm'
+            }
+          }
+        ]
+      },
+      {
+        path: 'table',
+        component: getParentLayout(),
+        redirect: '/components/table/default-table',
+        name: 'TableDemo',
+        meta: {
+          title: t('router.table'),
+          alwaysShow: true
+        },
+        children: [
+          {
+            path: 'default-table',
+            component: () => import('@/views/Components/Table/DefaultTable.vue'),
+            name: 'DefaultTable',
+            meta: {
+              title: t('router.defaultTable')
+            }
+          },
+          {
+            path: 'use-table',
+            component: () => import('@/views/Components/Table/UseTableDemo.vue'),
+            name: 'UseTable',
+            meta: {
+              title: 'UseTable'
+            }
+          },
+          {
+            path: 'tree-table',
+            component: () => import('@/views/Components/Table/TreeTable.vue'),
+            name: 'TreeTable',
+            meta: {
+              title: t('router.treeTable')
+            }
+          },
+          {
+            path: 'table-image-preview',
+            component: () => import('@/views/Components/Table/TableImagePreview.vue'),
+            name: 'TableImagePreview',
+            meta: {
+              title: t('router.PicturePreview')
+            }
+          }
+        ]
+      },
+      {
+        path: 'editor-demo',
+        component: getParentLayout(),
+        redirect: '/components/editor-demo/editor',
+        name: 'EditorDemo',
+        meta: {
+          title: t('router.editor'),
+          alwaysShow: true
+        },
+        children: [
+          {
+            path: 'editor',
+            component: () => import('@/views/Components/Editor/Editor.vue'),
+            name: 'Editor',
+            meta: {
+              title: t('router.richText')
+            }
           }
-          // {
-          //   path: 'use-form',
-          //   component: () => import('@/views/Components/Form/UseFormDemo.vue'),
-          //   name: 'UseForm',
-          //   meta: {
-          //     title: 'UseForm'
-          //   }
-          // }
-          // {
-          //   path: 'ref-form',
-          //   component: () => import('@/views/Components/Form/RefForm.vue'),
-          //   name: 'RefForm',
-          //   meta: {
-          //     title: 'RefForm'
-          //   }
-          // }
         ]
+      },
+      {
+        path: 'search',
+        component: () => import('@/views/Components/Search.vue'),
+        name: 'Search',
+        meta: {
+          title: t('router.search')
+        }
+      },
+      {
+        path: 'descriptions',
+        component: () => import('@/views/Components/Descriptions.vue'),
+        name: 'Descriptions',
+        meta: {
+          title: t('router.descriptions')
+        }
+      },
+      {
+        path: 'image-viewer',
+        component: () => import('@/views/Components/ImageViewer.vue'),
+        name: 'ImageViewer',
+        meta: {
+          title: t('router.imageViewer')
+        }
+      },
+      {
+        path: 'dialog',
+        component: () => import('@/views/Components/Dialog.vue'),
+        name: 'Dialog',
+        meta: {
+          title: t('router.dialog')
+        }
+      },
+      {
+        path: 'icon',
+        component: () => import('@/views/Components/Icon.vue'),
+        name: 'Icon',
+        meta: {
+          title: t('router.icon')
+        }
+      },
+      {
+        path: 'echart',
+        component: () => import('@/views/Components/Echart.vue'),
+        name: 'Echart',
+        meta: {
+          title: t('router.echart')
+        }
+      },
+      {
+        path: 'count-to',
+        component: () => import('@/views/Components/CountTo.vue'),
+        name: 'CountTo',
+        meta: {
+          title: t('router.countTo')
+        }
+      },
+      {
+        path: 'qrcode',
+        component: () => import('@/views/Components/Qrcode.vue'),
+        name: 'Qrcode',
+        meta: {
+          title: t('router.qrcode')
+        }
+      },
+      {
+        path: 'highlight',
+        component: () => import('@/views/Components/Highlight.vue'),
+        name: 'Highlight',
+        meta: {
+          title: t('router.highlight')
+        }
+      },
+      {
+        path: 'infotip',
+        component: () => import('@/views/Components/Infotip.vue'),
+        name: 'Infotip',
+        meta: {
+          title: t('router.infotip')
+        }
+      },
+      {
+        path: 'input-password',
+        component: () => import('@/views/Components/InputPassword.vue'),
+        name: 'InputPassword',
+        meta: {
+          title: t('router.inputPassword')
+        }
+      }
+    ]
+  },
+  {
+    path: '/hooks',
+    component: Layout,
+    redirect: '/hooks/useWatermark',
+    name: 'Hooks',
+    meta: {
+      title: 'hooks',
+      icon: 'ic:outline-webhook',
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: 'useWatermark',
+        component: () => import('@/views/hooks/useWatermark.vue'),
+        name: 'UseWatermark',
+        meta: {
+          title: 'useWatermark'
+        }
       }
       // {
-      //   path: 'table',
-      //   component: getParentLayout(),
-      //   redirect: '/components/table/default-table',
-      //   name: 'TableDemo',
-      //   meta: {
-      //     title: t('router.table'),
-      //     alwaysShow: true
-      //   },
-      //   children: [
-      //     {
-      //       path: 'default-table',
-      //       component: () => import('@/views/Components/Table/DefaultTable.vue'),
-      //       name: 'DefaultTable',
-      //       meta: {
-      //         title: t('router.defaultTable')
-      //       }
-      //     },
-      //     {
-      //       path: 'use-table',
-      //       component: () => import('@/views/Components/Table/UseTableDemo.vue'),
-      //       name: 'UseTable',
-      //       meta: {
-      //         title: 'UseTable'
-      //       }
-      //     },
-      //     {
-      //       path: 'ref-table',
-      //       component: () => import('@/views/Components/Table/RefTable.vue'),
-      //       name: 'RefTable',
-      //       meta: {
-      //         title: 'RefTable'
-      //       }
-      //     }
-      //   ]
-      // },
-      // {
-      //   path: 'editor-demo',
-      //   component: getParentLayout(),
-      //   redirect: '/components/editor-demo/editor',
-      //   name: 'EditorDemo',
-      //   meta: {
-      //     title: t('router.editor'),
-      //     alwaysShow: true
-      //   },
-      //   children: [
-      //     {
-      //       path: 'editor',
-      //       component: () => import('@/views/Components/Editor/Editor.vue'),
-      //       name: 'Editor',
-      //       meta: {
-      //         title: t('router.richText')
-      //       }
-      //     }
-      //   ]
-      // },
-      // {
-      //   path: 'search',
-      //   component: () => import('@/views/Components/Search.vue'),
-      //   name: 'Search',
-      //   meta: {
-      //     title: t('router.search')
-      //   }
-      // },
-      // {
-      //   path: 'descriptions',
-      //   component: () => import('@/views/Components/Descriptions.vue'),
-      //   name: 'Descriptions',
-      //   meta: {
-      //     title: t('router.descriptions')
-      //   }
-      // },
-      // {
-      //   path: 'image-viewer',
-      //   component: () => import('@/views/Components/ImageViewer.vue'),
-      //   name: 'ImageViewer',
-      //   meta: {
-      //     title: t('router.imageViewer')
-      //   }
-      // },
-      // {
-      //   path: 'dialog',
-      //   component: () => import('@/views/Components/Dialog.vue'),
-      //   name: 'Dialog',
-      //   meta: {
-      //     title: t('router.dialog')
-      //   }
-      // },
-      // {
-      //   path: 'icon',
-      //   component: () => import('@/views/Components/Icon.vue'),
-      //   name: 'Icon',
-      //   meta: {
-      //     title: t('router.icon')
-      //   }
-      // },
-      // {
-      //   path: 'echart',
-      //   component: () => import('@/views/Components/Echart.vue'),
-      //   name: 'Echart',
-      //   meta: {
-      //     title: t('router.echart')
-      //   }
-      // },
-      // {
-      //   path: 'count-to',
-      //   component: () => import('@/views/Components/CountTo.vue'),
-      //   name: 'CountTo',
-      //   meta: {
-      //     title: t('router.countTo')
-      //   }
-      // },
-      // {
-      //   path: 'qrcode',
-      //   component: () => import('@/views/Components/Qrcode.vue'),
-      //   name: 'Qrcode',
-      //   meta: {
-      //     title: t('router.qrcode')
-      //   }
-      // },
-      // {
-      //   path: 'highlight',
-      //   component: () => import('@/views/Components/Highlight.vue'),
-      //   name: 'Highlight',
-      //   meta: {
-      //     title: t('router.highlight')
-      //   }
-      // },
-      // {
-      //   path: 'infotip',
-      //   component: () => import('@/views/Components/Infotip.vue'),
-      //   name: 'Infotip',
-      //   meta: {
-      //     title: t('router.infotip')
-      //   }
-      // },
-      // {
-      //   path: 'input-password',
-      //   component: () => import('@/views/Components/InputPassword.vue'),
-      //   name: 'InputPassword',
+      //   path: 'useOpenTab',
+      //   component: () => import('@/views/hooks/useOpenTab.vue'),
+      //   name: 'UseOpenTab',
       //   meta: {
-      //     title: t('router.inputPassword')
+      //     title: 'useOpenTab'
       //   }
-      // },
+      // }
       // {
-      //   path: 'sticky',
-      //   component: () => import('@/views/Components/Sticky.vue'),
-      //   name: 'Sticky',
+      //   path: 'useCrudSchemas',
+      //   component: () => import('@/views/hooks/useCrudSchemas.vue'),
+      //   name: 'UseCrudSchemas',
       //   meta: {
-      //     title: t('router.sticky')
+      //     title: 'useCrudSchemas'
       //   }
       // }
     ]
   },
-  // {
-  //   path: '/hooks',
-  //   component: Layout,
-  //   redirect: '/hooks/useWatermark',
-  //   name: 'Hooks',
-  //   meta: {
-  //     title: 'hooks',
-  //     icon: 'ic:outline-webhook',
-  //     alwaysShow: true
-  //   },
-  //   children: [
-  //     {
-  //       path: 'useWatermark',
-  //       component: () => import('@/views/hooks/useWatermark.vue'),
-  //       name: 'UseWatermark',
-  //       meta: {
-  //         title: 'useWatermark'
-  //       }
-  //     },
-  //     {
-  //       path: 'useCrudSchemas',
-  //       component: () => import('@/views/hooks/useCrudSchemas.vue'),
-  //       name: 'UseCrudSchemas',
-  //       meta: {
-  //         title: 'useCrudSchemas'
-  //       }
-  //     }
-  //   ]
-  // },
-  // {
-  //   path: '/level',
-  //   component: Layout,
-  //   redirect: '/level/menu1/menu1-1/menu1-1-1',
-  //   name: 'Level',
-  //   meta: {
-  //     title: t('router.level'),
-  //     icon: 'carbon:skill-level-advanced'
-  //   },
-  //   children: [
-  //     {
-  //       path: 'menu1',
-  //       name: 'Menu1',
-  //       component: getParentLayout(),
-  //       redirect: '/level/menu1/menu1-1/menu1-1-1',
-  //       meta: {
-  //         title: t('router.menu1')
-  //       },
-  //       children: [
-  //         {
-  //           path: 'menu1-1',
-  //           name: 'Menu11',
-  //           component: getParentLayout(),
-  //           redirect: '/level/menu1/menu1-1/menu1-1-1',
-  //           meta: {
-  //             title: t('router.menu11'),
-  //             alwaysShow: true
-  //           },
-  //           children: [
-  //             {
-  //               path: 'menu1-1-1',
-  //               name: 'Menu111',
-  //               component: () => import('@/views/Level/Menu111.vue'),
-  //               meta: {
-  //                 title: t('router.menu111')
-  //               }
-  //             }
-  //           ]
-  //         },
-  //         {
-  //           path: 'menu1-2',
-  //           name: 'Menu12',
-  //           component: () => import('@/views/Level/Menu12.vue'),
-  //           meta: {
-  //             title: t('router.menu12')
-  //           }
-  //         }
-  //       ]
-  //     },
-  //     {
-  //       path: 'menu2',
-  //       name: 'Menu2',
-  //       component: () => import('@/views/Level/Menu2.vue'),
-  //       meta: {
-  //         title: t('router.menu2')
-  //       }
-  //     }
-  //   ]
-  // },
-  // {
-  //   path: '/example',
-  //   component: Layout,
-  //   redirect: '/example/example-dialog',
-  //   name: 'Example',
-  //   meta: {
-  //     title: t('router.example'),
-  //     icon: 'ep:management',
-  //     alwaysShow: true
-  //   },
-  //   children: [
-  //     {
-  //       path: 'example-dialog',
-  //       component: () => import('@/views/Example/Dialog/ExampleDialog.vue'),
-  //       name: 'ExampleDialog',
-  //       meta: {
-  //         title: t('router.exampleDialog')
-  //       }
-  //     },
-  //     {
-  //       path: 'example-page',
-  //       component: () => import('@/views/Example/Page/ExamplePage.vue'),
-  //       name: 'ExamplePage',
-  //       meta: {
-  //         title: t('router.examplePage')
-  //       }
-  //     },
-  //     {
-  //       path: 'example-add',
-  //       component: () => import('@/views/Example/Page/ExampleAdd.vue'),
-  //       name: 'ExampleAdd',
-  //       meta: {
-  //         title: t('router.exampleAdd'),
-  //         noTagsView: true,
-  //         noCache: true,
-  //         hidden: true,
-  //         canTo: true,
-  //         activeMenu: '/example/example-page'
-  //       }
-  //     },
-  //     {
-  //       path: 'example-edit',
-  //       component: () => import('@/views/Example/Page/ExampleEdit.vue'),
-  //       name: 'ExampleEdit',
-  //       meta: {
-  //         title: t('router.exampleEdit'),
-  //         noTagsView: true,
-  //         noCache: true,
-  //         hidden: true,
-  //         canTo: true,
-  //         activeMenu: '/example/example-page'
-  //       }
-  //     },
-  //     {
-  //       path: 'example-detail',
-  //       component: () => import('@/views/Example/Page/ExampleDetail.vue'),
-  //       name: 'ExampleDetail',
-  //       meta: {
-  //         title: t('router.exampleDetail'),
-  //         noTagsView: true,
-  //         noCache: true,
-  //         hidden: true,
-  //         canTo: true,
-  //         activeMenu: '/example/example-page'
-  //       }
-  //     }
-  //   ]
-  // },
+  {
+    path: '/level',
+    component: Layout,
+    redirect: '/level/menu1/menu1-1/menu1-1-1',
+    name: 'Level',
+    meta: {
+      title: t('router.level'),
+      icon: 'carbon:skill-level-advanced'
+    },
+    children: [
+      {
+        path: 'menu1',
+        name: 'Menu1',
+        component: getParentLayout(),
+        redirect: '/level/menu1/menu1-1/menu1-1-1',
+        meta: {
+          title: t('router.menu1')
+        },
+        children: [
+          {
+            path: 'menu1-1',
+            name: 'Menu11',
+            component: getParentLayout(),
+            redirect: '/level/menu1/menu1-1/menu1-1-1',
+            meta: {
+              title: t('router.menu11'),
+              alwaysShow: true
+            },
+            children: [
+              {
+                path: 'menu1-1-1',
+                name: 'Menu111',
+                component: () => import('@/views/Level/Menu111.vue'),
+                meta: {
+                  title: t('router.menu111')
+                }
+              }
+            ]
+          },
+          {
+            path: 'menu1-2',
+            name: 'Menu12',
+            component: () => import('@/views/Level/Menu12.vue'),
+            meta: {
+              title: t('router.menu12')
+            }
+          }
+        ]
+      },
+      {
+        path: 'menu2',
+        name: 'Menu2',
+        component: () => import('@/views/Level/Menu2.vue'),
+        meta: {
+          title: t('router.menu2')
+        }
+      }
+    ]
+  },
+  {
+    path: '/example',
+    component: Layout,
+    redirect: '/example/example-dialog',
+    name: 'Example',
+    meta: {
+      title: t('router.example'),
+      icon: 'ep:management',
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: 'example-dialog',
+        component: () => import('@/views/Example/Dialog/ExampleDialog.vue'),
+        name: 'ExampleDialog',
+        meta: {
+          title: t('router.exampleDialog')
+        }
+      },
+      {
+        path: 'example-page',
+        component: () => import('@/views/Example/Page/ExamplePage.vue'),
+        name: 'ExamplePage',
+        meta: {
+          title: t('router.examplePage')
+        }
+      },
+      {
+        path: 'example-add',
+        component: () => import('@/views/Example/Page/ExampleAdd.vue'),
+        name: 'ExampleAdd',
+        meta: {
+          title: t('router.exampleAdd'),
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          activeMenu: '/example/example-page'
+        }
+      },
+      {
+        path: 'example-edit',
+        component: () => import('@/views/Example/Page/ExampleEdit.vue'),
+        name: 'ExampleEdit',
+        meta: {
+          title: t('router.exampleEdit'),
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          activeMenu: '/example/example-page'
+        }
+      },
+      {
+        path: 'example-detail',
+        component: () => import('@/views/Example/Page/ExampleDetail.vue'),
+        name: 'ExampleDetail',
+        meta: {
+          title: t('router.exampleDetail'),
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          activeMenu: '/example/example-page'
+        }
+      }
+    ]
+  },
   {
     path: '/error',
     component: Layout,
@@ -513,36 +513,80 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
         }
       }
     ]
+  },
+  {
+    path: '/authorization',
+    component: Layout,
+    redirect: '/authorization/user',
+    name: 'Authorization',
+    meta: {
+      title: t('router.authorization'),
+      icon: 'eos-icons:role-binding',
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: 'department',
+        component: () => import('@/views/Authorization/Department/Department.vue'),
+        name: 'Department',
+        meta: {
+          title: t('router.department')
+        }
+      },
+      {
+        path: 'user',
+        component: () => import('@/views/Authorization/User/User.vue'),
+        name: 'User',
+        meta: {
+          title: t('router.user')
+        }
+      },
+      {
+        path: 'menu',
+        component: () => import('@/views/Authorization/Menu/Menu.vue'),
+        name: 'Menu',
+        meta: {
+          title: t('router.menuManagement')
+        }
+      },
+      {
+        path: 'role',
+        component: () => import('@/views/Authorization/Role/Role.vue'),
+        name: 'Role',
+        meta: {
+          title: t('router.role')
+        }
+      },
+      {
+        path: 'test',
+        component: () => import('@/views/Authorization/Test/Test.vue'),
+        name: 'Test',
+        meta: {
+          title: t('router.permission'),
+          permission: ['add', 'edit', 'delete']
+        }
+      }
+    ]
+  },
+  {
+    path: '/dynamic',
+    component: Layout,
+    redirect: '/404',
+    name: 'Dynamic',
+    meta: {
+      hidden: true
+    },
+    children: [
+      {
+        path: 'tab/:id',
+        component: () => import('@/components/Dynamic/src/Dynamic.vue'),
+        name: 'Dynamic',
+        meta: {
+          title: '详情页'
+        }
+      }
+    ]
   }
-  // {
-  //   path: '/authorization',
-  //   component: Layout,
-  //   redirect: '/authorization/user',
-  //   name: 'Authorization',
-  //   meta: {
-  //     title: t('router.authorization'),
-  //     icon: 'eos-icons:role-binding',
-  //     alwaysShow: true
-  //   },
-  //   children: [
-  //     {
-  //       path: 'user',
-  //       component: () => import('@/views/Authorization/User.vue'),
-  //       name: 'User',
-  //       meta: {
-  //         title: t('router.user')
-  //       }
-  //     },
-  //     {
-  //       path: 'role',
-  //       component: () => import('@/views/Authorization/Role.vue'),
-  //       name: 'Role',
-  //       meta: {
-  //         title: t('router.role')
-  //       }
-  //     }
-  //   ]
-  // }
 ]
 
 const router = createRouter({

+ 22 - 25
src/store/modules/app.ts

@@ -1,13 +1,10 @@
 import { defineStore } from 'pinia'
 import { store } from '../index'
 import { setCssVar, humpToUnderline } from '@/utils'
-import { ElMessage } from 'element-plus'
-import { ElementPlusSize } from '@/types/elementPlus'
-import { useCache } from '@/hooks/web/useCache'
-import { LayoutType } from '@/types/layout'
-import { ThemeTypes } from '@/types/theme'
+import { ElMessage, ComponentSize } from 'element-plus'
+import { useStorage } from '@/hooks/web/useStorage'
 
-const { wsCache } = useCache()
+const { getStorage, setStorage } = useStorage()
 
 interface AppState {
   breadcrumb: boolean
@@ -29,8 +26,8 @@ interface AppState {
   title: string
   userInfo: string
   isDark: boolean
-  currentSize: ElementPlusSize
-  sizeMap: ElementPlusSize[]
+  currentSize: ComponentSize
+  sizeMap: ComponentSize[]
   mobile: boolean
   footer: boolean
   theme: ThemeTypes
@@ -40,7 +37,7 @@ interface AppState {
 export const useAppStore = defineStore('app', {
   state: (): AppState => {
     return {
-      userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其项目冲突
+      userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其项目冲突
       sizeMap: ['default', 'large', 'small'],
       mobile: false, // 是否是移动端
       title: import.meta.env.VITE_APP_TITLE, // 标题
@@ -60,13 +57,13 @@ export const useAppStore = defineStore('app', {
       fixedHeader: true, // 固定toolheader
       footer: true, // 显示页脚
       greyMode: false, // 是否开始灰色模式,用于特殊悼念日
-      dynamicRouter: wsCache.get('dynamicRouter') || false, // 是否动态路由
-      fixedMenu: wsCache.get('fixedMenu') || false, // 是否固定菜单
+      dynamicRouter: getStorage('dynamicRouter') || false, // 是否动态路由
+      fixedMenu: getStorage('fixedMenu') || false, // 是否固定菜单
 
-      layout: wsCache.get('layout') || 'classic', // layout布局
-      isDark: wsCache.get('isDark') || false, // 是否是暗黑模式
-      currentSize: wsCache.get('default') || 'default', // 组件尺寸
-      theme: wsCache.get('theme') || {
+      layout: getStorage('layout') || 'classic', // layout布局
+      isDark: getStorage('isDark') || false, // 是否是暗黑模式
+      currentSize: getStorage('default') || 'default', // 组件尺寸
+      theme: getStorage('theme') || {
         // 主题色
         elColorPrimary: '#409eff',
         // 左侧菜单边框颜色
@@ -159,10 +156,10 @@ export const useAppStore = defineStore('app', {
     getIsDark(): boolean {
       return this.isDark
     },
-    getCurrentSize(): ElementPlusSize {
+    getCurrentSize(): ComponentSize {
       return this.currentSize
     },
-    getSizeMap(): ElementPlusSize[] {
+    getSizeMap(): ComponentSize[] {
       return this.sizeMap
     },
     getMobile(): boolean {
@@ -216,11 +213,11 @@ export const useAppStore = defineStore('app', {
       this.greyMode = greyMode
     },
     setDynamicRouter(dynamicRouter: boolean) {
-      wsCache.set('dynamicRouter', dynamicRouter)
+      setStorage('dynamicRouter', dynamicRouter)
       this.dynamicRouter = dynamicRouter
     },
     setFixedMenu(fixedMenu: boolean) {
-      wsCache.set('fixedMenu', fixedMenu)
+      setStorage('fixedMenu', fixedMenu)
       this.fixedMenu = fixedMenu
     },
     setPageLoading(pageLoading: boolean) {
@@ -228,11 +225,11 @@ export const useAppStore = defineStore('app', {
     },
     setLayout(layout: LayoutType) {
       if (this.mobile && layout !== 'classic') {
-        ElMessage.warning('移动端模式下不支持切换其布局')
+        ElMessage.warning('移动端模式下不支持切换其布局')
         return
       }
       this.layout = layout
-      wsCache.set('layout', this.layout)
+      setStorage('layout', this.layout)
     },
     setTitle(title: string) {
       this.title = title
@@ -246,18 +243,18 @@ export const useAppStore = defineStore('app', {
         document.documentElement.classList.add('light')
         document.documentElement.classList.remove('dark')
       }
-      wsCache.set('isDark', this.isDark)
+      setStorage('isDark', this.isDark)
     },
-    setCurrentSize(currentSize: ElementPlusSize) {
+    setCurrentSize(currentSize: ComponentSize) {
       this.currentSize = currentSize
-      wsCache.set('currentSize', this.currentSize)
+      setStorage('currentSize', this.currentSize)
     },
     setMobile(mobile: boolean) {
       this.mobile = mobile
     },
     setTheme(theme: ThemeTypes) {
       this.theme = Object.assign(this.theme, theme)
-      wsCache.set('theme', this.theme)
+      setStorage('theme', this.theme)
     },
     setCssVarTheme() {
       for (const key in this.theme) {

+ 6 - 6
src/store/modules/locale.ts

@@ -2,10 +2,10 @@ import { defineStore } from 'pinia'
 import { store } from '../index'
 import zhCn from 'element-plus/es/locale/lang/zh-cn'
 import en from 'element-plus/es/locale/lang/en'
-import { useCache } from '@/hooks/web/useCache'
-import { LocaleDropdownType } from '@/types/localeDropdown'
+import { useStorage } from '@/hooks/web/useStorage'
+import { LocaleDropdownType } from '@/components/LocaleDropdown'
 
-const { wsCache } = useCache()
+const { getStorage, setStorage } = useStorage()
 
 const elLocaleMap = {
   'zh-CN': zhCn,
@@ -20,8 +20,8 @@ export const useLocaleStore = defineStore('locales', {
   state: (): LocaleState => {
     return {
       currentLocale: {
-        lang: wsCache.get('lang') || 'zh-CN',
-        elLocale: elLocaleMap[wsCache.get('lang') || 'zh-CN']
+        lang: getStorage('lang') || 'zh-CN',
+        elLocale: elLocaleMap[getStorage('lang') || 'zh-CN']
       },
       // 多语言
       localeMap: [
@@ -49,7 +49,7 @@ export const useLocaleStore = defineStore('locales', {
       // this.locale = Object.assign(this.locale, localeMap)
       this.currentLocale.lang = localeMap?.lang
       this.currentLocale.elLocale = elLocaleMap[localeMap?.lang]
-      wsCache.set('lang', localeMap?.lang)
+      setStorage('lang', localeMap?.lang)
     }
   }
 })

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác