Compare commits

...

19 Commits

Author SHA1 Message Date
zhuoda
63ef235b95 v3.28.0 【优化】优化 Long、BigInteger、BigDecimal 的 JSON 序列化【优化】个人中心样式【优化】Spin加载 2025-08-24 23:05:00 +08:00
1024创新实验室
3ceea05ba1
!90 perf(json): 优化 Long、BigInteger、BigDecimal 的 JSON 序列化
Merge pull request !90 from CoderKK/feat-json
2025-08-24 12:47:54 +00:00
CoderKK
9fda0a7bd6 perf(json): 优化 Long、BigInteger、BigDecimal 的 JSON 序列化
- 为 Long、BigInteger 和 BigDecimal 类型添加自定义序列化器
- 超出 JavaScript 安全整数范围的 Long 值将被序列化为字符串
- BigInteger 和 BigDecimal值将始终被序列化为字符串
2025-08-20 16:04:18 +08:00
zhuoda
9361097097 v3.27.0 【优化】员工缓存;【优化】代码生成;【优化】redis缓存失效时间 2025-08-18 21:07:32 +08:00
zhuoda
942c628cc6 v3.27.0 【优化】员工缓存;【优化】代码生成;【优化】redis缓存失效时间 2025-08-18 20:49:07 +08:00
zhuoda
521b98746f v3.27.0 【优化】员工缓存;【优化】代码生成;【优化】redis缓存失效时间 2025-08-18 20:22:44 +08:00
zhuoda
4229ec80b0 v3.27.0 【优化】员工缓存;【优化】代码生成;【优化】redis缓存失效时间 2025-08-18 20:21:02 +08:00
zhuoda
4582656e27 v3.27.0 【优化】员工缓存;【优化】代码生成;【优化】redis缓存失效时间 2025-08-18 20:18:23 +08:00
zhuoda
fff0120058 v3.27.0 【优化】员工缓存;【优化】代码生成;【优化】redis缓存失效时间 2025-08-18 20:15:03 +08:00
1024创新实验室
b8f2200af6
!88 update smart-admin-web-javascript/src/views/system/home/index.vue.
Merge pull request !88 from 192890/N/A
2025-08-18 11:43:33 +00:00
1024创新实验室
0a56497b51
!89 feat(cache): 实现自定义 Spring Cache 缓存管理器支持 TTL- 新增 CustomRedisCacheManage…
Merge pull request !89 from CoderKK/feat-cache
2025-08-18 11:43:25 +00:00
CoderKK
2eb3742063 feat(cache): 实现自定义 Spring Cache 缓存管理器支持 TTL- 新增 CustomRedisCacheManager 类,用于解析 cacheName 中的 TTL 设置
- 修改 CacheConfig,使用新的自定义缓存管理器
- 更新 RedisCacheServiceImpl,使用 StandardCharsets.UTF_8 替代 魔法值"utf-8"
2025-08-15 13:45:36 +08:00
192890
74aa2da89b
update smart-admin-web-javascript/src/views/system/home/index.vue.
修改内容:订正“待办”、“更新日志”的位置

Signed-off-by: 192890 <zhang4342@163.com>
2025-08-12 06:31:49 +00:00
1024创新实验室
3dcad0b78a
!87 refactor(sql): 修改 smart_admin_v3.sql 中 response 字段的注释
Merge pull request !87 from CoderKK/fix
2025-08-11 10:59:53 +00:00
CoderKK
6ba6b18849 refactor(sql): 修改 smart_admin_v3.sql 中 response 字段的注释
- 将 `response` 字段的注释从 "请求参数" 修改为 "返回值"
2025-08-11 09:48:10 +08:00
zhuoda
d2c55e35ff v3.26.0 【优化】分页请求2次;【优化】菜单展开单个代码优化;【优化】操作记录返回结果;【优化】json viewer升级;【优化】S3协议优化;【优化】代码生成字典优化; 2025-08-10 13:02:01 +08:00
zhuoda
8135e0ec10 v3.25.0【优化】密码加密随机盐;【优化】java依赖版本;【优化】后端依赖库;【优化】单号生成器;【优化】防重复提交;【优化】sa-base.yaml健康检查邮箱;【新增】前端夜间模式;【优化】标签页issue;【优化】字典int回显bug 2025-08-05 21:00:12 +08:00
zhuoda
2a545117fa v3.25.0【优化】密码加密随机盐;【优化】java依赖版本;【优化】后端依赖库;【优化】单号生成器;【优化】防重复提交;【优化】sa-base.yaml健康检查邮箱;【新增】前端夜间模式;【优化】标签页issue;【优化】字典int回显bug 2025-08-05 20:46:21 +08:00
zhuoda
d8baf9dba7 v3.25.0【优化】密码加密随机盐;【优化】java依赖版本;【优化】后端依赖库;【优化】单号生成器;【优化】防重复提交;【优化】sa-base.yaml健康检查邮箱;【新增】前端夜间模式;【优化】标签页issue;【优化】字典int回显bug 2025-08-05 20:44:51 +08:00
148 changed files with 936 additions and 345 deletions

View File

@ -4,9 +4,9 @@
**<font color="#DC143C">国内首个满足《网络安全-三级等保》、《数据安全》</font>** 功能要求,支持登录限制、接口国产加解密、数据脱敏等一系列安全要求。 **<font color="#DC143C">国内首个满足《网络安全-三级等保》、《数据安全》</font>** 功能要求,支持登录限制、接口国产加解密、数据脱敏等一系列安全要求。
**<font color="#DC143C">支持国产数据库达梦、金仓、南大通用、OceanBase、GaussDB 高斯、阿里PolarDB、GoldenDB</font>** **<font color="#DC143C">支持国产数据库:达梦、金仓、南大通用、OceanBase、GaussDB 高斯、阿里PolarDB、GoldenDB】等主流数据库【Mysql, PostgreSQL】等</font>**
前端提供 **<font color="#DC143C">JavaScript和TypeScript双版本</font>**,后端提供 **<font color="#DC143C">Java8+SpringBoot2.X和Java17+SpringBoot3.X 双版本</font>**。 **<font color="#DC143C">前端提供JavaScript和TypeScript双版本后端提供Java8+SpringBoot2.X和Java17+SpringBoot3.X 双版本</font>**。
同时 **<font color="#DC143C">重磅开源</font>** 开源六年来 **<font color="#DC143C">千余家企业验证过且正在使用</font>** 的代码规范: **<font color="#DC143C">《高质量代码思想》、《Vue3规范》、《Java规范》</font>** ,让大家在这浮躁的世界里感受到一股把代码写好的清流!同时又能节省大量时间,减少加班,快乐工作,保持谦逊,保持学习,**<font color="#DC143C">热爱代码,更热爱生活</font>** 同时 **<font color="#DC143C">重磅开源</font>** 开源六年来 **<font color="#DC143C">千余家企业验证过且正在使用</font>** 的代码规范: **<font color="#DC143C">《高质量代码思想》、《Vue3规范》、《Java规范》</font>** ,让大家在这浮躁的世界里感受到一股把代码写好的清流!同时又能节省大量时间,减少加班,快乐工作,保持谦逊,保持学习,**<font color="#DC143C">热爱代码,更热爱生活</font>**
### **技术体系** ### **技术体系**

View File

@ -67,7 +67,7 @@ public class AdminInterceptor implements HandlerInterceptor {
Method method = ((HandlerMethod) handler).getMethod(); Method method = ((HandlerMethod) handler).getMethod();
NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class); NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class);
if (noNeedLogin != null) { if (noNeedLogin != null) {
checkActiveTimeout(requestEmployee); updateActiveTimeout(requestEmployee);
SmartRequestUtil.setRequestUser(requestEmployee); SmartRequestUtil.setRequestUser(requestEmployee);
return true; return true;
} }
@ -77,8 +77,8 @@ public class AdminInterceptor implements HandlerInterceptor {
return false; return false;
} }
// 检测token 活跃频率 // 更新活跃
checkActiveTimeout(requestEmployee); updateActiveTimeout(requestEmployee);
// --------------- 第三步 校验 权限 --------------- // --------------- 第三步 校验 权限 ---------------
@ -123,15 +123,12 @@ public class AdminInterceptor implements HandlerInterceptor {
/** /**
* 检测token 最低活跃频率单位如果 token 超过此时间没有访问系统就会被冻结 * 更新活跃时间
*/ */
private void checkActiveTimeout(RequestEmployee requestEmployee) { private void updateActiveTimeout(RequestEmployee requestEmployee) {
// 用户不在线也不用检测
if (requestEmployee == null) { if (requestEmployee == null) {
return; return;
} }
StpUtil.checkActiveTimeout();
StpUtil.updateLastActiveToNow(); StpUtil.updateLastActiveToNow();
} }

View File

@ -153,8 +153,19 @@ public class LoginManager {
} }
@CacheEvict(value = {AdminCacheConst.Login.USER_PERMISSION, AdminCacheConst.Login.REQUEST_EMPLOYEE}, allEntries = true) /**
public void clear(){ * 清除用户权限
*/
@CacheEvict(value = AdminCacheConst.Login.USER_PERMISSION)
public void clearUserPermission(Long employeeId) {
}
/**
* 清除用户登录信息
*/
@CacheEvict(value = AdminCacheConst.Login.REQUEST_EMPLOYEE)
public void clearUserLoginInfo(Long employeeId) {
} }

View File

@ -317,8 +317,8 @@ public class LoginService implements StpInterface {
// sa token 登出 // sa token 登出
StpUtil.logout(); StpUtil.logout();
// 空登录信息缓存 // 除用户登录信息缓存和权限信息
loginManager.clear(); this.clearLoginEmployeeCache(requestUser.getUserId());
//保存登出日志 //保存登出日志
LoginLogEntity loginEntity = LoginLogEntity.builder() LoginLogEntity loginEntity = LoginLogEntity.builder()
@ -474,6 +474,7 @@ public class LoginService implements StpInterface {
} }
public void clearLoginEmployeeCache(Long employeeId) { public void clearLoginEmployeeCache(Long employeeId) {
loginManager.clear(); loginManager.clearUserPermission(employeeId);
loginManager.clearUserLoginInfo(employeeId);
} }
} }

View File

@ -20,17 +20,25 @@ public class LongJsonSerializer extends JsonSerializer<Long> {
public static final LongJsonSerializer INSTANCE = new LongJsonSerializer(); public static final LongJsonSerializer INSTANCE = new LongJsonSerializer();
/**
* JS 安全整数范围
* 根据 JS Number.MIN_SAFE_INTEGER Number.MAX_SAFE_INTEGER 得来
*/
private static final long JS_MIN_SAFE_INTEGER = -9007199254740991L;
private static final long JS_MAX_SAFE_INTEGER = 9007199254740991L;
@Override @Override
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { public void serialize(Long value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
if (null == value) { if (null == value) {
gen.writeNull(); gen.writeNull();
return; return;
} }
// js中最大安全整数16位 Number.MAX_SAFE_INTEGER // 如果超出了 JavaScript 安全整数范围则序列化为字符串
String longStr = String.valueOf(value); if (value < JS_MIN_SAFE_INTEGER || value > JS_MAX_SAFE_INTEGER) {
if (longStr.length() > 16) { gen.writeString(Long.toString(value));
gen.writeString(longStr);
} else { } else {
// 否则序列化为数字
gen.writeNumber(value); gen.writeNumber(value);
} }
} }

View File

@ -5,18 +5,19 @@ import jakarta.annotation.Resource;
import net.lab1024.sa.base.module.support.cache.CacheService; import net.lab1024.sa.base.module.support.cache.CacheService;
import net.lab1024.sa.base.module.support.cache.CaffeineCacheServiceImpl; import net.lab1024.sa.base.module.support.cache.CaffeineCacheServiceImpl;
import net.lab1024.sa.base.module.support.cache.RedisCacheServiceImpl; import net.lab1024.sa.base.module.support.cache.RedisCacheServiceImpl;
import net.lab1024.sa.base.module.support.redis.CustomRedisCacheManager;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializationContext;
/** /**
* 缓存配置 * 缓存配置
* *
* @author zhoumingfa
* @date 2025/03/28
*/ */
@Configuration @Configuration
public class CacheConfig { public class CacheConfig {
@ -26,27 +27,44 @@ public class CacheConfig {
@Resource @Resource
private RedisConnectionFactory factory; private RedisConnectionFactory redisConnectionFactory;
/**
* 创建自定义Redis缓存管理器Bean 整合spring-cache
* Redis连接工厂用于建立与Redis服务器的连接
*
* @return CacheManager Redis缓存管理器实例
*/
@Bean @Bean
@ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE) @ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE)
public RedisCacheConfiguration redisCacheConfiguration() { public CacheManager cacheManager() {
return RedisCacheConfiguration.defaultCacheConfig() // 使用非阻塞模式的缓存写入器适用于大多数高并发场景
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
// 构建默认缓存配置
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
// 禁止缓存 null 避免缓存穿透
.disableCachingNullValues() .disableCachingNullValues()
.computePrefixWith(name -> "cache:" + name + ":") .computePrefixWith(name -> "cache:" + name + ":")
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer())); // 使用 FastJSON 序列化缓存值支持复杂对象
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericFastJsonRedisSerializer()));
// 返回自定义缓存管理器支持 cacheName#ttl 格式与永久缓存#-1
return new CustomRedisCacheManager(redisCacheWriter, defaultCacheConfig);
} }
@Bean
@ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE)
public CacheService redisCacheService() {
return new RedisCacheServiceImpl();
}
@Bean @Bean
@ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = CAFFEINE_CACHE) @ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE)
public CacheService caffeineCacheService() { public CacheService redisCacheService() {
return new CaffeineCacheServiceImpl(); return new RedisCacheServiceImpl();
} }
@Bean
@ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = CAFFEINE_CACHE)
public CacheService caffeineCacheService() {
return new CaffeineCacheServiceImpl();
}
} }

View File

@ -81,7 +81,7 @@ public class FileConfig implements WebMvcConfigurer {
StaticCredentialsProvider.create( StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey))) AwsBasicCredentials.create(accessKey, secretKey)))
.serviceConfiguration(S3Configuration.builder() .serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(false) .pathStyleAccessEnabled(true)
.chunkedEncodingEnabled(false) .chunkedEncodingEnabled(false)
.build()) .build())
.build(); .build();

View File

@ -2,6 +2,7 @@ package net.lab1024.sa.base.config;
import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.date.LocalDateTimeUtil;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; 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;
@ -13,6 +14,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
@ -37,6 +40,9 @@ public class JsonConfig {
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); builder.serializerByType(Long.class, LongJsonSerializer.INSTANCE);
builder.serializerByType(Long.TYPE, LongJsonSerializer.INSTANCE);
builder.serializerByType(BigInteger.class, ToStringSerializer.instance);
builder.serializerByType(BigDecimal.class, ToStringSerializer.instance);
}; };
} }

View File

@ -30,7 +30,7 @@ public class DictDataVO implements Serializable {
private String dictName; private String dictName;
@Schema(description = "字典禁用状态") @Schema(description = "字典禁用状态")
private Integer dictDisabledFlag; private Boolean dictDisabledFlag;
@Schema(description = "字典项值") @Schema(description = "字典项值")
private String dataValue; private String dataValue;

View File

@ -30,7 +30,7 @@ public class DictVO {
private String remark; private String remark;
@Schema(description = "禁用状态") @Schema(description = "禁用状态")
private Integer disabledFlag; private Boolean disabledFlag;
@Schema(description = "创建时间") @Schema(description = "创建时间")
private LocalDateTime createTime; private LocalDateTime createTime;

View File

@ -101,7 +101,18 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
userMetadata.put(USER_METADATA_FILE_FORMAT, fileType); userMetadata.put(USER_METADATA_FILE_FORMAT, fileType);
userMetadata.put(USER_METADATA_FILE_SIZE, String.valueOf(file.getSize())); userMetadata.put(USER_METADATA_FILE_SIZE, String.valueOf(file.getSize()));
PutObjectRequest putObjectRequest = PutObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).metadata(userMetadata).contentLength(file.getSize()).contentType(this.getContentType(fileType)).contentEncoding(StandardCharsets.UTF_8.name()).contentDisposition("attachment;filename=" + urlEncoderFilename).build(); // 根据文件路径获取并设置访问权限
ObjectCannedACL acl = this.getACL(path);
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(cloudConfig.getBucketName())
.key(fileKey)
.metadata(userMetadata)
.contentLength(file.getSize())
.contentType(this.getContentType(fileType))
.contentEncoding(StandardCharsets.UTF_8.name())
.contentDisposition("attachment;filename=" + urlEncoderFilename)
.acl(acl)
.build();
InputStream inputStream = null; InputStream inputStream = null;
try { try {
inputStream = file.getInputStream(); inputStream = file.getInputStream();
@ -112,10 +123,6 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
} finally { } finally {
IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(inputStream);
} }
// 根据文件路径获取并设置访问权限
ObjectCannedACL acl = this.getACL(path);
PutObjectAclRequest aclRequest = PutObjectAclRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).acl(this.getACL(path)).build();
s3Client.putObjectAcl(aclRequest);
// 返回上传结果 // 返回上传结果
FileUploadVO uploadVO = new FileUploadVO(); FileUploadVO uploadVO = new FileUploadVO();
uploadVO.setFileName(originalFileName); uploadVO.setFileName(originalFileName);

View File

@ -11,6 +11,7 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.constant.StringConst; import net.lab1024.sa.base.common.constant.StringConst;
import net.lab1024.sa.base.common.domain.RequestUser; import net.lab1024.sa.base.common.domain.RequestUser;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartIpUtil; import net.lab1024.sa.base.common.util.SmartIpUtil;
import net.lab1024.sa.base.common.util.SmartRequestUtil; import net.lab1024.sa.base.common.util.SmartRequestUtil;
import net.lab1024.sa.base.module.support.operatelog.OperateLogDao; import net.lab1024.sa.base.module.support.operatelog.OperateLogDao;
@ -46,7 +47,7 @@ import java.util.concurrent.ThreadPoolExecutor;
* @Date 2021-12-08 20:48:52 * @Date 2021-12-08 20:48:52
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/ */
@Slf4j @Slf4j
@Aspect @Aspect
@ -71,14 +72,14 @@ public abstract class OperateLogAspect {
public void logPointCut() { public void logPointCut() {
} }
@AfterReturning(pointcut = "logPointCut()") @AfterReturning(pointcut = "logPointCut()", returning = "responseDTO")
public void doAfterReturning(JoinPoint joinPoint) { public void doAfterReturning(JoinPoint joinPoint, Object responseDTO) {
handleLog(joinPoint, null); handleLog(joinPoint, null, responseDTO);
} }
@AfterThrowing(value = "logPointCut()", throwing = "e") @AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) { public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
handleLog(joinPoint, e); handleLog(joinPoint, e, null);
} }
/** /**
@ -109,16 +110,15 @@ public abstract class OperateLogAspect {
taskExecutor.setWaitForTasksToCompleteOnShutdown(true); taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
} }
protected void handleLog(final JoinPoint joinPoint, final Exception e) { protected void handleLog(final JoinPoint joinPoint, final Exception e, Object responseDTO) {
try { try {
OperateLog operateLog = this.getAnnotationLog(joinPoint); OperateLog operateLog = this.getAnnotationLog(joinPoint);
if (operateLog == null) { if (operateLog == null) {
return; return;
} }
this.submitLog(joinPoint, e); this.submitLog(joinPoint, e, responseDTO);
} catch (Exception exp) { } catch (Exception exp) {
log.error("保存操作日志异常:{}", exp.getMessage()); log.error("保存操作日志异常:{}", exp.getMessage());
exp.printStackTrace();
} }
} }
@ -173,11 +173,8 @@ public abstract class OperateLogAspect {
/** /**
* 提交存储操作日志 * 提交存储操作日志
* *
* @param joinPoint
* @param e
* @throws Exception
*/ */
private void submitLog(final JoinPoint joinPoint, final Throwable e) throws Exception { private void submitLog(final JoinPoint joinPoint, final Throwable e, Object responseDTO) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//设置用户信息 //设置用户信息
RequestUser user = SmartRequestUtil.getRequestUser(); RequestUser user = SmartRequestUtil.getRequestUser();
@ -191,7 +188,7 @@ public abstract class OperateLogAspect {
String methodName = joinPoint.getSignature().getName(); String methodName = joinPoint.getSignature().getName();
String operateMethod = className + "." + methodName; String operateMethod = className + "." + methodName;
String failReason = null; String failReason = null;
Boolean successFlag = true; boolean successFlag = true;
if (e != null) { if (e != null) {
successFlag = false; successFlag = false;
failReason = getExceptionString(e); failReason = getExceptionString(e);
@ -210,15 +207,32 @@ public abstract class OperateLogAspect {
.userAgent(user.getUserAgent()) .userAgent(user.getUserAgent())
.failReason(failReason) .failReason(failReason)
.successFlag(successFlag).build(); .successFlag(successFlag).build();
Operation apiOperation = this.getApiOperation(joinPoint); Operation apiOperation = this.getApiOperation(joinPoint);
if (apiOperation != null) { if (apiOperation != null) {
operateLogEntity.setContent(apiOperation.summary()); operateLogEntity.setContent(apiOperation.summary());
} }
Tag api = this.getApi(joinPoint); Tag api = this.getApi(joinPoint);
if (api != null) { if (api != null) {
String name = api.name(); String name = api.name();
operateLogEntity.setModule(StrUtil.join(",", name)); operateLogEntity.setModule(StrUtil.join(",", name));
} }
// 处理返回值 ResponseDTO
if(responseDTO instanceof ResponseDTO) {
ResponseDTO response = (ResponseDTO) responseDTO;
ResponseDTO logResponseDTO = new ResponseDTO(
response.getCode(),
response.getLevel(),
response.getOk(),
response.getMsg(),
null
);
logResponseDTO.setDataType(response.getDataType());
operateLogEntity.setResponse(JSON.toJSONString(logResponseDTO));
}
taskExecutor.execute(() -> { taskExecutor.execute(() -> {
this.saveLog(operateLogEntity); this.saveLog(operateLogEntity);
}); });

View File

@ -71,6 +71,11 @@ public class OperateLogEntity {
*/ */
private String param; private String param;
/**
* 返回值
*/
private String response;
/** /**
* 客户ip * 客户ip
*/ */

View File

@ -47,6 +47,9 @@ public class OperateLogVO {
@Schema(description = "请求参数") @Schema(description = "请求参数")
private String param; private String param;
@Schema(description = "返回值")
private String response;
@Schema(description = "客户ip") @Schema(description = "客户ip")
private String ip; private String ip;

View File

@ -0,0 +1,145 @@
package net.lab1024.sa.base.module.support.redis;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.data.redis.cache.*;
import java.time.Duration;
import static net.lab1024.sa.base.common.constant.StringConst.COLON;
/**
* 自定义 RedisCacheManager支持在 cacheName 中通过 '#' 指定 TTL过期时间
*
* @Author CoderKK
* @Date 2025-08-15 13:01:01
* <p>
* 支持格式{@code cacheName#ttl}其中 ttl 支持 Spring Duration 格式
* 特殊值{@code -1} 表示永久缓存永不过期
* </p>
*
* <h3>使用示例</h3>
* <pre>
* // 10 秒后过期
* &#64;Cacheable(value = "user#10s", key = "#id")
* // 2 小时后过期
* &#64;Cacheable(value = "report#2h", key = "#date")
* // 30 分钟后过期
* &#64;Cacheable(value = "session#30m", key = "#token")
* // 永不过期永久缓存适用于极少变化的配置数据
* &#64;Cacheable(value = "appConfig#-1", key = "'globalSettings'")
* // TTL使用全局默认过期时间 7
* &#64;Cacheable(value = "product", key = "#productId")
* </pre>
*
* <h3>生成的 Redis Key 格式</h3>
* <pre>
* cache:cacheName:key
* 例如cache:user:123
* cache:appConfig:globalSettings
* </pre>
*
* <h3>支持的 TTL 单位</h3>
* <ul>
* <li>{@code ms} / {@code millis} / {@code milliseconds} - 毫秒</li>
* <li>{@code s} / {@code secs} / {@code seconds} - </li>
* <li>{@code m} / {@code mins} / {@code minutes} - 分钟</li>
* <li>{@code h} / {@code hrs} / {@code hours} - 小时</li>
* <li>{@code d} / {@code days} - </li>
* </ul>
*
* <h3>注意事项</h3>
* <ul>
* <li>不写单位默认为毫秒</li>
* <li>永久缓存#-1不会自动过期请配合 &#64;CacheEvict 手动清理</li>
* <li>避免对频繁更新的数据使用永久缓存防止数据陈旧</li>
* <li>cacheName 中的 '#' 只解析第一个后续字符将作为 TTL 处理</li>
* </ul>
*/
@Slf4j
public class CustomRedisCacheManager extends RedisCacheManager {
/**
* 缓存全局前缀
*/
private static final String CACHE_PREFIX = "cache";
/**
* 自定义 TTL 分隔符用于在 cacheName 后附加过期时间
*/
private static final String CUSTOM_TTL_SEPARATOR = "#";
/**
* 默认缓存过期时间7
*/
private static final Duration DEFAULT_TTL = Duration.ofDays(7);
public CustomRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
/**
* 创建 RedisCache 实例支持从 cacheName 解析 TTL
*
* @param name 缓存名称支持 name#ttl 格式
* @param cacheConfig 默认缓存配置
* @return RedisCache
*/
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
Duration ttl = parseTtlFromCacheName(name);
if (ttl == null) {
ttl = DEFAULT_TTL;
}
CacheKeyPrefix keyPrefix = cacheName -> {
if (StrUtil.isBlank(cacheName)) {
return CACHE_PREFIX + COLON;
}
String[] parts = cacheName.split(CUSTOM_TTL_SEPARATOR, 2);
String cleanName = StrUtil.trim(parts[0]);
return CACHE_PREFIX + COLON + cleanName + COLON;
};
// 构建最终缓存配置设置 key 前缀 + TTL
RedisCacheConfiguration config = cacheConfig.computePrefixWith(keyPrefix).entryTtl(ttl);
return super.createRedisCache(name, config);
}
/**
* cacheName 中解析 TTL
*
* @param name 缓存名称格式如users#10m, products#2h, config#-1永久
* @return 解析出的 Duration若无效则返回 null若为 -1则返回 Duration.ofMillis(-1) 表示永久缓存
*/
private Duration parseTtlFromCacheName(String name) {
if (StrUtil.isBlank(name)) {
return null;
}
String[] parts = name.split(CUSTOM_TTL_SEPARATOR, 2);
if (parts.length < 2) {
return null; // TTL 部分
}
String ttlStr = StrUtil.trim(parts[1]);
if (StrUtil.isBlank(ttlStr)) {
return null;
}
// 特殊处理-1 表示永久缓存
if ("-1".equals(ttlStr)) {
return Duration.ofMillis(-1); // Spring Redis 中负数 Duration 表示永不过期
}
try {
Duration ttl = DurationStyle.detectAndParse(ttlStr);
return ttl.getSeconds() > 0 ? ttl : null;
} catch (IllegalArgumentException e) {
log.error("解析缓存 TTL 失败cacheName='{}', ttl='{}', 错误: {}", name, ttlStr, e);
return null;
}
}
}

View File

@ -48,7 +48,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")
@ -106,7 +106,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")

View File

@ -22,7 +22,7 @@
#end #end
#if($field.queryTypeEnum == "Dict") #if($field.queryTypeEnum == "Dict")
<a-form-item label="${field.label}" class="smart-query-form-item"> <a-form-item label="${field.label}" class="smart-query-form-item">
<DictSelect dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" /> <DictSelect :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
</a-form-item> </a-form-item>
#end #end
#if($field.queryTypeEnum == "Enum") #if($field.queryTypeEnum == "Enum")

View File

@ -48,7 +48,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")
@ -106,7 +106,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")

View File

@ -22,7 +22,7 @@
#end #end
#if($field.queryTypeEnum == "Dict") #if($field.queryTypeEnum == "Dict")
<a-form-item label="${field.label}" class="smart-query-form-item"> <a-form-item label="${field.label}" class="smart-query-form-item">
<DictSelect dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" /> <DictSelect :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
</a-form-item> </a-form-item>
#end #end
#if($field.queryTypeEnum == "Enum") #if($field.queryTypeEnum == "Enum")

View File

@ -26,7 +26,7 @@
AND (INSTR(module,#{query.keywords}) OR INSTR(content,#{query.keywords})) AND (INSTR(module,#{query.keywords}) OR INSTR(content,#{query.keywords}))
</if> </if>
<if test="query.requestKeywords != null and query.requestKeywords != ''"> <if test="query.requestKeywords != null and query.requestKeywords != ''">
AND (INSTR(url,#{query.requestKeywords}) OR INSTR(method,#{query.requestKeywords}) OR INSTR(param,#{query.requestKeywords})) AND (INSTR(url,#{query.requestKeywords}) OR INSTR(method,#{query.requestKeywords}) OR INSTR(param,#{query.requestKeywords}) OR INSTR(response,#{query.requestKeywords}))
</if> </if>
<if test="query.successFlag != null"> <if test="query.successFlag != null">
AND success_flag = #{query.successFlag} AND success_flag = #{query.successFlag}

View File

@ -48,7 +48,7 @@
<ip2region.version>2.7.0</ip2region.version> <ip2region.version>2.7.0</ip2region.version>
<bcprov.version>1.80</bcprov.version> <bcprov.version>1.80</bcprov.version>
<smartdb.version>1.2.0</smartdb.version> <smartdb.version>1.2.0</smartdb.version>
<redisson.version>3.50.0</redisson.version> <redisson.version>3.25.0</redisson.version>
<snakeyaml.version>2.4</snakeyaml.version> <snakeyaml.version>2.4</snakeyaml.version>
<freemarker.version>2.3.34</freemarker.version> <freemarker.version>2.3.34</freemarker.version>
<jsoup.version>1.21.1</jsoup.version> <jsoup.version>1.21.1</jsoup.version>

View File

@ -67,7 +67,7 @@ public class AdminInterceptor implements HandlerInterceptor {
Method method = ((HandlerMethod) handler).getMethod(); Method method = ((HandlerMethod) handler).getMethod();
NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class); NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class);
if (noNeedLogin != null) { if (noNeedLogin != null) {
checkActiveTimeout(requestEmployee); updateActiveTimeout(requestEmployee);
SmartRequestUtil.setRequestUser(requestEmployee); SmartRequestUtil.setRequestUser(requestEmployee);
return true; return true;
} }
@ -77,8 +77,8 @@ public class AdminInterceptor implements HandlerInterceptor {
return false; return false;
} }
// 检测token 活跃频率 // 更新活跃
checkActiveTimeout(requestEmployee); updateActiveTimeout(requestEmployee);
// --------------- 第三步 校验 权限 --------------- // --------------- 第三步 校验 权限 ---------------
@ -123,15 +123,12 @@ public class AdminInterceptor implements HandlerInterceptor {
/** /**
* 检测token 最低活跃频率单位如果 token 超过此时间没有访问系统就会被冻结 * 更新活跃时间
*/ */
private void checkActiveTimeout(RequestEmployee requestEmployee) { private void updateActiveTimeout(RequestEmployee requestEmployee) {
// 用户不在线也不用检测
if (requestEmployee == null) { if (requestEmployee == null) {
return; return;
} }
StpUtil.checkActiveTimeout();
StpUtil.updateLastActiveToNow(); StpUtil.updateLastActiveToNow();
} }

View File

@ -153,8 +153,19 @@ public class LoginManager {
} }
@CacheEvict(value = {AdminCacheConst.Login.USER_PERMISSION, AdminCacheConst.Login.REQUEST_EMPLOYEE}, allEntries = true) /**
public void clear(){ * 清除用户权限
*/
@CacheEvict(value = AdminCacheConst.Login.USER_PERMISSION)
public void clearUserPermission(Long employeeId) {
}
/**
* 清除用户登录信息
*/
@CacheEvict(value = AdminCacheConst.Login.REQUEST_EMPLOYEE)
public void clearUserLoginInfo(Long employeeId) {
} }

View File

@ -319,8 +319,8 @@ public class LoginService implements StpInterface {
// sa token 登出 // sa token 登出
StpUtil.logout(); StpUtil.logout();
// 空登录信息缓存 // 除用户登录信息缓存和权限信息
loginManager.clear(); this.clearLoginEmployeeCache(requestUser.getUserId());
//保存登出日志 //保存登出日志
LoginLogEntity loginEntity = LoginLogEntity.builder() LoginLogEntity loginEntity = LoginLogEntity.builder()
@ -476,6 +476,7 @@ public class LoginService implements StpInterface {
} }
public void clearLoginEmployeeCache(Long employeeId) { public void clearLoginEmployeeCache(Long employeeId) {
loginManager.clear(); loginManager.clearUserPermission(employeeId);
loginManager.clearUserLoginInfo(employeeId);
} }
} }

View File

@ -20,17 +20,25 @@ public class LongJsonSerializer extends JsonSerializer<Long> {
public static final LongJsonSerializer INSTANCE = new LongJsonSerializer(); public static final LongJsonSerializer INSTANCE = new LongJsonSerializer();
/**
* JS 安全整数范围
* 根据 JS Number.MIN_SAFE_INTEGER Number.MAX_SAFE_INTEGER 得来
*/
private static final long JS_MIN_SAFE_INTEGER = -9007199254740991L;
private static final long JS_MAX_SAFE_INTEGER = 9007199254740991L;
@Override @Override
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { public void serialize(Long value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
if (null == value) { if (null == value) {
gen.writeNull(); gen.writeNull();
return; return;
} }
// js中最大安全整数16位 Number.MAX_SAFE_INTEGER // 如果超出了 JavaScript 安全整数范围则序列化为字符串
String longStr = String.valueOf(value); if (value < JS_MIN_SAFE_INTEGER || value > JS_MAX_SAFE_INTEGER) {
if (longStr.length() > 16) { gen.writeString(Long.toString(value));
gen.writeString(longStr);
} else { } else {
// 否则序列化为数字
gen.writeNumber(value); gen.writeNumber(value);
} }
} }

View File

@ -4,10 +4,13 @@ import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import net.lab1024.sa.base.module.support.cache.CacheService; import net.lab1024.sa.base.module.support.cache.CacheService;
import net.lab1024.sa.base.module.support.cache.CaffeineCacheServiceImpl; import net.lab1024.sa.base.module.support.cache.CaffeineCacheServiceImpl;
import net.lab1024.sa.base.module.support.cache.RedisCacheServiceImpl; import net.lab1024.sa.base.module.support.cache.RedisCacheServiceImpl;
import net.lab1024.sa.base.module.support.redis.CustomRedisCacheManager;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializationContext;
@ -16,8 +19,6 @@ import javax.annotation.Resource;
/** /**
* 缓存配置 * 缓存配置
* *
* @author zhoumingfa
* @date 2025/03/28
*/ */
@Configuration @Configuration
public class CacheConfig { public class CacheConfig {
@ -27,27 +28,44 @@ public class CacheConfig {
@Resource @Resource
private RedisConnectionFactory factory; private RedisConnectionFactory redisConnectionFactory;
/**
* 创建自定义Redis缓存管理器Bean 整合spring-cache
* Redis连接工厂用于建立与Redis服务器的连接
*
* @return CacheManager Redis缓存管理器实例
*/
@Bean @Bean
@ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE) @ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE)
public RedisCacheConfiguration redisCacheConfiguration() { public CacheManager cacheManager() {
return RedisCacheConfiguration.defaultCacheConfig() // 使用非阻塞模式的缓存写入器适用于大多数高并发场景
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
// 构建默认缓存配置
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
// 禁止缓存 null 避免缓存穿透
.disableCachingNullValues() .disableCachingNullValues()
.computePrefixWith(name -> "cache:" + name + ":") .computePrefixWith(name -> "cache:" + name + ":")
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer())); // 使用 FastJSON 序列化缓存值支持复杂对象
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericFastJsonRedisSerializer()));
// 返回自定义缓存管理器支持 cacheName#ttl 格式与永久缓存#-1
return new CustomRedisCacheManager(redisCacheWriter, defaultCacheConfig);
} }
@Bean
@ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE)
public CacheService redisCacheService() {
return new RedisCacheServiceImpl();
}
@Bean @Bean
@ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = CAFFEINE_CACHE) @ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE)
public CacheService caffeineCacheService() { public CacheService redisCacheService() {
return new CaffeineCacheServiceImpl(); return new RedisCacheServiceImpl();
} }
@Bean
@ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = CAFFEINE_CACHE)
public CacheService caffeineCacheService() {
return new CaffeineCacheServiceImpl();
}
} }

View File

@ -81,7 +81,7 @@ public class FileConfig implements WebMvcConfigurer {
StaticCredentialsProvider.create( StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey))) AwsBasicCredentials.create(accessKey, secretKey)))
.serviceConfiguration(S3Configuration.builder() .serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(false) .pathStyleAccessEnabled(true)
.chunkedEncodingEnabled(false) .chunkedEncodingEnabled(false)
.build()) .build())
.build(); .build();

View File

@ -2,6 +2,7 @@ package net.lab1024.sa.base.config;
import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.date.LocalDateTimeUtil;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; 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;
@ -13,6 +14,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
@ -37,6 +40,9 @@ public class JsonConfig {
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); builder.serializerByType(Long.class, LongJsonSerializer.INSTANCE);
builder.serializerByType(Long.TYPE, LongJsonSerializer.INSTANCE);
builder.serializerByType(BigInteger.class, ToStringSerializer.instance);
builder.serializerByType(BigDecimal.class, ToStringSerializer.instance);
}; };
} }

View File

@ -30,7 +30,7 @@ public class DictDataVO implements Serializable {
private String dictName; private String dictName;
@Schema(description = "字典禁用状态") @Schema(description = "字典禁用状态")
private Integer dictDisabledFlag; private Boolean dictDisabledFlag;
@Schema(description = "字典项值") @Schema(description = "字典项值")
private String dataValue; private String dataValue;

View File

@ -30,7 +30,7 @@ public class DictVO {
private String remark; private String remark;
@Schema(description = "禁用状态") @Schema(description = "禁用状态")
private Integer disabledFlag; private Boolean disabledFlag;
@Schema(description = "创建时间") @Schema(description = "创建时间")
private LocalDateTime createTime; private LocalDateTime createTime;

View File

@ -106,7 +106,18 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
userMetadata.put(USER_METADATA_FILE_FORMAT, fileType); userMetadata.put(USER_METADATA_FILE_FORMAT, fileType);
userMetadata.put(USER_METADATA_FILE_SIZE, String.valueOf(file.getSize())); userMetadata.put(USER_METADATA_FILE_SIZE, String.valueOf(file.getSize()));
PutObjectRequest putObjectRequest = PutObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).metadata(userMetadata).contentLength(file.getSize()).contentType(this.getContentType(fileType)).contentEncoding(StandardCharsets.UTF_8.name()).contentDisposition("attachment;filename=" + urlEncoderFilename).build(); // 根据文件路径获取并设置访问权限
ObjectCannedACL acl = this.getACL(path);
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(cloudConfig.getBucketName())
.key(fileKey)
.metadata(userMetadata)
.contentLength(file.getSize())
.contentType(this.getContentType(fileType))
.contentEncoding(StandardCharsets.UTF_8.name())
.contentDisposition("attachment;filename=" + urlEncoderFilename)
.acl(acl)
.build();
InputStream inputStream = null; InputStream inputStream = null;
try { try {
inputStream = file.getInputStream(); inputStream = file.getInputStream();
@ -117,10 +128,6 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
} finally { } finally {
IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(inputStream);
} }
// 根据文件路径获取并设置访问权限
ObjectCannedACL acl = this.getACL(path);
PutObjectAclRequest aclRequest = PutObjectAclRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).acl(this.getACL(path)).build();
s3Client.putObjectAcl(aclRequest);
// 返回上传结果 // 返回上传结果
FileUploadVO uploadVO = new FileUploadVO(); FileUploadVO uploadVO = new FileUploadVO();
uploadVO.setFileName(originalFileName); uploadVO.setFileName(originalFileName);

View File

@ -3,11 +3,12 @@ package net.lab1024.sa.base.module.support.operatelog.core;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.constant.StringConst; import net.lab1024.sa.base.common.constant.StringConst;
import net.lab1024.sa.base.common.domain.RequestUser; import net.lab1024.sa.base.common.domain.RequestUser;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartIpUtil; import net.lab1024.sa.base.common.util.SmartIpUtil;
import net.lab1024.sa.base.common.util.SmartRequestUtil; import net.lab1024.sa.base.common.util.SmartRequestUtil;
import net.lab1024.sa.base.module.support.operatelog.OperateLogDao; import net.lab1024.sa.base.module.support.operatelog.OperateLogDao;
@ -20,7 +21,6 @@ import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.bind.BindResult; import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
@ -36,7 +36,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.BindException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
@ -48,7 +47,7 @@ import java.util.concurrent.ThreadPoolExecutor;
* @Date 2021-12-08 20:48:52 * @Date 2021-12-08 20:48:52
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/ */
@Slf4j @Slf4j
@Aspect @Aspect
@ -73,14 +72,14 @@ public abstract class OperateLogAspect {
public void logPointCut() { public void logPointCut() {
} }
@AfterReturning(pointcut = "logPointCut()") @AfterReturning(pointcut = "logPointCut()", returning = "responseDTO")
public void doAfterReturning(JoinPoint joinPoint) { public void doAfterReturning(JoinPoint joinPoint, Object responseDTO) {
handleLog(joinPoint, null); handleLog(joinPoint, null, responseDTO);
} }
@AfterThrowing(value = "logPointCut()", throwing = "e") @AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) { public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
handleLog(joinPoint, e); handleLog(joinPoint, e, null);
} }
/** /**
@ -111,16 +110,15 @@ public abstract class OperateLogAspect {
taskExecutor.setWaitForTasksToCompleteOnShutdown(true); taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
} }
protected void handleLog(final JoinPoint joinPoint, final Exception e) { protected void handleLog(final JoinPoint joinPoint, final Exception e, Object responseDTO) {
try { try {
OperateLog operateLog = this.getAnnotationLog(joinPoint); OperateLog operateLog = this.getAnnotationLog(joinPoint);
if (operateLog == null) { if (operateLog == null) {
return; return;
} }
this.submitLog(joinPoint, e); this.submitLog(joinPoint, e, responseDTO);
} catch (Exception exp) { } catch (Exception exp) {
log.error("保存操作日志异常:{}", exp.getMessage()); log.error("保存操作日志异常:{}", exp.getMessage());
exp.printStackTrace();
} }
} }
@ -175,11 +173,8 @@ public abstract class OperateLogAspect {
/** /**
* 提交存储操作日志 * 提交存储操作日志
* *
* @param joinPoint
* @param e
* @throws Exception
*/ */
private void submitLog(final JoinPoint joinPoint, final Throwable e) throws Exception { private void submitLog(final JoinPoint joinPoint, final Throwable e, Object responseDTO) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//设置用户信息 //设置用户信息
RequestUser user = SmartRequestUtil.getRequestUser(); RequestUser user = SmartRequestUtil.getRequestUser();
@ -193,7 +188,7 @@ public abstract class OperateLogAspect {
String methodName = joinPoint.getSignature().getName(); String methodName = joinPoint.getSignature().getName();
String operateMethod = className + "." + methodName; String operateMethod = className + "." + methodName;
String failReason = null; String failReason = null;
Boolean successFlag = true; boolean successFlag = true;
if (e != null) { if (e != null) {
successFlag = false; successFlag = false;
failReason = getExceptionString(e); failReason = getExceptionString(e);
@ -212,15 +207,32 @@ public abstract class OperateLogAspect {
.userAgent(user.getUserAgent()) .userAgent(user.getUserAgent())
.failReason(failReason) .failReason(failReason)
.successFlag(successFlag).build(); .successFlag(successFlag).build();
Operation apiOperation = this.getApiOperation(joinPoint); Operation apiOperation = this.getApiOperation(joinPoint);
if (apiOperation != null) { if (apiOperation != null) {
operateLogEntity.setContent(apiOperation.summary()); operateLogEntity.setContent(apiOperation.summary());
} }
Tag api = this.getApi(joinPoint); Tag api = this.getApi(joinPoint);
if (api != null) { if (api != null) {
String name = api.name(); String name = api.name();
operateLogEntity.setModule(StrUtil.join(",", name)); operateLogEntity.setModule(StrUtil.join(",", name));
} }
// 处理返回值 ResponseDTO
if(responseDTO instanceof ResponseDTO) {
ResponseDTO response = (ResponseDTO) responseDTO;
ResponseDTO logResponseDTO = new ResponseDTO(
response.getCode(),
response.getLevel(),
response.getOk(),
response.getMsg(),
null
);
logResponseDTO.setDataType(response.getDataType());
operateLogEntity.setResponse(JSON.toJSONString(logResponseDTO));
}
taskExecutor.execute(() -> { taskExecutor.execute(() -> {
this.saveLog(operateLogEntity); this.saveLog(operateLogEntity);
}); });

View File

@ -71,6 +71,11 @@ public class OperateLogEntity {
*/ */
private String param; private String param;
/**
* 返回值
*/
private String response;
/** /**
* 客户ip * 客户ip
*/ */

View File

@ -47,6 +47,9 @@ public class OperateLogVO {
@Schema(description = "请求参数") @Schema(description = "请求参数")
private String param; private String param;
@Schema(description = "返回值")
private String response;
@Schema(description = "客户ip") @Schema(description = "客户ip")
private String ip; private String ip;

View File

@ -0,0 +1,145 @@
package net.lab1024.sa.base.module.support.redis;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.data.redis.cache.*;
import java.time.Duration;
import static net.lab1024.sa.base.common.constant.StringConst.COLON;
/**
* 自定义 RedisCacheManager支持在 cacheName 中通过 '#' 指定 TTL过期时间
*
* @Author CoderKK
* @Date 2025-08-15 13:01:01
* <p>
* 支持格式{@code cacheName#ttl}其中 ttl 支持 Spring Duration 格式
* 特殊值{@code -1} 表示永久缓存永不过期
* </p>
*
* <h3>使用示例</h3>
* <pre>
* // 10 秒后过期
* &#64;Cacheable(value = "user#10s", key = "#id")
* // 2 小时后过期
* &#64;Cacheable(value = "report#2h", key = "#date")
* // 30 分钟后过期
* &#64;Cacheable(value = "session#30m", key = "#token")
* // 永不过期永久缓存适用于极少变化的配置数据
* &#64;Cacheable(value = "appConfig#-1", key = "'globalSettings'")
* // TTL使用全局默认过期时间 7
* &#64;Cacheable(value = "product", key = "#productId")
* </pre>
*
* <h3>生成的 Redis Key 格式</h3>
* <pre>
* cache:cacheName:key
* 例如cache:user:123
* cache:appConfig:globalSettings
* </pre>
*
* <h3>支持的 TTL 单位</h3>
* <ul>
* <li>{@code ms} / {@code millis} / {@code milliseconds} - 毫秒</li>
* <li>{@code s} / {@code secs} / {@code seconds} - </li>
* <li>{@code m} / {@code mins} / {@code minutes} - 分钟</li>
* <li>{@code h} / {@code hrs} / {@code hours} - 小时</li>
* <li>{@code d} / {@code days} - </li>
* </ul>
*
* <h3>注意事项</h3>
* <ul>
* <li>不写单位默认为毫秒</li>
* <li>永久缓存#-1不会自动过期请配合 &#64;CacheEvict 手动清理</li>
* <li>避免对频繁更新的数据使用永久缓存防止数据陈旧</li>
* <li>cacheName 中的 '#' 只解析第一个后续字符将作为 TTL 处理</li>
* </ul>
*/
@Slf4j
public class CustomRedisCacheManager extends RedisCacheManager {
/**
* 缓存全局前缀
*/
private static final String CACHE_PREFIX = "cache";
/**
* 自定义 TTL 分隔符用于在 cacheName 后附加过期时间
*/
private static final String CUSTOM_TTL_SEPARATOR = "#";
/**
* 默认缓存过期时间7
*/
private static final Duration DEFAULT_TTL = Duration.ofDays(7);
public CustomRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
/**
* 创建 RedisCache 实例支持从 cacheName 解析 TTL
*
* @param name 缓存名称支持 name#ttl 格式
* @param cacheConfig 默认缓存配置
* @return RedisCache
*/
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
Duration ttl = parseTtlFromCacheName(name);
if (ttl == null) {
ttl = DEFAULT_TTL;
}
CacheKeyPrefix keyPrefix = cacheName -> {
if (StrUtil.isBlank(cacheName)) {
return CACHE_PREFIX + COLON;
}
String[] parts = cacheName.split(CUSTOM_TTL_SEPARATOR, 2);
String cleanName = StrUtil.trim(parts[0]);
return CACHE_PREFIX + COLON + cleanName + COLON;
};
// 构建最终缓存配置设置 key 前缀 + TTL
RedisCacheConfiguration config = cacheConfig.computePrefixWith(keyPrefix).entryTtl(ttl);
return super.createRedisCache(name, config);
}
/**
* cacheName 中解析 TTL
*
* @param name 缓存名称格式如users#10m, products#2h, config#-1永久
* @return 解析出的 Duration若无效则返回 null若为 -1则返回 Duration.ofMillis(-1) 表示永久缓存
*/
private Duration parseTtlFromCacheName(String name) {
if (StrUtil.isBlank(name)) {
return null;
}
String[] parts = name.split(CUSTOM_TTL_SEPARATOR, 2);
if (parts.length < 2) {
return null; // TTL 部分
}
String ttlStr = StrUtil.trim(parts[1]);
if (StrUtil.isBlank(ttlStr)) {
return null;
}
// 特殊处理-1 表示永久缓存
if ("-1".equals(ttlStr)) {
return Duration.ofMillis(-1); // Spring Redis 中负数 Duration 表示永不过期
}
try {
Duration ttl = DurationStyle.detectAndParse(ttlStr);
return ttl.getSeconds() > 0 ? ttl : null;
} catch (IllegalArgumentException e) {
log.error("解析缓存 TTL 失败cacheName='{}', ttl='{}', 错误: {}", name, ttlStr, e);
return null;
}
}
}

View File

@ -48,7 +48,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")
@ -106,7 +106,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")

View File

@ -22,7 +22,7 @@
#end #end
#if($field.queryTypeEnum == "Dict") #if($field.queryTypeEnum == "Dict")
<a-form-item label="${field.label}" class="smart-query-form-item"> <a-form-item label="${field.label}" class="smart-query-form-item">
<DictSelect dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" /> <DictSelect :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
</a-form-item> </a-form-item>
#end #end
#if($field.queryTypeEnum == "Enum") #if($field.queryTypeEnum == "Enum")

View File

@ -48,7 +48,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")
@ -106,7 +106,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")

View File

@ -22,7 +22,7 @@
#end #end
#if($field.queryTypeEnum == "Dict") #if($field.queryTypeEnum == "Dict")
<a-form-item label="${field.label}" class="smart-query-form-item"> <a-form-item label="${field.label}" class="smart-query-form-item">
<DictSelect dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" /> <DictSelect :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
</a-form-item> </a-form-item>
#end #end
#if($field.queryTypeEnum == "Enum") #if($field.queryTypeEnum == "Enum")

View File

@ -26,7 +26,7 @@
AND (INSTR(module,#{query.keywords}) OR INSTR(content,#{query.keywords})) AND (INSTR(module,#{query.keywords}) OR INSTR(content,#{query.keywords}))
</if> </if>
<if test="query.requestKeywords != null and query.requestKeywords != ''"> <if test="query.requestKeywords != null and query.requestKeywords != ''">
AND (INSTR(url,#{query.requestKeywords}) OR INSTR(method,#{query.requestKeywords}) OR INSTR(param,#{query.requestKeywords})) AND (INSTR(url,#{query.requestKeywords}) OR INSTR(method,#{query.requestKeywords}) OR INSTR(param,#{query.requestKeywords}) OR INSTR(response,#{query.requestKeywords}))
</if> </if>
<if test="query.successFlag != null"> <if test="query.successFlag != null">
AND success_flag = #{query.successFlag} AND success_flag = #{query.successFlag}

View File

@ -24,7 +24,7 @@
"axios": "1.6.8", "axios": "1.6.8",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"crypto-js": "4.1.1", "crypto-js": "4.1.1",
"dayjs": "1.10.5", "dayjs": "1.11.13",
"decimal.js": "10.3.1", "decimal.js": "10.3.1",
"diff": "5.2.0", "diff": "5.2.0",
"diff2html": "3.4.47", "diff2html": "3.4.47",
@ -43,7 +43,7 @@
"vue": "3.4.27", "vue": "3.4.27",
"vue-i18n": "9.13.1", "vue-i18n": "9.13.1",
"vue-router": "4.3.2", "vue-router": "4.3.2",
"vue3-json-viewer": "2.2.2" "vue3-json-viewer": "2.3.1"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",

View File

@ -51,7 +51,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
<a-modal v-model:open="visibleDiff" width="90%" title="数据比对" :footer="null"> <a-modal v-model:open="visibleDiff" width="90%" title="数据比对" :footer="null">

View File

@ -71,7 +71,6 @@
v-model:pageSize="params.pageSize" v-model:pageSize="params.pageSize"
:total="total" :total="total"
@change="queryEmployee" @change="queryEmployee"
@showSizeChange="queryEmployee"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -27,7 +27,7 @@ export const appDefaultConfig = {
// 圆角 // 圆角
borderRadius: 6, borderRadius: 6,
// 菜单展开模式 // 菜单展开模式
flatPattern: false, menuSingleExpandFlag: true,
// 标签页 // 标签页
pageTagFlag: true, pageTagFlag: true,
// 标签页样式: default、 antd、chrome // 标签页样式: default、 antd、chrome

View File

@ -21,11 +21,11 @@ export default {
'setting.menu.layout': 'Menu Layout', 'setting.menu.layout': 'Menu Layout',
'setting.menu.width': 'Menu Width', 'setting.menu.width': 'Menu Width',
'setting.menu.theme': 'Menu Theme', 'setting.menu.theme': 'Menu Theme',
'setting.menu.expand': 'Menu Expand',
'setting.page.width': 'Page Width', 'setting.page.width': 'Page Width',
'setting.border.radius': 'Border Radius', 'setting.border.radius': 'Border Radius',
'setting.compact': 'Page Compact', 'setting.compact': 'Page Compact',
'setting.bread': 'Show Bread', 'setting.bread': 'Show Bread',
'setting.flatPattern': 'Flat Pattern',
'setting.pagetag': 'Show PageTag', 'setting.pagetag': 'Show PageTag',
'setting.pagetag.style': 'PageTag Style', 'setting.pagetag.style': 'PageTag Style',
'setting.footer': 'Show Footer', 'setting.footer': 'Show Footer',

View File

@ -21,11 +21,11 @@ export default {
'setting.menu.layout': '菜单布局', 'setting.menu.layout': '菜单布局',
'setting.menu.width': '菜单宽度', 'setting.menu.width': '菜单宽度',
'setting.menu.theme': '菜单主题', 'setting.menu.theme': '菜单主题',
'setting.menu.expand': '菜单展开',
'setting.compact': '页面紧凑', 'setting.compact': '页面紧凑',
'setting.border.radius': '页面圆角', 'setting.border.radius': '页面圆角',
'setting.page.width': '页面宽度', 'setting.page.width': '页面宽度',
'setting.bread': '面包屑', 'setting.bread': '面包屑',
'setting.flatPattern': '菜单展开模式',
'setting.pagetag': '标签页', 'setting.pagetag': '标签页',
'setting.pagetag.style': '标签页样式', 'setting.pagetag.style': '标签页样式',
'setting.footer': '页脚', 'setting.footer': '页脚',

View File

@ -83,8 +83,8 @@
<a-radio-button value="chrome">Chrome</a-radio-button> <a-radio-button value="chrome">Chrome</a-radio-button>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item :label="$t('setting.flatPattern')" v-if="formState.layout === LAYOUT_ENUM.SIDE.value"> <a-form-item :label="$t('setting.menu.expand')" v-if="formState.layout === LAYOUT_ENUM.SIDE.value">
<a-switch @change="changeFlatPattern" v-model:checked="formState.flatPattern" checked-children="多个" un-checked-children="" /> <a-switch @change="changeMenuExpandFlag" v-model:checked="formState.menuSingleExpandFlag" checked-children="单个" un-checked-children="" />
</a-form-item> </a-form-item>
<a-form-item :label="$t('setting.pagetag')"> <a-form-item :label="$t('setting.pagetag')">
<a-switch @change="changePageTagFlag" v-model:checked="formState.pageTagFlag" checked-children="显示" un-checked-children="隐藏" /> <a-switch @change="changePageTagFlag" v-model:checked="formState.pageTagFlag" checked-children="显示" un-checked-children="隐藏" />
@ -213,8 +213,8 @@
borderRadius: appConfigStore.borderRadius, borderRadius: appConfigStore.borderRadius,
// //
pageTagFlag: appConfigStore.pageTagFlag, pageTagFlag: appConfigStore.pageTagFlag,
// //
flatPattern: appConfigStore.flatPattern, menuSingleExpandFlag: appConfigStore.menuSingleExpandFlag,
// //
pageTagStyle: appConfigStore.pageTagStyle, pageTagStyle: appConfigStore.pageTagStyle,
// //
@ -303,9 +303,9 @@
}); });
} }
function changeFlatPattern(e) { function changeMenuExpandFlag(e) {
appConfigStore.$patch({ appConfigStore.$patch({
flatPattern: e, menuSingleExpandFlag: e,
}); });
} }

View File

@ -36,7 +36,7 @@
import { useUserStore } from '/@/store/modules/system/user'; import { useUserStore } from '/@/store/modules/system/user';
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme); const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
const flatPattern = computed(() => useAppConfigStore().$state.flatPattern); const menuSingleExpandFlag = computed(() => useAppConfigStore().$state.menuSingleExpandFlag);
const props = defineProps({ const props = defineProps({
collapsed: { collapsed: {
@ -46,8 +46,7 @@
}); });
const menuTree = computed(() => useUserStore().getMenuTree || []); const menuTree = computed(() => useUserStore().getMenuTree || []);
const rootSubmenuKeys = computed(()=>menuTree.value.map(item=>item.menuId)); const rootSubmenuKeys = computed(() => menuTree.value.map((item) => item.menuId));
// //
let currentRoute = useRoute(); let currentRoute = useRoute();
@ -76,9 +75,15 @@
let parentList = menuParentIdListMap.get(currentRoute.name) || []; let parentList = menuParentIdListMap.get(currentRoute.name) || [];
// openkey // openkey
if (!props.collapsed) { if (props.collapsed) {
return;
}
let needOpenKeys = _.map(parentList, 'name').map(Number);
if (menuSingleExpandFlag.value) {
openKeys.value = [...needOpenKeys];
} else {
// 使lodashunion // 使lodashunion
let needOpenKeys = _.map(parentList, 'name').map(Number);
openKeys.value = _.union(openKeys.value, needOpenKeys); openKeys.value = _.union(openKeys.value, needOpenKeys);
} }
} }
@ -92,17 +97,18 @@
immediate: true, immediate: true,
} }
); );
function onOpenChange(openKeysParams){
if(flatPattern.value){ function onOpenChange(openKeysParams) {
return; if (!menuSingleExpandFlag.value) {
return;
}
const latestOpenKey = openKeysParams.find((key) => openKeys.value.indexOf(key) === -1);
if (rootSubmenuKeys.value.indexOf(latestOpenKey) === -1) {
openKeys.value = openKeysParams;
} else {
openKeys.value = latestOpenKey ? [latestOpenKey] : [];
}
} }
const latestOpenKey = openKeysParams.find(key => openKeys.value.indexOf(key) === -1);
if (rootSubmenuKeys.value.indexOf(latestOpenKey) === -1) {
openKeys.value = openKeysParams;
} else {
openKeys.value = latestOpenKey ? [latestOpenKey] : [];
}
};
defineExpose({ defineExpose({
updateOpenKeysAndSelectKeys, updateOpenKeysAndSelectKeys,
}); });

View File

@ -8,6 +8,7 @@
* @Copyright 1024创新实验室 https://1024lab.net Since 2012 * @Copyright 1024创新实验室 https://1024lab.net Since 2012
*/ */
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { smartSentry } from '/@/lib/smart-sentry.js';
export const useSpinStore = defineStore({ export const useSpinStore = defineStore({
id: 'spin', id: 'spin',
@ -18,13 +19,27 @@ export const useSpinStore = defineStore({
actions: { actions: {
hide() { hide() {
this.loading = false; this.loading = false;
let spins = document.querySelector('.ant-spin-nested-loading'); // 安全的DOM操作避免null引用错误
spins.style.zIndex = 999; try {
const spins = document.querySelector('.ant-spin-nested-loading');
if (spins) {
spins.style.zIndex = '999';
}
} catch (error) {
smartSentry.captureError('Spin hide操作失败:', error);
}
}, },
show() { show() {
this.loading = true; this.loading = true;
let spins = document.querySelector('.ant-spin-nested-loading'); // 安全的DOM操作避免null引用错误
spins.style.zIndex = 1001; try {
const spins = document.querySelector('.ant-spin-nested-loading');
if (spins) {
spins.style.zIndex = '1001';
}
} catch (error) {
smartSentry.captureError('Spin hide操作失败:', error);
}
}, },
}, },
}); });

View File

@ -149,7 +149,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryData" @change="queryData"
@showSizeChange="queryData"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -78,7 +78,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -63,7 +63,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryEmployee" @change="queryEmployee"
@showSizeChange="queryEmployee"
:show-total="showTableTotal" :show-total="showTableTotal"
/> />
</div> </div>

View File

@ -75,7 +75,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -102,7 +102,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -53,7 +53,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryViewRecord" @change="queryViewRecord"
@showSizeChange="queryViewRecord"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -72,7 +72,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryNoticeList" @change="queryNoticeList"
@showSizeChange="queryNoticeList"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -122,7 +122,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryNoticeList" @change="queryNoticeList"
@showSizeChange="queryNoticeList"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -107,7 +107,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryData" @change="queryData"
@showSizeChange="queryData"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -74,7 +74,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -3,12 +3,16 @@ import { convertUpperCamel } from '/@/utils/str-util';
// -------------------------------- java 类型 -------------------------------- // -------------------------------- java 类型 --------------------------------
export const JavaTypeMap = new Map(); export const JavaTypeMap = new Map();
JavaTypeMap.set('bit', 'Boolean'); JavaTypeMap.set('bit', 'Boolean');
JavaTypeMap.set('bool', 'Boolean');
JavaTypeMap.set('int', 'Integer'); JavaTypeMap.set('int', 'Integer');
JavaTypeMap.set('int2', 'Integer');
JavaTypeMap.set('int4', 'Integer');
JavaTypeMap.set('tinyint', 'Integer'); JavaTypeMap.set('tinyint', 'Integer');
JavaTypeMap.set('smallint', 'Integer'); JavaTypeMap.set('smallint', 'Integer');
JavaTypeMap.set('integer', 'Integer'); JavaTypeMap.set('integer', 'Integer');
JavaTypeMap.set('year', 'Integer'); JavaTypeMap.set('year', 'Integer');
JavaTypeMap.set('bigint', 'Long'); JavaTypeMap.set('bigint', 'Long');
JavaTypeMap.set('int8', 'Long');
JavaTypeMap.set('float', 'BigDecimal'); JavaTypeMap.set('float', 'BigDecimal');
JavaTypeMap.set('double', 'BigDecimal'); JavaTypeMap.set('double', 'BigDecimal');
JavaTypeMap.set('decimal', 'BigDecimal'); JavaTypeMap.set('decimal', 'BigDecimal');
@ -20,6 +24,7 @@ JavaTypeMap.set('longtext', 'String');
JavaTypeMap.set('blob', 'String'); JavaTypeMap.set('blob', 'String');
JavaTypeMap.set('date', 'LocalDate'); JavaTypeMap.set('date', 'LocalDate');
JavaTypeMap.set('datetime', 'LocalDateTime'); JavaTypeMap.set('datetime', 'LocalDateTime');
JavaTypeMap.set('timestamp', 'LocalDateTime');
export const JavaTypeList = [ export const JavaTypeList = [
'Boolean', // 'Boolean', //
@ -39,7 +44,11 @@ export function getJavaType(dataType) {
// -------------------------------- js 类型 -------------------------------- // -------------------------------- js 类型 --------------------------------
export const JsTypeMap = new Map(); export const JsTypeMap = new Map();
JsTypeMap.set('bit', 'Boolean'); JsTypeMap.set('bit', 'Boolean');
JsTypeMap.set('bool', 'Boolean');
JsTypeMap.set('int', 'Number'); JsTypeMap.set('int', 'Number');
JsTypeMap.set('int2', 'Number');
JsTypeMap.set('int4', 'Number');
JsTypeMap.set('int8', 'Number');
JsTypeMap.set('tinyint', 'Number'); JsTypeMap.set('tinyint', 'Number');
JsTypeMap.set('smallint', 'Number'); JsTypeMap.set('smallint', 'Number');
JsTypeMap.set('integer', 'Number'); JsTypeMap.set('integer', 'Number');
@ -50,12 +59,14 @@ JsTypeMap.set('double', 'Number');
JsTypeMap.set('decimal', 'Number'); JsTypeMap.set('decimal', 'Number');
JsTypeMap.set('char', 'String'); JsTypeMap.set('char', 'String');
JsTypeMap.set('varchar', 'String'); JsTypeMap.set('varchar', 'String');
JsTypeMap.set('character', 'String');
JsTypeMap.set('tinytext', 'String'); JsTypeMap.set('tinytext', 'String');
JsTypeMap.set('text', 'String'); JsTypeMap.set('text', 'String');
JsTypeMap.set('longtext', 'String'); JsTypeMap.set('longtext', 'String');
JsTypeMap.set('blob', 'String'); JsTypeMap.set('blob', 'String');
JsTypeMap.set('date', 'Date'); JsTypeMap.set('date', 'Date');
JsTypeMap.set('datetime', 'Date'); JsTypeMap.set('datetime', 'Date');
JsTypeMap.set('timestamp', 'Date');
export const JsTypeList = [ export const JsTypeList = [
'Boolean', // 'Boolean', //
@ -72,17 +83,23 @@ export function getJsType(dataType) {
export const FrontComponentMap = new Map(); export const FrontComponentMap = new Map();
FrontComponentMap.set('bit', 'BooleanSelect'); FrontComponentMap.set('bit', 'BooleanSelect');
FrontComponentMap.set('bool', 'BooleanSelect');
FrontComponentMap.set('int', 'InputNumber'); FrontComponentMap.set('int', 'InputNumber');
FrontComponentMap.set('int2', 'InputNumber');
FrontComponentMap.set('int4', 'InputNumber');
FrontComponentMap.set('int8', 'InputNumber');
FrontComponentMap.set('tinyint', 'InputNumber'); FrontComponentMap.set('tinyint', 'InputNumber');
FrontComponentMap.set('smallint', 'InputNumber'); FrontComponentMap.set('smallint', 'InputNumber');
FrontComponentMap.set('integer', 'InputNumber'); FrontComponentMap.set('integer', 'InputNumber');
FrontComponentMap.set('year', 'Date'); FrontComponentMap.set('year', 'Date');
FrontComponentMap.set('timestamp', 'Date');
FrontComponentMap.set('bigint', 'InputNumber'); FrontComponentMap.set('bigint', 'InputNumber');
FrontComponentMap.set('float', 'InputNumber'); FrontComponentMap.set('float', 'InputNumber');
FrontComponentMap.set('double', 'InputNumber'); FrontComponentMap.set('double', 'InputNumber');
FrontComponentMap.set('decimal', 'InputNumber'); FrontComponentMap.set('decimal', 'InputNumber');
FrontComponentMap.set('char', 'Input'); FrontComponentMap.set('char', 'Input');
FrontComponentMap.set('varchar', 'Input'); FrontComponentMap.set('varchar', 'Input');
FrontComponentMap.set('character', 'Input');
FrontComponentMap.set('tinytext', 'Input'); FrontComponentMap.set('tinytext', 'Input');
FrontComponentMap.set('text', 'Textarea'); FrontComponentMap.set('text', 'Textarea');
FrontComponentMap.set('longtext', 'Textarea'); FrontComponentMap.set('longtext', 'Textarea');

View File

@ -66,7 +66,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -98,7 +98,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -64,7 +64,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryList" @change="queryList"
@showSizeChange="queryList"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -103,7 +103,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryData" @change="queryData"
@showSizeChange="queryData"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -79,7 +79,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -88,7 +88,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryHelpDocList" @change="queryHelpDocList"
@showSizeChange="queryHelpDocList"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -49,7 +49,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryViewRecord" @change="queryViewRecord"
@showSizeChange="queryViewRecord"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -117,7 +117,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryJobList" @change="queryJobList"
@showSizeChange="queryJobList"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -71,7 +71,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryLogList" @change="queryLogList"
@showSizeChange="queryLogList"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -136,7 +136,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryJobList" @change="queryJobList"
@showSizeChange="queryJobList"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -102,7 +102,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="onSearch" @change="onSearch"
@showSizeChange="onSearch"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -89,7 +89,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -47,7 +47,6 @@
v-model:pageSize="queryParam.pageSize" v-model:pageSize="queryParam.pageSize"
:total="total" :total="total"
@change="queryList" @change="queryList"
@showSizeChange="queryList"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -78,7 +78,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryData" @change="queryData"
@showSizeChange="queryData"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -12,6 +12,10 @@
<div class="info-box"> <div class="info-box">
<a-row class="smart-margin-top10"> <a-row class="smart-margin-top10">
<a-col :span="16"> <a-col :span="16">
<a-row class="detail-info">
<a-col :span="12"> 用户id{{ detail.operateUserId }}</a-col>
<a-col :span="12"> 用户名称 {{ detail.operateUserName }}</a-col>
</a-row>
<a-row class="detail-info"> <a-row class="detail-info">
<a-col :span="12"> 请求url {{ detail.url }}</a-col> <a-col :span="12"> 请求url {{ detail.url }}</a-col>
<a-col :span="12"> 请求日期 {{ detail.createTime }}</a-col> <a-col :span="12"> 请求日期 {{ detail.createTime }}</a-col>
@ -21,8 +25,7 @@
<a-col :span="12"> IP地区 {{ detail.ipRegion }}</a-col> <a-col :span="12"> IP地区 {{ detail.ipRegion }}</a-col>
</a-row> </a-row>
<a-row class="detail-info"> <a-row class="detail-info">
<a-col :span="12"> 用户id{{ detail.operateUserId }}</a-col> <a-col :span="12"> 客户端 {{ detail.os }} / {{ detail.browser }} {{ detail.device ? '/' + detail.device : detail.device }}</a-col>
<a-col :span="12"> 用户名称 {{ detail.operateUserName }}</a-col>
</a-row> </a-row>
</a-col> </a-col>
<a-col :span="8"> <a-col :span="8">
@ -32,21 +35,26 @@
</a-typography-text> </a-typography-text>
</a-col> </a-col>
</a-row> </a-row>
</div> <a-row class="detail-info">
<div class="info-box"> <a-col :span="24"> 方法 {{ detail.method }}</a-col>
<h4>请求明细</h4> </a-row>
<a-col :span="24"> 方法 {{ detail.method }}</a-col> <a-row class="detail-info">
<a-col :span="24"> 说明 {{ detail.module }} - {{ detail.content }}</a-col> <a-col :span="24"> 说明 {{ detail.module }} - {{ detail.content }}</a-col>
</a-row>
</div> </div>
<div class="info-box"> <div class="info-box">
<h4>请求参数</h4> <h4>请求参数</h4>
<JsonViewer :value="detail.param ? JSON.parse(detail.param) : ''" theme="jv-dark" copyable boxed sort /> <JsonViewer :value="detail.param ? JSON.parse(detail.param) : ''" :expanded="true" :expandDepth="10" copyable boxed sort theme="light" />
</div>
<div class="info-box" v-if="detail.successFlag">
<h4>返回结果</h4>
<JsonViewer :value="detail.response ? JSON.parse(detail.response) : ''" :expanded="true" :expandDepth="10" copyable boxed sort theme="light" />
</div> </div>
<div class="info-box" v-if="detail.failReason"> <div class="info-box" v-if="detail.failReason">
<h4>请求失败原因</h4> <h4>请求失败原因</h4>
<div> <a-card>
{{ detail.failReason }} {{ detail.failReason }}
</div> </a-card>
</div> </div>
</a-modal> </a-modal>
</template> </template>
@ -57,6 +65,7 @@
import { operateLogApi } from '/@/api/support/operate-log-api'; import { operateLogApi } from '/@/api/support/operate-log-api';
import { smartSentry } from '/@/lib/smart-sentry'; import { smartSentry } from '/@/lib/smart-sentry';
import { SmartLoading } from '/@/components/framework/smart-loading'; import { SmartLoading } from '/@/components/framework/smart-loading';
import uaparser from 'ua-parser-js';
defineExpose({ defineExpose({
show, show,
@ -87,11 +96,16 @@
param: '', param: '',
url: '', url: '',
}); });
async function getDetail(operateLogId) { async function getDetail(operateLogId) {
try { try {
SmartLoading.show(); SmartLoading.show();
let res = await operateLogApi.detail(operateLogId); let res = await operateLogApi.detail(operateLogId);
detail = Object.assign(detail, res.data); detail = Object.assign(detail, res.data);
let ua = uaparser(res.data.userAgent);
detail.browser = ua.browser.name;
detail.os = ua.os.name;
detail.device = ua.device.vendor ? ua.device.vendor + ua.device.model : '';
} catch (e) { } catch (e) {
smartSentry.captureError(e); smartSentry.captureError(e);
} finally { } finally {
@ -107,10 +121,11 @@
font-size: 20px; font-size: 20px;
font-weight: bold; font-weight: bold;
} }
.info-box { .info-box {
border-bottom: 1px solid #f0f0f0;
padding: 10px 8px; padding: 10px 8px;
} }
.detail-info { .detail-info {
.ant-col { .ant-col {
line-height: 1.46; line-height: 1.46;
@ -118,6 +133,7 @@
padding-right: 5px; padding-right: 5px;
} }
} }
.detail-right-title { .detail-right-title {
text-align: right; text-align: right;
color: grey; color: grey;

View File

@ -14,7 +14,7 @@
<a-input style="width: 150px" v-model:value="queryForm.keywords" placeholder="模块/操作内容" /> <a-input style="width: 150px" v-model:value="queryForm.keywords" placeholder="模块/操作内容" />
</a-form-item> </a-form-item>
<a-form-item label="请求关键字" class="smart-query-form-item"> <a-form-item label="请求关键字" class="smart-query-form-item">
<a-input style="width: 220px" v-model:value="queryForm.requestKeywords" placeholder="请求地址/请求方法/请求参数" /> <a-input style="width: 270px" v-model:value="queryForm.requestKeywords" placeholder="请求地址/请求方法/请求参数/返回结果" />
</a-form-item> </a-form-item>
<a-form-item label="用户名称" class="smart-query-form-item"> <a-form-item label="用户名称" class="smart-query-form-item">
<a-input style="width: 100px" v-model:value="queryForm.userName" placeholder="用户名称" /> <a-input style="width: 100px" v-model:value="queryForm.userName" placeholder="用户名称" />
@ -24,7 +24,7 @@
<a-range-picker @change="changeCreateDate" v-model:value="createDateRange" :presets="defaultChooseTimeRange" style="width: 240px" /> <a-range-picker @change="changeCreateDate" v-model:value="createDateRange" :presets="defaultChooseTimeRange" style="width: 240px" />
</a-form-item> </a-form-item>
<a-form-item label="快速筛选" class="smart-query-form-item"> <a-form-item label="状态:" class="smart-query-form-item">
<a-radio-group v-model:value="queryForm.successFlag" @change="onSearch"> <a-radio-group v-model:value="queryForm.successFlag" @change="onSearch">
<a-radio-button :value="undefined">全部</a-radio-button> <a-radio-button :value="undefined">全部</a-radio-button>
<a-radio-button :value="true">成功</a-radio-button> <a-radio-button :value="true">成功</a-radio-button>
@ -51,18 +51,23 @@
</a-row> </a-row>
</a-form> </a-form>
<a-card size="small" :bordered="false" :hoverable="true" style="height: 100%"> <a-card size="small" :bordered="false" :hoverable="true" >
<a-row justify="end"> <a-row justify="end">
<TableOperator class="smart-margin-bottom5" v-model="columns" :tableId="TABLE_ID_CONST.SUPPORT.CONFIG" :refresh="ajaxQuery" /> <TableOperator class="smart-margin-bottom5" v-model="columns" :tableId="TABLE_ID_CONST.SUPPORT.CONFIG" :refresh="ajaxQuery" />
</a-row> </a-row>
<a-table size="small" :loading="tableLoading" :dataSource="tableData" :columns="columns" bordered rowKey="operateLogId" :pagination="false"> <a-table size="small" :loading="tableLoading" :dataSource="tableData" :columns="columns" bordered rowKey="operateLogId" :pagination="false">
<template #bodyCell="{ text, record, column }"> <template #bodyCell="{ text, record, column }">
<template v-if="column.dataIndex === 'response'">
<a-typography-text v-if="text && text.ok">{{ text ? text.msg : '-' }}</a-typography-text>
<a-typography-text v-else type="warning">{{ text ? text.msg : '-' }}</a-typography-text>
</template>
<template v-if="column.dataIndex === 'successFlag'"> <template v-if="column.dataIndex === 'successFlag'">
<a-tag :color="text ? 'success' : 'error'">{{ text ? '成功' : '失败' }}</a-tag> <a-tag :color="text ? 'success' : 'error'">{{ text ? '成功' : '报错' }}</a-tag>
</template> </template>
<template v-if="column.dataIndex === 'userAgent'"> <template v-if="column.dataIndex === 'userAgent'">
<div>{{ record.browser }} / {{ record.os }} / {{ record.device }}</div> <div>{{ record.os }} / {{ record.browser }} {{ record.device ? '/' + record.device : record.device }}</div>
</template> </template>
<template v-if="column.dataIndex === 'operateUserType'"> <template v-if="column.dataIndex === 'operateUserType'">
@ -88,7 +93,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>
@ -135,14 +139,15 @@
ellipsis: true, ellipsis: true,
}, },
{ {
title: 'IP', title: '返回结果',
dataIndex: 'ip', dataIndex: 'response',
ellipsis: true, ellipsis: true,
}, },
{ {
title: 'IP地区', title: 'IP地区',
dataIndex: 'ipRegion', dataIndex: 'ipRegion',
ellipsis: true, ellipsis: true,
width: 150,
}, },
{ {
title: '客户端', title: '客户端',
@ -150,20 +155,15 @@
ellipsis: true, ellipsis: true,
}, },
{ {
title: '请求方法', title: '操作时间',
dataIndex: 'method',
ellipsis: true,
},
{
title: '请求结果',
dataIndex: 'successFlag',
width: 80,
},
{
title: '时间',
dataIndex: 'createTime', dataIndex: 'createTime',
width: 150, width: 150,
}, },
{
title: '状态',
dataIndex: 'successFlag',
width: 60,
},
{ {
title: '操作', title: '操作',
dataIndex: 'action', dataIndex: 'action',
@ -212,6 +212,10 @@
let responseModel = await operateLogApi.queryList(queryForm); let responseModel = await operateLogApi.queryList(queryForm);
for (const e of responseModel.data.list) { for (const e of responseModel.data.list) {
if(e.response){
e.response = JSON.parse(e.response);
}
if (!e.userAgent) { if (!e.userAgent) {
continue; continue;
} }

View File

@ -32,7 +32,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -234,12 +234,21 @@
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.center-container { .center-container {
height: 100%;
display: flex;
flex-direction: column;
.header-title { .header-title {
font-size: 20px; font-size: 20px;
flex-shrink: 0;
} }
.center-form-area { .center-form-area {
margin-top: 20px; margin-top: 20px;
flex: 1;
overflow-y: auto;
min-height: 0;
.avatar-container { .avatar-container {
position: relative; position: relative;

View File

@ -63,7 +63,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -75,7 +75,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -68,7 +68,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -94,7 +94,6 @@
v-model:pageSize="params.pageSize" v-model:pageSize="params.pageSize"
:total="total" :total="total"
@change="queryEmployee" @change="queryEmployee"
@showSizeChange="queryEmployee"
:show-total="showTableTotal" :show-total="showTableTotal"
/> />
</div> </div>

View File

@ -8,23 +8,24 @@
* @Copyright 1024创新实验室 https://1024lab.net Since 2012 * @Copyright 1024创新实验室 https://1024lab.net Since 2012
--> -->
<template> <template>
<a-modal :open="visible" width="600px" :bodyStyle="{height:'480px'}" title="" :closable="false" :maskClosable="true"> <a-modal :open="visible" width="600px" :bodyStyle="{height:'360px'}" title="" :closable="false" :maskClosable="true">
<a-row><div style="font-weight:bolder;margin: 0 auto;font-size: 16px">助力卓大抖音1000个粉丝开播写代码🎉🎉</div> </a-row> <a-row><div style="font-weight:bolder;margin: 0 auto;font-size: 16px;color: red;">重磅更新国产数据库支持🎉🎉</div> </a-row>
<a-row><div style="font-weight:bolder;margin: 20px auto;font-size: 15px">和1024创新实验室一起热爱代码热爱生活永远年轻永远前行🎉🎉</div> </a-row> <a-row><div style="font-weight:bolder;margin: 10px auto;font-size: 16px;color: red;">支持达梦人大金仓华为高斯GaussDB 🎉🎉</div> </a-row>
<br /> <br />
<div class="app-qr-box"> <div class="app-qr-box">
<div class="app-qr"> <div class="app-qr">
<a-image <a-image
:width="300" :width="200"
style="border-radius: 15px;" style="border-radius: 15px;"
src="https://img.smartadmin.1024lab.net/wechat/douyin.png" src="https://img.smartadmin.1024lab.net/wechat/zhuoda-wechat.jpg"
/> />
<span class="qr-desc strong"> 打开抖音APP-点击左上角侧边栏-点击扫一扫-进行关注</span> <span class="qr-desc strong"> 添加卓大微信备注对应数据库 达梦</span>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<a-button type="primary" @click="hide">知道了</a-button> <a-button type="default" @click="hide">知道了</a-button>
<a-button danger type="default" @click="goto" target="_blank" href="https://smartadmin.vip/views/other/china-db/">去看看国产数据库了解一下</a-button>
</template> </template>
</a-modal> </a-modal>
</template> </template>
@ -36,6 +37,9 @@ defineExpose({
}); });
const visible = ref(true); const visible = ref(true);
function goto(){
}
function show() { function show() {
visible.value = true; visible.value = true;
} }
@ -64,8 +68,7 @@ defineExpose({
display: flex; display: flex;
margin-top: 20px; margin-top: 20px;
align-items: center; align-items: center;
font-size: 13px; font-size: 15px;
color: red;
text-align: center; text-align: center;
overflow-x: hidden; overflow-x: hidden;
> img { > img {

View File

@ -108,7 +108,7 @@
let lunarMonth = lunar.getMonthInChinese(); let lunarMonth = lunar.getMonthInChinese();
let lunarDay = lunar.getDayInChinese(); let lunarDay = lunar.getDayInChinese();
// //
let jieqi = lunar.getPrevJieQi().getName(); let jieqi = lunar.getJieQi();
let next = lunar.getNextJieQi(); let next = lunar.getNextJieQi();
let nextJieqi = next.getName() + ' ' + next.getSolar().toYmd(); let nextJieqi = next.getName() + ' ' + next.getSolar().toYmd();

View File

@ -52,11 +52,11 @@
<a-col :span="24"> <a-col :span="24">
<OfficialAccountCard /> <OfficialAccountCard />
</a-col> </a-col>
<!--待办已办--> <!--更新日志-->
<a-col :span="24"> <a-col :span="24">
<ChangelogCard /> <ChangelogCard />
</a-col> </a-col>
<!--更新日志--> <!--待办已办-->
<a-col :span="24"> <a-col :span="24">
<ToBeDoneCard /> <ToBeDoneCard />
</a-col> </a-col>

View File

@ -86,7 +86,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryData" @change="queryData"
@showSizeChange="queryData"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -68,7 +68,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryRoleEmployee" @change="queryRoleEmployee"
@showSizeChange="queryRoleEmployee"
:show-total="showTableTotal" :show-total="showTableTotal"
/> />
</div> </div>

View File

@ -24,7 +24,7 @@
"axios": "1.6.8", "axios": "1.6.8",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"crypto-js": "4.1.1", "crypto-js": "4.1.1",
"dayjs": "1.10.5", "dayjs": "1.11.13",
"decimal.js": "10.3.1", "decimal.js": "10.3.1",
"default-passive-events": "^2.0.0", "default-passive-events": "^2.0.0",
"diff": "5.2.0", "diff": "5.2.0",
@ -44,7 +44,7 @@
"vue": "3.4.27", "vue": "3.4.27",
"vue-i18n": "9.13.1", "vue-i18n": "9.13.1",
"vue-router": "4.3.2", "vue-router": "4.3.2",
"vue3-json-viewer": "2.2.2" "vue3-json-viewer": "2.3.1"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "5.1.4", "@vitejs/plugin-vue": "5.1.4",

View File

@ -51,7 +51,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
<a-modal v-model:open="visibleDiff" width="90%" title="数据比对" :footer="null"> <a-modal v-model:open="visibleDiff" width="90%" title="数据比对" :footer="null">

View File

@ -71,7 +71,6 @@
v-model:pageSize="params.pageSize" v-model:pageSize="params.pageSize"
:total="total" :total="total"
@change="queryEmployee" @change="queryEmployee"
@showSizeChange="queryEmployee"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -27,7 +27,7 @@ export const appDefaultConfig = {
// 圆角 // 圆角
borderRadius: 6, borderRadius: 6,
// 菜单展开模式 // 菜单展开模式
flatPattern: false, menuSingleExpandFlag: true,
// 标签页 // 标签页
pageTagFlag: true, pageTagFlag: true,
// 标签页样式: default、 antd、chrome // 标签页样式: default、 antd、chrome

View File

@ -21,11 +21,11 @@ export default {
'setting.menu.layout': 'Menu Layout', 'setting.menu.layout': 'Menu Layout',
'setting.menu.width': 'Menu Width', 'setting.menu.width': 'Menu Width',
'setting.menu.theme': 'Menu Theme', 'setting.menu.theme': 'Menu Theme',
'setting.menu.expand': 'Menu Expand',
'setting.page.width': 'Page Width', 'setting.page.width': 'Page Width',
'setting.border.radius': 'Border Radius', 'setting.border.radius': 'Border Radius',
'setting.compact': 'Page Compact', 'setting.compact': 'Page Compact',
'setting.bread': 'Show Bread', 'setting.bread': 'Show Bread',
'setting.flatPattern': 'Flat Pattern',
'setting.pagetag': 'Show PageTag', 'setting.pagetag': 'Show PageTag',
'setting.pagetag.style': 'PageTag Style', 'setting.pagetag.style': 'PageTag Style',
'setting.footer': 'Show Footer', 'setting.footer': 'Show Footer',

View File

@ -21,11 +21,11 @@ export default {
'setting.menu.layout': '菜单布局', 'setting.menu.layout': '菜单布局',
'setting.menu.width': '菜单宽度', 'setting.menu.width': '菜单宽度',
'setting.menu.theme': '菜单主题', 'setting.menu.theme': '菜单主题',
'setting.menu.expand': '菜单展开',
'setting.compact': '页面紧凑', 'setting.compact': '页面紧凑',
'setting.border.radius': '页面圆角', 'setting.border.radius': '页面圆角',
'setting.page.width': '页面宽度', 'setting.page.width': '页面宽度',
'setting.bread': '面包屑', 'setting.bread': '面包屑',
'setting.flatPattern': '菜单展开模式',
'setting.pagetag': '标签页', 'setting.pagetag': '标签页',
'setting.pagetag.style': '标签页样式', 'setting.pagetag.style': '标签页样式',
'setting.footer': '页脚', 'setting.footer': '页脚',

View File

@ -83,8 +83,8 @@
<a-radio-button value="chrome">Chrome</a-radio-button> <a-radio-button value="chrome">Chrome</a-radio-button>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item :label="$t('setting.flatPattern')" v-if="formState.layout === LAYOUT_ENUM.SIDE.value"> <a-form-item :label="$t('setting.menu.expand')" v-if="formState.layout === LAYOUT_ENUM.SIDE.value">
<a-switch @change="changeFlatPattern" v-model:checked="formState.flatPattern" checked-children="多个" un-checked-children="" /> <a-switch @change="changeMenuExpandFlag" v-model:checked="formState.menuSingleExpandFlag" checked-children="单个" un-checked-children="" />
</a-form-item> </a-form-item>
<a-form-item :label="$t('setting.pagetag')"> <a-form-item :label="$t('setting.pagetag')">
<a-switch @change="changePageTagFlag" v-model:checked="formState.pageTagFlag" checked-children="显示" un-checked-children="隐藏" /> <a-switch @change="changePageTagFlag" v-model:checked="formState.pageTagFlag" checked-children="显示" un-checked-children="隐藏" />
@ -213,8 +213,8 @@
borderRadius: appConfigStore.borderRadius, borderRadius: appConfigStore.borderRadius,
// //
pageTagFlag: appConfigStore.pageTagFlag, pageTagFlag: appConfigStore.pageTagFlag,
// //
flatPattern: appConfigStore.flatPattern, menuSingleExpandFlag: appConfigStore.menuSingleExpandFlag,
// //
pageTagStyle: appConfigStore.pageTagStyle, pageTagStyle: appConfigStore.pageTagStyle,
// //
@ -303,9 +303,9 @@
}); });
} }
function changeFlatPattern(e) { function changeMenuExpandFlag(e) {
appConfigStore.$patch({ appConfigStore.$patch({
flatPattern: e, menuSingleExpandFlag: e,
}); });
} }

View File

@ -36,7 +36,7 @@
import { useUserStore } from '/@/store/modules/system/user'; import { useUserStore } from '/@/store/modules/system/user';
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme); const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
const flatPattern = computed(() => useAppConfigStore().$state.flatPattern); const menuSingleExpandFlag = computed(() => useAppConfigStore().$state.menuSingleExpandFlag);
const props = defineProps({ const props = defineProps({
collapsed: { collapsed: {
@ -46,8 +46,7 @@
}); });
const menuTree = computed(() => useUserStore().getMenuTree || []); const menuTree = computed(() => useUserStore().getMenuTree || []);
const rootSubmenuKeys = computed(()=>menuTree.value.map(item=>item.menuId)); const rootSubmenuKeys = computed(() => menuTree.value.map((item) => item.menuId));
// //
let currentRoute = useRoute(); let currentRoute = useRoute();
@ -76,9 +75,15 @@
let parentList = menuParentIdListMap.get(currentRoute.name) || []; let parentList = menuParentIdListMap.get(currentRoute.name) || [];
// openkey // openkey
if (!props.collapsed) { if (props.collapsed) {
return;
}
let needOpenKeys = _.map(parentList, 'name').map(Number);
if (menuSingleExpandFlag.value) {
openKeys.value = [...needOpenKeys];
} else {
// 使lodashunion // 使lodashunion
let needOpenKeys = _.map(parentList, 'name').map(Number);
openKeys.value = _.union(openKeys.value, needOpenKeys); openKeys.value = _.union(openKeys.value, needOpenKeys);
} }
} }
@ -92,17 +97,18 @@
immediate: true, immediate: true,
} }
); );
function onOpenChange(openKeysParams){
if(flatPattern.value){ function onOpenChange(openKeysParams) {
return; if (!menuSingleExpandFlag.value) {
return;
}
const latestOpenKey = openKeysParams.find((key) => openKeys.value.indexOf(key) === -1);
if (rootSubmenuKeys.value.indexOf(latestOpenKey) === -1) {
openKeys.value = openKeysParams;
} else {
openKeys.value = latestOpenKey ? [latestOpenKey] : [];
}
} }
const latestOpenKey = openKeysParams.find(key => openKeys.value.indexOf(key) === -1);
if (rootSubmenuKeys.value.indexOf(latestOpenKey) === -1) {
openKeys.value = openKeysParams;
} else {
openKeys.value = latestOpenKey ? [latestOpenKey] : [];
}
};
defineExpose({ defineExpose({
updateOpenKeysAndSelectKeys, updateOpenKeysAndSelectKeys,
}); });

View File

@ -8,6 +8,7 @@
* @Copyright 1024 https://1024lab.net Since 2012 * @Copyright 1024 https://1024lab.net Since 2012
*/ */
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { smartSentry } from '/@/lib/smart-sentry.js';
export const useSpinStore = defineStore({ export const useSpinStore = defineStore({
id: 'spin', id: 'spin',
@ -18,13 +19,27 @@ export const useSpinStore = defineStore({
actions: { actions: {
hide() { hide() {
this.loading = false; this.loading = false;
let spins = document.querySelector('.ant-spin-nested-loading'); // 安全的DOM操作避免null引用错误
spins.style.zIndex = 999; try {
const spins = document.querySelector('.ant-spin-nested-loading');
if (spins) {
spins.style.zIndex = '999';
}
} catch (error) {
smartSentry.captureError('Spin hide操作失败:', error);
}
}, },
show() { show() {
this.loading = true; this.loading = true;
let spins = document.querySelector('.ant-spin-nested-loading'); // 安全的DOM操作避免null引用错误
spins.style.zIndex = 1001; try {
const spins = document.querySelector('.ant-spin-nested-loading');
if (spins) {
spins.style.zIndex = '1001';
}
} catch (error) {
smartSentry.captureError('Spin hide操作失败:', error);
}
}, },
}, },
}); });

View File

@ -149,7 +149,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryData" @change="queryData"
@showSizeChange="queryData"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -78,7 +78,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -63,7 +63,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="queryEmployee" @change="queryEmployee"
@showSizeChange="queryEmployee"
:show-total="showTableTotal" :show-total="showTableTotal"
/> />
</div> </div>

View File

@ -75,7 +75,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

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