v3.8.0【优化】简介明了的数据范围说明文档;【优化】Long序列化;【优化】标签页右键关闭;【优化】表格排序Demo

This commit is contained in:
zhuoda 2024-10-17 21:55:56 +08:00
parent 753283191a
commit 053d562157
72 changed files with 1838 additions and 331 deletions

View File

@ -1,9 +1,11 @@
package net.lab1024.sa.admin.module.business.goods.domain.form;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import net.lab1024.sa.admin.module.business.goods.constant.GoodsStatusEnum;
import net.lab1024.sa.base.common.domain.PageParam;
import net.lab1024.sa.base.common.json.deserializer.DictValueVoDeserializer;
import net.lab1024.sa.base.common.swagger.SchemaEnum;
import net.lab1024.sa.base.common.validator.enumeration.CheckEnum;
import org.hibernate.validator.constraints.Length;
@ -32,6 +34,7 @@ public class GoodsQueryForm extends PageParam {
private Integer goodsStatus;
@Schema(description = "产地")
@JsonDeserialize(using = DictValueVoDeserializer.class)
private String place;
@Schema(description = "上架状态")

View File

@ -52,7 +52,6 @@ public class NoticeEmployeeService {
public ResponseDTO<PageResult<NoticeEmployeeVO>> queryList(Long requestEmployeeId, NoticeEmployeeQueryForm noticeEmployeeQueryForm) {
Page<?> page = SmartPageUtil.convert2PageQuery(noticeEmployeeQueryForm);
//获取请求人的 部门及其子部门
List<Long> employeeDepartmentIdList = Lists.newArrayList();
EmployeeEntity employeeEntity = employeeService.getById(requestEmployeeId);
// 如果不是管理员 则获取请求人的 部门及其子部门

View File

@ -25,7 +25,7 @@ import java.util.List;
* @Date 2021-12-09 22:57:49
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@RestController
@Tag(name = AdminSwaggerTagConst.System.SYSTEM_EMPLOYEE)
@ -110,7 +110,7 @@ public class EmployeeController {
@Operation(summary = "重置员工密码 @author 卓大")
@GetMapping("/employee/update/password/reset/{employeeId}")
@SaCheckPermission("system:employee:password:reset")
public ResponseDTO<String> resetPassword(@PathVariable Integer employeeId) {
public ResponseDTO<String> resetPassword(@PathVariable Long employeeId) {
return employeeService.resetPassword(employeeId);
}

View File

@ -113,6 +113,6 @@ public interface EmployeeDao extends BaseMapper<EmployeeEntity> {
/**
* 员工重置密码
*/
Integer updatePassword(@Param("employeeId") Integer employeeId, @Param("password") String password);
Integer updatePassword(@Param("employeeId") Long employeeId, @Param("password") String password);
}

View File

@ -47,8 +47,6 @@ import java.util.stream.Collectors;
@Service
public class EmployeeService {
private static final String PASSWORD_SALT_FORMAT = "smart_%s_admin_$^&*";
@Resource
private EmployeeDao employeeDao;
@ -364,7 +362,7 @@ public class EmployeeService {
/**
* 重置密码
*/
public ResponseDTO<String> resetPassword(Integer employeeId) {
public ResponseDTO<String> resetPassword(Long employeeId) {
String password = securityPasswordService.randomPassword();
employeeDao.updatePassword(employeeId, SecurityPasswordService.getEncryptPwd(password));
return ResponseDTO.ok(password);

View File

@ -16,7 +16,7 @@ import java.util.List;
* @Date 2022-04-08 21:53:04
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Service
public class RoleEmployeeManager extends ServiceImpl<RoleEmployeeDao, RoleEmployeeEntity> {

View File

@ -29,7 +29,7 @@ import org.springframework.web.bind.annotation.RestController;
* @Date 2023/10/17 19:07:27
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>Since 2012
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>Since 2012
*/
@RestController

View File

@ -95,25 +95,25 @@
left join t_notice_type on t_notice.notice_type_id = t_notice_type.notice_type_id
<where>
<if test="!administratorFlag">
(
(
t_notice.notice_id in
(select t_notice_visible_range.notice_id
from t_notice_visible_range
where
( t_notice_visible_range.data_type = #{departmentDataType}
<if test="requestEmployeeDepartmentIdList != null and requestEmployeeDepartmentIdList.size > 0">
and
t_notice_visible_range.data_id
in
<foreach collection="requestEmployeeDepartmentIdList" open="(" close=")" separator="," item="item">
#{item}
</foreach>
</if>
)
or ( t_notice_visible_range.data_type = #{employeeDataType} and t_notice_visible_range.data_id = #{requestEmployeeId} )
<if test="requestEmployeeDepartmentIdList != null and requestEmployeeDepartmentIdList.size > 0">
and
t_notice_visible_range.data_id
in
<foreach collection="requestEmployeeDepartmentIdList" open="(" close=")" separator="," item="item">
#{item}
</foreach>
</if>
)
or ( t_notice_visible_range.data_type = #{employeeDataType} and t_notice_visible_range.data_id = #{requestEmployeeId} )
)
or t_notice.all_visible_flag = true
)
)
</if>
and t_notice.deleted_flag = #{deletedFlag}

View File

@ -8,7 +8,7 @@
# 项目配置: 名称、日志目录
project:
name: sa-admin
log-directory: /home/project/smartadmin/sit/log
log-directory: /home/project/smartadmin/test/log
# 项目端口和url根路径
server:

View File

@ -14,15 +14,24 @@ import java.io.IOException;
* @Date 2020-06-02 22:55:07
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public class LongJsonSerializer extends JsonSerializer<Long> {
public static final LongJsonSerializer INSTANCE = new LongJsonSerializer();
@Override
public void serialize(Long value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
String text = (value == null ? null : String.valueOf(value));
if (text != null) {
jsonGenerator.writeString(text);
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
if (null == value) {
gen.writeNull();
return;
}
// js中最大安全整数16位 Number.MAX_SAFE_INTEGER
String longStr = String.valueOf(value);
if (longStr.length() > 16) {
gen.writeString(longStr);
} else {
gen.writeNumber(value);
}
}
}

View File

@ -6,6 +6,7 @@ import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import net.lab1024.sa.base.common.json.serializer.LongJsonSerializer;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
@ -17,16 +18,16 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
/**
* java8 localDate 时间类格式化配置
* Json 序列化配置
*
* @Author 1024创新实验室-主任: 卓大
* @Date 2017-11-28 15:21:10
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Configuration
public class DateConfig {
public class JsonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
@ -35,6 +36,7 @@ public class DateConfig {
builder.deserializers(new LocalDateTimeDeserializer(DatePattern.NORM_DATETIME_FORMAT.getDateTimeFormatter()));
builder.serializers(new LocalDateSerializer(DatePattern.NORM_DATE_FORMAT.getDateTimeFormatter()));
builder.serializers(new LocalDateTimeSerializer(DatePattern.NORM_DATETIME_FORMAT.getDateTimeFormatter()));
builder.serializerByType(Long.class, LongJsonSerializer.INSTANCE);
};
}

View File

@ -31,10 +31,9 @@ import java.util.Optional;
/**
* springdoc-openapi 配置
* nginx配置前缀时如果需要访问/swagger-ui/index.html需添加额外nginx配置
* location /v3/api-docs/ {
* proxy_pass http://127.0.0.1:11024/v3/api-docs/;
* location /v3/api-docs/ {
* proxy_pass http://127.0.0.1:1024/v3/api-docs/;
* }
*
* @Author 1024创新实验室-主任: 卓大
* @Date 2020-03-25 22:54:46
* @Wechat zhuoda1024
@ -48,7 +47,7 @@ public class SwaggerConfig {
/**
* 用于解决/swagger-ui/index.html页面ServersUrl 测试环境部署错误问题
*/
@Value("${springdoc.swagger-ui.server-base-url:''}")
@Value("${springdoc.swagger-ui.server-base-url}")
private String serverBaseUrl;
public static final String[] SWAGGER_WHITELIST = {
@ -59,20 +58,20 @@ public class SwaggerConfig {
"/v3/api-docs",
"/v3/api-docs/**",
"/doc.html",
};
};
@Bean
public OpenAPI api() {
return new OpenAPI()
.components(components())
.info(new Info()
.title("SmartAdmin 3.X 接口文档")
.contact(new Contact().name("1024创新实验室").email("lab1024@163.com").url("https://1024lab.net"))
.version("v3.X")
.description("<font color=\"#DC143C\">**以「高质量代码」为核心,「简洁、高效、安全」**</font>基于 SpringBoot + Sa-Token + Mybatis-Plus 和 Vue3 + Vite5 + Ant Design (同时支持JavaScript和TypeScript双版本) 的快速开发平台。" +
"<br/><font color=\"#DC143C\">**国内首个满足《网络安全》、《数据安全》、三级等保**</font> 支持登录限制、支持国产接口加解密等安全、支持数据加解密等一系列安全体系的开源项目。" +
"<br/><font color=\"#DC143C\">**我们开源一套漂亮的代码和一套整洁的代码规范**</font>,让大家在这浮躁的代码世界里感受到一股把代码写好的清流!同时又让开发者节省大量的时间,减少加班,快乐工作,保持谦逊,保持学习,热爱代码,更热爱生活!")
)
.title("SmartAdmin 3.X 接口文档")
.contact(new Contact().name("1024创新实验室").email("lab1024@163.com").url("https://1024lab.net"))
.version("v3.X")
.description("<font color=\"#DC143C\">**以「高质量代码」为核心,「简洁、高效、安全」**</font>基于 SpringBoot + Sa-Token + Mybatis-Plus 和 Vue3 + Vite5 + Ant Design (同时支持JavaScript和TypeScript双版本) 的快速开发平台。" +
"<br/><font color=\"#DC143C\">**国内首个满足《网络安全》、《数据安全》、三级等保**</font> 支持登录限制、支持国产接口加解密等安全、支持数据加解密等一系列安全体系的开源项目。" +
"<br/><font color=\"#DC143C\">**我们开源一套漂亮的代码和一套整洁的代码规范**</font>,让大家在这浮躁的代码世界里感受到一股把代码写好的清流!同时又让开发者节省大量的时间,减少加班,快乐工作,保持谦逊,保持学习,热爱代码,更热爱生活!")
)
.addSecurityItem(new SecurityRequirement().addList(RequestHeaderConst.TOKEN));
}
@ -84,27 +83,26 @@ public class SwaggerConfig {
@Bean
public GroupedOpenApi businessApi() {
return GroupedOpenApi.builder()
.group("业务接口")
.pathsToMatch("/**")
.pathsToExclude(SwaggerTagConst.Support.URL_PREFIX + "/**")
.addOperationCustomizer(new SmartOperationCustomizer())
.build();
.group("业务接口")
.pathsToMatch("/**")
.pathsToExclude(SwaggerTagConst.Support.URL_PREFIX + "/**")
.addOperationCustomizer(new SmartOperationCustomizer())
.build();
}
@Bean
public GroupedOpenApi supportApi() {
return GroupedOpenApi.builder()
.group("支撑接口(Support)")
.pathsToMatch(SwaggerTagConst.Support.URL_PREFIX + "/**")
.addOperationCustomizer(new SmartOperationCustomizer())
.build();
.group("支撑接口(Support)")
.pathsToMatch(SwaggerTagConst.Support.URL_PREFIX + "/**")
.addOperationCustomizer(new SmartOperationCustomizer())
.build();
}
/**
* 以下代码可以用于设置 /swagger-ui/index.html 的serverBaseUrl
* 如果使用knife4j则不需要
*
* @param openAPI
* @param securityParser
* @param springDocConfigProperties
@ -132,6 +130,6 @@ public class SwaggerConfig {
}
});
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties,
propertyResolverUtils, openApiBuilderCustomizers, Optional.of(list), javadocProvider);
propertyResolverUtils, openApiBuilderCustomizers, Optional.of(list), javadocProvider);
}
}

View File

@ -28,7 +28,7 @@ import java.util.Set;
* @Date 2022-05-30 21:22:12
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Configuration
@Slf4j

View File

@ -67,14 +67,14 @@ public class WebServerListener implements ApplicationListener<WebServerInitializ
String swaggerUrl = URLUtil.normalize(String.format("http://localhost:%d%s/swagger-ui/index.html", port, contextPath), false, true);
String knife4jUrl = URLUtil.normalize(String.format("http://localhost:%d%s/doc.html", port, contextPath), false, true);
log.warn("\n{}\n" +
"\t当前启动环境:\t{} , {}" +
"\n\t返回码初始化:\t完成{}个返回码初始化" +
"\n\t服务本机地址:\t{}" +
"\n\t服务外网地址:\t{}" +
"\n\tSwagger地址:\t{}" +
"\n\tknife4j地址:\t{}" +
"\n-------------------------------------------------------------------------------------\n",
title, profile, environmentEnum.getDesc(), codeCount, localhostUrl, externalUrl, swaggerUrl, knife4jUrl);
"\t当前启动环境:\t{} , {}" +
"\n\t返回码初始化:\t完成{}个返回码初始化" +
"\n\t服务本机地址:\t{}" +
"\n\t服务外网地址:\t{}" +
"\n\tSwagger地址:\t{}" +
"\n\tknife4j地址:\t{}" +
"\n-------------------------------------------------------------------------------------\n",
title, profile, environmentEnum.getDesc(), codeCount, localhostUrl, externalUrl, swaggerUrl, knife4jUrl);
}
/**

View File

@ -1,6 +1,8 @@
package net.lab1024.sa.base.module.support.apiencrypt.advice;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.domain.ResponseDTO;
@ -33,6 +35,9 @@ public class EncryptResponseAdvice implements ResponseBodyAdvice<ResponseDTO> {
@Resource
private ApiEncryptService apiEncryptService;
@Resource
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.hasMethodAnnotation(ApiEncrypt.class) || returnType.getContainingClass().isAnnotationPresent(ApiEncrypt.class);
@ -44,7 +49,12 @@ public class EncryptResponseAdvice implements ResponseBodyAdvice<ResponseDTO> {
return body;
}
String encrypt = apiEncryptService.encrypt(JSON.toJSONString(body.getData()));
String encrypt = null;
try {
encrypt = apiEncryptService.encrypt(objectMapper.writeValueAsString(body.getData()));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
body.setData(encrypt);
body.setDataType(DataTypeEnum.ENCRYPT.getValue());
return body;

View File

@ -52,14 +52,6 @@ public class ApiEncryptServiceSmImpl implements ApiEncryptService {
}
}
public static void main(String[] args) {
String content = "zkm1024";
String en = new ApiEncryptServiceSmImpl().encrypt(content);
System.out.println(en);
String ori = new ApiEncryptServiceSmImpl().decrypt(en);
System.out.println(ori);
}
@Override
public String decrypt(String data) {

View File

@ -45,7 +45,7 @@ import java.util.stream.Collectors;
* @Date 2022-06-30 22:15:38
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Service

View File

@ -18,7 +18,7 @@ import java.util.stream.Collectors;
* @Date 2022/9/29 17:20:41
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public abstract class CodeGenerateBaseVariableService {

View File

@ -101,7 +101,6 @@ public class AddFormVariableService extends CodeGenerateBaseVariableService {
}
}
//字典
if (SmartStringUtil.isNotEmpty(codeField.getDict())) {
finalFieldMap.put("dict", "\n @JsonDeserialize(using = DictValueVoDeserializer.class)");

View File

@ -15,7 +15,7 @@ import java.util.*;
* @Date 2022/9/29 17:20:41
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public class MapperVariableService extends CodeGenerateBaseVariableService {

View File

@ -18,7 +18,7 @@ import java.util.stream.Collectors;
* @Date 2022/9/29 17:20:41
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public class QueryFormVariableService extends CodeGenerateBaseVariableService {

View File

@ -17,7 +17,7 @@ import java.util.stream.Collectors;
* @Date 2022/9/29 17:20:41
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public class VOVariableService extends CodeGenerateBaseVariableService {

View File

@ -39,7 +39,7 @@ import java.util.stream.Collectors;
* @Date 2019年10月11日 15:34:47
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Service
public class FileService {

View File

@ -4,13 +4,13 @@ spring:
url: jdbc:p6spy:mysql://127.0.0.1:3306/smart_admin_v3?autoReconnect=true&useServerPreparedStmts=false&rewriteBatchedStatements=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai
username: root
password: SmartAdmin666
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
initial-size: 2
min-idle: 2
max-active: 10
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
filters: stat
druid:
username: druid
@ -40,7 +40,7 @@ spring:
host: smtp.163.com
port: 465
username: lab1024@163.com
password: 1
password: 1024lab
properties:
mail:
smtp:

View File

@ -40,7 +40,7 @@ spring:
host: smtp.163.com
port: 465
username: lab1024@163.com
password: 1
password: 1024lab
properties:
mail:
smtp:

View File

@ -30,10 +30,11 @@ spring:
timeout: 10000ms
lettuce:
pool:
max-active: 100
min-idle: 10
max-idle: 50
max-active: 5
min-idle: 1
max-idle: 3
max-wait: 30000ms
# 邮件置以SSL的方式发送, 这个需要使用这种方式并且端口是465
mail:
host: smtp.163.com

View File

@ -40,7 +40,7 @@ spring:
host: smtp.163.com
port: 465
username: lab1024@163.com
password: 1
password: 1024lab
properties:
mail:
smtp:

View File

@ -86,7 +86,7 @@ public class AdminInterceptor implements HandlerInterceptor {
Method method = ((HandlerMethod) handler).getMethod();
NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class);
if (noNeedLogin != null) {
checkActiveTimeout(requestEmployee,debugNumberTokenFlag);
checkActiveTimeout(requestEmployee, debugNumberTokenFlag);
return true;
}
@ -96,7 +96,7 @@ public class AdminInterceptor implements HandlerInterceptor {
}
// 检测token 活跃频率
checkActiveTimeout(requestEmployee,debugNumberTokenFlag);
checkActiveTimeout(requestEmployee, debugNumberTokenFlag);
// --------------- 第三步 校验 权限 ---------------
@ -107,8 +107,8 @@ public class AdminInterceptor implements HandlerInterceptor {
}
// 如果是超级管理员的话不需要校验权限
if(requestEmployee.getAdministratorFlag()){
return true;
if (requestEmployee.getAdministratorFlag()) {
return true;
}
SaStrategy.instance.checkMethodAnnotation.accept(method);

View File

@ -110,7 +110,7 @@ public class EmployeeController {
@Operation(summary = "重置员工密码 @author 卓大")
@GetMapping("/employee/update/password/reset/{employeeId}")
@SaCheckPermission("system:employee:password:reset")
public ResponseDTO<String> resetPassword(@PathVariable Integer employeeId) {
public ResponseDTO<String> resetPassword(@PathVariable Long employeeId) {
return employeeService.resetPassword(employeeId);
}

View File

@ -113,6 +113,6 @@ public interface EmployeeDao extends BaseMapper<EmployeeEntity> {
/**
* 员工重置密码
*/
Integer updatePassword(@Param("employeeId") Integer employeeId, @Param("password") String password);
Integer updatePassword(@Param("employeeId") Long employeeId, @Param("password") String password);
}

View File

@ -362,7 +362,7 @@ public class EmployeeService {
/**
* 重置密码
*/
public ResponseDTO<String> resetPassword(Integer employeeId) {
public ResponseDTO<String> resetPassword(Long employeeId) {
String password = securityPasswordService.randomPassword();
employeeDao.updatePassword(employeeId, SecurityPasswordService.getEncryptPwd(password));
return ResponseDTO.ok(password);

View File

@ -14,15 +14,24 @@ import java.io.IOException;
* @Date 2020-06-02 22:55:07
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public class LongJsonSerializer extends JsonSerializer<Long> {
public static final LongJsonSerializer INSTANCE = new LongJsonSerializer();
@Override
public void serialize(Long value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
String text = (value == null ? null : String.valueOf(value));
if (text != null) {
jsonGenerator.writeString(text);
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
if (null == value) {
gen.writeNull();
return;
}
// js中最大安全整数16位 Number.MAX_SAFE_INTEGER
String longStr = String.valueOf(value);
if (longStr.length() > 16) {
gen.writeString(longStr);
} else {
gen.writeNumber(value);
}
}
}

View File

@ -6,6 +6,7 @@ import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import net.lab1024.sa.base.common.json.serializer.LongJsonSerializer;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
@ -17,16 +18,16 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
/**
* java8 localDate 时间类格式化配置
* json 序列化配置
*
* @Author 1024创新实验室-主任: 卓大
* @Date 2017-11-28 15:21:10
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Configuration
public class DateConfig {
public class JsonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
@ -35,6 +36,7 @@ public class DateConfig {
builder.deserializers(new LocalDateTimeDeserializer(DatePattern.NORM_DATETIME_FORMAT.getDateTimeFormatter()));
builder.serializers(new LocalDateSerializer(DatePattern.NORM_DATE_FORMAT.getDateTimeFormatter()));
builder.serializers(new LocalDateTimeSerializer(DatePattern.NORM_DATETIME_FORMAT.getDateTimeFormatter()));
builder.serializerByType(Long.class, LongJsonSerializer.INSTANCE);
};
}

View File

@ -1,6 +1,8 @@
package net.lab1024.sa.base.module.support.apiencrypt.advice;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.enumeration.DataTypeEnum;
@ -34,6 +36,9 @@ public class EncryptResponseAdvice implements ResponseBodyAdvice<ResponseDTO> {
@Resource
private ApiEncryptService apiEncryptService;
@Resource
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.hasMethodAnnotation(ApiEncrypt.class) || returnType.getContainingClass().isAnnotationPresent(ApiEncrypt.class);
@ -45,7 +50,12 @@ public class EncryptResponseAdvice implements ResponseBodyAdvice<ResponseDTO> {
return body;
}
String encrypt = apiEncryptService.encrypt(JSON.toJSONString(body.getData()));
String encrypt = null;
try {
encrypt = apiEncryptService.encrypt(objectMapper.writeValueAsString(body.getData()));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
body.setData(encrypt);
body.setDataType(DataTypeEnum.ENCRYPT.getValue());
return body;

View File

@ -39,7 +39,7 @@ spring:
host: smtp.163.com
port: 465
username: lab1024@163.com
password: 1
password: 1024lab
properties:
mail:
smtp:

View File

@ -39,7 +39,7 @@ spring:
host: smtp.163.com
port: 465
username: lab1024@163.com
password: 1
password: 1024lab
properties:
mail:
smtp:

View File

@ -39,7 +39,7 @@ spring:
host: smtp.163.com
port: 465
username: lab1024@163.com
password: 1
password: 1024lab
properties:
mail:
smtp:

View File

@ -32,6 +32,7 @@
},
},
}"
:getPopupContainer="getPopupContainer"
>
<!---全局loading--->
<a-spin :spinning="spinning" tip="稍等片刻,我在拼命加载中..." size="large">
@ -49,6 +50,8 @@
import { useSpinStore } from '/@/store/modules/system/spin';
import { theme } from 'ant-design-vue';
import { themeColors } from '/@/theme/color.js';
import { SmartLoading } from '/@/components/framework/smart-loading/index.js';
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
const antdLocale = computed(() => messages[useAppConfigStore().language].antdLocale);
const dayjsLocale = computed(() => messages[useAppConfigStore().language].dayjsLocale);
@ -67,4 +70,31 @@
const borderRadius = computed(() => {
return useAppConfigStore().borderRadius;
});
function getPopupContainer(node, dialogContext) {
let fullScreenFlag = useAppConfigStore().$state.fullScreenFlag;
if(fullScreenFlag){
return getFullScreenContainer(node, dialogContext);
}else{
return getNotFullScreenContainer(node, dialogContext);
}
}
function getFullScreenContainer(node, dialogContext) {
if (node === document.body) {
return document.getElementById(LAYOUT_ELEMENT_IDS.content);
}else if (node) {
return node.parentNode;
} else {
return document.getElementById(LAYOUT_ELEMENT_IDS.content);
}
}
function getNotFullScreenContainer(node, dialogContext) {
if (node) {
return node.parentNode;
} else {
return document.body;
}
}
</script>

View File

@ -12,12 +12,12 @@
<template>
<span>
<a-tooltip title="全屏" v-if="!fullScreenFlag">
<a-button type="text" @click="fullScreen" size="small">
<a-button type="text" @click="onFullScreen" size="small">
<template #icon><fullscreen-outlined /></template>
</a-button>
</a-tooltip>
<a-tooltip title="取消全屏" v-if="fullScreenFlag">
<a-button type="text" @click="fullScreen" size="small">
<a-button type="text" @click="onFullScreen" size="small">
<template #icon><fullscreen-exit-outlined /></template>
</a-button>
</a-tooltip>
@ -43,11 +43,13 @@
import { message } from 'ant-design-vue';
import { mergeColumn } from './smart-table-column-merge';
import { smartSentry } from '/@/lib/smart-sentry';
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
import { useAppConfigStore } from '/@/store/modules/system/app-config.js';
const props = defineProps({
//
modelValue: {
type: Array,
default: new Array(),
default: [],
},
//
refresh: {
@ -93,22 +95,39 @@
// ----------------- -------------------
const fullScreenFlag = ref(false);
function fullScreen() {
function onFullScreen() {
if (fullScreenFlag.value) {
//
exitFullscreen(document.querySelector('#smartAdminLayoutContent'));
fullScreenFlag.value = false;
document.querySelector('#smartAdminPageTag').style.visibility = 'visible';
// 退
handleExitFullScreen();
exitElementFullscreen(document.getElementById(LAYOUT_ELEMENT_IDS.content));
} else {
//
launchFullScreen(document.querySelector('#smartAdminLayoutContent'));
message.config({
getContainer: () => document.getElementById(LAYOUT_ELEMENT_IDS.content),
});
fullScreenFlag.value = true;
document.querySelector('#smartAdminPageTag').style.visibility = 'hidden';
useAppConfigStore().startFullScreen();
launchElementFullScreen(document.getElementById(LAYOUT_ELEMENT_IDS.content));
}
}
// 退
function handleExitFullScreen(){
//
message.config({
getContainer: () => document.body,
});
fullScreenFlag.value = false;
useAppConfigStore().exitFullScreen();
document.removeEventListener('fullscreenchange', handleFullscreenChange);
document.removeEventListener('mozfullscreenchange', handleFullscreenChange); // Firefox
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange); // Chrome, Safari and Opera
document.removeEventListener('MSFullscreenChange', handleFullscreenChange); // Internet Explorer and Edge
}
// -
function launchFullScreen(element) {
function launchElementFullScreen(element) {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
@ -120,9 +139,23 @@
} else {
message.error('当前浏览器不支持部分全屏!');
}
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('mozfullscreenchange', handleFullscreenChange); // Firefox
document.addEventListener('webkitfullscreenchange', handleFullscreenChange); // Chrome, Safari and Opera
document.addEventListener('MSFullscreenChange', handleFullscreenChange); // Internet Explorer and Edge
}
function handleFullscreenChange() {
if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
console.log('进入全屏模式');
} else {
console.log('退出全屏模式');
handleExitFullScreen();
}
}
// -退
function exitFullscreen(element) {
function exitElementFullscreen(element) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {

View File

@ -23,7 +23,7 @@
</a-tab-pane>
</a-tabs>
</div>
<template #rightExtra>
<template #overlay>
<a-menu>
<a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item>
<a-menu-item @click="closeByMenu(true)">关闭所有</a-menu-item>

View File

@ -24,7 +24,7 @@
</a-tab-pane>
</a-tabs>
</div>
<template #rightExtra>
<template #overlay>
<a-menu>
<a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item>
<a-menu-item @click="closeByMenu(true)">关闭所有</a-menu-item>

View File

@ -79,10 +79,10 @@
const websiteName = computed(() => useAppConfigStore().websiteName);
const windowHeight = window.innerHeight;
onMounted(() => {
watermark.set('smartAdminLayoutContent', useUserStore().actualName);
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
});
const backTopTarget = () => {
return document.getElementById('smartAdminMain');
return document.getElementById(LAYOUT_ELEMENT_IDS.main);
};
const router = useRouter();
const route = useRoute();

View File

@ -0,0 +1,9 @@
/**
* layout 相关元素 id
*/
export const LAYOUT_ELEMENT_IDS = {
menu: 'smartAdminMenu',
main: 'smartAdminMain',
header: 'smartAdminHeader',
content: 'smartAdminLayoutContent',
}

View File

@ -10,15 +10,15 @@
<template>
<a-layout class="admin-layout" style="min-height: 100%">
<!-- 侧边菜单 side-menu -->
<a-layout-sider :theme="theme" class="side-menu" :collapsed="collapsed" :trigger="null">
<a-layout-sider :id="LAYOUT_ELEMENT_IDS.menu" :theme="theme" class="side-menu" :collapsed="collapsed" :trigger="null">
<!-- 左侧菜单 -->
<SideExpandMenu :collapsed="collapsed" />
</a-layout-sider>
<!--中间内容一共三部分1顶部;2中间内容区域;3底部一般是公司版权信息;-->
<a-layout class="admin-layout-main" :style="`height: ${windowHeight}px`" id="smartAdminMain">
<a-layout class="admin-layout-main" :style="`height: ${windowHeight}px`" :id="LAYOUT_ELEMENT_IDS.main">
<!-- 顶部头部信息 -->
<a-layout-header class="smart-layout-header">
<a-layout-header class="smart-layout-header" :id="LAYOUT_ELEMENT_IDS.header">
<a-row justify="space-between" class="smart-layout-header-user">
<a-col class="smart-layout-header-left">
<span class="collapsed-button">
@ -44,19 +44,19 @@
</a-layout-header>
<!--中间内容-->
<a-layout-content class="admin-layout-content" id="smartAdminLayoutContent">
<a-layout-content class="admin-layout-content" :id="LAYOUT_ELEMENT_IDS.content">
<!--不keepAlive的iframe使用单个iframe组件-->
<IframeIndex v-show="iframeNotKeepAlivePageFlag" :key="route.name" :name="route.name" :url="route.meta.frameUrl" />
<!--keepAlive的iframe 每个页面一个iframe组件-->
<IframeIndex
v-for="item in keepAliveIframePages"
v-show="route.name == item.name"
v-show="route.name === item.name"
:key="item.name"
:name="item.name"
:url="item.meta.frameUrl"
/>
<!--非iframe使用router-view-->
<div v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name != e.name)">
<div v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name !== e.name)">
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveIncludes">
<component :is="Component" :key="route.name" />
@ -98,6 +98,7 @@
import SideHelpDoc from './components/side-help-doc/index.vue';
import { useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
const windowHeight = ref(window.innerHeight);
@ -130,7 +131,7 @@
//
onMounted(() => {
if (watermarkFlag.value) {
watermark.set('smartAdminLayoutContent', useUserStore().actualName);
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
} else {
watermark.clear();
}
@ -140,7 +141,7 @@
() => watermarkFlag.value,
(newValue) => {
if (newValue) {
watermark.set('smartAdminLayoutContent', useUserStore().actualName);
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
} else {
watermark.clear();
}
@ -153,7 +154,7 @@
//
const backTopTarget = () => {
return document.getElementById('smartAdminMain');
return document.getElementById(LAYOUT_ELEMENT_IDS.main);
};
// ----------------------- keep-alive -----------------------
let { route, keepAliveIncludes, iframeNotKeepAlivePageFlag, keepAliveIframePages } = smartKeepAlive();

View File

@ -1,15 +1,15 @@
<template>
<a-layout class="admin-layout" style="min-height: 100%">
<!-- 侧边菜单 side-menu -->
<a-layout-sider class="side-menu" :width="sideMenuWidth" :collapsed="collapsed" :theme="theme">
<a-layout-sider :id="LAYOUT_ELEMENT_IDS.menu" class="side-menu" :width="sideMenuWidth" :collapsed="collapsed" :theme="theme">
<!-- 左侧菜单 -->
<SideMenu :collapsed="collapsed" />
</a-layout-sider>
<!--中间内容一共三部分1顶部;2中间内容区域;3底部一般是公司版权信息;-->
<a-layout id="smartAdminMain" :style="`height: ${windowHeight}px`" class="admin-layout-main">
<a-layout :id="LAYOUT_ELEMENT_IDS.main" :style="`height: ${windowHeight}px`" class="admin-layout-main">
<!-- 顶部头部信息 -->
<a-layout-header class="layout-header">
<a-layout-header class="layout-header" :id="LAYOUT_ELEMENT_IDS.header">
<a-row class="layout-header-user" justify="space-between">
<a-col class="layout-header-left">
<span class="collapsed-button">
@ -35,13 +35,13 @@
</a-layout-header>
<!--中间内容-->
<a-layout-content id="smartAdminLayoutContent" class="admin-layout-content">
<a-layout-content :id="LAYOUT_ELEMENT_IDS.content" class="admin-layout-content">
<!--不keepAlive的iframe使用单个iframe组件-->
<IframeIndex v-if="iframeNotKeepAlivePageFlag" :key="route.name" :name="route.name" :url="route.meta.frameUrl" />
<!--keepAlive的iframe 每个页面一个iframe组件-->
<IframeIndex
v-for="item in keepAliveIframePages"
v-show="route.name == item.name"
v-show="route.name === item.name"
:key="item.name"
:name="item.name"
:url="item.meta.frameUrl"
@ -93,6 +93,7 @@
import SideHelpDoc from './components/side-help-doc/index.vue';
import { useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
const windowHeight = ref(window.innerHeight);
//
@ -126,7 +127,7 @@
//
onMounted(() => {
if (watermarkFlag.value) {
watermark.set('smartAdminLayoutContent', useUserStore().actualName);
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
} else {
watermark.clear();
}
@ -136,7 +137,7 @@
() => watermarkFlag.value,
(newValue) => {
if (newValue) {
watermark.set('smartAdminLayoutContent', useUserStore().actualName);
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
} else {
watermark.clear();
}
@ -145,7 +146,7 @@
//
const backTopTarget = () => {
return document.getElementById('smartAdminMain');
return document.getElementById(LAYOUT_ELEMENT_IDS.main);
};
const router = useRouter();

View File

@ -1,14 +1,14 @@
<template>
<a-layout class="admin-layout">
<!-- 顶部菜单 -->
<a-layout-header class="top-menu" :theme="theme">
<a-layout-header class="top-menu" :theme="theme" :id="LAYOUT_ELEMENT_IDS.menu">
<TopMenu />
</a-layout-header>
<!--中间内容-->
<a-layout-content id="smartAdminLayoutContent" class="admin-layout-content">
<a-layout-content :id="LAYOUT_ELEMENT_IDS.content" class="admin-layout-content">
<!---标签页-->
<div class="page-tag-div" v-show="pageTagFlag">
<div class="page-tag-div" v-show="pageTagFlag" :id="LAYOUT_ELEMENT_IDS.header">
<PageTag />
</div>
@ -18,14 +18,14 @@
<!--keepAlive的iframe 每个页面一个iframe组件-->
<IframeIndex
v-for="item in keepAliveIframePages"
v-show="route.name == item.name"
v-show="route.name === item.name"
:key="item.name"
:name="item.name"
:url="item.meta.frameUrl"
/>
<!--非iframe使用router-view-->
<div v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name != e.name)">
<div v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name !== e.name)">
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveIncludes">
<component :is="Component" :key="route.name" />
@ -55,6 +55,7 @@
import { useUserStore } from '/@/store/modules/system/user';
import { useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
const windowHeight = ref(window.innerHeight);
//
@ -87,7 +88,7 @@
//
onMounted(() => {
if (watermarkFlag.value) {
watermark.set('smartAdminLayoutContent', useUserStore().actualName);
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
} else {
watermark.clear();
}
@ -97,7 +98,7 @@
() => watermarkFlag.value,
(newValue) => {
if (newValue) {
watermark.set('smartAdminLayoutContent', useUserStore().actualName);
watermark.set(LAYOUT_ELEMENT_IDS.content, useUserStore().actualName);
} else {
watermark.clear();
}
@ -106,7 +107,7 @@
//
const backTopTarget = () => {
return document.getElementById('smartAdminMain');
return document.getElementById(LAYOUT_ELEMENT_IDS.main);
};
const router = useRouter();

View File

@ -13,7 +13,9 @@ import localStorageKeyConst from '/@/constants/local-storage-key-const';
import { smartSentry } from '/@/lib/smart-sentry';
import { localRead } from '/@/utils/local-util';
let state = { ...appDefaultConfig };
let state = {
...appDefaultConfig
};
let appConfigStr = localRead(localStorageKeyConst.APP_CONFIG);
let language = appDefaultConfig.language;
@ -38,6 +40,8 @@ export const useAppConfigStore = defineStore({
state: () => ({
// 读取config下的默认配置
...state,
// 全屏
fullScreenFlag: false,
}),
actions: {
reset() {
@ -51,5 +55,11 @@ export const useAppConfigStore = defineStore({
hideHelpDoc() {
this.helpDocExpandFlag = false;
},
startFullScreen() {
this.fullScreenFlag = true;
},
exitFullScreen() {
this.fullScreenFlag = false;
},
},
});

View File

@ -97,7 +97,6 @@
</div>
</a-row>
<!---------- 表格操作行 end ----------->
<a-table
size="small"
:dataSource="tableData"
@ -105,7 +104,9 @@
rowKey="goodsId"
bordered
:pagination="false"
:showSorterTooltip="false"
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
@change="onChange"
>
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'place'">
@ -190,6 +191,7 @@
import SmartEnumSelect from '/@/components/framework/smart-enum-select/index.vue';
import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const.js';
import FileUpload from '/@/components/support/file-upload/index.vue';
import _ from 'lodash';
// ---------------------------- ----------------------------
@ -205,6 +207,7 @@
{
title: '商品状态',
dataIndex: 'goodsStatus',
sorter: true
},
{
title: '产地',
@ -213,10 +216,12 @@
{
title: '商品价格',
dataIndex: 'price',
sorter: true
},
{
title: '上架状态',
dataIndex: 'shelvesFlag',
sorter: true
},
{
title: '备注',
@ -246,9 +251,10 @@
goodsType: undefined,
pageNum: 1,
pageSize: 10,
sortItemList: []
};
// form
const queryForm = reactive({ ...queryFormState });
const queryForm = reactive(_.cloneDeep(queryFormState));
// loading
const tableLoading = ref(false);
//
@ -259,7 +265,7 @@
//
function resetQuery() {
let pageSize = queryForm.pageSize;
Object.assign(queryForm, queryFormState);
Object.assign(queryForm, _.cloneDeep(queryFormState));
queryForm.pageSize = pageSize;
queryData();
}
@ -414,4 +420,27 @@
async function onExportGoods() {
await goodsApi.exportGoods();
}
function onChange(pagination, filters, sorter, { action }){
if (action === 'sort') {
const { order, field } = sorter;
let column = camelToUnderscore(field);
let findIndex = queryForm.sortItemList.findIndex(e => e.column === column);
if (findIndex !== -1) {
queryForm.sortItemList.splice(findIndex, 1);
}
if (order) {
let isAsc = order !== 'ascend';
queryForm.sortItemList.push({
column,
isAsc
});
}
queryData();
}
}
function camelToUnderscore(str) {
return str.replace(/([A-Z])/g, "_$1").toLowerCase();
}
</script>

View File

@ -1,5 +1,5 @@
VITE_APP_API_URL = 'http://smartadmin.dev.1024lab.net/api/'
VITE_APP_API_URL = 'http://smartadmin.dev.1024lab.net/sa-api/'
VITE_APP_PROJECT_TITLE = 'SmartAdmin 开发环境(Dev)'

View File

@ -1,5 +1,5 @@
VITE_APP_API_URL = 'http://smartadmin.dev.1024lab.net/api/'
VITE_APP_API_URL = 'http://smartadmin.dev.1024lab.net/sa-api/'
VITE_APP_PROJECT_TITLE = 'SmartAdmin 测试环境(Test)'

View File

@ -48,8 +48,10 @@
"vue3-tabs-chrome": "^0.3.3"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/lodash": "^4.14.186",
"@types/nprogress": "^0.2.0",
"@types/sm-crypto": "^0.3.4",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"@vitejs/plugin-legacy": "5.4.1",

View File

@ -0,0 +1,17 @@
/**
* 数据脱敏api
*
* @Author: 1024创新实验室-主任-卓大
* @Date: 2024-07-31 21:02:37
* @Copyright 1024创新实验室
*/
import { getRequest } from '/src/lib/axios';
export const dataMaskingApi = {
/**
* 查询脱敏数据
*/
query: () => {
return getRequest('/support/dataMasking/demo/query');
},
};

View File

@ -32,7 +32,7 @@ export const fileApi = {
/**
* 下载文件流根据fileKey @author 胡克
*/
downLoadFile: (fileKey) => {
return getDownload('/support/file/downLoad', { fileKey });
downLoadFile: (fileName, fileKey) => {
return getDownload(fileName, '/support/file/downLoad', { fileKey });
},
};

View File

@ -13,5 +13,5 @@ export const heartBeatApi = {
// 分页查询 @author 卓大
queryList: (param) => {
return postRequest('/support/heartBeat/query', param);
},
}
};

View File

@ -0,0 +1,24 @@
/**
* 三级等保 api 封装
*
* @Author: 1024创新实验室-主任-卓大
* @Date: 2024-07-31 21:02:37
* @Copyright 1024创新实验室
*/
import { postRequest, getRequest } from '/src/lib/axios';
export const level3ProtectApi = {
/**
* 查询 三级等保配置 @author 1024创新实验室-主任-卓大
*/
getConfig: () => {
return getRequest('/support/protect/level3protect/getConfig');
},
/**
* 更新三级等保配置 @author 1024创新实验室-主任-卓大
*/
updateConfig: (form) => {
return postRequest('/support/protect/level3protect/updateConfig', form);
},
};

View File

@ -24,7 +24,7 @@ export const departmentApi = {
* @param {*}
* @return {*}
*/
queryDepartmentTree: () => {
queryDepartmentTree: () => {
return getRequest('/department/treeList');
},
@ -50,6 +50,6 @@ export const departmentApi = {
* @return {*}
*/
deleteDepartment: (departmentId) => {
return getRequest(`/department/delete/${departmentId}`);
},
return getRequest(`/department/delete/${ departmentId }`);
}
};

View File

@ -48,4 +48,18 @@ export const loginApi = {
refresh: () => {
return getRequest('/login/refresh');
},
/**
* 获取邮箱登录验证码 @author 卓大
*/
sendLoginEmailCode: (loginName) => {
return getRequest(`/login/sendEmailCode/${loginName}`);
},
/**
* 获取双因子登录标识 @author 卓大
*/
getTwoFactorLoginFlag: () => {
return getRequest('/login/getTwoFactorLoginFlag');
},
};

View File

@ -0,0 +1,55 @@
/**
* 职务表 api 封装
*
* @Author: kaiyun
* @Date: 2024-06-23 23:31:38
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
import { postRequest, getRequest } from '/@/lib/axios';
export const positionApi = {
/**
* 分页查询 @author kaiyun
*/
queryPage : (param) => {
return postRequest('/position/queryPage', param);
},
/**
* 增加 @author kaiyun
*/
add: (param) => {
return postRequest('/position/add', param);
},
/**
* 修改 @author kaiyun
*/
update: (param) => {
return postRequest('/position/update', param);
},
/**
* 删除 @author kaiyun
*/
delete: (id) => {
return getRequest(`/position/delete/${id}`);
},
/**
* 批量删除 @author kaiyun
*/
batchDelete: (idList) => {
return postRequest('/position/batchDelete', idList);
},
/**
* 查询列表 @author kaiyun
*/
queryList: () => {
return getRequest('/position/queryList');
},
};

View File

@ -23,7 +23,7 @@
</a-tab-pane>
</a-tabs>
</div>
<template #rightExtra>
<template #overlay>
<a-menu>
<a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item>
<a-menu-item @click="closeByMenu(true)">关闭所有</a-menu-item>

View File

@ -23,7 +23,7 @@
</a-tab-pane>
</a-tabs>
</div>
<template #rightExtra>
<template #overlay>
<a-menu>
<a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item>
<a-menu-item @click="closeByMenu(true)">关闭所有</a-menu-item>

View File

@ -1,32 +1,45 @@
// @ts-ignore
import CryptoJS from 'crypto-js';
// @ts-ignore
import CryptoSM from 'sm-crypto';
function object2string(data) {
if (typeof data === 'Object') {
function object2string(data: any) {
if (typeof data === 'object') {
return JSON.stringify(data);
}
let str = JSON.stringify(data);
if (str.startsWith("'") || str.startsWith('"')) {
if (str.startsWith('\'') || str.startsWith('"')) {
str = str.substring(1);
}
if (str.endsWith("'") || str.endsWith('"')) {
if (str.endsWith('\'') || str.endsWith('"')) {
str = str.substring(0, str.length - 1);
}
return str;
}
// ----------------------- AES 加密、解密 -----------------------
const AES_KEY = '1024abcd1024abcd1024abcd1024abcd';
/**
*
*/
function stringToHex(str: string) {
let hex = '';
for(let i = 0; i < str.length; i++) {
hex += str.charCodeAt(i).toString(16).padStart(2, '0');
}
return hex;
}
const AES = {
// ----------------------- AES 加密、解密 -----------------------
const AES_KEY = '1024lab__1024lab';
export const AES = {
encryptData: function (data: any) {
// AES 加密 并转为 base64
let utf8Data = CryptoJS.enc.Utf8.parse(object2string(data));
const key = CryptoJS.enc.Utf8.parse(AES_KEY);
const encrypted = CryptoJS.AES.encrypt(utf8Data, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
@ -38,20 +51,20 @@ const AES = {
// 第二步AES 解密
const key = CryptoJS.enc.Utf8.parse(AES_KEY);
return CryptoJS.AES.decrypt({ ciphertext: words }, key, {
return CryptoJS.AES.decrypt({ciphertext: words}, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
padding: CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8);
},
}
};
// ----------------------- 国密SM4算法 加密、解密 -----------------------
const SM4_KEY = '1024abcd1024abcd1024abcd1024abcd';
const SM4_KEY = '1024lab__1024lab';
const SM4 = {
encryptData: function (data: any) {
// 第一步SM4 加密
let encryptData = CryptoSM.sm4.encrypt(object2string(data), SM4_KEY);
let encryptData = CryptoSM.sm4.encrypt(object2string(data), stringToHex(SM4_KEY));
// 第二步: Base64 编码
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(encryptData));
},
@ -62,8 +75,8 @@ const SM4 = {
let decode64Str = CryptoJS.enc.Utf8.stringify(words);
// 第二步SM4 解密
return CryptoSM.sm4.decrypt(decode64Str, SM4_KEY);
},
return CryptoSM.sm4.decrypt(decode64Str, stringToHex(SM4_KEY));
}
};
// ----------------------- 对外暴露: 加密、解密 -----------------------

View File

@ -10,21 +10,21 @@
import nProgress from 'nprogress';
import 'nprogress/nprogress.css';
import { nextTick } from 'vue';
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import { routerArray } from './routers';
import { PAGE_PATH_404, PAGE_PATH_LOGIN } from '/@/constants/common-const';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import {nextTick} from 'vue';
import {createRouter, createWebHashHistory, RouteRecordRaw} from 'vue-router';
import {routerArray} from './routers';
import {PAGE_PATH_404, PAGE_PATH_LOGIN} from '/@/constants/common-const';
import {HOME_PAGE_NAME} from '/@/constants/system/home-const';
import SmartLayout from '/@/layout/smart-layout.vue';
import { useUserStore } from '/@/store/modules/system/user';
import { clearAllCoolies, getTokenFromCookie } from '/@/utils/cookie-util';
import { localClear } from '/@/utils/local-util';
import {useUserStore} from '/@/store/modules/system/user';
import {clearAllCoolies, getTokenFromCookie} from '/@/utils/cookie-util';
import {localClear} from '/@/utils/local-util';
export const router = createRouter({
history: createWebHashHistory(),
routes: routerArray,
strict: true,
scrollBehavior: () => ({ left: 0, top: 0 }),
scrollBehavior: () => ({left: 0, top: 0})
});
// ----------------------- 路由加载前 -----------------------
@ -43,7 +43,7 @@ router.beforeEach(async (to, from, next) => {
if (!token) {
clearAllCoolies();
localClear();
next({ path: PAGE_PATH_LOGIN });
next({path: PAGE_PATH_LOGIN});
return;
}
@ -79,6 +79,7 @@ router.afterEach(() => {
// ----------------------- 构建router对象 -----------------------
const routerMap = new Map();
export function buildRoutes(menuRouterList) {
let menuList = menuRouterList ? menuRouterList : useUserStore().getMenuRouterList || [];
/**
@ -119,8 +120,8 @@ export function buildRoutes(menuRouterList) {
// 是否为外链
frameFlag: e.frameFlag,
// 外链地址
frameUrl: e.frameUrl,
},
frameUrl: e.frameUrl
}
};
if (e.frameFlag) {
@ -151,6 +152,6 @@ export function buildRoutes(menuRouterList) {
path: '/',
meta: {},
component: SmartLayout,
children: resList,
children: resList
});
}

View File

@ -270,7 +270,7 @@ function view(file) {
//
async function download(file) {
try {
await fileApi.downLoadFile(file.fileKey);
await fileApi.downLoadFile(file.fileName, file.fileKey);
} catch (e) {
smartSentry.captureError(e);
}

View File

@ -0,0 +1,137 @@
<!--
* 数据脱敏
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2024-08-02 20:23:08
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-card size="small" :bordered="false" :hoverable="true">
<a-alert>
<template v-slot:message>
<h4>数据脱敏 Data Masking介绍</h4>
</template>
<template v-slot:description>
<pre>
简介信息安全技术 网络安全等级保护基本要求明确规定二级以上保护则需要对敏感数据进行脱敏处理
原理数据脱敏是指对某些敏感信息通过脱敏规则进行数据的变形实现敏感隐私数据的可靠保护
举例在不违反系统规则条件下身份证号手机号卡号客户号等个人信息都需要进行数据脱敏
使用方式
1脱敏注解 @DataMasking 支持数据类型如用户ID手机号密码地址银行卡车牌号等
2脱敏工具类 SmartDataMaskingUtil
</pre
>
</template>
</a-alert>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button type="primary" @click="onSearch">
<template #icon>
<ReloadOutlined/>
</template>
查询
</a-button>
</a-button-group>
</a-form-item>
</a-row>
</a-form>
<a-table
size="small"
bordered
:scroll="{ x: 1100 }"
:loading="tableLoading"
class="smart-margin-top10"
:dataSource="tableData"
:columns="columns"
:pagination="false"
/>
</a-card>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { heartBeatApi } from '/@/api/support/heart-beat/heart-beat-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { defaultTimeRanges } from '/@/lib/default-time-ranges';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
import { dataMaskingApi } from '/@/api/support/data-masking/data-masking-api.js';
//------------------------ ---------------------
const columns = ref([
{
title: '用户ID',
dataIndex: 'userId',
width: 70
},
{
title: '默认',
dataIndex: 'other',
width: 100
},
{
title: '手机号',
dataIndex: 'phone',
width: 100
},
{
title: '身份证',
dataIndex: 'idCard',
width: 150
},
{
title: '密码',
dataIndex: 'password',
width: 100
},
{
title: '邮箱',
dataIndex: 'email',
width: 120
},
{
title: '车牌号',
dataIndex: 'carLicense',
width: 120
},
{
title: '银行卡',
dataIndex: 'bankCard',
width: 170
},
{
title: '地址',
dataIndex: 'address',
width: 210
}
]);
const tableLoading = ref(false);
const tableData = ref([]);
function onSearch() {
ajaxQuery();
}
async function ajaxQuery() {
try {
tableLoading.value = true;
let responseModel = await dataMaskingApi.query();
tableData.value = responseModel.data;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
onMounted(ajaxQuery);
</script>

View File

@ -0,0 +1,254 @@
<!--
* 三级等保配置
*
* @Author: 1024创新实验室-主任-卓大
* @Date: 2024-07-31 22:02:37
* @Copyright 1024创新实验室
-->
<template>
<a-alert closable>
<template v-slot:message>
<h4>三级等保</h4>
</template>
<template v-slot:description>
<pre>
1.三级等保是中国国家等级保护认证中的最高级别认证该认证包含了五个等级保护安全技术要求和五个安全管理要求共涉及测评分类73类要求非常严格
2.三级等保是地市级以上国家机关重要企事业单位需要达成的认证在金融行业中可以看作是除了银行机构以外最高级别的信息安全等级保护
3.具体三级等保要求请查看1024创新实验室写的相关文档 <a href="https://smartadmin.vip/views/level3protect/basic.html" target="_blank">三级等保文档</a></pre>
</template>
</a-alert>
<br />
<!---------- 三级等保配置表单 begin ----------->
<a-card title="三级等保配置">
<a-form
:model="form"
:rules="rules"
ref="formRef"
style="width: 800px"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
autocomplete="off"
class="smart-query-form"
>
<a-form-item
label="配置双因子登录模式"
class="smart-query-form-item"
extra="在用户登录时,需要同时提供用户名和密码以及其他形式的身份验证信息,例如短信验证码等"
>
<a-switch v-model:checked="form.twoFactorLoginEnabled" checked-children="开启 " un-checked-children="关闭 " />
</a-form-item>
<a-form-item
label="最大连续登录失败次数"
class="smart-query-form-item"
extra="连续登录失败超过一定次数则需要锁定默认5次0则不锁定"
name="loginFailMaxTimes"
>
<a-input-number :min="0" :max="10" v-model:value="form.loginFailMaxTimes" placeholder="最大连续登录失败次数" addon-after="" />
</a-form-item>
<a-form-item
name="loginFailLockMinutes"
label="连续登录失败锁定分钟"
class="smart-query-form-item"
extra="连续登录失败锁定的时间默认30分钟0则不锁定"
>
<a-input-number :min="0" v-model:value="form.loginFailLockMinutes" placeholder="连续登录失败锁定时分钟" addon-after="分钟" />
</a-form-item>
<a-form-item
name="loginActiveTimeoutMinutes"
label="登录后无操作自动退出的分钟"
class="smart-query-form-item"
extra="如登录1小时没操作自动退出当前登录状态默认30分钟"
>
<a-input-number :min="-1" v-model:value="form.loginActiveTimeoutMinutes" placeholder="登录后无操作自动退出的分钟" addon-after="分钟" />
</a-form-item>
<a-form-item
label="开启密码复杂度"
class="smart-query-form-item"
extra="密码长度为8-20位且必须包含字母、数字、特殊符号@#$%^&*()_+-=)等三种字符"
>
<a-switch v-model:checked="form.passwordComplexityEnabled" checked-children="开启 " un-checked-children="关闭 " />
</a-form-item>
<a-form-item
name="regularChangePasswordMonths"
label="定期修改密码时间间隔"
class="smart-query-form-item"
extra="定期修改密码时间间隔默认3个月"
>
<a-input-number :min="-1" :max="6" v-model:value="form.regularChangePasswordMonths" placeholder="定期修改密码时间间隔" addon-after="" />
</a-form-item>
<a-form-item
name="regularChangePasswordNotAllowRepeatTimes"
label="定期修改密码不允许重复次数"
class="smart-query-form-item"
extra="定期修改密码不允许重复次数默认3次以内密码不能相同"
>
<a-input-number
:min="-1"
:max="6"
v-model:value="form.regularChangePasswordNotAllowRepeatTimes"
placeholder="相同密码不允许重复次数"
addon-after="次"
/>
</a-form-item>
<a-form-item
label="文件安全检测"
class="smart-query-form-item"
extra="对文件类型、恶意文件进行检测;(具体请看后端: SecurityFileService 类 checkFile 方法 "
>
<a-switch v-model:checked="form.fileDetectFlag" checked-children="开启 " un-checked-children="关闭 " />
</a-form-item>
<a-form-item
name="maxUploadFileSizeMb"
label="上传文件大小限制"
class="smart-query-form-item"
extra="上传文件大小限制,默认 50 mb ( 0 表示不限制)"
>
<a-input-number :min="0" v-model:value="form.maxUploadFileSizeMb" placeholder="上传文件大小限制" addon-after="mb()" />
</a-form-item>
<br />
<a-form-item :wrapper-col="{ span: 14, offset: 6 }">
<a-button type="primary" style="margin-right: 20px" @click.prevent="onSubmit">保存配置</a-button>
<a-button style="margin-right: 20px" @click="reset">恢复三级等保默认配置</a-button>
<a-button danger @click="clear">清除所有配置</a-button>
</a-form-item>
</a-form>
</a-card>
<!---------- 请求参数加密 end ----------->
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { level3ProtectApi } from '/@/api/support/level3-protect/level3-protect-api.js';
import { SmartLoading } from '/@/components/framework/smart-loading/index.ts';
import { smartSentry } from '/@/lib/smart-sentry.ts';
import { message, Modal } from 'ant-design-vue';
//
const protectDefaultValues = {
//
loginFailMaxTimes: 5,
//
loginFailLockMinutes: 30,
//
loginActiveTimeoutMinutes: 30,
//
passwordComplexityEnabled: true,
//
regularChangePasswordMonths: 3,
// 3
regularChangePasswordNotAllowRepeatTimes: 3,
//
twoFactorLoginEnabled: true,
//
fileDetectFlag: true,
// mb (50 mb)
maxUploadFileSizeMb: 50,
};
//
const noProtectDefaultValues = {
//
loginFailMaxTimes: 0,
//
loginFailLockMinutes: 0,
//
loginActiveTimeoutMinutes: 0,
//
passwordComplexityEnabled: false,
//
regularChangePasswordMonths: 0,
//
regularChangePasswordNotAllowRepeatTimes: 0,
//
twoFactorLoginEnabled: false,
// mb
maxUploadFileSizeMb: 0,
};
//
const form = reactive({
...protectDefaultValues,
});
const rules = {
loginFailMaxTimes: [{ required: true, message: '请输入 最大连续登录失败次数' }],
loginFailLockMinutes: [{ required: true, message: '请输入 连续登录失败锁定分钟' }],
loginActiveTimeoutMinutes: [{ required: true, message: '请输入 最低活跃时间分钟' }],
regularChangePasswordMonths: [{ required: true, message: '请输入 定期修改密码时间间隔' }],
regularChangePasswordNotAllowRepeatTimes: [{ required: true, message: '请输入 定期修改密码时间间隔' }],
maxUploadFileSizeMb: [{ required: true, message: '请输入 上传文件大小限制' }],
};
//
async function getConfig() {
SmartLoading.show();
try {
let res = await level3ProtectApi.getConfig();
if (!res.data) {
message.warn('当前未配置三级等保');
return;
}
let json = JSON.parse(res.data);
form.loginFailMaxTimes = json.loginFailMaxTimes;
form.loginFailLockMinutes = json.loginFailLockMinutes;
form.loginActiveTimeoutMinutes = json.loginActiveTimeoutMinutes;
form.passwordComplexityEnabled = json.passwordComplexityEnabled;
form.regularChangePasswordMonths = json.regularChangePasswordMonths;
form.regularChangePasswordNotAllowRepeatTimes = json.regularChangePasswordNotAllowRepeatTimes;
form.twoFactorLoginEnabled = json.twoFactorLoginEnabled;
form.maxUploadFileSizeMb = json.maxUploadFileSizeMb;
form.fileDetectFlag = json.fileDetectFlag;
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
onMounted(getConfig);
const formRef = ref();
//
function onSubmit() {
formRef.value
.validate()
.then(save)
.catch((error) => {
message.error('参数验证错误,请仔细填写表单数据!');
});
}
//
async function save() {
SmartLoading.show();
try {
let res = await level3ProtectApi.updateConfig(form);
message.success(res.msg);
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
//
function reset() {
Object.assign(form, protectDefaultValues);
save();
}
//
function clear() {
Modal.confirm({
title: '提示',
content: '确定要清除三级等保配置吗?这样系统不安全哦',
okText: '清除三级等保配置',
okType: 'danger',
onOk() {
Object.assign(form, noProtectDefaultValues);
save();
},
cancelText: '取消',
onCancel() {},
});
}
</script>

View File

@ -0,0 +1,138 @@
<!--
* 部门表单 弹窗
*
* @Author: 1024创新实验室-主任卓大
* @Date: 2022-08-08 20:46:18
* @Wechat: zhuda1024
* @Email: lab1024@163.com
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<a-modal v-model:open="visible" :title="formState.departmentId ? '编辑部门' : '添加部门'" @ok="handleOk" destroyOnClose>
<a-form ref="formRef" :model="formState" :rules="rules" layout="vertical">
<a-form-item label="上级部门" name="parentId" v-if="formState.parentId != 0">
<DepartmentTreeSelect ref="departmentTreeSelect" v-model:value="formState.parentId" :defaultValueFlag="false" width="100%" />
</a-form-item>
<a-form-item label="部门名称" name="name">
<a-input v-model:value.trim="formState.name" placeholder="请输入部门名称" />
</a-form-item>
<a-form-item label="部门负责人" name="managerId">
<EmployeeSelect ref="employeeSelect" placeholder="请选择部门负责人" width="100%" v-model:value="formState.managerId" :leaveFlag="false" />
</a-form-item>
<a-form-item label="部门排序 (值越大越靠前!)" name="sort">
<a-input-number style="width: 100%" v-model:value="formState.sort" :min="0" placeholder="请输入部门名称" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import message from 'ant-design-vue/lib/message';
import { reactive, ref } from 'vue';
import { departmentApi } from '/@/api/system/department/department-api';
import DepartmentTreeSelect from '/@/components/system/department-tree-select/index.vue';
import EmployeeSelect from '/@/components/system/employee-select/index.vue';
import { smartSentry } from '/@/lib/smart-sentry';
import { SmartLoading } from '/@/components/framework/smart-loading';
// ----------------------- ---------------------
defineExpose({
showModal,
});
// ----------------------- modal ---------------------
const emits = defineEmits(['refresh']);
const visible = ref(false);
function showModal(data) {
visible.value = true;
updateFormData(data);
}
function closeModal() {
visible.value = false;
resetFormData();
}
// ----------------------- form ---------------------
const formRef = ref();
const departmentTreeSelect = ref();
const defaultDepartmentForm = {
id: undefined,
managerId: undefined, //
name: undefined,
parentId: undefined,
sort: 0,
};
const employeeSelect = ref();
let formState = reactive({
...defaultDepartmentForm,
});
//
const rules = {
parentId: [{ required: true, message: '上级部门不能为空' }],
name: [
{ required: true, message: '部门名称不能为空' },
{ max: 50, message: '部门名称不能大于20个字符', trigger: 'blur' },
],
managerId: [{ required: true, message: '部门负责人不能为空' }],
};
//
function updateFormData(data) {
Object.assign(formState, defaultDepartmentForm);
if (data) {
Object.assign(formState, data);
}
visible.value = true;
}
//
function resetFormData() {
Object.assign(formState, defaultDepartmentForm);
}
async function handleOk() {
try {
await formRef.value.validate();
if (formState.departmentId) {
updateDepartment();
} else {
addDepartment();
}
} catch (error) {
message.error('参数验证错误,请仔细填写表单数据!');
}
}
// ----------------------- form ajax ---------------------
//ajax
async function addDepartment() {
SmartLoading.show();
try {
await departmentApi.addDepartment(formState);
emits('refresh');
closeModal();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
}
//ajax
async function updateDepartment() {
SmartLoading.show();
try {
if (formState.parentId == formState.departmentId) {
message.warning('上级菜单不能为自己');
return;
}
await departmentApi.updateDepartment(formState);
emits('refresh');
closeModal();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
}
</script>

View File

@ -0,0 +1,258 @@
<template>
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="部门名称" class="smart-query-form-item">
<a-input style="width: 300px" v-model:value="keywords" placeholder="请输入部门名称" />
</a-form-item>
<a-form-item class="smart-query-form-item smart-margin-left10">
<a-button-group>
<a-button v-privilege="'support:department:query'" type="primary" @click="onSearch">
<template #icon>
<ReloadOutlined />
</template>
查询
</a-button>
<a-button v-privilege="'support:department:query'" @click="resetQuery">
<template #icon>
<SearchOutlined />
</template>
重置
</a-button>
</a-button-group>
<a-button v-privilege="'system:department:add'" type="primary" @click="addDepartment" class="smart-margin-left20">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
</a-form-item>
</a-row>
</a-form>
<a-card size="small" :bordered="true">
<a-table
size="small"
bordered
:loading="tableLoading"
rowKey="departmentId"
:columns="columns"
:data-source="departmentTreeData"
:defaultExpandAllRows="false"
:defaultExpandedRowKeys="defaultExpandedRowList"
:pagination="false"
>
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="addDepartment(record)" v-privilege="'system:department:add'" type="link">添加下级</a-button>
<a-button @click="updateDepartment(record)" v-privilege="'system:department:update'" type="link">编辑</a-button>
<a-button
danger
v-if="record.departmentId !== topDepartmentId"
v-privilege="'system:department:delete'"
@click="deleteDepartment(record.departmentId)"
type="link"
>删除</a-button
>
</div>
</template>
</template>
</a-table>
<!-- 添加编辑部门弹窗 -->
<DepartmentFormModal ref="departmentFormModal" @refresh="queryDepartmentTree" />
</a-card>
</template>
<script setup>
import { onMounted, reactive, ref, watch, createVNode } from 'vue';
import { departmentApi } from '/@/api/system/department/department-api';
import { Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import _ from 'lodash';
import { SmartLoading } from '/@/components/framework/smart-loading';
import DepartmentFormModal from './components/department-form-modal.vue';
import { smartSentry } from '/@/lib/smart-sentry';
const DEPARTMENT_PARENT_ID = 0;
// ----------------------- ---------------------
const keywords = ref('');
// ----------------------- ---------------------
const tableLoading = ref(false);
const topDepartmentId = ref();
//
const departmentList = ref([]);
//
const departmentTreeData = ref([]);
// id
const idInfoMap = ref(new Map());
//
const defaultExpandedRowList = reactive([]);
const columns = ref([
{
title: '部门名称',
dataIndex: 'name',
key: 'name',
},
{
title: '负责人',
dataIndex: 'managerName',
key: 'managerName',
width: 100,
},
{
title: '排序',
dataIndex: 'sort',
key: 'sort',
width: 100,
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 150,
},
{
title: '更新时间',
dataIndex: 'updateTime',
width: 150,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 200,
},
]);
onMounted(() => {
queryDepartmentTree();
});
//
async function queryDepartmentTree() {
try {
tableLoading.value = true;
let res = await departmentApi.queryAllDepartment();
let data = res.data;
data.forEach((e) => {
idInfoMap.value.set(e.departmentId, e);
});
departmentList.value = data;
departmentTreeData.value = buildDepartmentTree(data, DEPARTMENT_PARENT_ID);
// IDID
if (!_.isEmpty(departmentTreeData.value) && departmentTreeData.value.length > 0) {
topDepartmentId.value = departmentTreeData.value[0].departmentId;
}
defaultExpandedRowList.value = [];
defaultExpandedRowList.push(topDepartmentId.value);
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
//
function buildDepartmentTree(data, parentId) {
let children = data.filter((e) => e.parentId === parentId) || [];
if (!_.isEmpty(children)) {
children.forEach((e) => {
e.children = buildDepartmentTree(data, e.departmentId);
});
return children;
}
return null;
}
//
function resetQuery() {
keywords.value = '';
onSearch();
}
//
function onSearch() {
if (!keywords.value) {
departmentTreeData.value = buildDepartmentTree(departmentList.value, DEPARTMENT_PARENT_ID);
return;
}
let originData = departmentList.value.concat();
if (!originData) {
return;
}
//
let filterDepartment = originData.filter((e) => e.name.indexOf(keywords.value) > -1);
let filterDepartmentList = [];
//
filterDepartment.forEach((e) => {
recursionFilterDepartment(filterDepartmentList, e.departmentId, false);
});
departmentTreeData.value = buildDepartmentTree(filterDepartmentList, DEPARTMENT_PARENT_ID);
}
// ID
function recursionFilterDepartment(resList, id, unshift) {
let info = idInfoMap.value.get(id);
if (!info || resList.some((e) => e.departmentId === id)) {
return;
}
if (unshift) {
resList.unshift(info);
} else {
resList.push(info);
}
if (info.parentId && info.parentId !== 0) {
recursionFilterDepartment(resList, info.parentId, unshift);
}
}
// ----------------------- /// ---------------------
const departmentFormModal = ref();
//
function addDepartment(e) {
let data = {
departmentId: 0,
name: '',
parentId: e.departmentId || null,
};
departmentFormModal.value.showModal(data);
}
//
function updateDepartment(e) {
departmentFormModal.value.showModal(e);
}
//
function deleteDepartment(id) {
Modal.confirm({
title: '提醒',
icon: createVNode(ExclamationCircleOutlined),
content: '确定要删除该部门吗?',
okText: '删除',
okType: 'danger',
async onOk() {
SmartLoading.show();
try {
await departmentApi.deleteDepartment(id);
await queryDepartmentTree();
} catch (error) {
smartSentry.captureError(error);
} finally {
SmartLoading.hide();
}
},
cancelText: '取消',
onCancel() {},
});
}
</script>
<style scoped lang="less"></style>

View File

@ -17,11 +17,20 @@
</div>
</div>
<div class="box-item login">
<img class="login-qr" :src="loginQR" />
<img class="login-qr" :src="loginQR"/>
<div class="login-title">账号登录</div>
<a-form ref="formRef" class="login-form" :model="loginForm" :rules="rules">
<a-form-item name="loginName">
<a-input v-model:value.trim="loginForm.loginName" placeholder="请输入用户名" />
<a-input v-model:value.trim="loginForm.loginName" placeholder="请输入用户名"/>
</a-form-item>
<a-form-item name="emailCode" v-if="emailCodeShowFlag">
<a-input-group compact>
<a-input style="width: calc(100% - 110px)" v-model:value="loginForm.emailCode" autocomplete="on"
placeholder="请输入邮箱验证码"/>
<a-button @click="sendSmsCode" class="code-btn" type="primary" :disabled="emailCodeButtonDisabled">
{{ emailCodeTips }}
</a-button>
</a-input-group>
</a-form-item>
<a-form-item name="password">
<a-input-password
@ -32,8 +41,8 @@
/>
</a-form-item>
<a-form-item name="captchaCode">
<a-input class="captcha-input" v-model:value.trim="loginForm.captchaCode" placeholder="请输入验证码" />
<img class="captcha-img" :src="captchaBase64Image" @click="getCaptcha" />
<a-input class="captcha-input" v-model:value.trim="loginForm.captchaCode" placeholder="请输入验证码"/>
<img class="captcha-img" :src="captchaBase64Image" @click="getCaptcha"/>
</a-form-item>
<a-form-item>
<a-checkbox v-model:checked="rememberPwd">记住密码</a-checkbox>
@ -50,135 +59,188 @@
<p class="line"></p>
</div>
<div class="login-type">
<img :src="wechatIcon" />
<img :src="aliIcon" />
<img :src="douyinIcon" />
<img :src="qqIcon" />
<img :src="weiboIcon" />
<img :src="feishuIcon" />
<img :src="googleIcon" />
<img :src="wechatIcon"/>
<img :src="aliIcon"/>
<img :src="douyinIcon"/>
<img :src="qqIcon"/>
<img :src="weiboIcon"/>
<img :src="feishuIcon"/>
<img :src="googleIcon"/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { message } from 'ant-design-vue';
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { loginApi } from '/@/api/system/login/login-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const';
import { useUserStore } from '/@/store/modules/system/user';
import loginQR from '/@/assets/images/login/login-qr.png';
import wechatIcon from '/@/assets/images/login/wechat-icon.png';
import aliIcon from '/@/assets/images/login/ali-icon.png';
import douyinIcon from '/@/assets/images/login/douyin-icon.png';
import qqIcon from '/@/assets/images/login/qq-icon.png';
import weiboIcon from '/@/assets/images/login/weibo-icon.png';
import feishuIcon from '/@/assets/images/login/feishu-icon.png';
import googleIcon from '/@/assets/images/login/google-icon.png';
import { message } from 'ant-design-vue';
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { loginApi } from '/src/api/system/login/login-api';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const';
import { useUserStore } from '/@/store/modules/system/user';
import loginQR from '/@/assets/images/login/login-qr.png';
import wechatIcon from '/@/assets/images/login/wechat-icon.png';
import aliIcon from '/@/assets/images/login/ali-icon.png';
import douyinIcon from '/@/assets/images/login/douyin-icon.png';
import qqIcon from '/@/assets/images/login/qq-icon.png';
import weiboIcon from '/@/assets/images/login/weibo-icon.png';
import feishuIcon from '/@/assets/images/login/feishu-icon.png';
import googleIcon from '/@/assets/images/login/google-icon.png';
import { buildRoutes } from '/@/router/index';
import { smartSentry } from '/@/lib/smart-sentry';
import { encryptData } from '/@/lib/encrypt';
import { localSave } from '/@/utils/local-util.ts';
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.ts';
import { saveTokenToCookie } from '/@/utils/cookie-util';
import { buildRoutes } from '/@/router/index';
import { smartSentry } from '/@/lib/smart-sentry';
import { encryptData } from '/@/lib/encrypt';
import { saveTokenToCookie } from '/@/utils/cookie-util';
//--------------------- ---------------------------------
//--------------------- ---------------------------------
const loginForm = reactive({
loginName: 'admin',
password: '',
captchaCode: '',
captchaUuid: '',
loginDevice: LOGIN_DEVICE_ENUM.PC.value,
});
const rules = {
loginName: [{ required: true, message: '用户名不能为空' }],
password: [{ required: true, message: '密码不能为空' }],
captchaCode: [{ required: true, message: '验证码不能为空' }],
const loginForm = reactive({
loginName: 'admin',
password: '',
emailCode: '', //
captchaCode: '',
captchaUuid: '',
loginDevice: LOGIN_DEVICE_ENUM.PC.value
});
const rules = {
loginName: [{required: true, message: '用户名不能为空'}],
password: [{required: true, message: '密码不能为空'}],
captchaCode: [{required: true, message: '验证码不能为空'}]
};
const showPassword = ref(false);
const router = useRouter();
const formRef = ref();
const rememberPwd = ref(false);
onMounted(() => {
document.onkeyup = (e) => {
if (e.keyCode == 13) {
onLogin();
}
};
});
const showPassword = ref(false);
const router = useRouter();
const formRef = ref();
const rememberPwd = ref(false);
onUnmounted(() => {
document.onkeyup = null;
});
onMounted(() => {
document.onkeyup = (e) => {
if (e.keyCode == 13) {
onLogin();
}
};
});
onUnmounted(() => {
document.onkeyup = null;
});
//
async function onLogin() {
formRef.value.validate().then(async () => {
try {
SmartLoading.show();
//
let encryptPasswordForm = Object.assign({}, loginForm, {
password: encryptData(loginForm.password),
});
const res = await loginApi.login(encryptPasswordForm);
stopRefrestCaptchaInterval();
// localSave(LocalStorageKeyConst.USER_TOKEN, res.data.token ? res.data.token : '');
saveTokenToCookie(res.data.token ? res.data.token : '');
message.success('登录成功');
//pinia
useUserStore().setUserLoginInfo(res.data);
//
buildRoutes();
router.push('/home');
} catch (e) {
if (e.data && e.data.code !== 0) {
loginForm.captchaCode = '';
getCaptcha();
}
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
});
}
//--------------------- ---------------------------------
const captchaBase64Image = ref('');
async function getCaptcha() {
//
async function onLogin() {
formRef.value.validate().then(async() => {
try {
let captchaResult = await loginApi.getCaptcha();
captchaBase64Image.value = captchaResult.data.captchaBase64Image;
loginForm.captchaUuid = captchaResult.data.captchaUuid;
beginRefrestCaptchaInterval(captchaResult.data.expireSeconds);
SmartLoading.show();
//
let encryptPasswordForm = Object.assign({}, loginForm, {
password: encryptData(loginForm.password)
});
const res = await loginApi.login(encryptPasswordForm);
stopRefrestCaptchaInterval();
// localSave(LocalStorageKeyConst.USER_TOKEN, res.data.token ? res.data.token : '');
saveTokenToCookie(res.data.token ? res.data.token : '');
message.success('登录成功');
//pinia
useUserStore().setUserLoginInfo(res.data);
//
buildRoutes();
router.push('/home');
} catch (e) {
console.log(e);
if (e.data && e.data.code !== 0) {
loginForm.captchaCode = '';
getCaptcha();
}
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
});
}
let refrestCaptchaInterval = null;
function beginRefrestCaptchaInterval(expireSeconds) {
if (refrestCaptchaInterval === null) {
refrestCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
//--------------------- ---------------------------------
const captchaBase64Image = ref('');
async function getCaptcha() {
try {
let captchaResult = await loginApi.getCaptcha();
captchaBase64Image.value = captchaResult.data.captchaBase64Image;
loginForm.captchaUuid = captchaResult.data.captchaUuid;
beginRefrestCaptchaInterval(captchaResult.data.expireSeconds);
} catch (e) {
console.log(e);
}
}
let refrestCaptchaInterval = null;
function beginRefrestCaptchaInterval(expireSeconds) {
if (refrestCaptchaInterval === null) {
refrestCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
}
}
function stopRefrestCaptchaInterval() {
if (refrestCaptchaInterval != null) {
clearInterval(refrestCaptchaInterval);
refrestCaptchaInterval = null;
}
}
onMounted(() => {
getCaptcha();
getTwoFactorLoginFlag();
});
//--------------------- ---------------------------------
const emailCodeShowFlag = ref(false);
let emailCodeTips = ref('获取邮箱验证码');
let emailCodeButtonDisabled = ref(false);
//
let countDownTimer = null;
//
function runCountDown() {
emailCodeButtonDisabled.value = true;
let countDown = 60;
emailCodeTips.value = `${ countDown }秒后重新获取`;
countDownTimer = setInterval(() => {
if (countDown > 1) {
countDown--;
emailCodeTips.value = `${ countDown }秒后重新获取`;
} else {
clearInterval(countDownTimer);
emailCodeButtonDisabled.value = false;
emailCodeTips.value = '获取验证码';
}
}
}, 1000);
}
function stopRefrestCaptchaInterval() {
if (refrestCaptchaInterval != null) {
clearInterval(refrestCaptchaInterval);
refrestCaptchaInterval = null;
}
//
async function getTwoFactorLoginFlag() {
try {
let result = await loginApi.getTwoFactorLoginFlag();
emailCodeShowFlag.value = result.data;
} catch (e) {
smartSentry.captureError(e);
}
}
onMounted(getCaptcha);
//
async function sendSmsCode() {
try {
SmartLoading.show();
let result = await loginApi.sendLoginEmailCode(loginForm.loginName);
message.success('验证码发送成功!请登录邮箱查看验证码~');
runCountDown();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
</script>
<style lang="less" scoped>
@import './login.less';
@import "./login.less";
</style>

View File

@ -0,0 +1,124 @@
<!--
* 职务表
*
* @Author: kaiyun
* @Date: 2024-06-23 23:31:38
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
-->
<template>
<a-modal
:title="form.positionId ? '编辑' : '添加'"
width="600px"
:open="visibleFlag"
@cancel="onClose"
:maskClosable="false"
:destroyOnClose="true"
forceRender
>
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 6 }">
<a-form-item label="职务名称" name="positionName">
<a-input style="width: 100%" v-model:value="form.positionName" placeholder="职务名称"/>
</a-form-item>
<a-form-item label="职级" name="level">
<a-input style="width: 100%" v-model:value="form.level" placeholder="职级"/>
</a-form-item>
<a-form-item label="排序" name="sort">
<a-input-number :min="0" :step="1" :precision="0" style="width: 100%" v-model:value="form.sort" placeholder="排序"/>
</a-form-item>
<a-form-item label="备注" name="remark">
<a-input style="width: 100%" v-model:value="form.remark" placeholder="备注"/>
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button @click="onClose">取消</a-button>
<a-button type="primary" @click="onSubmit">保存</a-button>
</a-space>
</template>
</a-modal>
</template>
<script setup>
import { reactive, ref, nextTick } from 'vue';
import _ from 'lodash';
import { message } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { positionApi } from '/@/api/system/position/position-api';
import { smartSentry } from '/@/lib/smart-sentry';
// ------------------------ ------------------------
const emits = defineEmits(['reloadList']);
// ------------------------ ------------------------
//
const visibleFlag = ref(false);
function show (rowData) {
Object.assign(form, formDefault);
if (rowData && !_.isEmpty(rowData)) {
Object.assign(form, rowData);
}
visibleFlag.value = true;
nextTick(() => {
formRef.value.clearValidate();
});
}
function onClose () {
Object.assign(form, formDefault);
visibleFlag.value = false;
}
// ------------------------ ------------------------
// ref
const formRef = ref();
const formDefault = {
positionId: undefined,
positionName: undefined, //
level: undefined,//
sort: 0,
remark: undefined, //
};
let form = reactive({ ...formDefault });
const rules = {
positionName: [{ required: true, message: '请输入职务名称' }],
};
//
async function onSubmit () {
try {
await formRef.value.validateFields();
save();
} catch (err) {
message.error('参数验证错误,请仔细填写表单数据!');
}
}
// API
async function save () {
SmartLoading.show();
try {
if (form.positionId) {
await positionApi.update(form);
} else {
await positionApi.add(form);
}
message.success('操作成功');
emits('reloadList');
onClose();
} catch (err) {
smartSentry.captureError(err);
} finally {
SmartLoading.hide();
}
}
defineExpose({
show,
});
</script>

View File

@ -0,0 +1,262 @@
<!--
* 职务表
*
* @Author: kaiyun
* @Date: 2024-06-23 23:31:38
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
-->
<template>
<!---------- 查询表单form begin ----------->
<a-form class="smart-query-form">
<a-row class="smart-query-form-row">
<a-form-item label="关键字查询" class="smart-query-form-item">
<a-input style="width: 200px" v-model:value="queryForm.keywords" placeholder="关键字查询" />
</a-form-item>
<a-form-item class="smart-query-form-item">
<a-button type="primary" @click="queryData">
<template #icon>
<SearchOutlined />
</template>
查询
</a-button>
<a-button @click="resetQuery" class="smart-margin-left10">
<template #icon>
<ReloadOutlined />
</template>
重置
</a-button>
</a-form-item>
</a-row>
</a-form>
<!---------- 查询表单form end ----------->
<a-card size="small" :bordered="false" :hoverable="true">
<!---------- 表格操作行 begin ----------->
<a-row class="smart-table-btn-block">
<div class="smart-table-operate-block">
<a-button @click="showForm" type="primary">
<template #icon>
<PlusOutlined />
</template>
新建
</a-button>
<a-button @click="confirmBatchDelete" type="primary" danger :disabled="selectedRowKeyList.length === 0">
<template #icon>
<DeleteOutlined />
</template>
批量删除
</a-button>
</div>
<div class="smart-table-setting-block">
<TableOperator v-model="columns" :tableId="TABLE_ID_CONST.SYSTEM.EMPLOYEE" :refresh="queryData" />
</div>
</a-row>
<!---------- 表格操作行 end ----------->
<!---------- 表格 begin ----------->
<a-table
size="small"
:dataSource="tableData"
:columns="columns"
rowKey="positionId"
bordered
:loading="tableLoading"
:pagination="false"
:row-selection="{ selectedRowKeys: selectedRowKeyList, onChange: onSelectChange }"
>
<template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'action'">
<div class="smart-table-operate">
<a-button @click="showForm(record)" type="link">编辑</a-button>
<a-button @click="onDelete(record)" danger type="link">删除</a-button>
</div>
</template>
</template>
</a-table>
<!---------- 表格 end ----------->
<div class="smart-query-table-page">
<a-pagination
showSizeChanger
showQuickJumper
show-less-items
:pageSizeOptions="PAGE_SIZE_OPTIONS"
:defaultPageSize="queryForm.pageSize"
v-model:current="queryForm.pageNum"
v-model:pageSize="queryForm.pageSize"
:total="total"
@change="queryData"
@showSizeChange="queryData"
:show-total="(total) => `共${total}条`"
/>
</div>
<PositionForm ref="formRef" @reloadList="queryData" />
</a-card>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { positionApi } from '/@/api/system/position/position-api';
import { PAGE_SIZE_OPTIONS } from '/@/constants/common-const';
import { smartSentry } from '/@/lib/smart-sentry';
import TableOperator from '/@/components/support/table-operator/index.vue';
import PositionForm from './position-form.vue';
import _ from 'lodash';
import { TABLE_ID_CONST } from '/@/constants/support/table-id-const';
// ---------------------------- ----------------------------
const columns = ref([
{
title: '职务名称',
dataIndex: 'positionName',
ellipsis: true,
},
{
title: '职级',
dataIndex: 'level',
ellipsis: true,
},
{
title: '排序',
dataIndex: 'sort',
ellipsis: true,
},
{
title: '备注',
dataIndex: 'remark',
ellipsis: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
ellipsis: true,
},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 90,
},
]);
// ---------------------------- ----------------------------
const queryFormState = {
keywords: undefined, //
pageNum: 1,
pageSize: 10,
};
// form
const queryForm = reactive({ ...queryFormState });
// loading
const tableLoading = ref(false);
//
const tableData = ref([]);
//
const total = ref(0);
//
function resetQuery() {
let pageSize = queryForm.pageSize;
Object.assign(queryForm, queryFormState);
queryForm.pageSize = pageSize;
queryData();
}
//
async function queryData() {
tableLoading.value = true;
try {
let queryResult = await positionApi.queryPage(queryForm);
tableData.value = queryResult.data.list;
total.value = queryResult.data.total;
} catch (e) {
smartSentry.captureError(e);
} finally {
tableLoading.value = false;
}
}
onMounted(queryData);
// ---------------------------- / ----------------------------
const formRef = ref();
function showForm(data) {
formRef.value.show(data);
}
// ---------------------------- ----------------------------
//
function onDelete(data) {
Modal.confirm({
title: '提示',
content: '确定要删除选吗?',
okText: '删除',
okType: 'danger',
onOk() {
requestDelete(data);
},
cancelText: '取消',
onCancel() {},
});
}
//
async function requestDelete(data) {
SmartLoading.show();
try {
await positionApi.delete(data.positionId);
message.success('删除成功');
queryData();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
// ---------------------------- ----------------------------
//
const selectedRowKeyList = ref([]);
function onSelectChange(selectedRowKeys) {
selectedRowKeyList.value = selectedRowKeys;
}
//
function confirmBatchDelete() {
if (_.isEmpty(selectedRowKeyList.value)) {
message.success('请选择要删除的数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要批量删除这些数据吗?',
okText: '删除',
okType: 'danger',
onOk() {
requestBatchDelete();
},
cancelText: '取消',
onCancel() {},
});
}
//
async function requestBatchDelete() {
try {
SmartLoading.show();
await positionApi.batchDelete(selectedRowKeyList.value);
message.success('删除成功');
queryData();
} catch (e) {
smartSentry.captureError(e);
} finally {
SmartLoading.hide();
}
}
</script>