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; 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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import net.lab1024.sa.admin.module.business.goods.constant.GoodsStatusEnum; import net.lab1024.sa.admin.module.business.goods.constant.GoodsStatusEnum;
import net.lab1024.sa.base.common.domain.PageParam; 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.swagger.SchemaEnum;
import net.lab1024.sa.base.common.validator.enumeration.CheckEnum; import net.lab1024.sa.base.common.validator.enumeration.CheckEnum;
import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Length;
@ -32,6 +34,7 @@ public class GoodsQueryForm extends PageParam {
private Integer goodsStatus; private Integer goodsStatus;
@Schema(description = "产地") @Schema(description = "产地")
@JsonDeserialize(using = DictValueVoDeserializer.class)
private String place; private String place;
@Schema(description = "上架状态") @Schema(description = "上架状态")

View File

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

View File

@ -110,7 +110,7 @@ public class EmployeeController {
@Operation(summary = "重置员工密码 @author 卓大") @Operation(summary = "重置员工密码 @author 卓大")
@GetMapping("/employee/update/password/reset/{employeeId}") @GetMapping("/employee/update/password/reset/{employeeId}")
@SaCheckPermission("system:employee:password:reset") @SaCheckPermission("system:employee:password:reset")
public ResponseDTO<String> resetPassword(@PathVariable Integer employeeId) { public ResponseDTO<String> resetPassword(@PathVariable Long employeeId) {
return employeeService.resetPassword(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 @Service
public class EmployeeService { public class EmployeeService {
private static final String PASSWORD_SALT_FORMAT = "smart_%s_admin_$^&*";
@Resource @Resource
private EmployeeDao employeeDao; 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(); String password = securityPasswordService.randomPassword();
employeeDao.updatePassword(employeeId, SecurityPasswordService.getEncryptPwd(password)); employeeDao.updatePassword(employeeId, SecurityPasswordService.getEncryptPwd(password));
return ResponseDTO.ok(password); return ResponseDTO.ok(password);

View File

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

View File

@ -18,11 +18,20 @@ import java.io.IOException;
*/ */
public class LongJsonSerializer extends JsonSerializer<Long> { public class LongJsonSerializer extends JsonSerializer<Long> {
public static final LongJsonSerializer INSTANCE = new LongJsonSerializer();
@Override @Override
public void serialize(Long value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { public void serialize(Long value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
String text = (value == null ? null : String.valueOf(value)); if (null == value) {
if (text != null) { gen.writeNull();
jsonGenerator.writeString(text); 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.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; 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.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -17,7 +18,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
/** /**
* java8 localDate 时间类格式化配置 * Json 序列化配置
* *
* @Author 1024创新实验室-主任: 卓大 * @Author 1024创新实验室-主任: 卓大
* @Date 2017-11-28 15:21:10 * @Date 2017-11-28 15:21:10
@ -26,7 +27,7 @@ import java.time.format.DateTimeParseException;
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/ */
@Configuration @Configuration
public class DateConfig { public class JsonConfig {
@Bean @Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() { public Jackson2ObjectMapperBuilderCustomizer customizer() {
@ -35,6 +36,7 @@ public class DateConfig {
builder.deserializers(new LocalDateTimeDeserializer(DatePattern.NORM_DATETIME_FORMAT.getDateTimeFormatter())); builder.deserializers(new LocalDateTimeDeserializer(DatePattern.NORM_DATETIME_FORMAT.getDateTimeFormatter()));
builder.serializers(new LocalDateSerializer(DatePattern.NORM_DATE_FORMAT.getDateTimeFormatter())); builder.serializers(new LocalDateSerializer(DatePattern.NORM_DATE_FORMAT.getDateTimeFormatter()));
builder.serializers(new LocalDateTimeSerializer(DatePattern.NORM_DATETIME_FORMAT.getDateTimeFormatter())); builder.serializers(new LocalDateTimeSerializer(DatePattern.NORM_DATETIME_FORMAT.getDateTimeFormatter()));
builder.serializerByType(Long.class, LongJsonSerializer.INSTANCE);
}; };
} }

View File

@ -32,9 +32,8 @@ import java.util.Optional;
* springdoc-openapi 配置 * springdoc-openapi 配置
* nginx配置前缀时如果需要访问/swagger-ui/index.html需添加额外nginx配置 * nginx配置前缀时如果需要访问/swagger-ui/index.html需添加额外nginx配置
* location /v3/api-docs/ { * location /v3/api-docs/ {
* proxy_pass http://127.0.0.1:11024/v3/api-docs/; * proxy_pass http://127.0.0.1:1024/v3/api-docs/;
* } * }
*
* @Author 1024创新实验室-主任: 卓大 * @Author 1024创新实验室-主任: 卓大
* @Date 2020-03-25 22:54:46 * @Date 2020-03-25 22:54:46
* @Wechat zhuoda1024 * @Wechat zhuoda1024
@ -48,7 +47,7 @@ public class SwaggerConfig {
/** /**
* 用于解决/swagger-ui/index.html页面ServersUrl 测试环境部署错误问题 * 用于解决/swagger-ui/index.html页面ServersUrl 测试环境部署错误问题
*/ */
@Value("${springdoc.swagger-ui.server-base-url:''}") @Value("${springdoc.swagger-ui.server-base-url}")
private String serverBaseUrl; private String serverBaseUrl;
public static final String[] SWAGGER_WHITELIST = { public static final String[] SWAGGER_WHITELIST = {
@ -104,7 +103,6 @@ public class SwaggerConfig {
/** /**
* 以下代码可以用于设置 /swagger-ui/index.html 的serverBaseUrl * 以下代码可以用于设置 /swagger-ui/index.html 的serverBaseUrl
* 如果使用knife4j则不需要 * 如果使用knife4j则不需要
*
* @param openAPI * @param openAPI
* @param securityParser * @param securityParser
* @param springDocConfigProperties * @param springDocConfigProperties

View File

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

View File

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

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 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 username: root
password: SmartAdmin666 password: SmartAdmin666
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
initial-size: 2 initial-size: 2
min-idle: 2 min-idle: 2
max-active: 10 max-active: 10
max-wait: 60000 max-wait: 60000
time-between-eviction-runs-millis: 60000 time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000 min-evictable-idle-time-millis: 300000
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
filters: stat filters: stat
druid: druid:
username: druid username: druid
@ -40,7 +40,7 @@ spring:
host: smtp.163.com host: smtp.163.com
port: 465 port: 465
username: lab1024@163.com username: lab1024@163.com
password: 1 password: 1024lab
properties: properties:
mail: mail:
smtp: smtp:

View File

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

View File

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

View File

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

View File

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

View File

@ -18,11 +18,20 @@ import java.io.IOException;
*/ */
public class LongJsonSerializer extends JsonSerializer<Long> { public class LongJsonSerializer extends JsonSerializer<Long> {
public static final LongJsonSerializer INSTANCE = new LongJsonSerializer();
@Override @Override
public void serialize(Long value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { public void serialize(Long value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
String text = (value == null ? null : String.valueOf(value)); if (null == value) {
if (text != null) { gen.writeNull();
jsonGenerator.writeString(text); 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.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; 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.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -17,7 +18,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
/** /**
* java8 localDate 时间类格式化配置 * json 序列化配置
* *
* @Author 1024创新实验室-主任: 卓大 * @Author 1024创新实验室-主任: 卓大
* @Date 2017-11-28 15:21:10 * @Date 2017-11-28 15:21:10
@ -26,7 +27,7 @@ import java.time.format.DateTimeParseException;
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/ */
@Configuration @Configuration
public class DateConfig { public class JsonConfig {
@Bean @Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() { public Jackson2ObjectMapperBuilderCustomizer customizer() {
@ -35,6 +36,7 @@ public class DateConfig {
builder.deserializers(new LocalDateTimeDeserializer(DatePattern.NORM_DATETIME_FORMAT.getDateTimeFormatter())); builder.deserializers(new LocalDateTimeDeserializer(DatePattern.NORM_DATETIME_FORMAT.getDateTimeFormatter()));
builder.serializers(new LocalDateSerializer(DatePattern.NORM_DATE_FORMAT.getDateTimeFormatter())); builder.serializers(new LocalDateSerializer(DatePattern.NORM_DATE_FORMAT.getDateTimeFormatter()));
builder.serializers(new LocalDateTimeSerializer(DatePattern.NORM_DATETIME_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; package net.lab1024.sa.base.module.support.apiencrypt.advice;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.domain.ResponseDTO; import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.enumeration.DataTypeEnum; import net.lab1024.sa.base.common.enumeration.DataTypeEnum;
@ -34,6 +36,9 @@ public class EncryptResponseAdvice implements ResponseBodyAdvice<ResponseDTO> {
@Resource @Resource
private ApiEncryptService apiEncryptService; private ApiEncryptService apiEncryptService;
@Resource
private ObjectMapper objectMapper;
@Override @Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.hasMethodAnnotation(ApiEncrypt.class) || returnType.getContainingClass().isAnnotationPresent(ApiEncrypt.class); return returnType.hasMethodAnnotation(ApiEncrypt.class) || returnType.getContainingClass().isAnnotationPresent(ApiEncrypt.class);
@ -45,7 +50,12 @@ public class EncryptResponseAdvice implements ResponseBodyAdvice<ResponseDTO> {
return body; 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.setData(encrypt);
body.setDataType(DataTypeEnum.ENCRYPT.getValue()); body.setDataType(DataTypeEnum.ENCRYPT.getValue());
return body; return body;

View File

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

View File

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

View File

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

View File

@ -32,6 +32,7 @@
}, },
}, },
}" }"
:getPopupContainer="getPopupContainer"
> >
<!---全局loading---> <!---全局loading--->
<a-spin :spinning="spinning" tip="稍等片刻,我在拼命加载中..." size="large"> <a-spin :spinning="spinning" tip="稍等片刻,我在拼命加载中..." size="large">
@ -49,6 +50,8 @@
import { useSpinStore } from '/@/store/modules/system/spin'; import { useSpinStore } from '/@/store/modules/system/spin';
import { theme } from 'ant-design-vue'; import { theme } from 'ant-design-vue';
import { themeColors } from '/@/theme/color.js'; 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 antdLocale = computed(() => messages[useAppConfigStore().language].antdLocale);
const dayjsLocale = computed(() => messages[useAppConfigStore().language].dayjsLocale); const dayjsLocale = computed(() => messages[useAppConfigStore().language].dayjsLocale);
@ -67,4 +70,31 @@
const borderRadius = computed(() => { const borderRadius = computed(() => {
return useAppConfigStore().borderRadius; 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> </script>

View File

@ -12,12 +12,12 @@
<template> <template>
<span> <span>
<a-tooltip title="全屏" v-if="!fullScreenFlag"> <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> <template #icon><fullscreen-outlined /></template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip title="取消全屏" v-if="fullScreenFlag"> <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> <template #icon><fullscreen-exit-outlined /></template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -43,11 +43,13 @@
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { mergeColumn } from './smart-table-column-merge'; import { mergeColumn } from './smart-table-column-merge';
import { smartSentry } from '/@/lib/smart-sentry'; 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({ const props = defineProps({
// //
modelValue: { modelValue: {
type: Array, type: Array,
default: new Array(), default: [],
}, },
// //
refresh: { refresh: {
@ -93,22 +95,39 @@
// ----------------- ------------------- // ----------------- -------------------
const fullScreenFlag = ref(false); const fullScreenFlag = ref(false);
function fullScreen() {
function onFullScreen() {
if (fullScreenFlag.value) { if (fullScreenFlag.value) {
// // 退
exitFullscreen(document.querySelector('#smartAdminLayoutContent')); handleExitFullScreen();
fullScreenFlag.value = false; exitElementFullscreen(document.getElementById(LAYOUT_ELEMENT_IDS.content));
document.querySelector('#smartAdminPageTag').style.visibility = 'visible';
} else { } else {
// //
launchFullScreen(document.querySelector('#smartAdminLayoutContent')); message.config({
getContainer: () => document.getElementById(LAYOUT_ELEMENT_IDS.content),
});
fullScreenFlag.value = true; 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) { if (element.requestFullscreen) {
element.requestFullscreen(); element.requestFullscreen();
} else if (element.mozRequestFullScreen) { } else if (element.mozRequestFullScreen) {
@ -120,9 +139,23 @@
} else { } else {
message.error('当前浏览器不支持部分全屏!'); 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) { if (document.exitFullscreen) {
document.exitFullscreen(); document.exitFullscreen();
} else if (document.mozCancelFullScreen) { } else if (document.mozCancelFullScreen) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -48,8 +48,10 @@
"vue3-tabs-chrome": "^0.3.3" "vue3-tabs-chrome": "^0.3.3"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/lodash": "^4.14.186", "@types/lodash": "^4.14.186",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@types/sm-crypto": "^0.3.4",
"@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1", "@typescript-eslint/parser": "^5.40.1",
"@vitejs/plugin-legacy": "5.4.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 胡克 * 下载文件流根据fileKey @author 胡克
*/ */
downLoadFile: (fileKey) => { downLoadFile: (fileName, fileKey) => {
return getDownload('/support/file/downLoad', { fileKey }); return getDownload(fileName, '/support/file/downLoad', { fileKey });
}, },
}; };

View File

@ -13,5 +13,5 @@ export const heartBeatApi = {
// 分页查询 @author 卓大 // 分页查询 @author 卓大
queryList: (param) => { queryList: (param) => {
return postRequest('/support/heartBeat/query', 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

@ -51,5 +51,5 @@ export const departmentApi = {
*/ */
deleteDepartment: (departmentId) => { deleteDepartment: (departmentId) => {
return getRequest(`/department/delete/${ departmentId }`); return getRequest(`/department/delete/${ departmentId }`);
}, }
}; };

View File

@ -48,4 +48,18 @@ export const loginApi = {
refresh: () => { refresh: () => {
return getRequest('/login/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-tab-pane>
</a-tabs> </a-tabs>
</div> </div>
<template #rightExtra> <template #overlay>
<a-menu> <a-menu>
<a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item> <a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item>
<a-menu-item @click="closeByMenu(true)">关闭所有</a-menu-item> <a-menu-item @click="closeByMenu(true)">关闭所有</a-menu-item>

View File

@ -23,7 +23,7 @@
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</div> </div>
<template #rightExtra> <template #overlay>
<a-menu> <a-menu>
<a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item> <a-menu-item @click="closeByMenu(false)">关闭其他</a-menu-item>
<a-menu-item @click="closeByMenu(true)">关闭所有</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'; import CryptoJS from 'crypto-js';
// @ts-ignore
import CryptoSM from 'sm-crypto'; import CryptoSM from 'sm-crypto';
function object2string(data) { function object2string(data: any) {
if (typeof data === 'Object') { if (typeof data === 'object') {
return JSON.stringify(data); return JSON.stringify(data);
} }
let str = JSON.stringify(data); let str = JSON.stringify(data);
if (str.startsWith("'") || str.startsWith('"')) { if (str.startsWith('\'') || str.startsWith('"')) {
str = str.substring(1); str = str.substring(1);
} }
if (str.endsWith("'") || str.endsWith('"')) { if (str.endsWith('\'') || str.endsWith('"')) {
str = str.substring(0, str.length - 1); str = str.substring(0, str.length - 1);
} }
return str; 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) { encryptData: function (data: any) {
// AES 加密 并转为 base64 // AES 加密 并转为 base64
let utf8Data = CryptoJS.enc.Utf8.parse(object2string(data)); let utf8Data = CryptoJS.enc.Utf8.parse(object2string(data));
const key = CryptoJS.enc.Utf8.parse(AES_KEY); const key = CryptoJS.enc.Utf8.parse(AES_KEY);
const encrypted = CryptoJS.AES.encrypt(utf8Data, key, { const encrypted = CryptoJS.AES.encrypt(utf8Data, key, {
mode: CryptoJS.mode.ECB, mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7, padding: CryptoJS.pad.Pkcs7
}); });
return encrypted.toString(); return encrypted.toString();
@ -40,18 +53,18 @@ const AES = {
const key = CryptoJS.enc.Utf8.parse(AES_KEY); 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, mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7, padding: CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8); }).toString(CryptoJS.enc.Utf8);
}, }
}; };
// ----------------------- 国密SM4算法 加密、解密 ----------------------- // ----------------------- 国密SM4算法 加密、解密 -----------------------
const SM4_KEY = '1024abcd1024abcd1024abcd1024abcd'; const SM4_KEY = '1024lab__1024lab';
const SM4 = { const SM4 = {
encryptData: function (data: any) { encryptData: function (data: any) {
// 第一步SM4 加密 // 第一步SM4 加密
let encryptData = CryptoSM.sm4.encrypt(object2string(data), SM4_KEY); let encryptData = CryptoSM.sm4.encrypt(object2string(data), stringToHex(SM4_KEY));
// 第二步: Base64 编码 // 第二步: Base64 编码
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(encryptData)); return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(encryptData));
}, },
@ -62,8 +75,8 @@ const SM4 = {
let decode64Str = CryptoJS.enc.Utf8.stringify(words); let decode64Str = CryptoJS.enc.Utf8.stringify(words);
// 第二步SM4 解密 // 第二步SM4 解密
return CryptoSM.sm4.decrypt(decode64Str, SM4_KEY); return CryptoSM.sm4.decrypt(decode64Str, stringToHex(SM4_KEY));
}, }
}; };
// ----------------------- 对外暴露: 加密、解密 ----------------------- // ----------------------- 对外暴露: 加密、解密 -----------------------

View File

@ -24,7 +24,7 @@ export const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes: routerArray, routes: routerArray,
strict: true, strict: true,
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({left: 0, top: 0})
}); });
// ----------------------- 路由加载前 ----------------------- // ----------------------- 路由加载前 -----------------------
@ -79,6 +79,7 @@ router.afterEach(() => {
// ----------------------- 构建router对象 ----------------------- // ----------------------- 构建router对象 -----------------------
const routerMap = new Map(); const routerMap = new Map();
export function buildRoutes(menuRouterList) { export function buildRoutes(menuRouterList) {
let menuList = menuRouterList ? menuRouterList : useUserStore().getMenuRouterList || []; let menuList = menuRouterList ? menuRouterList : useUserStore().getMenuRouterList || [];
/** /**
@ -119,8 +120,8 @@ export function buildRoutes(menuRouterList) {
// 是否为外链 // 是否为外链
frameFlag: e.frameFlag, frameFlag: e.frameFlag,
// 外链地址 // 外链地址
frameUrl: e.frameUrl, frameUrl: e.frameUrl
}, }
}; };
if (e.frameFlag) { if (e.frameFlag) {
@ -151,6 +152,6 @@ export function buildRoutes(menuRouterList) {
path: '/', path: '/',
meta: {}, meta: {},
component: SmartLayout, component: SmartLayout,
children: resList, children: resList
}); });
} }

View File

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

@ -23,6 +23,15 @@
<a-form-item name="loginName"> <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>
<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-form-item name="password">
<a-input-password <a-input-password
v-model:value="loginForm.password" v-model:value="loginForm.password"
@ -66,7 +75,7 @@
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { onMounted, onUnmounted, reactive, ref } from 'vue'; import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { loginApi } from '/@/api/system/login/login-api'; import { loginApi } from '/src/api/system/login/login-api';
import { SmartLoading } from '/@/components/framework/smart-loading'; import { SmartLoading } from '/@/components/framework/smart-loading';
import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const'; import { LOGIN_DEVICE_ENUM } from '/@/constants/system/login-device-const';
import { useUserStore } from '/@/store/modules/system/user'; import { useUserStore } from '/@/store/modules/system/user';
@ -82,8 +91,6 @@
import { buildRoutes } from '/@/router/index'; import { buildRoutes } from '/@/router/index';
import { smartSentry } from '/@/lib/smart-sentry'; import { smartSentry } from '/@/lib/smart-sentry';
import { encryptData } from '/@/lib/encrypt'; 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 { saveTokenToCookie } from '/@/utils/cookie-util';
//--------------------- --------------------------------- //--------------------- ---------------------------------
@ -91,14 +98,15 @@
const loginForm = reactive({ const loginForm = reactive({
loginName: 'admin', loginName: 'admin',
password: '', password: '',
emailCode: '', //
captchaCode: '', captchaCode: '',
captchaUuid: '', captchaUuid: '',
loginDevice: LOGIN_DEVICE_ENUM.PC.value, loginDevice: LOGIN_DEVICE_ENUM.PC.value
}); });
const rules = { const rules = {
loginName: [{required: true, message: '用户名不能为空'}], loginName: [{required: true, message: '用户名不能为空'}],
password: [{required: true, message: '密码不能为空'}], password: [{required: true, message: '密码不能为空'}],
captchaCode: [{ required: true, message: '验证码不能为空' }], captchaCode: [{required: true, message: '验证码不能为空'}]
}; };
const showPassword = ref(false); const showPassword = ref(false);
@ -125,7 +133,7 @@
SmartLoading.show(); SmartLoading.show();
// //
let encryptPasswordForm = Object.assign({}, loginForm, { let encryptPasswordForm = Object.assign({}, loginForm, {
password: encryptData(loginForm.password), password: encryptData(loginForm.password)
}); });
const res = await loginApi.login(encryptPasswordForm); const res = await loginApi.login(encryptPasswordForm);
stopRefrestCaptchaInterval(); stopRefrestCaptchaInterval();
@ -152,6 +160,7 @@
//--------------------- --------------------------------- //--------------------- ---------------------------------
const captchaBase64Image = ref(''); const captchaBase64Image = ref('');
async function getCaptcha() { async function getCaptcha() {
try { try {
let captchaResult = await loginApi.getCaptcha(); let captchaResult = await loginApi.getCaptcha();
@ -164,6 +173,7 @@
} }
let refrestCaptchaInterval = null; let refrestCaptchaInterval = null;
function beginRefrestCaptchaInterval(expireSeconds) { function beginRefrestCaptchaInterval(expireSeconds) {
if (refrestCaptchaInterval === null) { if (refrestCaptchaInterval === null) {
refrestCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000); refrestCaptchaInterval = setInterval(getCaptcha, (expireSeconds - 5) * 1000);
@ -177,8 +187,60 @@
} }
} }
onMounted(getCaptcha); 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);
}
//
async function getTwoFactorLoginFlag() {
try {
let result = await loginApi.getTwoFactorLoginFlag();
emailCodeShowFlag.value = result.data;
} catch (e) {
smartSentry.captureError(e);
}
}
//
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> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import './login.less'; @import "./login.less";
</style> </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>