瀏覽代碼

Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro

shizhong 2 年之前
父節點
當前提交
4584774d74
共有 100 個文件被更改,包括 2376 次插入719 次删除
  1. 58 59
      README.md
  2. 3 3
      pom.xml
  3. 1 0
      sql/mysql/optional/vue3-menu.sql
  4. 2 0
      sql/mysql/ruoyi-vue-pro.sql
  5. 1 0
      sql/optional/visualization/jimureport.mysql5.7.create.sql
  6. 24 11
      yudao-dependencies/pom.xml
  7. 2 2
      yudao-example/yudao-sso-demo-by-code/pom.xml
  8. 2 2
      yudao-example/yudao-sso-demo-by-password/pom.xml
  9. 1 0
      yudao-framework/pom.xml
  10. 1 1
      yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java
  11. 0 1
      yudao-framework/yudao-spring-boot-starter-biz-ip/pom.xml
  12. 3 1
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java
  13. 1 1
      yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml
  14. 10 6
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java
  15. 1 1
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java
  16. 7 1
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java
  17. 1 0
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientConfig.java
  18. 19 3
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java
  19. 80 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/job/RedisPendingMessageResendJob.java
  20. 2 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
  21. 37 0
      yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
  22. 14 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java
  23. 29 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java
  24. 34 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
  25. 24 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java
  26. 9 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java
  27. 24 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java
  28. 36 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java
  29. 31 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java
  30. 49 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java
  31. 1 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java
  32. 1 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  33. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/application-unit-test.yaml
  34. 6 0
      yudao-module-infra/yudao-module-infra-biz/pom.xml
  35. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/ConfigController.java
  36. 8 3
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentDAOImpl.java
  37. 45 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/SemaphoreUtils.java
  38. 16 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketConfig.java
  39. 86 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketServer.java
  40. 178 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketUsers.java
  41. 7 19
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm
  42. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceTest.java
  43. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/application-unit-test.yaml
  44. 1 4
      yudao-module-mall/pom.xml
  45. 1 1
      yudao-module-mall/yudao-module-product-biz/src/test/resources/application-unit-test.yaml
  46. 1 1
      yudao-module-mall/yudao-module-promotion-biz/src/test/resources/application-unit-test.yaml
  47. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/test/resources/application-unit-test.yaml
  48. 1 1
      yudao-module-member/yudao-module-member-biz/src/test/resources/application-unit-test.yaml
  49. 1 1
      yudao-module-pay/yudao-module-pay-biz/src/test/resources/application-unit-test.yaml
  50. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java
  51. 2 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dept/DeptDO.java
  52. 8 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java
  53. 9 3
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
  54. 8 0
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceTest.java
  55. 2 2
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dict/DictDataServiceTest.java
  56. 1 1
      yudao-module-system/yudao-module-system-biz/src/test/resources/application-unit-test.yaml
  57. 3 2
      yudao-module-visualization/yudao-module-visualization-biz/src/main/java/cn/iocoder/yudao/module/visualization/framework/jmreport/config/JmReportConfiguration.java
  58. 72 18
      yudao-module-visualization/yudao-module-visualization-biz/src/main/java/cn/iocoder/yudao/module/visualization/framework/jmreport/core/service/JmReportTokenServiceImpl.java
  59. 1 1
      yudao-server/pom.xml
  60. 5 0
      yudao-server/src/main/resources/application.yaml
  61. 1 1
      yudao-ui-admin-uniapp/uni_modules/uni-swiper-dot/components/uni-swiper-dot/uni-swiper-dot.vue
  62. 11 11
      yudao-ui-admin-vue3/README.md
  63. 17 17
      yudao-ui-admin-vue3/package.json
  64. 525 149
      yudao-ui-admin-vue3/pnpm-lock.yaml
  65. 0 1
      yudao-ui-admin-vue3/src/components/Crontab/src/Crontab.vue
  66. 3 0
      yudao-ui-admin-vue3/src/components/XTable/index.ts
  67. 335 0
      yudao-ui-admin-vue3/src/components/XTable/src/XTable.vue
  68. 81 0
      yudao-ui-admin-vue3/src/components/XTable/src/style/dark.scss
  69. 6 0
      yudao-ui-admin-vue3/src/components/XTable/src/style/index.scss
  70. 16 0
      yudao-ui-admin-vue3/src/components/XTable/src/style/light.scss
  71. 25 0
      yudao-ui-admin-vue3/src/components/XTable/src/type.ts
  72. 2 0
      yudao-ui-admin-vue3/src/components/index.ts
  73. 1 1
      yudao-ui-admin-vue3/src/hooks/web/useVxeCrudSchemas.ts
  74. 32 0
      yudao-ui-admin-vue3/src/hooks/web/useXTable.ts
  75. 4 2
      yudao-ui-admin-vue3/src/main.ts
  76. 1 18
      yudao-ui-admin-vue3/src/plugins/vxeTable/index.ts
  77. 1 0
      yudao-ui-admin-vue3/src/plugins/vxeTable/renderer/index.tsx
  78. 34 0
      yudao-ui-admin-vue3/src/plugins/vxeTable/renderer/preview.tsx
  79. 2 8
      yudao-ui-admin-vue3/src/router/index.ts
  80. 36 26
      yudao-ui-admin-vue3/src/store/modules/dict.ts
  81. 17 32
      yudao-ui-admin-vue3/src/utils/auth.ts
  82. 15 41
      yudao-ui-admin-vue3/src/views/Login/components/LoginForm.vue
  83. 5 6
      yudao-ui-admin-vue3/src/views/infra/apiAccessLog/index.vue
  84. 9 13
      yudao-ui-admin-vue3/src/views/infra/apiErrorLog/index.vue
  85. 7 16
      yudao-ui-admin-vue3/src/views/infra/codegen/index.vue
  86. 7 19
      yudao-ui-admin-vue3/src/views/infra/config/index.vue
  87. 6 13
      yudao-ui-admin-vue3/src/views/infra/dataSourceConfig/index.vue
  88. 7 14
      yudao-ui-admin-vue3/src/views/infra/fileConfig/index.vue
  89. 1 1
      yudao-ui-admin-vue3/src/views/infra/fileList/fileList.data.ts
  90. 6 13
      yudao-ui-admin-vue3/src/views/infra/fileList/index.vue
  91. 5 11
      yudao-ui-admin-vue3/src/views/infra/job/JobLog.vue
  92. 11 22
      yudao-ui-admin-vue3/src/views/infra/job/index.vue
  93. 118 0
      yudao-ui-admin-vue3/src/views/infra/webSocket/index.vue
  94. 7 19
      yudao-ui-admin-vue3/src/views/pay/app/index.vue
  95. 7 19
      yudao-ui-admin-vue3/src/views/pay/merchant/index.vue
  96. 5 11
      yudao-ui-admin-vue3/src/views/pay/order/index.vue
  97. 5 12
      yudao-ui-admin-vue3/src/views/pay/refund/index.vue
  98. 9 15
      yudao-ui-admin-vue3/src/views/system/dept/index.vue
  99. 14 38
      yudao-ui-admin-vue3/src/views/system/dict/index.vue
  100. 6 13
      yudao-ui-admin-vue3/src/views/system/errorCode/index.vue

+ 58 - 59
README.md

@@ -1,6 +1,4 @@
-**严肃声明:现在、未来都不会有商业版本,所有功能全部开源!**
-
-**拒绝虚假开源,售卖商业版,程序员不骗程序员!!**
+**严肃声明:现在、未来都不会有商业版本,所有代码全部开源!**
 
 **「我喜欢写代码,乐此不疲」**  
 **「我喜欢做开源,以此为乐」**
@@ -27,7 +25,7 @@
 
 * 管理后台的 Vue3 版本采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) ,Vue2 版本采用 [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) 
 * 管理后台的移动端采用 [uni-app](https://github.com/dcloudio/uni-app) 方案,一份代码多终端适配,同时支持 APP、小程序、H5!
-* 后端采用 Spring Boot、MySQL + MyBatis Plus、Redis + Redisson
+* 后端采用 Spring Boot 多模块架构、MySQL + MyBatis Plus、Redis + Redisson
 * 数据库可使用 MySQL、Oracle、PostgreSQL、SQL Server、MariaDB、国产达梦 DM、TiDB 等
 * 权限认证使用 Spring Security & Token & Redis,支持多终端、多种用户的认证系统,支持 SSO 单点登录
 * 支持加载动态权限菜单,按钮级别权限控制,本地缓存提升性能
@@ -38,12 +36,12 @@
 * 集成阿里云、腾讯云等短信渠道,集成 MinIO、阿里云、腾讯云、七牛云等云存储服务
 * 集成报表设计器,支持数据报表、图形报表、打印设计等
 
-| 项目名                | 说明                     | 传送门                                                                                                                               |
-|--------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
-| `ruoyi-vue-pro`    | Spring Boot 多模块        | **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-pro)**     [Github](https://github.com/YunaiV/ruoyi-vue-pro)   |
-| `yudao-cloud`  | Spring Cloud 微服务       | **[Gitee](https://gitee.com/zhijiantianya/yudao-cloud)**     [Github](https://github.com/YunaiV/yudao-cloud)       |
-| `Spring-Boot-Labs` | Spring Boot & Cloud 入门 | **[Gitee](https://gitee.com/zhijiantianya/SpringBoot-Labs)**     [Github](https://github.com/YunaiV/SpringBoot-Labs) |
- | `ruoyi-vue-pro-mini` | 精简版 移除工作流 支付等模块| **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/mini)**  |
+| 项目名                  | 说明                     | 传送门                                                                                                                                 |
+|----------------------|------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
+| `ruoyi-vue-pro`      | Spring Boot 多模块        | **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-pro)**     [Github](https://github.com/YunaiV/ruoyi-vue-pro)     |
+| `yudao-cloud`        | Spring Cloud 微服务       | **[Gitee](https://gitee.com/zhijiantianya/yudao-cloud)**     [Github](https://github.com/YunaiV/yudao-cloud)         |
+| `Spring-Boot-Labs`   | Spring Boot & Cloud 入门 | **[Gitee](https://gitee.com/zhijiantianya/SpringBoot-Labs)**     [Github](https://github.com/YunaiV/SpringBoot-Labs) |
+ | `ruoyi-vue-pro-mini` | 精简版:移除工作流、支付等模块        | **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/mini)**                                                                |
 
 ## 😎 开源协议
 
@@ -96,8 +94,9 @@
 | ⭐️  | 登录日志  | 系统登录日志记录查询,包含登录异常               |
 | 🚀  | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务     |
 |     | 通知公告  | 系统通知公告信息发布维护                    |
-| 🚀  | 敏感词  | 配置系统敏感词,支持标签分组                  |
+| 🚀  | 敏感词   | 配置系统敏感词,支持标签分组                  |
 | 🚀  | 应用管理  | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 |
+| 🚀  | 地区管理  | 展示省份、城市、区镇等城市信息,支持 IP 对应城市      |
 
 ### 工作流程
 
@@ -132,7 +131,7 @@ ps:核心功能已经实现,正在对接微信小程序中...
 |     | 表单构建     | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件         |
 | 🚀  | 配置管理     | 对系统动态配置常用参数,支持 SpringBoot 加载                 |
 | ⭐️  | 定时任务     | 在线(添加、修改、删除)任务调度包含执行结果日志                     |
-| 🚀  | 文件服务     | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等      | 
+| 🚀  | 文件服务     | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等   | 
 | 🚀  | API 日志   | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题   |
 |     | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈              |
 |     | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理           |
@@ -169,47 +168,47 @@ ps:核心功能已经实现,正在对接微信小程序中...
 
 ## 🐨 技术栈
 
-| 项目                      | 说明                 |
-|-------------------------|-----------------------|
-| `yudao-dependencies`    | Maven 依赖版本管理       |
-| `yudao-framework`       | Java 框架拓展          |
-| `yudao-server`          | 管理后台 + 用户 APP 的服务端 |
-| `yudao-ui-admin`        | 管理后台的 Vue2 前端项目     |
-| `yudao-ui-admin-vue3`   | 管理后台的 Vue3 前端项目     |
-| `yudao-ui-admin-uniapp` | 管理后台的 uni-app 多端项目  |
-| `yudao-ui-app`          | 用户 APP 的 UI 界面     |
-| `yudao-module-system`   | 系统功能的 Module 模块    |
-| `yudao-module-member`   | 会员中心的 Module 模块    |
-| `yudao-module-infra`    | 基础设施的 Module 模块    |
-| `yudao-module-tool`     | 研发工具的 Module 模块    |
-| `yudao-module-bpm`      | 工作流程的 Module 模块    |
-| `yudao-module-pay`      | 支付系统的 Module 模块    |
+| 项目                           | 说明                 |
+|------------------------------|--------------------|
+| `yudao-dependencies`         | Maven 依赖版本管理       |
+| `yudao-framework`            | Java 框架拓展          |
+| `yudao-server`               | 管理后台 + 用户 APP 的服务端 |
+| `yudao-ui-admin`             | 管理后台的 Vue2 前端项目    |
+| `yudao-ui-admin-vue3`        | 管理后台的 Vue3 前端项目    |
+| `yudao-ui-admin-uniapp`      | 管理后台的 uni-app 多端项目 |
+| `yudao-ui-app`               | 用户 APP 的 UI 界面     |
+| `yudao-module-system`        | 系统功能的 Module 模块    |
+| `yudao-module-member`        | 会员中心的 Module 模块    |
+| `yudao-module-infra`         | 基础设施的 Module 模块    |
+| `yudao-module-bpm`           | 工作流程的 Module 模块    |
+| `yudao-module-pay`           | 支付系统的 Module 模块    |
+| `yudao-module-visualization` | 大屏报表 Module 模块     |
 
 ### 后端
 
-| 框架                                                                                         | 说明                   | 版本          | 学习指南                                                           |
-|---------------------------------------------------------------------------------------------|-----------------------|-------------|----------------------------------------------------------------|
-| [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架             | 2.7.6       | [文档](https://github.com/YunaiV/SpringBoot-Labs)                |
-| [MySQL](https://www.mysql.com/cn/)                                                          | 数据库服务器             | 5.7 / 8.0+  |                                                                |
-| [Druid](https://github.com/alibaba/druid)                                                   | JDBC 连接池、监控组件     | 1.2.15      | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
-| [MyBatis Plus](https://mp.baomidou.com/)                                                    | MyBatis 增强工具包       | 3.5.2       | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao)         |
-| [Dynamic Datasource](https://dynamic-datasource.com/)                                       | 动态数据源               | 3.6.0       | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
-| [Redis](https://redis.io/)                                                                  | key-value 数据库        | 5.0 / 6.0   |                                                                |
-| [Redisson](https://github.com/redisson/redisson)                                            | Redis 客户端            | 3.18.0      | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao)           |
-| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架               | 5.3.24      | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao)               |
-| [Spring Security](https://github.com/spring-projects/spring-security)                       | Spring 安全框架         | 5.7.5       | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
-| [Hibernate Validator](https://github.com/hibernate/hibernate-validator)                     | 参数校验组件             | 6.2.5       | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao)      |
-| [Flowable](https://github.com/flowable/flowable-engine)                                     | 工作流引擎               | 6.7.2       | [文档](https://doc.iocoder.cn/bpm/)                                                     |
-| [Quartz](https://github.com/quartz-scheduler)                                               | 任务调度组件             | 2.3.2       | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao)             |
-| [Knife4j](https://gitee.com/xiaoym/knife4j)                                                 | Swagger 增强 UI 实现    | 3.0.3       | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao)         |
-| [Resilience4j](https://github.com/resilience4j/resilience4j)                                | 服务保障组件             | 1.7.1       | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao)    |
+| 框架                                                                                          | 说明               | 版本          | 学习指南                                                           |
+|---------------------------------------------------------------------------------------------|------------------|-------------|----------------------------------------------------------------|
+| [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架           | 2.7.7       | [文档](https://github.com/YunaiV/SpringBoot-Labs)                |
+| [MySQL](https://www.mysql.com/cn/)                                                          | 数据库服务器           | 5.7 / 8.0+  |                                                                |
+| [Druid](https://github.com/alibaba/druid)                                                   | JDBC 连接池、监控组件    | 1.2.15      | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
+| [MyBatis Plus](https://mp.baomidou.com/)                                                    | MyBatis 增强工具包    | 3.5.3       | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao)         |
+| [Dynamic Datasource](https://dynamic-datasource.com/)                                       | 动态数据源            | 3.6.1       | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
+| [Redis](https://redis.io/)                                                                  | key-value 数据库    | 5.0 / 6.0   |                                                                |
+| [Redisson](https://github.com/redisson/redisson)                                            | Redis 客户端        | 3.18.0      | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao)           |
+| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架           | 5.3.24      | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao)               |
+| [Spring Security](https://github.com/spring-projects/spring-security)                       | Spring 安全框架      | 5.7.5       | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
+| [Hibernate Validator](https://github.com/hibernate/hibernate-validator)                     | 参数校验组件           | 6.2.5       | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao)      |
+| [Flowable](https://github.com/flowable/flowable-engine)                                     | 工作流引擎            | 6.8.0       | [文档](https://doc.iocoder.cn/bpm/)                              |
+| [Quartz](https://github.com/quartz-scheduler)                                               | 任务调度组件           | 2.3.2       | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao)             |
+| [Knife4j](https://gitee.com/xiaoym/knife4j)                                                 | Swagger 增强 UI 实现 | 3.0.3       | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao)         |
+| [Resilience4j](https://github.com/resilience4j/resilience4j)                                | 服务保障组件           | 1.7.1       | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao)    |
 | [SkyWalking](https://skywalking.apache.org/)                                                | 分布式应用追踪系统        | 8.12.0      | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao)      |
-| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin)                       | Spring Boot 监控平台    | 2.7.9       | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao)           |
-| [Jackson](https://github.com/FasterXML/jackson)                                             | JSON 工具库             | 2.13.3      |                                                                |
-| [MapStruct](https://mapstruct.org/)                                                         | Java Bean 转换         | 1.5.3.Final | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao)       |
-| [Lombok](https://projectlombok.org/)                                                        | 消除冗长的 Java 代码     | 1.18.24     | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao)          |
-| [JUnit](https://junit.org/junit5/)                                                          | Java 单元测试框架        | 5.8.2       | -                                                              |
-| [Mockito](https://github.com/mockito/mockito)                                               | Java Mock 框架         | 4.8.0       | -                                                              |
+| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin)                       | Spring Boot 监控平台 | 2.7.9       | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao)           |
+| [Jackson](https://github.com/FasterXML/jackson)                                             | JSON 工具库         | 2.13.3      |                                                                |
+| [MapStruct](https://mapstruct.org/)                                                         | Java Bean 转换     | 1.5.3.Final | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao)       |
+| [Lombok](https://projectlombok.org/)                                                        | 消除冗长的 Java 代码    | 1.18.24     | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao)          |
+| [JUnit](https://junit.org/junit5/)                                                          | Java 单元测试框架      | 5.8.2       | -                                                              |
+| [Mockito](https://github.com/mockito/mockito)                                               | Java Mock 框架     | 4.8.0       | -                                                              |
 
 ### [管理后台 Vue2 前端](./yudao-ui-admin)
 
@@ -220,22 +219,22 @@ ps:核心功能已经实现,正在对接微信小程序中...
 
 ### [管理后台 Vue3 前端](./yudao-ui-admin-vue3)
 
-| 框架                                                                  |     说明      |   版本   |
+| 框架                                                                   |      说明      |   版本   |
 |----------------------------------------------------------------------|:------------:|:------:|
-| [Vue](https://staging-cn.vuejs.org/)                                 |   Vue 框架    | 3.2.45 |
-| [Vite](https://cn.vitejs.dev//)                                      | 开发与构建工具  | 4.0.3  |
-| [Element Plus](https://element-plus.org/zh-CN/)                      | Element Plus | 2.2.27 |
+| [Vue](https://staging-cn.vuejs.org/)                                 |    Vue 框架    | 3.2.45 |
+| [Vite](https://cn.vitejs.dev//)                                      |   开发与构建工具    | 4.0.4  |
+| [Element Plus](https://element-plus.org/zh-CN/)                      | Element Plus | 2.2.28 |
 | [TypeScript](https://www.typescriptlang.org/docs/)                   |  TypeScript  | 4.9.4  |
 | [pinia](https://pinia.vuejs.org/)                                    |    vuex5     | 2.0.28 |
-| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) |    国际化     | 9.2.2  |
-| [vxe-table](https://vxetable.cn/)                                    |  vue最强表单  | 4.3.7  |
+| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) |     国际化      | 9.2.2  |
+| [vxe-table](https://vxetable.cn/)                                    |   vue最强表单    | 4.3.7  |
 
 ### [管理后台 uni-app 跨端](./yudao-ui-admin-uniapp)
 
-| 框架                                                                  | 说明               | 版本     |
-|----------------------------------------------------------------------|------------------|--------|
-| [uni-app](hhttps://github.com/dcloudio/uni-app)                                 | 跨平台框架           | 2.0.0 |
-| [uni-ui](https://github.com/dcloudio/uni-ui)                                      | 基于 uni-app 的 UI 框架          | 1.4.20  |
+| 框架                                              | 说明                 | 版本     |
+|-------------------------------------------------|--------------------|--------|
+| [uni-app](hhttps://github.com/dcloudio/uni-app) | 跨平台框架              | 2.0.0  |
+| [uni-ui](https://github.com/dcloudio/uni-ui)    | 基于 uni-app 的 UI 框架 | 1.4.20 |
 
 ## 🐷 演示图
 

+ 3 - 3
pom.xml

@@ -14,12 +14,12 @@
         <module>yudao-server</module>
         <!-- 各种 module 拓展 -->
         <module>yudao-module-member</module>
-        <module>yudao-module-bpm</module>
         <module>yudao-module-system</module>
         <module>yudao-module-infra</module>
         <module>yudao-module-pay</module>
-<!--        <module>yudao-module-mall</module>-->
+<!--        <module>yudao-module-bpm</module>-->
         <module>yudao-module-visualization</module>
+<!--        <module>yudao-module-mall</module>-->
         <!-- 示例项目 -->
         <module>yudao-example</module>
     </modules>
@@ -29,7 +29,7 @@
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <properties>
-        <revision>1.6.5-snapshot</revision>
+        <revision>1.6.6-snapshot</revision>
         <!-- Maven 相关 -->
         <java.version>1.8</java.version>
         <maven.compiler.source>${java.version}</maven.compiler.source>

+ 1 - 0
sql/mysql/optional/vue3-menu.sql

@@ -262,5 +262,6 @@ INSERT INTO `system_menu` VALUES (1266, '客户端更新', 'system:oauth2-client
 INSERT INTO `system_menu` VALUES (1267, '客户端删除', 'system:oauth2-client:delete', 3, 4, 1263, '', '', '', 0, b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:33', b'0');
 INSERT INTO `system_menu` VALUES (1281, '可视化报表', '', 1, 12, 0, '/visualization', 'ep:histogram', NULL, 0, b'1', b'1', '1', '2022-07-10 20:22:15', '1', '2022-07-10 20:33:30', b'0');
 INSERT INTO `system_menu` VALUES (1282, '积木报表', '', 2, 1, 1281, 'jimu-report', 'ep:histogram', 'visualization/jmreport/index', 0, b'1', b'1', '1', '2022-07-10 20:26:36', '1', '2022-07-28 21:17:34', b'0');
+INSERT INTO `system_menu` VALUES (1283, 'webSocket连接', '', 2, 14, 2, 'webSocket', 'ep:turn-off', 'infra/webSocket/index', 0, b'1', b'1', '1', '2023-01-01 11:43:04', '1', '2023-01-01 11:43:04', b'0');
 
 SET FOREIGN_KEY_CHECKS = 1;

+ 2 - 0
sql/mysql/ruoyi-vue-pro.sql

@@ -1710,6 +1710,8 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1267, '客户端删除', 'system:oauth2-client:delete', 3, 4, 1263, '', '', '', 0, b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:33', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1281, '可视化报表', '', 1, 12, 0, '/visualization', 'chart', NULL, 0, b'1', b'1', '1', '2022-07-10 20:22:15', '1', '2022-07-10 20:33:30', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1282, '积木报表', '', 2, 1, 1281, 'jimu-report', 'example', 'visualization/jmreport/index', 0, b'1', b'1', '1', '2022-07-10 20:26:36', '1', '2022-07-28 21:17:34', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1283, 'webSocket连接', '', 2, 14, 2, 'webSocket', 'message', 'infra/webSocket/index', 0, b'1', b'1', '1', '2023-01-01 11:43:04', '1', '2023-01-01 11:43:04', b'0');
+
 COMMIT;
 
 -- ----------------------------

+ 1 - 0
sql/optional/visualization/jimureport.mysql5.7.create.sql

@@ -1344,6 +1344,7 @@ CREATE TABLE `jimu_report_share`  (
   `last_update_time` datetime NULL DEFAULT NULL COMMENT '最后更新时间',
   `term_of_validity` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '有效期(0:永久有效,1:1天,2:7天)',
   `status` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '是否过期(0未过期,1已过期)',
+  `preview_lock_status` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码锁状态',
   PRIMARY KEY (`id`) USING BTREE
 ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '积木报表预览权限表' ROW_FORMAT = Dynamic;
 

+ 24 - 11
yudao-dependencies/pom.xml

@@ -14,18 +14,18 @@
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <properties>
-        <revision>1.6.5-snapshot</revision>
+        <revision>1.6.6-snapshot</revision>
         <!-- 统一依赖管理 -->
-        <spring.boot.version>2.7.6</spring.boot.version>
+        <spring.boot.version>2.7.7</spring.boot.version>
         <!-- Web 相关 -->
         <knife4j.version>3.0.3</knife4j.version>
         <swagger-annotations.version>1.6.8</swagger-annotations.version>
         <servlet.versoin>2.5</servlet.versoin>
         <!-- DB 相关 -->
         <druid.version>1.2.15</druid.version>
-        <mybatis-plus.version>3.5.2</mybatis-plus.version>
-        <mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
-        <dynamic-datasource.version>3.6.0</dynamic-datasource.version>
+        <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
+        <mybatis-plus-generator.version>3.5.3.1</mybatis-plus-generator.version>
+        <dynamic-datasource.version>3.6.1</dynamic-datasource.version>
         <redisson.version>3.18.0</redisson.version>
         <!-- 服务保障相关 -->
         <lock4j.version>2.2.3</lock4j.version>
@@ -37,14 +37,14 @@
         <!-- Test 测试相关 -->
         <podam.version>7.2.11.RELEASE</podam.version>
         <jedis-mock.version>1.0.5</jedis-mock.version>
-        <mockito-inline.version>4.8.0</mockito-inline.version>
+        <mockito-inline.version>4.11.0</mockito-inline.version>
         <!-- Bpm 工作流相关 -->
-        <flowable.version>6.7.2</flowable.version>
+        <flowable.version>6.8.0</flowable.version>
         <!-- 工具类相关 -->
         <lombok.version>1.18.24</lombok.version>
         <mapstruct.version>1.5.3.Final</mapstruct.version>
-        <hutool.version>5.8.10</hutool.version>
-        <easyexcel.verion>3.1.3</easyexcel.verion>
+        <hutool.version>5.8.11</hutool.version>
+        <easyexcel.verion>3.1.4</easyexcel.verion>
         <velocity.version>2.3</velocity.version>
         <screw.version>1.0.5</screw.version>
         <fastjson.version>1.2.83</fastjson.version>
@@ -55,14 +55,15 @@
         <jsch.version>0.1.55</jsch.version>
         <tika-core.version>2.6.0</tika-core.version>
         <aj-captcha.version>1.3.0</aj-captcha.version>
-        <netty-all.version>4.1.85.Final</netty-all.version>
+        <netty-all.version>4.1.86.Final</netty-all.version>
+        <ip2region.version>2.6.6</ip2region.version>
         <!-- 三方云服务相关 -->
         <okio.version>3.0.0</okio.version>
         <okhttp3.version>4.10.0</okhttp3.version>
         <minio.version>8.4.6</minio.version>
         <aliyun-java-sdk-core.version>4.6.3</aliyun-java-sdk-core.version>
         <aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
-        <tencentcloud-sdk-java.version>3.1.637</tencentcloud-sdk-java.version>
+        <tencentcloud-sdk-java.version>3.1.660</tencentcloud-sdk-java.version>
         <justauth.version>1.4.0</justauth.version>
         <jimureport.version>1.5.6</jimureport.version>
         <xercesImpl.version>2.12.2</xercesImpl.version>
@@ -515,6 +516,12 @@
                 <version>${netty-all.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>org.lionsoul</groupId>
+                <artifactId>ip2region</artifactId>
+                <version>${ip2region.version}</version>
+            </dependency>
+
             <!-- 三方云服务相关 -->
             <dependency>
                 <groupId>com.squareup.okio</groupId>
@@ -588,6 +595,12 @@
                 <artifactId>xercesImpl</artifactId>
                 <version>${xercesImpl.version}</version>
             </dependency>
+            <!-- SpringBoot Websocket -->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-websocket</artifactId>
+                <version>${spring.boot.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 

+ 2 - 2
yudao-example/yudao-sso-demo-by-code/pom.xml

@@ -21,7 +21,7 @@
         <maven.compiler.target>8</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <!-- 统一依赖管理 -->
-        <spring.boot.version>2.7.6</spring.boot.version>
+        <spring.boot.version>2.7.7</spring.boot.version>
     </properties>
 
     <dependencyManagement>
@@ -52,7 +52,7 @@
         <dependency>
             <groupId>cn.hutool</groupId>
             <artifactId>hutool-all</artifactId>
-            <version>5.8.10</version>
+            <version>5.8.11</version>
         </dependency>
 
         <dependency>

+ 2 - 2
yudao-example/yudao-sso-demo-by-password/pom.xml

@@ -21,7 +21,7 @@
         <maven.compiler.target>8</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <!-- 统一依赖管理 -->
-        <spring.boot.version>2.7.6</spring.boot.version>
+        <spring.boot.version>2.7.7</spring.boot.version>
     </properties>
 
     <dependencyManagement>
@@ -52,7 +52,7 @@
         <dependency>
             <groupId>cn.hutool</groupId>
             <artifactId>hutool-all</artifactId>
-            <version>5.8.10</version>
+            <version>5.8.11</version>
         </dependency>
 
         <dependency>

+ 1 - 0
yudao-framework/pom.xml

@@ -40,6 +40,7 @@
 
         <module>yudao-spring-boot-starter-flowable</module>
         <module>yudao-spring-boot-starter-captcha</module>
+        <module>yudao-spring-boot-starter-websocket</module>
     </modules>
 
     <artifactId>yudao-framework</artifactId>

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java

@@ -227,7 +227,7 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
             // 调用
             Expression expression = rule.getExpression(tableName, tableAlias);
             // 断言
-            assertEquals("u.dept_id IN (10, 20) OR u.id = 1", expression.toString());
+            assertEquals("(u.dept_id IN (10, 20) OR u.id = 1)", expression.toString());
             assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
         }
     }

+ 0 - 1
yudao-framework/yudao-spring-boot-starter-biz-ip/pom.xml

@@ -34,7 +34,6 @@
         <dependency>
             <groupId>org.lionsoul</groupId>
             <artifactId>ip2region</artifactId>
-            <version>${ip2region.version}</version>
         </dependency>
 
         <dependency>

+ 3 - 1
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java

@@ -28,7 +28,9 @@ public class AreaUtils {
     @SuppressWarnings("InstantiationOfUtilityClass")
     private final static AreaUtils INSTANCE = new AreaUtils();
 
-
+    /**
+     * Area 内存缓存,提升访问速度
+     */
     private static Map<Integer, Area> areas;
 
     private AreaUtils() {

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml

@@ -52,7 +52,7 @@
         <dependency>
             <groupId>com.alipay.sdk</groupId>
             <artifactId>alipay-sdk-java</artifactId>
-            <version>4.35.0.ALL</version>
+            <version>4.35.9.ALL</version>
             <exclusions>
                 <exclusion>
                     <groupId>org.bouncycastle</groupId>

+ 10 - 6
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java

@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
 
 import cn.hutool.core.bean.BeanUtil;
-import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.date.TemporalAccessorUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.StrUtil;
@@ -26,8 +26,8 @@ import com.github.binarywang.wxpay.service.WxPayService;
 import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
 import lombok.extern.slf4j.Slf4j;
 
+import java.time.LocalDateTime;
 import java.time.ZoneId;
-import java.util.Date;
 import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
@@ -100,8 +100,8 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
         WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
                 .outTradeNo(reqDTO.getMerchantOrderId())
                 .body(reqDTO.getBody())
-                .totalFee(reqDTO.getAmount().intValue()) // 单位分
-                .timeExpire(DateUtil.format(Date.from(reqDTO.getExpireTime().atZone(ZoneId.systemDefault()).toInstant()), "yyyy-MM-dd'T'HH:mm:ssXXX"))
+                .totalFee(reqDTO.getAmount()) // 单位分
+                .timeExpire(formatDate(reqDTO.getExpireTime()))
                 .spbillCreateIp(reqDTO.getUserIp())
                 .openid(getOpenid(reqDTO))
                 .notifyUrl(reqDTO.getNotifyUrl())
@@ -115,8 +115,8 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
         WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
         request.setOutTradeNo(reqDTO.getMerchantOrderId());
         request.setDescription(reqDTO.getBody());
-        request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分
-        request.setTimeExpire(DateUtil.format(Date.from(reqDTO.getExpireTime().atZone(ZoneId.systemDefault()).toInstant()), "yyyy-MM-dd'T'HH:mm:ssXXX"));
+        request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分
+        request.setTimeExpire(formatDate(reqDTO.getExpireTime()));
         request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
         request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
         request.setNotifyUrl(reqDTO.getNotifyUrl());
@@ -196,4 +196,8 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
         throw new UnsupportedOperationException();
     }
 
+    private static String formatDate(LocalDateTime time) {
+        return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), "yyyy-MM-dd'T'HH:mm:ssXXX");
+    }
+
 }

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java

@@ -35,8 +35,8 @@ public class AliyunSmsCodeMapping implements SmsCodeMapping {
             case "isv.OUT_OF_SERVICE": return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH;
             case "isv.MOBILE_NUMBER_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID;
             case "isv.TEMPLATE_MISSING_PARAMETERS": return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR;
+            default: return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
         }
-        return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
     }
 
 }

+ 7 - 1
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java

@@ -9,10 +9,11 @@ import io.minio.*;
 import java.io.ByteArrayInputStream;
 
 import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_ALIYUN;
+import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_TENCENT;
 
 /**
  * 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
- *
+ * <p>
  * S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库
  *
  * @author 芋道源码
@@ -78,6 +79,11 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
                     .replaceAll("-internal", "")// 去除内网 Endpoint 的后缀
                     .replaceAll("https://", "");
         }
+        // 腾讯云必须有 region,否则会报错
+        if (config.getEndpoint().contains(ENDPOINT_TENCENT)) {
+            return StrUtil.subAfter(config.getEndpoint(), ".cos.", false)
+                    .replaceAll("." + ENDPOINT_TENCENT, ""); // 去除 Endpoint
+        }
         return null;
     }
 

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientConfig.java

@@ -19,6 +19,7 @@ public class S3FileClientConfig implements FileClientConfig {
 
     public static final String ENDPOINT_QINIU = "qiniucs.com";
     public static final String ENDPOINT_ALIYUN = "aliyuncs.com";
+    public static final String ENDPOINT_TENCENT = "myqcloud.com";
 
     /**
      * 节点地址

+ 19 - 3
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java

@@ -8,8 +8,11 @@ import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
 import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
 import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
 import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener;
+import cn.iocoder.yudao.framework.mq.job.RedisPendingMessageResendJob;
 import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
 import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.context.annotation.Bean;
 import org.springframework.data.redis.connection.RedisServerCommands;
@@ -24,7 +27,7 @@ import org.springframework.data.redis.listener.ChannelTopic;
 import org.springframework.data.redis.listener.RedisMessageListenerContainer;
 import org.springframework.data.redis.stream.DefaultStreamMessageListenerContainerX;
 import org.springframework.data.redis.stream.StreamMessageListenerContainer;
-import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
 import java.util.List;
 import java.util.Properties;
@@ -35,6 +38,7 @@ import java.util.Properties;
  * @author 芋道源码
  */
 @Slf4j
+@EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息
 @AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
 public class YudaoMQAutoConfiguration {
 
@@ -69,9 +73,20 @@ public class YudaoMQAutoConfiguration {
         return container;
     }
 
+    /**
+     * 创建 Redis Stream 重新消费的任务
+     */
+    @Bean
+    public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractStreamMessageListener<?>> listeners,
+                                                                     RedisMQTemplate redisTemplate,
+                                                                     @Value("${spring.application.name}") String groupName,
+                                                                     RedissonClient redissonClient) {
+        return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient);
+    }
+
     /**
      * 创建 Redis Stream 集群消费的容器
-     *
+     * <p>
      * Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html
      */
     @Bean(initMethod = "start", destroyMethod = "stop")
@@ -99,7 +114,8 @@ public class YudaoMQAutoConfiguration {
             // 创建 listener 对应的消费者分组
             try {
                 redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup());
-            } catch (Exception ignore) {}
+            } catch (Exception ignore) {
+            }
             // 设置 listener 对应的 redisTemplate
             listener.setRedisMQTemplate(redisMQTemplate);
             // 创建 Consumer 对象

+ 80 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/job/RedisPendingMessageResendJob.java

@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.framework.mq.job;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.data.redis.connection.stream.Consumer;
+import org.springframework.data.redis.connection.stream.MapRecord;
+import org.springframework.data.redis.connection.stream.PendingMessagesSummary;
+import org.springframework.data.redis.connection.stream.ReadOffset;
+import org.springframework.data.redis.connection.stream.StreamOffset;
+import org.springframework.data.redis.connection.stream.StreamRecords;
+import org.springframework.data.redis.core.StreamOperations;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 这个任务用于处理,crash 之后的消费者未消费完的消息
+ */
+@Slf4j
+@AllArgsConstructor
+public class RedisPendingMessageResendJob {
+
+    private static final String LOCK_KEY = "redis:pending:msg:lock";
+
+    private final List<AbstractStreamMessageListener<?>> listeners;
+    private final RedisMQTemplate redisTemplate;
+    private final String groupName;
+    private final RedissonClient redissonClient;
+
+    /**
+     * 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题
+     */
+    @Scheduled(cron = "35 * * * * ?")
+    public void messageResend() {
+        RLock lock = redissonClient.getLock(LOCK_KEY);
+        // 尝试加锁
+        if (lock.tryLock()) {
+            try {
+                execute();
+            } catch (Exception ex) {
+                log.error("[messageResend][执行异常]", ex);
+            } finally {
+                lock.unlock();
+            }
+        }
+    }
+
+    private void execute() {
+        StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
+        listeners.forEach(listener -> {
+            PendingMessagesSummary pendingMessagesSummary = ops.pending(listener.getStreamKey(), groupName);
+            // 每个消费者的 pending 队列消息数量
+            Map<String, Long> pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer();
+            pendingMessagesPerConsumer.forEach((consumerName, pendingMessageCount) -> {
+                log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount);
+
+                // 从消费者的 pending 队列中读取消息
+                List<MapRecord<String, Object, Object>> records = ops.read(Consumer.from(groupName, consumerName), StreamOffset.create(listener.getStreamKey(), ReadOffset.from("0")));
+                if (CollUtil.isEmpty(records)) {
+                    return;
+                }
+                for (MapRecord<String, Object, Object> record : records) {
+                    // 重新投递消息
+                    redisTemplate.getRedisTemplate().opsForStream().add(StreamRecords.newRecord()
+                            .ofObject(record.getValue()) // 设置内容
+                            .withStreamKey(listener.getStreamKey()));
+
+                    // ack 消息消费完成
+                    redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, record);
+                }
+            });
+        });
+    }
+}

+ 2 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java

@@ -129,6 +129,8 @@ public class YudaoWebSecurityConfigurerAdapter {
                 .antMatchers(buildAppApi("/**")).permitAll()
                 // 1.5 验证码captcha 允许匿名访问
                 .antMatchers("/captcha/get", "/captcha/check").permitAll()
+                // 1.6 webSocket 允许匿名访问
+                .antMatchers("/websocket/message").permitAll()
                 // ②:每个项目的自定义规则
                 .and().authorizeRequests(registry -> // 下面,循环设置自定义规则
                         authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry)))

+ 37 - 0
yudao-framework/yudao-spring-boot-starter-websocket/pom.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>cn.iocoder.boot</groupId>
+        <artifactId>yudao-framework</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>yudao-spring-boot-starter-websocket</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>WebSocket</description>
+    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
+
+
+    <dependencies>
+
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-common</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 14 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.framework.websocket.config;
+
+import cn.iocoder.yudao.framework.websocket.core.UserHandshakeInterceptor;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+@EnableConfigurationProperties(WebSocketProperties.class)
+public class WebSocketHandlerConfig {
+    @Bean
+    public HandshakeInterceptor handshakeInterceptor() {
+        return new UserHandshakeInterceptor();
+    }
+}

+ 29 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.framework.websocket.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+/**
+ * WebSocket 配置项
+ *
+ * @author xingyu4j
+ */
+@ConfigurationProperties("yudao.websocket")
+@Data
+@Validated
+public class WebSocketProperties {
+
+    /**
+     * 路径
+     */
+    private String path = "";
+    /**
+     * 默认最多允许同时在线用户数
+     */
+    private int maxOnlineCount = 0;
+    /**
+     * 是否保存session
+     */
+    private boolean sessionMap = true;
+}

+ 34 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.framework.websocket.config;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.List;
+
+/**
+ * WebSocket 自动配置
+ *
+ * @author xingyu4j
+ */
+@AutoConfiguration
+// 允许使用 yudao.websocket.enable=false 禁用websocket
+@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true)
+@EnableConfigurationProperties(WebSocketProperties.class)
+public class YudaoWebSocketAutoConfiguration {
+    @Bean
+    @ConditionalOnMissingBean
+    public WebSocketConfigurer webSocketConfigurer(List<HandshakeInterceptor> handshakeInterceptor,
+                                                   WebSocketHandler webSocketHandler,
+                                                   WebSocketProperties webSocketProperties) {
+
+        return registry -> registry
+                .addHandler(webSocketHandler, webSocketProperties.getPath())
+                .addInterceptors(handshakeInterceptor.toArray(new HandshakeInterceptor[0]));
+    }
+}

+ 24 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.framework.websocket.core;
+
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.Map;
+
+public class UserHandshakeInterceptor implements HandshakeInterceptor {
+    @Override
+    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
+        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+        attributes.put(WebSocketKeyDefine.LOGIN_USER, loginUser);
+        return true;
+    }
+
+    @Override
+    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
+
+    }
+}

+ 9 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java

@@ -0,0 +1,9 @@
+package cn.iocoder.yudao.framework.websocket.core;
+
+
+import lombok.Data;
+
+@Data
+public class WebSocketKeyDefine {
+    public static final String LOGIN_USER ="LOGIN_USER";
+}

+ 24 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.framework.websocket.core;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+@Data
+@Accessors(chain = true)
+public class WebSocketMessageDO {
+    /**
+     * 接收消息的seesion
+     */
+    private List<Object> seesionKeyList;
+    /**
+     * 发送消息
+     */
+    private String msgText;
+
+    public static WebSocketMessageDO build(List<Object> seesionKeyList, String msgText) {
+        return new WebSocketMessageDO().setMsgText(msgText).setSeesionKeyList(seesionKeyList);
+    }
+
+}

+ 36 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.framework.websocket.core;
+
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+public final class WebSocketSessionHandler {
+    private WebSocketSessionHandler() {
+    }
+
+    private static final Map<String, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>();
+
+    public static void addSession(Object sessionKey, WebSocketSession session) {
+        SESSION_MAP.put(sessionKey.toString(), session);
+    }
+
+    public static void removeSession(Object sessionKey) {
+        SESSION_MAP.remove(sessionKey.toString());
+    }
+
+    public static WebSocketSession getSession(Object sessionKey) {
+        return SESSION_MAP.get(sessionKey.toString());
+    }
+
+    public static Collection<WebSocketSession> getSessions() {
+        return SESSION_MAP.values();
+    }
+
+    public static Set<String> getSessionKeys() {
+        return SESSION_MAP.keySet();
+    }
+
+}

+ 31 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.framework.websocket.core;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+
+@Slf4j
+public class WebSocketUtils {
+    public static boolean sendMessage(WebSocketSession seesion, String message) {
+        if (seesion == null) {
+            log.error("seesion 不存在");
+            return false;
+        }
+        if (seesion.isOpen()) {
+            try {
+                seesion.sendMessage(new TextMessage(message));
+            } catch (IOException e) {
+                log.error("WebSocket 消息发送异常 Session={} | msg= {} | exception={}", seesion, message, e);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static boolean sendMessage(Object sessionKey, String message) {
+        WebSocketSession session = WebSocketSessionHandler.getSession(sessionKey);
+        return sendMessage(session, message);
+    }
+}

+ 49 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.framework.websocket.core;
+
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
+
+public class YudaoWebSocketHandlerDecorator extends WebSocketHandlerDecorator {
+    public YudaoWebSocketHandlerDecorator(WebSocketHandler delegate) {
+        super(delegate);
+    }
+
+    /**
+     * websocket 连接时执行的动作
+     * @param session websocket session 对象
+     * @throws Exception 异常对象
+     */
+    @Override
+    public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
+        Object sessionKey = sessionKeyGen(session);
+        WebSocketSessionHandler.addSession(sessionKey, session);
+    }
+
+    /**
+     * websocket 关闭连接时执行的动作
+     * @param session websocket session 对象
+     * @param closeStatus 关闭状态对象
+     * @throws Exception 异常对象
+     */
+    @Override
+    public void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception {
+        Object sessionKey = sessionKeyGen(session);
+        WebSocketSessionHandler.removeSession(sessionKey);
+    }
+
+    public Object sessionKeyGen(WebSocketSession webSocketSession) {
+
+        Object obj = webSocketSession.getAttributes().get(WebSocketKeyDefine.LOGIN_USER);
+
+        if (obj instanceof LoginUser) {
+            LoginUser loginUser = (LoginUser) obj;
+            // userId 作为唯一区分
+            return String.valueOf(loginUser.getId());
+        }
+
+        return null;
+    }
+}

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.yudao.framework.websocket;

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+cn.iocoder.yudao.framework.websocket.config.YudaoWebSocketAutoConfiguration

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/application-unit-test.yaml

@@ -9,7 +9,7 @@ spring:
   # 数据源配置项
   datasource:
     name: ruoyi-vue-pro
-    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
+    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
     driver-class-name: org.h2.Driver
     username: sa
     password:

+ 6 - 0
yudao-module-infra/yudao-module-infra-biz/pom.xml

@@ -111,6 +111,12 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-file</artifactId>
         </dependency>
+
+        <!-- WebSocket -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 1 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/ConfigController.java

@@ -75,7 +75,7 @@ public class ConfigController {
         if (config == null) {
             return null;
         }
-        if (config.getVisible()) {
+        if (!config.getVisible()) {
             throw ServiceExceptionUtil.exception(ErrorCodeConstants.CONFIG_GET_VALUE_ERROR_IF_VISIBLE);
         }
         return success(config.getValue());

+ 8 - 3
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentDAOImpl.java

@@ -1,11 +1,14 @@
 package cn.iocoder.yudao.module.infra.dal.mysql.file;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.file.core.client.db.DBFileContentFrameworkDAO;
 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileContentDO;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.springframework.stereotype.Repository;
 
 import javax.annotation.Resource;
+import java.util.List;
+import java.util.Optional;
 
 @Repository
 public class FileContentDAOImpl implements DBFileContentFrameworkDAO {
@@ -27,9 +30,11 @@ public class FileContentDAOImpl implements DBFileContentFrameworkDAO {
 
     @Override
     public byte[] selectContent(Long configId, String path) {
-        FileContentDO fileContentDO = fileContentMapper.selectOne(
-                buildQuery(configId, path).select(FileContentDO::getContent));
-        return fileContentDO != null ? fileContentDO.getContent() : null;
+        List<FileContentDO> list = fileContentMapper.selectList(
+                buildQuery(configId, path).select(FileContentDO::getContent).orderByDesc(FileContentDO::getId));
+        return Optional.ofNullable(CollUtil.getFirst(list))
+                .map(FileContentDO::getContent)
+                .orElse(null);
     }
 
     private LambdaQueryWrapper<FileContentDO> buildQuery(Long configId, String path) {

+ 45 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/SemaphoreUtils.java

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.infra.websocket;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.concurrent.Semaphore;
+
+/**
+ * 信号量相关处理
+ *
+ */
+@Slf4j
+public class SemaphoreUtils {
+
+    /**
+     * 获取信号量
+     *
+     * @param semaphore
+     * @return
+     */
+    public static boolean tryAcquire(Semaphore semaphore) {
+        boolean flag = false;
+
+        try {
+            flag = semaphore.tryAcquire();
+        } catch (Exception e) {
+            log.error("获取信号量异常", e);
+        }
+
+        return flag;
+    }
+
+    /**
+     * 释放信号量
+     *
+     * @param semaphore
+     */
+    public static void release(Semaphore semaphore) {
+
+        try {
+            semaphore.release();
+        } catch (Exception e) {
+            log.error("释放信号量异常", e);
+        }
+    }
+}

+ 16 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketConfig.java

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.infra.websocket;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+/**
+ * websocket 配置
+ */
+@Configuration
+public class WebSocketConfig {
+    @Bean
+    public ServerEndpointExporter serverEndpointExporter() {
+        return new ServerEndpointExporter();
+    }
+}

+ 86 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketServer.java

@@ -0,0 +1,86 @@
+package cn.iocoder.yudao.module.infra.websocket;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.*;
+import javax.websocket.server.ServerEndpoint;
+import java.util.concurrent.Semaphore;
+
+/**
+ * websocket 消息处理
+ */
+@Component
+@ServerEndpoint("/websocket/message")
+@Slf4j
+public class WebSocketServer {
+
+    /**
+     * 默认最多允许同时在线用户数100
+     */
+    public static int socketMaxOnlineCount = 100;
+
+    private static final Semaphore SOCKET_SEMAPHORE = new Semaphore(socketMaxOnlineCount);
+
+    /**
+     * 连接建立成功调用的方法
+     */
+    @OnOpen
+    public void onOpen(Session session) throws Exception {
+        // 尝试获取信号量
+        boolean semaphoreFlag = SemaphoreUtils.tryAcquire(SOCKET_SEMAPHORE);
+        if (!semaphoreFlag) {
+            // 未获取到信号量
+            log.error("当前在线人数超过限制数:{}", socketMaxOnlineCount);
+            WebSocketUsers.sendMessage(session, "当前在线人数超过限制数:" + socketMaxOnlineCount);
+            session.close();
+        } else {
+            String userId = WebSocketUsers.getParam("userId", session);
+            if (userId != null) {
+                // 添加用户
+                WebSocketUsers.addSession(userId, session);
+                log.info("用户【userId={}】建立连接,当前连接用户总数:{}", userId, WebSocketUsers.getUsers().size());
+                WebSocketUsers.sendMessage(session, "接收内容:连接成功");
+            } else {
+                WebSocketUsers.sendMessage(session, "接收内容:连接失败");
+            }
+        }
+    }
+
+    /**
+     * 连接关闭时处理
+     */
+    @OnClose
+    public void onClose(Session session) {
+        log.info("用户【sessionId={}】关闭连接!", session.getId());
+        // 移除用户
+        WebSocketUsers.removeSession(session);
+        // 获取到信号量则需释放
+        SemaphoreUtils.release(SOCKET_SEMAPHORE);
+    }
+
+    /**
+     * 抛出异常时处理
+     */
+    @OnError
+    public void onError(Session session, Throwable exception) throws Exception {
+        if (session.isOpen()) {
+            // 关闭连接
+            session.close();
+        }
+        String sessionId = session.getId();
+        log.info("用户【sessionId={}】连接异常!异常信息:{}", sessionId, exception);
+        // 移出用户
+        WebSocketUsers.removeSession(session);
+        // 获取到信号量则需释放
+        SemaphoreUtils.release(SOCKET_SEMAPHORE);
+    }
+
+    /**
+     * 收到客户端消息时调用的方法
+     */
+    @OnMessage
+    public void onMessage(Session session, String message) {
+        WebSocketUsers.sendMessage(session, "接收内容:" + message);
+    }
+}

+ 178 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketUsers.java

@@ -0,0 +1,178 @@
+package cn.iocoder.yudao.module.infra.websocket;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.bouncycastle.util.Strings;
+
+import javax.validation.constraints.NotNull;
+import javax.websocket.Session;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * websocket 客户端用户
+ */
+@Slf4j
+public class WebSocketUsers {
+
+    /**
+     * 用户集
+     *  TODO 需要登录用户的session?
+     */
+    private static final Map<String, Session> SESSION_MAP = new ConcurrentHashMap<>();
+
+    /**
+     * 存储用户
+     *
+     * @param userId  唯一键
+     * @param session 用户信息
+     */
+    public static void addSession(String userId, Session session) {
+        SESSION_MAP.put(userId, session);
+    }
+
+    /**
+     * 移除用户
+     *
+     * @param session 用户信息
+     * @return 移除结果
+     */
+    public static boolean removeSession(Session session) {
+        String key = null;
+        boolean flag = SESSION_MAP.containsValue(session);
+        if (flag) {
+            Set<Map.Entry<String, Session>> entries = SESSION_MAP.entrySet();
+            for (Map.Entry<String, Session> entry : entries) {
+                Session value = entry.getValue();
+                if (value.equals(session)) {
+                    key = entry.getKey();
+                    break;
+                }
+            }
+        } else {
+            return true;
+        }
+        return removeSession(key);
+    }
+
+    /**
+     * 移出用户
+     *
+     * @param userId 用户id
+     */
+    public static boolean removeSession(String userId) {
+        log.info("用户【userId={}】退出", userId);
+        Session remove = SESSION_MAP.remove(userId);
+        if (remove != null) {
+            boolean containsValue = SESSION_MAP.containsValue(remove);
+            log.info("用户【userId={}】退出{},当前连接用户总数:{}", userId, containsValue ? "失败" : "成功", SESSION_MAP.size());
+            return containsValue;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 获取在线用户列表
+     *
+     * @return 返回用户集合
+     */
+    public static Map<String, Session> getUsers() {
+        return SESSION_MAP;
+    }
+
+    /**
+     * 向所有在线人发送消息
+     *
+     * @param message 消息内容
+     */
+    public static void sendMessageToAll(String message) {
+        SESSION_MAP.forEach((userId, session) -> {
+            if (session.isOpen()) {
+                sendMessage(session, message);
+            }
+        });
+    }
+
+    /**
+     * 异步发送文本消息
+     *
+     * @param session 用户session
+     * @param message 消息内容
+     */
+    public static void sendMessageAsync(Session session, String message) {
+        if (session.isOpen()) {
+            // TODO 需要加synchronized锁(synchronized(session))?单个session创建线程?
+            session.getAsyncRemote().sendText(message);
+        } else {
+            log.warn("用户【session={}】不在线", session.getId());
+        }
+    }
+
+    /**
+     * 同步发送文本消息
+     *
+     * @param session 用户session
+     * @param message 消息内容
+     */
+    public static void sendMessage(Session session, String message) {
+        try {
+            if (session.isOpen()) {
+                // TODO 需要加synchronized锁(synchronized(session))?单个session创建线程?
+                session.getBasicRemote().sendText(message);
+            } else {
+                log.warn("用户【session={}】不在线", session.getId());
+            }
+        } catch (IOException e) {
+            log.error("发送消息异常", e);
+        }
+
+    }
+
+    /**
+     * 根据用户id发送消息
+     *
+     * @param userId  用户id
+     * @param message 消息内容
+     */
+    public static void sendMessage(String userId, String message) {
+        Session session = SESSION_MAP.get(userId);
+        //判断是否存在该用户的session,并且是否在线
+        if (session == null || !session.isOpen()) {
+            return;
+        }
+        sendMessage(session, message);
+    }
+
+
+    /**
+     * 获取session中的指定参数值
+     *
+     * @param key     参数key
+     * @param session 用户session
+     */
+    public static String getParam(@NotNull String key, Session session) {
+        //TODO 目前只针对获取一个key的值,后期根据情况拓展多个 或者直接在onClose onOpen上获取参数?
+        String value = null;
+        Map<String, List<String>> parameters = session.getRequestParameterMap();
+        if (MapUtil.isNotEmpty(parameters)) {
+            value = parameters.get(key).get(0);
+        } else {
+            String queryString = session.getQueryString();
+            if (!StrUtil.isEmpty(queryString)) {
+                String[] params = Strings.split(queryString, '&');
+                for (String paramPair : params) {
+                    String[] nameValues = Strings.split(paramPair, '=');
+                    if (key.equals(nameValues[0])) {
+                        value = nameValues[1];
+                    }
+                }
+            }
+        }
+        return value;
+    }
+}

+ 7 - 19
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -17,7 +17,7 @@
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['${permissionPrefix}:export']"
-          @click="handleExport()"
+          @click="exportList('${table.classComment}.xls')"
         />
       </template>
       <template #actionbtns_default="{ row }">
@@ -40,10 +40,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['${permissionPrefix}:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <!-- 弹窗 -->
   <XModal id="${classNameVar}Model" :loading="modelLoading" v-model="modelVisible" :title="modelTitle">
@@ -79,8 +79,7 @@
 import { ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 // 业务相关的 import
 import { rules, allSchemas } from './${classNameVar}.data'
@@ -90,8 +89,7 @@ const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData, exportList } = useVxeGrid<${simpleClassName}Api.${simpleClassName}VO>({
+const [registerTable, { reload, deleteData, exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: ${simpleClassName}Api.get${simpleClassName}PageApi,
   deleteApi: ${simpleClassName}Api.delete${simpleClassName}Api,
@@ -121,11 +119,6 @@ const handleCreate = () => {
   modelLoading.value = false
 }
 
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '${table.classComment}.xls')
-}
-
 // 修改操作
 const handleUpdate = async (rowId: number) => {
   setDialogTile('update')
@@ -143,11 +136,6 @@ const handleDetail = async (rowId: number) => {
   modelLoading.value = false
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -169,7 +157,7 @@ const submitForm = async () => {
       } finally {
         actionLoading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

+ 1 - 1
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceTest.java

@@ -79,7 +79,7 @@ public class FileServiceTest extends BaseDbUnitTest {
         FileClient client = mock(FileClient.class);
         when(fileConfigService.getMasterFileClient()).thenReturn(client);
         String url = randomString();
-        when(client.upload(same(content), same(path), same("image/jpeg"))).thenReturn(url);
+        when(client.upload(same(content), same(path), eq("image/jpeg"))).thenReturn(url);
         when(client.getId()).thenReturn(10L);
         String name = "单测文件名";
         // 调用

+ 1 - 1
yudao-module-infra/yudao-module-infra-biz/src/test/resources/application-unit-test.yaml

@@ -9,7 +9,7 @@ spring:
   # 数据源配置项
   datasource:
     name: ruoyi-vue-pro
-    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
+    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
     driver-class-name: org.h2.Driver
     username: sa
     password:

+ 1 - 4
yudao-module-mall/pom.xml

@@ -15,18 +15,15 @@
     <name>${project.artifactId}</name>
 
     <description>
-        商城大模块,由 product 商品、promotion 营销、trade 交易 coupon等组成
+        商城大模块,由 product 商品、promotion 营销、trade 交易等组成
     </description>
     <modules>
-<!--        <module>yudao-module-coupon-api</module>-->
-<!--        <module>yudao-module-coupon-biz</module>-->
         <module>yudao-module-promotion-api</module>
         <module>yudao-module-promotion-biz</module>
         <module>yudao-module-product-api</module>
         <module>yudao-module-product-biz</module>
         <module>yudao-module-trade-api</module>
         <module>yudao-module-trade-biz</module>
-
     </modules>
 
 </project>

+ 1 - 1
yudao-module-mall/yudao-module-product-biz/src/test/resources/application-unit-test.yaml

@@ -9,7 +9,7 @@ spring:
   # 数据源配置项
   datasource:
     name: ruoyi-vue-pro
-    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
+    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
     driver-class-name: org.h2.Driver
     username: sa
     password:

+ 1 - 1
yudao-module-mall/yudao-module-promotion-biz/src/test/resources/application-unit-test.yaml

@@ -9,7 +9,7 @@ spring:
   # 数据源配置项
   datasource:
     name: ruoyi-vue-pro
-    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
+    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
     driver-class-name: org.h2.Driver
     username: sa
     password:

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/test/resources/application-unit-test.yaml

@@ -9,7 +9,7 @@ spring:
   # 数据源配置项
   datasource:
     name: ruoyi-vue-pro
-    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
+    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
     driver-class-name: org.h2.Driver
     username: sa
     password:

+ 1 - 1
yudao-module-member/yudao-module-member-biz/src/test/resources/application-unit-test.yaml

@@ -9,7 +9,7 @@ spring:
   # 数据源配置项
   datasource:
     name: ruoyi-vue-pro
-    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
+    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
     driver-class-name: org.h2.Driver
     username: sa
     password:

+ 1 - 1
yudao-module-pay/yudao-module-pay-biz/src/test/resources/application-unit-test.yaml

@@ -9,7 +9,7 @@ spring:
   # 数据源配置项
   datasource:
     name: ruoyi-vue-pro
-    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
+    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
     driver-class-name: org.h2.Driver
     username: sa
     password:

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.java

@@ -112,7 +112,7 @@ public class UserController {
     @GetMapping("/list-all-simple")
     @ApiOperation(value = "获取用户精简信息列表", notes = "只包含被开启的用户,主要用于前端的下拉选项")
     public CommonResult<List<UserSimpleRespVO>> getSimpleUsers() {
-        // 获用户列表,只要开启状态的
+        // 获用户列表,只要开启状态的
         List<AdminUserDO> list = userService.getUsersByStatus(CommonStatusEnum.ENABLE.getStatus());
         // 排序后,返回给前端
         return success(UserConvert.INSTANCE.convertList04(list));

+ 2 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dept/DeptDO.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.module.system.dal.dataobject.dept;
 
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
@@ -19,7 +19,7 @@ import lombok.EqualsAndHashCode;
 @KeySequence("system_dept_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data
 @EqualsAndHashCode(callSuper = true)
-public class DeptDO extends BaseDO {
+public class DeptDO extends TenantBaseDO {
 
     /**
      * 部门ID

+ 8 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
@@ -198,12 +199,19 @@ public class DeptServiceImpl implements DeptService {
         if (recursiveCount == 0) {
             return;
         }
+
         // 获得子部门
         Collection<DeptDO> depts = parentDeptMap.get(parentId);
         if (CollUtil.isEmpty(depts)) {
             return;
         }
+        // 针对多租户,过滤掉非当前租户的部门
+        Long tenantId = TenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            depts = CollUtil.filterNew(depts, dept -> tenantId.equals(dept.getTenantId()));
+        }
         result.addAll(depts);
+
         // 继续递归
         depts.forEach(dept -> getDeptsByParentIdFromCache(result, dept.getId(),
                 recursiveCount - 1, parentDeptMap));

+ 9 - 3
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java

@@ -8,6 +8,7 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
 import cn.iocoder.yudao.module.infra.api.file.FileApi;
 import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
@@ -72,6 +73,10 @@ public class AdminUserServiceImpl implements AdminUserService {
     @Resource
     private FileApi fileApi;
 
+    @Resource
+    @Lazy // 循环依赖(自己依赖自己),避免报错
+    private AdminUserServiceImpl self;
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createUser(UserCreateReqVO reqVO) {
@@ -83,7 +88,7 @@ public class AdminUserServiceImpl implements AdminUserService {
             }
         });
         // 校验正确性
-        checkCreateOrUpdate(null, reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(),
+        self.checkCreateOrUpdate(null, reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(),
                 reqVO.getDeptId(), reqVO.getPostIds());
         // 插入用户
         AdminUserDO user = UserConvert.INSTANCE.convert(reqVO);
@@ -102,7 +107,7 @@ public class AdminUserServiceImpl implements AdminUserService {
     @Transactional(rollbackFor = Exception.class)
     public void updateUser(UserUpdateReqVO reqVO) {
         // 校验正确性
-        checkCreateOrUpdate(reqVO.getId(), reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(),
+        self.checkCreateOrUpdate(reqVO.getId(), reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(),
                 reqVO.getDeptId(), reqVO.getPostIds());
         // 更新用户
         AdminUserDO updateObj = UserConvert.INSTANCE.convert(reqVO);
@@ -299,7 +304,8 @@ public class AdminUserServiceImpl implements AdminUserService {
         return deptIds;
     }
 
-    private void checkCreateOrUpdate(Long id, String username, String mobile, String email,
+    @DataPermission(enable = false) // 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确
+    public void checkCreateOrUpdate(Long id, String username, String mobile, String email,
                                      Long deptId, Set<Long> postIds) {
         // 校验用户存在
         checkUserExists(id);

+ 8 - 0
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceTest.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.system.service.dept;
 
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO;
@@ -12,6 +13,7 @@ import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
 import com.google.common.collect.Multimap;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
@@ -47,6 +49,12 @@ public class DeptServiceTest extends BaseDbUnitTest {
     @MockBean
     private DeptProducer deptProducer;
 
+    @BeforeEach
+    public void setUp() {
+        // 清理租户上下文
+        TenantContextHolder.clear();
+    }
+
     @Test
     @SuppressWarnings("unchecked")
     void testInitLocalCache() {

+ 2 - 2
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/dict/DictDataServiceTest.java

@@ -57,7 +57,7 @@ public class DictDataServiceTest extends BaseDbUnitTest {
         // 准备参数
         DictDataPageReqVO reqVO = new DictDataPageReqVO();
         reqVO.setLabel("芋");
-        reqVO.setDictType("yu");
+        reqVO.setDictType("yunai");
         reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
 
         // 调用
@@ -86,7 +86,7 @@ public class DictDataServiceTest extends BaseDbUnitTest {
         // 准备参数
         DictDataExportReqVO reqVO = new DictDataExportReqVO();
         reqVO.setLabel("芋");
-        reqVO.setDictType("yu");
+        reqVO.setDictType("yunai");
         reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
 
         // 调用

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/test/resources/application-unit-test.yaml

@@ -9,7 +9,7 @@ spring:
   # 数据源配置项
   datasource:
     name: ruoyi-vue-pro
-    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
+    url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
     driver-class-name: org.h2.Driver
     username: sa
     password:

+ 3 - 2
yudao-module-visualization/yudao-module-visualization-biz/src/main/java/cn/iocoder/yudao/module/visualization/framework/jmreport/config/JmReportConfiguration.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.visualization.framework.jmreport.config;
 
+import cn.iocoder.yudao.framework.security.config.SecurityProperties;
 import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
 import cn.iocoder.yudao.module.visualization.framework.jmreport.core.service.JmReportTokenServiceImpl;
 import org.jeecg.modules.jmreport.api.JmReportTokenServiceI;
@@ -18,8 +19,8 @@ public class JmReportConfiguration {
 
     @Bean
     @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
-    public JmReportTokenServiceI jmReportTokenService(OAuth2TokenApi oAuth2TokenApi) {
-        return new JmReportTokenServiceImpl(oAuth2TokenApi);
+    public JmReportTokenServiceI jmReportTokenService(OAuth2TokenApi oAuth2TokenApi, SecurityProperties securityProperties) {
+        return new JmReportTokenServiceImpl(oAuth2TokenApi, securityProperties);
     }
 
 }

+ 72 - 18
yudao-module-visualization/yudao-module-visualization-biz/src/main/java/cn/iocoder/yudao/module/visualization/framework/jmreport/core/service/JmReportTokenServiceImpl.java

@@ -1,7 +1,10 @@
 package cn.iocoder.yudao.module.visualization.framework.jmreport.core.service;
 
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.yudao.framework.security.config.SecurityProperties;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
@@ -10,6 +13,10 @@ import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
 import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
 import lombok.RequiredArgsConstructor;
 import org.jeecg.modules.jmreport.api.JmReportTokenServiceI;
+import org.springframework.http.HttpHeaders;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Objects;
 
 /**
  * {@link JmReportTokenServiceI} 实现类,提供积木报表的 Token 校验、用户信息的查询等功能
@@ -19,8 +26,37 @@ import org.jeecg.modules.jmreport.api.JmReportTokenServiceI;
 @RequiredArgsConstructor
 public class JmReportTokenServiceImpl implements JmReportTokenServiceI {
 
+    /**
+     * 积木 token head 头
+     */
+    private static final String JM_TOKEN_HEADER = "X-Access-Token";
+    /**
+     * auth 相关格式
+     */
+    private static final String AUTHORIZATION_FORMAT = SecurityFrameworkUtils.AUTHORIZATION_BEARER + " %s";
+
     private final OAuth2TokenApi oauth2TokenApi;
 
+    private final SecurityProperties securityProperties;
+
+    /**
+     * 自定义 API 数据集appian自定义 Header,解决 Token 传递。
+     * 参考 <a href="http://report.jeecg.com/2222224">api数据集token机制详解</a> 文档
+     *
+     * @return 新 head
+     */
+    @Override
+    public HttpHeaders customApiHeader() {
+        // 读取积木标标系统的 token
+        HttpServletRequest request = ServletUtils.getRequest();
+        String token = request.getHeader(JM_TOKEN_HEADER);
+
+        // 设置到 yudao 系统的 token
+        HttpHeaders headers = new HttpHeaders();
+        headers.add(securityProperties.getTokenHeader(), String.format(AUTHORIZATION_FORMAT, token));
+        return headers;
+    }
+
     /**
      * 校验 Token 是否有效,即验证通过
      *
@@ -29,8 +65,40 @@ public class JmReportTokenServiceImpl implements JmReportTokenServiceI {
      */
     @Override
     public Boolean verifyToken(String token) {
+        Long userId = SecurityFrameworkUtils.getLoginUserId();
+        if (!Objects.isNull(userId)) {
+            return true;
+        }
+        return buildLoginUserByToken(token) != null;
+    }
+
+    /**
+     * 获得用户编号
+     * <p>
+     * 虽然方法名获得的是 username,实际对应到项目中是用户编号
+     *
+     * @param token JmReport 前端传递的 token
+     * @return 用户编号
+     */
+    @Override
+    public String getUsername(String token) {
+        Long userId = SecurityFrameworkUtils.getLoginUserId();
+        if (ObjectUtil.isNotNull(userId)) {
+            return String.valueOf(userId);
+        }
+        LoginUser user = buildLoginUserByToken(token);
+        return user == null ? null : String.valueOf(user.getId());
+    }
+
+    /**
+     * 基于 token 构建登录用户
+     *
+     * @param token token
+     * @return 返回 token 对应的用户信息
+     */
+    private LoginUser buildLoginUserByToken(String token) {
         if (StrUtil.isEmpty(token)) {
-            return false;
+            return null;
         }
         // TODO 如下的实现不算特别优雅,主要咱是不想搞的太复杂,所以参考对应的 Filter 先实现了
 
@@ -41,7 +109,7 @@ public class JmReportTokenServiceImpl implements JmReportTokenServiceI {
         try {
             OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token);
             if (accessToken == null) {
-                return false;
+                return null;
             }
             user = new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
                     .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes());
@@ -49,7 +117,7 @@ public class JmReportTokenServiceImpl implements JmReportTokenServiceI {
             // do nothing:如果报错,说明认证失败,则返回 false 即可
         }
         if (user == null) {
-            return false;
+            return null;
         }
         SecurityFrameworkUtils.setLoginUser(user, WebFrameworkUtils.getRequest());
 
@@ -57,21 +125,7 @@ public class JmReportTokenServiceImpl implements JmReportTokenServiceI {
         // 目的:基于 LoginUser 获得到的租户编号,设置到 Tenant 上下文,避免查询数据库时的报错
         TenantContextHolder.setIgnore(false);
         TenantContextHolder.setTenantId(user.getTenantId());
-        return true;
-    }
-
-    /**
-     * 获得用户编号
-     *
-     * 虽然方法名获得的是 username,实际对应到项目中是用户编号
-     *
-     * @param token JmReport 前端传递的 token
-     * @return 用户编号
-     */
-    @Override
-    public String getUsername(String token) {
-        Long userId = SecurityFrameworkUtils.getLoginUserId();
-        return userId != null ? String.valueOf(userId) : null;
+        return user;
     }
 
 }

+ 1 - 1
yudao-server/pom.xml

@@ -104,7 +104,7 @@
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
-                <version>2.7.6</version> <!-- 如果 spring.boot.version 版本修改,则这里也要跟着修改 -->
+                <version>2.7.7</version> <!-- 如果 spring.boot.version 版本修改,则这里也要跟着修改 -->
                 <configuration>
                     <fork>true</fork>
                 </configuration>

+ 5 - 0
yudao-server/src/main/resources/application.yaml

@@ -92,6 +92,11 @@ yudao:
   security:
     permit-all_urls:
       - /admin-ui/** # /resources/admin-ui 目录下的静态资源
+  websocket:
+    enable: true # websocket的开关
+    path: /websocket/message # 路径
+    maxOnlineCount: 0 # 最大连接人数
+    sessionMap: true # 保存sessionMap
   swagger:
     title: 管理后台
     description: 提供管理员管理的所有功能

+ 1 - 1
yudao-ui-admin-uniapp/uni_modules/uni-swiper-dot/components/uni-swiper-dot/uni-swiper-dot.vue

@@ -35,7 +35,7 @@
 	 * @tutorial https://ext.dcloud.net.cn/plugin?id=284
 	 * @property {Number} current 当前指示点索引,必须是通过 `swiper` 的 `change` 事件获取到的 `e.detail.current`
 	 * @property {String} mode = [default|round|nav|indexes] 指示点的类型
-	 * 	@value defualt 默认指示点
+	 * 	@value default 默认指示点
 	 * 	@value round 圆形指示点
 	 * 	@value nav 条形指示点
 	 * 	@value indexes 索引指示点

+ 11 - 11
yudao-ui-admin-vue3/README.md

@@ -26,19 +26,19 @@
 
 ### 前端依赖
 
-| 框架 | 说明 | 版本 |
-| --- | --- | --- |
+| 框架 | 说明 | 版本     |
+| --- | --- |--------|
 | [Vue](https://staging-cn.vuejs.org/) | vue 框架 | 3.2.45 |
-| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.0.3 |
-| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.27 |
-| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.9.4 |
+| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.0.4  |
+| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.28 |
+| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.9.4  |
 | [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.28 |
-| [vueuse](https://vueuse.org/) | 常用工具集 | 9.8.2 |
-| [vxe-table](https://vxetable.cn/) | vue 最强表单 | 4.3.7 |
-| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
-| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.6 |
-| [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 |
-| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 3.0.0 |
+| [vueuse](https://vueuse.org/) | 常用工具集 | 9.10.0  |
+| [vxe-table](https://vxetable.cn/) | vue 最强表单 | 4.3.7  |
+| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2  |
+| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.6  |
+| [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6  |
+| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 3.0.1  |
 | [wangeditor](https://www.wangeditor.com/) | 富文本编辑器 | 5.1.23 |
 
 ### 推荐 VScode 开发,插件如下

+ 17 - 17
yudao-ui-admin-vue3/package.json

@@ -1,6 +1,6 @@
 {
   "name": "yudao-ui-admin-vue3",
-  "version": "1.6.5.1879",
+  "version": "1.6.6-snapshot.1901",
   "description": "基于vue3、vite4、element-plus、typesScript",
   "author": "xingyu",
   "private": false,
@@ -25,18 +25,18 @@
   },
   "dependencies": {
     "@iconify/iconify": "^3.0.1",
-    "@vueuse/core": "^9.9.0",
+    "@vueuse/core": "^9.10.0",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.10",
     "@zxcvbn-ts/core": "^2.1.0",
     "animate.css": "^4.1.1",
-    "axios": "^1.2.1",
+    "axios": "^1.2.2",
     "cropperjs": "^1.5.13",
     "crypto-js": "^4.1.1",
     "dayjs": "^1.11.7",
     "echarts": "^5.4.1",
     "echarts-wordcloud": "^2.1.0",
-    "element-plus": "2.2.27",
+    "element-plus": "2.2.28",
     "intro.js": "^6.0.0",
     "jsencrypt": "^3.3.1",
     "lodash-es": "^4.17.21",
@@ -55,27 +55,27 @@
     "xe-utils": "^3.5.7"
   },
   "devDependencies": {
-    "@commitlint/cli": "^17.3.0",
-    "@commitlint/config-conventional": "^17.3.0",
-    "@iconify/json": "^2.1.157",
+    "@commitlint/cli": "^17.4.0",
+    "@commitlint/config-conventional": "^17.4.0",
+    "@iconify/json": "^2.2.2",
     "@intlify/unplugin-vue-i18n": "^0.8.1",
     "@purge-icons/generated": "^0.9.0",
     "@types/intro.js": "^5.1.0",
     "@types/lodash-es": "^4.17.6",
-    "@types/node": "^18.11.17",
+    "@types/node": "^18.11.18",
     "@types/nprogress": "^0.2.0",
     "@types/qrcode": "^1.5.0",
     "@types/qs": "^6.9.7",
-    "@typescript-eslint/eslint-plugin": "^5.47.0",
-    "@typescript-eslint/parser": "^5.47.0",
+    "@typescript-eslint/eslint-plugin": "^5.48.0",
+    "@typescript-eslint/parser": "^5.48.0",
     "@vitejs/plugin-legacy": "^3.0.1",
     "@vitejs/plugin-vue": "^4.0.0",
     "@vitejs/plugin-vue-jsx": "^3.0.0",
     "autoprefixer": "^10.4.13",
     "consola": "^2.15.3",
-    "eslint": "^8.30.0",
-    "eslint-config-prettier": "^8.5.0",
-    "eslint-define-config": "^1.12.0",
+    "eslint": "^8.31.0",
+    "eslint-config-prettier": "^8.6.0",
+    "eslint-define-config": "^1.13.0",
     "eslint-plugin-prettier": "^4.2.1",
     "eslint-plugin-vue": "^9.8.0",
     "lint-staged": "^13.1.0",
@@ -84,9 +84,9 @@
     "postcss-scss": "^4.0.6",
     "prettier": "^2.8.1",
     "rimraf": "^3.0.2",
-    "rollup": "^3.8.1",
+    "rollup": "^3.9.1",
     "sass": "^1.57.1",
-    "stylelint": "^14.16.0",
+    "stylelint": "^14.16.1",
     "stylelint-config-html": "^1.1.0",
     "stylelint-config-prettier": "^9.0.4",
     "stylelint-config-recommended": "^9.0.0",
@@ -94,7 +94,7 @@
     "stylelint-order": "^5.0.0",
     "terser": "^5.16.1",
     "typescript": "4.9.4",
-    "vite": "4.0.3",
+    "vite": "4.0.4",
     "vite-plugin-compression": "^0.5.1",
     "vite-plugin-ejs": "^1.6.4",
     "vite-plugin-eslint": "^1.8.1",
@@ -104,7 +104,7 @@
     "vite-plugin-svg-icons": "^2.0.1",
     "vite-plugin-vue-setup-extend": "^0.4.0",
     "vite-plugin-windicss": "^1.8.10",
-    "vue-tsc": "^1.0.17",
+    "vue-tsc": "^1.0.22",
     "windicss": "^3.5.6"
   },
   "engines": {

文件差異過大導致無法顯示
+ 525 - 149
yudao-ui-admin-vue3/pnpm-lock.yaml


+ 0 - 1
yudao-ui-admin-vue3/src/components/Crontab/src/Crontab.vue

@@ -353,7 +353,6 @@ const select = ref()
 watch(
   () => select.value,
   () => {
-    console.info(select.value)
     if (select.value == 'custom') {
       open()
     } else {

+ 3 - 0
yudao-ui-admin-vue3/src/components/XTable/index.ts

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

+ 335 - 0
yudao-ui-admin-vue3/src/components/XTable/src/XTable.vue

@@ -0,0 +1,335 @@
+<template>
+  <VxeGrid v-bind="getProps" ref="xGrid" :class="`${prefixCls}`" class="xtable-scrollbar">
+    <template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
+      <slot :name="item" v-bind="data || {}"></slot>
+    </template>
+  </VxeGrid>
+</template>
+<script lang="ts" setup name="XTable">
+import { computed, PropType, ref, unref, useAttrs, watch } from 'vue'
+import { SizeType, VxeGridInstance } from 'vxe-table'
+import { useAppStore } from '@/store/modules/app'
+import { useDesign } from '@/hooks/web/useDesign'
+import { XTableProps } from './type'
+import { isBoolean, isFunction } from '@/utils/is'
+import { useMessage } from '@/hooks/web/useMessage'
+import download from '@/utils/download'
+import { useI18n } from '@/hooks/web/useI18n'
+
+const { t } = useI18n()
+const message = useMessage() // 消息弹窗
+
+const appStore = useAppStore()
+
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('x-vxe-table')
+
+const attrs = useAttrs()
+const emit = defineEmits(['register'])
+
+watch(
+  () => appStore.getIsDark,
+  () => {
+    if (appStore.getIsDark == true) {
+      import('./style/dark.scss')
+    }
+    if (appStore.getIsDark == false) {
+      import('./style/light.scss')
+    }
+  },
+  { immediate: true }
+)
+
+const currentSize = computed(() => {
+  let resSize: SizeType = 'small'
+  const appsize = appStore.getCurrentSize
+  switch (appsize) {
+    case 'large':
+      resSize = 'medium'
+      break
+    case 'default':
+      resSize = 'small'
+      break
+    case 'small':
+      resSize = 'mini'
+      break
+  }
+  return resSize
+})
+
+const props = defineProps({
+  options: {
+    type: Object as PropType<XTableProps>,
+    default: () => {}
+  }
+})
+const innerProps = ref<Partial<XTableProps>>()
+
+const getProps = computed(() => {
+  const options = innerProps.value || props.options
+  options.size = currentSize as any
+  options.height = 700
+  getOptionInitConfig(options)
+  getColumnsConfig(options)
+  getProxyConfig(options)
+  getPageConfig(options)
+  getToolBarConfig(options)
+  // console.log(options);
+  return {
+    ...options,
+    ...attrs
+  }
+})
+
+const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
+
+let proxyForm = false
+
+const getOptionInitConfig = (options: XTableProps) => {
+  options.size = currentSize as any
+  options.rowConfig = {
+    isCurrent: true, // 当鼠标点击行时,是否要高亮当前行
+    isHover: true // 当鼠标移到行时,是否要高亮当前行
+  }
+}
+
+// columns
+const getColumnsConfig = (options: XTableProps) => {
+  const { allSchemas } = options
+  if (!allSchemas) return
+  if (allSchemas.printSchema) {
+    options.printConfig = {
+      columns: allSchemas.printSchema
+    }
+  }
+  if (allSchemas.formSchema) {
+    proxyForm = true
+    options.formConfig = {
+      enabled: true,
+      titleWidth: 100,
+      titleAlign: 'right',
+      items: allSchemas.searchSchema
+    }
+  }
+  if (allSchemas.tableSchema) {
+    options.columns = allSchemas.tableSchema
+  }
+}
+
+// 动态请求
+const getProxyConfig = (options: XTableProps) => {
+  const { getListApi, proxyConfig, data, isList } = options
+  if (proxyConfig || data) return
+  if (getListApi && isFunction(getListApi) && !isList) {
+    if (!isList) {
+      options.proxyConfig = {
+        seq: true, // 启用动态序号代理(分页之后索引自动计算为当前页的起始序号)
+        form: proxyForm, // 启用表单代理,当点击表单提交按钮时会自动触发 reload 行为
+        props: { result: 'list', total: 'total' },
+        ajax: {
+          query: async ({ page, form }) => {
+            let queryParams: any = Object.assign({}, JSON.parse(JSON.stringify(form)))
+            if (options.params) {
+              queryParams = Object.assign(queryParams, options.params)
+            }
+            if (!options?.treeConfig) {
+              queryParams.pageSize = page.pageSize
+              queryParams.pageNo = page.currentPage
+            }
+            return new Promise(async (resolve) => {
+              resolve(await getListApi(queryParams))
+            })
+          },
+          delete: ({ body }) => {
+            return new Promise(async (resolve) => {
+              if (options.deleteApi) {
+                resolve(await options.deleteApi(JSON.stringify(body)))
+              } else {
+                Promise.reject('未设置deleteApi')
+              }
+            })
+          },
+          queryAll: ({ form }) => {
+            const queryParams = Object.assign({}, JSON.parse(JSON.stringify(form)))
+            return new Promise(async (resolve) => {
+              if (options.getAllListApi) {
+                resolve(await options.getAllListApi(queryParams))
+              } else {
+                resolve(await getListApi(queryParams))
+              }
+            })
+          }
+        }
+      }
+    } else {
+      options.proxyConfig = {
+        seq: true, // 启用动态序号代理(分页之后索引自动计算为当前页的起始序号)
+        form: true, // 启用表单代理,当点击表单提交按钮时会自动触发 reload 行为
+        props: { result: 'data' },
+        ajax: {
+          query: ({ form }) => {
+            let queryParams: any = Object.assign({}, JSON.parse(JSON.stringify(form)))
+            if (options?.params) {
+              queryParams = Object.assign(queryParams, options.params)
+            }
+            return new Promise(async (resolve) => {
+              resolve(await getListApi(queryParams))
+            })
+          }
+        }
+      }
+    }
+  }
+  if (options.exportListApi) {
+    options.exportConfig = {
+      filename: options?.exportName,
+      // 默认选中类型
+      type: 'csv',
+      // 自定义数据量列表
+      modes: options?.getAllListApi ? ['current', 'all'] : ['current'],
+      columns: options?.allSchemas?.printSchema
+    }
+  }
+}
+
+// 分页
+const getPageConfig = (options: XTableProps) => {
+  const { pagination, pagerConfig, treeConfig } = options
+  if (treeConfig) {
+    options.treeConfig = options.treeConfig
+    return
+  }
+  if (pagerConfig) return
+  if (pagination) {
+    if (isBoolean(pagination)) {
+      options.pagerConfig = {
+        border: false, // 带边框
+        background: true, // 带背景颜色
+        perfect: false, // 配套的样式
+        pageSize: 10, // 每页大小
+        pagerCount: 7, // 显示页码按钮的数量
+        autoHidden: false, // 当只有一页时自动隐藏
+        pageSizes: [5, 10, 20, 30, 50, 100], // 每页大小选项列表
+        layouts: [
+          'PrevJump',
+          'PrevPage',
+          'JumpNumber',
+          'NextPage',
+          'NextJump',
+          'Sizes',
+          'FullJump',
+          'Total'
+        ]
+      }
+      return
+    }
+    options.pagerConfig = pagination
+  } else {
+    if (pagination != false) {
+      options.pagerConfig = {
+        border: false, // 带边框
+        background: true, // 带背景颜色
+        perfect: false, // 配套的样式
+        pageSize: 10, // 每页大小
+        pagerCount: 7, // 显示页码按钮的数量
+        autoHidden: false, // 当只有一页时自动隐藏
+        pageSizes: [5, 10, 20, 30, 50, 100], // 每页大小选项列表
+        layouts: [
+          'PrevJump',
+          'PrevPage',
+          'JumpNumber',
+          'NextPage',
+          'NextJump',
+          'Sizes',
+          'FullJump',
+          'Total'
+        ]
+      }
+    }
+  }
+}
+
+// tool bar
+const getToolBarConfig = (options: XTableProps) => {
+  const { toolBar, toolbarConfig, topActionSlots } = options
+  if (toolbarConfig) return
+  if (toolBar) {
+    if (!isBoolean(toolBar)) {
+      options.toolbarConfig = toolBar
+      return
+    }
+  } else if (!topActionSlots) {
+    options.toolbarConfig = {
+      slots: { buttons: 'toolbar_buttons' }
+    }
+  }
+}
+
+// 刷新列表
+const reload = () => {
+  const g = unref(xGrid)
+  if (!g) {
+    return
+  }
+  g.commitProxy('query')
+}
+
+// 删除
+const deleteData = async (ids: string | number) => {
+  const g = unref(xGrid)
+  if (!g) {
+    return
+  }
+  const options = innerProps.value || props.options
+  if (!options.deleteApi) {
+    console.error('未传入delListApi')
+    return
+  }
+  return new Promise(async () => {
+    message.delConfirm().then(async () => {
+      await (options?.deleteApi && options?.deleteApi(ids))
+      message.success(t('common.delSuccess'))
+      // 刷新列表
+      reload()
+    })
+  })
+}
+
+// 导出
+const exportList = async (fileName?: string) => {
+  const g = unref(xGrid)
+  if (!g) {
+    return
+  }
+  const options = innerProps.value || props.options
+  if (!options?.exportListApi) {
+    console.error('未传入exportListApi')
+    return
+  }
+  const queryParams = Object.assign({}, JSON.parse(JSON.stringify(g.getProxyInfo()?.form)))
+  message.exportConfirm().then(async () => {
+    const res = await (options?.exportListApi && options?.exportListApi(queryParams))
+    download.excel(res as unknown as Blob, fileName ? fileName : 'excel.xls')
+  })
+}
+
+// 获取查询参数
+const getSearchData = () => {
+  const g = unref(xGrid)
+  if (!g) {
+    return
+  }
+  const queryParams = Object.assign({}, JSON.parse(JSON.stringify(g.getProxyInfo()?.form)))
+  return queryParams
+}
+
+const setProps = (prop: Partial<XTableProps>) => {
+  innerProps.value = { ...unref(innerProps), ...prop }
+}
+
+defineExpose({ reload, Ref: xGrid, getSearchData, deleteData, exportList })
+emit('register', { reload, getSearchData, setProps, deleteData, exportList })
+</script>
+<style lang="scss">
+@import './style/index.scss';
+</style>

+ 81 - 0
yudao-ui-admin-vue3/src/components/XTable/src/style/dark.scss

@@ -0,0 +1,81 @@
+// 修改样式变量
+//@import 'vxe-table/styles/variable.scss';
+
+/*font*/
+$vxe-font-color: #e5e7eb;
+// $vxe-font-size: 14px !default;
+// $vxe-font-size-medium: 16px !default;
+// $vxe-font-size-small: 14px !default;
+// $vxe-font-size-mini: 12px !default;
+
+/*color*/
+$vxe-primary-color: #409eff !default;
+$vxe-success-color: #67c23a !default;
+$vxe-info-color: #909399 !default;
+$vxe-warning-color: #e6a23c !default;
+$vxe-danger-color: #f56c6c !default;
+$vxe-disabled-color: #bfbfbf !default;
+$vxe-primary-disabled-color: #c0c4cc !default;
+
+/*loading*/
+$vxe-loading-color: $vxe-primary-color !default;
+$vxe-loading-background-color: #1d1e1f !default;
+$vxe-loading-z-index: 999 !default;
+
+/*icon*/
+$vxe-icon-font-family: Verdana, Arial, Tahoma !default;
+$vxe-icon-background-color: #e5e7eb !default;
+
+/*toolbar*/
+$vxe-toolbar-background-color: #1d1e1f !default;
+$vxe-toolbar-button-border: #dcdfe6 !default;
+$vxe-toolbar-custom-active-background-color: #d9dadb !default;
+$vxe-toolbar-panel-background-color: #e5e7eb !default;
+
+$vxe-table-font-color: #e5e7eb;
+$vxe-table-header-background-color: #1d1e1f;
+$vxe-table-body-background-color: #141414;
+$vxe-table-row-striped-background-color: #1d1d1d;
+$vxe-table-row-hover-background-color: #1d1e1f;
+$vxe-table-row-hover-striped-background-color: #1e1e1e;
+$vxe-table-footer-background-color: #1d1e1f;
+$vxe-table-row-current-background-color: #302d2d;
+$vxe-table-column-current-background-color: #302d2d;
+$vxe-table-column-hover-background-color: #302d2d;
+$vxe-table-row-hover-current-background-color: #302d2d;
+$vxe-table-row-checkbox-checked-background-color: #3e3c37 !default;
+$vxe-table-row-hover-checkbox-checked-background-color: #615a4a !default;
+$vxe-table-menu-background-color: #1d1e1f;
+$vxe-table-border-width: 1px !default;
+$vxe-table-border-color: #4c4d4f !default;
+$vxe-table-fixed-left-scrolling-box-shadow: 8px 0px 10px -5px rgba(0, 0, 0, 0.12) !default;
+$vxe-table-fixed-right-scrolling-box-shadow: -8px 0px 10px -5px rgba(0, 0, 0, 0.12) !default;
+
+$vxe-form-background-color: #141414;
+
+/*pager*/
+$vxe-pager-background-color: #1d1e1f !default;
+$vxe-pager-perfect-background-color: #262727 !default;
+$vxe-pager-perfect-button-background-color: #a7a3a3 !default;
+
+$vxe-input-background-color: #141414;
+$vxe-input-border-color: #4c4d4f !default;
+
+$vxe-select-option-hover-background-color: #262626 !default;
+$vxe-select-panel-background-color: #141414 !default;
+$vxe-select-empty-color: #262626 !default;
+$vxe-optgroup-title-color: #909399 !default;
+
+/*button*/
+$vxe-button-default-background-color: #262626;
+$vxe-button-dropdown-panel-background-color: #141414;
+
+/*modal*/
+$vxe-modal-header-background-color: #141414;
+$vxe-modal-body-background-color: #141414;
+$vxe-modal-border-color: #3b3b3b;
+
+/*pulldown*/
+$vxe-pulldown-panel-background-color: #262626 !default;
+
+@import 'vxe-table/styles/index';

+ 6 - 0
yudao-ui-admin-vue3/src/components/XTable/src/style/index.scss

@@ -0,0 +1,6 @@
+@import 'vxe-table/styles/variable.scss';
+@import 'vxe-table/styles/modules.scss';
+// @import './theme/light.scss';
+i {
+  border-color: initial;
+}

+ 16 - 0
yudao-ui-admin-vue3/src/components/XTable/src/style/light.scss

@@ -0,0 +1,16 @@
+// 修改样式变量
+// /*font*/
+// $vxe-font-size: 12px !default;
+// $vxe-font-size-medium: 16px !default;
+// $vxe-font-size-small: 14px !default;
+// $vxe-font-size-mini: 12px !default;
+/*color*/
+$vxe-primary-color: #409eff !default;
+$vxe-success-color: #67c23a !default;
+$vxe-info-color: #909399 !default;
+$vxe-warning-color: #e6a23c !default;
+$vxe-danger-color: #f56c6c !default;
+$vxe-disabled-color: #bfbfbf !default;
+$vxe-primary-disabled-color: #c0c4cc !default;
+
+@import 'vxe-table/styles/index';

+ 25 - 0
yudao-ui-admin-vue3/src/components/XTable/src/type.ts

@@ -0,0 +1,25 @@
+import { CrudSchema } from '@/hooks/web/useCrudSchemas'
+import type { VxeGridProps, VxeGridPropTypes, VxeTablePropTypes } from 'vxe-table'
+
+export type XTableProps<D = any> = VxeGridProps<D> & {
+  allSchemas?: CrudSchema
+  height?: number // 高度 默认730
+  topActionSlots?: boolean // 是否开启表格内顶部操作栏插槽
+  treeConfig?: VxeTablePropTypes.TreeConfig // 树形表单配置
+  isList?: boolean // 是否不带分页的list
+  getListApi?: Function // 获取列表接口
+  getAllListApi?: Function // 获取全部数据接口 用于 vxe 导出
+  deleteApi?: Function // 删除接口
+  exportListApi?: Function // 导出接口
+  exportName?: string // 导出文件夹名称
+  params?: any // 其他查询参数
+  pagination?: boolean | VxeGridPropTypes.PagerConfig // 分页配置参数
+  toolBar?: boolean | VxeGridPropTypes.ToolbarConfig // 右侧工具栏配置参数
+}
+export type XColumns = VxeGridPropTypes.Columns
+
+export type VxeTableColumn = {
+  field: string
+  title?: string
+  children?: VxeTableColumn[]
+} & Recordable

+ 2 - 0
yudao-ui-admin-vue3/src/components/index.ts

@@ -4,6 +4,7 @@ import { Form } from '@/components/Form'
 import { Table } from '@/components/Table'
 import { Search } from '@/components/Search'
 import { XModal } from '@/components/XModal'
+import { XTable } from '@/components/XTable'
 import { XButton, XTextButton } from '@/components/XButton'
 import { DictTag } from '@/components/DictTag'
 import { ContentWrap } from '@/components/ContentWrap'
@@ -15,6 +16,7 @@ export const setupGlobCom = (app: App<Element>): void => {
   app.component('Table', Table)
   app.component('Search', Search)
   app.component('XModal', XModal)
+  app.component('XTable', XTable)
   app.component('XButton', XButton)
   app.component('XTextButton', XTextButton)
   app.component('DictTag', DictTag)

+ 1 - 1
yudao-ui-admin-vue3/src/hooks/web/useVxeCrudSchemas.ts

@@ -165,7 +165,7 @@ const filterSearchSchema = (crudSchema: VxeCrudSchema): VxeFormItemProps[] => {
     // 添加搜索按钮
     const buttons: VxeFormItemProps = {
       span: 24,
-      align: 'center',
+      align: 'right',
       collapseNode: searchSchema.length > spanLength,
       itemRender: {
         name: '$buttons',

+ 32 - 0
yudao-ui-admin-vue3/src/hooks/web/useXTable.ts

@@ -0,0 +1,32 @@
+import { ref, unref } from 'vue'
+import { XTableProps } from '@/components/XTable/src/type'
+
+export interface tableMethod {
+  reload: () => void
+  setProps: (props: XTableProps) => void
+  deleteData: (ids: string | number) => void
+  exportList: (fileName?: string) => void
+}
+
+export const useXTable = (props: XTableProps): [Function, tableMethod] => {
+  const tableRef = ref<Nullable<tableMethod>>(null)
+
+  const register = (instance) => {
+    tableRef.value = instance
+    props && instance.setProps(props)
+  }
+  const getInstance = (): tableMethod => {
+    const table = unref(tableRef)
+    if (!table) {
+      console.error('表格实例不存在')
+    }
+    return table as tableMethod
+  }
+  const methods: tableMethod = {
+    reload: () => getInstance().reload(),
+    setProps: (props) => getInstance().setProps(props),
+    deleteData: (ids: string | number) => getInstance().deleteData(ids),
+    exportList: (fileName?: string) => getInstance().exportList(fileName)
+  }
+  return [register, methods]
+}

+ 4 - 2
yudao-ui-admin-vue3/src/main.ts

@@ -26,10 +26,10 @@ import '@/styles/index.scss'
 import '@/plugins/animate.css'
 
 // 路由
-import { setupRouter } from './router'
+import router, { setupRouter } from '@/router'
 
 // 权限
-import { setupAuth } from './directives'
+import { setupAuth } from '@/directives'
 
 import { createApp } from 'vue'
 
@@ -53,6 +53,8 @@ const setupAll = async () => {
 
   setupAuth(app)
 
+  await router.isReady()
+
   app.mount('#app')
 }
 

+ 1 - 18
yudao-ui-admin-vue3/src/plugins/vxeTable/index.ts

@@ -1,9 +1,7 @@
-import { App, unref, watch } from 'vue'
+import { App, unref } from 'vue'
 import XEUtils from 'xe-utils'
-import './index.scss'
 import './renderer'
 import { i18n } from '@/plugins/vueI18n'
-import { useAppStore } from '@/store/modules/app'
 import zhCN from 'vxe-table/lib/locale/lang/zh-CN'
 import enUS from 'vxe-table/lib/locale/lang/en-US'
 import {
@@ -46,21 +44,6 @@ import {
   Table
 } from 'vxe-table'
 
-const appStore = useAppStore()
-watch(
-  () => appStore.getIsDark,
-  () => {
-    if (appStore.getIsDark) {
-      import('./theme/dark.scss')
-    } else {
-      import('./theme/light.scss')
-    }
-  },
-  {
-    deep: true,
-    immediate: true
-  }
-)
 // 全局默认参数
 VXETable.setup({
   size: 'medium', // 全局尺寸

+ 1 - 0
yudao-ui-admin-vue3/src/plugins/vxeTable/renderer/index.tsx

@@ -4,3 +4,4 @@ import './dict'
 import './html'
 import './link'
 import './img'
+import './preview'

+ 34 - 0
yudao-ui-admin-vue3/src/plugins/vxeTable/renderer/preview.tsx

@@ -0,0 +1,34 @@
+import { VXETable } from 'vxe-table'
+import { ElImage, ElLink } from 'element-plus'
+
+// 图片渲染
+VXETable.renderer.add('XPreview', {
+  // 默认显示模板
+  renderDefault(_renderOpts, params) {
+    const { row, column } = params
+    if (row.type.indexOf('image/') === 0) {
+      return (
+        <ElImage
+          style="width: 80px; height: 50px"
+          src={row[column.field]}
+          key={row[column.field]}
+          preview-src-list={[row[column.field]]}
+          fit="contain"
+          lazy
+        ></ElImage>
+      )
+    } else if (row.type.indexOf('video/') === 0) {
+      return (
+        <video>
+          <source src={row[column.field]}></source>
+        </video>
+      )
+    } else {
+      return (
+        <ElLink href={row[column.field]} target="_blank">
+          {row[column.field]}
+        </ElLink>
+      )
+    }
+  }
+})

+ 2 - 8
yudao-ui-admin-vue3/src/router/index.ts

@@ -6,15 +6,11 @@ import { isRelogin } from '@/config/axios/service'
 import { getAccessToken } from '@/utils/auth'
 import { useTitle } from '@/hooks/web/useTitle'
 import { useNProgress } from '@/hooks/web/useNProgress'
-import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
 import { usePageLoading } from '@/hooks/web/usePageLoading'
 import { useDictStoreWithOut } from '@/store/modules/dict'
 import { useUserStoreWithOut } from '@/store/modules/user'
 import { usePermissionStoreWithOut } from '@/store/modules/permission'
 import { getInfoApi } from '@/api/login'
-import { listSimpleDictDataApi } from '@/api/system/dict/dict.data'
-
-const { wsCache } = useCache()
 
 const { start, done } = useNProgress()
 
@@ -50,10 +46,8 @@ router.beforeEach(async (to, from, next) => {
       const dictStore = useDictStoreWithOut()
       const userStore = useUserStoreWithOut()
       const permissionStore = usePermissionStoreWithOut()
-      const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
-      if (!dictMap) {
-        const res = await listSimpleDictDataApi()
-        dictStore.setDictMap(res)
+      if (!dictStore.getIsSetDict) {
+        dictStore.setDictMap()
       }
       if (!userStore.getIsSetUser) {
         isRelogin.show = true

+ 36 - 26
yudao-ui-admin-vue3/src/store/modules/dict.ts

@@ -3,6 +3,7 @@ import { store } from '../index'
 import { DictDataVO } from '@/api/system/dict/types'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
 const { wsCache } = useCache('sessionStorage')
+import { listSimpleDictDataApi } from '@/api/system/dict/dict.data'
 
 export interface DictValueType {
   value: any
@@ -16,45 +17,54 @@ export interface DictTypeType {
 }
 export interface DictState {
   dictMap: Map<string, any>
+  isSetDict: boolean
 }
 
 export const useDictStore = defineStore('dict', {
   state: (): DictState => ({
-    dictMap: new Map<string, any>()
+    dictMap: new Map<string, any>(),
+    isSetDict: false
   }),
   getters: {
     getDictMap(): Recordable {
       const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
-      return dictMap ? dictMap : this.dictMap
-    },
-    getHasDictData(): boolean {
-      if (this.dictMap.size > 0) {
-        return true
-      } else {
-        return false
+      if (dictMap) {
+        this.dictMap = dictMap
       }
+      return this.dictMap
+    },
+    getIsSetDict(): boolean {
+      return this.isSetDict
     }
   },
   actions: {
-    setDictMap(dictMap: Recordable) {
-      // 设置数据
-      const dictDataMap = new Map<string, any>()
-      dictMap.forEach((dictData: DictDataVO) => {
-        // 获得 dictType 层级
-        const enumValueObj = dictDataMap[dictData.dictType]
-        if (!enumValueObj) {
-          dictDataMap[dictData.dictType] = []
-        }
-        // 处理 dictValue 层级
-        dictDataMap[dictData.dictType].push({
-          value: dictData.value,
-          label: dictData.label,
-          colorType: dictData.colorType,
-          cssClass: dictData.cssClass
+    async setDictMap() {
+      const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
+      if (dictMap) {
+        this.dictMap = dictMap
+        this.isSetDict = true
+      } else {
+        const res = await listSimpleDictDataApi()
+        // 设置数据
+        const dictDataMap = new Map<string, any>()
+        res.forEach((dictData: DictDataVO) => {
+          // 获得 dictType 层级
+          const enumValueObj = dictDataMap[dictData.dictType]
+          if (!enumValueObj) {
+            dictDataMap[dictData.dictType] = []
+          }
+          // 处理 dictValue 层级
+          dictDataMap[dictData.dictType].push({
+            value: dictData.value,
+            label: dictData.label,
+            colorType: dictData.colorType,
+            cssClass: dictData.cssClass
+          })
         })
-      })
-      this.dictMap = dictDataMap
-      wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期
+        this.dictMap = dictDataMap
+        this.isSetDict = true
+        wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期
+      }
     }
   }
 })

+ 17 - 32
yudao-ui-admin-vue3/src/utils/auth.ts

@@ -36,45 +36,30 @@ export const formatToken = (token: string): string => {
 }
 // ========== 账号相关 ==========
 
-const UsernameKey = 'USERNAME'
-const PasswordKey = 'PASSWORD'
-const RememberMeKey = 'REMEMBER_ME'
+const LoginFormKey = 'LOGINFORM'
 
-export const getUsername = () => {
-  return wsCache.get(UsernameKey)
+export type LoginFormType = {
+  tenantName: string
+  username: string
+  password: string
+  rememberMe: boolean
 }
 
-export const setUsername = (username: string) => {
-  wsCache.set(UsernameKey, username, { exp: 30 * 24 * 60 * 60 })
+export const getLoginForm = () => {
+  const loginForm: LoginFormType = wsCache.get(LoginFormKey)
+  if (loginForm) {
+    loginForm.password = decrypt(loginForm.password) as string
+  }
+  return loginForm
 }
 
-export const removeUsername = () => {
-  wsCache.delete(UsernameKey)
+export const setLoginForm = (loginForm: LoginFormType) => {
+  loginForm.password = encrypt(loginForm.password) as string
+  wsCache.set(LoginFormKey, loginForm, { exp: 30 * 24 * 60 * 60 })
 }
 
-export const getPassword = () => {
-  const password = wsCache.get(PasswordKey)
-  return password ? decrypt(password) : undefined
-}
-
-export const setPassword = (password: string) => {
-  wsCache.set(PasswordKey, encrypt(password), { exp: 30 * 24 * 60 * 60 })
-}
-
-export const removePassword = () => {
-  wsCache.delete(PasswordKey)
-}
-
-export const getRememberMe = () => {
-  return wsCache.get(RememberMeKey) === true
-}
-
-export const setRememberMe = (rememberMe: boolean) => {
-  wsCache.set(RememberMeKey, rememberMe, { exp: 30 * 24 * 60 * 60 })
-}
-
-export const removeRememberMe = () => {
-  wsCache.delete(RememberMeKey)
+export const removeLoginForm = () => {
+  wsCache.delete(LoginFormKey)
 }
 
 // ========== 租户相关 ==========

+ 15 - 41
yudao-ui-admin-vue3/src/views/Login/components/LoginForm.vue

@@ -148,7 +148,6 @@ import { useIcon } from '@/hooks/web/useIcon'
 import { useMessage } from '@/hooks/web/useMessage'
 import { required } from '@/utils/formRules'
 import * as authUtil from '@/utils/auth'
-import { decrypt } from '@/utils/jsencrypt'
 import { Verify } from '@/components/Verifition'
 import { usePermissionStore } from '@/store/modules/permission'
 import * as LoginApi from '@/api/login'
@@ -180,10 +179,6 @@ const loginData = reactive({
   isShowPassword: false,
   captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
   tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
-  token: '',
-  loading: {
-    signIn: false
-  },
   loginForm: {
     tenantName: '芋道源码',
     username: 'admin',
@@ -194,22 +189,10 @@ const loginData = reactive({
 })
 
 const socialList = [
-  {
-    icon: 'ant-design:github-filled',
-    type: 0
-  },
-  {
-    icon: 'ant-design:wechat-filled',
-    type: 30
-  },
-  {
-    icon: 'ant-design:alipay-circle-filled',
-    type: 0
-  },
-  {
-    icon: 'ant-design:dingtalk-circle-filled',
-    type: 20
-  }
+  { icon: 'ant-design:github-filled', type: 0 },
+  { icon: 'ant-design:wechat-filled', type: 30 },
+  { icon: 'ant-design:alipay-circle-filled', type: 0 },
+  { icon: 'ant-design:dingtalk-circle-filled', type: 20 }
 ]
 
 // 获取验证码
@@ -232,18 +215,15 @@ const getTenantId = async () => {
 }
 // 记住我
 const getCookie = () => {
-  const username = authUtil.getUsername()
-  const password = authUtil.getPassword()
-    ? decrypt(authUtil.getPassword() as unknown as string)
-    : undefined
-  const rememberMe = authUtil.getRememberMe()
-  const tenantName = authUtil.getTenantName()
-  loginData.loginForm = {
-    ...loginData.loginForm,
-    username: username ? username : loginData.loginForm.username,
-    password: password ? password : loginData.loginForm.password,
-    rememberMe: rememberMe ? true : false,
-    tenantName: tenantName ? tenantName : loginData.loginForm.tenantName
+  const loginForm = authUtil.getLoginForm()
+  if (loginForm) {
+    loginData.loginForm = {
+      ...loginData.loginForm,
+      username: loginForm.username ? loginForm.username : loginData.loginForm.username,
+      password: loginForm.password ? loginForm.password : loginData.loginForm.password,
+      rememberMe: loginForm.rememberMe ? true : false,
+      tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
+    }
   }
 }
 // 登录
@@ -266,15 +246,9 @@ const handleLogin = async (params) => {
       background: 'rgba(0, 0, 0, 0.7)'
     })
     if (loginData.loginForm.rememberMe) {
-      authUtil.setUsername(loginData.loginForm.username)
-      authUtil.setPassword(loginData.loginForm.password)
-      authUtil.setRememberMe(loginData.loginForm.rememberMe)
-      authUtil.setTenantName(loginData.loginForm.tenantName)
+      authUtil.setLoginForm(loginData.loginForm)
     } else {
-      authUtil.removeUsername()
-      authUtil.removePassword()
-      authUtil.removeRememberMe()
-      authUtil.removeTenantName()
+      authUtil.removeLoginForm()
     }
     authUtil.setToken(res)
     if (!redirect.value) {

+ 5 - 6
yudao-ui-admin-vue3/src/views/infra/apiAccessLog/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #duration_default="{ row }">
         <span>{{ row.duration + 'ms' }}</span>
       </template>
@@ -17,7 +17,7 @@
           @click="handleDetail(row)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <XModal v-model="dialogVisible" :title="dialogTitle">
     <!-- 对话框(详情) -->
@@ -38,15 +38,14 @@
 <script setup lang="ts" name="ApiAccessLog">
 import { ref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { allSchemas } from './apiAccessLog.data'
 import * as ApiAccessLogApi from '@/api/infra/apiAccessLog'
+
 const { t } = useI18n() // 国际化
 
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions } = useVxeGrid<ApiAccessLogApi.ApiAccessLogVO>({
+const [registerTable] = useXTable({
   allSchemas: allSchemas,
   topActionSlots: false,
   getListApi: ApiAccessLogApi.getApiAccessLogPageApi

+ 9 - 13
yudao-ui-admin-vue3/src/views/infra/apiErrorLog/index.vue

@@ -1,14 +1,14 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <!-- 操作:导出 -->
       <template #toolbar_buttons>
         <XButton
           type="warning"
           preIcon="ep:download"
           :title="t('action.export')"
-          @click="handleExport()"
+          @click="exportList('错误数据.xls')"
         />
       </template>
       <template #duration_default="{ row }">
@@ -40,7 +40,7 @@
           @click="handleProcessClick(row, InfraApiErrorLogProcessStatusEnum.IGNORE, '已忽略')"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <XModal v-model="dialogVisible" :title="dialogTitle">
     <!-- 对话框(详情) -->
@@ -54,18 +54,17 @@
 <script setup lang="ts" name="ApiErrorLog">
 import { ref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { allSchemas } from './apiErrorLog.data'
 import * as ApiErrorLogApi from '@/api/infra/apiErrorLog'
 import { InfraApiErrorLogProcessStatusEnum } from '@/utils/constants'
 import { useMessage } from '@/hooks/web/useMessage'
-const message = useMessage()
+
 const { t } = useI18n() // 国际化
+const message = useMessage()
 
 // ========== 列表相关 ==========
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, exportList } = useVxeGrid<ApiErrorLogApi.ApiErrorLogVO>({
+const [registerTable, { reload, exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: ApiErrorLogApi.getApiErrorLogPageApi,
   exportListApi: ApiErrorLogApi.exportApiErrorLogApi
@@ -82,10 +81,7 @@ const handleDetail = (row: ApiErrorLogApi.ApiErrorLogVO) => {
   dialogTitle.value = t('action.detail')
   dialogVisible.value = true
 }
-// 导出
-const handleExport = async () => {
-  await exportList(xGrid, '错误数据.xls')
-}
+
 // 异常处理操作
 const handleProcessClick = (
   row: ApiErrorLogApi.ApiErrorLogVO,
@@ -100,7 +96,7 @@ const handleProcessClick = (
     })
     .finally(async () => {
       // 刷新列表
-      await getList(xGrid)
+      await reload()
     })
     .catch(() => {})
 }

+ 7 - 16
yudao-ui-admin-vue3/src/views/infra/codegen/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:导入 -->
         <XButton
@@ -32,7 +32,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['infra:codegen:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
         <!-- 操作:同步 -->
         <XTextButton
@@ -49,20 +49,19 @@
           @click="handleGenTable(row)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <!-- 弹窗:导入表 -->
-  <ImportTable ref="importRef" @ok="handleQuery()" />
+  <ImportTable ref="importRef" @ok="reload()" />
   <!-- 弹窗:预览代码 -->
   <Preview ref="previewRef" />
 </template>
 <script setup lang="ts" name="Codegen">
 import { ref } from 'vue'
 import { useRouter } from 'vue-router'
-import { VxeGridInstance } from 'vxe-table'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
+import { useXTable } from '@/hooks/web/useXTable'
 import download from '@/utils/download'
 import * as CodegenApi from '@/api/infra/codegen'
 import { CodegenTableVO } from '@/api/infra/codegen/types'
@@ -73,8 +72,7 @@ const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 const { push } = useRouter() // 路由跳转
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData } = useVxeGrid<CodegenTableVO>({
+const [registerTable, { reload, deleteData }] = useXTable({
   allSchemas: allSchemas,
   getListApi: CodegenApi.getCodegenTablePageApi,
   deleteApi: CodegenApi.deleteCodegenTableApi
@@ -105,17 +103,10 @@ const handleSynchDb = (row: CodegenTableVO) => {
       message.success('同步成功')
     })
 }
+
 // 生成代码操作
 const handleGenTable = async (row: CodegenTableVO) => {
   const res = await CodegenApi.downloadCodegenApi(row.id)
   download.zip(res, 'codegen-' + row.className + '.zip')
 }
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-// 查询操作
-const handleQuery = async () => {
-  await getList(xGrid)
-}
 </script>

+ 7 - 19
yudao-ui-admin-vue3/src/views/infra/config/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -17,7 +17,7 @@
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['infra:config:export']"
-          @click="handleExport()"
+          @click="exportList('配置.xls')"
         />
       </template>
       <template #visible_default="{ row }">
@@ -43,10 +43,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['infra:config:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
 
   <XModal v-model="dialogVisible" :title="dialogTitle">
@@ -87,8 +87,7 @@
 import { ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 // 业务相关的 import
 import * as ConfigApi from '@/api/infra/config'
@@ -97,8 +96,7 @@ import { rules, allSchemas } from './config.data'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData, exportList } = useVxeGrid<ConfigApi.ConfigVO>({
+const [registerTable, { reload, deleteData, exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: ConfigApi.getConfigPageApi,
   deleteApi: ConfigApi.deleteConfigApi,
@@ -125,11 +123,6 @@ const handleCreate = () => {
   setDialogTile('create')
 }
 
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '配置.xls')
-}
-
 // 修改操作
 const handleUpdate = async (rowId: number) => {
   setDialogTile('update')
@@ -145,11 +138,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -171,7 +159,7 @@ const submitForm = async () => {
       } finally {
         actionLoading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

+ 6 - 13
yudao-ui-admin-vue3/src/views/infra/dataSourceConfig/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <XButton
           type="primary"
@@ -31,10 +31,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['infra:data-source-config:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <XModal v-model="dialogVisible" :title="dialogTitle">
     <!-- 对话框(添加 / 修改) -->
@@ -69,9 +69,8 @@
 // 全局相关的 import
 import { ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
+import { useXTable } from '@/hooks/web/useXTable'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
 import { FormExpose } from '@/components/Form'
 // 业务相关的 import
 import * as DataSourceConfiggApi from '@/api/infra/dataSourceConfig'
@@ -80,8 +79,7 @@ import { rules, allSchemas } from './dataSourceConfig.data'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData } = useVxeGrid<DataSourceConfiggApi.DataSourceConfigVO>({
+const [registerTable, { reload, deleteData }] = useXTable({
   allSchemas: allSchemas,
   isList: true,
   getListApi: DataSourceConfiggApi.getDataSourceConfigListApi,
@@ -123,11 +121,6 @@ const handleDetail = async (rowId: number) => {
   setDialogTile('detail')
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -149,7 +142,7 @@ const submitForm = async () => {
       } finally {
         loading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

+ 7 - 14
yudao-ui-admin-vue3/src/views/infra/fileConfig/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -41,10 +41,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['infra:file-config:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <XModal v-model="dialogVisible" :title="dialogTitle">
     <!-- 对话框(添加 / 修改) -->
@@ -173,8 +173,7 @@ import {
 } from 'element-plus'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 // 业务相关的 import
 import * as FileConfigApi from '@/api/infra/fileConfig'
 import { rules, allSchemas } from './fileConfig.data'
@@ -183,8 +182,7 @@ import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData } = useVxeGrid<FileConfigApi.FileConfigVO>({
+const [registerTable, { reload, deleteData }] = useXTable({
   allSchemas: allSchemas,
   getListApi: FileConfigApi.getFileConfigPageApi,
   deleteApi: FileConfigApi.deleteFileConfigApi
@@ -276,7 +274,7 @@ const handleMaster = (row: FileConfigApi.FileConfigVO) => {
     .confirm('是否确认修改配置【 ' + row.name + ' 】为主配置?', t('common.reminder'))
     .then(async () => {
       await FileConfigApi.updateFileConfigMasterApi(row.id)
-      await getList(xGrid)
+      await reload()
     })
 }
 
@@ -285,11 +283,6 @@ const handleTest = async (rowId: number) => {
   message.alert('测试通过,上传文件成功!访问地址:' + res)
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交按钮
 const submitForm = async (formEl: FormInstance | undefined) => {
   if (!formEl) return
@@ -308,7 +301,7 @@ const submitForm = async (formEl: FormInstance | undefined) => {
         dialogVisible.value = false
       } finally {
         actionLoading.value = false
-        await getList(xGrid)
+        await reload()
       }
     }
   })

+ 1 - 1
yudao-ui-admin-vue3/src/views/infra/fileList/fileList.data.ts

@@ -23,7 +23,7 @@ const crudSchemas = reactive<VxeCrudSchema>({
       field: 'url',
       table: {
         cellRender: {
-          name: 'XImg'
+          name: 'XPreview'
         }
       }
     },

+ 6 - 13
yudao-ui-admin-vue3/src/views/infra/fileList/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <XButton
           type="primary"
@@ -21,10 +21,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['infra:file:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <XModal v-model="dialogVisible" :title="dialogTitle">
     <!-- 对话框(详情) -->
@@ -85,8 +85,7 @@
 import { ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { ElUpload, ElImage, UploadInstance, UploadRawFile } from 'element-plus'
 // 业务相关的 import
 import { allSchemas } from './fileList.data'
@@ -98,8 +97,7 @@ const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData } = useVxeGrid<FileApi.FileVO>({
+const [registerTable, { reload, deleteData }] = useXTable({
   allSchemas: allSchemas,
   getListApi: FileApi.getFilePageApi,
   deleteApi: FileApi.deleteFileApi
@@ -145,7 +143,7 @@ const handleFileSuccess = async (response: any): Promise<void> => {
   message.success('上传成功')
   uploadDialogVisible.value = false
   uploadDisabled.value = false
-  await getList(xGrid)
+  await reload()
 }
 // 文件数超出提示
 const handleExceed = (): void => {
@@ -164,11 +162,6 @@ const handleDetail = (row: FileApi.FileVO) => {
   dialogVisible.value = true
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // ========== 复制相关 ==========
 const handleCopy = async (text: string) => {
   const { copy, copied, isSupported } = useClipboard({ source: text })

+ 5 - 11
yudao-ui-admin-vue3/src/views/infra/job/JobLog.vue

@@ -1,14 +1,14 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <XButton
           type="warning"
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['infra:job:export']"
-          @click="handleExport()"
+          @click="exportList('定时任务详情.xls')"
         />
       </template>
       <template #beginTime_default="{ row }">
@@ -29,7 +29,7 @@
           @click="handleDetail(row)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <XModal v-model="dialogVisible" :title="dialogTitle">
     <!-- 对话框(详情) -->
@@ -51,15 +51,13 @@
 import { ref } from 'vue'
 import dayjs from 'dayjs'
 import { useI18n } from '@/hooks/web/useI18n'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import * as JobLogApi from '@/api/infra/jobLog'
 import { allSchemas } from './jobLog.data'
 
 const { t } = useI18n() // 国际化
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, exportList } = useVxeGrid<JobLogApi.JobLogVO>({
+const [registerTable, { exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: JobLogApi.getJobLogPageApi,
   exportListApi: JobLogApi.exportJobLogApi
@@ -79,8 +77,4 @@ const handleDetail = async (row: JobLogApi.JobLogVO) => {
   dialogTitle.value = t('action.detail')
   dialogVisible.value = true
 }
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '定时任务详情.xls')
-}
 </script>

+ 11 - 22
yudao-ui-admin-vue3/src/views/infra/job/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -17,14 +17,14 @@
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['infra:job:export']"
-          @click="handleExport()"
+          @click="exportList('定时任务.xls')"
         />
         <XButton
           type="info"
           preIcon="ep:zoom-in"
           title="执行日志"
           v-hasPermi="['infra:job:query']"
-          @click="handleJobLog"
+          @click="handleJobLog()"
         />
       </template>
       <template #actionbtns_default="{ row }">
@@ -46,7 +46,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['infra:job:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
         <el-dropdown class="p-0.5" v-hasPermi="['infra:job:trigger', 'infra:job:query']">
           <XTextButton :title="t('action.more')" postIcon="ep:arrow-down" />
@@ -83,7 +83,7 @@
           </template>
         </el-dropdown>
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <XModal v-model="dialogVisible" :title="dialogTitle">
     <!-- 对话框(添加 / 修改) -->
@@ -134,8 +134,7 @@ import { useRouter } from 'vue-router'
 import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 import { Crontab } from '@/components/Crontab'
 import * as JobApi from '@/api/infra/job'
@@ -147,8 +146,7 @@ const message = useMessage() // 消息弹窗
 const { push } = useRouter()
 
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData, exportList } = useVxeGrid<JobApi.JobVO>({
+const [registerTable, { reload, deleteData, exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: JobApi.getJobPageApi,
   deleteApi: JobApi.deleteJobApi,
@@ -181,11 +179,6 @@ const handleCreate = () => {
   setDialogTile('create')
 }
 
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '定时任务.xls')
-}
-
 // 修改操作
 const handleUpdate = async (rowId: number) => {
   setDialogTile('update')
@@ -250,10 +243,6 @@ const parseTime = (time) => {
   return time_str
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
 const handleChangeStatus = async (row: JobApi.JobVO) => {
   const text = row.status === InfraJobStatusEnum.STOP ? '开启' : '关闭'
   const status =
@@ -267,7 +256,7 @@ const handleChangeStatus = async (row: JobApi.JobVO) => {
           : InfraJobStatusEnum.STOP
       await JobApi.updateJobStatusApi(row.id, status)
       message.success(text + '成功')
-      await getList(xGrid)
+      await reload()
     })
     .catch(() => {
       row.status =
@@ -277,7 +266,7 @@ const handleChangeStatus = async (row: JobApi.JobVO) => {
     })
 }
 // 执行日志
-const handleJobLog = (rowId: number) => {
+const handleJobLog = (rowId?: number) => {
   if (rowId) {
     push('/job/job-log?id=' + rowId)
   } else {
@@ -289,7 +278,7 @@ const handleRun = (row: JobApi.JobVO) => {
   message.confirm('确认要立即执行一次' + row.name + '?', t('common.reminder')).then(async () => {
     await JobApi.runJobApi(row.id)
     message.success('执行成功')
-    await getList(xGrid)
+    await reload()
   })
 }
 // 提交按钮
@@ -312,7 +301,7 @@ const submitForm = async () => {
         dialogVisible.value = false
       } finally {
         actionLoading.value = false
-        await getList(xGrid)
+        await reload()
       }
     }
   })

+ 118 - 0
yudao-ui-admin-vue3/src/views/infra/webSocket/index.vue

@@ -0,0 +1,118 @@
+<template>
+  <div class="flex">
+    <el-card class="w-1/2" :gutter="12" shadow="always">
+      <template #header>
+        <div class="card-header">
+          <span>连接</span>
+        </div>
+      </template>
+      <div class="flex items-center">
+        <span class="text-lg font-medium mr-4"> 连接状态: </span>
+        <el-tag :color="getTagColor">{{ status }}</el-tag>
+      </div>
+      <hr class="my-4" />
+
+      <div class="flex">
+        <el-input v-model="server" disabled>
+          <template #prepend> 服务地址 </template>
+        </el-input>
+        <el-button :type="getIsOpen ? 'danger' : 'primary'" @click="toggle">
+          {{ getIsOpen ? '关闭连接' : '开启连接' }}
+        </el-button>
+      </div>
+      <p class="text-lg font-medium mt-4">设置</p>
+      <hr class="my-4" />
+      <el-input
+        v-model="sendValue"
+        :autosize="{ minRows: 2, maxRows: 4 }"
+        type="textarea"
+        :disabled="!getIsOpen"
+        clearable
+      />
+      <el-button type="primary" block class="mt-4" :disabled="!getIsOpen" @click="handlerSend">
+        发送
+      </el-button>
+    </el-card>
+    <el-card class="w-1/2" :gutter="12" shadow="always">
+      <template #header>
+        <div class="card-header">
+          <span>消息记录</span>
+        </div>
+      </template>
+      <div class="max-h-80 overflow-auto">
+        <ul>
+          <li v-for="item in getList" class="mt-2" :key="item.time">
+            <div class="flex items-center">
+              <span class="mr-2 text-primary font-medium">收到消息:</span>
+              <span>{{ dayjs(item.time).format('YYYY-MM-DD HH:mm:ss') }}</span>
+            </div>
+            <div>
+              {{ item.res }}
+            </div>
+          </li>
+        </ul>
+      </div>
+    </el-card>
+  </div>
+</template>
+<script setup lang="ts">
+import { computed, reactive, ref, watchEffect } from 'vue'
+import { ElCard, ElInput, ElTag } from 'element-plus'
+import { useWebSocket } from '@vueuse/core'
+import dayjs from 'dayjs'
+import { useUserStore } from '@/store/modules/user'
+
+const userStore = useUserStore()
+
+const sendValue = ref('')
+
+const server = ref(
+  (import.meta.env.VITE_BASE_URL + '/websocket/message').replace('http', 'ws') +
+    '?userId=' +
+    userStore.getUser.id
+)
+
+const state = reactive({
+  recordList: [] as { id: number; time: number; res: string }[]
+})
+
+const { status, data, send, close, open } = useWebSocket(server.value, {
+  autoReconnect: false,
+  heartbeat: true
+})
+
+watchEffect(() => {
+  if (data.value) {
+    try {
+      const res = JSON.parse(data.value)
+      state.recordList.push(res)
+    } catch (error) {
+      state.recordList.push({
+        res: data.value,
+        id: Math.ceil(Math.random() * 1000),
+        time: new Date().getTime()
+      })
+    }
+  }
+})
+
+const getIsOpen = computed(() => status.value === 'OPEN')
+const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red'))
+
+const getList = computed(() => {
+  return [...state.recordList].reverse()
+})
+
+function handlerSend() {
+  send(sendValue.value)
+  sendValue.value = ''
+}
+
+function toggle() {
+  if (getIsOpen.value) {
+    close()
+  } else {
+    open()
+  }
+}
+</script>

+ 7 - 19
yudao-ui-admin-vue3/src/views/pay/app/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -17,7 +17,7 @@
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['pay:app:export']"
-          @click="handleExport()"
+          @click="exportList('应用信息.xls')"
         />
       </template>
       <template #actionbtns_default="{ row }">
@@ -40,10 +40,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['pay:app:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
 
   <XModal v-model="dialogVisible" :title="dialogTitle">
@@ -79,8 +79,7 @@
 import { ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 import { rules, allSchemas } from './app.data'
 import * as AppApi from '@/api/pay/app'
@@ -89,8 +88,7 @@ const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData, exportList } = useVxeGrid<AppApi.AppVO>({
+const [registerTable, { reload, deleteData, exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: AppApi.getAppPageApi,
   deleteApi: AppApi.deleteAppApi,
@@ -117,11 +115,6 @@ const handleCreate = () => {
   setDialogTile('create')
 }
 
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '应用信息.xls')
-}
-
 // 修改操作
 const handleUpdate = async (rowId: number) => {
   setDialogTile('update')
@@ -137,11 +130,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -163,7 +151,7 @@ const submitForm = async () => {
       } finally {
         actionLoading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

+ 7 - 19
yudao-ui-admin-vue3/src/views/pay/merchant/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -17,7 +17,7 @@
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['pay:merchant:export']"
-          @click="handleExport()"
+          @click="exportList('商户列表.xls')"
         />
       </template>
       <template #actionbtns_default="{ row }">
@@ -40,10 +40,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['pay:merchant:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <XModal v-model="dialogVisible" :title="dialogTitle">
     <!-- 对话框(添加 / 修改) -->
@@ -78,8 +78,7 @@
 import { ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 import { rules, allSchemas } from './merchant.data'
 import * as MerchantApi from '@/api/pay/merchant'
@@ -87,8 +86,7 @@ import * as MerchantApi from '@/api/pay/merchant'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData, exportList } = useVxeGrid<MerchantApi.MerchantVO>({
+const [registerTable, { reload, deleteData, exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: MerchantApi.getMerchantPageApi,
   deleteApi: MerchantApi.deleteMerchantApi,
@@ -115,11 +113,6 @@ const handleCreate = () => {
   setDialogTile('create')
 }
 
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '商户列表.xls')
-}
-
 // 修改操作
 const handleUpdate = async (rowId: number) => {
   setDialogTile('update')
@@ -135,11 +128,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -161,7 +149,7 @@ const submitForm = async () => {
       } finally {
         actionLoading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

+ 5 - 11
yudao-ui-admin-vue3/src/views/pay/order/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -17,7 +17,7 @@
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['pay:order:export']"
-          @click="handleExport()"
+          @click="exportList('订单数据.xls')"
         />
       </template>
       <template #actionbtns_default="{ row }">
@@ -29,7 +29,7 @@
           @click="handleDetail(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <XModal v-model="dialogVisible" :title="dialogTitle">
     <!-- 对话框(详情) -->
@@ -44,15 +44,13 @@
 <script setup lang="ts" name="Order">
 import { ref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { allSchemas } from './order.data'
 import * as OrderApi from '@/api/pay/order'
 
 const { t } = useI18n() // 国际化
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, exportList } = useVxeGrid<OrderApi.OrderVO>({
+const [registerTable, { exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: OrderApi.getOrderPageApi,
   exportListApi: OrderApi.exportOrderApi
@@ -74,10 +72,6 @@ const setDialogTile = (type: string) => {
 const handleCreate = () => {
   setDialogTile('create')
 }
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '订单数据.xls')
-}
 
 // 详情操作
 const handleDetail = async (rowId: number) => {

+ 5 - 12
yudao-ui-admin-vue3/src/views/pay/refund/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:导出 -->
         <XButton
@@ -9,7 +9,7 @@
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['pay:refund:export']"
-          @click="handleExport()"
+          @click="exportList('退款订单.xls')"
         />
       </template>
       <template #actionbtns_default="{ row }">
@@ -21,7 +21,7 @@
           @click="handleDetail(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
 
   <XModal v-model="dialogVisible" :title="t('action.detail')">
@@ -36,26 +36,19 @@
 <script setup lang="ts" name="Refund">
 import { ref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { allSchemas } from './refund.data'
 import * as RefundApi from '@/api/pay/refund'
 
 const { t } = useI18n() // 国际化
 
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, exportList } = useVxeGrid<RefundApi.RefundVO>({
+const [registerTable, { exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: RefundApi.getRefundPageApi,
   exportListApi: RefundApi.exportRefundApi
 })
 
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '退款订单.xls')
-}
-
 // ========== CRUD 相关 ==========
 const dialogVisible = ref(false) // 是否显示弹出层
 const detailData = ref() // 详情 Ref

+ 9 - 15
yudao-ui-admin-vue3/src/views/system/dept/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" show-overflow class="xtable-scrollbar">
+    <XTable ref="xGrid" @register="registerTable" show-overflow>
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -11,8 +11,8 @@
           v-hasPermi="['system:dept:create']"
           @click="handleCreate()"
         />
-        <XButton title="展开所有" @click="xGrid?.setAllTreeExpand(true)" />
-        <XButton title="关闭所有" @click="xGrid?.clearTreeExpand()" />
+        <XButton title="展开所有" @click="xGrid?.Ref.setAllTreeExpand(true)" />
+        <XButton title="关闭所有" @click="xGrid?.Ref.clearTreeExpand()" />
       </template>
       <template #leaderUserId_default="{ row }">
         <span>{{ userNicknameFormat(row) }}</span>
@@ -30,10 +30,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:dept:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <!-- 添加或修改菜单对话框 -->
   <XModal id="deptModel" v-model="dialogVisible" :title="dialogTitle">
@@ -77,11 +77,10 @@
 <script setup lang="ts" name="Dept">
 import { nextTick, onMounted, ref, unref } from 'vue'
 import { ElSelect, ElTreeSelect, ElOption } from 'element-plus'
-import { VxeGridInstance } from 'vxe-table'
 import { handleTree, defaultProps } from '@/utils/tree'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 import { allSchemas, rules } from './dept.data'
 import * as DeptApi from '@/api/system/dept'
@@ -90,7 +89,7 @@ import { getListSimpleUsersApi, UserVO } from '@/api/system/user'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
+const xGrid = ref<any>() // 列表 Grid Ref
 const treeConfig = {
   transform: true,
   rowField: 'id',
@@ -119,7 +118,7 @@ const getTree = async () => {
   dept.children = handleTree(res)
   deptOptions.value.push(dept)
 }
-const { gridOptions, getList, deleteData } = useVxeGrid<DeptApi.DeptVO>({
+const [registerTable, { reload, deleteData }] = useXTable({
   allSchemas: allSchemas,
   treeConfig: treeConfig,
   getListApi: DeptApi.getDeptPageApi,
@@ -168,17 +167,12 @@ const submitForm = async () => {
         dialogVisible.value = false
       } finally {
         actionLoading.value = false
-        await getList(xGrid)
+        await reload()
       }
     }
   })
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 const userNicknameFormat = (row) => {
   if (!row || !row.leaderUserId) {
     return '未设置'

+ 14 - 38
yudao-ui-admin-vue3/src/views/system/dict/index.vue

@@ -7,12 +7,7 @@
           <span>字典分类</span>
         </div>
       </template>
-      <vxe-grid
-        ref="xTypeGrid"
-        v-bind="typeGridOptions"
-        @cell-click="cellClickEvent"
-        class="xtable-scrollbar"
-      >
+      <XTable @register="registerType" @cell-click="cellClickEvent">
         <!-- 操作:新增类型 -->
         <template #toolbar_buttons>
           <XButton
@@ -36,10 +31,10 @@
             preIcon="ep:delete"
             :title="t('action.del')"
             v-hasPermi="['system:dict:delete']"
-            @click="handleTypeDelete(row.id)"
+            @click="typeDeleteData(row.id)"
           />
         </template>
-      </vxe-grid>
+      </XTable>
       <!-- @星语:分页和列表重叠在一起了 -->
     </el-card>
     <!-- ====== 字典数据 ====== -->
@@ -55,7 +50,7 @@
       </div>
       <div v-if="tableTypeSelect">
         <!-- 列表 -->
-        <vxe-grid ref="xDataGrid" v-bind="dataGridOptions" class="xtable-scrollbar">
+        <XTable @register="registerData">
           <!-- 操作:新增数据 -->
           <template #toolbar_buttons>
             <XButton
@@ -79,10 +74,10 @@
               v-hasPermi="['system:dict:delete']"
               preIcon="ep:delete"
               :title="t('action.del')"
-              @click="handleDataDelete(row.id)"
+              @click="dataDeleteData(row.id)"
             />
           </template>
-        </vxe-grid>
+        </XTable>
       </div>
     </el-card>
     <XModal id="dictModel" v-model="dialogVisible" :title="dialogTitle">
@@ -130,8 +125,8 @@
 import { ref, unref, reactive } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance, VxeTableEvents } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
+import { VxeTableEvents } from 'vxe-table'
 import { FormExpose } from '@/components/Form'
 import { ElInput, ElTag, ElCard } from 'element-plus'
 import * as DictTypeSchemas from './dict.type'
@@ -143,28 +138,18 @@ import { DictDataVO, DictTypeVO } from '@/api/system/dict/types'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
-const xTypeGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const {
-  gridOptions: typeGridOptions,
-  getList: typeGetList,
-  deleteData: typeDeleteData
-} = useVxeGrid<DictTypeVO>({
+const [registerType, { reload: typeGetList, deleteData: typeDeleteData }] = useXTable({
   allSchemas: DictTypeSchemas.allSchemas,
   getListApi: DictTypeApi.getDictTypePageApi,
   deleteApi: DictTypeApi.deleteDictTypeApi
 })
 
-const xDataGrid = ref<VxeGridInstance>() // 列表 Grid Ref
 const queryParams = reactive({
   dictType: null
 })
-const {
-  gridOptions: dataGridOptions,
-  getList: dataGetList,
-  deleteData: dataDeleteData
-} = useVxeGrid<DictDataVO>({
+const [registerData, { reload: dataGetList, deleteData: dataDeleteData }] = useXTable({
   allSchemas: DictDataSchemas.allSchemas,
-  queryParams: queryParams,
+  params: queryParams,
   getListApi: DictDataApi.getDictDataPageApi,
   deleteApi: DictDataApi.deleteDictDataApi
 })
@@ -199,7 +184,7 @@ const tableTypeSelect = ref(false)
 const cellClickEvent: VxeTableEvents.CellClick = async ({ row }) => {
   tableTypeSelect.value = true
   queryParams.dictType = row['type']
-  await dataGetList(xDataGrid)
+  await dataGetList()
   parentType.value = row['type']
 }
 // 弹出框
@@ -217,15 +202,6 @@ const setDialogTile = (type: string) => {
   dialogVisible.value = true
 }
 
-// 删除操作
-const handleTypeDelete = async (rowId: number) => {
-  await typeDeleteData(xTypeGrid, rowId)
-}
-
-const handleDataDelete = async (rowId: number) => {
-  await dataDeleteData(xDataGrid, rowId)
-}
-
 // 提交按钮
 const submitTypeForm = async () => {
   const elForm = unref(typeFormRef)?.getElFormRef()
@@ -247,7 +223,7 @@ const submitTypeForm = async () => {
         dialogVisible.value = false
       } finally {
         actionLoading.value = false
-        typeGetList(xTypeGrid)
+        typeGetList()
       }
     }
   })
@@ -272,7 +248,7 @@ const submitDataForm = async () => {
         dialogVisible.value = false
       } finally {
         actionLoading.value = false
-        dataGetList(xDataGrid)
+        dataGetList()
       }
     }
   })

+ 6 - 13
yudao-ui-admin-vue3/src/views/system/errorCode/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <!-- 操作:新增 -->
       <template #toolbar_buttons>
         <XButton
@@ -32,10 +32,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:error-code:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <!-- 弹窗 -->
   <XModal id="errorCodeModel" v-model="dialogVisible" :title="dialogTitle">
@@ -71,8 +71,7 @@
 import { ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 // 业务相关的 import
 import { rules, allSchemas } from './errorCode.data'
@@ -81,8 +80,7 @@ import * as ErrorCodeApi from '@/api/system/errorCode'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // grid Ref
-const { gridOptions, getList, deleteData } = useVxeGrid<ErrorCodeApi.ErrorCodeVO>({
+const [registerTable, { reload, deleteData }] = useXTable({
   allSchemas: allSchemas,
   getListApi: ErrorCodeApi.getErrorCodePageApi,
   deleteApi: ErrorCodeApi.deleteErrorCodeApi
@@ -123,11 +121,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交新增/修改的表单
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -149,7 +142,7 @@ const submitForm = async () => {
       } finally {
         actionLoading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

部分文件因文件數量過多而無法顯示