Просмотр исходного кода

Merge remote-tracking branch 'origin/dev' into feature/springdoc

# Conflicts:
#	yudao-dependencies/pom.xml
#	yudao-framework/yudao-spring-boot-starter-web/pom.xml
#	yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/YudaoSwaggerAutoConfiguration.java
#	yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/core/SpringFoxHandlerProviderBeanPostProcessor.java
#	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java
xingyu 2 лет назад
Родитель
Сommit
1c0d8fc1eb
100 измененных файлов с 2135 добавлено и 1074 удалено
  1. 4 4
      README.md
  2. 10 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. 27 14
      yudao-dependencies/pom.xml
  7. 1 0
      yudao-framework/pom.xml
  8. 0 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java
  9. 1 1
      yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml
  10. 4 5
      yudao-framework/yudao-spring-boot-starter-captcha/pom.xml
  11. 0 3
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/core/enums/CaptchaRedisKeyConstants.java
  12. 0 1
      yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/core/service/RedisCaptchaServiceImpl.java
  13. 15 1
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
  14. 2 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
  15. 6 0
      yudao-framework/yudao-spring-boot-starter-web/pom.xml
  16. 37 3
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java
  17. 80 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/JsoupXssCleaner.java
  18. 15 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/XssCleaner.java
  19. 5 2
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssFilter.java
  20. 50 95
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssRequestWrapper.java
  21. 59 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/json/XssStringJsonDeserializer.java
  22. 37 0
      yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
  23. 14 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java
  24. 29 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java
  25. 34 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
  26. 24 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java
  27. 9 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java
  28. 24 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java
  29. 36 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java
  30. 31 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java
  31. 49 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java
  32. 1 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java
  33. 1 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  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 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/codegen/CodegenConvert.java
  37. 8 6
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenColumnDO.java
  38. 45 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/SemaphoreUtils.java
  39. 16 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketConfig.java
  40. 86 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketServer.java
  41. 178 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketUsers.java
  42. 2 12
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm
  43. 2 2
      yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/DefaultDatabaseQueryTest.java
  44. 27 13
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java
  45. 3 2
      yudao-module-visualization/yudao-module-visualization-biz/src/main/java/cn/iocoder/yudao/module/visualization/framework/jmreport/config/JmReportConfiguration.java
  46. 72 18
      yudao-module-visualization/yudao-module-visualization-biz/src/main/java/cn/iocoder/yudao/module/visualization/framework/jmreport/core/service/JmReportTokenServiceImpl.java
  47. 1 1
      yudao-server/pom.xml
  48. 5 0
      yudao-server/src/main/resources/application.yaml
  49. 1 1
      yudao-ui-admin-vue3/README.md
  50. 17 17
      yudao-ui-admin-vue3/package.json
  51. 489 163
      yudao-ui-admin-vue3/pnpm-lock.yaml
  52. 0 1
      yudao-ui-admin-vue3/src/components/Crontab/src/Crontab.vue
  53. 1 1
      yudao-ui-admin-vue3/src/components/XTable/src/style/dark.scss
  54. 2 2
      yudao-ui-admin-vue3/src/components/XTable/src/style/index.scss
  55. 7 8
      yudao-ui-admin-vue3/src/components/XTable/src/type.ts
  56. 1 1
      yudao-ui-admin-vue3/src/hooks/web/useVxeCrudSchemas.ts
  57. 6 2
      yudao-ui-admin-vue3/src/main.ts
  58. 70 0
      yudao-ui-admin-vue3/src/permission.ts
  59. 0 6
      yudao-ui-admin-vue3/src/plugins/vxeTable/index.scss
  60. 1 0
      yudao-ui-admin-vue3/src/plugins/vxeTable/renderer/index.tsx
  61. 34 0
      yudao-ui-admin-vue3/src/plugins/vxeTable/renderer/preview.tsx
  62. 0 81
      yudao-ui-admin-vue3/src/plugins/vxeTable/theme/dark.scss
  63. 0 16
      yudao-ui-admin-vue3/src/plugins/vxeTable/theme/light.scss
  64. 0 77
      yudao-ui-admin-vue3/src/router/index.ts
  65. 36 26
      yudao-ui-admin-vue3/src/store/modules/dict.ts
  66. 6 1
      yudao-ui-admin-vue3/src/store/modules/user.ts
  67. 2 0
      yudao-ui-admin-vue3/src/styles/variables.scss
  68. 17 32
      yudao-ui-admin-vue3/src/utils/auth.ts
  69. 8 9
      yudao-ui-admin-vue3/src/utils/propTypes.ts
  70. 15 41
      yudao-ui-admin-vue3/src/views/Login/components/LoginForm.vue
  71. 1 1
      yudao-ui-admin-vue3/src/views/Profile/Index.vue
  72. 2 5
      yudao-ui-admin-vue3/src/views/infra/apiErrorLog/index.vue
  73. 5 12
      yudao-ui-admin-vue3/src/views/infra/codegen/EditTable.vue
  74. 99 3
      yudao-ui-admin-vue3/src/views/infra/codegen/components/BasicInfoForm.vue
  75. 84 80
      yudao-ui-admin-vue3/src/views/infra/codegen/components/CloumInfoForm.vue
  76. 0 135
      yudao-ui-admin-vue3/src/views/infra/codegen/components/GenInfoForm.vue
  77. 1 1
      yudao-ui-admin-vue3/src/views/infra/codegen/components/Preview.vue
  78. 1 2
      yudao-ui-admin-vue3/src/views/infra/codegen/components/index.ts
  79. 3 10
      yudao-ui-admin-vue3/src/views/infra/codegen/index.vue
  80. 2 12
      yudao-ui-admin-vue3/src/views/infra/config/index.vue
  81. 1 6
      yudao-ui-admin-vue3/src/views/infra/dataSourceConfig/index.vue
  82. 1 6
      yudao-ui-admin-vue3/src/views/infra/fileConfig/index.vue
  83. 1 1
      yudao-ui-admin-vue3/src/views/infra/fileList/fileList.data.ts
  84. 1 6
      yudao-ui-admin-vue3/src/views/infra/fileList/index.vue
  85. 1 5
      yudao-ui-admin-vue3/src/views/infra/job/JobLog.vue
  86. 4 13
      yudao-ui-admin-vue3/src/views/infra/job/index.vue
  87. 118 0
      yudao-ui-admin-vue3/src/views/infra/webSocket/index.vue
  88. 2 12
      yudao-ui-admin-vue3/src/views/pay/app/index.vue
  89. 2 12
      yudao-ui-admin-vue3/src/views/pay/merchant/index.vue
  90. 1 5
      yudao-ui-admin-vue3/src/views/pay/order/index.vue
  91. 1 6
      yudao-ui-admin-vue3/src/views/pay/refund/index.vue
  92. 6 11
      yudao-ui-admin-vue3/src/views/system/dept/index.vue
  93. 3 12
      yudao-ui-admin-vue3/src/views/system/dict/index.vue
  94. 1 6
      yudao-ui-admin-vue3/src/views/system/errorCode/index.vue
  95. 1 6
      yudao-ui-admin-vue3/src/views/system/loginlog/index.vue
  96. 7 13
      yudao-ui-admin-vue3/src/views/system/menu/index.vue
  97. 1 6
      yudao-ui-admin-vue3/src/views/system/notice/index.vue
  98. 1 6
      yudao-ui-admin-vue3/src/views/system/oauth2/client/index.vue
  99. 1 1
      yudao-ui-admin-vue3/src/views/system/oauth2/token/index.vue
  100. 1 6
      yudao-ui-admin-vue3/src/views/system/operatelog/index.vue

+ 4 - 4
README.md

@@ -191,19 +191,19 @@ ps:核心功能已经实现,正在对接微信小程序中...
 | [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)         |
+| [MyBatis Plus](https://mp.baomidou.com/)                                                    | MyBatis 增强工具包    | 3.5.3.1     | [文档](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) |
+| [Spring Security](https://github.com/spring-projects/spring-security)                       | Spring 安全框架      | 5.7.6       | [文档](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 实现 | 4.0.0       | [文档](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)           |
+| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin)                       | Spring Boot 监控平台 | 2.7.10      | [文档](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)          |
@@ -227,7 +227,7 @@ ps:核心功能已经实现,正在对接微信小程序中...
 | [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  |
+| [vxe-table](https://vxetable.cn/)                                    |   vue最强表单    | 4.3.9  |
 
 ### [管理后台 uni-app 跨端](./yudao-ui-admin-uniapp)
 

+ 10 - 3
pom.xml

@@ -29,15 +29,16 @@
     <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>
         <maven.compiler.target>${java.version}</maven.compiler.target>
         <maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
-        <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
+        <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
         <!-- 看看咋放到 bom 里 -->
         <lombok.version>1.18.24</lombok.version>
+        <spring.boot.version>2.7.7</spring.boot.version>
         <mapstruct.version>1.5.3.Final</mapstruct.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
@@ -64,13 +65,19 @@
                     <artifactId>maven-surefire-plugin</artifactId>
                     <version>${maven-surefire-plugin.version}</version>
                 </plugin>
-                <!-- maven-compiler-plugin 插件,解决 Lombok + MapStruct 组合 -->
+                <!-- maven-compiler-plugin 插件,解决 spring-boot-configuration-processor + Lombok + MapStruct 组合 -->
+                <!-- https://stackoverflow.com/questions/33483697/re-run-spring-boot-configuration-annotation-processor-to-update-generated-metada -->
                 <plugin>
                     <groupId>org.apache.maven.plugins</groupId>
                     <artifactId>maven-compiler-plugin</artifactId>
                     <version>${maven-compiler-plugin.version}</version>
                     <configuration>
                         <annotationProcessorPaths>
+                            <path>
+                                <groupId>org.springframework.boot</groupId>
+                                <artifactId>spring-boot-configuration-processor</artifactId>
+                                <version>${spring.boot.version}</version>
+                            </path>
                             <path>
                                 <groupId>org.projectlombok</groupId>
                                 <artifactId>lombok</artifactId>

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

+ 27 - 14
yudao-dependencies/pom.xml

@@ -14,7 +14,7 @@
     <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.7</spring.boot.version>
         <!-- Web 相关 -->
@@ -23,8 +23,8 @@
         <servlet.versoin>2.5</servlet.versoin>
         <!-- DB 相关 -->
         <druid.version>1.2.15</druid.version>
-        <mybatis-plus.version>3.5.3</mybatis-plus.version>
-        <mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.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>
         <!-- 服务保障相关 -->
@@ -32,7 +32,7 @@
         <resilience4j.version>1.7.1</resilience4j.version>
         <!-- 监控相关 -->
         <skywalking.version>8.12.0</skywalking.version>
-        <spring-boot-admin.version>2.7.9</spring-boot-admin.version>
+        <spring-boot-admin.version>2.7.10</spring-boot-admin.version>
         <opentracing.version>0.33.0</opentracing.version>
         <!-- Test 测试相关 -->
         <podam.version>7.2.11.RELEASE</podam.version>
@@ -41,10 +41,12 @@
         <!-- Bpm 工作流相关 -->
         <flowable.version>6.8.0</flowable.version>
         <!-- 工具类相关 -->
+        <captcha-plus.version>1.0.0</captcha-plus.version>
+        <jsoup.version>1.15.3</jsoup.version>
         <lombok.version>1.18.24</lombok.version>
         <mapstruct.version>1.5.3.Final</mapstruct.version>
         <hutool.version>5.8.11</hutool.version>
-        <easyexcel.verion>3.1.4</easyexcel.verion>
+        <easyexcel.verion>3.1.5</easyexcel.verion>
         <velocity.version>2.3</velocity.version>
         <screw.version>1.0.5</screw.version>
         <fastjson.version>1.2.83</fastjson.version>
@@ -54,16 +56,15 @@
         <commons-net.version>3.8.0</commons-net.version>
         <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.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>
+        <minio.version>8.5.1</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.660</tencentcloud-sdk-java.version>
+        <tencentcloud-sdk-java.version>3.1.676</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>
@@ -439,12 +440,6 @@
                 <version>${tika-core.version}</version>
             </dependency>
 
-            <dependency>
-                <groupId>com.anji-plus</groupId>
-                <artifactId>spring-boot-starter-captcha</artifactId>
-                <version>${aj-captcha.version}</version>
-            </dependency>
-
             <dependency>
                 <groupId>org.apache.velocity</groupId>
                 <artifactId>velocity-engine-core</artifactId>
@@ -509,12 +504,24 @@
                 <version>${netty-all.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>com.xingyuv</groupId>
+                <artifactId>spring-boot-starter-captcha-plus</artifactId>
+                <version>${captcha-plus.version}</version>
+            </dependency>
+
             <dependency>
                 <groupId>org.lionsoul</groupId>
                 <artifactId>ip2region</artifactId>
                 <version>${ip2region.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>org.jsoup</groupId>
+                <artifactId>jsoup</artifactId>
+                <version>${jsoup.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>
 

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

+ 0 - 1
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java

@@ -1,7 +1,6 @@
 package cn.iocoder.yudao.framework.common.util.validation;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.StrUtil;
 import org.springframework.util.StringUtils;
 
 import javax.validation.ConstraintViolation;

+ 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.9.ALL</version>
+            <version>4.35.32.ALL</version>
             <exclusions>
                 <exclusion>
                     <groupId>org.bouncycastle</groupId>

+ 4 - 5
yudao-framework/yudao-spring-boot-starter-captcha/pom.xml

@@ -17,6 +17,10 @@
     </description>
 
     <dependencies>
+        <dependency>
+            <groupId>com.xingyuv</groupId>
+            <artifactId>spring-boot-starter-captcha-plus</artifactId>
+        </dependency>
         <!-- Spring 核心 -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
@@ -29,11 +33,6 @@
             <artifactId>yudao-spring-boot-starter-redis</artifactId>
         </dependency>
 
-        <!-- 验证码相关 -->
-        <dependency>
-            <groupId>com.anji-plus</groupId>
-            <artifactId>spring-boot-starter-captcha</artifactId>
-        </dependency>
     </dependencies>
 
 </project>

+ 0 - 3
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/core/enums/CaptchaRedisKeyConstants.java

@@ -2,12 +2,9 @@ package cn.iocoder.yudao.framework.captcha.core.enums;
 
 import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
 import com.anji.captcha.model.vo.PointVO;
-import org.redisson.api.RLock;
 
 import java.time.Duration;
-import java.util.Map;
 
-import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH;
 import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
 
 /**

+ 0 - 1
yudao-framework/yudao-spring-boot-starter-captcha/src/main/java/cn/iocoder/yudao/framework/captcha/core/service/RedisCaptchaServiceImpl.java

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.captcha.core.service;
 import com.anji.captcha.service.CaptchaCacheService;
 import lombok.AllArgsConstructor;
 import lombok.NoArgsConstructor;
-import lombok.RequiredArgsConstructor;
 import org.springframework.data.redis.core.StringRedisTemplate;
 
 import javax.annotation.Resource;

+ 15 - 1
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java

@@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.mybatis.core.mapper;
 
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -10,6 +9,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
+import com.baomidou.mybatisplus.extension.toolkit.Db;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.Collection;
@@ -92,8 +92,22 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
         entities.forEach(this::insert);
     }
 
+    /**
+     * 批量插入,适合大量数据插入
+     *
+     * @param entities 实体们
+     * @param size     插入数量 Db.saveBatch 默认为1000
+     */
+    default void insertBatch(Collection<T> entities, int size) {
+        Db.saveBatch(entities, size);
+    }
+
     default void updateBatch(T update) {
         update(update, new QueryWrapper<>());
     }
 
+    default void updateBatch(Collection<T> entities, int size) {
+        Db.updateBatchById(entities, size);
+    }
+
 }

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

+ 6 - 0
yudao-framework/yudao-spring-boot-starter-web/pom.xml

@@ -68,6 +68,12 @@
             <scope>provided</scope> <!-- 设置为 provided,主要是 GlobalExceptionHandler 使用 -->
         </dependency>
 
+        <!-- xss -->
+        <dependency>
+            <groupId>org.jsoup</groupId>
+            <artifactId>jsoup</artifactId>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 37 - 3
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/config/YudaoWebAutoConfiguration.java

@@ -2,15 +2,22 @@ package cn.iocoder.yudao.framework.web.config;
 
 import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
+import cn.iocoder.yudao.framework.web.core.clean.JsoupXssCleaner;
+import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
 import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter;
 import cn.iocoder.yudao.framework.web.core.filter.DemoFilter;
 import cn.iocoder.yudao.framework.web.core.filter.XssFilter;
 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
 import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler;
+import cn.iocoder.yudao.framework.web.core.json.XssStringJsonDeserializer;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
@@ -48,7 +55,7 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
      * 设置 API 前缀,仅仅匹配 controller 包下的
      *
      * @param configurer 配置
-     * @param api API 配置
+     * @param api        API 配置
      */
     private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) {
         AntPathMatcher antPathMatcher = new AntPathMatcher(".");
@@ -104,8 +111,9 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
      * 创建 XssFilter Bean,解决 Xss 安全问题
      */
     @Bean
-    public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher) {
-        return createFilterBean(new XssFilter(properties, pathMatcher), WebFilterOrderEnum.XSS_FILTER);
+    @ConditionalOnBean(XssCleaner.class)
+    public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) {
+        return createFilterBean(new XssFilter(properties, pathMatcher, xssCleaner), WebFilterOrderEnum.XSS_FILTER);
     }
 
     /**
@@ -117,6 +125,32 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
         return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER);
     }
 
+
+    /**
+     * Xss 清理者
+     *
+     * @return XssCleaner
+     */
+    @Bean
+    @ConditionalOnMissingBean(XssCleaner.class)
+    public XssCleaner xssCleaner() {
+        return new JsoupXssCleaner();
+    }
+
+    /**
+     * 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤
+     *
+     * @return Jackson2ObjectMapperBuilderCustomizer
+     */
+    @Bean
+    @ConditionalOnMissingBean(name = "xssJacksonCustomizer")
+    @ConditionalOnBean(ObjectMapper.class)
+    @ConditionalOnProperty(value = "yudao.xss.enable", havingValue = "true")
+    public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssCleaner xssCleaner) {
+        // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理
+        return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(xssCleaner));
+    }
+
     private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
         FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
         bean.setOrder(order);

+ 80 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/JsoupXssCleaner.java

@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.framework.web.core.clean;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.safety.Safelist;
+
+/**
+ * jsonp 过滤字符串
+ */
+public class JsoupXssCleaner implements XssCleaner {
+
+    private final Safelist safelist;
+
+    /**
+     * 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分)
+     */
+    private final String baseUri;
+
+    /**
+     * 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表
+     */
+    public JsoupXssCleaner() {
+        this.safelist = buildSafelist();
+        this.baseUri = "";
+    }
+
+    public JsoupXssCleaner(Safelist safelist) {
+        this.safelist = safelist;
+        this.baseUri = "";
+    }
+
+    public JsoupXssCleaner(String baseUri) {
+        this.safelist = buildSafelist();
+        this.baseUri = baseUri;
+    }
+
+    public JsoupXssCleaner(Safelist safelist, String baseUri) {
+        this.safelist = safelist;
+        this.baseUri = baseUri;
+    }
+
+    /**
+     * 构建一个 Xss 清理的 Safelist 规则。
+     * 基于 Safelist#relaxed() 的基础上:
+     * 1. 扩展支持了 style 和 class 属性
+     * 2. a 标签额外支持了 target 属性
+     * 3. img 标签额外支持了 data 协议,便于支持 base64
+     *
+     * @return Safelist
+     */
+    private Safelist buildSafelist() {
+        // 使用 jsoup 提供的默认的
+        Safelist relaxedSafelist = Safelist.relaxed();
+        // 富文本编辑时一些样式是使用 style 来进行实现的
+        // 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性
+        // 注意:style 属性会有注入风险 <img STYLE="background-image:url(javascript:alert('XSS'))">
+        relaxedSafelist.addAttributes(":all", "style", "class");
+        // 保留 a 标签的 target 属性
+        relaxedSafelist.addAttributes("a", "target");
+        // 支持img 为base64
+        relaxedSafelist.addProtocols("img", "src", "data");
+
+        // 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除
+        // WHITELIST.preserveRelativeLinks(false);
+
+        // 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如 <img src=javascript:alert("xss")>
+        // 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径
+        // WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto");
+        // WHITELIST.removeProtocols("img", "src", "http", "https");
+
+        return relaxedSafelist;
+    }
+
+    @Override
+    public String clean(String html) {
+        return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false));
+    }
+
+}
+

+ 15 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/clean/XssCleaner.java

@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.framework.web.core.clean;
+
+/**
+ * 对 html 文本中的有 Xss 风险的数据进行清理
+ */
+public interface XssCleaner {
+
+    /**
+     * 清理有 Xss 风险的文本
+     *
+     * @param html 原 html
+     * @return 清理后的 html
+     */
+    String clean(String html);
+}

+ 5 - 2
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssFilter.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.framework.web.core.filter;
 
 import cn.iocoder.yudao.framework.web.config.XssProperties;
+import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
 import lombok.AllArgsConstructor;
 import org.springframework.util.PathMatcher;
 import org.springframework.web.filter.OncePerRequestFilter;
@@ -13,7 +14,7 @@ import java.io.IOException;
 
 /**
  * Xss 过滤器
- *
+ * <p>
  * 对 Xss 不了解的胖友,可以看看 http://www.iocoder.cn/Fight/The-new-girl-asked-me-why-AJAX-requests-are-not-secure-I-did-not-answer/
  *
  * @author 芋道源码
@@ -30,10 +31,12 @@ public class XssFilter extends OncePerRequestFilter {
      */
     private final PathMatcher pathMatcher;
 
+    private final XssCleaner xssCleaner;
+
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
             throws IOException, ServletException {
-        filterChain.doFilter(new XssRequestWrapper(request), response);
+        filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response);
     }
 
     @Override

+ 50 - 95
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/XssRequestWrapper.java

@@ -1,21 +1,10 @@
 package cn.iocoder.yudao.framework.web.core.filter;
 
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.util.ArrayUtil;
-import cn.hutool.core.util.ReflectUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.http.HTMLFilter;
-import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
+import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
 
-import javax.servlet.ReadListener;
-import javax.servlet.ServletInputStream;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequestWrapper;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 /**
@@ -24,113 +13,79 @@ import java.util.Map;
  * @author 芋道源码
  */
 public class XssRequestWrapper extends HttpServletRequestWrapper {
+    private final XssCleaner xssCleaner;
 
-    /**
-     * 基于线程级别的 HTMLFilter 对象,因为它线程非安全
-     */
-    private static final ThreadLocal<HTMLFilter> HTML_FILTER = ThreadLocal.withInitial(() -> {
-        HTMLFilter htmlFilter = new HTMLFilter();
-        // 反射修改 encodeQuotes 属性为 false,避免 " 被转移成 &quot; 字符
-        ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false);
-        return htmlFilter;
-    });
-
-    public XssRequestWrapper(HttpServletRequest request) {
+    public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) {
         super(request);
+        this.xssCleaner = xssCleaner;
     }
 
-    private static String filterXss(String content) {
-        if (StrUtil.isEmpty(content)) {
-            return content;
-        }
-        return HTML_FILTER.get().filter(content);
-    }
-
-    // ========== IO 流相关 ==========
-
+    // ============================ parameter ============================
     @Override
-    public BufferedReader getReader() throws IOException {
-        return new BufferedReader(new InputStreamReader(this.getInputStream()));
+    public Map<String, String[]> getParameterMap() {
+        Map<String, String[]> map = new LinkedHashMap<>();
+        Map<String, String[]> parameters = super.getParameterMap();
+        for (Map.Entry<String, String[]> entry : parameters.entrySet()) {
+            String[] values = entry.getValue();
+            for (int i = 0; i < values.length; i++) {
+                values[i] = xssCleaner.clean(values[i]);
+            }
+            map.put(entry.getKey(), values);
+        }
+        return map;
     }
 
     @Override
-    public ServletInputStream getInputStream() throws IOException {
-        // 如果非 json 请求,不进行 Xss 处理
-        if (!ServletUtils.isJsonRequest(this)) {
-            return super.getInputStream();
+    public String[] getParameterValues(String name) {
+        String[] values = super.getParameterValues(name);
+        if (values == null) {
+            return null;
         }
-
-        // 读取内容,并过滤
-        String content = IoUtil.readUtf8(super.getInputStream());
-        content = filterXss(content);
-        final ByteArrayInputStream newInputStream = new ByteArrayInputStream(content.getBytes());
-        // 返回 ServletInputStream
-        return new ServletInputStream() {
-
-            @Override
-            public int read() {
-                return newInputStream.read();
-            }
-
-            @Override
-            public boolean isFinished() {
-                return true;
-            }
-
-            @Override
-            public boolean isReady() {
-                return true;
-            }
-
-            @Override
-            public void setReadListener(ReadListener readListener) {}
-
-        };
+        int count = values.length;
+        String[] encodedValues = new String[count];
+        for (int i = 0; i < count; i++) {
+            encodedValues[i] = xssCleaner.clean(values[i]);
+        }
+        return encodedValues;
     }
 
-    // ========== Param 相关 ==========
-
     @Override
     public String getParameter(String name) {
         String value = super.getParameter(name);
-        return filterXss(value);
+        if (value == null) {
+            return null;
+        }
+        return xssCleaner.clean(value);
     }
 
+    // ============================ attribute ============================
     @Override
-    public String[] getParameterValues(String name) {
-        String[] values = super.getParameterValues(name);
-        if (ArrayUtil.isEmpty(values)) {
-            return values;
-        }
-        // 过滤处理
-        for (int i = 0; i < values.length; i++) {
-            values[i] = filterXss(values[i]);
+    public Object getAttribute(String name) {
+        Object value = super.getAttribute(name);
+        if (value instanceof String) {
+            xssCleaner.clean((String) value);
         }
-        return values;
+        return value;
     }
 
+    // ============================ header ============================
     @Override
-    public Map<String, String[]> getParameterMap() {
-        Map<String, String[]> valueMap = super.getParameterMap();
-        if (CollUtil.isEmpty(valueMap)) {
-            return valueMap;
-        }
-        // 过滤处理
-        for (Map.Entry<String, String[]> entry : valueMap.entrySet()) {
-            String[] values = entry.getValue();
-            for (int i = 0; i < values.length; i++) {
-                values[i] = filterXss(values[i]);
-            }
+    public String getHeader(String name) {
+        String value = super.getHeader(name);
+        if (value == null) {
+            return null;
         }
-        return valueMap;
+        return xssCleaner.clean(value);
     }
 
-    // ========== Header 相关 ==========
-
+    // ============================ queryString ============================
     @Override
-    public String getHeader(String name) {
-        String value = super.getHeader(name);
-        return filterXss(value);
+    public String getQueryString() {
+        String value = super.getQueryString();
+        if (value == null) {
+            return null;
+        }
+        return xssCleaner.clean(value);
     }
 
 }

+ 59 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/json/XssStringJsonDeserializer.java

@@ -0,0 +1,59 @@
+package cn.iocoder.yudao.framework.web.core.json;
+
+import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+
+/**
+ * XSS 过滤 jackson 反序列化器。
+ * 在反序列化的过程中,会对字符串进行 XSS 过滤。
+ *
+ * @author Hccake
+ */
+@Slf4j
+@AllArgsConstructor
+public class XssStringJsonDeserializer extends StringDeserializer {
+
+    private final XssCleaner xssCleaner;
+
+    @Override
+    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+        if (p.hasToken(JsonToken.VALUE_STRING)) {
+            return xssCleaner.clean(p.getText());
+        }
+        JsonToken t = p.currentToken();
+        // [databind#381]
+        if (t == JsonToken.START_ARRAY) {
+            return _deserializeFromArray(p, ctxt);
+        }
+        // need to gracefully handle byte[] data, as base64
+        if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
+            Object ob = p.getEmbeddedObject();
+            if (ob == null) {
+                return null;
+            }
+            if (ob instanceof byte[]) {
+                return ctxt.getBase64Variant().encode((byte[]) ob, false);
+            }
+            // otherwise, try conversion using toString()...
+            return ob.toString();
+        }
+        // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
+        if (t == JsonToken.START_OBJECT) {
+            return ctxt.extractScalarFromObject(p, this, _valueClass);
+        }
+
+        if (t.isScalarValue()) {
+            String text = p.getValueAsString();
+            return xssCleaner.clean(text);
+        }
+        return (String) ctxt.handleUnexpectedToken(_valueClass, p);
+    }
+}
+

+ 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

+ 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 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/codegen/CodegenConvert.java

@@ -11,9 +11,11 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
 import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
 import com.baomidou.mybatisplus.generator.config.po.TableField;
 import com.baomidou.mybatisplus.generator.config.po.TableInfo;
+import org.apache.ibatis.type.JdbcType;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
+import org.mapstruct.Named;
 import org.mapstruct.factory.Mappers;
 
 import java.util.List;
@@ -37,7 +39,7 @@ public interface CodegenConvert {
 
     @Mappings({
             @Mapping(source = "name", target = "columnName"),
-            @Mapping(source = "type", target = "dataType"),
+            @Mapping(source = "metaInfo.jdbcType", target = "dataType", qualifiedByName = "getDataType"),
             @Mapping(source = "comment", target = "columnComment"),
             @Mapping(source = "metaInfo.nullable", target = "nullable"),
             @Mapping(source = "keyFlag", target = "primaryKey"),
@@ -47,6 +49,11 @@ public interface CodegenConvert {
     })
     CodegenColumnDO convert(TableField bean);
 
+    @Named("getDataType")
+    default String getDataType(JdbcType jdbcType) {
+        return jdbcType.name();
+    }
+
     // ========== CodegenTableDO 相关 ==========
 
 //    List<CodegenTableRespVO> convertList02(List<CodegenTableDO> list);

+ 8 - 6
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenColumnDO.java

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnListConditionEnu
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.generator.config.po.TableField;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.experimental.Accessors;
@@ -29,7 +30,7 @@ public class CodegenColumnDO extends BaseDO {
     private Long id;
     /**
      * 表编号
-     *
+     * <p>
      * 关联 {@link CodegenTableDO#getId()}
      */
     private Long tableId;
@@ -41,7 +42,8 @@ public class CodegenColumnDO extends BaseDO {
      */
     private String columnName;
     /**
-     * 字段类型
+     * 数据库字段类型
+     * 关联 {@link TableField.MetaInfo#getJdbcType()}
      */
     private String dataType;
     /**
@@ -69,7 +71,7 @@ public class CodegenColumnDO extends BaseDO {
 
     /**
      * Java 属性类型
-     *
+     * <p>
      * 例如说 String、Boolean 等等
      */
     private String javaType;
@@ -79,7 +81,7 @@ public class CodegenColumnDO extends BaseDO {
     private String javaField;
     /**
      * 字典类型
-     *
+     * <p>
      * 关联 DictTypeDO 的 type 属性
      */
     private String dictType;
@@ -104,7 +106,7 @@ public class CodegenColumnDO extends BaseDO {
     private Boolean listOperation;
     /**
      * List 查询操作的条件类型
-     *
+     * <p>
      * 枚举 {@link CodegenColumnListConditionEnum}
      */
     private String listOperationCondition;
@@ -117,7 +119,7 @@ public class CodegenColumnDO extends BaseDO {
 
     /**
      * 显示类型
-     *
+     * <p>
      * 枚举 {@link CodegenColumnHtmlTypeEnum}
      */
     private String htmlType;

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

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

@@ -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,7 +40,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['${permissionPrefix}:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
     </XTable>
@@ -119,11 +119,6 @@ const handleCreate = () => {
   modelLoading.value = false
 }
 
-// 导出操作
-const handleExport = async () => {
-  await exportList('${table.classComment}.xls')
-}
-
 // 修改操作
 const handleUpdate = async (rowId: number) => {
   setDialogTile('update')
@@ -141,11 +136,6 @@ const handleDetail = async (rowId: number) => {
   modelLoading.value = false
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()

+ 2 - 2
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/DefaultDatabaseQueryTest.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.module.infra.service;
 
 import cn.hutool.core.util.StrUtil;
-import com.baomidou.mybatisplus.generator.IDatabaseQuery.DefaultDatabaseQuery;
+import com.baomidou.mybatisplus.generator.query.DefaultQuery;
 import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
 import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder;
 import com.baomidou.mybatisplus.generator.config.po.TableInfo;
@@ -19,7 +19,7 @@ public class DefaultDatabaseQueryTest {
 
         ConfigBuilder builder = new ConfigBuilder(null, dataSourceConfig, null, null, null, null);
 
-        DefaultDatabaseQuery query = new DefaultDatabaseQuery(builder);
+        DefaultQuery query = new DefaultQuery(builder);
 
         long time = System.currentTimeMillis();
         List<TableInfo> tableInfos = query.queryTables();

+ 27 - 13
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java

@@ -1,47 +1,61 @@
 package cn.iocoder.yudao.module.system.controller.admin.captcha;
 
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.servlet.ServletUtil;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 import com.anji.captcha.model.common.ResponseModel;
 import com.anji.captcha.model.vo.CaptchaVO;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import io.swagger.v3.oas.annotations.Operation;
+import com.anji.captcha.service.CaptchaService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import javax.annotation.Resource;
 import javax.annotation.security.PermitAll;
 import javax.servlet.http.HttpServletRequest;
 
 /**
  * 验证码
  *
- * 问题:为什么不直接使用 anji 提供的 CaptchaController,而要另外继承?
- * 回答:管理使用 /admin-api/* 作为前缀,所以需要继承!
- *
  * @author 芋道源码
  */
-@Tag(name = "管理后台 - 验证码")
+@Api(tags = "管理后台 - 验证码")
 @RestController("adminCaptchaController")
 @RequestMapping("/system/captcha")
-public class CaptchaController extends com.anji.captcha.controller.CaptchaController {
+public class CaptchaController {
+
+    @Resource
+    private CaptchaService captchaService;
 
     @PostMapping({"/get"})
-    @Operation(summary = "获得验证码")
+    @ApiOperation("获得验证码")
     @PermitAll
     @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
-    @Override
     public ResponseModel get(@RequestBody CaptchaVO data, HttpServletRequest request) {
-        return super.get(data, request);
+        assert request.getRemoteHost() != null;
+        data.setBrowserInfo(getRemoteId(request));
+        return captchaService.get(data);
     }
 
     @PostMapping("/check")
-    @Operation(summary = "校验验证码")
+    @ApiOperation("校验验证码")
     @PermitAll
     @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
-    @Override
     public ResponseModel check(@RequestBody CaptchaVO data, HttpServletRequest request) {
-        return super.check(data, request);
+        data.setBrowserInfo(getRemoteId(request));
+        return captchaService.check(data);
+    }
+
+    public static String getRemoteId(HttpServletRequest request) {
+        String ip = ServletUtil.getClientIP(request);
+        String ua = request.getHeader("user-agent");
+        if (StrUtil.isNotBlank(ip)) {
+            return ip + ua;
+        }
+        return request.getRemoteAddr() + ua;
     }
 
 }

+ 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

@@ -97,6 +97,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-vue3/README.md

@@ -34,7 +34,7 @@
 | [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.10.0  |
-| [vxe-table](https://vxetable.cn/) | vue 最强表单 | 4.3.7  |
+| [vxe-table](https://vxetable.cn/) | vue 最强表单 | 4.3.9  |
 | [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  |

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

@@ -1,6 +1,6 @@
 {
   "name": "yudao-ui-admin-vue3",
-  "version": "1.6.5.1901",
+  "version": "1.6.6-snapshot.1912",
   "description": "基于vue3、vite4、element-plus、typesScript",
   "author": "xingyu",
   "private": false,
@@ -42,7 +42,7 @@
     "lodash-es": "^4.17.21",
     "mitt": "^3.0.0",
     "nprogress": "^0.2.0",
-    "pinia": "^2.0.28",
+    "pinia": "^2.0.29",
     "qrcode": "^1.5.1",
     "qs": "^6.11.0",
     "url": "^0.11.0",
@@ -50,14 +50,14 @@
     "vue-i18n": "9.2.2",
     "vue-router": "^4.1.6",
     "vue-types": "^5.0.2",
-    "vxe-table": "^4.3.7",
+    "vxe-table": "^4.3.9",
     "web-storage-cache": "^1.1.1",
     "xe-utils": "^3.5.7"
   },
   "devDependencies": {
-    "@commitlint/cli": "^17.3.0",
-    "@commitlint/config-conventional": "^17.3.0",
-    "@iconify/json": "^2.2.1",
+    "@commitlint/cli": "^17.4.2",
+    "@commitlint/config-conventional": "^17.4.2",
+    "@iconify/json": "^2.2.7",
     "@intlify/unplugin-vue-i18n": "^0.8.1",
     "@purge-icons/generated": "^0.9.0",
     "@types/intro.js": "^5.1.0",
@@ -66,32 +66,32 @@
     "@types/nprogress": "^0.2.0",
     "@types/qrcode": "^1.5.0",
     "@types/qs": "^6.9.7",
-    "@typescript-eslint/eslint-plugin": "^5.48.0",
-    "@typescript-eslint/parser": "^5.48.0",
+    "@typescript-eslint/eslint-plugin": "^5.48.1",
+    "@typescript-eslint/parser": "^5.48.1",
     "@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.31.0",
+    "eslint": "^8.32.0",
     "eslint-config-prettier": "^8.6.0",
-    "eslint-define-config": "^1.13.0",
+    "eslint-define-config": "^1.14.0",
     "eslint-plugin-prettier": "^4.2.1",
-    "eslint-plugin-vue": "^9.8.0",
+    "eslint-plugin-vue": "^9.9.0",
     "lint-staged": "^13.1.0",
-    "postcss": "^8.4.20",
+    "postcss": "^8.4.21",
     "postcss-html": "^1.5.0",
     "postcss-scss": "^4.0.6",
-    "prettier": "^2.8.1",
-    "rimraf": "^3.0.2",
-    "rollup": "^3.9.1",
+    "prettier": "^2.8.3",
+    "rimraf": "^4.0.7",
+    "rollup": "^3.10.0",
     "sass": "^1.57.1",
     "stylelint": "^14.16.1",
     "stylelint-config-html": "^1.1.0",
     "stylelint-config-prettier": "^9.0.4",
     "stylelint-config-recommended": "^9.0.0",
     "stylelint-config-standard": "^29.0.0",
-    "stylelint-order": "^5.0.0",
+    "stylelint-order": "^6.0.1",
     "terser": "^5.16.1",
     "typescript": "4.9.4",
     "vite": "4.0.4",
@@ -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.20",
+    "vue-tsc": "^1.0.24",
     "windicss": "^3.5.6"
   },
   "engines": {

Разница между файлами не показана из-за своего большого размера
+ 489 - 163
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 {

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

@@ -78,4 +78,4 @@ $vxe-modal-border-color: #3b3b3b;
 /*pulldown*/
 $vxe-pulldown-panel-background-color: #262626 !default;
 
-@import 'vxe-table/styles/index';
+@import 'vxe-table/styles/index.scss';

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

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

+ 7 - 8
yudao-ui-admin-vue3/src/components/XTable/src/type.ts

@@ -7,15 +7,14 @@ export type XTableProps<D = any> = VxeGridProps<D> & {
   topActionSlots?: boolean // 是否开启表格内顶部操作栏插槽
   treeConfig?: VxeTablePropTypes.TreeConfig // 树形表单配置
   isList?: boolean // 是否不带分页的list
-  getListApi?: Function
-  getAllListApi?: Function
-  deleteApi?: Function
-  exportListApi?: Function
+  getListApi?: Function // 获取列表接口
+  getAllListApi?: Function // 获取全部数据接口 用于 vxe 导出
+  deleteApi?: Function // 删除接口
+  exportListApi?: Function // 导出接口
   exportName?: string // 导出文件夹名称
-  params?: any
-  pagination?: boolean | VxeGridPropTypes.PagerConfig
-  toolBar?: boolean | VxeGridPropTypes.ToolbarConfig
-  afterFetch?: Function
+  params?: any // 其他查询参数
+  pagination?: boolean | VxeGridPropTypes.PagerConfig // 分页配置参数
+  toolBar?: boolean | VxeGridPropTypes.ToolbarConfig // 右侧工具栏配置参数
 }
 export type XColumns = VxeGridPropTypes.Columns
 

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

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

@@ -26,15 +26,17 @@ 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'
 
 import App from './App.vue'
 
+import './permission'
+
 // 创建实例
 const setupAll = async () => {
   const app = createApp(App)
@@ -53,6 +55,8 @@ const setupAll = async () => {
 
   setupAuth(app)
 
+  await router.isReady()
+
   app.mount('#app')
 }
 

+ 70 - 0
yudao-ui-admin-vue3/src/permission.ts

@@ -0,0 +1,70 @@
+import router from './router'
+import type { RouteRecordRaw } from 'vue-router'
+import { isRelogin } from '@/config/axios/service'
+import { getAccessToken } from '@/utils/auth'
+import { useTitle } from '@/hooks/web/useTitle'
+import { useNProgress } from '@/hooks/web/useNProgress'
+import { usePageLoading } from '@/hooks/web/usePageLoading'
+import { useDictStoreWithOut } from '@/store/modules/dict'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { usePermissionStoreWithOut } from '@/store/modules/permission'
+
+const { start, done } = useNProgress()
+
+const { loadStart, loadDone } = usePageLoading()
+// 路由不重定向白名单
+const whiteList = [
+  '/login',
+  '/social-login',
+  '/auth-redirect',
+  '/bind',
+  '/register',
+  '/oauthLogin/gitee'
+]
+
+// 路由加载前
+router.beforeEach(async (to, from, next) => {
+  start()
+  loadStart()
+  if (getAccessToken()) {
+    if (to.path === '/login') {
+      next({ path: '/' })
+    } else {
+      // 获取所有字典
+      const dictStore = useDictStoreWithOut()
+      const userStore = useUserStoreWithOut()
+      const permissionStore = usePermissionStoreWithOut()
+      if (!dictStore.getIsSetDict) {
+        dictStore.setDictMap()
+      }
+      if (!userStore.getIsSetUser) {
+        isRelogin.show = true
+        await userStore.setUserInfoAction()
+        isRelogin.show = false
+        // 后端过滤菜单
+        await permissionStore.generateRoutes()
+        permissionStore.getAddRouters.forEach((route) => {
+          router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
+        })
+        const redirectPath = from.query.redirect || to.path
+        const redirect = decodeURIComponent(redirectPath as string)
+        const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
+        next(nextData)
+      } else {
+        next()
+      }
+    }
+  } else {
+    if (whiteList.indexOf(to.path) !== -1) {
+      next()
+    } else {
+      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
+    }
+  }
+})
+
+router.afterEach((to) => {
+  useTitle(to?.meta?.title as string)
+  done() // 结束Progress
+  loadDone()
+})

+ 0 - 6
yudao-ui-admin-vue3/src/plugins/vxeTable/index.scss

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

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

+ 0 - 81
yudao-ui-admin-vue3/src/plugins/vxeTable/theme/dark.scss

@@ -1,81 +0,0 @@
-// 修改样式变量
-//@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';

+ 0 - 16
yudao-ui-admin-vue3/src/plugins/vxeTable/theme/light.scss

@@ -1,16 +0,0 @@
-// 修改样式变量
-// /*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.scss';

+ 0 - 77
yudao-ui-admin-vue3/src/router/index.ts

@@ -2,23 +2,6 @@ import type { App } from 'vue'
 import type { RouteRecordRaw } from 'vue-router'
 import { createRouter, createWebHashHistory } from 'vue-router'
 import remainingRouter from './modules/remaining'
-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()
-
-const { loadStart, loadDone } = usePageLoading()
 
 // 创建路由实例
 const router = createRouter({
@@ -28,66 +11,6 @@ const router = createRouter({
   scrollBehavior: () => ({ left: 0, top: 0 })
 })
 
-// 路由不重定向白名单
-const whiteList = [
-  '/login',
-  '/social-login',
-  '/auth-redirect',
-  '/bind',
-  '/register',
-  '/oauthLogin/gitee'
-]
-
-// 路由加载前
-router.beforeEach(async (to, from, next) => {
-  start()
-  loadStart()
-  if (getAccessToken()) {
-    if (to.path === '/login') {
-      next({ path: '/' })
-    } else {
-      // 获取所有字典
-      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 (!userStore.getIsSetUser) {
-        isRelogin.show = true
-        const res = await getInfoApi()
-        await userStore.setUserInfoAction(res)
-        isRelogin.show = false
-        // 后端过滤菜单
-        await permissionStore.generateRoutes()
-        permissionStore.getAddRouters.forEach((route) => {
-          router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
-        })
-        const redirectPath = from.query.redirect || to.path
-        const redirect = decodeURIComponent(redirectPath as string)
-        const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
-        next(nextData)
-      } else {
-        next()
-      }
-    }
-  } else {
-    if (whiteList.indexOf(to.path) !== -1) {
-      next()
-    } else {
-      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
-    }
-  }
-})
-
-router.afterEach((to) => {
-  useTitle(to?.meta?.title as string)
-  done() // 结束Progress
-  loadDone()
-})
-
 export const resetRouter = (): void => {
   const resetWhiteNameList = ['Redirect', 'Login', 'NoFind', 'Root']
   router.getRoutes().forEach((route) => {

+ 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 秒 过期
+      }
     }
   }
 })

+ 6 - 1
yudao-ui-admin-vue3/src/store/modules/user.ts

@@ -2,6 +2,7 @@ import { store } from '../index'
 import { defineStore } from 'pinia'
 import { getAccessToken, removeToken } from '@/utils/auth'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { getInfoApi } from '@/api/login'
 
 const { wsCache } = useCache()
 
@@ -43,11 +44,15 @@ export const useUserStore = defineStore('admin-user', {
     }
   },
   actions: {
-    async setUserInfoAction(userInfo: UserInfoVO) {
+    async setUserInfoAction() {
       if (!getAccessToken()) {
         this.resetState()
         return null
       }
+      let userInfo = wsCache.get(CACHE_KEY.USER)
+      if (!userInfo) {
+        userInfo = await getInfoApi()
+      }
       this.permissions = userInfo.permissions
       this.roles = userInfo.roles
       this.user = userInfo.user

+ 2 - 0
yudao-ui-admin-vue3/src/styles/variables.scss

@@ -2,3 +2,5 @@
 $namespace: v;
 // el命名空间
 $elNamespace: el;
+// vxe命名空间
+$vxeNamespace: vxe;

+ 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)
 }
 
 // ========== 租户相关 ==========

+ 8 - 9
yudao-ui-admin-vue3/src/utils/propTypes.ts

@@ -17,13 +17,12 @@ const propTypes = createTypes({
 
 // 需要自定义扩展的类型
 // see: https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html#the-extend-method
-propTypes.extend([
-  {
-    name: 'style',
-    getter: true,
-    type: [String, Object],
-    default: undefined
-  }
-])
-
+// propTypes.extend([
+//   {
+//     name: 'style',
+//     getter: true,
+//     type: [String, Object],
+//     default: undefined
+//   }
+// ])
 export { propTypes }

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

+ 1 - 1
yudao-ui-admin-vue3/src/views/Profile/Index.vue

@@ -8,7 +8,7 @@
       </template>
       <ProfileUser />
     </el-card>
-    <el-card class="w-2/3 user" style="margin-left: 10px" shadow="hover">
+    <el-card class="w-2/3 user ml-3" shadow="hover">
       <template #header>
         <div class="card-header">
           <span>{{ t('profile.info.title') }}</span>

+ 2 - 5
yudao-ui-admin-vue3/src/views/infra/apiErrorLog/index.vue

@@ -8,7 +8,7 @@
           type="warning"
           preIcon="ep:download"
           :title="t('action.export')"
-          @click="handleExport()"
+          @click="exportList('错误数据.xls')"
         />
       </template>
       <template #duration_default="{ row }">
@@ -81,10 +81,7 @@ const handleDetail = (row: ApiErrorLogApi.ApiErrorLogVO) => {
   dialogTitle.value = t('action.detail')
   dialogVisible.value = true
 }
-// 导出
-const handleExport = async () => {
-  await exportList('错误数据.xls')
-}
+
 // 异常处理操作
 const handleProcessClick = (
   row: ApiErrorLogApi.ApiErrorLogVO,

+ 5 - 12
yudao-ui-admin-vue3/src/views/infra/codegen/EditTable.vue

@@ -8,9 +8,6 @@
         <el-tab-pane label="字段信息" name="cloum">
           <CloumInfoForm ref="cloumInfoRef" :info="cloumCurrentRow" />
         </el-tab-pane>
-        <el-tab-pane label="生成信息" name="genInfo">
-          <GenInfoForm ref="genInfoRef" :genInfo="tableCurrentRow" />
-        </el-tab-pane>
       </el-tabs>
       <template #right>
         <XButton
@@ -30,7 +27,7 @@ import { ElTabs, ElTabPane } from 'element-plus'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
 import { ContentDetailWrap } from '@/components/ContentDetailWrap'
-import { BasicInfoForm, CloumInfoForm, GenInfoForm } from './components'
+import { BasicInfoForm, CloumInfoForm } from './components'
 import { getCodegenTableApi, updateCodegenTableApi } from '@/api/infra/codegen'
 import { CodegenTableVO, CodegenColumnVO, CodegenUpdateReqVO } from '@/api/infra/codegen/types'
 
@@ -40,33 +37,29 @@ const { push } = useRouter()
 const { query } = useRoute()
 const loading = ref(false)
 const title = ref('代码生成')
-const activeName = ref('cloum')
+const activeName = ref('basicInfo')
 const cloumInfoRef = ref(null)
 const tableCurrentRow = ref<CodegenTableVO>()
 const cloumCurrentRow = ref<CodegenColumnVO[]>([])
 const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>()
-const genInfoRef = ref<ComponentRef<typeof GenInfoForm>>()
 
 const getList = async () => {
   const id = query.id as unknown as number
   if (id) {
     // 获取表详细信息
     const res = await getCodegenTableApi(id)
-    tableCurrentRow.value = res.table
     title.value = '修改[ ' + res.table.tableName + ' ]生成配置'
+    tableCurrentRow.value = res.table
     cloumCurrentRow.value = res.columns
   }
 }
 const submitForm = async () => {
   const basicInfo = unref(basicInfoRef)
-  const genInfo = unref(genInfoRef)
   const basicForm = await basicInfo?.elFormRef?.validate()?.catch(() => {})
-  const genForm = await genInfo?.elFormRef?.validate()?.catch(() => {})
-  if (basicForm && genForm) {
+  if (basicForm) {
     const basicInfoData = (await basicInfo?.getFormData()) as CodegenTableVO
-    const genInfoData = (await genInfo?.getFormData()) as CodegenTableVO
     const genTable: CodegenUpdateReqVO = {
-      table: Object.assign({}, basicInfoData, genInfoData),
+      table: basicInfoData,
       columns: cloumCurrentRow.value
     }
     await updateCodegenTableApi(genTable)

+ 99 - 3
yudao-ui-admin-vue3/src/views/infra/codegen/components/BasicInfoForm.vue

@@ -2,25 +2,59 @@
   <Form :rules="rules" @register="register" />
 </template>
 <script setup lang="ts">
-import { PropType, reactive, watch } from 'vue'
+import { onMounted, PropType, reactive, ref, watch } from 'vue'
 import { required } from '@/utils/formRules'
 import { useForm } from '@/hooks/web/useForm'
 import { Form } from '@/components/Form'
 import { FormSchema } from '@/types/form'
 import { CodegenTableVO } from '@/api/infra/codegen/types'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { listSimpleMenusApi } from '@/api/system/menu'
+import { handleTree, defaultProps } from '@/utils/tree'
+
 const props = defineProps({
   basicInfo: {
     type: Object as PropType<Nullable<CodegenTableVO>>,
     default: () => null
   }
 })
+
+const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)
+const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)
+const menuOptions = ref<any>([]) // 树形结构
+const getTree = async () => {
+  const res = await listSimpleMenusApi()
+  menuOptions.value = handleTree(res)
+}
+
 const rules = reactive({
   tableName: [required],
   tableComment: [required],
   className: [required],
-  author: [required]
+  author: [required],
+  templateType: [required],
+  scene: [required],
+  moduleName: [required],
+  businessName: [required],
+  businessPackage: [required],
+  classComment: [required]
 })
 const schema = reactive<FormSchema[]>([
+  {
+    label: '上级菜单',
+    field: 'parentMenuId',
+    component: 'TreeSelect',
+    componentProps: {
+      data: menuOptions,
+      props: defaultProps,
+      checkStrictly: true,
+      nodeKey: 'id'
+    },
+    labelMessage: '分配到指定菜单下,例如 系统管理',
+    colProps: {
+      span: 24
+    }
+  },
   {
     label: '表名称',
     field: 'tableName',
@@ -45,6 +79,64 @@ const schema = reactive<FormSchema[]>([
       span: 12
     }
   },
+  {
+    label: '类名称',
+    field: 'className',
+    component: 'Input',
+    labelMessage: '类名称(首字母大写),例如SysUser、SysMenu、SysDictData 等等',
+    colProps: {
+      span: 12
+    }
+  },
+  {
+    label: '生成模板',
+    field: 'templateType',
+    component: 'Select',
+    componentProps: {
+      options: templateTypeOptions
+    },
+    colProps: {
+      span: 12
+    }
+  },
+  {
+    label: '生成场景',
+    field: 'scene',
+    component: 'Select',
+    componentProps: {
+      options: sceneOptions
+    },
+    colProps: {
+      span: 12
+    }
+  },
+  {
+    label: '模块名',
+    field: 'moduleName',
+    component: 'Input',
+    labelMessage: '模块名,即一级目录,例如 system、infra、tool 等等',
+    colProps: {
+      span: 12
+    }
+  },
+  {
+    label: '业务名',
+    field: 'businessName',
+    component: 'Input',
+    labelMessage: '业务名,即二级目录,例如 user、permission、dict 等等',
+    colProps: {
+      span: 12
+    }
+  },
+  {
+    label: '类描述',
+    field: 'classComment',
+    component: 'Input',
+    labelMessage: '用作类描述,例如 用户',
+    colProps: {
+      span: 12
+    }
+  },
   {
     label: '作者',
     field: 'author',
@@ -62,7 +154,7 @@ const schema = reactive<FormSchema[]>([
       rows: 4
     },
     colProps: {
-      span: 12
+      span: 24
     }
   }
 ])
@@ -81,6 +173,10 @@ watch(
     immediate: true
   }
 )
+// ========== 初始化 ==========
+onMounted(async () => {
+  await getTree()
+})
 
 defineExpose({
   elFormRef,

+ 84 - 80
yudao-ui-admin-vue3/src/views/infra/codegen/components/CloumInfoForm.vue

@@ -1,113 +1,117 @@
 <template>
   <vxe-table
     ref="dragTable"
+    border
     :data="info"
     max-height="600"
     stripe
     class="xtable-scrollbar"
     :column-config="{ resizable: true }"
   >
-    <vxe-column title="字段列名" field="columnName" fixed="left" width="80" />
-    <vxe-column title="字段描述" field="columnComment">
-      <template #default="{ row }">
-        <el-input v-model="row.columnComment" />
-      </template>
-    </vxe-column>
-    <vxe-column title="物理类型" field="dataType" width="10%" />
-    <vxe-column title="Java类型" width="10%" field="javaType">
-      <template #default="{ row }">
-        <el-select v-model="row.javaType">
-          <el-option label="Long" value="Long" />
-          <el-option label="String" value="String" />
-          <el-option label="Integer" value="Integer" />
-          <el-option label="Double" value="Double" />
-          <el-option label="BigDecimal" value="BigDecimal" />
-          <el-option label="LocalDateTime" value="LocalDateTime" />
-          <el-option label="Boolean" value="Boolean" />
-        </el-select>
-      </template>
-    </vxe-column>
-    <vxe-column title="java属性" width="10%" field="javaField">
-      <template #default="{ row }">
-        <el-input v-model="row.javaField" />
-      </template>
-    </vxe-column>
-    <vxe-column title="插入" width="4%" field="createOperation">
-      <template #default="{ row }">
-        <vxe-checkbox true-label="true" false-label="false" v-model="row.createOperation" />
-      </template>
-    </vxe-column>
-    <vxe-column title="编辑" width="4%" field="updateOperation">
-      <template #default="{ row }">
-        <vxe-checkbox true-label="true" false-label="false" v-model="row.updateOperation" />
-      </template>
-    </vxe-column>
-    <vxe-column title="列表" width="4%" field="listOperationResult">
-      <template #default="{ row }">
-        <vxe-checkbox true-label="true" false-label="false" v-model="row.listOperationResult" />
-      </template>
-    </vxe-column>
-    <vxe-column title="查询" width="4%" field="listOperation">
-      <template #default="{ row }">
-        <vxe-checkbox true-label="true" false-label="false" v-model="row.listOperation" />
-      </template>
-    </vxe-column>
-    <vxe-column title="查询方式" width="8%" field="listOperationCondition">
-      <template #default="{ row }">
-        <el-select v-model="row.listOperationCondition">
-          <el-option label="=" value="=" />
-          <el-option label="!=" value="!=" />
-          <el-option label=">" value=">" />
-          <el-option label=">=" value=">=" />
-          <el-option label="<" value="<>" />
-          <el-option label="<=" value="<=" />
-          <el-option label="LIKE" value="LIKE" />
-          <el-option label="BETWEEN" value="BETWEEN" />
-        </el-select>
-      </template>
-    </vxe-column>
-    <vxe-column title="允许空" width="4%" field="nullable">
-      <template #default="{ row }">
-        <vxe-checkbox true-label="true" false-label="false" v-model="row.nullable" />
-      </template>
-    </vxe-column>
+    <vxe-column title="字段列名" field="columnName" fixed="left" width="10%" />
+    <vxe-colgroup title="基础属性">
+      <vxe-column title="字段描述" field="columnComment" width="10%">
+        <template #default="{ row }">
+          <vxe-input v-model="row.columnComment" placeholder="请输入字段描述" />
+        </template>
+      </vxe-column>
+      <vxe-column title="物理类型" field="dataType" width="10%" />
+      <vxe-column title="Java类型" width="10%" field="javaType">
+        <template #default="{ row }">
+          <vxe-select v-model="row.javaType" placeholder="请选择Java类型">
+            <vxe-option label="Long" value="Long" />
+            <vxe-option label="String" value="String" />
+            <vxe-option label="Integer" value="Integer" />
+            <vxe-option label="Double" value="Double" />
+            <vxe-option label="BigDecimal" value="BigDecimal" />
+            <vxe-option label="LocalDateTime" value="LocalDateTime" />
+            <vxe-option label="Boolean" value="Boolean" />
+          </vxe-select>
+        </template>
+      </vxe-column>
+      <vxe-column title="java属性" width="8%" field="javaField">
+        <template #default="{ row }">
+          <vxe-input v-model="row.javaField" placeholder="请输入java属性" />
+        </template>
+      </vxe-column>
+    </vxe-colgroup>
+    <vxe-colgroup title="增删改查">
+      <vxe-column title="插入" width="40px" field="createOperation">
+        <template #default="{ row }">
+          <vxe-checkbox true-label="true" false-label="false" v-model="row.createOperation" />
+        </template>
+      </vxe-column>
+      <vxe-column title="编辑" width="40px" field="updateOperation">
+        <template #default="{ row }">
+          <vxe-checkbox true-label="true" false-label="false" v-model="row.updateOperation" />
+        </template>
+      </vxe-column>
+      <vxe-column title="列表" width="40px" field="listOperationResult">
+        <template #default="{ row }">
+          <vxe-checkbox true-label="true" false-label="false" v-model="row.listOperationResult" />
+        </template>
+      </vxe-column>
+      <vxe-column title="查询" width="40px" field="listOperation">
+        <template #default="{ row }">
+          <vxe-checkbox true-label="true" false-label="false" v-model="row.listOperation" />
+        </template>
+      </vxe-column>
+      <vxe-column title="允许空" width="40px" field="nullable">
+        <template #default="{ row }">
+          <vxe-checkbox true-label="true" false-label="false" v-model="row.nullable" />
+        </template>
+      </vxe-column>
+      <vxe-column title="查询方式" width="60px" field="listOperationCondition">
+        <template #default="{ row }">
+          <vxe-select v-model="row.listOperationCondition" placeholder="请选择查询方式">
+            <vxe-option label="=" value="=" />
+            <vxe-option label="!=" value="!=" />
+            <vxe-option label=">" value=">" />
+            <vxe-option label=">=" value=">=" />
+            <vxe-option label="<" value="<>" />
+            <vxe-option label="<=" value="<=" />
+            <vxe-option label="LIKE" value="LIKE" />
+            <vxe-option label="BETWEEN" value="BETWEEN" />
+          </vxe-select>
+        </template>
+      </vxe-column>
+    </vxe-colgroup>
     <vxe-column title="显示类型" width="10%" field="htmlType">
       <template #default="{ row }">
-        <el-select v-model="row.htmlType">
-          <el-option label="文本框" value="input" />
-          <el-option label="文本域" value="textarea" />
-          <el-option label="下拉框" value="select" />
-          <el-option label="单选框" value="radio" />
-          <el-option label="复选框" value="checkbox" />
-          <el-option label="日期控件" value="datetime" />
-          <el-option label="图片上传" value="imageUpload" />
-          <el-option label="文件上传" value="fileUpload" />
-          <el-option label="富文本控件" value="editor" />
-        </el-select>
+        <vxe-select v-model="row.htmlType" placeholder="请选择显示类型">
+          <vxe-option label="文本框" value="input" />
+          <vxe-option label="文本域" value="textarea" />
+          <vxe-option label="下拉框" value="select" />
+          <vxe-option label="单选框" value="radio" />
+          <vxe-option label="复选框" value="checkbox" />
+          <vxe-option label="日期控件" value="datetime" />
+          <vxe-option label="图片上传" value="imageUpload" />
+          <vxe-option label="文件上传" value="fileUpload" />
+          <vxe-option label="富文本控件" value="editor" />
+        </vxe-select>
       </template>
     </vxe-column>
     <vxe-column title="字典类型" width="10%" field="dictType">
       <template #default="{ row }">
-        <el-select v-model="row.dictType" clearable filterable placeholder="请选择">
-          <el-option
+        <vxe-select v-model="row.dictType" clearable filterable placeholder="请选择字典类型">
+          <vxe-option
             v-for="dict in dictOptions"
             :key="dict.id"
             :label="dict.name"
             :value="dict.type"
           />
-        </el-select>
+        </vxe-select>
       </template>
     </vxe-column>
     <vxe-column title="示例" field="example">
       <template #default="{ row }">
-        <el-input v-model="row.example" />
+        <vxe-input v-model="row.example" placeholder="请输入示例" />
       </template>
     </vxe-column>
   </vxe-table>
 </template>
 <script setup lang="ts">
 import { onMounted, PropType, ref } from 'vue'
-import { ElInput, ElSelect, ElOption } from 'element-plus'
 import { DictTypeVO } from '@/api/system/dict/types'
 import { CodegenColumnVO } from '@/api/infra/codegen/types'
 import { listSimpleDictTypeApi } from '@/api/system/dict/dict.type'

+ 0 - 135
yudao-ui-admin-vue3/src/views/infra/codegen/components/GenInfoForm.vue

@@ -1,135 +0,0 @@
-<template>
-  <Form :rules="rules" @register="register" />
-</template>
-<script setup lang="ts">
-import { onMounted, PropType, reactive, ref, watch } from 'vue'
-import { Form } from '@/components/Form'
-import { useForm } from '@/hooks/web/useForm'
-import { required } from '@/utils/formRules'
-import { handleTree, defaultProps } from '@/utils/tree'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { listSimpleMenusApi } from '@/api/system/menu'
-import { CodegenTableVO } from '@/api/infra/codegen/types'
-import { FormSchema } from '@/types/form'
-const props = defineProps({
-  genInfo: {
-    type: Object as PropType<Nullable<CodegenTableVO>>,
-    default: () => null
-  }
-})
-const rules = reactive({
-  templateType: [required],
-  scene: [required],
-  moduleName: [required],
-  businessName: [required],
-  businessPackage: [required],
-  className: [required],
-  classComment: [required]
-})
-const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)
-const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)
-const menuOptions = ref<any>([]) // 树形结构
-const getTree = async () => {
-  const res = await listSimpleMenusApi()
-  menuOptions.value = handleTree(res)
-}
-const schema = reactive<FormSchema[]>([
-  {
-    label: '生成模板',
-    field: 'templateType',
-    component: 'Select',
-    componentProps: {
-      options: templateTypeOptions
-    },
-    colProps: {
-      span: 12
-    }
-  },
-  {
-    label: '生成场景',
-    field: 'scene',
-    component: 'Select',
-    componentProps: {
-      options: sceneOptions
-    },
-    colProps: {
-      span: 12
-    }
-  },
-  {
-    label: '模块名',
-    field: 'moduleName',
-    component: 'Input',
-    labelMessage: '模块名,即一级目录,例如 system、infra、tool 等等',
-    colProps: {
-      span: 12
-    }
-  },
-  {
-    label: '业务名',
-    field: 'businessName',
-    component: 'Input',
-    labelMessage: '业务名,即二级目录,例如 user、permission、dict 等等',
-    colProps: {
-      span: 12
-    }
-  },
-  {
-    label: '类名称',
-    field: 'className',
-    component: 'Input',
-    labelMessage: '类名称(首字母大写),例如SysUser、SysMenu、SysDictData 等等',
-    colProps: {
-      span: 12
-    }
-  },
-  {
-    label: '类描述',
-    field: 'classComment',
-    component: 'Input',
-    labelMessage: '用作类描述,例如 用户',
-    colProps: {
-      span: 12
-    }
-  },
-  {
-    label: '上级菜单',
-    field: 'parentMenuId',
-    component: 'TreeSelect',
-    componentProps: {
-      data: menuOptions,
-      props: defaultProps,
-      checkStrictly: true,
-      nodeKey: 'id'
-    },
-    labelMessage: '分配到指定菜单下,例如 系统管理',
-    colProps: {
-      span: 12
-    }
-  }
-])
-const { register, methods, elFormRef } = useForm({
-  schema
-})
-
-// ========== 初始化 ==========
-onMounted(async () => {
-  await getTree()
-})
-watch(
-  () => props.genInfo,
-  (genInfo) => {
-    if (!genInfo) return
-    const { setValues } = methods
-    setValues(genInfo)
-  },
-  {
-    deep: true,
-    immediate: true
-  }
-)
-defineExpose({
-  elFormRef,
-  getFormData: methods.getFormData
-})
-</script>

+ 1 - 1
yudao-ui-admin-vue3/src/views/infra/codegen/components/Preview.vue

@@ -13,7 +13,7 @@
           />
         </el-scrollbar>
       </el-card>
-      <el-card class="w-3/4" style="margin-left: 10px" :gutter="12" shadow="hover">
+      <el-card class="w-3/4 ml-3" :gutter="12" shadow="hover">
         <el-tabs v-model="preview.activeName">
           <el-tab-pane
             v-for="item in previewCodegen"

+ 1 - 2
yudao-ui-admin-vue3/src/views/infra/codegen/components/index.ts

@@ -1,6 +1,5 @@
 import BasicInfoForm from './BasicInfoForm.vue'
 import CloumInfoForm from './CloumInfoForm.vue'
-import GenInfoForm from './GenInfoForm.vue'
 import ImportTable from './ImportTable.vue'
 import Preview from './Preview.vue'
-export { BasicInfoForm, CloumInfoForm, GenInfoForm, ImportTable, Preview }
+export { BasicInfoForm, CloumInfoForm, ImportTable, Preview }

+ 3 - 10
yudao-ui-admin-vue3/src/views/infra/codegen/index.vue

@@ -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
@@ -52,7 +52,7 @@
     </XTable>
   </ContentWrap>
   <!-- 弹窗:导入表 -->
-  <ImportTable ref="importRef" @ok="handleQuery()" />
+  <ImportTable ref="importRef" @ok="reload()" />
   <!-- 弹窗:预览代码 -->
   <Preview ref="previewRef" />
 </template>
@@ -103,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(rowId)
-}
-// 查询操作
-const handleQuery = async () => {
-  await reload()
-}
 </script>

+ 2 - 12
yudao-ui-admin-vue3/src/views/infra/config/index.vue

@@ -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,7 +43,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['infra:config:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
     </XTable>
@@ -123,11 +123,6 @@ const handleCreate = () => {
   setDialogTile('create')
 }
 
-// 导出操作
-const handleExport = async () => {
-  await exportList('配置.xls')
-}
-
 // 修改操作
 const handleUpdate = async (rowId: number) => {
   setDialogTile('update')
@@ -143,11 +138,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()

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

@@ -31,7 +31,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['infra:data-source-config:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
     </XTable>
@@ -121,11 +121,6 @@ const handleDetail = async (rowId: number) => {
   setDialogTile('detail')
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()

+ 1 - 6
yudao-ui-admin-vue3/src/views/infra/fileConfig/index.vue

@@ -41,7 +41,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['infra:file-config:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
     </XTable>
@@ -283,11 +283,6 @@ const handleTest = async (rowId: number) => {
   message.alert('测试通过,上传文件成功!访问地址:' + res)
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(rowId)
-}
-
 // 提交按钮
 const submitForm = async (formEl: FormInstance | undefined) => {
   if (!formEl) return

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

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

@@ -21,7 +21,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['infra:file:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
     </XTable>
@@ -162,11 +162,6 @@ const handleDetail = (row: FileApi.FileVO) => {
   dialogVisible.value = true
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(rowId)
-}
-
 // ========== 复制相关 ==========
 const handleCopy = async (text: string) => {
   const { copy, copied, isSupported } = useClipboard({ source: text })

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

@@ -8,7 +8,7 @@
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['infra:job:export']"
-          @click="handleExport()"
+          @click="exportList('定时任务详情.xls')"
         />
       </template>
       <template #beginTime_default="{ row }">
@@ -77,8 +77,4 @@ const handleDetail = async (row: JobLogApi.JobLogVO) => {
   dialogTitle.value = t('action.detail')
   dialogVisible.value = true
 }
-// 导出操作
-const handleExport = async () => {
-  await exportList('定时任务详情.xls')
-}
 </script>

+ 4 - 13
yudao-ui-admin-vue3/src/views/infra/job/index.vue

@@ -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" />
@@ -179,11 +179,6 @@ const handleCreate = () => {
   setDialogTile('create')
 }
 
-// 导出操作
-const handleExport = async () => {
-  await exportList('定时任务.xls')
-}
-
 // 修改操作
 const handleUpdate = async (rowId: number) => {
   setDialogTile('update')
@@ -248,10 +243,6 @@ const parseTime = (time) => {
   return time_str
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(rowId)
-}
 const handleChangeStatus = async (row: JobApi.JobVO) => {
   const text = row.status === InfraJobStatusEnum.STOP ? '开启' : '关闭'
   const status =
@@ -275,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 {

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

+ 2 - 12
yudao-ui-admin-vue3/src/views/pay/app/index.vue

@@ -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,7 +40,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['pay:app:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
     </XTable>
@@ -115,11 +115,6 @@ const handleCreate = () => {
   setDialogTile('create')
 }
 
-// 导出操作
-const handleExport = async () => {
-  await exportList('应用信息.xls')
-}
-
 // 修改操作
 const handleUpdate = async (rowId: number) => {
   setDialogTile('update')
@@ -135,11 +130,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()

+ 2 - 12
yudao-ui-admin-vue3/src/views/pay/merchant/index.vue

@@ -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,7 +40,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['pay:merchant:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
     </XTable>
@@ -113,11 +113,6 @@ const handleCreate = () => {
   setDialogTile('create')
 }
 
-// 导出操作
-const handleExport = async () => {
-  await exportList('商户列表.xls')
-}
-
 // 修改操作
 const handleUpdate = async (rowId: number) => {
   setDialogTile('update')
@@ -133,11 +128,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()

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

@@ -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 }">
@@ -72,10 +72,6 @@ const setDialogTile = (type: string) => {
 const handleCreate = () => {
   setDialogTile('create')
 }
-// 导出操作
-const handleExport = async () => {
-  await exportList('订单数据.xls')
-}
 
 // 详情操作
 const handleDetail = async (rowId: number) => {

+ 1 - 6
yudao-ui-admin-vue3/src/views/pay/refund/index.vue

@@ -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 }">
@@ -49,11 +49,6 @@ const [registerTable, { exportList }] = useXTable({
   exportListApi: RefundApi.exportRefundApi
 })
 
-// 导出操作
-const handleExport = async () => {
-  await exportList('退款订单.xls')
-}
-
 // ========== CRUD 相关 ==========
 const dialogVisible = ref(false) // 是否显示弹出层
 const detailData = ref() // 详情 Ref

+ 6 - 11
yudao-ui-admin-vue3/src/views/system/dept/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <XTable @register="registerTable" show-overflow>
+    <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,7 +30,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:dept:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
     </XTable>
@@ -77,7 +77,6 @@
 <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'
@@ -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',
@@ -168,17 +167,13 @@ const submitForm = async () => {
         dialogVisible.value = false
       } finally {
         actionLoading.value = false
+        await getTree()
         await reload()
       }
     }
   })
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(rowId)
-}
-
 const userNicknameFormat = (row) => {
   if (!row || !row.leaderUserId) {
     return '未设置'

+ 3 - 12
yudao-ui-admin-vue3/src/views/system/dict/index.vue

@@ -31,14 +31,14 @@
             preIcon="ep:delete"
             :title="t('action.del')"
             v-hasPermi="['system:dict:delete']"
-            @click="handleTypeDelete(row.id)"
+            @click="typeDeleteData(row.id)"
           />
         </template>
       </XTable>
       <!-- @星语:分页和列表重叠在一起了 -->
     </el-card>
     <!-- ====== 字典数据 ====== -->
-    <el-card class="w-1/2 dict" style="margin-left: 10px" :gutter="12" shadow="hover">
+    <el-card class="w-1/2 dict ml-3" :gutter="12" shadow="hover">
       <template #header>
         <div class="card-header">
           <span>字典数据</span>
@@ -74,7 +74,7 @@
               v-hasPermi="['system:dict:delete']"
               preIcon="ep:delete"
               :title="t('action.del')"
-              @click="handleDataDelete(row.id)"
+              @click="dataDeleteData(row.id)"
             />
           </template>
         </XTable>
@@ -202,15 +202,6 @@ const setDialogTile = (type: string) => {
   dialogVisible.value = true
 }
 
-// 删除操作
-const handleTypeDelete = async (rowId: number) => {
-  await typeDeleteData(rowId)
-}
-
-const handleDataDelete = async (rowId: number) => {
-  await dataDeleteData(rowId)
-}
-
 // 提交按钮
 const submitTypeForm = async () => {
   const elForm = unref(typeFormRef)?.getElFormRef()

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

@@ -32,7 +32,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:error-code:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
     </XTable>
@@ -121,11 +121,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(rowId)
-}
-
 // 提交新增/修改的表单
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()

+ 1 - 6
yudao-ui-admin-vue3/src/views/system/loginlog/index.vue

@@ -8,7 +8,7 @@
           type="warning"
           preIcon="ep:download"
           :title="t('action.export')"
-          @click="handleExport()"
+          @click="exportList('登录列表.xls')"
         />
       </template>
       <template #actionbtns_default="{ row }">
@@ -54,9 +54,4 @@ const handleDetail = async (row: LoginLogVO) => {
   detailData.value = row
   dialogVisible.value = true
 }
-
-// 导出操作
-const handleExport = async () => {
-  await exportList('登录列表.xls')
-}
 </script>

+ 7 - 13
yudao-ui-admin-vue3/src/views/system/menu/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <XTable @register="registerTable" show-overflow>
+    <XTable ref="xGrid" @register="registerTable" show-overflow>
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -11,8 +11,8 @@
           v-hasPermi="['system:menu: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 #name_default="{ row }">
         <Icon :icon="row.icon" />
@@ -31,7 +31,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:menu:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
     </XTable>
@@ -194,7 +194,6 @@ import {
 } from 'element-plus'
 import { Tooltip } from '@/components/Tooltip'
 import { IconSelect } from '@/components/Icon'
-import { VxeGridInstance } from 'vxe-table'
 // 业务相关的 import
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { SystemMenuTypeEnum, CommonStatusEnum } from '@/utils/constants'
@@ -206,9 +205,10 @@ import { useXTable } from '@/hooks/web/useXTable'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 const { wsCache } = useCache()
+
+const xGrid = ref<any>(null)
+
 // 列表相关的变量
-// 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
 const treeConfig = {
   transform: true,
   rowField: 'id',
@@ -334,10 +334,4 @@ const submitForm = async () => {
 const isExternal = (path: string) => {
   return /^(https?:|mailto:|tel:)/.test(path)
 }
-
-// ========== 删除 ==========
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(rowId)
-}
 </script>

+ 1 - 6
yudao-ui-admin-vue3/src/views/system/notice/index.vue

@@ -32,7 +32,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:notice:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
     </XTable>
@@ -126,11 +126,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(rowId)
-}
-
 // 提交新增/修改的表单
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()

+ 1 - 6
yudao-ui-admin-vue3/src/views/system/oauth2/client/index.vue

@@ -48,7 +48,7 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:oauth2-client:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
     </XTable>
@@ -184,11 +184,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(rowId)
-}
-
 // 提交新增/修改的表单
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()

+ 1 - 1
yudao-ui-admin-vue3/src/views/system/oauth2/token/index.vue

@@ -5,7 +5,7 @@
       <template #actionbtns_default="{ row }">
         <!-- 操作:详情 -->
         <XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
-        <!-- 操作:删除 -->
+        <!-- 操作:登出 -->
         <XTextButton
           preIcon="ep:delete"
           :title="t('action.logout')"

+ 1 - 6
yudao-ui-admin-vue3/src/views/system/operatelog/index.vue

@@ -9,7 +9,7 @@
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['system:operate-log:export']"
-          @click="handleExport()"
+          @click="exportList('操作日志.xls')"
         />
       </template>
       <template #duration="{ row }">
@@ -68,9 +68,4 @@ const handleDetail = (row: OperateLogApi.OperateLogVO) => {
   detailData.value = row
   dialogVisible.value = true
 }
-
-// 导出操作
-const handleExport = async () => {
-  await exportList('操作日志.xls')
-}
 </script>

Некоторые файлы не были показаны из-за большого количества измененных файлов