运维页面
This commit is contained in:
@@ -16,6 +16,9 @@ CREATE TABLE `sys_operation_log` (
|
|||||||
`cost_time` BIGINT NOT NULL COMMENT '耗时ms',
|
`cost_time` BIGINT NOT NULL COMMENT '耗时ms',
|
||||||
`user_agent` VARCHAR(512) DEFAULT NULL,
|
`user_agent` VARCHAR(512) DEFAULT NULL,
|
||||||
`error_msg` TEXT COMMENT '异常信息(截断2000)',
|
`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 '记录时间',
|
`create_time` DATETIME(3) NOT NULL COMMENT '记录时间',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_sol_create_time` (`create_time`),
|
KEY `idx_sol_create_time` (`create_time`),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.yunhai.bschclassmanage.enums.SyncDataType;
|
|||||||
import com.yunhai.bschclassmanage.service.ISyncLogService;
|
import com.yunhai.bschclassmanage.service.ISyncLogService;
|
||||||
import com.yunhai.bschclassmanage.utils.OtherUtil;
|
import com.yunhai.bschclassmanage.utils.OtherUtil;
|
||||||
import com.yunhai.bschclassmanage.utils.SensitiveDataUtil;
|
import com.yunhai.bschclassmanage.utils.SensitiveDataUtil;
|
||||||
|
import com.yunhai.bschclassmanage.utils.SyncLogRetryContext;
|
||||||
import com.yunhai.bschclassmanage.utils.UserContext;
|
import com.yunhai.bschclassmanage.utils.UserContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
@@ -47,7 +48,12 @@ public class SyncLogAspect {
|
|||||||
SyncLog entity = new SyncLog();
|
SyncLog entity = new SyncLog();
|
||||||
// 枚举类型 key/name
|
// 枚举类型 key/name
|
||||||
entity.setSyncDataTypeKey(dataType.getKey());
|
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 前缀推导)
|
// 大类:Send / Sync(由 key 前缀推导)
|
||||||
boolean isSend = dataType.getKey() != null && dataType.getKey().startsWith("Send");
|
boolean isSend = dataType.getKey() != null && dataType.getKey().startsWith("Send");
|
||||||
entity.setSyncType(isSend ? "Send" : "Sync");
|
entity.setSyncType(isSend ? "Send" : "Sync");
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.yunhai.bschclassmanage.utils.GzipUtil;
|
|||||||
import com.yunhai.bschclassmanage.utils.OperationLogResolver;
|
import com.yunhai.bschclassmanage.utils.OperationLogResolver;
|
||||||
import com.yunhai.bschclassmanage.utils.OtherUtil;
|
import com.yunhai.bschclassmanage.utils.OtherUtil;
|
||||||
import com.yunhai.bschclassmanage.utils.SensitiveDataUtil;
|
import com.yunhai.bschclassmanage.utils.SensitiveDataUtil;
|
||||||
|
import com.yunhai.bschclassmanage.utils.SyncLogRetryContext;
|
||||||
import com.yunhai.bschclassmanage.utils.UserContext;
|
import com.yunhai.bschclassmanage.utils.UserContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
@@ -98,7 +99,12 @@ public class SysOperationLogAspect {
|
|||||||
e.setRequestMethod(httpMethod);
|
e.setRequestMethod(httpMethod);
|
||||||
e.setClassMethod(point.getTarget().getClass().getName() + "." + methodName);
|
e.setClassMethod(point.getTarget().getClass().getName() + "." + methodName);
|
||||||
e.setLogLevel(OperationLogResolver.resolveLevel(httpMethod, uri, methodName).getCode());
|
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) {
|
if (request != null) {
|
||||||
e.setUserAgent(request.getHeader("User-Agent"));
|
e.setUserAgent(request.getHeader("User-Agent"));
|
||||||
e.setRequestParams(SensitiveDataUtil.mask(buildRequestParams(point, request)));
|
e.setRequestParams(SensitiveDataUtil.mask(buildRequestParams(point, request)));
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.servlet.http.HttpSession;
|
import javax.servlet.http.HttpSession;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -37,6 +39,21 @@ public class LoginUserController {
|
|||||||
return authUserWithDept;
|
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")
|
@GetMapping("Logout")
|
||||||
protected void logout(HttpServletRequest request) {
|
protected void logout(HttpServletRequest request) {
|
||||||
HttpSession session = request.getSession();
|
HttpSession session = request.getSession();
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ public class SyncLogController extends BaseController {
|
|||||||
Object result = syncLogService.retry(id);
|
Object result = syncLogService.retry(id);
|
||||||
Map<String, Object> m = new LinkedHashMap<>();
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
m.put("success", true);
|
m.put("success", true);
|
||||||
m.put("message", "重试已提交,执行结果见新增的同步日志");
|
m.put("message", "重试成功");
|
||||||
if (result != null) m.put("data", result);
|
if (result != null) m.put("data", result);
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -80,7 +80,7 @@ public class SysOperationLogController extends BaseController {
|
|||||||
Object result = sysOperationLogService.retry(id);
|
Object result = sysOperationLogService.retry(id);
|
||||||
Map<String, Object> m = new LinkedHashMap<>();
|
Map<String, Object> m = new LinkedHashMap<>();
|
||||||
m.put("success", true);
|
m.put("success", true);
|
||||||
m.put("message", "重试已提交,执行结果见新增的审计日志");
|
m.put("message", "重试成功");
|
||||||
if (result != null) m.put("data", result);
|
if (result != null) m.put("data", result);
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,18 @@ public class SyncLog implements Serializable {
|
|||||||
@Column(name = "status")
|
@Column(name = "status")
|
||||||
private Integer 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
|
@CreationTimestamp
|
||||||
@Column(name = "create_time", nullable = false, updatable = false, columnDefinition = "DATETIME(3)")
|
@Column(name = "create_time", nullable = false, updatable = false, columnDefinition = "DATETIME(3)")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|||||||
@@ -71,6 +71,18 @@ public class SysOperationLog implements Serializable {
|
|||||||
@Column(name = "error_msg", columnDefinition = "TEXT")
|
@Column(name = "error_msg", columnDefinition = "TEXT")
|
||||||
private String errorMsg; // 异常信息,截断 2000
|
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
|
@CreationTimestamp
|
||||||
@Column(name = "create_time", nullable = false, updatable = false, columnDefinition = "DATETIME(3)")
|
@Column(name = "create_time", nullable = false, updatable = false, columnDefinition = "DATETIME(3)")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.yunhai.bschclassmanage.entity.SyncLog;
|
|||||||
import com.yunhai.bschclassmanage.model.vo.PageVO;
|
import com.yunhai.bschclassmanage.model.vo.PageVO;
|
||||||
import com.yunhai.bschclassmanage.service.ISyncLogService;
|
import com.yunhai.bschclassmanage.service.ISyncLogService;
|
||||||
import com.yunhai.bschclassmanage.utils.ReflectRetryUtil;
|
import com.yunhai.bschclassmanage.utils.ReflectRetryUtil;
|
||||||
|
import com.yunhai.bschclassmanage.utils.SyncLogRetryContext;
|
||||||
import com.yunhai.platform.utils.exception.DException;
|
import com.yunhai.platform.utils.exception.DException;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -23,6 +24,7 @@ import javax.persistence.criteria.Predicate;
|
|||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Parameter;
|
import java.lang.reflect.Parameter;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
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.getClassMethod())) throw new DException("该日志缺少调用信息(classMethod),无法重试");
|
||||||
if (StringUtils.isBlank(e.getJsonData())) throw new DException("该日志缺少入参(jsonData),无法重试");
|
if (StringUtils.isBlank(e.getJsonData())) throw new DException("该日志缺少入参(jsonData),无法重试");
|
||||||
|
|
||||||
|
// 记录本次重试元数据(重试次数 +1、重试时间)
|
||||||
|
e.setRetryCount((e.getRetryCount() == null ? 0 : e.getRetryCount()) + 1);
|
||||||
|
e.setLastRetryTime(LocalDateTime.now());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, Object> argMap = JSON.parseObject(e.getJsonData(), LinkedHashMap.class);
|
Map<String, Object> argMap = JSON.parseObject(e.getJsonData(), LinkedHashMap.class);
|
||||||
@@ -133,11 +139,26 @@ public class SyncLogServiceImpl implements ISyncLogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Method invokeMethod = ReflectRetryUtil.getInvokeMethod(resolved.bean, target);
|
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) {
|
} catch (DException de) {
|
||||||
|
e.setLastRetryStatus(0); // 本次重试失败
|
||||||
throw de;
|
throw de;
|
||||||
} catch (InvocationTargetException | IllegalAccessException ex) {
|
} catch (InvocationTargetException | IllegalAccessException ex) {
|
||||||
|
e.setLastRetryStatus(0); // 本次重试失败
|
||||||
throw ReflectRetryUtil.rethrow(ex);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-4
@@ -7,6 +7,7 @@ import com.yunhai.bschclassmanage.model.vo.PageVO;
|
|||||||
import com.yunhai.bschclassmanage.service.ISysOperationLogService;
|
import com.yunhai.bschclassmanage.service.ISysOperationLogService;
|
||||||
import com.yunhai.bschclassmanage.utils.GzipUtil;
|
import com.yunhai.bschclassmanage.utils.GzipUtil;
|
||||||
import com.yunhai.bschclassmanage.utils.ReflectRetryUtil;
|
import com.yunhai.bschclassmanage.utils.ReflectRetryUtil;
|
||||||
|
import com.yunhai.bschclassmanage.utils.SyncLogRetryContext;
|
||||||
import com.yunhai.bschclassmanage.utils.UserContext;
|
import com.yunhai.bschclassmanage.utils.UserContext;
|
||||||
import com.yunhai.platform.utils.exception.DException;
|
import com.yunhai.platform.utils.exception.DException;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -139,6 +140,13 @@ public class SysOperationLogServiceImpl implements ISysOperationLogService {
|
|||||||
throw new DException("非 JSON 数组入参(如 GET query)无法可靠重放,请手动重试");
|
throw new DException("非 JSON 数组入参(如 GET query)无法可靠重放,请手动重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录本次重试元数据(重试次数 +1、重试时间)
|
||||||
|
e.setRetryCount((e.getRetryCount() == null ? 0 : e.getRetryCount()) + 1);
|
||||||
|
e.setLastRetryTime(LocalDateTime.now());
|
||||||
|
|
||||||
|
// 还原原操作人上下文,使重放触发的审计切面记录正确操作人;结束后恢复本次请求上下文
|
||||||
|
UserContext.UserInfo snapshot = UserContext.getCurrentUser();
|
||||||
|
try {
|
||||||
JSONArray arr;
|
JSONArray arr;
|
||||||
try {
|
try {
|
||||||
arr = JSONArray.parseArray(e.getRequestParams());
|
arr = JSONArray.parseArray(e.getRequestParams());
|
||||||
@@ -163,19 +171,32 @@ public class SysOperationLogServiceImpl implements ISysOperationLogService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 还原原操作人上下文,使重放触发的审计切面记录正确操作人;结束后恢复本次请求上下文
|
|
||||||
UserContext.UserInfo snapshot = UserContext.getCurrentUser();
|
|
||||||
try {
|
|
||||||
UserContext.setUserId(e.getUserId());
|
UserContext.setUserId(e.getUserId());
|
||||||
UserContext.setUserName(e.getUserName());
|
UserContext.setUserName(e.getUserName());
|
||||||
UserContext.setSchId(e.getSchId());
|
UserContext.setSchId(e.getSchId());
|
||||||
UserContext.setIp(e.getRequestIp());
|
UserContext.setIp(e.getRequestIp());
|
||||||
Method invokeMethod = ReflectRetryUtil.getInvokeMethod(resolved.bean, target);
|
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) {
|
} catch (InvocationTargetException | IllegalAccessException ex) {
|
||||||
|
e.setLastRetryStatus(0); // 本次重试失败
|
||||||
throw ReflectRetryUtil.rethrow(ex);
|
throw ReflectRetryUtil.rethrow(ex);
|
||||||
|
} catch (RuntimeException re) {
|
||||||
|
e.setLastRetryStatus(0); // 本次重试失败(入参转换 / 定位等)
|
||||||
|
throw re;
|
||||||
} finally {
|
} finally {
|
||||||
UserContext.setUserInfo(snapshot);
|
UserContext.setUserInfo(snapshot);
|
||||||
|
SyncLogRetryContext.clear(); // 清除重试标记,避免线程池复用泄漏
|
||||||
|
try {
|
||||||
|
repository.save(e); // 同步落库重试元数据(次数 / 上次状态 / 时间)
|
||||||
|
} catch (Exception saveEx) {
|
||||||
|
log.error("审计日志重试元数据落库失败, id={}", e.getId(), saveEx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,17 @@
|
|||||||
:root {
|
:root {
|
||||||
--bg: #f5f7fa;
|
--bg: #f5f7fa;
|
||||||
--panel: #ffffff;
|
--panel: #ffffff;
|
||||||
--panel-2: #eef2f7;
|
--panel-2: #f8f8f9;
|
||||||
--border: #d8dee6;
|
--border: #e8eaec;
|
||||||
--text: #1f2933;
|
--border-in: #dcdee2;
|
||||||
--muted: #6b7785;
|
--text: #515a6e;
|
||||||
--accent: #2f6fd1;
|
--muted: #808695;
|
||||||
--accent-2: #1f5fbf;
|
--accent: #2d8cf0;
|
||||||
--green: #1a9e54;
|
--accent-hover: #57a3f3;
|
||||||
--red: #d64545;
|
--accent-2: #2487e8;
|
||||||
--yellow: #b8860b;
|
--green: #19be6b;
|
||||||
|
--red: #ed4014;
|
||||||
|
--yellow: #ff9900;
|
||||||
--mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
--mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
}
|
}
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
@@ -27,29 +29,30 @@
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
header {
|
/* ===== Breadcrumb(View UI Breadcrumb 风格) ===== */
|
||||||
padding: 16px 24px;
|
.breadcrumb {
|
||||||
border-bottom: 1px solid var(--border);
|
padding: 14px 24px; background: #fff;
|
||||||
background: linear-gradient(180deg, #eef2f7, #f5f7fa);
|
border-bottom: 1px solid #e8eaec;
|
||||||
|
font-size: 14px; color: #515a6e;
|
||||||
}
|
}
|
||||||
header .sub .pt { color: var(--text); font-weight: 600; }
|
.breadcrumb a { color: #515a6e; text-decoration: none; transition: color .2s; }
|
||||||
header .sub { margin-top: 4px; color: var(--muted); font-size: 12px; }
|
.breadcrumb a:hover { color: #2d8cf0; }
|
||||||
|
.breadcrumb .sep { margin: 0 8px; color: #dcdee2; }
|
||||||
|
.breadcrumb .current { color: #515a6e; }
|
||||||
|
|
||||||
/* ===== Tabs ===== */
|
/* ===== Tabs(View UI line 型:底部下划线激活) ===== */
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex; gap: 4px; padding: 10px 24px 0;
|
display: flex; gap: 24px; padding: 0 24px;
|
||||||
border-bottom: 1px solid var(--border); background: #e9eef4;
|
border-bottom: 1px solid var(--border-in); background: #fff;
|
||||||
}
|
}
|
||||||
.tabs .tab {
|
.tabs .tab {
|
||||||
padding: 9px 20px; cursor: pointer; font-size: 14px; color: var(--muted);
|
padding: 11px 2px; cursor: pointer; font-size: 14px; color: var(--muted);
|
||||||
border: 1px solid transparent; border-bottom: none;
|
border-bottom: 2px solid transparent; user-select: none; transition: color .2s;
|
||||||
border-radius: 8px 8px 0 0; user-select: none;
|
|
||||||
}
|
}
|
||||||
.tabs .tab:hover { color: var(--text); }
|
.tabs .tab:hover { color: var(--accent); }
|
||||||
.tabs .tab.active {
|
.tabs .tab.active {
|
||||||
color: var(--accent); background: var(--panel);
|
color: var(--accent); font-weight: 500;
|
||||||
border-color: var(--border); position: relative; top: 1px;
|
border-bottom-color: var(--accent);
|
||||||
border-bottom: 1px solid var(--panel);
|
|
||||||
}
|
}
|
||||||
.panel { display: none; padding: 16px 24px; }
|
.panel { display: none; padding: 16px 24px; }
|
||||||
.panel.active { display: block; }
|
.panel.active { display: block; }
|
||||||
@@ -59,9 +62,9 @@
|
|||||||
.left { width: 320px; flex: 0 0 320px; }
|
.left { width: 320px; flex: 0 0 320px; }
|
||||||
.right { flex: 1 1 auto; min-width: 0; }
|
.right { flex: 1 1 auto; min-width: 0; }
|
||||||
.card {
|
.card {
|
||||||
background: var(--panel);
|
background: #fff;
|
||||||
border: 1px solid var(--border);
|
border-radius: 4px;
|
||||||
border-radius: 10px;
|
box-shadow: 0 1px 1px rgba(0,0,0,.1); /* View UI Card */
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.card + .card { margin-top: 16px; }
|
.card + .card { margin-top: 16px; }
|
||||||
@@ -71,29 +74,32 @@
|
|||||||
input[type=text], input[type=number] {
|
input[type=text], input[type=number] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 9px 11px;
|
padding: 9px 11px;
|
||||||
background: var(--panel-2);
|
background: #fff;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid #dcdee2;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
outline: none;
|
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 {
|
.btn {
|
||||||
padding: 9px 14px;
|
padding: 5px 15px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border-in);
|
||||||
background: var(--panel-2);
|
background: #fff;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
transition: border-color .2s, color .2s, background .2s;
|
||||||
}
|
}
|
||||||
.btn:hover { border-color: var(--accent); }
|
.btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
.btn.primary { background: var(--accent-2); border-color: var(--accent); color: #fff; }
|
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||||
.btn.primary:hover { background: var(--accent); }
|
.btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: #fff; }
|
||||||
.btn.danger { background: transparent; border-color: var(--red); color: var(--red); }
|
.btn.danger { background: #fff; border-color: var(--red); color: var(--red); }
|
||||||
.btn.danger:hover { background: rgba(255,107,107,.12); }
|
.btn.danger:hover { background: rgba(237,64,20,.08); border-color: var(--red); color: var(--red); }
|
||||||
.btn.mini { padding: 3px 10px; font-size: 12px; border-radius: 6px; }
|
.btn.mini { padding: 2px 10px; font-size: 12px; border-radius: 4px; }
|
||||||
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
.hint { color: var(--muted); font-size: 11px; margin-top: 6px; }
|
.hint { color: var(--muted); font-size: 11px; margin-top: 6px; }
|
||||||
|
|
||||||
@@ -109,8 +115,8 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.keys-list .k:hover { background: var(--panel-2); }
|
.keys-list .k:hover { background: #f3f3f3; }
|
||||||
.keys-list .k.active { background: rgba(76,154,255,.15); border-left: 3px solid var(--accent); padding-left: 7px; }
|
.keys-list .k.active { background: #f0faff; }
|
||||||
.keys-list .k .kname {
|
.keys-list .k .kname {
|
||||||
display: block; color: var(--accent); font-size: 11px; margin-bottom: 2px;
|
display: block; color: var(--accent); font-size: 11px; margin-bottom: 2px;
|
||||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
@@ -123,28 +129,30 @@
|
|||||||
.meta .mk { color: var(--muted); font-size: 12px; }
|
.meta .mk { color: var(--muted); font-size: 12px; }
|
||||||
.meta .mv { font-family: var(--mono); word-break: break-all; }
|
.meta .mv { font-family: var(--mono); word-break: break-all; }
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block; padding: 2px 8px; border-radius: 999px;
|
display: inline-block; padding: 0 8px; height: 22px; line-height: 20px;
|
||||||
font-size: 11px; border: 1px solid var(--border); color: var(--muted);
|
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.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(76,154,255,.4); }
|
.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(247,201,72,.4); }
|
.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; }
|
.toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
|
||||||
.count { color: var(--muted); font-size: 12px; }
|
.count { color: var(--muted); font-size: 12px; }
|
||||||
select {
|
select {
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
background: var(--panel-2);
|
background: #fff;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid #dcdee2;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
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 {
|
pre.json {
|
||||||
margin: 0; padding: 12px; background: #f0f3f7; border: 1px solid var(--border);
|
margin: 0; padding: 12px; background: #f8f8f9; border: 1px solid #e8eaec;
|
||||||
border-radius: 8px; overflow: auto; max-height: 60vh; font-family: var(--mono);
|
border-radius: 4px; overflow: auto; max-height: 60vh; font-family: var(--mono);
|
||||||
font-size: 12px; line-height: 1.5; color: #1f2933; white-space: pre;
|
font-size: 12px; line-height: 1.5; color: #1f2933; white-space: pre;
|
||||||
}
|
}
|
||||||
pre.json .jk { color: #c0392b; } /* key */
|
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); }
|
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 { 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 label { margin-bottom: 0; }
|
.filter-bar .fi { display: flex; align-items: center; gap: 5px; flex-shrink: 0; }
|
||||||
.filter-bar .fi input[type=text], .filter-bar .fi input[type=number] { width: 170px; padding: 7px 10px; font-size: 13px; }
|
.filter-bar .fi label { margin: 0; color: #515a6e; font-size: 12px; white-space: nowrap; }
|
||||||
.filter-bar .fi select { min-width: 110px; }
|
.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 { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
||||||
table.log th, table.log td {
|
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;
|
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 th { color: #515a6e; font-weight: 600; white-space: nowrap; background: #f8f8f9; position: sticky; top: 0; }
|
||||||
table.log tr:hover td { background: rgba(76,154,255,.05); }
|
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.mono { font-family: var(--mono); font-size: 12px; }
|
||||||
table.log td .cell-clip {
|
table.log td .cell-clip {
|
||||||
display: inline-block; max-width: 280px; overflow: hidden;
|
display: inline-block; max-width: 280px; overflow: hidden;
|
||||||
text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;
|
text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;
|
||||||
}
|
}
|
||||||
.table-wrap { overflow: auto; max-height: 62vh; border: 1px solid var(--border); border-radius: 8px; }
|
.table-wrap { overflow: auto; max-height: 62vh; border: 1px solid #e8eaec; border-radius: 4px; }
|
||||||
.tag { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 11px; border: 1px solid; white-space: nowrap; }
|
.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(54,194,117,.4); }
|
.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(255,107,107,.4); }
|
.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(247,201,72,.4); }
|
.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(76,154,255,.4); }
|
.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: var(--border); }
|
.tag.gray { color: var(--muted); border-color: #e8eaec; background: #f8f8f9; }
|
||||||
.pager { display: flex; gap: 10px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
|
/* 分页条:View UI 风格页码按钮,靠右 */
|
||||||
.pager .pinfo { color: var(--muted); font-size: 12px; }
|
.pager { display: flex; gap: 8px; align-items: center; justify-content: flex-end; margin-top: 14px; flex-wrap: wrap; color: #808695; font-size: 13px; }
|
||||||
.row-actions { display: flex; gap: 6px; white-space: nowrap; }
|
.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 {
|
.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;
|
display: none; z-index: 90; align-items: center; justify-content: center;
|
||||||
}
|
}
|
||||||
.modal-mask.show { display: flex; }
|
.modal-mask.show { display: flex; }
|
||||||
.modal {
|
.modal {
|
||||||
width: min(860px, 92vw); max-height: 84vh; overflow: auto;
|
width: min(860px, 92vw); max-height: 84vh; overflow: hidden;
|
||||||
background: var(--panel); border: 1px solid var(--border);
|
background: #fff; border-radius: 6px;
|
||||||
border-radius: 12px; padding: 18px 20px;
|
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 { display: grid; grid-template-columns: 110px 1fr; gap: 8px 14px; margin-bottom: 12px; }
|
||||||
.modal .kv .k { color: var(--muted); font-size: 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 .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 {
|
/* 确认弹窗(View UI Modal.confirm 风格:居中 + 警示图标) */
|
||||||
position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%);
|
.confirm-mask {
|
||||||
background: #fdecec; border: 1px solid var(--red); color: #a02020;
|
position: fixed; inset: 0; z-index: 120; display: none;
|
||||||
padding: 10px 16px; border-radius: 8px; font-size: 13px; max-width: 80vw;
|
background: rgba(55,55,55,.6);
|
||||||
display: none; z-index: 100;
|
align-items: flex-start; justify-content: center;
|
||||||
}
|
}
|
||||||
.toast.ok { background: #e7f6ed; border-color: var(--green); color: #1a7a40; }
|
.confirm-mask.show { display: flex; }
|
||||||
.toast.show { display: block; }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<div class="breadcrumb">
|
||||||
<div class="sub"><b class="pt">运维工作台:</b>接口日志 / 同步日志 / 缓存管理。仅超级管理员可访问,部署后同源访问自动带登录态。</div>
|
<a href="#">首页</a><span class="sep">/</span>
|
||||||
</header>
|
<a href="#">运维管理</a><span class="sep">/</span>
|
||||||
|
<span class="current" id="bcCurrent">接口日志</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<div class="tab active" data-tab="oplog">接口日志</div>
|
<div class="tab active" data-tab="oplog">接口日志</div>
|
||||||
@@ -268,12 +346,9 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="pager">
|
<div class="pager">
|
||||||
<button class="btn mini" id="op_prev">上一页</button>
|
<span class="pinfo" id="op_pinfo">共 0 条</span>
|
||||||
<button class="btn mini" id="op_next">下一页</button>
|
<span class="pg" id="op_pg"></span>
|
||||||
<span class="pinfo" id="op_pinfo"></span>
|
<span class="size">每页<select id="op_size"><option>10</option><option selected>20</option><option>50</option><option>100</option></select>条</span>
|
||||||
<span style="flex:1"></span>
|
|
||||||
<label style="margin:0">每页</label>
|
|
||||||
<select id="op_size"><option>10</option><option selected>20</option><option>50</option><option>100</option></select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -295,7 +370,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="fi"><label>大类名称</label><input type="text" id="sy_syncTypeName" placeholder="如 同步 / 推送"/></div>
|
<div class="fi"><label>大类名称</label><input type="text" id="sy_syncTypeName" placeholder="如 同步 / 推送"/></div>
|
||||||
<div class="fi"><label>类名.方法名</label><input type="text" id="sy_classMethod" placeholder="如 SyncUserBaseService.addGroup"/></div>
|
<div class="fi"><label>类名.方法名</label><input type="text" id="sy_classMethod" placeholder="如 SendUserBaseService.deleteTeachers"/></div>
|
||||||
<div class="fi"><label>执行状态</label>
|
<div class="fi"><label>执行状态</label>
|
||||||
<select id="sy_status">
|
<select id="sy_status">
|
||||||
<option value="">全部</option>
|
<option value="">全部</option>
|
||||||
@@ -319,12 +394,9 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="pager">
|
<div class="pager">
|
||||||
<button class="btn mini" id="sy_prev">上一页</button>
|
<span class="pinfo" id="sy_pinfo">共 0 条</span>
|
||||||
<button class="btn mini" id="sy_next">下一页</button>
|
<span class="pg" id="sy_pg"></span>
|
||||||
<span class="pinfo" id="sy_pinfo"></span>
|
<span class="size">每页<select id="sy_size"><option>10</option><option selected>20</option><option>50</option><option>100</option></select>条</span>
|
||||||
<span style="flex:1"></span>
|
|
||||||
<label style="margin:0">每页</label>
|
|
||||||
<select id="sy_size"><option>10</option><option selected>20</option><option>50</option><option>100</option></select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -386,13 +458,33 @@
|
|||||||
<!-- ==================== 日志详情弹窗 ==================== -->
|
<!-- ==================== 日志详情弹窗 ==================== -->
|
||||||
<div class="modal-mask" id="modalMask">
|
<div class="modal-mask" id="modalMask">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
<button class="btn mini mclose" id="modalClose">关闭</button>
|
<div class="modal-header">
|
||||||
<h3 id="modalTitle">日志详情</h3>
|
<h3 id="modalTitle">日志详情</h3>
|
||||||
|
<button class="mclose" id="modalClose" title="关闭">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
<div class="kv" id="modalKv"></div>
|
<div class="kv" id="modalKv"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-actions" id="modalActions"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ==================== 顶部确认弹窗(重试确认等) ==================== -->
|
||||||
|
<div class="confirm-mask" id="confirmMask">
|
||||||
|
<div class="confirm-box">
|
||||||
|
<div class="confirm-body">
|
||||||
|
<span class="confirm-icon">!</span>
|
||||||
|
<div class="confirm-msg" id="confirmMsg"></div>
|
||||||
|
</div>
|
||||||
|
<div class="confirm-btns">
|
||||||
|
<button class="btn" id="confirmCancel">取消</button>
|
||||||
|
<button class="btn primary" id="confirmOk">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
<div class="toast-top" id="toastTop"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// ==================== 公共 ====================
|
// ==================== 公共 ====================
|
||||||
@@ -404,25 +496,18 @@
|
|||||||
const API = resolveApi();
|
const API = resolveApi();
|
||||||
|
|
||||||
// ==================== 登录态校验 ====================
|
// ==================== 登录态校验 ====================
|
||||||
// CAS 登录页基础地址;service 指回当前 work-view.html 完整地址,登录成功后会跳回本页
|
// 通过后端 checkLogin 接口完成登录校验:已登录则后端 302 重定向回 work-view.html;
|
||||||
const CAS_LOGIN = 'http://sso.yunhaixingtong.com/cas/front/index.html';
|
// 未登录则由 CAS 拦截器 302 到 CAS 登录页,登录成功后再回跳本页。
|
||||||
function casLoginUrl() {
|
// 用 sessionStorage 标记"已完成校验",避免重定向回本页后再次跳转造成死循环。
|
||||||
const service = location.origin + location.pathname; // 例如 http://192.168.10.166:8086/work-view.html
|
|
||||||
return CAS_LOGIN + '?service=' + encodeURIComponent(service);
|
|
||||||
}
|
|
||||||
// 主动校验是否登录:调用 /api/GetUserInfo,返回 200 视为已登录;其余(含网络异常)一律跳转 CAS 登录页
|
|
||||||
let loginChecked = false;
|
let loginChecked = false;
|
||||||
async function checkLogin() {
|
async function checkLogin() {
|
||||||
if (loginChecked) return true;
|
if (loginChecked) return true;
|
||||||
try {
|
if (sessionStorage.getItem('wbLoginChecked') === '1') {
|
||||||
// redirect:'manual' 防止浏览器自动跟随 302 跳到 CAS 登录页(否则跟随后 status 会变成 200 造成误判)
|
loginChecked = true;
|
||||||
const res = await fetch(API + '/GetUserInfo', {
|
return true;
|
||||||
method: 'GET', credentials: 'include', headers: authHeaders(), redirect: 'manual'
|
}
|
||||||
});
|
sessionStorage.setItem('wbLoginChecked', '1');
|
||||||
if (res.status === 200) { loginChecked = true; return true; }
|
location.replace(API + '/checkLogin');
|
||||||
} catch (e) { /* 网络异常视为未登录 */ }
|
|
||||||
// 登录失败:跳转 CAS 登录页(登录成功会带着 service 跳回本页)
|
|
||||||
location.replace(casLoginUrl());
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +531,15 @@
|
|||||||
t._timer = setTimeout(() => t.classList.remove('show'), 3500);
|
t._timer = setTimeout(() => t.classList.remove('show'), 3500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 顶部提示(重试结果等,显示在页面顶部)
|
||||||
|
function toastTop(msg, ok) {
|
||||||
|
const t = $('toastTop');
|
||||||
|
t.textContent = msg;
|
||||||
|
t.className = 'toast-top show' + (ok ? '' : ' err');
|
||||||
|
clearTimeout(t._timer);
|
||||||
|
t._timer = setTimeout(() => t.classList.remove('show'), 3500);
|
||||||
|
}
|
||||||
|
|
||||||
function showLoading(btn, text) {
|
function showLoading(btn, text) {
|
||||||
btn.dataset.old = btn.textContent;
|
btn.dataset.old = btn.textContent;
|
||||||
btn.innerHTML = '<span class="spinner"></span>' + text;
|
btn.innerHTML = '<span class="spinner"></span>' + text;
|
||||||
@@ -535,23 +629,38 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Tab 切换 ====================
|
// ==================== Tab 切换 ====================
|
||||||
|
// 记住当前 Tab 与各表格页码(sessionStorage,点击刷新后恢复)
|
||||||
|
function saveState(key, val) {
|
||||||
|
try { sessionStorage.setItem(key, String(val)); } catch (e) {}
|
||||||
|
}
|
||||||
|
function loadState(key) {
|
||||||
|
try { return sessionStorage.getItem(key); } catch (e) { return null; }
|
||||||
|
}
|
||||||
|
function savedPage(prefix) {
|
||||||
|
const v = parseInt(loadState('wbPage_' + prefix), 10);
|
||||||
|
return isNaN(v) || v < 1 ? 1 : v;
|
||||||
|
}
|
||||||
const loadedTabs = {};
|
const loadedTabs = {};
|
||||||
function switchTab(name) {
|
function switchTab(name) {
|
||||||
|
saveState('wbTab', name); // 记住点击/切换时的 Tab
|
||||||
document.querySelectorAll('.tabs .tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
|
document.querySelectorAll('.tabs .tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
|
||||||
document.querySelectorAll('.panel').forEach(p => p.classList.toggle('active', p.id === 'panel-' + name));
|
document.querySelectorAll('.panel').forEach(p => p.classList.toggle('active', p.id === 'panel-' + name));
|
||||||
// 首次切到日志页时自动加载
|
// 同步面包屑末级文字
|
||||||
|
const bcMap = { oplog: '接口日志', synclog: '同步日志', cache: '缓存管理' };
|
||||||
|
const bcEl = document.getElementById('bcCurrent');
|
||||||
|
if (bcEl) bcEl.textContent = bcMap[name] || '';
|
||||||
|
// 首次切到日志页时自动加载(用记住的页码)
|
||||||
if (!loadedTabs[name]) {
|
if (!loadedTabs[name]) {
|
||||||
loadedTabs[name] = true;
|
loadedTabs[name] = true;
|
||||||
if (name === 'oplog') { loadSchoolOptions(); loadOpLog(1); }
|
if (name === 'oplog') { loadSchoolOptions(); loadOpLog(savedPage('op')); }
|
||||||
if (name === 'synclog') { loadSchoolOptions(); loadSyncLog(1); }
|
if (name === 'synclog') { loadSchoolOptions(); loadSyncLog(savedPage('sy')); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.querySelectorAll('.tabs .tab').forEach(t => { t.onclick = () => switchTab(t.dataset.tab); });
|
document.querySelectorAll('.tabs .tab').forEach(t => { t.onclick = () => switchTab(t.dataset.tab); });
|
||||||
|
|
||||||
// ==================== 通用分页表格状态 ====================
|
// ==================== 通用分页表格状态 ====================
|
||||||
function makePager(prefix, loader) {
|
function makePager(prefix, loader) {
|
||||||
$(prefix + '_prev').onclick = () => loader(state[prefix].page - 1);
|
state[prefix].loader = loader;
|
||||||
$(prefix + '_next').onclick = () => loader(state[prefix].page + 1);
|
|
||||||
$(prefix + '_size').onchange = () => loader(1);
|
$(prefix + '_size').onchange = () => loader(1);
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
@@ -559,22 +668,54 @@
|
|||||||
sy: { page: 1, totalPages: 1 }
|
sy: { page: 1, totalPages: 1 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// View UI 风格分页:共N条 + 页码按钮(含省略号) + 上一页/下一页
|
||||||
function renderPager(prefix, pageVO) {
|
function renderPager(prefix, pageVO) {
|
||||||
const s = state[prefix];
|
const s = state[prefix];
|
||||||
s.page = pageVO.current || 1;
|
s.page = pageVO.current || 1;
|
||||||
s.totalPages = pageVO.totalPages || 1;
|
s.totalPages = pageVO.totalPages || 1;
|
||||||
$(prefix + '_pinfo').textContent =
|
saveState('wbPage_' + prefix, s.page); // 记住当前页码,刷新后恢复
|
||||||
'第 ' + s.page + ' / ' + s.totalPages + ' 页 · 共 ' + (pageVO.total || 0) + ' 条';
|
$(prefix + '_pinfo').textContent = '共 ' + (pageVO.total || 0) + ' 条';
|
||||||
$(prefix + '_prev').disabled = s.page <= 1;
|
const el = $(prefix + '_pg');
|
||||||
$(prefix + '_next').disabled = s.page >= s.totalPages;
|
if (!el) return;
|
||||||
|
const cur = s.page, total = s.totalPages;
|
||||||
|
let html = '<button class="pg-btn" data-p="' + (cur - 1) + '"' + (cur <= 1 ? ' disabled' : '') + '>‹</button>';
|
||||||
|
const pages = [];
|
||||||
|
if (total <= 7) {
|
||||||
|
for (let i = 1; i <= total; i++) pages.push(i);
|
||||||
|
} else {
|
||||||
|
pages.push(1);
|
||||||
|
if (cur > 3) pages.push(-1); // 省略号
|
||||||
|
for (let i = Math.max(2, cur - 1); i <= Math.min(total - 1, cur + 1); i++) pages.push(i);
|
||||||
|
if (cur < total - 2) pages.push(-2); // 省略号
|
||||||
|
pages.push(total);
|
||||||
|
}
|
||||||
|
pages.forEach(p => {
|
||||||
|
if (p < 0) html += '<span class="pg-ellipsis">…</span>';
|
||||||
|
else html += '<button class="pg-btn' + (p === cur ? ' active' : '') + '" data-p="' + p + '">' + p + '</button>';
|
||||||
|
});
|
||||||
|
html += '<button class="pg-btn" data-p="' + (cur + 1) + '"' + (cur >= total ? ' disabled' : '') + '>›</button>';
|
||||||
|
el.innerHTML = html;
|
||||||
|
const loader = state[prefix].loader;
|
||||||
|
el.querySelectorAll('button.pg-btn[data-p]').forEach(b => {
|
||||||
|
b.onclick = () => { if (loader) loader(Number(b.dataset.p)); };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 详情弹窗 ====================
|
// ==================== 详情弹窗 ====================
|
||||||
function openModal(title, rows) {
|
function openModal(title, rows, actionsHtml, retryHandler) {
|
||||||
$('modalTitle').textContent = title;
|
$('modalTitle').textContent = title;
|
||||||
$('modalKv').innerHTML = rows.map(r =>
|
$('modalKv').innerHTML = rows.map(r =>
|
||||||
'<div class="k">' + escHtml(r[0]) + '</div><div class="v">' + escHtml(r[1] == null || r[1] === '' ? '-' : r[1]) + '</div>'
|
'<div class="k">' + escHtml(r[0]) + '</div>' +
|
||||||
|
'<div class="v' + (r[2] ? ' scroll' : '') + '">' + escHtml(r[1] == null || r[1] === '' ? '-' : r[1]) + '</div>'
|
||||||
).join('');
|
).join('');
|
||||||
|
const act = $('modalActions');
|
||||||
|
act.innerHTML = actionsHtml || '';
|
||||||
|
act.style.display = actionsHtml ? 'flex' : 'none';
|
||||||
|
act.querySelectorAll('button[data-a]').forEach(b => {
|
||||||
|
b.onclick = () => {
|
||||||
|
if (b.dataset.a === 'retry') (retryHandler || doSyncRetry)(Number(b.dataset.id));
|
||||||
|
};
|
||||||
|
});
|
||||||
$('modalMask').classList.add('show');
|
$('modalMask').classList.add('show');
|
||||||
}
|
}
|
||||||
$('modalClose').onclick = () => $('modalMask').classList.remove('show');
|
$('modalClose').onclick = () => $('modalMask').classList.remove('show');
|
||||||
@@ -661,7 +802,6 @@
|
|||||||
'<td><div class="row-actions">' +
|
'<td><div class="row-actions">' +
|
||||||
'<button class="btn mini" data-a="detail" data-id="' + r.id + '">详情</button>' +
|
'<button class="btn mini" data-a="detail" data-id="' + r.id + '">详情</button>' +
|
||||||
'<button class="btn mini" data-a="download" data-id="' + r.id + '">下载</button>' +
|
'<button class="btn mini" data-a="download" data-id="' + r.id + '">下载</button>' +
|
||||||
(ok ? '' : '<button class="btn mini" data-a="retry" data-id="' + r.id + '">重试</button>') +
|
|
||||||
'</div></td>' +
|
'</div></td>' +
|
||||||
'</tr>';
|
'</tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -674,6 +814,8 @@
|
|||||||
if (action === 'detail') {
|
if (action === 'detail') {
|
||||||
const r = rows.find(x => x.id === id);
|
const r = rows.find(x => x.id === id);
|
||||||
if (!r) return;
|
if (!r) return;
|
||||||
|
const lrStatus = r.lastRetryStatus === 1 ? '成功'
|
||||||
|
: r.lastRetryStatus === 0 ? '失败' : '-';
|
||||||
openModal('接口日志详情 #' + id, [
|
openModal('接口日志详情 #' + id, [
|
||||||
['ID', r.id],
|
['ID', r.id],
|
||||||
['日志级别', r.logLevel === 1 ? '1-重要(删除)' : r.logLevel === 2 ? '2-次要(新增/编辑)' : r.logLevel === 3 ? '3-一般(查询)' : r.logLevel],
|
['日志级别', r.logLevel === 1 ? '1-重要(删除)' : r.logLevel === 2 ? '2-次要(新增/编辑)' : r.logLevel === 3 ? '3-一般(查询)' : r.logLevel],
|
||||||
@@ -685,23 +827,18 @@
|
|||||||
['请求 URI', r.requestUri],
|
['请求 URI', r.requestUri],
|
||||||
['请求方法', r.requestMethod],
|
['请求方法', r.requestMethod],
|
||||||
['类名.方法名', r.classMethod],
|
['类名.方法名', r.classMethod],
|
||||||
['请求参数(脱敏)', r.requestParams],
|
['请求参数(脱敏)', r.requestParams, true],
|
||||||
['状态码', r.statusCode],
|
['状态码', r.statusCode],
|
||||||
['耗时', (r.costTime == null ? '-' : r.costTime + ' ms')],
|
['耗时', (r.costTime == null ? '-' : r.costTime + ' ms')],
|
||||||
['异常信息', r.errorMsg],
|
['异常信息', r.errorMsg, true],
|
||||||
['User-Agent', r.userAgent],
|
['User-Agent', r.userAgent],
|
||||||
['时间', fmtTime(r.createTime)]
|
['时间', fmtTime(r.createTime)],
|
||||||
|
['重试次数', (r.retryCount == null ? 0 : r.retryCount) + ' 次'],
|
||||||
|
['上次重试状态', lrStatus],
|
||||||
|
['上次重试时间', r.lastRetryTime ? fmtTime(r.lastRetryTime) : '-']
|
||||||
]);
|
]);
|
||||||
} else if (action === 'download') {
|
} else if (action === 'download') {
|
||||||
downloadFile('/syslog/download/' + id, 'audit-' + id + '.txt');
|
downloadFile('/syslog/download/' + id, 'audit-' + id + '.txt');
|
||||||
} else if (action === 'retry') {
|
|
||||||
if (!confirm('确认按记录中的 classMethod + requestParams 反射重放该操作?\n执行结果见新增的审计日志。')) return;
|
|
||||||
try {
|
|
||||||
const data = await apiPost('/syslog/retry/' + id);
|
|
||||||
toast((data && (data.message || data.msg)) || '重试已提交', true);
|
|
||||||
} catch (e) {
|
|
||||||
toast('重试失败:' + e.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -768,6 +905,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 类名.方法名 缩写:去掉包名前缀,只保留 类名.方法名(如 ...SendUserBaseService.deleteTeachers → SendUserBaseService.deleteTeachers)
|
||||||
|
function shortClassMethod(cm) {
|
||||||
|
if (cm == null) return '-';
|
||||||
|
const parts = String(cm).split('.');
|
||||||
|
return parts.length >= 2 ? parts.slice(-2).join('.') : String(cm);
|
||||||
|
}
|
||||||
|
|
||||||
function renderSyRows(rows) {
|
function renderSyRows(rows) {
|
||||||
const tb = $('sy_tbody');
|
const tb = $('sy_tbody');
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
@@ -789,7 +933,7 @@
|
|||||||
'<td class="mono">' + r.id + '</td>' +
|
'<td class="mono">' + r.id + '</td>' +
|
||||||
'<td>' + big + '</td>' +
|
'<td>' + big + '</td>' +
|
||||||
'<td>' + escHtml(typeText) + '</td>' +
|
'<td>' + escHtml(typeText) + '</td>' +
|
||||||
'<td class="mono"><span class="cell-clip" title="' + escHtml(r.classMethod) + '">' + escHtml(r.classMethod) + '</span></td>' +
|
'<td class="mono"><span class="cell-clip" title="' + escHtml(r.classMethod) + '">' + escHtml(shortClassMethod(r.classMethod)) + '</span></td>' +
|
||||||
'<td>' + st + '</td>' +
|
'<td>' + st + '</td>' +
|
||||||
'<td>' + escHtml(r.userName || (r.userId == null ? '-' : r.userId)) + '</td>' +
|
'<td>' + escHtml(r.userName || (r.userId == null ? '-' : r.userId)) + '</td>' +
|
||||||
'<td>' + escHtml(srcName) + '</td>' +
|
'<td>' + escHtml(srcName) + '</td>' +
|
||||||
@@ -798,7 +942,6 @@
|
|||||||
'<td><div class="row-actions">' +
|
'<td><div class="row-actions">' +
|
||||||
'<button class="btn mini" data-a="detail" data-id="' + r.id + '">详情</button>' +
|
'<button class="btn mini" data-a="detail" data-id="' + r.id + '">详情</button>' +
|
||||||
'<button class="btn mini" data-a="download" data-id="' + r.id + '">下载</button>' +
|
'<button class="btn mini" data-a="download" data-id="' + r.id + '">下载</button>' +
|
||||||
(r.status !== 1 ? '<button class="btn mini" data-a="retry" data-id="' + r.id + '">重试</button>' : '') +
|
|
||||||
'</div></td>' +
|
'</div></td>' +
|
||||||
'</tr>';
|
'</tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -811,6 +954,11 @@
|
|||||||
if (action === 'detail') {
|
if (action === 'detail') {
|
||||||
const r = rows.find(x => x.id === id);
|
const r = rows.find(x => x.id === id);
|
||||||
if (!r) return;
|
if (!r) return;
|
||||||
|
const lrStatus = r.lastRetryStatus === 1 ? '成功'
|
||||||
|
: r.lastRetryStatus === 0 ? '失败' : '-';
|
||||||
|
const retryBtn = r.status !== 1
|
||||||
|
? '<button class="btn primary" data-a="retry" data-id="' + r.id + '">重试</button>'
|
||||||
|
: '';
|
||||||
openModal('同步日志详情 #' + id, [
|
openModal('同步日志详情 #' + id, [
|
||||||
['ID', r.id],
|
['ID', r.id],
|
||||||
['大类', (r.syncTypeName || r.syncType || '-') + (r.syncType ? '(' + r.syncType + ')' : '')],
|
['大类', (r.syncTypeName || r.syncType || '-') + (r.syncType ? '(' + r.syncType + ')' : '')],
|
||||||
@@ -821,20 +969,47 @@
|
|||||||
['学校 schId', r.schId],
|
['学校 schId', r.schId],
|
||||||
['来源(学校)', schoolMap[r.schId] || r.schId || '-'],
|
['来源(学校)', schoolMap[r.schId] || r.schId || '-'],
|
||||||
['客户端 IP', r.requestIp],
|
['客户端 IP', r.requestIp],
|
||||||
['请求入参(脱敏)', r.jsonData],
|
['请求入参(脱敏)', r.jsonData, true],
|
||||||
['异常信息', r.errorMsg],
|
['异常信息', r.errorMsg, true],
|
||||||
['时间', fmtTime(r.createTime)]
|
['时间', fmtTime(r.createTime)],
|
||||||
]);
|
['重试次数', (r.retryCount == null ? 0 : r.retryCount) + ' 次'],
|
||||||
|
['上次重试状态', lrStatus],
|
||||||
|
['上次重试时间', r.lastRetryTime ? fmtTime(r.lastRetryTime) : '-']
|
||||||
|
], retryBtn);
|
||||||
} else if (action === 'download') {
|
} else if (action === 'download') {
|
||||||
downloadFile('/synclog/download/' + id, 'sync-' + id + '.txt');
|
downloadFile('/synclog/download/' + id, 'sync-' + id + '.txt');
|
||||||
} else if (action === 'retry') {
|
}
|
||||||
if (!confirm('确认按记录中的 classMethod + jsonData 反射重放该同步/推送?\n执行结果见新增的同步日志。')) return;
|
}
|
||||||
|
|
||||||
|
// 顶部确认弹窗(替代原生 confirm,提示框显示在页面顶部)
|
||||||
|
function confirmTop(msg) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
$('confirmMsg').textContent = msg;
|
||||||
|
const mask = $('confirmMask');
|
||||||
|
mask.classList.add('show');
|
||||||
|
const done = (val) => {
|
||||||
|
mask.classList.remove('show');
|
||||||
|
$('confirmOk').onclick = null;
|
||||||
|
$('confirmCancel').onclick = null;
|
||||||
|
mask.onclick = null;
|
||||||
|
resolve(val);
|
||||||
|
};
|
||||||
|
$('confirmOk').onclick = () => done(true);
|
||||||
|
$('confirmCancel').onclick = () => done(false);
|
||||||
|
mask.onclick = (e) => { if (e.target === mask) done(false); };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试:从详情弹窗发起(成功提示 + 刷新列表展示最新重试字段)
|
||||||
|
async function doSyncRetry(id) {
|
||||||
|
if (!await confirmTop('确认按记录中的 classMethod + jsonData 反射重放该同步/推送?')) return;
|
||||||
try {
|
try {
|
||||||
const data = await apiPost('/synclog/retry/' + id);
|
const data = await apiPost('/synclog/retry/' + id);
|
||||||
toast((data && (data.message || data.msg)) || '重试已提交', true);
|
toastTop((data && (data.message || data.msg)) || '重试成功', true);
|
||||||
|
$('modalMask').classList.remove('show');
|
||||||
|
loadSyncLog(state.sy.page); // 刷新当前页,展示最新重试次数/状态/时间
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast('重试失败:' + e.message);
|
toastTop('重试失败:' + e.message, false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,11 +1251,12 @@
|
|||||||
$('keyInput').addEventListener('keydown', e => { if (e.key === 'Enter') queryByKey(e.target.value.trim()); });
|
$('keyInput').addEventListener('keydown', e => { if (e.key === 'Enter') queryByKey(e.target.value.trim()); });
|
||||||
$('patternInput').addEventListener('keydown', e => { if (e.key === 'Enter') listKeys(e.target.value.trim() || '*'); });
|
$('patternInput').addEventListener('keydown', e => { if (e.key === 'Enter') listKeys(e.target.value.trim() || '*'); });
|
||||||
|
|
||||||
// 打开页面先校验登录态,未登录直接跳转 CAS;已登录再加载首页
|
// 打开页面先校验登录态,未登录直接跳转 CAS;已登录再恢复上次的 Tab 与表格页码
|
||||||
(async () => {
|
(async () => {
|
||||||
const ok = await checkLogin();
|
const ok = await checkLogin();
|
||||||
if (!ok) return; // 已跳转登录页
|
if (!ok) return; // 已跳转登录页
|
||||||
switchTab('oplog');
|
const savedTab = loadState('wbTab');
|
||||||
|
switchTab(savedTab === 'oplog' || savedTab === 'synclog' || savedTab === 'cache' ? savedTab : 'oplog');
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user