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:
xingyu 2023-01-16 14:25:03 +08:00
commit 1c0d8fc1eb
117 changed files with 2765 additions and 2561 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,10 @@
package cn.iocoder.yudao.framework.web.core.filter;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HTMLFilter;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.web.core.clean.XssCleaner;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedHashMap;
import java.util.Map;
/**
@ -24,113 +13,79 @@ import java.util.Map;
* @author 芋道源码
*/
public class XssRequestWrapper extends HttpServletRequestWrapper {
private final XssCleaner xssCleaner;
/**
* 基于线程级别的 HTMLFilter 对象因为它线程非安全
*/
private static final ThreadLocal<HTMLFilter> HTML_FILTER = ThreadLocal.withInitial(() -> {
HTMLFilter htmlFilter = new HTMLFilter();
// 反射修改 encodeQuotes 属性为 false避免 " 被转移成 &quot; 字符
ReflectUtil.setFieldValue(htmlFilter, "encodeQuotes", false);
return htmlFilter;
});
public XssRequestWrapper(HttpServletRequest request) {
public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) {
super(request);
this.xssCleaner = xssCleaner;
}
private static String filterXss(String content) {
if (StrUtil.isEmpty(content)) {
return content;
// ============================ 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
* 例如说 StringBoolean 等等
*/
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: 提供管理员管理的所有功能

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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