From ec306f0a986f9e2b22bfba38f4f5bec30226d63c Mon Sep 17 00:00:00 2001 From: wanghu Date: Fri, 21 Aug 2026 00:49:38 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=90=E7=BB=B4=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlscript/sys_operation_log.sql | 3 + .../bschclassmanage/aspect/SyncLogAspect.java | 8 +- .../aspect/SysOperationLogAspect.java | 8 +- .../controller/api/LoginUserController.java | 17 + .../controller/api/SyncLogController.java | 2 +- .../api/SysOperationLogController.java | 2 +- .../bschclassmanage/entity/SyncLog.java | 12 + .../entity/SysOperationLog.java | 12 + .../service/impl/SyncLogServiceImpl.java | 23 +- .../impl/SysOperationLogServiceImpl.java | 69 ++- src/main/resources/static/work-view.html | 498 ++++++++++++------ 11 files changed, 464 insertions(+), 190 deletions(-) diff --git a/sqlscript/sys_operation_log.sql b/sqlscript/sys_operation_log.sql index 452271d..7a01f36 100644 --- a/sqlscript/sys_operation_log.sql +++ b/sqlscript/sys_operation_log.sql @@ -16,6 +16,9 @@ CREATE TABLE `sys_operation_log` ( `cost_time` BIGINT NOT NULL COMMENT '耗时ms', `user_agent` VARCHAR(512) DEFAULT NULL, `error_msg` TEXT COMMENT '异常信息(截断2000)', + `retry_count` INT DEFAULT 0 COMMENT '重试次数', + `last_retry_status` TINYINT DEFAULT NULL COMMENT '上次重试状态: 1 成功 / 0 失败', + `last_retry_time` DATETIME(3) DEFAULT NULL COMMENT '上次重试时间', `create_time` DATETIME(3) NOT NULL COMMENT '记录时间', PRIMARY KEY (`id`), KEY `idx_sol_create_time` (`create_time`), diff --git a/src/main/java/com/yunhai/bschclassmanage/aspect/SyncLogAspect.java b/src/main/java/com/yunhai/bschclassmanage/aspect/SyncLogAspect.java index 492498a..3902d91 100644 --- a/src/main/java/com/yunhai/bschclassmanage/aspect/SyncLogAspect.java +++ b/src/main/java/com/yunhai/bschclassmanage/aspect/SyncLogAspect.java @@ -7,6 +7,7 @@ import com.yunhai.bschclassmanage.enums.SyncDataType; import com.yunhai.bschclassmanage.service.ISyncLogService; import com.yunhai.bschclassmanage.utils.OtherUtil; import com.yunhai.bschclassmanage.utils.SensitiveDataUtil; +import com.yunhai.bschclassmanage.utils.SyncLogRetryContext; import com.yunhai.bschclassmanage.utils.UserContext; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; @@ -47,7 +48,12 @@ public class SyncLogAspect { SyncLog entity = new SyncLog(); // 枚举类型 key/name entity.setSyncDataTypeKey(dataType.getKey()); - entity.setSyncDataTypeName(dataType.getName()); + String typeName = dataType.getName(); + if (SyncLogRetryContext.isRetry()) { + // 重试重放时,在同步类型名上追加"(重试)"标记,便于与首次调用区分 + typeName = (typeName == null ? "" : typeName) + "(重试)"; + } + entity.setSyncDataTypeName(typeName); // 大类:Send / Sync(由 key 前缀推导) boolean isSend = dataType.getKey() != null && dataType.getKey().startsWith("Send"); entity.setSyncType(isSend ? "Send" : "Sync"); diff --git a/src/main/java/com/yunhai/bschclassmanage/aspect/SysOperationLogAspect.java b/src/main/java/com/yunhai/bschclassmanage/aspect/SysOperationLogAspect.java index 82e8713..f793628 100644 --- a/src/main/java/com/yunhai/bschclassmanage/aspect/SysOperationLogAspect.java +++ b/src/main/java/com/yunhai/bschclassmanage/aspect/SysOperationLogAspect.java @@ -7,6 +7,7 @@ import com.yunhai.bschclassmanage.utils.GzipUtil; import com.yunhai.bschclassmanage.utils.OperationLogResolver; import com.yunhai.bschclassmanage.utils.OtherUtil; import com.yunhai.bschclassmanage.utils.SensitiveDataUtil; +import com.yunhai.bschclassmanage.utils.SyncLogRetryContext; import com.yunhai.bschclassmanage.utils.UserContext; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -98,7 +99,12 @@ public class SysOperationLogAspect { e.setRequestMethod(httpMethod); e.setClassMethod(point.getTarget().getClass().getName() + "." + methodName); e.setLogLevel(OperationLogResolver.resolveLevel(httpMethod, uri, methodName).getCode()); - e.setOperationType(OperationLogResolver.resolveOperationType(httpMethod, uri, methodName)); + String opType = OperationLogResolver.resolveOperationType(httpMethod, uri, methodName); + if (SyncLogRetryContext.isRetry()) { + // 重试重放时,在操作类型上追加"(重试)"标记,便于与首次调用区分 + opType = (opType == null ? "" : opType) + "(重试)"; + } + e.setOperationType(opType); if (request != null) { e.setUserAgent(request.getHeader("User-Agent")); e.setRequestParams(SensitiveDataUtil.mask(buildRequestParams(point, request))); diff --git a/src/main/java/com/yunhai/bschclassmanage/controller/api/LoginUserController.java b/src/main/java/com/yunhai/bschclassmanage/controller/api/LoginUserController.java index 9eb7c66..de7e761 100644 --- a/src/main/java/com/yunhai/bschclassmanage/controller/api/LoginUserController.java +++ b/src/main/java/com/yunhai/bschclassmanage/controller/api/LoginUserController.java @@ -13,7 +13,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import java.io.IOException; import java.util.Enumeration; import java.util.List; import java.util.Optional; @@ -37,6 +39,21 @@ public class LoginUserController { return authUserWithDept; } + /** + * 判断是否登录:已登录则重定向到运维工作台 /work-view.html(兼容 context-path); + * 未登录返回 401(通常 CAS 拦截器会先行 302 到 CAS 登录页)。 + */ + @GetMapping("checkLogin") + public void checkLogin(BoaUser authUserWithDept, HttpServletRequest request, HttpServletResponse response) throws IOException { + if (authUserWithDept != null) { + response.sendRedirect(request.getContextPath() + "/work-view.html"); + } else { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); + } + } + @GetMapping("Logout") protected void logout(HttpServletRequest request) { HttpSession session = request.getSession(); diff --git a/src/main/java/com/yunhai/bschclassmanage/controller/api/SyncLogController.java b/src/main/java/com/yunhai/bschclassmanage/controller/api/SyncLogController.java index a7b1a9f..17f958c 100644 --- a/src/main/java/com/yunhai/bschclassmanage/controller/api/SyncLogController.java +++ b/src/main/java/com/yunhai/bschclassmanage/controller/api/SyncLogController.java @@ -79,7 +79,7 @@ public class SyncLogController extends BaseController { Object result = syncLogService.retry(id); Map m = new LinkedHashMap<>(); m.put("success", true); - m.put("message", "重试已提交,执行结果见新增的同步日志"); + m.put("message", "重试成功"); if (result != null) m.put("data", result); return m; } diff --git a/src/main/java/com/yunhai/bschclassmanage/controller/api/SysOperationLogController.java b/src/main/java/com/yunhai/bschclassmanage/controller/api/SysOperationLogController.java index 41d0805..8135eca 100644 --- a/src/main/java/com/yunhai/bschclassmanage/controller/api/SysOperationLogController.java +++ b/src/main/java/com/yunhai/bschclassmanage/controller/api/SysOperationLogController.java @@ -80,7 +80,7 @@ public class SysOperationLogController extends BaseController { Object result = sysOperationLogService.retry(id); Map m = new LinkedHashMap<>(); m.put("success", true); - m.put("message", "重试已提交,执行结果见新增的审计日志"); + m.put("message", "重试成功"); if (result != null) m.put("data", result); return m; } diff --git a/src/main/java/com/yunhai/bschclassmanage/entity/SyncLog.java b/src/main/java/com/yunhai/bschclassmanage/entity/SyncLog.java index 0ae729d..f4d630e 100644 --- a/src/main/java/com/yunhai/bschclassmanage/entity/SyncLog.java +++ b/src/main/java/com/yunhai/bschclassmanage/entity/SyncLog.java @@ -74,6 +74,18 @@ public class SyncLog implements Serializable { @Column(name = "status") private Integer status; + /** 重试次数 */ + @Column(name = "retry_count", columnDefinition = "INT DEFAULT 0") + private Integer retryCount; + + /** 上次重试状态:1 成功 / 0 失败 */ + @Column(name = "last_retry_status") + private Integer lastRetryStatus; + + /** 上次重试时间 */ + @Column(name = "last_retry_time", columnDefinition = "DATETIME(3)") + private LocalDateTime lastRetryTime; + @CreationTimestamp @Column(name = "create_time", nullable = false, updatable = false, columnDefinition = "DATETIME(3)") private LocalDateTime createTime; diff --git a/src/main/java/com/yunhai/bschclassmanage/entity/SysOperationLog.java b/src/main/java/com/yunhai/bschclassmanage/entity/SysOperationLog.java index 8179e14..8882d9a 100644 --- a/src/main/java/com/yunhai/bschclassmanage/entity/SysOperationLog.java +++ b/src/main/java/com/yunhai/bschclassmanage/entity/SysOperationLog.java @@ -71,6 +71,18 @@ public class SysOperationLog implements Serializable { @Column(name = "error_msg", columnDefinition = "TEXT") private String errorMsg; // 异常信息,截断 2000 + /** 重试次数 */ + @Column(name = "retry_count", columnDefinition = "INT DEFAULT 0") + private Integer retryCount; + + /** 上次重试状态:1 成功 / 0 失败 */ + @Column(name = "last_retry_status") + private Integer lastRetryStatus; + + /** 上次重试时间 */ + @Column(name = "last_retry_time", columnDefinition = "DATETIME(3)") + private LocalDateTime lastRetryTime; + @CreationTimestamp @Column(name = "create_time", nullable = false, updatable = false, columnDefinition = "DATETIME(3)") private LocalDateTime createTime; diff --git a/src/main/java/com/yunhai/bschclassmanage/service/impl/SyncLogServiceImpl.java b/src/main/java/com/yunhai/bschclassmanage/service/impl/SyncLogServiceImpl.java index 369501d..0f63894 100644 --- a/src/main/java/com/yunhai/bschclassmanage/service/impl/SyncLogServiceImpl.java +++ b/src/main/java/com/yunhai/bschclassmanage/service/impl/SyncLogServiceImpl.java @@ -6,6 +6,7 @@ import com.yunhai.bschclassmanage.entity.SyncLog; import com.yunhai.bschclassmanage.model.vo.PageVO; import com.yunhai.bschclassmanage.service.ISyncLogService; import com.yunhai.bschclassmanage.utils.ReflectRetryUtil; +import com.yunhai.bschclassmanage.utils.SyncLogRetryContext; import com.yunhai.platform.utils.exception.DException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -23,6 +24,7 @@ import javax.persistence.criteria.Predicate; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -104,6 +106,10 @@ public class SyncLogServiceImpl implements ISyncLogService { if (StringUtils.isBlank(e.getClassMethod())) throw new DException("该日志缺少调用信息(classMethod),无法重试"); if (StringUtils.isBlank(e.getJsonData())) throw new DException("该日志缺少入参(jsonData),无法重试"); + // 记录本次重试元数据(重试次数 +1、重试时间) + e.setRetryCount((e.getRetryCount() == null ? 0 : e.getRetryCount()) + 1); + e.setLastRetryTime(LocalDateTime.now()); + try { @SuppressWarnings("unchecked") Map argMap = JSON.parseObject(e.getJsonData(), LinkedHashMap.class); @@ -133,11 +139,26 @@ public class SyncLogServiceImpl implements ISyncLogService { } Method invokeMethod = ReflectRetryUtil.getInvokeMethod(resolved.bean, target); - return invokeMethod.invoke(resolved.bean, args); + SyncLogRetryContext.mark(); // 标记为重试,切面生成新日志时在同步类型名追加"(重试)" + Object result = invokeMethod.invoke(resolved.bean, args); + e.setLastRetryStatus(1); // 本次重试成功 + return result; } catch (DException de) { + e.setLastRetryStatus(0); // 本次重试失败 throw de; } catch (InvocationTargetException | IllegalAccessException ex) { + e.setLastRetryStatus(0); // 本次重试失败 throw ReflectRetryUtil.rethrow(ex); + } catch (RuntimeException re) { + e.setLastRetryStatus(0); // 本次重试失败(入参转换 / 定位等) + throw re; + } finally { + SyncLogRetryContext.clear(); // 清除重试标记,避免线程池复用泄漏 + try { + repository.save(e); // 同步落库重试元数据(次数 / 上次状态 / 时间) + } catch (Exception saveEx) { + log.error("重试元数据落库失败, id={}", e.getId(), saveEx); + } } } } diff --git a/src/main/java/com/yunhai/bschclassmanage/service/impl/SysOperationLogServiceImpl.java b/src/main/java/com/yunhai/bschclassmanage/service/impl/SysOperationLogServiceImpl.java index 22caefe..ad607e3 100644 --- a/src/main/java/com/yunhai/bschclassmanage/service/impl/SysOperationLogServiceImpl.java +++ b/src/main/java/com/yunhai/bschclassmanage/service/impl/SysOperationLogServiceImpl.java @@ -7,6 +7,7 @@ import com.yunhai.bschclassmanage.model.vo.PageVO; import com.yunhai.bschclassmanage.service.ISysOperationLogService; import com.yunhai.bschclassmanage.utils.GzipUtil; import com.yunhai.bschclassmanage.utils.ReflectRetryUtil; +import com.yunhai.bschclassmanage.utils.SyncLogRetryContext; import com.yunhai.bschclassmanage.utils.UserContext; import com.yunhai.platform.utils.exception.DException; import lombok.extern.slf4j.Slf4j; @@ -139,43 +140,63 @@ public class SysOperationLogServiceImpl implements ISysOperationLogService { throw new DException("非 JSON 数组入参(如 GET query)无法可靠重放,请手动重试"); } - JSONArray arr; - try { - arr = JSONArray.parseArray(e.getRequestParams()); - } catch (Exception ex) { - throw new DException("入参解析失败(requestParams),无法重试"); - } - if (arr == null) throw new DException("入参解析失败(requestParams),无法重试"); - - ReflectRetryUtil.Resolved resolved = ReflectRetryUtil.resolveBeanAndMethod( - applicationContext, e.getClassMethod(), arr.size()); - - Method target = resolved.method; - Parameter[] params = target.getParameters(); - Object[] args = new Object[params.length]; - int jsonIdx = 0; - for (int i = 0; i < params.length; i++) { - if (ReflectRetryUtil.isSkippable(params[i].getType())) { - args[i] = null; // 容器注入参数,无法重建 - } else { - Object raw = (jsonIdx < arr.size()) ? arr.get(jsonIdx++) : null; - args[i] = ReflectRetryUtil.convert(raw, params[i]); - } - } + // 记录本次重试元数据(重试次数 +1、重试时间) + e.setRetryCount((e.getRetryCount() == null ? 0 : e.getRetryCount()) + 1); + e.setLastRetryTime(LocalDateTime.now()); // 还原原操作人上下文,使重放触发的审计切面记录正确操作人;结束后恢复本次请求上下文 UserContext.UserInfo snapshot = UserContext.getCurrentUser(); try { + JSONArray arr; + try { + arr = JSONArray.parseArray(e.getRequestParams()); + } catch (Exception ex) { + throw new DException("入参解析失败(requestParams),无法重试"); + } + if (arr == null) throw new DException("入参解析失败(requestParams),无法重试"); + + ReflectRetryUtil.Resolved resolved = ReflectRetryUtil.resolveBeanAndMethod( + applicationContext, e.getClassMethod(), arr.size()); + + Method target = resolved.method; + Parameter[] params = target.getParameters(); + Object[] args = new Object[params.length]; + int jsonIdx = 0; + for (int i = 0; i < params.length; i++) { + if (ReflectRetryUtil.isSkippable(params[i].getType())) { + args[i] = null; // 容器注入参数,无法重建 + } else { + Object raw = (jsonIdx < arr.size()) ? arr.get(jsonIdx++) : null; + args[i] = ReflectRetryUtil.convert(raw, params[i]); + } + } + UserContext.setUserId(e.getUserId()); UserContext.setUserName(e.getUserName()); UserContext.setSchId(e.getSchId()); UserContext.setIp(e.getRequestIp()); Method invokeMethod = ReflectRetryUtil.getInvokeMethod(resolved.bean, target); - return invokeMethod.invoke(resolved.bean, args); + SyncLogRetryContext.mark(); // 标记为重试,审计切面生成新日志时在操作类型追加"(重试)" + Object result = invokeMethod.invoke(resolved.bean, args); + e.setLastRetryStatus(1); // 本次重试成功 + return result; + } catch (DException de) { + e.setLastRetryStatus(0); // 本次重试失败 + throw de; } catch (InvocationTargetException | IllegalAccessException ex) { + e.setLastRetryStatus(0); // 本次重试失败 throw ReflectRetryUtil.rethrow(ex); + } catch (RuntimeException re) { + e.setLastRetryStatus(0); // 本次重试失败(入参转换 / 定位等) + throw re; } finally { UserContext.setUserInfo(snapshot); + SyncLogRetryContext.clear(); // 清除重试标记,避免线程池复用泄漏 + try { + repository.save(e); // 同步落库重试元数据(次数 / 上次状态 / 时间) + } catch (Exception saveEx) { + log.error("审计日志重试元数据落库失败, id={}", e.getId(), saveEx); + } } } } diff --git a/src/main/resources/static/work-view.html b/src/main/resources/static/work-view.html index 4e31bbe..3a84e35 100644 --- a/src/main/resources/static/work-view.html +++ b/src/main/resources/static/work-view.html @@ -8,15 +8,17 @@ :root { --bg: #f5f7fa; --panel: #ffffff; - --panel-2: #eef2f7; - --border: #d8dee6; - --text: #1f2933; - --muted: #6b7785; - --accent: #2f6fd1; - --accent-2: #1f5fbf; - --green: #1a9e54; - --red: #d64545; - --yellow: #b8860b; + --panel-2: #f8f8f9; + --border: #e8eaec; + --border-in: #dcdee2; + --text: #515a6e; + --muted: #808695; + --accent: #2d8cf0; + --accent-hover: #57a3f3; + --accent-2: #2487e8; + --green: #19be6b; + --red: #ed4014; + --yellow: #ff9900; --mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; } * { box-sizing: border-box; } @@ -27,29 +29,30 @@ color: var(--text); font-size: 14px; } - header { - padding: 16px 24px; - border-bottom: 1px solid var(--border); - background: linear-gradient(180deg, #eef2f7, #f5f7fa); + /* ===== Breadcrumb(View UI Breadcrumb 风格) ===== */ + .breadcrumb { + padding: 14px 24px; background: #fff; + border-bottom: 1px solid #e8eaec; + font-size: 14px; color: #515a6e; } - header .sub .pt { color: var(--text); font-weight: 600; } - header .sub { margin-top: 4px; color: var(--muted); font-size: 12px; } + .breadcrumb a { color: #515a6e; text-decoration: none; transition: color .2s; } + .breadcrumb a:hover { color: #2d8cf0; } + .breadcrumb .sep { margin: 0 8px; color: #dcdee2; } + .breadcrumb .current { color: #515a6e; } - /* ===== Tabs ===== */ + /* ===== Tabs(View UI line 型:底部下划线激活) ===== */ .tabs { - display: flex; gap: 4px; padding: 10px 24px 0; - border-bottom: 1px solid var(--border); background: #e9eef4; + display: flex; gap: 24px; padding: 0 24px; + border-bottom: 1px solid var(--border-in); background: #fff; } .tabs .tab { - padding: 9px 20px; cursor: pointer; font-size: 14px; color: var(--muted); - border: 1px solid transparent; border-bottom: none; - border-radius: 8px 8px 0 0; user-select: none; + padding: 11px 2px; cursor: pointer; font-size: 14px; color: var(--muted); + border-bottom: 2px solid transparent; user-select: none; transition: color .2s; } - .tabs .tab:hover { color: var(--text); } + .tabs .tab:hover { color: var(--accent); } .tabs .tab.active { - color: var(--accent); background: var(--panel); - border-color: var(--border); position: relative; top: 1px; - border-bottom: 1px solid var(--panel); + color: var(--accent); font-weight: 500; + border-bottom-color: var(--accent); } .panel { display: none; padding: 16px 24px; } .panel.active { display: block; } @@ -59,9 +62,9 @@ .left { width: 320px; flex: 0 0 320px; } .right { flex: 1 1 auto; min-width: 0; } .card { - background: var(--panel); - border: 1px solid var(--border); - border-radius: 10px; + background: #fff; + border-radius: 4px; + box-shadow: 0 1px 1px rgba(0,0,0,.1); /* View UI Card */ padding: 16px; } .card + .card { margin-top: 16px; } @@ -71,29 +74,32 @@ input[type=text], input[type=number] { width: 100%; padding: 9px 11px; - background: var(--panel-2); - border: 1px solid var(--border); - border-radius: 8px; + background: #fff; + border: 1px solid #dcdee2; + border-radius: 4px; color: var(--text); font-family: var(--mono); outline: none; + transition: border-color .2s, box-shadow .2s; } - input[type=text]:focus, input[type=number]:focus { border-color: var(--accent); } + input[type=text]:hover, input[type=number]:hover { border-color: #57a3f3; } + input[type=text]:focus, input[type=number]:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(45,140,240,.2); outline: none; } .btn { - padding: 9px 14px; - border: 1px solid var(--border); - background: var(--panel-2); + padding: 5px 15px; + border: 1px solid var(--border-in); + background: #fff; color: var(--text); - border-radius: 8px; + border-radius: 4px; cursor: pointer; font-size: 13px; + transition: border-color .2s, color .2s, background .2s; } - .btn:hover { border-color: var(--accent); } - .btn.primary { background: var(--accent-2); border-color: var(--accent); color: #fff; } - .btn.primary:hover { background: var(--accent); } - .btn.danger { background: transparent; border-color: var(--red); color: var(--red); } - .btn.danger:hover { background: rgba(255,107,107,.12); } - .btn.mini { padding: 3px 10px; font-size: 12px; border-radius: 6px; } + .btn:hover { border-color: var(--accent); color: var(--accent); } + .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; } + .btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: #fff; } + .btn.danger { background: #fff; border-color: var(--red); color: var(--red); } + .btn.danger:hover { background: rgba(237,64,20,.08); border-color: var(--red); color: var(--red); } + .btn.mini { padding: 2px 10px; font-size: 12px; border-radius: 4px; } .btn-row { display: flex; gap: 8px; flex-wrap: wrap; } .hint { color: var(--muted); font-size: 11px; margin-top: 6px; } @@ -109,8 +115,8 @@ overflow: hidden; text-overflow: ellipsis; } - .keys-list .k:hover { background: var(--panel-2); } - .keys-list .k.active { background: rgba(76,154,255,.15); border-left: 3px solid var(--accent); padding-left: 7px; } + .keys-list .k:hover { background: #f3f3f3; } + .keys-list .k.active { background: #f0faff; } .keys-list .k .kname { display: block; color: var(--accent); font-size: 11px; margin-bottom: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -123,28 +129,30 @@ .meta .mk { color: var(--muted); font-size: 12px; } .meta .mv { font-family: var(--mono); word-break: break-all; } .badge { - display: inline-block; padding: 2px 8px; border-radius: 999px; - font-size: 11px; border: 1px solid var(--border); color: var(--muted); + display: inline-block; padding: 0 8px; height: 22px; line-height: 20px; + border-radius: 4px; font-size: 12px; border: 1px solid #e8eaec; color: var(--muted); background: #fff; } - .badge.string { color: var(--green); border-color: rgba(54,194,117,.4); } - .badge.hash { color: var(--accent); border-color: rgba(76,154,255,.4); } - .badge.list, .badge.set, .badge.zset { color: var(--yellow); border-color: rgba(247,201,72,.4); } + .badge.string { color: var(--green); border-color: rgba(25,190,107,.4); background: rgba(25,190,107,.1); } + .badge.hash { color: var(--accent); border-color: rgba(45,140,240,.4); background: rgba(45,140,240,.1); } + .badge.list, .badge.set, .badge.zset { color: var(--yellow); border-color: rgba(255,153,0,.4); background: rgba(255,153,0,.1); } .toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; } .count { color: var(--muted); font-size: 12px; } select { padding: 7px 10px; - background: var(--panel-2); - border: 1px solid var(--border); - border-radius: 8px; + background: #fff; + border: 1px solid #dcdee2; + border-radius: 4px; color: var(--text); font-size: 13px; outline: none; cursor: pointer; + transition: border-color .2s, box-shadow .2s; } - select:focus { border-color: var(--accent); } + select:hover { border-color: #57a3f3; } + select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(45,140,240,.2); } pre.json { - margin: 0; padding: 12px; background: #f0f3f7; border: 1px solid var(--border); - border-radius: 8px; overflow: auto; max-height: 60vh; font-family: var(--mono); + margin: 0; padding: 12px; background: #f8f8f9; border: 1px solid #e8eaec; + border-radius: 4px; overflow: auto; max-height: 60vh; font-family: var(--mono); font-size: 12px; line-height: 1.5; color: #1f2933; white-space: pre; } pre.json .jk { color: #c0392b; } /* key */ @@ -161,65 +169,135 @@ code { background: var(--panel-2); padding: 1px 5px; border-radius: 4px; font-family: var(--mono); } /* ===== 日志表格 ===== */ - .filter-bar { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; margin-bottom: 14px; } - .filter-bar .fi { display: flex; flex-direction: column; gap: 4px; } - .filter-bar .fi label { margin-bottom: 0; } - .filter-bar .fi input[type=text], .filter-bar .fi input[type=number] { width: 170px; padding: 7px 10px; font-size: 13px; } - .filter-bar .fi select { min-width: 110px; } + /* 筛选工具栏(参考页面风格:白底圆角卡、一行内联 + 横向滚动) */ + .filter-bar { display: flex; gap: 8px; flex-wrap: nowrap; align-items: center; background: var(--panel); border: 1px solid #e8eaec; border-radius: 4px; padding: 8px 12px; margin-bottom: 14px; overflow-x: auto; } + .filter-bar .fi { display: flex; align-items: center; gap: 5px; flex-shrink: 0; } + .filter-bar .fi label { margin: 0; color: #515a6e; font-size: 12px; white-space: nowrap; } + .filter-bar .fi input[type=text], .filter-bar .fi input[type=number] { width: 105px; padding: 5px 8px; font-size: 12px; border: 1px solid #dcdee2; border-radius: 4px; background: #fff; color: var(--text); outline: none; transition: border-color .2s, box-shadow .2s; } + .filter-bar .fi input[type=text]:hover, .filter-bar .fi input[type=number]:hover { border-color: #57a3f3; } + .filter-bar .fi input[type=text]:focus, .filter-bar .fi input[type=number]:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(45,140,240,.2); } + .filter-bar .fi select { min-width: 80px; padding: 5px 8px; border: 1px solid #dcdee2; border-radius: 4px; background: #fff; font-size: 12px; color: var(--text); transition: border-color .2s, box-shadow .2s; } + .filter-bar .fi select:hover { border-color: #57a3f3; } + .filter-bar .fi select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(45,140,240,.2); } + /* 搜索框左侧放大镜图标 */ + .filter-bar .fi.search { position: relative; } + .filter-bar .fi.search input[type=text] { padding-left: 26px; } + .filter-bar .fi.search::before { + content: "\1F50D"; /* 🔍 */ + position: absolute; left: 9px; top: 50%; transform: translateY(-50%); + color: var(--muted); font-size: 13px; pointer-events: none; + } + .filter-bar > .btn.primary { margin-left: auto; flex-shrink: 0; } /* 查询按钮置右 */ table.log { width: 100%; border-collapse: collapse; font-size: 12.5px; } table.log th, table.log td { - padding: 8px 10px; border-bottom: 1px solid var(--border); + padding: 9px 12px; border-bottom: 1px solid #e8eaec; text-align: left; vertical-align: top; } - table.log th { color: var(--muted); font-weight: 500; white-space: nowrap; background: var(--panel-2); position: sticky; top: 0; } - table.log tr:hover td { background: rgba(76,154,255,.05); } + table.log th { color: #515a6e; font-weight: 600; white-space: nowrap; background: #f8f8f9; position: sticky; top: 0; } + table.log td { color: #515a6e; } + table.log tr:hover td { background: #ebf7ff; } table.log td.mono { font-family: var(--mono); font-size: 12px; } table.log td .cell-clip { display: inline-block; max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; } - .table-wrap { overflow: auto; max-height: 62vh; border: 1px solid var(--border); border-radius: 8px; } - .tag { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 11px; border: 1px solid; white-space: nowrap; } - .tag.green { color: var(--green); border-color: rgba(54,194,117,.4); } - .tag.red { color: var(--red); border-color: rgba(255,107,107,.4); } - .tag.yellow { color: var(--yellow); border-color: rgba(247,201,72,.4); } - .tag.blue { color: var(--accent); border-color: rgba(76,154,255,.4); } - .tag.gray { color: var(--muted); border-color: var(--border); } - .pager { display: flex; gap: 10px; align-items: center; margin-top: 12px; flex-wrap: wrap; } - .pager .pinfo { color: var(--muted); font-size: 12px; } - .row-actions { display: flex; gap: 6px; white-space: nowrap; } + .table-wrap { overflow: auto; max-height: 62vh; border: 1px solid #e8eaec; border-radius: 4px; } + .tag { display: inline-block; padding: 0 8px; height: 22px; line-height: 20px; border-radius: 4px; font-size: 12px; border: 1px solid; white-space: nowrap; } + .tag.green { color: var(--green); border-color: rgba(25,190,107,.4); background: rgba(25,190,107,.1); } + .tag.red { color: var(--red); border-color: rgba(237,64,20,.4); background: rgba(237,64,20,.1); } + .tag.yellow { color: var(--yellow); border-color: rgba(255,153,0,.4); background: rgba(255,153,0,.1); } + .tag.blue { color: var(--accent); border-color: rgba(45,140,240,.4); background: rgba(45,140,240,.1); } + .tag.gray { color: var(--muted); border-color: #e8eaec; background: #f8f8f9; } + /* 分页条:View UI 风格页码按钮,靠右 */ + .pager { display: flex; gap: 8px; align-items: center; justify-content: flex-end; margin-top: 14px; flex-wrap: wrap; color: #808695; font-size: 13px; } + .pager .pinfo { color: #808695; margin-right: 4px; } + .pager .pg { display: inline-flex; gap: 4px; } + .pager .pg-btn { min-width: 32px; height: 32px; padding: 0 6px; border: 1px solid #dcdee2; border-radius: 4px; background: #fff; color: #515a6e; cursor: pointer; font-size: 13px; text-align: center; line-height: 30px; transition: border-color .2s, color .2s; } + .pager .pg-btn:hover:not(:disabled) { border-color: #2d8cf0; color: #2d8cf0; } + .pager .pg-btn.active { border-color: #2d8cf0; color: #2d8cf0; font-weight: 600; } + .pager .pg-btn:disabled { opacity: .5; cursor: not-allowed; } + .pager .pg-ellipsis { line-height: 32px; padding: 0 2px; color: #c5c8ce; } + .pager .size { display: inline-flex; align-items: center; gap: 4px; } + .pager .size select { padding: 5px 8px; border: 1px solid #dcdee2; border-radius: 4px; background: #fff; font-size: 13px; color: #515a6e; } + .row-actions { display: flex; gap: 4px; white-space: nowrap; } + .row-actions .btn.mini { padding: 2px 8px; border: 1px solid transparent; border-radius: 4px; background: none; color: #2d8cf0; font-size: 12px; } + .row-actions .btn.mini:hover { border-color: #2d8cf0; background: rgba(45,140,240,.06); } - /* ===== 详情弹窗 ===== */ + /* ===== 详情弹窗(View UI Modal 风格) ===== */ .modal-mask { - position: fixed; inset: 0; background: rgba(0,0,0,.55); + position: fixed; inset: 0; background: rgba(55,55,55,.6); display: none; z-index: 90; align-items: center; justify-content: center; } .modal-mask.show { display: flex; } .modal { - width: min(860px, 92vw); max-height: 84vh; overflow: auto; - background: var(--panel); border: 1px solid var(--border); - border-radius: 12px; padding: 18px 20px; + width: min(860px, 92vw); max-height: 84vh; overflow: hidden; + background: #fff; border-radius: 6px; + box-shadow: 0 2px 8px rgba(0,0,0,.2); + display: flex; flex-direction: column; } - .modal h3 { margin: 0 0 12px; font-size: 15px; } + .modal .modal-header { + padding: 13px 16px; border-bottom: 1px solid #e8eaec; + display: flex; align-items: center; justify-content: space-between; flex: 0 0 auto; + } + .modal .modal-header h3 { margin: 0; font-size: 14px; font-weight: 600; color: #17233d; } + .modal .mclose { + border: none; background: none; cursor: pointer; + font-size: 20px; line-height: 1; color: #808695; padding: 0 4px; border-radius: 4px; + transition: color .2s; + } + .modal .mclose:hover { color: #2d8cf0; } + .modal .modal-body { padding: 16px; overflow: auto; flex: 1 1 auto; } .modal .kv { display: grid; grid-template-columns: 110px 1fr; gap: 8px 14px; margin-bottom: 12px; } .modal .kv .k { color: var(--muted); font-size: 12px; } .modal .kv .v { font-family: var(--mono); font-size: 12.5px; word-break: break-all; white-space: pre-wrap; } - .modal .mclose { float: right; } + .modal .kv .v.scroll { max-height: 160px; overflow-y: auto; padding: 6px 8px; background: #f8f8f9; border: 1px solid #e8eaec; border-radius: 4px; } + .modal .modal-actions { display: none; gap: 8px; justify-content: flex-end; padding: 12px 16px; border-top: 1px solid #e8eaec; flex: 0 0 auto; } - .toast { - position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%); - background: #fdecec; border: 1px solid var(--red); color: #a02020; - padding: 10px 16px; border-radius: 8px; font-size: 13px; max-width: 80vw; - display: none; z-index: 100; + /* 确认弹窗(View UI Modal.confirm 风格:居中 + 警示图标) */ + .confirm-mask { + position: fixed; inset: 0; z-index: 120; display: none; + background: rgba(55,55,55,.6); + align-items: flex-start; justify-content: center; } - .toast.ok { background: #e7f6ed; border-color: var(--green); color: #1a7a40; } - .toast.show { display: block; } + .confirm-mask.show { display: flex; } + .confirm-box { + margin-top: 20vh; width: min(416px, 92vw); + background: #fff; border-radius: 6px; padding: 20px 24px 16px; + box-shadow: 0 2px 8px rgba(0,0,0,.2); + } + .confirm-body { display: flex; gap: 12px; align-items: flex-start; margin-bottom: 18px; } + .confirm-icon { + flex: 0 0 32px; width: 32px; height: 32px; border-radius: 50%; + background: #ff9900; color: #fff; font-size: 20px; font-weight: 700; + display: flex; align-items: center; justify-content: center; margin-top: 2px; + } + .confirm-msg { font-size: 14px; line-height: 1.6; color: #515a6e; } + .confirm-btns { display: flex; justify-content: flex-end; gap: 8px; } + + /* 消息提示(View UI Message 风格:顶部居中白卡 + 彩色图标) */ + .toast, .toast-top { + position: fixed; top: 16px; left: 50%; transform: translateX(-50%); + background: #fff; border-radius: 4px; + box-shadow: 0 1px 6px rgba(0,0,0,.2); + padding: 10px 16px; font-size: 14px; color: #515a6e; max-width: 80vw; + display: none; z-index: 100; align-items: center; gap: 8px; + } + .toast.show, .toast-top.show { display: flex; } + .toast::before, .toast-top::before { + content: ''; display: inline-block; flex: 0 0 16px; width: 16px; height: 16px; + border-radius: 50%; font-size: 11px; line-height: 16px; text-align: center; color: #fff; + } + .toast.show::before, .toast-top.err.show::before { content: '✕'; background: #ed4014; } + .toast.ok.show::before, .toast-top.show::before { content: '✓'; background: #19be6b; } + .toast-top { z-index: 130; } -
-
运维工作台:接口日志 / 同步日志 / 缓存管理。仅超级管理员可访问,部署后同源访问自动带登录态。
-
+
接口日志
@@ -268,12 +346,9 @@
- - - - - - + 共 0 条 + + 每页条
@@ -295,7 +370,7 @@
-
+
+ 共 0 条 + + 每页条
@@ -386,13 +458,33 @@ + + +
+
+
+ ! +
+
+
+ + +
+