Kaynağa Gözat

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

# Conflicts:
#	yudao-server/src/main/resources/application.yaml
YunaiV 2 yıl önce
ebeveyn
işleme
b9246d1543
96 değiştirilmiş dosya ile 2379 ekleme ve 860 silme
  1. 6 6
      README.md
  2. 1 1
      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. 17 11
      yudao-dependencies/pom.xml
  7. 2 2
      yudao-example/yudao-sso-demo-by-code/pom.xml
  8. 2 2
      yudao-example/yudao-sso-demo-by-password/pom.xml
  9. 1 0
      yudao-framework/pom.xml
  10. 1 1
      yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml
  11. 7 1
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java
  12. 1 0
      yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientConfig.java
  13. 2 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
  14. 37 0
      yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
  15. 14 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java
  16. 29 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java
  17. 34 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
  18. 24 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java
  19. 9 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java
  20. 24 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java
  21. 36 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java
  22. 31 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java
  23. 49 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java
  24. 1 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java
  25. 1 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  26. 6 0
      yudao-module-infra/yudao-module-infra-biz/pom.xml
  27. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/ConfigController.java
  28. 8 3
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentDAOImpl.java
  29. 45 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/SemaphoreUtils.java
  30. 16 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketConfig.java
  31. 86 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketServer.java
  32. 178 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketUsers.java
  33. 7 19
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm
  34. 3 2
      yudao-module-visualization/yudao-module-visualization-biz/src/main/java/cn/iocoder/yudao/module/visualization/framework/jmreport/config/JmReportConfiguration.java
  35. 72 18
      yudao-module-visualization/yudao-module-visualization-biz/src/main/java/cn/iocoder/yudao/module/visualization/framework/jmreport/core/service/JmReportTokenServiceImpl.java
  36. 1 1
      yudao-server/pom.xml
  37. 5 0
      yudao-server/src/main/resources/application.yaml
  38. 11 11
      yudao-ui-admin-vue3/README.md
  39. 17 17
      yudao-ui-admin-vue3/package.json
  40. 525 149
      yudao-ui-admin-vue3/pnpm-lock.yaml
  41. 0 1
      yudao-ui-admin-vue3/src/components/Crontab/src/Crontab.vue
  42. 3 0
      yudao-ui-admin-vue3/src/components/XTable/index.ts
  43. 335 0
      yudao-ui-admin-vue3/src/components/XTable/src/XTable.vue
  44. 81 0
      yudao-ui-admin-vue3/src/components/XTable/src/style/dark.scss
  45. 6 0
      yudao-ui-admin-vue3/src/components/XTable/src/style/index.scss
  46. 16 0
      yudao-ui-admin-vue3/src/components/XTable/src/style/light.scss
  47. 25 0
      yudao-ui-admin-vue3/src/components/XTable/src/type.ts
  48. 2 0
      yudao-ui-admin-vue3/src/components/index.ts
  49. 1 1
      yudao-ui-admin-vue3/src/hooks/web/useVxeCrudSchemas.ts
  50. 32 0
      yudao-ui-admin-vue3/src/hooks/web/useXTable.ts
  51. 4 2
      yudao-ui-admin-vue3/src/main.ts
  52. 1 18
      yudao-ui-admin-vue3/src/plugins/vxeTable/index.ts
  53. 1 0
      yudao-ui-admin-vue3/src/plugins/vxeTable/renderer/index.tsx
  54. 34 0
      yudao-ui-admin-vue3/src/plugins/vxeTable/renderer/preview.tsx
  55. 2 8
      yudao-ui-admin-vue3/src/router/index.ts
  56. 36 26
      yudao-ui-admin-vue3/src/store/modules/dict.ts
  57. 17 32
      yudao-ui-admin-vue3/src/utils/auth.ts
  58. 15 41
      yudao-ui-admin-vue3/src/views/Login/components/LoginForm.vue
  59. 5 6
      yudao-ui-admin-vue3/src/views/infra/apiAccessLog/index.vue
  60. 9 13
      yudao-ui-admin-vue3/src/views/infra/apiErrorLog/index.vue
  61. 7 16
      yudao-ui-admin-vue3/src/views/infra/codegen/index.vue
  62. 7 19
      yudao-ui-admin-vue3/src/views/infra/config/index.vue
  63. 6 13
      yudao-ui-admin-vue3/src/views/infra/dataSourceConfig/index.vue
  64. 7 14
      yudao-ui-admin-vue3/src/views/infra/fileConfig/index.vue
  65. 1 1
      yudao-ui-admin-vue3/src/views/infra/fileList/fileList.data.ts
  66. 6 13
      yudao-ui-admin-vue3/src/views/infra/fileList/index.vue
  67. 5 11
      yudao-ui-admin-vue3/src/views/infra/job/JobLog.vue
  68. 11 22
      yudao-ui-admin-vue3/src/views/infra/job/index.vue
  69. 118 0
      yudao-ui-admin-vue3/src/views/infra/webSocket/index.vue
  70. 7 19
      yudao-ui-admin-vue3/src/views/pay/app/index.vue
  71. 7 19
      yudao-ui-admin-vue3/src/views/pay/merchant/index.vue
  72. 5 11
      yudao-ui-admin-vue3/src/views/pay/order/index.vue
  73. 5 12
      yudao-ui-admin-vue3/src/views/pay/refund/index.vue
  74. 9 15
      yudao-ui-admin-vue3/src/views/system/dept/index.vue
  75. 14 38
      yudao-ui-admin-vue3/src/views/system/dict/index.vue
  76. 6 13
      yudao-ui-admin-vue3/src/views/system/errorCode/index.vue
  77. 5 12
      yudao-ui-admin-vue3/src/views/system/loginlog/index.vue
  78. 11 17
      yudao-ui-admin-vue3/src/views/system/menu/index.vue
  79. 6 13
      yudao-ui-admin-vue3/src/views/system/notice/index.vue
  80. 6 13
      yudao-ui-admin-vue3/src/views/system/oauth2/client/index.vue
  81. 6 8
      yudao-ui-admin-vue3/src/views/system/oauth2/token/index.vue
  82. 5 12
      yudao-ui-admin-vue3/src/views/system/operatelog/index.vue
  83. 7 19
      yudao-ui-admin-vue3/src/views/system/post/index.vue
  84. 6 13
      yudao-ui-admin-vue3/src/views/system/role/index.vue
  85. 12 25
      yudao-ui-admin-vue3/src/views/system/sensitiveWord/index.vue
  86. 6 13
      yudao-ui-admin-vue3/src/views/system/sms/smsChannel/index.vue
  87. 5 12
      yudao-ui-admin-vue3/src/views/system/sms/smsLog/index.vue
  88. 6 13
      yudao-ui-admin-vue3/src/views/system/sms/smsTemplate/index.vue
  89. 7 19
      yudao-ui-admin-vue3/src/views/system/tenant/index.vue
  90. 10 14
      yudao-ui-admin-vue3/src/views/system/tenantPackage/index.vue
  91. 11 16
      yudao-ui-admin-vue3/src/views/system/user/index.vue
  92. 1 1
      yudao-ui-admin/package.json
  93. 14 8
      yudao-ui-admin/src/views/infra/file/index.vue
  94. 2 2
      yudao-ui-admin/src/views/infra/redis/index.vue
  95. 92 0
      yudao-ui-admin/src/views/infra/webSocket/index.vue
  96. 1 1
      yudao-ui-app/utils/request/responseInterceptors.js

+ 6 - 6
README.md

@@ -188,17 +188,17 @@ ps:核心功能已经实现,正在对接微信小程序中...
 
 | 框架                                                                                          | 说明               | 版本          | 学习指南                                                           |
 |---------------------------------------------------------------------------------------------|------------------|-------------|----------------------------------------------------------------|
-| [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架           | 2.7.6       | [文档](https://github.com/YunaiV/SpringBoot-Labs)                |
+| [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.2       | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao)         |
-| [Dynamic Datasource](https://dynamic-datasource.com/)                                       | 动态数据源            | 3.6.0       | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
+| [MyBatis Plus](https://mp.baomidou.com/)                                                    | MyBatis 增强工具包    | 3.5.3       | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao)         |
+| [Dynamic Datasource](https://dynamic-datasource.com/)                                       | 动态数据源            | 3.6.1       | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
 | [Redis](https://redis.io/)                                                                  | key-value 数据库    | 5.0 / 6.0   |                                                                |
 | [Redisson](https://github.com/redisson/redisson)                                            | Redis 客户端        | 3.18.0      | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao)           |
 | [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架           | 5.3.24      | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao)               |
 | [Spring Security](https://github.com/spring-projects/spring-security)                       | Spring 安全框架      | 5.7.5       | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
 | [Hibernate Validator](https://github.com/hibernate/hibernate-validator)                     | 参数校验组件           | 6.2.5       | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao)      |
-| [Flowable](https://github.com/flowable/flowable-engine)                                     | 工作流引擎            | 6.7.2       | [文档](https://doc.iocoder.cn/bpm/)                              |
+| [Flowable](https://github.com/flowable/flowable-engine)                                     | 工作流引擎            | 6.8.0       | [文档](https://doc.iocoder.cn/bpm/)                              |
 | [Quartz](https://github.com/quartz-scheduler)                                               | 任务调度组件           | 2.3.2       | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao)             |
 | [Knife4j](https://gitee.com/xiaoym/knife4j)                                                 | Swagger 增强 UI 实现 | 3.0.3       | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao)         |
 | [Resilience4j](https://github.com/resilience4j/resilience4j)                                | 服务保障组件           | 1.7.1       | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao)    |
@@ -222,8 +222,8 @@ ps:核心功能已经实现,正在对接微信小程序中...
 | 框架                                                                   |      说明      |   版本   |
 |----------------------------------------------------------------------|:------------:|:------:|
 | [Vue](https://staging-cn.vuejs.org/)                                 |    Vue 框架    | 3.2.45 |
-| [Vite](https://cn.vitejs.dev//)                                      |   开发与构建工具    | 4.0.3  |
-| [Element Plus](https://element-plus.org/zh-CN/)                      | Element Plus | 2.2.27 |
+| [Vite](https://cn.vitejs.dev//)                                      |   开发与构建工具    | 4.0.4  |
+| [Element Plus](https://element-plus.org/zh-CN/)                      | Element Plus | 2.2.28 |
 | [TypeScript](https://www.typescriptlang.org/docs/)                   |  TypeScript  | 4.9.4  |
 | [pinia](https://pinia.vuejs.org/)                                    |    vuex5     | 2.0.28 |
 | [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) |     国际化      | 9.2.2  |

+ 1 - 1
pom.xml

@@ -30,7 +30,7 @@
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <properties>
-        <revision>1.6.5-snapshot</revision>
+        <revision>1.6.6-snapshot</revision>
         <!-- Maven 相关 -->
         <java.version>1.8</java.version>
         <maven.compiler.source>${java.version}</maven.compiler.source>

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

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

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

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

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

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

+ 17 - 11
yudao-dependencies/pom.xml

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

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

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

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

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

+ 1 - 0
yudao-framework/pom.xml

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 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

@@ -111,7 +111,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

@@ -93,6 +93,11 @@ yudao:
     permit-all_urls:
       - /admin-ui/** # /resources/admin-ui 目录下的静态资源
       - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录
+  websocket:
+    enable: true # websocket的开关
+    path: /websocket/message # 路径
+    maxOnlineCount: 0 # 最大连接人数
+    sessionMap: true # 保存sessionMap
   swagger:
     title: 管理后台
     description: 提供管理员管理的所有功能

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

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

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

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

Dosya farkı çok büyük olduğundan ihmal edildi
+ 525 - 149
yudao-ui-admin-vue3/pnpm-lock.yaml


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 5 - 12
yudao-ui-admin-vue3/src/views/system/loginlog/index.vue

@@ -1,21 +1,21 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <!-- 操作:导出 -->
       <template #toolbar_buttons>
         <XButton
           type="warning"
           preIcon="ep:download"
           :title="t('action.export')"
-          @click="handleExport()"
+          @click="exportList('登录列表.xls')"
         />
       </template>
       <template #actionbtns_default="{ row }">
         <!-- 操作:详情 -->
         <XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <!-- 弹窗 -->
   <XModal id="postModel" v-model="dialogVisible" :title="dialogTitle">
@@ -31,16 +31,14 @@
 // 全局相关的 import
 import { ref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 // 业务相关的 import
 import { allSchemas } from './loginLog.data'
 import { getLoginLogPageApi, exportLoginLogApi, LoginLogVO } from '@/api/system/loginLog'
 
 const { t } = useI18n() // 国际化
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, exportList } = useVxeGrid<LoginLogVO>({
+const [registerTable, { exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: getLoginLogPageApi,
   exportListApi: exportLoginLogApi
@@ -56,9 +54,4 @@ const handleDetail = async (row: LoginLogVO) => {
   detailData.value = row
   dialogVisible.value = true
 }
-
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '登录列表.xls')
-}
 </script>

+ 11 - 17
yudao-ui-admin-vue3/src/views/system/menu/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" show-overflow class="xtable-scrollbar">
+    <XTable ref="xGrid" @register="registerTable" show-overflow>
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -11,8 +11,8 @@
           v-hasPermi="['system: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,10 +31,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:menu:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <!-- 添加或修改菜单对话框 -->
   <XModal id="menuModel" v-model="dialogVisible" :title="dialogTitle">
@@ -194,28 +194,28 @@ 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'
 import { handleTree, defaultProps } from '@/utils/tree'
 import * as MenuApi from '@/api/system/menu'
 import { allSchemas, rules } from './menu.data'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
+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',
   parentField: 'parentId',
   expandAll: false
 }
-const { gridOptions, getList, deleteData } = useVxeGrid<MenuApi.MenuVO>({
+const [registerTable, { reload, deleteData }] = useXTable({
   allSchemas: allSchemas,
   treeConfig: treeConfig,
   getListApi: MenuApi.getMenuListApi,
@@ -326,7 +326,7 @@ const submitForm = async () => {
     actionLoading.value = false
     wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
     // 操作成功,重新加载列表
-    await getList(xGrid)
+    await reload()
   }
 }
 
@@ -334,10 +334,4 @@ const submitForm = async () => {
 const isExternal = (path: string) => {
   return /^(https?:|mailto:|tel:)/.test(path)
 }
-
-// ========== 删除 ==========
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
 </script>

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

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <!-- 操作:新增 -->
       <template #toolbar_buttons>
         <XButton
@@ -32,10 +32,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:notice:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <!-- 弹窗 -->
   <XModal id="noticeModel" v-model="dialogVisible" :title="dialogTitle">
@@ -75,8 +75,7 @@
 import { ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 // 业务相关的 import
 import * as NoticeApi from '@/api/system/notice'
@@ -86,8 +85,7 @@ import { Editor } from '@/components/Editor'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData } = useVxeGrid<NoticeApi.NoticeVO>({
+const [registerTable, { reload, deleteData }] = useXTable({
   allSchemas: allSchemas,
   getListApi: NoticeApi.getNoticePageApi,
   deleteApi: NoticeApi.deleteNoticeApi
@@ -128,11 +126,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交新增/修改的表单
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -153,7 +146,7 @@ const submitForm = async () => {
         dialogVisible.value = false
       } finally {
         actionLoading.value = false
-        await getList(xGrid)
+        await reload()
       }
     }
   })

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

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -48,10 +48,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:oauth2-client:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <!-- 弹窗 -->
   <XModal id="postModel" v-model="dialogVisible" :title="dialogTitle">
@@ -135,8 +135,7 @@ import { ref, unref } from 'vue'
 import { ElTag } from 'element-plus'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 // 业务相关的 import
 import * as ClientApi from '@/api/system/oauth2/client'
@@ -146,8 +145,7 @@ const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData } = useVxeGrid<ClientApi.OAuth2ClientVO>({
+const [registerTable, { reload, deleteData }] = useXTable({
   allSchemas: allSchemas,
   getListApi: ClientApi.getOAuth2ClientPageApi,
   deleteApi: ClientApi.deleteOAuth2ClientApi
@@ -186,11 +184,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交新增/修改的表单
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -212,7 +205,7 @@ const submitForm = async () => {
       } finally {
         actionLoading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

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

@@ -1,11 +1,11 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #actionbtns_default="{ row }">
         <!-- 操作:详情 -->
         <XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
-        <!-- 操作:删除 -->
+        <!-- 操作:登出 -->
         <XTextButton
           preIcon="ep:delete"
           :title="t('action.logout')"
@@ -13,7 +13,7 @@
           @click="handleForceLogout(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <XModal v-model="dialogVisible" :title="dialogTitle">
     <!-- 对话框(详情) -->
@@ -28,8 +28,7 @@
 import { ref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 
 import { allSchemas } from './token.data'
 import * as TokenApi from '@/api/system/oauth2/token'
@@ -37,8 +36,7 @@ import * as TokenApi from '@/api/system/oauth2/token'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList } = useVxeGrid<TokenApi.OAuth2TokenVO>({
+const [registerTable, { reload }] = useXTable({
   allSchemas: allSchemas,
   topActionSlots: false,
   getListApi: TokenApi.getAccessTokenPageApi
@@ -65,7 +63,7 @@ const handleForceLogout = (rowId: number) => {
     })
     .finally(async () => {
       // 刷新列表
-      await getList(xGrid)
+      await reload()
     })
 }
 </script>

+ 5 - 12
yudao-ui-admin-vue3/src/views/system/operatelog/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -9,7 +9,7 @@
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['system:operate-log:export']"
-          @click="handleExport()"
+          @click="exportList('操作日志.xls')"
         />
       </template>
       <template #duration="{ row }">
@@ -22,7 +22,7 @@
         <!-- 操作:详情 -->
         <XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <!-- 弹窗 -->
   <XModal id="postModel" v-model="dialogVisible" :title="t('action.detail')">
@@ -45,16 +45,14 @@
 // 全局相关的 import
 import { ref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 // 业务相关的 import
 import * as OperateLogApi from '@/api/system/operatelog'
 import { allSchemas } from './operatelog.data'
 
 const { t } = useI18n() // 国际化
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, exportList } = useVxeGrid<OperateLogApi.OperateLogVO>({
+const [registerTable, { exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: OperateLogApi.getOperateLogPageApi,
   exportListApi: OperateLogApi.exportOperateLogApi
@@ -70,9 +68,4 @@ const handleDetail = (row: OperateLogApi.OperateLogVO) => {
   detailData.value = row
   dialogVisible.value = true
 }
-
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '操作日志.xls')
-}
 </script>

+ 7 - 19
yudao-ui-admin-vue3/src/views/system/post/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -17,7 +17,7 @@
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['system:post:export']"
-          @click="handleExport()"
+          @click="exportList('岗位列表.xls')"
         />
       </template>
       <template #actionbtns_default="{ row }">
@@ -40,10 +40,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:post:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <!-- 弹窗 -->
   <XModal id="postModel" :loading="modelLoading" v-model="modelVisible" :title="modelTitle">
@@ -79,8 +79,7 @@
 import { ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 // 业务相关的 import
 import * as PostApi from '@/api/system/post'
@@ -89,8 +88,7 @@ import { rules, allSchemas } from './post.data'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData, exportList } = useVxeGrid<PostApi.PostVO>({
+const [registerTable, { reload, deleteData, exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: PostApi.getPostPageApi,
   deleteApi: PostApi.deletePostApi,
@@ -119,11 +117,6 @@ const handleCreate = () => {
   modelLoading.value = false
 }
 
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '岗位列表.xls')
-}
-
 // 修改操作
 const handleUpdate = async (rowId: number) => {
   setDialogTile('update')
@@ -141,11 +134,6 @@ const handleDetail = async (rowId: number) => {
   modelLoading.value = false
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交新增/修改的表单
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -167,7 +155,7 @@ const submitForm = async () => {
       } finally {
         actionLoading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

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

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <!-- 操作:新增 -->
       <template #toolbar_buttons>
         <XButton
@@ -46,10 +46,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:role:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
 
   <XModal v-model="dialogVisible" :title="dialogTitle">
@@ -159,11 +159,10 @@ import {
   ElSwitch,
   ElTag
 } from 'element-plus'
-import { VxeGridInstance } from 'vxe-table'
 import { FormExpose } from '@/components/Form'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
+import { useXTable } from '@/hooks/web/useXTable'
 import { handleTree, defaultProps } from '@/utils/tree'
 import { SystemDataScopeEnum } from '@/utils/constants'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@@ -176,8 +175,7 @@ import * as PermissionApi from '@/api/system/permission'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData } = useVxeGrid<RoleApi.RoleVO>({
+const [registerTable, { reload, deleteData }] = useXTable({
   allSchemas: allSchemas,
   getListApi: RoleApi.getRolePageApi,
   deleteApi: RoleApi.deleteRoleApi
@@ -219,11 +217,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -245,7 +238,7 @@ const submitForm = async () => {
       } finally {
         actionLoading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

+ 12 - 25
yudao-ui-admin-vue3/src/views/system/sensitiveWord/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -17,7 +17,7 @@
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['system:sensitive-word:export']"
-          @click="handleExport()"
+          @click="exportList('敏感词数据.xls')"
         />
       </template>
       <template #tags_default="{ row }">
@@ -50,10 +50,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:sensitive-word:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
 
   <XModal v-model="dialogVisible" :title="dialogTitle">
@@ -106,8 +106,7 @@
 import { onMounted, ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 import { ElTag, ElSelect, ElOption } from 'element-plus'
 import * as SensitiveWordApi from '@/api/system/sensitiveWord'
@@ -116,14 +115,12 @@ import { rules, allSchemas } from './sensitiveWord.data'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData, exportList } =
-  useVxeGrid<SensitiveWordApi.SensitiveWordVO>({
-    allSchemas: allSchemas,
-    getListApi: SensitiveWordApi.getSensitiveWordPageApi,
-    deleteApi: SensitiveWordApi.deleteSensitiveWordApi,
-    exportListApi: SensitiveWordApi.exportSensitiveWordApi
-  })
+const [registerTable, { reload, deleteData, exportList }] = useXTable({
+  allSchemas: allSchemas,
+  getListApi: SensitiveWordApi.getSensitiveWordPageApi,
+  deleteApi: SensitiveWordApi.deleteSensitiveWordApi,
+  exportListApi: SensitiveWordApi.exportSensitiveWordApi
+})
 const actionLoading = ref(false) // 遮罩层
 const actionType = ref('') // 操作按钮的类型
 const dialogVisible = ref(false) // 是否显示弹出层
@@ -150,11 +147,6 @@ const handleCreate = () => {
   setDialogTile('create')
 }
 
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '敏感词数据.xls')
-}
-
 // 修改操作
 const handleUpdate = async (rowId: number) => {
   setDialogTile('update')
@@ -170,11 +162,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -196,7 +183,7 @@ const submitForm = async () => {
       } finally {
         actionLoading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

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

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <!-- 操作:新增 -->
       <template #toolbar_buttons>
         <XButton
@@ -32,10 +32,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:sms-channel:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
 
   <XModal id="smsChannel" v-model="dialogVisible" :title="dialogTitle">
@@ -72,8 +72,7 @@
 import { ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 // 业务相关的 import
 import * as SmsChannelApi from '@/api/system/sms/smsChannel'
@@ -83,8 +82,7 @@ const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData } = useVxeGrid<SmsChannelApi.SmsChannelVO>({
+const [registerTable, { reload, deleteData }] = useXTable({
   allSchemas: allSchemas,
   getListApi: SmsChannelApi.getSmsChannelPageApi,
   deleteApi: SmsChannelApi.deleteSmsChannelApi
@@ -125,11 +123,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -151,7 +144,7 @@ const submitForm = async () => {
       } finally {
         actionLoading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

+ 5 - 12
yudao-ui-admin-vue3/src/views/system/sms/smsLog/index.vue

@@ -1,20 +1,20 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <!-- 操作:导出 -->
       <template #toolbar_buttons>
         <XButton
           type="warning"
           preIcon="ep:download"
           :title="t('action.export')"
-          @click="handleExport()"
+          @click="exportList('短信日志.xls')"
         />
       </template>
       <template #actionbtns_default="{ row }">
         <XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
 
   <XModal id="smsLog" v-model="dialogVisible" :title="dialogTitle">
@@ -34,15 +34,13 @@
 // 全局相关的 import
 import { ref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { allSchemas } from './sms.log.data'
 import * as SmsLoglApi from '@/api/system/sms/smsLog'
 const { t } = useI18n() // 国际化
 
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, exportList } = useVxeGrid<SmsLoglApi.SmsLogVO>({
+const [registerTable, { exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: SmsLoglApi.getSmsLogPageApi,
   exportListApi: SmsLoglApi.exportSmsLogApi
@@ -59,9 +57,4 @@ const handleDetail = (row: SmsLoglApi.SmsLogVO) => {
   detailData.value = row
   dialogVisible.value = true
 }
-
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '短信日志.xls')
-}
 </script>

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

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <!-- 操作:新增 -->
       <template #toolbar_buttons>
         <XButton
@@ -38,10 +38,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:sms-template:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <XModal id="smsTemplate" v-model="dialogVisible" :title="dialogTitle">
     <!-- 对话框(添加 / 修改) -->
@@ -113,8 +113,7 @@
 import { ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 import { ElForm, ElFormItem, ElInput } from 'element-plus'
 // 业务相关的 import
@@ -125,8 +124,7 @@ const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData } = useVxeGrid<SmsTemplateApi.SmsTemplateVO>({
+const [registerTable, { reload, deleteData }] = useXTable({
   allSchemas: allSchemas,
   getListApi: SmsTemplateApi.getSmsTemplatePageApi,
   deleteApi: SmsTemplateApi.deleteSmsTemplateApi
@@ -168,11 +166,6 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -194,7 +187,7 @@ const submitForm = async () => {
       } finally {
         actionLoading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

+ 7 - 19
yudao-ui-admin-vue3/src/views/system/tenant/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <!-- 操作:新增 -->
         <XButton
@@ -16,7 +16,7 @@
           preIcon="ep:download"
           :title="t('action.export')"
           v-hasPermi="['system:tenant:export']"
-          @click="handleExport()"
+          @click="exportList('租户列表.xls')"
         />
       </template>
       <template #accountCount_default="{ row }">
@@ -46,10 +46,10 @@
           preIcon="ep:delete"
           :title="t('action.del')"
           v-hasPermi="['system:tenant:delete']"
-          @click="handleDelete(row.id)"
+          @click="deleteData(row.id)"
         />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <XModal v-model="dialogVisible" :title="dialogTitle">
     <!-- 对话框(添加 / 修改) -->
@@ -89,8 +89,7 @@
 import { ref, unref } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { ElTag } from 'element-plus'
 import { FormExpose } from '@/components/Form'
 import * as TenantApi from '@/api/system/tenant'
@@ -99,8 +98,7 @@ import { rules, allSchemas, tenantPackageOption } from './tenant.data'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData, exportList } = useVxeGrid<TenantApi.TenantVO>({
+const [registerTable, { reload, deleteData, exportList }] = useXTable({
   allSchemas: allSchemas,
   getListApi: TenantApi.getTenantPageApi,
   deleteApi: TenantApi.deleteTenantApi,
@@ -151,16 +149,6 @@ const handleDetail = async (rowId: number) => {
   setDialogTile('detail')
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
-// 导出操作
-const handleExport = async () => {
-  await exportList(xGrid, '租户列表.xls')
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -183,7 +171,7 @@ const submitForm = async () => {
       } finally {
         actionLoading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

+ 10 - 14
yudao-ui-admin-vue3/src/views/system/tenantPackage/index.vue

@@ -1,7 +1,7 @@
 <template>
   <ContentWrap>
     <!-- 列表 -->
-    <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+    <XTable @register="registerTable">
       <template #toolbar_buttons>
         <XButton
           type="primary"
@@ -12,9 +12,9 @@
       </template>
       <template #actionbtns_default="{ row }">
         <XTextButton preIcon="ep:edit" :title="t('action.edit')" @click="handleUpdate(row.id)" />
-        <XTextButton preIcon="ep:delete" :title="t('action.del')" @click="handleDelete(row.id)" />
+        <XTextButton preIcon="ep:delete" :title="t('action.del')" @click="deleteData(row.id)" />
       </template>
-    </vxe-grid>
+    </XTable>
   </ContentWrap>
   <XModal v-model="dialogVisible" :title="dialogTitle">
     <!-- 对话框(添加 / 修改) -->
@@ -69,8 +69,7 @@ import { onMounted, ref, unref } from 'vue'
 import { handleTree, defaultProps } from '@/utils/tree'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
-import { VxeGridInstance } from 'vxe-table'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 import { ElCard, ElSwitch, ElTree } from 'element-plus'
 // 业务相关的 import
@@ -86,7 +85,6 @@ const menuExpand = ref(false)
 const menuNodeAll = ref(false)
 const treeRef = ref<InstanceType<typeof ElTree>>()
 const treeNodeAll = ref(false)
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
 const formRef = ref<FormExpose>() // 表单 Ref
 const loading = ref(false) // 遮罩层
 const actionType = ref('') // 操作按钮的类型
@@ -102,7 +100,7 @@ const getTree = async () => {
   menuOptions.value = handleTree(res)
 }
 
-const { gridOptions, getList, deleteData } = useVxeGrid<TenantPackageApi.TenantPackageVO>({
+const [registerTable, { reload, deleteData }] = useXTable({
   allSchemas: allSchemas,
   getListApi: TenantPackageApi.getTenantPackageTypePageApi,
   deleteApi: TenantPackageApi.deleteTenantPackageTypeApi
@@ -134,11 +132,6 @@ const handleUpdate = async (rowId: number) => {
   unref(treeRef)?.setCheckedKeys(res.menuIds)
 }
 
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
-
 // 提交按钮
 const submitForm = async () => {
   const elForm = unref(formRef)?.getElFormRef()
@@ -149,7 +142,10 @@ const submitForm = async () => {
       // 提交请求
       try {
         const data = unref(formRef)?.formModel as TenantPackageApi.TenantPackageVO
-        data.menuIds = treeRef.value!.getCheckedKeys(false) as number[]
+        data.menuIds = [
+          ...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>),
+          ...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>)
+        ]
         if (actionType.value === 'create') {
           await TenantPackageApi.createTenantPackageTypeApi(data)
           message.success(t('common.createSuccess'))
@@ -162,7 +158,7 @@ const submitForm = async () => {
       } finally {
         loading.value = false
         // 刷新列表
-        await getList(xGrid)
+        await reload()
       }
     }
   })

+ 11 - 16
yudao-ui-admin-vue3/src/views/system/user/index.vue

@@ -27,7 +27,7 @@
         </div>
       </template>
       <!-- 列表 -->
-      <vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
+      <XTable @register="registerTable">
         <template #toolbar_buttons>
           <!-- 操作:新增 -->
           <XButton
@@ -112,14 +112,14 @@
                     preIcon="ep:delete"
                     :title="t('action.del')"
                     v-hasPermi="['system:user:delete']"
-                    @click="handleDelete(row.id)"
+                    @click="deleteData(row.id)"
                   />
                 </el-dropdown-item>
               </el-dropdown-menu>
             </template>
           </el-dropdown>
         </template>
-      </vxe-grid>
+      </XTable>
     </el-card>
   </div>
   <XModal v-model="dialogVisible" :title="dialogTitle">
@@ -283,14 +283,13 @@ import {
   UploadRawFile
 } from 'element-plus'
 import { useRouter } from 'vue-router'
-import { VxeGridInstance } from 'vxe-table'
 import { handleTree, defaultProps } from '@/utils/tree'
 import download from '@/utils/download'
 import { CommonStatusEnum } from '@/utils/constants'
 import { getAccessToken, getTenantId } from '@/utils/auth'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
-import { useVxeGrid } from '@/hooks/web/useVxeGrid'
+import { useXTable } from '@/hooks/web/useXTable'
 import { FormExpose } from '@/components/Form'
 import { rules, allSchemas } from './user.data'
 import * as UserApi from '@/api/system/user'
@@ -312,10 +311,9 @@ const queryParams = reactive({
 // ========== 列表相关 ==========
 const tableTitle = ref('用户列表')
 // 列表相关的变量
-const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
-const { gridOptions, getList, deleteData, exportList } = useVxeGrid<UserApi.UserVO>({
+const [registerTable, { reload, deleteData, exportList }] = useXTable({
   allSchemas: allSchemas,
-  queryParams: queryParams,
+  params: queryParams,
   getListApi: UserApi.getUserPageApi,
   deleteApi: UserApi.deleteUserApi,
   exportListApi: UserApi.exportUserApi
@@ -334,7 +332,7 @@ const filterNode = (value: string, data: Tree) => {
 }
 const handleDeptNodeClick = async (row: { [key: string]: any }) => {
   queryParams.deptId = row.id
-  await getList(xGrid)
+  await reload()
 }
 const { push } = useRouter()
 const handleDeptEdit = () => {
@@ -407,10 +405,7 @@ const handleDetail = async (rowId: number) => {
   detailData.value = res
   await setDialogTile('detail')
 }
-// 删除操作
-const handleDelete = async (rowId: number) => {
-  await deleteData(xGrid, rowId)
-}
+
 // 提交按钮
 const submitForm = async () => {
   loading.value = true
@@ -428,7 +423,7 @@ const submitForm = async () => {
   } finally {
     // unref(formRef)?.setSchema(allSchemas.formSchema)
     // 刷新列表
-    await getList(xGrid)
+    await reload()
     loading.value = false
   }
 }
@@ -443,7 +438,7 @@ const handleStatusChange = async (row: UserApi.UserVO) => {
       await UserApi.updateUserStatusApi(row.id, row.status)
       message.success(text + '成功')
       // 刷新列表
-      await getList(xGrid)
+      await reload()
     })
     .catch(() => {
       row.status =
@@ -544,7 +539,7 @@ const handleFileSuccess = async (response: any): Promise<void> => {
     text += '< ' + username + ': ' + data.failureUsernames[username] + ' >'
   }
   message.alert(text)
-  await getList(xGrid)
+  await reload()
 }
 // 文件数超出提示
 const handleExceed = (): void => {

+ 1 - 1
yudao-ui-admin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "yudao-ui-admin",
-  "version": "1.6.5-snapshot",
+  "version": "1.6.6-snapshot",
   "description": "芋道管理系统",
   "author": "芋道",
   "license": "MIT",

+ 14 - 8
yudao-ui-admin/src/views/infra/file/index.vue

@@ -1,14 +1,16 @@
 <template>
   <div class="app-container">
-    <doc-alert title="上传下载" url="https://doc.iocoder.cn/file/" />
+    <doc-alert title="上传下载" url="https://doc.iocoder.cn/file/"/>
     <!-- 搜索工作栏 -->
     <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
       <el-form-item label="文件路径" prop="path">
         <el-input v-model="queryParams.path" placeholder="请输入文件路径" clearable @keyup.enter.native="handleQuery"/>
       </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
-        <el-date-picker v-model="queryParams.createTime" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss" type="daterange"
-                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="['00:00:00', '23:59:59']" />
+        <el-date-picker v-model="queryParams.createTime" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss"
+                        type="daterange"
+                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
+                        :default-time="['00:00:00', '23:59:59']"/>
       </el-form-item>
       <el-form-item>
         <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
@@ -35,6 +37,9 @@
         <template v-slot="scope">
           <image-preview v-if="scope.row.type&&scope.row.type.indexOf('image/') === 0" :src="scope.row.url"
                          :width="'100px'"></image-preview>
+          <video v-else-if="scope.row.type&&scope.row.type.indexOf('video/') === 0" :width="'100px'">
+            <source :src="scope.row.url"/>
+          </video>
           <i v-else>无法预览,点击
             <el-link type="primary" :underline="false" style="font-size:12px;vertical-align: baseline;" target="_blank"
                      :href="getFileUrl + scope.row.configId + '/get/' + scope.row.path">下载
@@ -118,7 +123,7 @@ export default {
         title: "", // 弹出层标题
         isUploading: false, // 是否禁用上传
         url: process.env.VUE_APP_BASE_API + "/admin-api/infra/file/upload", // 请求地址
-        headers: { Authorization: "Bearer " + getAccessToken() }, // 设置上传的请求头部
+        headers: {Authorization: "Bearer " + getAccessToken()}, // 设置上传的请求头部
         data: {} // 上传的额外数据,用于文件名
       },
     };
@@ -189,19 +194,20 @@ export default {
     /** 删除按钮操作 */
     handleDelete(row) {
       const id = row.id;
-      this.$modal.confirm('是否确认删除文件编号为"' + id + '"的数据项?').then(function() {
+      this.$modal.confirm('是否确认删除文件编号为"' + id + '"的数据项?').then(function () {
         return deleteFile(id);
       }).then(() => {
         this.getList();
         this.$modal.msgSuccess("删除成功");
-      }).catch(() => {});
+      }).catch(() => {
+      });
     },
     // 用户昵称展示
     sizeFormat(row, column) {
-      const unitArr = ["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"];
+      const unitArr = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
       const srcSize = parseFloat(row.size);
       const index = Math.floor(Math.log(srcSize) / Math.log(1024));
-      let size =srcSize/Math.pow(1024,index);
+      let size = srcSize / Math.pow(1024, index);
       size = size.toFixed(2);//保留的小数位数
       return size + ' ' + unitArr[index];
     },

+ 2 - 2
yudao-ui-admin/src/views/infra/redis/index.vue

@@ -136,8 +136,8 @@
 
 <script>
 import {getCache, getKeyDefineList, getKeyList, getKeyValue, deleteKey, deleteKeys} from "@/api/infra/redis";
-import echarts from "echarts";
-
+import * as echarts from 'echarts'
+require('echarts/theme/macarons') // echarts theme
 export default {
   name: "Server",
   data () {

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

@@ -0,0 +1,92 @@
+<template>
+  <div class="app-container">
+    <el-form label-width="120px">
+      <el-row type="flex" :gutter="0">
+        <el-col :sm="12">
+          <el-form-item label="WebSocket地址" size="small">
+            <el-input v-model="url" type="text"/>
+          </el-form-item>
+        </el-col>
+        <el-col :offset="1">
+          <el-form-item label="" label-width="0px" size="small">
+            <el-button @click="connect" type="primary" :disabled="ws&&ws.readyState===1">
+              {{ ws && ws.readyState === 1 ? "已连接" : "连接" }}
+            </el-button>
+            <el-button @click="exit" type="danger">断开</el-button>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="发送内容" size="small">
+        <el-input type="textarea" v-model="message" :rows="5"/>
+      </el-form-item>
+      <el-form-item label="" size="small">
+        <el-button type="success" @click="send">发送消息</el-button>
+      </el-form-item>
+      <el-form-item label="接收内容" size="small">
+        <el-input type="textarea" v-model="content" :rows="12" disabled/>
+      </el-form-item>
+      <el-form-item label="" size="small">
+        <el-button type="info" @click="content=''">清空消息</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import store from "@/store";
+import {getNowDateTime} from "@/utils/ruoyi";
+
+export default {
+  data() {
+    return {
+      url: process.env.VUE_APP_BASE_API + "/websocket/message",
+      message: "",
+      content: "",
+      ws: null,
+    };
+  },
+  created() {
+    this.url = this.url.replace("http", "ws")
+  },
+  methods: {
+    connect() {
+      if (!'WebSocket' in window) {
+        this.$modal.msgError("您的浏览器不支持WebSocket");
+        return;
+      }
+      const userId = store.getters.userId;
+      this.ws = new WebSocket(this.url + "?userId=" + userId);
+      const self = this;
+      this.ws.onopen = function (event) {
+        self.content = self.content + "\n**********************连接开始**********************\n";
+      };
+      this.ws.onmessage = function (event) {
+        self.content = self.content + "接收时间:" + getNowDateTime() + "\n" + event.data + "\n";
+      };
+      this.ws.onclose = function (event) {
+        self.content = self.content + "**********************连接关闭**********************\n";
+      };
+      this.ws.error = function (event) {
+        self.content = self.content + "**********************连接异常**********************\n";
+      };
+    },
+    exit() {
+      if (this.ws) {
+        this.ws.close();
+        this.ws = null;
+      }
+    },
+    send() {
+      if (!this.ws || this.ws.readyState !== 1) {
+        this.$modal.msgError("未连接到服务器");
+        return;
+      }
+      if (!this.message) {
+        this.$modal.msgError("请输入发送内容");
+        return;
+      }
+      this.ws.send(this.message);
+    }
+  },
+};
+</script>

+ 1 - 1
yudao-ui-app/utils/request/responseInterceptors.js

@@ -30,7 +30,7 @@ module.exports = vm => {
         if (!isRefreshToken) {
           isRefreshToken = true
           // 1. 如果获取不到刷新令牌,则只能执行登出操作
-          if (!vm.$store.getters.refreshToken()) {
+          if (!vm.$store.getters.refreshToken) {
             vm.$store.commit('CLEAR_LOGIN_INFO')
             return Promise.reject(res)
           }

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor