mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2025-01-18 19:20:05 +08:00
Merge remote-tracking branch 'origin/dev' into feature/springdoc
# Conflicts: # yudao-dependencies/pom.xml # yudao-framework/yudao-spring-boot-starter-web/pom.xml # yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/config/YudaoSwaggerAutoConfiguration.java # yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/swagger/core/SpringFoxHandlerProviderBeanPostProcessor.java # yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/captcha/CaptchaController.java
This commit is contained in:
commit
1c0d8fc1eb
@ -191,19 +191,19 @@ ps:核心功能已经实现,正在对接微信小程序中...
|
||||
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.7.7 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
|
||||
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | |
|
||||
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.15 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
|
||||
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.3 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
|
||||
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.3.1 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
|
||||
| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.6.1 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
|
||||
| [Redis](https://redis.io/) | key-value 数据库 | 5.0 / 6.0 | |
|
||||
| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.18.0 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) |
|
||||
| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.24 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) |
|
||||
| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.7.5 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
|
||||
| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.7.6 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
|
||||
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.5 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
|
||||
| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 6.8.0 | [文档](https://doc.iocoder.cn/bpm/) |
|
||||
| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) |
|
||||
| [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 4.0.0 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) |
|
||||
| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.1 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) |
|
||||
| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.12.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) |
|
||||
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.7.9 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
|
||||
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.7.10 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
|
||||
| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.13.3 | |
|
||||
| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.5.3.Final | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
|
||||
| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.18.24 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
|
||||
@ -227,7 +227,7 @@ ps:核心功能已经实现,正在对接微信小程序中...
|
||||
| [TypeScript](https://www.typescriptlang.org/docs/) | TypeScript | 4.9.4 |
|
||||
| [pinia](https://pinia.vuejs.org/) | vuex5 | 2.0.28 |
|
||||
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
|
||||
| [vxe-table](https://vxetable.cn/) | vue最强表单 | 4.3.7 |
|
||||
| [vxe-table](https://vxetable.cn/) | vue最强表单 | 4.3.9 |
|
||||
|
||||
### [管理后台 uni-app 跨端](./yudao-ui-admin-uniapp)
|
||||
|
||||
|
13
pom.xml
13
pom.xml
@ -29,15 +29,16 @@
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>1.6.5-snapshot</revision>
|
||||
<revision>1.6.6-snapshot</revision>
|
||||
<!-- Maven 相关 -->
|
||||
<java.version>1.8</java.version>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
|
||||
<maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
|
||||
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
|
||||
<!-- 看看咋放到 bom 里 -->
|
||||
<lombok.version>1.18.24</lombok.version>
|
||||
<spring.boot.version>2.7.7</spring.boot.version>
|
||||
<mapstruct.version>1.5.3.Final</mapstruct.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
@ -64,13 +65,19 @@
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
</plugin>
|
||||
<!-- maven-compiler-plugin 插件,解决 Lombok + MapStruct 组合 -->
|
||||
<!-- maven-compiler-plugin 插件,解决 spring-boot-configuration-processor + Lombok + MapStruct 组合 -->
|
||||
<!-- https://stackoverflow.com/questions/33483697/re-run-spring-boot-configuration-annotation-processor-to-update-generated-metada -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
-- ----------------------------
|
||||
|
@ -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;
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>1.6.5-snapshot</revision>
|
||||
<revision>1.6.6-snapshot</revision>
|
||||
<!-- 统一依赖管理 -->
|
||||
<spring.boot.version>2.7.7</spring.boot.version>
|
||||
<!-- Web 相关 -->
|
||||
@ -23,8 +23,8 @@
|
||||
<servlet.versoin>2.5</servlet.versoin>
|
||||
<!-- DB 相关 -->
|
||||
<druid.version>1.2.15</druid.version>
|
||||
<mybatis-plus.version>3.5.3</mybatis-plus.version>
|
||||
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
|
||||
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
|
||||
<mybatis-plus-generator.version>3.5.3.1</mybatis-plus-generator.version>
|
||||
<dynamic-datasource.version>3.6.1</dynamic-datasource.version>
|
||||
<redisson.version>3.18.0</redisson.version>
|
||||
<!-- 服务保障相关 -->
|
||||
@ -32,7 +32,7 @@
|
||||
<resilience4j.version>1.7.1</resilience4j.version>
|
||||
<!-- 监控相关 -->
|
||||
<skywalking.version>8.12.0</skywalking.version>
|
||||
<spring-boot-admin.version>2.7.9</spring-boot-admin.version>
|
||||
<spring-boot-admin.version>2.7.10</spring-boot-admin.version>
|
||||
<opentracing.version>0.33.0</opentracing.version>
|
||||
<!-- Test 测试相关 -->
|
||||
<podam.version>7.2.11.RELEASE</podam.version>
|
||||
@ -41,10 +41,12 @@
|
||||
<!-- Bpm 工作流相关 -->
|
||||
<flowable.version>6.8.0</flowable.version>
|
||||
<!-- 工具类相关 -->
|
||||
<captcha-plus.version>1.0.0</captcha-plus.version>
|
||||
<jsoup.version>1.15.3</jsoup.version>
|
||||
<lombok.version>1.18.24</lombok.version>
|
||||
<mapstruct.version>1.5.3.Final</mapstruct.version>
|
||||
<hutool.version>5.8.11</hutool.version>
|
||||
<easyexcel.verion>3.1.4</easyexcel.verion>
|
||||
<easyexcel.verion>3.1.5</easyexcel.verion>
|
||||
<velocity.version>2.3</velocity.version>
|
||||
<screw.version>1.0.5</screw.version>
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
@ -54,16 +56,15 @@
|
||||
<commons-net.version>3.8.0</commons-net.version>
|
||||
<jsch.version>0.1.55</jsch.version>
|
||||
<tika-core.version>2.6.0</tika-core.version>
|
||||
<aj-captcha.version>1.3.0</aj-captcha.version>
|
||||
<netty-all.version>4.1.86.Final</netty-all.version>
|
||||
<ip2region.version>2.6.6</ip2region.version>
|
||||
<!-- 三方云服务相关 -->
|
||||
<okio.version>3.0.0</okio.version>
|
||||
<okhttp3.version>4.10.0</okhttp3.version>
|
||||
<minio.version>8.4.6</minio.version>
|
||||
<minio.version>8.5.1</minio.version>
|
||||
<aliyun-java-sdk-core.version>4.6.3</aliyun-java-sdk-core.version>
|
||||
<aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
|
||||
<tencentcloud-sdk-java.version>3.1.660</tencentcloud-sdk-java.version>
|
||||
<tencentcloud-sdk-java.version>3.1.676</tencentcloud-sdk-java.version>
|
||||
<justauth.version>1.4.0</justauth.version>
|
||||
<jimureport.version>1.5.6</jimureport.version>
|
||||
<xercesImpl.version>2.12.2</xercesImpl.version>
|
||||
@ -439,12 +440,6 @@
|
||||
<version>${tika-core.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.anji-plus</groupId>
|
||||
<artifactId>spring-boot-starter-captcha</artifactId>
|
||||
<version>${aj-captcha.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.velocity</groupId>
|
||||
<artifactId>velocity-engine-core</artifactId>
|
||||
@ -509,12 +504,24 @@
|
||||
<version>${netty-all.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.xingyuv</groupId>
|
||||
<artifactId>spring-boot-starter-captcha-plus</artifactId>
|
||||
<version>${captcha-plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.lionsoul</groupId>
|
||||
<artifactId>ip2region</artifactId>
|
||||
<version>${ip2region.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>${jsoup.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 三方云服务相关 -->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okio</groupId>
|
||||
@ -588,6 +595,12 @@
|
||||
<artifactId>xercesImpl</artifactId>
|
||||
<version>${xercesImpl.version}</version>
|
||||
</dependency>
|
||||
<!-- SpringBoot Websocket -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
<version>${spring.boot.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
@ -40,6 +40,7 @@
|
||||
|
||||
<module>yudao-spring-boot-starter-flowable</module>
|
||||
<module>yudao-spring-boot-starter-captcha</module>
|
||||
<module>yudao-spring-boot-starter-websocket</module>
|
||||
</modules>
|
||||
|
||||
<artifactId>yudao-framework</artifactId>
|
||||
|
@ -1,7 +1,6 @@
|
||||
package cn.iocoder.yudao.framework.common.util.validation;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
|
@ -52,7 +52,7 @@
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>4.35.9.ALL</version>
|
||||
<version>4.35.32.ALL</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
|
@ -17,6 +17,10 @@
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xingyuv</groupId>
|
||||
<artifactId>spring-boot-starter-captcha-plus</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring 核心 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@ -29,11 +33,6 @@
|
||||
<artifactId>yudao-spring-boot-starter-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 验证码相关 -->
|
||||
<dependency>
|
||||
<groupId>com.anji-plus</groupId>
|
||||
<artifactId>spring-boot-starter-captcha</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -2,12 +2,9 @@ package cn.iocoder.yudao.framework.captcha.core.enums;
|
||||
|
||||
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
|
||||
import com.anji.captcha.model.vo.PointVO;
|
||||
import org.redisson.api.RLock;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH;
|
||||
import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
|
||||
|
||||
/**
|
||||
|
@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.captcha.core.service;
|
||||
import com.anji.captcha.service.CaptchaCacheService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.mybatis.core.mapper;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
@ -10,6 +9,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.Db;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Collection;
|
||||
@ -92,8 +92,22 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
|
||||
entities.forEach(this::insert);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入,适合大量数据插入
|
||||
*
|
||||
* @param entities 实体们
|
||||
* @param size 插入数量 Db.saveBatch 默认为1000
|
||||
*/
|
||||
default void insertBatch(Collection<T> entities, int size) {
|
||||
Db.saveBatch(entities, size);
|
||||
}
|
||||
|
||||
default void updateBatch(T update) {
|
||||
update(update, new QueryWrapper<>());
|
||||
}
|
||||
|
||||
default void updateBatch(Collection<T> entities, int size) {
|
||||
Db.updateBatchById(entities, size);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)))
|
||||
|
@ -68,6 +68,12 @@
|
||||
<scope>provided</scope> <!-- 设置为 provided,主要是 GlobalExceptionHandler 使用 -->
|
||||
</dependency>
|
||||
|
||||
<!-- xss -->
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -2,15 +2,22 @@ package cn.iocoder.yudao.framework.web.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService;
|
||||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||
import cn.iocoder.yudao.framework.web.core.clean.JsoupXssCleaner;
|
||||
import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
|
||||
import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyFilter;
|
||||
import cn.iocoder.yudao.framework.web.core.filter.DemoFilter;
|
||||
import cn.iocoder.yudao.framework.web.core.filter.XssFilter;
|
||||
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
|
||||
import cn.iocoder.yudao.framework.web.core.handler.GlobalResponseBodyHandler;
|
||||
import cn.iocoder.yudao.framework.web.core.json.XssStringJsonDeserializer;
|
||||
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@ -48,7 +55,7 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
|
||||
* 设置 API 前缀,仅仅匹配 controller 包下的
|
||||
*
|
||||
* @param configurer 配置
|
||||
* @param api API 配置
|
||||
* @param api API 配置
|
||||
*/
|
||||
private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) {
|
||||
AntPathMatcher antPathMatcher = new AntPathMatcher(".");
|
||||
@ -104,8 +111,9 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
|
||||
* 创建 XssFilter Bean,解决 Xss 安全问题
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher) {
|
||||
return createFilterBean(new XssFilter(properties, pathMatcher), WebFilterOrderEnum.XSS_FILTER);
|
||||
@ConditionalOnBean(XssCleaner.class)
|
||||
public FilterRegistrationBean<XssFilter> xssFilter(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) {
|
||||
return createFilterBean(new XssFilter(properties, pathMatcher, xssCleaner), WebFilterOrderEnum.XSS_FILTER);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -117,6 +125,32 @@ public class YudaoWebAutoConfiguration implements WebMvcConfigurer {
|
||||
return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Xss 清理者
|
||||
*
|
||||
* @return XssCleaner
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(XssCleaner.class)
|
||||
public XssCleaner xssCleaner() {
|
||||
return new JsoupXssCleaner();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤
|
||||
*
|
||||
* @return Jackson2ObjectMapperBuilderCustomizer
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "xssJacksonCustomizer")
|
||||
@ConditionalOnBean(ObjectMapper.class)
|
||||
@ConditionalOnProperty(value = "yudao.xss.enable", havingValue = "true")
|
||||
public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssCleaner xssCleaner) {
|
||||
// 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理
|
||||
return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(xssCleaner));
|
||||
}
|
||||
|
||||
private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
|
||||
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
|
||||
bean.setOrder(order);
|
||||
|
@ -0,0 +1,80 @@
|
||||
package cn.iocoder.yudao.framework.web.core.clean;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.safety.Safelist;
|
||||
|
||||
/**
|
||||
* jsonp 过滤字符串
|
||||
*/
|
||||
public class JsoupXssCleaner implements XssCleaner {
|
||||
|
||||
private final Safelist safelist;
|
||||
|
||||
/**
|
||||
* 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分)
|
||||
*/
|
||||
private final String baseUri;
|
||||
|
||||
/**
|
||||
* 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表
|
||||
*/
|
||||
public JsoupXssCleaner() {
|
||||
this.safelist = buildSafelist();
|
||||
this.baseUri = "";
|
||||
}
|
||||
|
||||
public JsoupXssCleaner(Safelist safelist) {
|
||||
this.safelist = safelist;
|
||||
this.baseUri = "";
|
||||
}
|
||||
|
||||
public JsoupXssCleaner(String baseUri) {
|
||||
this.safelist = buildSafelist();
|
||||
this.baseUri = baseUri;
|
||||
}
|
||||
|
||||
public JsoupXssCleaner(Safelist safelist, String baseUri) {
|
||||
this.safelist = safelist;
|
||||
this.baseUri = baseUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建一个 Xss 清理的 Safelist 规则。
|
||||
* 基于 Safelist#relaxed() 的基础上:
|
||||
* 1. 扩展支持了 style 和 class 属性
|
||||
* 2. a 标签额外支持了 target 属性
|
||||
* 3. img 标签额外支持了 data 协议,便于支持 base64
|
||||
*
|
||||
* @return Safelist
|
||||
*/
|
||||
private Safelist buildSafelist() {
|
||||
// 使用 jsoup 提供的默认的
|
||||
Safelist relaxedSafelist = Safelist.relaxed();
|
||||
// 富文本编辑时一些样式是使用 style 来进行实现的
|
||||
// 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性
|
||||
// 注意:style 属性会有注入风险 <img STYLE="background-image:url(javascript:alert('XSS'))">
|
||||
relaxedSafelist.addAttributes(":all", "style", "class");
|
||||
// 保留 a 标签的 target 属性
|
||||
relaxedSafelist.addAttributes("a", "target");
|
||||
// 支持img 为base64
|
||||
relaxedSafelist.addProtocols("img", "src", "data");
|
||||
|
||||
// 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除
|
||||
// WHITELIST.preserveRelativeLinks(false);
|
||||
|
||||
// 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如 <img src=javascript:alert("xss")>
|
||||
// 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径
|
||||
// WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto");
|
||||
// WHITELIST.removeProtocols("img", "src", "http", "https");
|
||||
|
||||
return relaxedSafelist;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String clean(String html) {
|
||||
return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
package cn.iocoder.yudao.framework.web.core.clean;
|
||||
|
||||
/**
|
||||
* 对 html 文本中的有 Xss 风险的数据进行清理
|
||||
*/
|
||||
public interface XssCleaner {
|
||||
|
||||
/**
|
||||
* 清理有 Xss 风险的文本
|
||||
*
|
||||
* @param html 原 html
|
||||
* @return 清理后的 html
|
||||
*/
|
||||
String clean(String html);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package cn.iocoder.yudao.framework.web.core.filter;
|
||||
|
||||
import cn.iocoder.yudao.framework.web.config.XssProperties;
|
||||
import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
@ -13,7 +14,7 @@ import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Xss 过滤器
|
||||
*
|
||||
* <p>
|
||||
* 对 Xss 不了解的胖友,可以看看 http://www.iocoder.cn/Fight/The-new-girl-asked-me-why-AJAX-requests-are-not-secure-I-did-not-answer/
|
||||
*
|
||||
* @author 芋道源码
|
||||
@ -30,10 +31,12 @@ public class XssFilter extends OncePerRequestFilter {
|
||||
*/
|
||||
private final PathMatcher pathMatcher;
|
||||
|
||||
private final XssCleaner xssCleaner;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
filterChain.doFilter(new XssRequestWrapper(request), response);
|
||||
filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,21 +1,10 @@
|
||||
package cn.iocoder.yudao.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HTMLFilter;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
|
||||
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@ -24,113 +13,79 @@ import java.util.Map;
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class XssRequestWrapper extends HttpServletRequestWrapper {
|
||||
private final XssCleaner xssCleaner;
|
||||
|
||||
/**
|
||||
* 基于线程级别的 HTMLFilter 对象,因为它线程非安全
|
||||
*/
|
||||
private static final ThreadLocal<HTMLFilter> HTML_FILTER = ThreadLocal.withInitial(() -> {
|
||||
HTMLFilter htmlFilter = new HTMLFilter();
|
||||
// 反射修改 encodeQuotes 属性为 false,避免 " 被转移成 " 字符
|
||||
ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false);
|
||||
return htmlFilter;
|
||||
});
|
||||
|
||||
public XssRequestWrapper(HttpServletRequest request) {
|
||||
public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) {
|
||||
super(request);
|
||||
this.xssCleaner = xssCleaner;
|
||||
}
|
||||
|
||||
private static String filterXss(String content) {
|
||||
if (StrUtil.isEmpty(content)) {
|
||||
return content;
|
||||
// ============================ parameter ============================
|
||||
@Override
|
||||
public Map<String, String[]> getParameterMap() {
|
||||
Map<String, String[]> map = new LinkedHashMap<>();
|
||||
Map<String, String[]> parameters = super.getParameterMap();
|
||||
for (Map.Entry<String, String[]> entry : parameters.entrySet()) {
|
||||
String[] values = entry.getValue();
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = xssCleaner.clean(values[i]);
|
||||
}
|
||||
map.put(entry.getKey(), values);
|
||||
}
|
||||
return HTML_FILTER.get().filter(content);
|
||||
}
|
||||
|
||||
// ========== IO 流相关 ==========
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
// 如果非 json 请求,不进行 Xss 处理
|
||||
if (!ServletUtils.isJsonRequest(this)) {
|
||||
return super.getInputStream();
|
||||
}
|
||||
|
||||
// 读取内容,并过滤
|
||||
String content = IoUtil.readUtf8(super.getInputStream());
|
||||
content = filterXss(content);
|
||||
final ByteArrayInputStream newInputStream = new ByteArrayInputStream(content.getBytes());
|
||||
// 返回 ServletInputStream
|
||||
return new ServletInputStream() {
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return newInputStream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Param 相关 ==========
|
||||
|
||||
@Override
|
||||
public String getParameter(String name) {
|
||||
String value = super.getParameter(name);
|
||||
return filterXss(value);
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] values = super.getParameterValues(name);
|
||||
if (ArrayUtil.isEmpty(values)) {
|
||||
return values;
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
// 过滤处理
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = filterXss(values[i]);
|
||||
int count = values.length;
|
||||
String[] encodedValues = new String[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
encodedValues[i] = xssCleaner.clean(values[i]);
|
||||
}
|
||||
return values;
|
||||
return encodedValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String[]> getParameterMap() {
|
||||
Map<String, String[]> valueMap = super.getParameterMap();
|
||||
if (CollUtil.isEmpty(valueMap)) {
|
||||
return valueMap;
|
||||
public String getParameter(String name) {
|
||||
String value = super.getParameter(name);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
// 过滤处理
|
||||
for (Map.Entry<String, String[]> entry : valueMap.entrySet()) {
|
||||
String[] values = entry.getValue();
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = filterXss(values[i]);
|
||||
}
|
||||
}
|
||||
return valueMap;
|
||||
return xssCleaner.clean(value);
|
||||
}
|
||||
|
||||
// ========== Header 相关 ==========
|
||||
// ============================ attribute ============================
|
||||
@Override
|
||||
public Object getAttribute(String name) {
|
||||
Object value = super.getAttribute(name);
|
||||
if (value instanceof String) {
|
||||
xssCleaner.clean((String) value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// ============================ header ============================
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
String value = super.getHeader(name);
|
||||
return filterXss(value);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return xssCleaner.clean(value);
|
||||
}
|
||||
|
||||
// ============================ queryString ============================
|
||||
@Override
|
||||
public String getQueryString() {
|
||||
String value = super.getQueryString();
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return xssCleaner.clean(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
package cn.iocoder.yudao.framework.web.core.json;
|
||||
|
||||
import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* XSS 过滤 jackson 反序列化器。
|
||||
* 在反序列化的过程中,会对字符串进行 XSS 过滤。
|
||||
*
|
||||
* @author Hccake
|
||||
*/
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class XssStringJsonDeserializer extends StringDeserializer {
|
||||
|
||||
private final XssCleaner xssCleaner;
|
||||
|
||||
@Override
|
||||
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
if (p.hasToken(JsonToken.VALUE_STRING)) {
|
||||
return xssCleaner.clean(p.getText());
|
||||
}
|
||||
JsonToken t = p.currentToken();
|
||||
// [databind#381]
|
||||
if (t == JsonToken.START_ARRAY) {
|
||||
return _deserializeFromArray(p, ctxt);
|
||||
}
|
||||
// need to gracefully handle byte[] data, as base64
|
||||
if (t == JsonToken.VALUE_EMBEDDED_OBJECT) {
|
||||
Object ob = p.getEmbeddedObject();
|
||||
if (ob == null) {
|
||||
return null;
|
||||
}
|
||||
if (ob instanceof byte[]) {
|
||||
return ctxt.getBase64Variant().encode((byte[]) ob, false);
|
||||
}
|
||||
// otherwise, try conversion using toString()...
|
||||
return ob.toString();
|
||||
}
|
||||
// 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML)
|
||||
if (t == JsonToken.START_OBJECT) {
|
||||
return ctxt.extractScalarFromObject(p, this, _valueClass);
|
||||
}
|
||||
|
||||
if (t.isScalarValue()) {
|
||||
String text = p.getValueAsString();
|
||||
return xssCleaner.clean(text);
|
||||
}
|
||||
return (String) ctxt.handleUnexpectedToken(_valueClass, p);
|
||||
}
|
||||
}
|
||||
|
37
yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
Normal file
37
yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
Normal 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>
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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]));
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
package cn.iocoder.yudao.framework.websocket;
|
@ -0,0 +1 @@
|
||||
cn.iocoder.yudao.framework.websocket.config.YudaoWebSocketAutoConfiguration
|
@ -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>
|
||||
|
@ -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());
|
||||
|
@ -11,9 +11,11 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableField;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Mappings;
|
||||
import org.mapstruct.Named;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
@ -37,7 +39,7 @@ public interface CodegenConvert {
|
||||
|
||||
@Mappings({
|
||||
@Mapping(source = "name", target = "columnName"),
|
||||
@Mapping(source = "type", target = "dataType"),
|
||||
@Mapping(source = "metaInfo.jdbcType", target = "dataType", qualifiedByName = "getDataType"),
|
||||
@Mapping(source = "comment", target = "columnComment"),
|
||||
@Mapping(source = "metaInfo.nullable", target = "nullable"),
|
||||
@Mapping(source = "keyFlag", target = "primaryKey"),
|
||||
@ -47,6 +49,11 @@ public interface CodegenConvert {
|
||||
})
|
||||
CodegenColumnDO convert(TableField bean);
|
||||
|
||||
@Named("getDataType")
|
||||
default String getDataType(JdbcType jdbcType) {
|
||||
return jdbcType.name();
|
||||
}
|
||||
|
||||
// ========== CodegenTableDO 相关 ==========
|
||||
|
||||
// List<CodegenTableRespVO> convertList02(List<CodegenTableDO> list);
|
||||
|
@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnListConditionEnu
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableField;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
@ -29,7 +30,7 @@ public class CodegenColumnDO extends BaseDO {
|
||||
private Long id;
|
||||
/**
|
||||
* 表编号
|
||||
*
|
||||
* <p>
|
||||
* 关联 {@link CodegenTableDO#getId()}
|
||||
*/
|
||||
private Long tableId;
|
||||
@ -41,7 +42,8 @@ public class CodegenColumnDO extends BaseDO {
|
||||
*/
|
||||
private String columnName;
|
||||
/**
|
||||
* 字段类型
|
||||
* 数据库字段类型
|
||||
* 关联 {@link TableField.MetaInfo#getJdbcType()}
|
||||
*/
|
||||
private String dataType;
|
||||
/**
|
||||
@ -69,7 +71,7 @@ public class CodegenColumnDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* Java 属性类型
|
||||
*
|
||||
* <p>
|
||||
* 例如说 String、Boolean 等等
|
||||
*/
|
||||
private String javaType;
|
||||
@ -79,7 +81,7 @@ public class CodegenColumnDO extends BaseDO {
|
||||
private String javaField;
|
||||
/**
|
||||
* 字典类型
|
||||
*
|
||||
* <p>
|
||||
* 关联 DictTypeDO 的 type 属性
|
||||
*/
|
||||
private String dictType;
|
||||
@ -104,7 +106,7 @@ public class CodegenColumnDO extends BaseDO {
|
||||
private Boolean listOperation;
|
||||
/**
|
||||
* List 查询操作的条件类型
|
||||
*
|
||||
* <p>
|
||||
* 枚举 {@link CodegenColumnListConditionEnum}
|
||||
*/
|
||||
private String listOperationCondition;
|
||||
@ -117,7 +119,7 @@ public class CodegenColumnDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 显示类型
|
||||
*
|
||||
* <p>
|
||||
* 枚举 {@link CodegenColumnHtmlTypeEnum}
|
||||
*/
|
||||
private String htmlType;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@
|
||||
preIcon="ep:download"
|
||||
:title="t('action.export')"
|
||||
v-hasPermi="['${permissionPrefix}:export']"
|
||||
@click="handleExport()"
|
||||
@click="exportList('${table.classComment}.xls')"
|
||||
/>
|
||||
</template>
|
||||
<template #actionbtns_default="{ row }">
|
||||
@ -40,7 +40,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['${permissionPrefix}:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
@ -119,11 +119,6 @@ const handleCreate = () => {
|
||||
modelLoading.value = false
|
||||
}
|
||||
|
||||
// 导出操作
|
||||
const handleExport = async () => {
|
||||
await exportList('${table.classComment}.xls')
|
||||
}
|
||||
|
||||
// 修改操作
|
||||
const handleUpdate = async (rowId: number) => {
|
||||
setDialogTile('update')
|
||||
@ -141,11 +136,6 @@ const handleDetail = async (rowId: number) => {
|
||||
modelLoading.value = false
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
|
||||
// 提交按钮
|
||||
const submitForm = async () => {
|
||||
const elForm = unref(formRef)?.getElFormRef()
|
||||
|
@ -1,7 +1,7 @@
|
||||
package cn.iocoder.yudao.module.infra.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.generator.IDatabaseQuery.DefaultDatabaseQuery;
|
||||
import com.baomidou.mybatisplus.generator.query.DefaultQuery;
|
||||
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
|
||||
import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
|
||||
@ -19,7 +19,7 @@ public class DefaultDatabaseQueryTest {
|
||||
|
||||
ConfigBuilder builder = new ConfigBuilder(null, dataSourceConfig, null, null, null, null);
|
||||
|
||||
DefaultDatabaseQuery query = new DefaultDatabaseQuery(builder);
|
||||
DefaultQuery query = new DefaultQuery(builder);
|
||||
|
||||
long time = System.currentTimeMillis();
|
||||
List<TableInfo> tableInfos = query.queryTables();
|
||||
|
@ -1,47 +1,61 @@
|
||||
package cn.iocoder.yudao.module.system.controller.admin.captcha;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
|
||||
import com.anji.captcha.model.common.ResponseModel;
|
||||
import com.anji.captcha.model.vo.CaptchaVO;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import com.anji.captcha.service.CaptchaService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.annotation.security.PermitAll;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*
|
||||
* 问题:为什么不直接使用 anji 提供的 CaptchaController,而要另外继承?
|
||||
* 回答:管理使用 /admin-api/* 作为前缀,所以需要继承!
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Tag(name = "管理后台 - 验证码")
|
||||
@Api(tags = "管理后台 - 验证码")
|
||||
@RestController("adminCaptchaController")
|
||||
@RequestMapping("/system/captcha")
|
||||
public class CaptchaController extends com.anji.captcha.controller.CaptchaController {
|
||||
public class CaptchaController {
|
||||
|
||||
@Resource
|
||||
private CaptchaService captchaService;
|
||||
|
||||
@PostMapping({"/get"})
|
||||
@Operation(summary = "获得验证码")
|
||||
@ApiOperation("获得验证码")
|
||||
@PermitAll
|
||||
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
|
||||
@Override
|
||||
public ResponseModel get(@RequestBody CaptchaVO data, HttpServletRequest request) {
|
||||
return super.get(data, request);
|
||||
assert request.getRemoteHost() != null;
|
||||
data.setBrowserInfo(getRemoteId(request));
|
||||
return captchaService.get(data);
|
||||
}
|
||||
|
||||
@PostMapping("/check")
|
||||
@Operation(summary = "校验验证码")
|
||||
@ApiOperation("校验验证码")
|
||||
@PermitAll
|
||||
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
|
||||
@Override
|
||||
public ResponseModel check(@RequestBody CaptchaVO data, HttpServletRequest request) {
|
||||
return super.check(data, request);
|
||||
data.setBrowserInfo(getRemoteId(request));
|
||||
return captchaService.check(data);
|
||||
}
|
||||
|
||||
public static String getRemoteId(HttpServletRequest request) {
|
||||
String ip = ServletUtil.getClientIP(request);
|
||||
String ua = request.getHeader("user-agent");
|
||||
if (StrUtil.isNotBlank(ip)) {
|
||||
return ip + ua;
|
||||
}
|
||||
return request.getRemoteAddr() + ua;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -97,6 +97,11 @@ yudao:
|
||||
security:
|
||||
permit-all_urls:
|
||||
- /admin-ui/** # /resources/admin-ui 目录下的静态资源
|
||||
websocket:
|
||||
enable: true # websocket的开关
|
||||
path: /websocket/message # 路径
|
||||
maxOnlineCount: 0 # 最大连接人数
|
||||
sessionMap: true # 保存sessionMap
|
||||
swagger:
|
||||
title: 管理后台
|
||||
description: 提供管理员管理的所有功能
|
||||
|
@ -34,7 +34,7 @@
|
||||
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.9.4 |
|
||||
| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.28 |
|
||||
| [vueuse](https://vueuse.org/) | 常用工具集 | 9.10.0 |
|
||||
| [vxe-table](https://vxetable.cn/) | vue 最强表单 | 4.3.7 |
|
||||
| [vxe-table](https://vxetable.cn/) | vue 最强表单 | 4.3.9 |
|
||||
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
|
||||
| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.6 |
|
||||
| [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 |
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yudao-ui-admin-vue3",
|
||||
"version": "1.6.5.1901",
|
||||
"version": "1.6.6-snapshot.1912",
|
||||
"description": "基于vue3、vite4、element-plus、typesScript",
|
||||
"author": "xingyu",
|
||||
"private": false,
|
||||
@ -42,7 +42,7 @@
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.0.28",
|
||||
"pinia": "^2.0.29",
|
||||
"qrcode": "^1.5.1",
|
||||
"qs": "^6.11.0",
|
||||
"url": "^0.11.0",
|
||||
@ -50,14 +50,14 @@
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue-types": "^5.0.2",
|
||||
"vxe-table": "^4.3.7",
|
||||
"vxe-table": "^4.3.9",
|
||||
"web-storage-cache": "^1.1.1",
|
||||
"xe-utils": "^3.5.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.3.0",
|
||||
"@commitlint/config-conventional": "^17.3.0",
|
||||
"@iconify/json": "^2.2.1",
|
||||
"@commitlint/cli": "^17.4.2",
|
||||
"@commitlint/config-conventional": "^17.4.2",
|
||||
"@iconify/json": "^2.2.7",
|
||||
"@intlify/unplugin-vue-i18n": "^0.8.1",
|
||||
"@purge-icons/generated": "^0.9.0",
|
||||
"@types/intro.js": "^5.1.0",
|
||||
@ -66,32 +66,32 @@
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.0",
|
||||
"@typescript-eslint/parser": "^5.48.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"@vitejs/plugin-legacy": "^3.0.1",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"consola": "^2.15.3",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-define-config": "^1.13.0",
|
||||
"eslint-define-config": "^1.14.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.8.0",
|
||||
"eslint-plugin-vue": "^9.9.0",
|
||||
"lint-staged": "^13.1.0",
|
||||
"postcss": "^8.4.20",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"prettier": "^2.8.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^3.9.1",
|
||||
"prettier": "^2.8.3",
|
||||
"rimraf": "^4.0.7",
|
||||
"rollup": "^3.10.0",
|
||||
"sass": "^1.57.1",
|
||||
"stylelint": "^14.16.1",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-prettier": "^9.0.4",
|
||||
"stylelint-config-recommended": "^9.0.0",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-order": "^5.0.0",
|
||||
"stylelint-order": "^6.0.1",
|
||||
"terser": "^5.16.1",
|
||||
"typescript": "4.9.4",
|
||||
"vite": "4.0.4",
|
||||
@ -104,7 +104,7 @@
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vite-plugin-windicss": "^1.8.10",
|
||||
"vue-tsc": "^1.0.20",
|
||||
"vue-tsc": "^1.0.24",
|
||||
"windicss": "^3.5.6"
|
||||
},
|
||||
"engines": {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -353,7 +353,6 @@ const select = ref()
|
||||
watch(
|
||||
() => select.value,
|
||||
() => {
|
||||
console.info(select.value)
|
||||
if (select.value == 'custom') {
|
||||
open()
|
||||
} else {
|
||||
|
@ -78,4 +78,4 @@ $vxe-modal-border-color: #3b3b3b;
|
||||
/*pulldown*/
|
||||
$vxe-pulldown-panel-background-color: #262626 !default;
|
||||
|
||||
@import 'vxe-table/styles/index';
|
||||
@import 'vxe-table/styles/index.scss';
|
||||
|
@ -1,5 +1,5 @@
|
||||
@import 'vxe-table/styles/variable.scss';
|
||||
@import 'vxe-table/styles/modules.scss';
|
||||
// @import 'vxe-table/styles/variable.scss';
|
||||
// @import 'vxe-table/styles/modules.scss';
|
||||
// @import './theme/light.scss';
|
||||
i {
|
||||
border-color: initial;
|
||||
|
@ -7,15 +7,14 @@ export type XTableProps<D = any> = VxeGridProps<D> & {
|
||||
topActionSlots?: boolean // 是否开启表格内顶部操作栏插槽
|
||||
treeConfig?: VxeTablePropTypes.TreeConfig // 树形表单配置
|
||||
isList?: boolean // 是否不带分页的list
|
||||
getListApi?: Function
|
||||
getAllListApi?: Function
|
||||
deleteApi?: Function
|
||||
exportListApi?: Function
|
||||
getListApi?: Function // 获取列表接口
|
||||
getAllListApi?: Function // 获取全部数据接口 用于 vxe 导出
|
||||
deleteApi?: Function // 删除接口
|
||||
exportListApi?: Function // 导出接口
|
||||
exportName?: string // 导出文件夹名称
|
||||
params?: any
|
||||
pagination?: boolean | VxeGridPropTypes.PagerConfig
|
||||
toolBar?: boolean | VxeGridPropTypes.ToolbarConfig
|
||||
afterFetch?: Function
|
||||
params?: any // 其他查询参数
|
||||
pagination?: boolean | VxeGridPropTypes.PagerConfig // 分页配置参数
|
||||
toolBar?: boolean | VxeGridPropTypes.ToolbarConfig // 右侧工具栏配置参数
|
||||
}
|
||||
export type XColumns = VxeGridPropTypes.Columns
|
||||
|
||||
|
@ -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',
|
||||
|
@ -26,15 +26,17 @@ import '@/styles/index.scss'
|
||||
import '@/plugins/animate.css'
|
||||
|
||||
// 路由
|
||||
import { setupRouter } from './router'
|
||||
import router, { setupRouter } from '@/router'
|
||||
|
||||
// 权限
|
||||
import { setupAuth } from './directives'
|
||||
import { setupAuth } from '@/directives'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
|
||||
import './permission'
|
||||
|
||||
// 创建实例
|
||||
const setupAll = async () => {
|
||||
const app = createApp(App)
|
||||
@ -53,6 +55,8 @@ const setupAll = async () => {
|
||||
|
||||
setupAuth(app)
|
||||
|
||||
await router.isReady()
|
||||
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
|
70
yudao-ui-admin-vue3/src/permission.ts
Normal file
70
yudao-ui-admin-vue3/src/permission.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import router from './router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { isRelogin } from '@/config/axios/service'
|
||||
import { getAccessToken } from '@/utils/auth'
|
||||
import { useTitle } from '@/hooks/web/useTitle'
|
||||
import { useNProgress } from '@/hooks/web/useNProgress'
|
||||
import { usePageLoading } from '@/hooks/web/usePageLoading'
|
||||
import { useDictStoreWithOut } from '@/store/modules/dict'
|
||||
import { useUserStoreWithOut } from '@/store/modules/user'
|
||||
import { usePermissionStoreWithOut } from '@/store/modules/permission'
|
||||
|
||||
const { start, done } = useNProgress()
|
||||
|
||||
const { loadStart, loadDone } = usePageLoading()
|
||||
// 路由不重定向白名单
|
||||
const whiteList = [
|
||||
'/login',
|
||||
'/social-login',
|
||||
'/auth-redirect',
|
||||
'/bind',
|
||||
'/register',
|
||||
'/oauthLogin/gitee'
|
||||
]
|
||||
|
||||
// 路由加载前
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
start()
|
||||
loadStart()
|
||||
if (getAccessToken()) {
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' })
|
||||
} else {
|
||||
// 获取所有字典
|
||||
const dictStore = useDictStoreWithOut()
|
||||
const userStore = useUserStoreWithOut()
|
||||
const permissionStore = usePermissionStoreWithOut()
|
||||
if (!dictStore.getIsSetDict) {
|
||||
dictStore.setDictMap()
|
||||
}
|
||||
if (!userStore.getIsSetUser) {
|
||||
isRelogin.show = true
|
||||
await userStore.setUserInfoAction()
|
||||
isRelogin.show = false
|
||||
// 后端过滤菜单
|
||||
await permissionStore.generateRoutes()
|
||||
permissionStore.getAddRouters.forEach((route) => {
|
||||
router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
|
||||
})
|
||||
const redirectPath = from.query.redirect || to.path
|
||||
const redirect = decodeURIComponent(redirectPath as string)
|
||||
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
|
||||
next(nextData)
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
next()
|
||||
} else {
|
||||
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach((to) => {
|
||||
useTitle(to?.meta?.title as string)
|
||||
done() // 结束Progress
|
||||
loadDone()
|
||||
})
|
@ -1,6 +0,0 @@
|
||||
@import 'vxe-table/styles/variable.scss';
|
||||
@import 'vxe-table/styles/modules.scss';
|
||||
// @import './theme/light.scss';
|
||||
i {
|
||||
border-color: initial;
|
||||
}
|
@ -4,3 +4,4 @@ import './dict'
|
||||
import './html'
|
||||
import './link'
|
||||
import './img'
|
||||
import './preview'
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
@ -1,81 +0,0 @@
|
||||
// 修改样式变量
|
||||
//@import 'vxe-table/styles/variable.scss';
|
||||
|
||||
/*font*/
|
||||
$vxe-font-color: #e5e7eb;
|
||||
// $vxe-font-size: 14px !default;
|
||||
// $vxe-font-size-medium: 16px !default;
|
||||
// $vxe-font-size-small: 14px !default;
|
||||
// $vxe-font-size-mini: 12px !default;
|
||||
|
||||
/*color*/
|
||||
$vxe-primary-color: #409eff !default;
|
||||
$vxe-success-color: #67c23a !default;
|
||||
$vxe-info-color: #909399 !default;
|
||||
$vxe-warning-color: #e6a23c !default;
|
||||
$vxe-danger-color: #f56c6c !default;
|
||||
$vxe-disabled-color: #bfbfbf !default;
|
||||
$vxe-primary-disabled-color: #c0c4cc !default;
|
||||
|
||||
/*loading*/
|
||||
$vxe-loading-color: $vxe-primary-color !default;
|
||||
$vxe-loading-background-color: #1d1e1f !default;
|
||||
$vxe-loading-z-index: 999 !default;
|
||||
|
||||
/*icon*/
|
||||
$vxe-icon-font-family: Verdana, Arial, Tahoma !default;
|
||||
$vxe-icon-background-color: #e5e7eb !default;
|
||||
|
||||
/*toolbar*/
|
||||
$vxe-toolbar-background-color: #1d1e1f !default;
|
||||
$vxe-toolbar-button-border: #dcdfe6 !default;
|
||||
$vxe-toolbar-custom-active-background-color: #d9dadb !default;
|
||||
$vxe-toolbar-panel-background-color: #e5e7eb !default;
|
||||
|
||||
$vxe-table-font-color: #e5e7eb;
|
||||
$vxe-table-header-background-color: #1d1e1f;
|
||||
$vxe-table-body-background-color: #141414;
|
||||
$vxe-table-row-striped-background-color: #1d1d1d;
|
||||
$vxe-table-row-hover-background-color: #1d1e1f;
|
||||
$vxe-table-row-hover-striped-background-color: #1e1e1e;
|
||||
$vxe-table-footer-background-color: #1d1e1f;
|
||||
$vxe-table-row-current-background-color: #302d2d;
|
||||
$vxe-table-column-current-background-color: #302d2d;
|
||||
$vxe-table-column-hover-background-color: #302d2d;
|
||||
$vxe-table-row-hover-current-background-color: #302d2d;
|
||||
$vxe-table-row-checkbox-checked-background-color: #3e3c37 !default;
|
||||
$vxe-table-row-hover-checkbox-checked-background-color: #615a4a !default;
|
||||
$vxe-table-menu-background-color: #1d1e1f;
|
||||
$vxe-table-border-width: 1px !default;
|
||||
$vxe-table-border-color: #4c4d4f !default;
|
||||
$vxe-table-fixed-left-scrolling-box-shadow: 8px 0px 10px -5px rgba(0, 0, 0, 0.12) !default;
|
||||
$vxe-table-fixed-right-scrolling-box-shadow: -8px 0px 10px -5px rgba(0, 0, 0, 0.12) !default;
|
||||
|
||||
$vxe-form-background-color: #141414;
|
||||
|
||||
/*pager*/
|
||||
$vxe-pager-background-color: #1d1e1f !default;
|
||||
$vxe-pager-perfect-background-color: #262727 !default;
|
||||
$vxe-pager-perfect-button-background-color: #a7a3a3 !default;
|
||||
|
||||
$vxe-input-background-color: #141414;
|
||||
$vxe-input-border-color: #4c4d4f !default;
|
||||
|
||||
$vxe-select-option-hover-background-color: #262626 !default;
|
||||
$vxe-select-panel-background-color: #141414 !default;
|
||||
$vxe-select-empty-color: #262626 !default;
|
||||
$vxe-optgroup-title-color: #909399 !default;
|
||||
|
||||
/*button*/
|
||||
$vxe-button-default-background-color: #262626;
|
||||
$vxe-button-dropdown-panel-background-color: #141414;
|
||||
|
||||
/*modal*/
|
||||
$vxe-modal-header-background-color: #141414;
|
||||
$vxe-modal-body-background-color: #141414;
|
||||
$vxe-modal-border-color: #3b3b3b;
|
||||
|
||||
/*pulldown*/
|
||||
$vxe-pulldown-panel-background-color: #262626 !default;
|
||||
|
||||
@import 'vxe-table/styles/index';
|
@ -1,16 +0,0 @@
|
||||
// 修改样式变量
|
||||
// /*font*/
|
||||
// $vxe-font-size: 12px !default;
|
||||
// $vxe-font-size-medium: 16px !default;
|
||||
// $vxe-font-size-small: 14px !default;
|
||||
// $vxe-font-size-mini: 12px !default;
|
||||
/*color*/
|
||||
$vxe-primary-color: #409eff !default;
|
||||
$vxe-success-color: #67c23a !default;
|
||||
$vxe-info-color: #909399 !default;
|
||||
$vxe-warning-color: #e6a23c !default;
|
||||
$vxe-danger-color: #f56c6c !default;
|
||||
$vxe-disabled-color: #bfbfbf !default;
|
||||
$vxe-primary-disabled-color: #c0c4cc !default;
|
||||
|
||||
@import 'vxe-table/styles/index.scss';
|
@ -2,23 +2,6 @@ import type { App } from 'vue'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import remainingRouter from './modules/remaining'
|
||||
import { isRelogin } from '@/config/axios/service'
|
||||
import { getAccessToken } from '@/utils/auth'
|
||||
import { useTitle } from '@/hooks/web/useTitle'
|
||||
import { useNProgress } from '@/hooks/web/useNProgress'
|
||||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
||||
import { usePageLoading } from '@/hooks/web/usePageLoading'
|
||||
import { useDictStoreWithOut } from '@/store/modules/dict'
|
||||
import { useUserStoreWithOut } from '@/store/modules/user'
|
||||
import { usePermissionStoreWithOut } from '@/store/modules/permission'
|
||||
import { getInfoApi } from '@/api/login'
|
||||
import { listSimpleDictDataApi } from '@/api/system/dict/dict.data'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
|
||||
const { start, done } = useNProgress()
|
||||
|
||||
const { loadStart, loadDone } = usePageLoading()
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
@ -28,66 +11,6 @@ const router = createRouter({
|
||||
scrollBehavior: () => ({ left: 0, top: 0 })
|
||||
})
|
||||
|
||||
// 路由不重定向白名单
|
||||
const whiteList = [
|
||||
'/login',
|
||||
'/social-login',
|
||||
'/auth-redirect',
|
||||
'/bind',
|
||||
'/register',
|
||||
'/oauthLogin/gitee'
|
||||
]
|
||||
|
||||
// 路由加载前
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
start()
|
||||
loadStart()
|
||||
if (getAccessToken()) {
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' })
|
||||
} else {
|
||||
// 获取所有字典
|
||||
const dictStore = useDictStoreWithOut()
|
||||
const userStore = useUserStoreWithOut()
|
||||
const permissionStore = usePermissionStoreWithOut()
|
||||
const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
|
||||
if (!dictMap) {
|
||||
const res = await listSimpleDictDataApi()
|
||||
dictStore.setDictMap(res)
|
||||
}
|
||||
if (!userStore.getIsSetUser) {
|
||||
isRelogin.show = true
|
||||
const res = await getInfoApi()
|
||||
await userStore.setUserInfoAction(res)
|
||||
isRelogin.show = false
|
||||
// 后端过滤菜单
|
||||
await permissionStore.generateRoutes()
|
||||
permissionStore.getAddRouters.forEach((route) => {
|
||||
router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
|
||||
})
|
||||
const redirectPath = from.query.redirect || to.path
|
||||
const redirect = decodeURIComponent(redirectPath as string)
|
||||
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
|
||||
next(nextData)
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
next()
|
||||
} else {
|
||||
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach((to) => {
|
||||
useTitle(to?.meta?.title as string)
|
||||
done() // 结束Progress
|
||||
loadDone()
|
||||
})
|
||||
|
||||
export const resetRouter = (): void => {
|
||||
const resetWhiteNameList = ['Redirect', 'Login', 'NoFind', 'Root']
|
||||
router.getRoutes().forEach((route) => {
|
||||
|
@ -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 秒 过期
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -2,6 +2,7 @@ import { store } from '../index'
|
||||
import { defineStore } from 'pinia'
|
||||
import { getAccessToken, removeToken } from '@/utils/auth'
|
||||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
||||
import { getInfoApi } from '@/api/login'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
|
||||
@ -43,11 +44,15 @@ export const useUserStore = defineStore('admin-user', {
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async setUserInfoAction(userInfo: UserInfoVO) {
|
||||
async setUserInfoAction() {
|
||||
if (!getAccessToken()) {
|
||||
this.resetState()
|
||||
return null
|
||||
}
|
||||
let userInfo = wsCache.get(CACHE_KEY.USER)
|
||||
if (!userInfo) {
|
||||
userInfo = await getInfoApi()
|
||||
}
|
||||
this.permissions = userInfo.permissions
|
||||
this.roles = userInfo.roles
|
||||
this.user = userInfo.user
|
||||
|
@ -2,3 +2,5 @@
|
||||
$namespace: v;
|
||||
// el命名空间
|
||||
$elNamespace: el;
|
||||
// vxe命名空间
|
||||
$vxeNamespace: vxe;
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
// ========== 租户相关 ==========
|
||||
|
@ -17,13 +17,12 @@ const propTypes = createTypes({
|
||||
|
||||
// 需要自定义扩展的类型
|
||||
// see: https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html#the-extend-method
|
||||
propTypes.extend([
|
||||
{
|
||||
name: 'style',
|
||||
getter: true,
|
||||
type: [String, Object],
|
||||
default: undefined
|
||||
}
|
||||
])
|
||||
|
||||
// propTypes.extend([
|
||||
// {
|
||||
// name: 'style',
|
||||
// getter: true,
|
||||
// type: [String, Object],
|
||||
// default: undefined
|
||||
// }
|
||||
// ])
|
||||
export { propTypes }
|
||||
|
@ -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) {
|
||||
|
@ -8,7 +8,7 @@
|
||||
</template>
|
||||
<ProfileUser />
|
||||
</el-card>
|
||||
<el-card class="w-2/3 user" style="margin-left: 10px" shadow="hover">
|
||||
<el-card class="w-2/3 user ml-3" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ t('profile.info.title') }}</span>
|
||||
|
@ -8,7 +8,7 @@
|
||||
type="warning"
|
||||
preIcon="ep:download"
|
||||
:title="t('action.export')"
|
||||
@click="handleExport()"
|
||||
@click="exportList('错误数据.xls')"
|
||||
/>
|
||||
</template>
|
||||
<template #duration_default="{ row }">
|
||||
@ -81,10 +81,7 @@ const handleDetail = (row: ApiErrorLogApi.ApiErrorLogVO) => {
|
||||
dialogTitle.value = t('action.detail')
|
||||
dialogVisible.value = true
|
||||
}
|
||||
// 导出
|
||||
const handleExport = async () => {
|
||||
await exportList('错误数据.xls')
|
||||
}
|
||||
|
||||
// 异常处理操作
|
||||
const handleProcessClick = (
|
||||
row: ApiErrorLogApi.ApiErrorLogVO,
|
||||
|
@ -8,9 +8,6 @@
|
||||
<el-tab-pane label="字段信息" name="cloum">
|
||||
<CloumInfoForm ref="cloumInfoRef" :info="cloumCurrentRow" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="生成信息" name="genInfo">
|
||||
<GenInfoForm ref="genInfoRef" :genInfo="tableCurrentRow" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template #right>
|
||||
<XButton
|
||||
@ -30,7 +27,7 @@ import { ElTabs, ElTabPane } from 'element-plus'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { ContentDetailWrap } from '@/components/ContentDetailWrap'
|
||||
import { BasicInfoForm, CloumInfoForm, GenInfoForm } from './components'
|
||||
import { BasicInfoForm, CloumInfoForm } from './components'
|
||||
import { getCodegenTableApi, updateCodegenTableApi } from '@/api/infra/codegen'
|
||||
import { CodegenTableVO, CodegenColumnVO, CodegenUpdateReqVO } from '@/api/infra/codegen/types'
|
||||
|
||||
@ -40,33 +37,29 @@ const { push } = useRouter()
|
||||
const { query } = useRoute()
|
||||
const loading = ref(false)
|
||||
const title = ref('代码生成')
|
||||
const activeName = ref('cloum')
|
||||
const activeName = ref('basicInfo')
|
||||
const cloumInfoRef = ref(null)
|
||||
const tableCurrentRow = ref<CodegenTableVO>()
|
||||
const cloumCurrentRow = ref<CodegenColumnVO[]>([])
|
||||
const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>()
|
||||
const genInfoRef = ref<ComponentRef<typeof GenInfoForm>>()
|
||||
|
||||
const getList = async () => {
|
||||
const id = query.id as unknown as number
|
||||
if (id) {
|
||||
// 获取表详细信息
|
||||
const res = await getCodegenTableApi(id)
|
||||
tableCurrentRow.value = res.table
|
||||
title.value = '修改[ ' + res.table.tableName + ' ]生成配置'
|
||||
tableCurrentRow.value = res.table
|
||||
cloumCurrentRow.value = res.columns
|
||||
}
|
||||
}
|
||||
const submitForm = async () => {
|
||||
const basicInfo = unref(basicInfoRef)
|
||||
const genInfo = unref(genInfoRef)
|
||||
const basicForm = await basicInfo?.elFormRef?.validate()?.catch(() => {})
|
||||
const genForm = await genInfo?.elFormRef?.validate()?.catch(() => {})
|
||||
if (basicForm && genForm) {
|
||||
if (basicForm) {
|
||||
const basicInfoData = (await basicInfo?.getFormData()) as CodegenTableVO
|
||||
const genInfoData = (await genInfo?.getFormData()) as CodegenTableVO
|
||||
const genTable: CodegenUpdateReqVO = {
|
||||
table: Object.assign({}, basicInfoData, genInfoData),
|
||||
table: basicInfoData,
|
||||
columns: cloumCurrentRow.value
|
||||
}
|
||||
await updateCodegenTableApi(genTable)
|
||||
|
@ -2,25 +2,59 @@
|
||||
<Form :rules="rules" @register="register" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { PropType, reactive, watch } from 'vue'
|
||||
import { onMounted, PropType, reactive, ref, watch } from 'vue'
|
||||
import { required } from '@/utils/formRules'
|
||||
import { useForm } from '@/hooks/web/useForm'
|
||||
import { Form } from '@/components/Form'
|
||||
import { FormSchema } from '@/types/form'
|
||||
import { CodegenTableVO } from '@/api/infra/codegen/types'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { listSimpleMenusApi } from '@/api/system/menu'
|
||||
import { handleTree, defaultProps } from '@/utils/tree'
|
||||
|
||||
const props = defineProps({
|
||||
basicInfo: {
|
||||
type: Object as PropType<Nullable<CodegenTableVO>>,
|
||||
default: () => null
|
||||
}
|
||||
})
|
||||
|
||||
const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)
|
||||
const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)
|
||||
const menuOptions = ref<any>([]) // 树形结构
|
||||
const getTree = async () => {
|
||||
const res = await listSimpleMenusApi()
|
||||
menuOptions.value = handleTree(res)
|
||||
}
|
||||
|
||||
const rules = reactive({
|
||||
tableName: [required],
|
||||
tableComment: [required],
|
||||
className: [required],
|
||||
author: [required]
|
||||
author: [required],
|
||||
templateType: [required],
|
||||
scene: [required],
|
||||
moduleName: [required],
|
||||
businessName: [required],
|
||||
businessPackage: [required],
|
||||
classComment: [required]
|
||||
})
|
||||
const schema = reactive<FormSchema[]>([
|
||||
{
|
||||
label: '上级菜单',
|
||||
field: 'parentMenuId',
|
||||
component: 'TreeSelect',
|
||||
componentProps: {
|
||||
data: menuOptions,
|
||||
props: defaultProps,
|
||||
checkStrictly: true,
|
||||
nodeKey: 'id'
|
||||
},
|
||||
labelMessage: '分配到指定菜单下,例如 系统管理',
|
||||
colProps: {
|
||||
span: 24
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '表名称',
|
||||
field: 'tableName',
|
||||
@ -45,6 +79,64 @@ const schema = reactive<FormSchema[]>([
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '类名称',
|
||||
field: 'className',
|
||||
component: 'Input',
|
||||
labelMessage: '类名称(首字母大写),例如SysUser、SysMenu、SysDictData 等等',
|
||||
colProps: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '生成模板',
|
||||
field: 'templateType',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: templateTypeOptions
|
||||
},
|
||||
colProps: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '生成场景',
|
||||
field: 'scene',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: sceneOptions
|
||||
},
|
||||
colProps: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '模块名',
|
||||
field: 'moduleName',
|
||||
component: 'Input',
|
||||
labelMessage: '模块名,即一级目录,例如 system、infra、tool 等等',
|
||||
colProps: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '业务名',
|
||||
field: 'businessName',
|
||||
component: 'Input',
|
||||
labelMessage: '业务名,即二级目录,例如 user、permission、dict 等等',
|
||||
colProps: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '类描述',
|
||||
field: 'classComment',
|
||||
component: 'Input',
|
||||
labelMessage: '用作类描述,例如 用户',
|
||||
colProps: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '作者',
|
||||
field: 'author',
|
||||
@ -62,7 +154,7 @@ const schema = reactive<FormSchema[]>([
|
||||
rows: 4
|
||||
},
|
||||
colProps: {
|
||||
span: 12
|
||||
span: 24
|
||||
}
|
||||
}
|
||||
])
|
||||
@ -81,6 +173,10 @@ watch(
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
// ========== 初始化 ==========
|
||||
onMounted(async () => {
|
||||
await getTree()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
elFormRef,
|
||||
|
@ -1,113 +1,117 @@
|
||||
<template>
|
||||
<vxe-table
|
||||
ref="dragTable"
|
||||
border
|
||||
:data="info"
|
||||
max-height="600"
|
||||
stripe
|
||||
class="xtable-scrollbar"
|
||||
:column-config="{ resizable: true }"
|
||||
>
|
||||
<vxe-column title="字段列名" field="columnName" fixed="left" width="80" />
|
||||
<vxe-column title="字段描述" field="columnComment">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.columnComment" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="物理类型" field="dataType" width="10%" />
|
||||
<vxe-column title="Java类型" width="10%" field="javaType">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.javaType">
|
||||
<el-option label="Long" value="Long" />
|
||||
<el-option label="String" value="String" />
|
||||
<el-option label="Integer" value="Integer" />
|
||||
<el-option label="Double" value="Double" />
|
||||
<el-option label="BigDecimal" value="BigDecimal" />
|
||||
<el-option label="LocalDateTime" value="LocalDateTime" />
|
||||
<el-option label="Boolean" value="Boolean" />
|
||||
</el-select>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="java属性" width="10%" field="javaField">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.javaField" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="插入" width="4%" field="createOperation">
|
||||
<template #default="{ row }">
|
||||
<vxe-checkbox true-label="true" false-label="false" v-model="row.createOperation" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="编辑" width="4%" field="updateOperation">
|
||||
<template #default="{ row }">
|
||||
<vxe-checkbox true-label="true" false-label="false" v-model="row.updateOperation" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="列表" width="4%" field="listOperationResult">
|
||||
<template #default="{ row }">
|
||||
<vxe-checkbox true-label="true" false-label="false" v-model="row.listOperationResult" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="查询" width="4%" field="listOperation">
|
||||
<template #default="{ row }">
|
||||
<vxe-checkbox true-label="true" false-label="false" v-model="row.listOperation" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="查询方式" width="8%" field="listOperationCondition">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.listOperationCondition">
|
||||
<el-option label="=" value="=" />
|
||||
<el-option label="!=" value="!=" />
|
||||
<el-option label=">" value=">" />
|
||||
<el-option label=">=" value=">=" />
|
||||
<el-option label="<" value="<>" />
|
||||
<el-option label="<=" value="<=" />
|
||||
<el-option label="LIKE" value="LIKE" />
|
||||
<el-option label="BETWEEN" value="BETWEEN" />
|
||||
</el-select>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="允许空" width="4%" field="nullable">
|
||||
<template #default="{ row }">
|
||||
<vxe-checkbox true-label="true" false-label="false" v-model="row.nullable" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="字段列名" field="columnName" fixed="left" width="10%" />
|
||||
<vxe-colgroup title="基础属性">
|
||||
<vxe-column title="字段描述" field="columnComment" width="10%">
|
||||
<template #default="{ row }">
|
||||
<vxe-input v-model="row.columnComment" placeholder="请输入字段描述" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="物理类型" field="dataType" width="10%" />
|
||||
<vxe-column title="Java类型" width="10%" field="javaType">
|
||||
<template #default="{ row }">
|
||||
<vxe-select v-model="row.javaType" placeholder="请选择Java类型">
|
||||
<vxe-option label="Long" value="Long" />
|
||||
<vxe-option label="String" value="String" />
|
||||
<vxe-option label="Integer" value="Integer" />
|
||||
<vxe-option label="Double" value="Double" />
|
||||
<vxe-option label="BigDecimal" value="BigDecimal" />
|
||||
<vxe-option label="LocalDateTime" value="LocalDateTime" />
|
||||
<vxe-option label="Boolean" value="Boolean" />
|
||||
</vxe-select>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="java属性" width="8%" field="javaField">
|
||||
<template #default="{ row }">
|
||||
<vxe-input v-model="row.javaField" placeholder="请输入java属性" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-colgroup>
|
||||
<vxe-colgroup title="增删改查">
|
||||
<vxe-column title="插入" width="40px" field="createOperation">
|
||||
<template #default="{ row }">
|
||||
<vxe-checkbox true-label="true" false-label="false" v-model="row.createOperation" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="编辑" width="40px" field="updateOperation">
|
||||
<template #default="{ row }">
|
||||
<vxe-checkbox true-label="true" false-label="false" v-model="row.updateOperation" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="列表" width="40px" field="listOperationResult">
|
||||
<template #default="{ row }">
|
||||
<vxe-checkbox true-label="true" false-label="false" v-model="row.listOperationResult" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="查询" width="40px" field="listOperation">
|
||||
<template #default="{ row }">
|
||||
<vxe-checkbox true-label="true" false-label="false" v-model="row.listOperation" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="允许空" width="40px" field="nullable">
|
||||
<template #default="{ row }">
|
||||
<vxe-checkbox true-label="true" false-label="false" v-model="row.nullable" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="查询方式" width="60px" field="listOperationCondition">
|
||||
<template #default="{ row }">
|
||||
<vxe-select v-model="row.listOperationCondition" placeholder="请选择查询方式">
|
||||
<vxe-option label="=" value="=" />
|
||||
<vxe-option label="!=" value="!=" />
|
||||
<vxe-option label=">" value=">" />
|
||||
<vxe-option label=">=" value=">=" />
|
||||
<vxe-option label="<" value="<>" />
|
||||
<vxe-option label="<=" value="<=" />
|
||||
<vxe-option label="LIKE" value="LIKE" />
|
||||
<vxe-option label="BETWEEN" value="BETWEEN" />
|
||||
</vxe-select>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-colgroup>
|
||||
<vxe-column title="显示类型" width="10%" field="htmlType">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.htmlType">
|
||||
<el-option label="文本框" value="input" />
|
||||
<el-option label="文本域" value="textarea" />
|
||||
<el-option label="下拉框" value="select" />
|
||||
<el-option label="单选框" value="radio" />
|
||||
<el-option label="复选框" value="checkbox" />
|
||||
<el-option label="日期控件" value="datetime" />
|
||||
<el-option label="图片上传" value="imageUpload" />
|
||||
<el-option label="文件上传" value="fileUpload" />
|
||||
<el-option label="富文本控件" value="editor" />
|
||||
</el-select>
|
||||
<vxe-select v-model="row.htmlType" placeholder="请选择显示类型">
|
||||
<vxe-option label="文本框" value="input" />
|
||||
<vxe-option label="文本域" value="textarea" />
|
||||
<vxe-option label="下拉框" value="select" />
|
||||
<vxe-option label="单选框" value="radio" />
|
||||
<vxe-option label="复选框" value="checkbox" />
|
||||
<vxe-option label="日期控件" value="datetime" />
|
||||
<vxe-option label="图片上传" value="imageUpload" />
|
||||
<vxe-option label="文件上传" value="fileUpload" />
|
||||
<vxe-option label="富文本控件" value="editor" />
|
||||
</vxe-select>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="字典类型" width="10%" field="dictType">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.dictType" clearable filterable placeholder="请选择">
|
||||
<el-option
|
||||
<vxe-select v-model="row.dictType" clearable filterable placeholder="请选择字典类型">
|
||||
<vxe-option
|
||||
v-for="dict in dictOptions"
|
||||
:key="dict.id"
|
||||
:label="dict.name"
|
||||
:value="dict.type"
|
||||
/>
|
||||
</el-select>
|
||||
</vxe-select>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="示例" field="example">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.example" />
|
||||
<vxe-input v-model="row.example" placeholder="请输入示例" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, PropType, ref } from 'vue'
|
||||
import { ElInput, ElSelect, ElOption } from 'element-plus'
|
||||
import { DictTypeVO } from '@/api/system/dict/types'
|
||||
import { CodegenColumnVO } from '@/api/infra/codegen/types'
|
||||
import { listSimpleDictTypeApi } from '@/api/system/dict/dict.type'
|
||||
|
@ -1,135 +0,0 @@
|
||||
<template>
|
||||
<Form :rules="rules" @register="register" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, PropType, reactive, ref, watch } from 'vue'
|
||||
import { Form } from '@/components/Form'
|
||||
import { useForm } from '@/hooks/web/useForm'
|
||||
import { required } from '@/utils/formRules'
|
||||
import { handleTree, defaultProps } from '@/utils/tree'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { listSimpleMenusApi } from '@/api/system/menu'
|
||||
import { CodegenTableVO } from '@/api/infra/codegen/types'
|
||||
import { FormSchema } from '@/types/form'
|
||||
const props = defineProps({
|
||||
genInfo: {
|
||||
type: Object as PropType<Nullable<CodegenTableVO>>,
|
||||
default: () => null
|
||||
}
|
||||
})
|
||||
const rules = reactive({
|
||||
templateType: [required],
|
||||
scene: [required],
|
||||
moduleName: [required],
|
||||
businessName: [required],
|
||||
businessPackage: [required],
|
||||
className: [required],
|
||||
classComment: [required]
|
||||
})
|
||||
const templateTypeOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)
|
||||
const sceneOptions = getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)
|
||||
const menuOptions = ref<any>([]) // 树形结构
|
||||
const getTree = async () => {
|
||||
const res = await listSimpleMenusApi()
|
||||
menuOptions.value = handleTree(res)
|
||||
}
|
||||
const schema = reactive<FormSchema[]>([
|
||||
{
|
||||
label: '生成模板',
|
||||
field: 'templateType',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: templateTypeOptions
|
||||
},
|
||||
colProps: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '生成场景',
|
||||
field: 'scene',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: sceneOptions
|
||||
},
|
||||
colProps: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '模块名',
|
||||
field: 'moduleName',
|
||||
component: 'Input',
|
||||
labelMessage: '模块名,即一级目录,例如 system、infra、tool 等等',
|
||||
colProps: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '业务名',
|
||||
field: 'businessName',
|
||||
component: 'Input',
|
||||
labelMessage: '业务名,即二级目录,例如 user、permission、dict 等等',
|
||||
colProps: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '类名称',
|
||||
field: 'className',
|
||||
component: 'Input',
|
||||
labelMessage: '类名称(首字母大写),例如SysUser、SysMenu、SysDictData 等等',
|
||||
colProps: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '类描述',
|
||||
field: 'classComment',
|
||||
component: 'Input',
|
||||
labelMessage: '用作类描述,例如 用户',
|
||||
colProps: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '上级菜单',
|
||||
field: 'parentMenuId',
|
||||
component: 'TreeSelect',
|
||||
componentProps: {
|
||||
data: menuOptions,
|
||||
props: defaultProps,
|
||||
checkStrictly: true,
|
||||
nodeKey: 'id'
|
||||
},
|
||||
labelMessage: '分配到指定菜单下,例如 系统管理',
|
||||
colProps: {
|
||||
span: 12
|
||||
}
|
||||
}
|
||||
])
|
||||
const { register, methods, elFormRef } = useForm({
|
||||
schema
|
||||
})
|
||||
|
||||
// ========== 初始化 ==========
|
||||
onMounted(async () => {
|
||||
await getTree()
|
||||
})
|
||||
watch(
|
||||
() => props.genInfo,
|
||||
(genInfo) => {
|
||||
if (!genInfo) return
|
||||
const { setValues } = methods
|
||||
setValues(genInfo)
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
defineExpose({
|
||||
elFormRef,
|
||||
getFormData: methods.getFormData
|
||||
})
|
||||
</script>
|
@ -13,7 +13,7 @@
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</el-card>
|
||||
<el-card class="w-3/4" style="margin-left: 10px" :gutter="12" shadow="hover">
|
||||
<el-card class="w-3/4 ml-3" :gutter="12" shadow="hover">
|
||||
<el-tabs v-model="preview.activeName">
|
||||
<el-tab-pane
|
||||
v-for="item in previewCodegen"
|
||||
|
@ -1,6 +1,5 @@
|
||||
import BasicInfoForm from './BasicInfoForm.vue'
|
||||
import CloumInfoForm from './CloumInfoForm.vue'
|
||||
import GenInfoForm from './GenInfoForm.vue'
|
||||
import ImportTable from './ImportTable.vue'
|
||||
import Preview from './Preview.vue'
|
||||
export { BasicInfoForm, CloumInfoForm, GenInfoForm, ImportTable, Preview }
|
||||
export { BasicInfoForm, CloumInfoForm, ImportTable, Preview }
|
||||
|
@ -32,7 +32,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['infra:codegen:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
<!-- 操作:同步 -->
|
||||
<XTextButton
|
||||
@ -52,7 +52,7 @@
|
||||
</XTable>
|
||||
</ContentWrap>
|
||||
<!-- 弹窗:导入表 -->
|
||||
<ImportTable ref="importRef" @ok="handleQuery()" />
|
||||
<ImportTable ref="importRef" @ok="reload()" />
|
||||
<!-- 弹窗:预览代码 -->
|
||||
<Preview ref="previewRef" />
|
||||
</template>
|
||||
@ -103,17 +103,10 @@ const handleSynchDb = (row: CodegenTableVO) => {
|
||||
message.success('同步成功')
|
||||
})
|
||||
}
|
||||
|
||||
// 生成代码操作
|
||||
const handleGenTable = async (row: CodegenTableVO) => {
|
||||
const res = await CodegenApi.downloadCodegenApi(row.id)
|
||||
download.zip(res, 'codegen-' + row.className + '.zip')
|
||||
}
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
// 查询操作
|
||||
const handleQuery = async () => {
|
||||
await reload()
|
||||
}
|
||||
</script>
|
||||
|
@ -17,7 +17,7 @@
|
||||
preIcon="ep:download"
|
||||
:title="t('action.export')"
|
||||
v-hasPermi="['infra:config:export']"
|
||||
@click="handleExport()"
|
||||
@click="exportList('配置.xls')"
|
||||
/>
|
||||
</template>
|
||||
<template #visible_default="{ row }">
|
||||
@ -43,7 +43,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['infra:config:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
@ -123,11 +123,6 @@ const handleCreate = () => {
|
||||
setDialogTile('create')
|
||||
}
|
||||
|
||||
// 导出操作
|
||||
const handleExport = async () => {
|
||||
await exportList('配置.xls')
|
||||
}
|
||||
|
||||
// 修改操作
|
||||
const handleUpdate = async (rowId: number) => {
|
||||
setDialogTile('update')
|
||||
@ -143,11 +138,6 @@ const handleDetail = async (rowId: number) => {
|
||||
detailData.value = res
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
|
||||
// 提交按钮
|
||||
const submitForm = async () => {
|
||||
const elForm = unref(formRef)?.getElFormRef()
|
||||
|
@ -31,7 +31,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['infra:data-source-config:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
@ -121,11 +121,6 @@ const handleDetail = async (rowId: number) => {
|
||||
setDialogTile('detail')
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
|
||||
// 提交按钮
|
||||
const submitForm = async () => {
|
||||
const elForm = unref(formRef)?.getElFormRef()
|
||||
|
@ -41,7 +41,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['infra:file-config:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
@ -283,11 +283,6 @@ const handleTest = async (rowId: number) => {
|
||||
message.alert('测试通过,上传文件成功!访问地址:' + res)
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
|
||||
// 提交按钮
|
||||
const submitForm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
@ -23,7 +23,7 @@ const crudSchemas = reactive<VxeCrudSchema>({
|
||||
field: 'url',
|
||||
table: {
|
||||
cellRender: {
|
||||
name: 'XImg'
|
||||
name: 'XPreview'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -21,7 +21,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['infra:file:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
@ -162,11 +162,6 @@ const handleDetail = (row: FileApi.FileVO) => {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
|
||||
// ========== 复制相关 ==========
|
||||
const handleCopy = async (text: string) => {
|
||||
const { copy, copied, isSupported } = useClipboard({ source: text })
|
||||
|
@ -8,7 +8,7 @@
|
||||
preIcon="ep:download"
|
||||
:title="t('action.export')"
|
||||
v-hasPermi="['infra:job:export']"
|
||||
@click="handleExport()"
|
||||
@click="exportList('定时任务详情.xls')"
|
||||
/>
|
||||
</template>
|
||||
<template #beginTime_default="{ row }">
|
||||
@ -77,8 +77,4 @@ const handleDetail = async (row: JobLogApi.JobLogVO) => {
|
||||
dialogTitle.value = t('action.detail')
|
||||
dialogVisible.value = true
|
||||
}
|
||||
// 导出操作
|
||||
const handleExport = async () => {
|
||||
await exportList('定时任务详情.xls')
|
||||
}
|
||||
</script>
|
||||
|
@ -17,14 +17,14 @@
|
||||
preIcon="ep:download"
|
||||
:title="t('action.export')"
|
||||
v-hasPermi="['infra:job:export']"
|
||||
@click="handleExport()"
|
||||
@click="exportList('定时任务.xls')"
|
||||
/>
|
||||
<XButton
|
||||
type="info"
|
||||
preIcon="ep:zoom-in"
|
||||
title="执行日志"
|
||||
v-hasPermi="['infra:job:query']"
|
||||
@click="handleJobLog"
|
||||
@click="handleJobLog()"
|
||||
/>
|
||||
</template>
|
||||
<template #actionbtns_default="{ row }">
|
||||
@ -46,7 +46,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['infra:job:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
<el-dropdown class="p-0.5" v-hasPermi="['infra:job:trigger', 'infra:job:query']">
|
||||
<XTextButton :title="t('action.more')" postIcon="ep:arrow-down" />
|
||||
@ -179,11 +179,6 @@ const handleCreate = () => {
|
||||
setDialogTile('create')
|
||||
}
|
||||
|
||||
// 导出操作
|
||||
const handleExport = async () => {
|
||||
await exportList('定时任务.xls')
|
||||
}
|
||||
|
||||
// 修改操作
|
||||
const handleUpdate = async (rowId: number) => {
|
||||
setDialogTile('update')
|
||||
@ -248,10 +243,6 @@ const parseTime = (time) => {
|
||||
return time_str
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
const handleChangeStatus = async (row: JobApi.JobVO) => {
|
||||
const text = row.status === InfraJobStatusEnum.STOP ? '开启' : '关闭'
|
||||
const status =
|
||||
@ -275,7 +266,7 @@ const handleChangeStatus = async (row: JobApi.JobVO) => {
|
||||
})
|
||||
}
|
||||
// 执行日志
|
||||
const handleJobLog = (rowId: number) => {
|
||||
const handleJobLog = (rowId?: number) => {
|
||||
if (rowId) {
|
||||
push('/job/job-log?id=' + rowId)
|
||||
} else {
|
||||
|
118
yudao-ui-admin-vue3/src/views/infra/webSocket/index.vue
Normal file
118
yudao-ui-admin-vue3/src/views/infra/webSocket/index.vue
Normal 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>
|
@ -17,7 +17,7 @@
|
||||
preIcon="ep:download"
|
||||
:title="t('action.export')"
|
||||
v-hasPermi="['pay:app:export']"
|
||||
@click="handleExport()"
|
||||
@click="exportList('应用信息.xls')"
|
||||
/>
|
||||
</template>
|
||||
<template #actionbtns_default="{ row }">
|
||||
@ -40,7 +40,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['pay:app:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
@ -115,11 +115,6 @@ const handleCreate = () => {
|
||||
setDialogTile('create')
|
||||
}
|
||||
|
||||
// 导出操作
|
||||
const handleExport = async () => {
|
||||
await exportList('应用信息.xls')
|
||||
}
|
||||
|
||||
// 修改操作
|
||||
const handleUpdate = async (rowId: number) => {
|
||||
setDialogTile('update')
|
||||
@ -135,11 +130,6 @@ const handleDetail = async (rowId: number) => {
|
||||
detailData.value = res
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
|
||||
// 提交按钮
|
||||
const submitForm = async () => {
|
||||
const elForm = unref(formRef)?.getElFormRef()
|
||||
|
@ -17,7 +17,7 @@
|
||||
preIcon="ep:download"
|
||||
:title="t('action.export')"
|
||||
v-hasPermi="['pay:merchant:export']"
|
||||
@click="handleExport()"
|
||||
@click="exportList('商户列表.xls')"
|
||||
/>
|
||||
</template>
|
||||
<template #actionbtns_default="{ row }">
|
||||
@ -40,7 +40,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['pay:merchant:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
@ -113,11 +113,6 @@ const handleCreate = () => {
|
||||
setDialogTile('create')
|
||||
}
|
||||
|
||||
// 导出操作
|
||||
const handleExport = async () => {
|
||||
await exportList('商户列表.xls')
|
||||
}
|
||||
|
||||
// 修改操作
|
||||
const handleUpdate = async (rowId: number) => {
|
||||
setDialogTile('update')
|
||||
@ -133,11 +128,6 @@ const handleDetail = async (rowId: number) => {
|
||||
detailData.value = res
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
|
||||
// 提交按钮
|
||||
const submitForm = async () => {
|
||||
const elForm = unref(formRef)?.getElFormRef()
|
||||
|
@ -17,7 +17,7 @@
|
||||
preIcon="ep:download"
|
||||
:title="t('action.export')"
|
||||
v-hasPermi="['pay:order:export']"
|
||||
@click="handleExport()"
|
||||
@click="exportList('订单数据.xls')"
|
||||
/>
|
||||
</template>
|
||||
<template #actionbtns_default="{ row }">
|
||||
@ -72,10 +72,6 @@ const setDialogTile = (type: string) => {
|
||||
const handleCreate = () => {
|
||||
setDialogTile('create')
|
||||
}
|
||||
// 导出操作
|
||||
const handleExport = async () => {
|
||||
await exportList('订单数据.xls')
|
||||
}
|
||||
|
||||
// 详情操作
|
||||
const handleDetail = async (rowId: number) => {
|
||||
|
@ -9,7 +9,7 @@
|
||||
preIcon="ep:download"
|
||||
:title="t('action.export')"
|
||||
v-hasPermi="['pay:refund:export']"
|
||||
@click="handleExport()"
|
||||
@click="exportList('退款订单.xls')"
|
||||
/>
|
||||
</template>
|
||||
<template #actionbtns_default="{ row }">
|
||||
@ -49,11 +49,6 @@ const [registerTable, { exportList }] = useXTable({
|
||||
exportListApi: RefundApi.exportRefundApi
|
||||
})
|
||||
|
||||
// 导出操作
|
||||
const handleExport = async () => {
|
||||
await exportList('退款订单.xls')
|
||||
}
|
||||
|
||||
// ========== CRUD 相关 ==========
|
||||
const dialogVisible = ref(false) // 是否显示弹出层
|
||||
const detailData = ref() // 详情 Ref
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 列表 -->
|
||||
<XTable @register="registerTable" show-overflow>
|
||||
<XTable ref="xGrid" @register="registerTable" show-overflow>
|
||||
<template #toolbar_buttons>
|
||||
<!-- 操作:新增 -->
|
||||
<XButton
|
||||
@ -11,8 +11,8 @@
|
||||
v-hasPermi="['system:dept:create']"
|
||||
@click="handleCreate()"
|
||||
/>
|
||||
<XButton title="展开所有" @click="xGrid?.setAllTreeExpand(true)" />
|
||||
<XButton title="关闭所有" @click="xGrid?.clearTreeExpand()" />
|
||||
<XButton title="展开所有" @click="xGrid?.Ref.setAllTreeExpand(true)" />
|
||||
<XButton title="关闭所有" @click="xGrid?.Ref.clearTreeExpand()" />
|
||||
</template>
|
||||
<template #leaderUserId_default="{ row }">
|
||||
<span>{{ userNicknameFormat(row) }}</span>
|
||||
@ -30,7 +30,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['system:dept:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
@ -77,7 +77,6 @@
|
||||
<script setup lang="ts" name="Dept">
|
||||
import { nextTick, onMounted, ref, unref } from 'vue'
|
||||
import { ElSelect, ElTreeSelect, ElOption } from 'element-plus'
|
||||
import { VxeGridInstance } from 'vxe-table'
|
||||
import { handleTree, defaultProps } from '@/utils/tree'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
@ -90,7 +89,7 @@ import { getListSimpleUsersApi, UserVO } from '@/api/system/user'
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
// 列表相关的变量
|
||||
const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
|
||||
const xGrid = ref<any>() // 列表 Grid Ref
|
||||
const treeConfig = {
|
||||
transform: true,
|
||||
rowField: 'id',
|
||||
@ -168,17 +167,13 @@ const submitForm = async () => {
|
||||
dialogVisible.value = false
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
await getTree()
|
||||
await reload()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
|
||||
const userNicknameFormat = (row) => {
|
||||
if (!row || !row.leaderUserId) {
|
||||
return '未设置'
|
||||
|
@ -31,14 +31,14 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['system:dict:delete']"
|
||||
@click="handleTypeDelete(row.id)"
|
||||
@click="typeDeleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
<!-- @星语:分页和列表重叠在一起了 -->
|
||||
</el-card>
|
||||
<!-- ====== 字典数据 ====== -->
|
||||
<el-card class="w-1/2 dict" style="margin-left: 10px" :gutter="12" shadow="hover">
|
||||
<el-card class="w-1/2 dict ml-3" :gutter="12" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>字典数据</span>
|
||||
@ -74,7 +74,7 @@
|
||||
v-hasPermi="['system:dict:delete']"
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
@click="handleDataDelete(row.id)"
|
||||
@click="dataDeleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
@ -202,15 +202,6 @@ const setDialogTile = (type: string) => {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleTypeDelete = async (rowId: number) => {
|
||||
await typeDeleteData(rowId)
|
||||
}
|
||||
|
||||
const handleDataDelete = async (rowId: number) => {
|
||||
await dataDeleteData(rowId)
|
||||
}
|
||||
|
||||
// 提交按钮
|
||||
const submitTypeForm = async () => {
|
||||
const elForm = unref(typeFormRef)?.getElFormRef()
|
||||
|
@ -32,7 +32,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['system:error-code:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
@ -121,11 +121,6 @@ const handleDetail = async (rowId: number) => {
|
||||
detailData.value = res
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
|
||||
// 提交新增/修改的表单
|
||||
const submitForm = async () => {
|
||||
const elForm = unref(formRef)?.getElFormRef()
|
||||
|
@ -8,7 +8,7 @@
|
||||
type="warning"
|
||||
preIcon="ep:download"
|
||||
:title="t('action.export')"
|
||||
@click="handleExport()"
|
||||
@click="exportList('登录列表.xls')"
|
||||
/>
|
||||
</template>
|
||||
<template #actionbtns_default="{ row }">
|
||||
@ -54,9 +54,4 @@ const handleDetail = async (row: LoginLogVO) => {
|
||||
detailData.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 导出操作
|
||||
const handleExport = async () => {
|
||||
await exportList('登录列表.xls')
|
||||
}
|
||||
</script>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 列表 -->
|
||||
<XTable @register="registerTable" show-overflow>
|
||||
<XTable ref="xGrid" @register="registerTable" show-overflow>
|
||||
<template #toolbar_buttons>
|
||||
<!-- 操作:新增 -->
|
||||
<XButton
|
||||
@ -11,8 +11,8 @@
|
||||
v-hasPermi="['system:menu:create']"
|
||||
@click="handleCreate()"
|
||||
/>
|
||||
<XButton title="展开所有" @click="xGrid?.setAllTreeExpand(true)" />
|
||||
<XButton title="关闭所有" @click="xGrid?.clearTreeExpand()" />
|
||||
<XButton title="展开所有" @click="xGrid?.Ref.setAllTreeExpand(true)" />
|
||||
<XButton title="关闭所有" @click="xGrid?.Ref.clearTreeExpand()" />
|
||||
</template>
|
||||
<template #name_default="{ row }">
|
||||
<Icon :icon="row.icon" />
|
||||
@ -31,7 +31,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['system:menu:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
@ -194,7 +194,6 @@ import {
|
||||
} from 'element-plus'
|
||||
import { Tooltip } from '@/components/Tooltip'
|
||||
import { IconSelect } from '@/components/Icon'
|
||||
import { VxeGridInstance } from 'vxe-table'
|
||||
// 业务相关的 import
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { SystemMenuTypeEnum, CommonStatusEnum } from '@/utils/constants'
|
||||
@ -206,9 +205,10 @@ import { useXTable } from '@/hooks/web/useXTable'
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { wsCache } = useCache()
|
||||
|
||||
const xGrid = ref<any>(null)
|
||||
|
||||
// 列表相关的变量
|
||||
// 列表相关的变量
|
||||
const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
|
||||
const treeConfig = {
|
||||
transform: true,
|
||||
rowField: 'id',
|
||||
@ -334,10 +334,4 @@ const submitForm = async () => {
|
||||
const isExternal = (path: string) => {
|
||||
return /^(https?:|mailto:|tel:)/.test(path)
|
||||
}
|
||||
|
||||
// ========== 删除 ==========
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
</script>
|
||||
|
@ -32,7 +32,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['system:notice:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
@ -126,11 +126,6 @@ const handleDetail = async (rowId: number) => {
|
||||
detailData.value = res
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
|
||||
// 提交新增/修改的表单
|
||||
const submitForm = async () => {
|
||||
const elForm = unref(formRef)?.getElFormRef()
|
||||
|
@ -48,7 +48,7 @@
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.del')"
|
||||
v-hasPermi="['system:oauth2-client:delete']"
|
||||
@click="handleDelete(row.id)"
|
||||
@click="deleteData(row.id)"
|
||||
/>
|
||||
</template>
|
||||
</XTable>
|
||||
@ -184,11 +184,6 @@ const handleDetail = async (rowId: number) => {
|
||||
detailData.value = res
|
||||
}
|
||||
|
||||
// 删除操作
|
||||
const handleDelete = async (rowId: number) => {
|
||||
await deleteData(rowId)
|
||||
}
|
||||
|
||||
// 提交新增/修改的表单
|
||||
const submitForm = async () => {
|
||||
const elForm = unref(formRef)?.getElFormRef()
|
||||
|
@ -5,7 +5,7 @@
|
||||
<template #actionbtns_default="{ row }">
|
||||
<!-- 操作:详情 -->
|
||||
<XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
|
||||
<!-- 操作:删除 -->
|
||||
<!-- 操作:登出 -->
|
||||
<XTextButton
|
||||
preIcon="ep:delete"
|
||||
:title="t('action.logout')"
|
||||
|
@ -9,7 +9,7 @@
|
||||
preIcon="ep:download"
|
||||
:title="t('action.export')"
|
||||
v-hasPermi="['system:operate-log:export']"
|
||||
@click="handleExport()"
|
||||
@click="exportList('操作日志.xls')"
|
||||
/>
|
||||
</template>
|
||||
<template #duration="{ row }">
|
||||
@ -68,9 +68,4 @@ const handleDetail = (row: OperateLogApi.OperateLogVO) => {
|
||||
detailData.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 导出操作
|
||||
const handleExport = async () => {
|
||||
await exportList('操作日志.xls')
|
||||
}
|
||||
</script>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user