This commit is contained in:
shizhong 2023-01-10 16:09:16 +08:00
commit 4584774d74
125 changed files with 3215 additions and 2564 deletions

117
README.md
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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";
/**
* 节点地址

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
}
}

View File

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

View File

@ -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]));
}
}

View File

@ -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) {
}
}

View File

@ -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";
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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());

View File

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

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

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

View File

@ -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()
}
}
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 开发,插件如下

View File

@ -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": {

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -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';

View File

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

View File

@ -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';

View File

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

View File

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

View File

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

View File

@ -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]
}

View File

@ -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')
}

View File

@ -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', // 全局尺寸

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(() => {})
}

View File

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

View File

@ -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()
}
}
})

View File

@ -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()
}
}
})

View File

@ -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()
}
}
})

View File

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

View File

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

View File

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

View File

@ -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()
}
}
})

View File

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

View File

@ -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()
}
}
})

View File

@ -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()
}
}
})

View File

@ -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) => {

View File

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

View File

@ -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 '未设置'

View File

@ -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()
}
}
})

View File

@ -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()
}
}
})

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