api生成器初版确定提交
This commit is contained in:
@@ -61,9 +61,14 @@ public class ApiDocGenerator {
|
||||
|
||||
String template = resourceLoader.loadTemplate();
|
||||
|
||||
// 懒初始化本地代理(仅第一次触发)
|
||||
String proxyUrl = DubboDebugProxy.getInstance().getInvokeUrl();
|
||||
|
||||
return template
|
||||
.replace("{{CSS}}", resourceLoader.loadCss())
|
||||
.replace("{{JS}}", resourceLoader.loadJs())
|
||||
.replace("{{HTML2CANVAS}}", resourceLoader.loadHtml2Canvas())
|
||||
.replace("{{PROXY_URL}}", proxyUrl)
|
||||
.replace("{{GENERATE_TIME}}", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
|
||||
.replace("{{STATS_BAR}}", buildStatsBar(docs.size(), totalClasses, totalMethods))
|
||||
.replace("{{SIDEBAR}}", buildSidebar(docs))
|
||||
@@ -167,7 +172,7 @@ public class ApiDocGenerator {
|
||||
sb.append(" <div class=\"methods-section\">\n");
|
||||
sb.append(" <div class=\"section-label\">📋 接口方法</div>\n");
|
||||
for (ApiDocModel.MethodInfo mi : ci.methods) {
|
||||
sb.append(buildMethodCard(anchor, mi));
|
||||
sb.append(buildMethodCard(anchor, mi, ci.fullClassName));
|
||||
}
|
||||
sb.append(" </div>\n");
|
||||
}
|
||||
@@ -206,15 +211,24 @@ public class ApiDocGenerator {
|
||||
}
|
||||
|
||||
/** 构建单个方法卡片 */
|
||||
private String buildMethodCard(String classAnchor, ApiDocModel.MethodInfo mi) {
|
||||
private String buildMethodCard(String classAnchor, ApiDocModel.MethodInfo mi, String fullClassName) {
|
||||
String methodId = classAnchor + "_" + safeId(mi.name);
|
||||
StringBuilder sb = new StringBuilder(2000);
|
||||
sb.append(" <div class=\"method-card\" id=\"").append(methodId).append("\">\n");
|
||||
String httpMethod = extractHttpMethod(mi.annotations);
|
||||
StringBuilder sb = new StringBuilder(3000);
|
||||
|
||||
// 所有方法默认嵌入调试元数据
|
||||
String paramTypesJson = buildParamTypesJson(mi);
|
||||
String sampleArgsJson = buildSampleArgsJson(mi.requestFields);
|
||||
sb.append(" <div class=\"method-card\" id=\"").append(methodId).append("\"");
|
||||
sb.append("\n data-dubbo-interface=\"").append(escapeAttr(fullClassName)).append("\"");
|
||||
sb.append("\n data-dubbo-method=\"").append(escapeAttr(mi.name)).append("\"");
|
||||
sb.append("\n data-dubbo-param-types='").append(paramTypesJson).append("'");
|
||||
sb.append("\n data-dubbo-sample-args='").append(sampleArgsJson).append("'");
|
||||
sb.append(">\n");
|
||||
|
||||
// 方法头
|
||||
sb.append(" <div class=\"method-header\" onclick=\"toggleMethod(this)\">\n");
|
||||
sb.append(" <span class=\"method-arrow\">▼</span>\n");
|
||||
String httpMethod = extractHttpMethod(mi.annotations);
|
||||
if (!httpMethod.isEmpty()) {
|
||||
sb.append(" <span class=\"http-badge ").append(httpMethod.toLowerCase()).append("\">").append(httpMethod).append("</span>\n");
|
||||
}
|
||||
@@ -222,6 +236,7 @@ public class ApiDocGenerator {
|
||||
if (mi.returnType != null) {
|
||||
sb.append(" <span class=\"method-return\">→ <code>").append(escapeHtml(mi.returnType)).append("</code></span>\n");
|
||||
}
|
||||
sb.append(" <button class=\"method-copy-btn dubbo\" onclick=\"event.stopPropagation();openDubboDebug(this.closest('.method-card'))\" title=\"接口调试\">🔧 调试</button>\n");
|
||||
sb.append(" <button class=\"method-copy-btn img\" onclick=\"event.stopPropagation();copyImage(this.closest('.method-card'),this)\" title=\"复制为图片(PNG)\">🖼 图片</button>\n");
|
||||
sb.append(" <button class=\"method-copy-btn txt\" onclick=\"event.stopPropagation();copyRtfFromEl(this.closest('.method-card'),this)\" title=\"复制为 Word 富文本(RTF)\">📋 富文本</button>\n");
|
||||
sb.append(" </div>\n");
|
||||
@@ -417,6 +432,108 @@ public class ApiDocGenerator {
|
||||
return "";
|
||||
}
|
||||
|
||||
// ==================== Dubbo 调试元数据 ====================
|
||||
|
||||
/** 提取参数类型列表 JSON 数组,如 ["com.xxx.ReqDTO"] */
|
||||
private String buildParamTypesJson(ApiDocModel.MethodInfo mi) {
|
||||
StringBuilder sb = new StringBuilder("[");
|
||||
if (mi.params != null) {
|
||||
boolean first = true;
|
||||
for (ApiDocModel.ParamInfo pi : mi.params) {
|
||||
if (!first) sb.append(",");
|
||||
String fqn = pi.fullQualifiedName != null && !pi.fullQualifiedName.isEmpty()
|
||||
? pi.fullQualifiedName : pi.type;
|
||||
sb.append("\"").append(escapeJson(fqn)).append("\"");
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** 根据请求字段生成示例 args JSON,如 [{"loanNo":"","seasonList":[...]}] */
|
||||
private String buildSampleArgsJson(List<ApiDocModel.FieldInfo> requestFields) {
|
||||
StringBuilder sb = new StringBuilder("[");
|
||||
if (requestFields != null && !requestFields.isEmpty()) {
|
||||
sb.append(fieldToSampleJson(requestFields));
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** 递归将字段列表转为示例 JSON 字符串 */
|
||||
private String fieldToSampleJson(List<ApiDocModel.FieldInfo> fields) {
|
||||
StringBuilder sb = new StringBuilder("{");
|
||||
for (int i = 0; i < fields.size(); i++) {
|
||||
if (i > 0) sb.append(",");
|
||||
ApiDocModel.FieldInfo f = fields.get(i);
|
||||
sb.append("\"").append(escapeJson(f.name)).append("\":");
|
||||
sb.append(sampleValueForField(f));
|
||||
}
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** 根据字段类型返回示例值 */
|
||||
private String sampleValueForField(ApiDocModel.FieldInfo f) {
|
||||
if (f.children != null && !f.children.isEmpty()) {
|
||||
// 如果类型是集合(如 List<DTO> / DTO[] / Set<DTO>),包裹一层数组
|
||||
if (isCollectionType(f.type)) {
|
||||
return "[" + fieldToSampleJson(f.children) + "]";
|
||||
}
|
||||
return fieldToSampleJson(f.children);
|
||||
}
|
||||
// 无 children 的纯集合字段 → 空数组
|
||||
if (isCollectionType(f.type)) {
|
||||
return "[]";
|
||||
}
|
||||
String type = f.type == null ? "" : f.type.trim();
|
||||
// 数字类型
|
||||
if ("int".equals(type) || "long".equals(type) || "short".equals(type) || "byte".equals(type)
|
||||
|| "double".equals(type) || "float".equals(type)
|
||||
|| "Integer".equals(type) || "Long".equals(type) || "Short".equals(type) || "Byte".equals(type)
|
||||
|| "Double".equals(type) || "Float".equals(type)
|
||||
|| "BigDecimal".equals(type) || "BigInteger".equals(type)) {
|
||||
return "0";
|
||||
}
|
||||
// 布尔
|
||||
if ("boolean".equals(type) || "Boolean".equals(type)) {
|
||||
return "false";
|
||||
}
|
||||
// 其他默认字符串
|
||||
return "\"\"";
|
||||
}
|
||||
|
||||
/** 判断 type 是否为集合/数组类型 */
|
||||
private boolean isCollectionType(String type) {
|
||||
if (type == null) return false;
|
||||
// 数组格式 DTO[]
|
||||
if (type.endsWith("[]")) return true;
|
||||
// 泛型集合 List<...>, Set<...>, Collection<...> 等
|
||||
if (type.startsWith("List<") || type.startsWith("Set<") || type.startsWith("Collection<")
|
||||
|| type.startsWith("ArrayList<") || type.startsWith("HashSet<")) return true;
|
||||
// 纯集合名(无泛型参数)
|
||||
String simple = type.trim();
|
||||
return "List".equals(simple) || "Set".equals(simple) || "Collection".equals(simple)
|
||||
|| "ArrayList".equals(simple) || "HashSet".equals(simple);
|
||||
}
|
||||
|
||||
/** JSON 字符串转义(双引号、反斜杠、换行等) */
|
||||
private String escapeJson(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
|
||||
/** HTML 属性值转义 */
|
||||
private String escapeAttr(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("&", "&").replace("\"", """).replace("<", "<").replace(">", ">");
|
||||
}
|
||||
|
||||
private String safeId(String s) {
|
||||
if (s == null) return "x";
|
||||
return s.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5_-]", "_");
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.example.workutils.biz.apidoc.generator;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* 本地 HTTP 代理,解决浏览器直连内网网关不通的问题。
|
||||
* 监听 localhost 随机端口,JS 请求本代理,由 Java 侧转发到 gw.test.bestpay.net。
|
||||
*
|
||||
* 单例,应用启动时创建,进程退出时自动回收。
|
||||
*/
|
||||
@Slf4j
|
||||
public class DubboDebugProxy {
|
||||
|
||||
private static volatile DubboDebugProxy INSTANCE;
|
||||
|
||||
private final int port;
|
||||
private final HttpServer server;
|
||||
|
||||
private DubboDebugProxy() {
|
||||
try {
|
||||
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
this.port = server.getAddress().getPort();
|
||||
server.createContext("/dubbo/invoke", this::handleInvoke);
|
||||
server.setExecutor(Executors.newFixedThreadPool(4));
|
||||
server.start();
|
||||
log.info("DubboDebugProxy 已启动: http://127.0.0.1:{}/dubbo/invoke", port);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("无法启动本地代理", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static DubboDebugProxy getInstance() {
|
||||
if (INSTANCE == null) {
|
||||
synchronized (DubboDebugProxy.class) {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new DubboDebugProxy();
|
||||
}
|
||||
}
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public String getInvokeUrl() {
|
||||
return "http://127.0.0.1:" + port + "/dubbo/invoke";
|
||||
}
|
||||
|
||||
private void handleInvoke(HttpExchange exchange) throws IOException {
|
||||
// CORS 头
|
||||
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
|
||||
exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "POST, OPTIONS");
|
||||
exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "*");
|
||||
|
||||
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
exchange.sendResponseHeaders(204, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
exchange.sendResponseHeaders(405, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取请求体
|
||||
byte[] body = readAllBytes(exchange.getRequestBody());
|
||||
|
||||
// 从 header 中取网关地址(JS 传自定义 header),默认走测试环境
|
||||
String gwUrl = exchange.getRequestHeaders().getFirst("X-Gateway-Url");
|
||||
if (gwUrl == null || gwUrl.isEmpty()) {
|
||||
gwUrl = "https://gw.test.bestpay.net/open/1.0/GenericFacade/genericInvoke";
|
||||
}
|
||||
|
||||
// 透传所有请求头到上游(排除 proxy 专用 header)
|
||||
Set<String> skipHeaders = new HashSet<>(Arrays.asList(
|
||||
"X-gateway-url", "Host", "Connection", "Transfer-encoding", "Content-length"));
|
||||
Map<String, List<String>> reqHeaders = exchange.getRequestHeaders();
|
||||
|
||||
try {
|
||||
// 转发到网关
|
||||
URI uri = URI.create(gwUrl);
|
||||
HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
conn.setConnectTimeout(15_000);
|
||||
conn.setReadTimeout(30_000);
|
||||
|
||||
// 透传所有 header
|
||||
for (Map.Entry<String, List<String>> entry : reqHeaders.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (key != null && !skipHeaders.contains(key.toLowerCase())) {
|
||||
for (String val : entry.getValue()) {
|
||||
conn.addRequestProperty(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try (OutputStream os = conn.getOutputStream()) {
|
||||
os.write(body);
|
||||
os.flush();
|
||||
}
|
||||
|
||||
int status = conn.getResponseCode();
|
||||
InputStream is = status >= 400 ? conn.getErrorStream() : conn.getInputStream();
|
||||
byte[] respBody = is != null ? readAllBytes(is) : new byte[0];
|
||||
|
||||
// 原样返回
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
exchange.sendResponseHeaders(status, respBody.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(respBody);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("代理转发失败: {} {}", gwUrl, e.getMessage());
|
||||
String err = "{\"error\":\"" + escapeJson(e.getMessage()) + "\"}";
|
||||
byte[] errBytes = err.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(502, errBytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(errBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream is) throws IOException {
|
||||
byte[] buf = new byte[8192];
|
||||
java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream();
|
||||
int n;
|
||||
while ((n = is.read(buf)) != -1) {
|
||||
bos.write(buf, 0, n);
|
||||
}
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
private static String escapeJson(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.example.workutils.biz.apidoc.generator;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* API 文档资源加载器。
|
||||
@@ -45,9 +45,19 @@ public class HtmlResourceLoader {
|
||||
return loadResource(RESOURCE_BASE + "api-doc.js");
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 html2canvas 库(本地打包,无外部 CDN 依赖)。
|
||||
* 对应资源文件:resources/templates/apidoc/html2canvas.min.js
|
||||
*/
|
||||
public String loadHtml2Canvas() {
|
||||
return loadResource(RESOURCE_BASE + "html2canvas.min.js");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 classpath 读取资源文件内容。
|
||||
* 所有资源必须以 UTF-8 编码。
|
||||
* 使用 ByteArrayOutputStream 替代 Scanner,避免 \\A 分隔符在不同平台上的兼容性问题。
|
||||
* 同时自动剥离 UTF-8 BOM 头,防止嵌入到 <script> 标签中导致 JS 解析失败。
|
||||
*/
|
||||
private String loadResource(String path) {
|
||||
ClassLoader cl = getClass().getClassLoader();
|
||||
@@ -58,12 +68,25 @@ public class HtmlResourceLoader {
|
||||
if (is == null) {
|
||||
throw new IOException("资源文件未找到: " + path);
|
||||
}
|
||||
try (Scanner scanner = new Scanner(is, StandardCharsets.UTF_8.name())) {
|
||||
scanner.useDelimiter("\\A");
|
||||
return scanner.hasNext() ? scanner.next() : "";
|
||||
byte[] bytes = readAllBytes(is);
|
||||
String content = new String(bytes, StandardCharsets.UTF_8);
|
||||
// 剥离 UTF-8 BOM(\uFEFF),避免在 <script> 标签中导致 JS 解析失败
|
||||
if (!content.isEmpty() && content.charAt(0) == '\uFEFF') {
|
||||
content = content.substring(1);
|
||||
}
|
||||
return content;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("加载资源失败: " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream is) throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
int n;
|
||||
while ((n = is.read(buf)) != -1) {
|
||||
bos.write(buf, 0, n);
|
||||
}
|
||||
return bos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ body{font-family:'Microsoft YaHei',-apple-system,sans-serif;color:#1d1d1f;backgr
|
||||
.nav-group-title.open .arrow{transform:rotate(90deg)}
|
||||
.nav-group-title small{font-weight:400;color:#86909c;font-size:11px}
|
||||
.nav-group-items{overflow:hidden;max-height:0;transition:max-height .3s ease}
|
||||
.nav-group-title.open+.nav-group-items{max-height:2000px}
|
||||
.nav-group-title.open+.nav-group-items{max-height:99999px}
|
||||
.nav-item{display:flex;align-items:center;gap:8px;padding:6px 16px 6px 32px;font-size:12px;color:#4e5969;text-decoration:none;transition:.15s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.nav-item:hover,.nav-item.active{color:#1677ff;background:#e8f3ff}
|
||||
.nav-item-icon{font-size:14px;flex-shrink:0}
|
||||
@@ -182,6 +182,57 @@ body{font-family:'Microsoft YaHei',-apple-system,sans-serif;color:#1d1d1f;backgr
|
||||
.api-card.search-hidden,.nav-item.search-hidden,.nav-group.search-hidden{display:none!important}
|
||||
.method-card.search-hidden{display:none!important}
|
||||
.search-highlight{background:#fff3cd;padding:1px 2px;border-radius:2px}
|
||||
|
||||
/* ===== Dubbo 调试按钮 ===== */
|
||||
.method-copy-btn.dubbo{background:#722ed1;color:#fff;border-color:#722ed1}
|
||||
.method-copy-btn.dubbo:hover{background:#531dab}
|
||||
|
||||
/* ===== Dubbo 调试弹窗(全屏双栏) ===== */
|
||||
.dubbo-modal-mask{position:fixed;inset:0;background:rgb(0 0 0/40%);z-index:9999;display:flex}
|
||||
.dubbo-modal{background:#fff;width:100vw;height:100vh;display:flex;flex-direction:column;border-radius:0;box-shadow:none}
|
||||
.dubbo-modal-header{display:flex;align-items:center;justify-content:space-between;padding:10px 20px;border-bottom:1px solid #e5e6eb;flex-shrink:0;background:#fafbfc}
|
||||
.dubbo-modal-title{font-size:15px;font-weight:700;color:#1d2129}
|
||||
.dubbo-modal-close{width:28px;height:28px;border:none;background:#f2f3f5;border-radius:4px;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#4e5969}
|
||||
.dubbo-modal-close:hover{background:#e5e6eb}
|
||||
|
||||
/* 双栏主体 */
|
||||
.dubbo-modal-body{flex:1;display:flex;overflow:hidden;min-height:0}
|
||||
.dubbo-modal-left{flex:1;overflow-y:auto;padding:16px 20px;min-width:0;display:flex;flex-direction:column}
|
||||
.dubbo-modal-right{width:42%;min-width:360px;max-width:600px;border-left:1px solid #e5e6eb;display:flex;flex-direction:column;background:#fafbfc;overflow:hidden}
|
||||
|
||||
.dubbo-modal-body label{display:block;font-size:13px;font-weight:600;color:#4e5969;margin-bottom:4px}
|
||||
.dubbo-modal-body label .hint{font-weight:400;color:#86909c}
|
||||
.dubbo-modal-body input,.dubbo-modal-body textarea,.dubbo-modal-body select{width:100%;padding:8px 12px;border:1px solid #d0d5dd;border-radius:4px;font-size:13px;font-family:'Consolas','Courier New',monospace;box-sizing:border-box}
|
||||
.dubbo-modal-body textarea{resize:vertical;min-height:120px;line-height:1.5}
|
||||
.dubbo-modal-body input:focus,.dubbo-modal-body textarea:focus,.dubbo-modal-body select:focus{outline:none;border-color:#1677ff;box-shadow:0 0 0 2px rgb(22 119 255/15%)}
|
||||
.dubbo-field-row{display:flex;gap:12px;margin-bottom:12px}
|
||||
.dubbo-field-row>div{flex:1;min-width:0}
|
||||
|
||||
/* 右栏响应区域 */
|
||||
.dubbo-response-panel{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
||||
.dubbo-response-panel-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #e5e6eb;font-size:12px;background:#fff;flex-shrink:0}
|
||||
.dubbo-response-panel-empty{flex:1;display:flex;align-items:center;justify-content:center;color:#c9cdd4;font-size:14px}
|
||||
.dubbo-response-status{font-weight:700}
|
||||
.dubbo-response-status.ok{color:#00b42a}
|
||||
.dubbo-response-status.err{color:#f53f3f}
|
||||
.dubbo-response-content{flex:1;overflow-y:auto;padding:12px;font-size:12px;font-family:'Consolas','Courier New',monospace;white-space:pre-wrap;word-break:break-all;line-height:1.6;background:#fff;border-top:1px solid #e5e6eb}
|
||||
.dubbo-response-content-time{font-size:11px;color:#86909c;padding:4px 12px;background:#fafbfc;border-top:1px solid #f2f3f5}
|
||||
|
||||
/* 底部按钮栏 */
|
||||
.dubbo-modal-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:10px 20px;border-top:1px solid #e5e6eb;flex-shrink:0;background:#fafbfc}
|
||||
.dubbo-btn{padding:8px 20px;border-radius:4px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid #d0d5dd;background:#fff;color:#4e5969;transition:.15s}
|
||||
.dubbo-btn:hover{background:#f7f8fa}
|
||||
.dubbo-btn.primary{background:#722ed1;color:#fff;border-color:#722ed1}
|
||||
.dubbo-btn.primary:hover{background:#531dab}
|
||||
.dubbo-btn.send{background:#1677ff;color:#fff;border-color:#1677ff}
|
||||
.dubbo-btn.send:hover{background:#0958d9}
|
||||
.dubbo-btn.send:disabled{background:#94bfff;border-color:#94bfff;cursor:not-allowed}
|
||||
.dubbo-spinner{display:inline-block;width:14px;height:14px;border:2px solid #d0d5dd;border-top-color:#1677ff;border-radius:50%;animation:dubboSpin .6s linear infinite;vertical-align:middle;margin-right:4px}
|
||||
@keyframes dubboSpin{to{transform:rotate(360deg)}}
|
||||
|
||||
/* 响应式——窄屏时隐藏右栏 */
|
||||
@media(max-width:800px){.dubbo-modal-right{display:none}}
|
||||
|
||||
.search-match{box-shadow:0 0 0 2px rgba(22,119,255,.25)}
|
||||
|
||||
/* ===== 响应式 ===== */
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
<style>
|
||||
{{CSS}}
|
||||
</style>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
||||
<script>var DUBBO_PROXY_URL='{{PROXY_URL}}';</script>
|
||||
<script>{{HTML2CANVAS}}</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
@@ -270,3 +270,818 @@ document.querySelectorAll('.api-card .api-body, .method-body').forEach(function(
|
||||
e.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
* Dubbo 接口调试
|
||||
* ============================================================ */
|
||||
|
||||
/** 默认网关 URL */
|
||||
var DUBBO_GW_URL_KEY = 'dubbo_gw_url';
|
||||
function getGwUrl() {
|
||||
return localStorage.getItem(DUBBO_GW_URL_KEY) || 'https://gw.test.bestpay.net/open/1.0/GenericFacade/genericInvoke';
|
||||
}
|
||||
function setGwUrl(url) { localStorage.setItem(DUBBO_GW_URL_KEY, url); }
|
||||
|
||||
/** 默认 header */
|
||||
function defaultHeaders() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'ApiDoc-Debugger/1.0',
|
||||
'Accept': '*/*'
|
||||
};
|
||||
}
|
||||
|
||||
/** 构建 eventContext 的值 — 注意内外 env 是不同概念 */
|
||||
function buildEventContext(tntId, env, evtEnv) {
|
||||
return JSON.stringify({
|
||||
tntId:tntId||'0101', env:evtEnv||env||'DEV', arNo:'', evtCd:'', pdCd:'', pdPath:''
|
||||
});
|
||||
}
|
||||
|
||||
/** 根据 env 下拉选中值计算默认 eventContext.env */
|
||||
function getDefaultEvtEnv(envVal) {
|
||||
if (envVal === 'DEV') return 'DEV_1156339215001361525';
|
||||
if (envVal === 'SIT') {
|
||||
var now = new Date();
|
||||
var y = now.getFullYear();
|
||||
var m = pad2(now.getMonth() + 1);
|
||||
var d = pad2(now.getDate());
|
||||
return 'SIT-' + y + m + d + '01';
|
||||
}
|
||||
// STB 或其他 → 空
|
||||
return '';
|
||||
}
|
||||
|
||||
/** env 下拉变更时自动填充 eventContext.env */
|
||||
function onEnvChange() {
|
||||
var envEl = document.getElementById('dubboEnv');
|
||||
var evtEl = document.getElementById('dubboEvtEnv');
|
||||
if (!envEl || !evtEl) return;
|
||||
evtEl.value = getDefaultEvtEnv(envEl.value);
|
||||
}
|
||||
|
||||
/** 打开 Dubbo 调试弹窗 */
|
||||
function openDubboDebug(card) {
|
||||
window._dubboActiveCard = card;
|
||||
var iface = card.getAttribute('data-dubbo-interface');
|
||||
var method = card.getAttribute('data-dubbo-method');
|
||||
var paramTypes = card.getAttribute('data-dubbo-param-types') || '[]';
|
||||
var sampleArgs = card.getAttribute('data-dubbo-sample-args') || '[{}]';
|
||||
var stateKey = getDubboStateKey(iface, method);
|
||||
var saved = loadDubboState(stateKey);
|
||||
|
||||
var gwUrl = (saved && saved.gwUrl) ? saved.gwUrl : getGwUrl();
|
||||
var argsVal = (saved && saved.args) ? saved.args : formatJsonPretty(sampleArgs);
|
||||
var tntVal = (saved && saved.tntId) ? saved.tntId : '0101';
|
||||
var envVal = (saved && saved.env) ? saved.env : 'DEV';
|
||||
var appVal = (saved && saved.appId) ? saved.appId : '980010000114002';
|
||||
var evtEnvVal = (saved && saved.evtEnv) ? saved.evtEnv : getDefaultEvtEnv(envVal);
|
||||
|
||||
var envOpts = ['DEV','SIT','STB'].map(function(v) {
|
||||
return '<option' + (v === envVal ? ' selected' : '') + '>' + v + '</option>';
|
||||
}).join('');
|
||||
|
||||
var lastRespHtml = '';
|
||||
if (saved && saved.lastResponse) {
|
||||
lastRespHtml = '<div class="dubbo-response-panel" id="dubboResponsePanel">' +
|
||||
'<div class="dubbo-response-panel-header"><span>响应结果</span>' +
|
||||
'<span class="dubbo-response-status ' + (saved.lastResponse.ok ? 'ok' : 'err') + '" id="dubboStatus">' +
|
||||
escHtml(saved.lastResponse.status) + '</span></div>' +
|
||||
(saved.lastResponse.time ? '<div class="dubbo-response-content-time">📅 ' + escHtml(saved.lastResponse.time) + '</div>' : '') +
|
||||
'<pre class="dubbo-response-content" id="dubboRespContent">' + escHtml(saved.lastResponse.body) + '</pre></div>';
|
||||
} else {
|
||||
lastRespHtml = '<div class="dubbo-response-panel" id="dubboResponsePanel">' +
|
||||
'<div class="dubbo-response-panel-header"><span>响应结果</span><span class="dubbo-response-status" id="dubboStatus"></span></div>' +
|
||||
'<div class="dubbo-response-panel-empty">点击发送请求查看结果</div></div>';
|
||||
}
|
||||
|
||||
var html = '<div class="dubbo-modal-mask" id="dubboMask" onclick="if(event.target===this)closeDubboDebug()">' +
|
||||
'<div class="dubbo-modal">' +
|
||||
'<div class="dubbo-modal-header">' +
|
||||
'<span class="dubbo-modal-title">🔧 ' + escHtml(method) + ' — 接口调试</span>' +
|
||||
'<button class="dubbo-modal-close" onclick="closeDubboDebug()">✕</button>' +
|
||||
'</div>' +
|
||||
'<div class="dubbo-modal-body">' +
|
||||
// 左栏:表单
|
||||
'<div class="dubbo-modal-left">' +
|
||||
'<div class="dubbo-field-row">' +
|
||||
'<div><label>接口 <span class="hint">interfacePath</span></label><input value="' + escHtml(iface) + '" readonly style="background:#f7f8fa;color:#86909c"></div>' +
|
||||
'<div style="max-width:280px"><label>方法 <span class="hint">method</span></label><input value="' + escHtml(method) + '" readonly style="background:#f7f8fa;color:#86909c"></div>' +
|
||||
'</div>' +
|
||||
'<div class="dubbo-field-row"><div><label>参数类型 <span class="hint">parameterTypes</span></label><input value="' + escHtml(paramTypes) + '" readonly style="background:#f7f8fa;color:#86909c"></div></div>' +
|
||||
'<div style="flex:1;display:flex;flex-direction:column;min-height:0;margin-bottom:12px">' +
|
||||
'<label>请求参数 <span class="hint">args</span>' +
|
||||
' <button class="dubbo-btn" style="padding:2px 8px;font-size:11px" onclick="formatDubboArgs()">格式化</button>' +
|
||||
' <button class="dubbo-btn" style="padding:2px 8px;font-size:11px" onclick="resetDubboArgs()" title="恢复默认示例">重置</button>' +
|
||||
' <button class="dubbo-btn" style="padding:2px 8px;font-size:11px" onclick="showLogExtractPopup()" title="从日志提取入参">📋 日志提取</button></label>' +
|
||||
'<textarea id="dubboArgs" spellcheck="false" style="flex:1;min-height:200px">' + escHtml(argsVal) + '</textarea>' +
|
||||
'</div>' +
|
||||
'<div class="dubbo-field-row">' +
|
||||
'<div><label>网关 URL</label><input id="dubboGwUrl" value="' + escHtml(gwUrl) + '"></div>' +
|
||||
'</div>' +
|
||||
'<div class="dubbo-field-row">' +
|
||||
'<div style="max-width:140px"><label>tnt_id</label><input id="dubboTntId" value="' + escHtml(tntVal) + '"></div>' +
|
||||
'<div style="max-width:140px"><label>env</label><select id="dubboEnv" onchange="onEnvChange()">' + envOpts + '</select></div>' +
|
||||
'<div style="max-width:160px"><label>app_id</label><input id="dubboAppId" value="' + escHtml(appVal) + '"></div>' +
|
||||
'<div style="max-width:220px"><label>eventContext.env <span class="hint">实例ID</span></label><input id="dubboEvtEnv" value="' + escHtml(evtEnvVal) + '" placeholder="如 DEV_XQYFGL170183"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
// 右栏:响应
|
||||
'<div class="dubbo-modal-right">' +
|
||||
lastRespHtml +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="dubbo-modal-footer">' +
|
||||
'<button class="dubbo-btn" onclick="copyDubboCurl()">📋 复制 cURL</button>' +
|
||||
'<button class="dubbo-btn" onclick="copyDubboBody()">📄 复制请求体</button>' +
|
||||
'<button class="dubbo-btn send" id="dubboSendBtn" onclick="sendDubboRequest()">▶ 发送请求</button>' +
|
||||
'</div>' +
|
||||
'</div></div>';
|
||||
|
||||
// 移除已有弹窗
|
||||
var old = document.getElementById('dubboMask');
|
||||
if (old) old.remove();
|
||||
document.body.insertAdjacentHTML('beforeend', html);
|
||||
|
||||
// 如果有保存的响应结果,展开显示
|
||||
if (saved && saved.lastResponse) {
|
||||
var respContent = document.getElementById('dubboRespContent');
|
||||
if (respContent) { respContent.style.display = ''; }
|
||||
var emptyEl = document.querySelector('.dubbo-response-panel-empty');
|
||||
if (emptyEl) { emptyEl.style.display = 'none'; }
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭弹窗 */
|
||||
function closeDubboDebug() {
|
||||
var mask = document.getElementById('dubboMask');
|
||||
if (mask) mask.remove();
|
||||
}
|
||||
|
||||
/** localStorage key */
|
||||
function getDubboStateKey(iface, method) {
|
||||
return 'dubbo_debug_' + (iface||'') + '_' + (method||'');
|
||||
}
|
||||
|
||||
/** 保存当前调试状态到 localStorage */
|
||||
function saveDubboState() {
|
||||
var card = getActiveDubboCard();
|
||||
if (!card) return;
|
||||
var iface = card.getAttribute('data-dubbo-interface');
|
||||
var method = card.getAttribute('data-dubbo-method');
|
||||
var key = getDubboStateKey(iface, method);
|
||||
var state = {
|
||||
gwUrl: (document.getElementById('dubboGwUrl') || {}).value || '',
|
||||
args: (document.getElementById('dubboArgs') || {}).value || '',
|
||||
tntId: (document.getElementById('dubboTntId') || {}).value || '',
|
||||
env: (document.getElementById('dubboEnv') || {}).value || '',
|
||||
appId: (document.getElementById('dubboAppId') || {}).value || '',
|
||||
evtEnv: (document.getElementById('dubboEvtEnv') || {}).value || ''
|
||||
};
|
||||
// 保留已有的响应结果(如果有)
|
||||
var existing = loadDubboState(key);
|
||||
if (existing && existing.lastResponse) {
|
||||
state.lastResponse = existing.lastResponse;
|
||||
}
|
||||
try { localStorage.setItem(key, JSON.stringify(state)); } catch(e) {}
|
||||
}
|
||||
|
||||
/** 读取调试状态 */
|
||||
function loadDubboState(key) {
|
||||
try {
|
||||
var raw = localStorage.getItem(key);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch(e) { return null; }
|
||||
}
|
||||
|
||||
/** 保存响应结果 */
|
||||
function saveDubboResponse(statusText, body, ok) {
|
||||
var card = getActiveDubboCard();
|
||||
if (!card) return;
|
||||
var iface = card.getAttribute('data-dubbo-interface');
|
||||
var method = card.getAttribute('data-dubbo-method');
|
||||
var key = getDubboStateKey(iface, method);
|
||||
var state = loadDubboState(key) || {};
|
||||
state.lastResponse = { status: statusText, body: body, ok: ok, time: formatLocalDateTime() };
|
||||
// 同时保存当前表单字段
|
||||
state.gwUrl = (document.getElementById('dubboGwUrl') || {}).value || state.gwUrl || '';
|
||||
state.args = (document.getElementById('dubboArgs') || {}).value || state.args || '';
|
||||
state.tntId = (document.getElementById('dubboTntId') || {}).value || state.tntId || '';
|
||||
state.env = (document.getElementById('dubboEnv') || {}).value || state.env || '';
|
||||
state.appId = (document.getElementById('dubboAppId') || {}).value || state.appId || '';
|
||||
state.evtEnv = (document.getElementById('dubboEvtEnv') || {}).value || state.evtEnv || '';
|
||||
try { localStorage.setItem(key, JSON.stringify(state)); } catch(e) {}
|
||||
}
|
||||
|
||||
/** 重置 args 为默认示例 */
|
||||
function resetDubboArgs() {
|
||||
var card = getActiveDubboCard();
|
||||
if (!card) return;
|
||||
var sampleArgs = card.getAttribute('data-dubbo-sample-args') || '[{}]';
|
||||
var ta = document.getElementById('dubboArgs');
|
||||
if (ta) ta.value = formatJsonPretty(sampleArgs);
|
||||
}
|
||||
|
||||
// 去除 JSON 中的单行和多行注释(兼容 Apifox 等工具复制出来的格式)
|
||||
function stripJsonComments(jsonStr) {
|
||||
if (!jsonStr) return '';
|
||||
return jsonStr
|
||||
.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, function(m, g) { return g ? '' : m; })
|
||||
.replace(/,\s*\}/g, '}').replace(/,\s*\]/g, ']'); // 去掉尾部逗号
|
||||
}
|
||||
|
||||
/** 格式化 JSON */
|
||||
function formatJsonPretty(jsonStr) {
|
||||
try {
|
||||
var obj = JSON.parse(stripJsonComments(jsonStr));
|
||||
return JSON.stringify(obj, null, 2);
|
||||
} catch(e) { return jsonStr; }
|
||||
}
|
||||
|
||||
/** 对 textarea 中的 JSON 进行格式化 */
|
||||
function formatDubboArgs() {
|
||||
var ta = document.getElementById('dubboArgs');
|
||||
if (!ta) return;
|
||||
ta.value = formatJsonPretty(ta.value);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 日志提取功能 — 从日志中解析 DTO 参数填入调试面板
|
||||
* ============================================================ */
|
||||
|
||||
/** 显示日志提取弹窗 */
|
||||
function showLogExtractPopup() {
|
||||
var old = document.getElementById('logExtractOverlay');
|
||||
if (old) old.remove();
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'logExtractOverlay';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:100000;display:flex;align-items:center;justify-content:center';
|
||||
overlay.innerHTML =
|
||||
'<div style="background:#fff;border-radius:8px;box-shadow:0 8px 40px rgba(0,0,0,.2);width:680px;max-width:92vw;max-height:85vh;display:flex;flex-direction:column">' +
|
||||
'<div style="padding:14px 20px;border-bottom:1px solid #e5e6eb;display:flex;align-items:center;justify-content:space-between">' +
|
||||
'<span style="font-size:15px;font-weight:600">📋 从日志提取入参</span>' +
|
||||
'<button onclick="closeLogExtractPopup()" style="background:none;border:none;font-size:22px;cursor:pointer;color:#86909c;padding:0;line-height:1">×</button>' +
|
||||
'</div>' +
|
||||
'<div style="padding:16px 20px;flex:1;display:flex;flex-direction:column;min-height:0">' +
|
||||
'<div style="color:#86909c;font-size:12px;margin-bottom:8px;line-height:1.6">粘贴日志内容,自动识别 <code style="background:#f2f3f5;padding:1px 6px;border-radius:3px">接口入参:ClassName(field=value,...)</code> 格式,<br>也支持只粘贴 DTO 参数部分。</div>' +
|
||||
'<textarea id="logExtractInput" placeholder="示例:QuerySplitAccountInfoReqDTO(loanNo=211254457192, seasonList=[QuerySplitAccountInfoReqDTO.Season(season=202305, subjectList=[...])])" style="flex:1;min-height:200px;border:1px solid #d0d5dd;border-radius:6px;padding:10px 12px;font-family:Consolas,Monaco,monospace;font-size:12px;resize:vertical;outline:none;line-height:1.6" spellcheck="false"></textarea>' +
|
||||
'<div style="margin-top:12px;display:flex;gap:8px">' +
|
||||
'<button onclick="doExtractLogArgs()" style="background:#165DFF;color:#fff;border:none;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer">🔄 解析并比对</button>' +
|
||||
'<button onclick="closeLogExtractPopup()" style="background:#f2f3f5;border:1px solid #e5e6eb;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;color:#4e5969">取消</button>' +
|
||||
'</div>' +
|
||||
'</div></div>';
|
||||
document.body.appendChild(overlay);
|
||||
overlay.addEventListener('click', function(e) { if (e.target === overlay) closeLogExtractPopup(); });
|
||||
setTimeout(function() { var ta = document.getElementById('logExtractInput'); if (ta) ta.focus(); }, 100);
|
||||
}
|
||||
|
||||
function closeLogExtractPopup() {
|
||||
var el = document.getElementById('logExtractOverlay');
|
||||
if (el) el.remove();
|
||||
}
|
||||
|
||||
/** 执行日志解析 → 弹出比对窗口 */
|
||||
function doExtractLogArgs() {
|
||||
var raw = (document.getElementById('logExtractInput') || {}).value;
|
||||
if (!raw || !raw.trim()) { showToast('⚠ 请粘贴日志内容'); return; }
|
||||
raw = raw.trim();
|
||||
var dtoText = raw;
|
||||
var markers = ['接口入参:', '接口入参:', '入参:', '入参:'];
|
||||
for (var i = 0; i < markers.length; i++) {
|
||||
var idx = raw.lastIndexOf(markers[i]);
|
||||
if (idx >= 0) { dtoText = raw.substring(idx + markers[i].length).trim(); break; }
|
||||
}
|
||||
try {
|
||||
var logObj = flattenSuperChain(parseDtoExpr(dtoText));
|
||||
var card = getActiveDubboCard();
|
||||
var sampleObj = {};
|
||||
if (card) {
|
||||
try {
|
||||
var sampleArr = JSON.parse(card.getAttribute('data-dubbo-sample-args') || '[{}]');
|
||||
if (Array.isArray(sampleArr) && sampleArr.length > 0) sampleObj = sampleArr[0];
|
||||
} catch (ignored) {}
|
||||
}
|
||||
closeLogExtractPopup();
|
||||
showParamComparePopup(sampleObj, logObj);
|
||||
} catch(e) {
|
||||
showToast('❌ 解析失败:' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/** 参数比对弹窗:默认参考值 vs 日志提取值 */
|
||||
function showParamComparePopup(sampleObj, logObj) {
|
||||
var old = document.getElementById('paramCompareOverlay');
|
||||
if (old) old.remove();
|
||||
|
||||
var diffs = diffObjects(sampleObj, logObj, '');
|
||||
var changedCount = 0, sampleOnlyCount = 0;
|
||||
diffs.forEach(function(d) {
|
||||
if (d.status === 'changed') changedCount++;
|
||||
else if (d.status === 'sample_only') sampleOnlyCount++;
|
||||
});
|
||||
|
||||
var rowsHtml = '';
|
||||
diffs.forEach(function(d, idx) {
|
||||
var badge, rowStyle;
|
||||
if (d.status === 'changed') {
|
||||
badge = '<span style="background:#fff7e6;color:#d46b08;padding:1px 8px;border-radius:3px;font-size:11px;font-weight:500">变更</span>';
|
||||
rowStyle = 'background:#fffbe6';
|
||||
} else if (d.status === 'sample_only') {
|
||||
badge = '<span style="background:#e6f7ff;color:#096dd9;padding:1px 8px;border-radius:3px;font-size:11px;font-weight:500">仅默认</span>';
|
||||
rowStyle = 'background:#f0f8ff';
|
||||
} else if (d.status === 'log_only') {
|
||||
badge = '<span style="background:#f6ffed;color:#389e0d;padding:1px 8px;border-radius:3px;font-size:11px;font-weight:500">仅日志</span>';
|
||||
rowStyle = 'background:#f6ffed';
|
||||
} else {
|
||||
badge = '<span style="color:#bbb;font-size:11px">一致</span>';
|
||||
rowStyle = '';
|
||||
}
|
||||
var sampleDisplay = d.sampleVal === undefined ? '<span style="color:#bbb">—</span>' : escHtml(fmtDiffVal(d.sampleVal));
|
||||
var logDisplay = d.logVal === undefined ? '<span style="color:#bbb">—</span>' : escHtml(fmtDiffVal(d.logVal));
|
||||
rowsHtml += '<tr style="' + rowStyle + '">' +
|
||||
'<td style="padding:5px 10px;font-size:11px">' + badge + '</td>' +
|
||||
'<td style="padding:5px 10px;font-family:Consolas,Monaco,monospace;font-size:12px;max-width:300px;word-break:break-all">' + escHtml(d.path || '(根)') + '</td>' +
|
||||
'<td style="padding:5px 10px;font-family:Consolas,Monaco,monospace;font-size:12px;max-width:280px;word-break:break-all;' + (d.status==='changed'?'color:#d46b08':'') + '">' + sampleDisplay + '</td>' +
|
||||
'<td style="padding:5px 10px;font-family:Consolas,Monaco,monospace;font-size:12px;max-width:280px;word-break:break-all;' + (d.status==='changed'?'color:#389e0d;font-weight:500':'') + '">' + logDisplay + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'paramCompareOverlay';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:100001;display:flex;align-items:center;justify-content:center';
|
||||
overlay.innerHTML =
|
||||
'<div style="background:#fff;border-radius:10px;box-shadow:0 12px 60px rgba(0,0,0,.25);width:1100px;max-width:96vw;max-height:90vh;display:flex;flex-direction:column">' +
|
||||
'<div style="padding:16px 24px;border-bottom:1px solid #e5e6eb;display:flex;align-items:center;justify-content:space-between;flex-shrink:0">' +
|
||||
'<div><span style="font-size:16px;font-weight:600">🔄 参数比对 — 默认值 vs 日志值</span>' +
|
||||
'<span style="margin-left:12px;font-size:12px;color:#86909c">' +
|
||||
(changedCount > 0 ? ' <span style="color:#d46b08;font-weight:500">' + changedCount + ' 项变更</span>' : '') +
|
||||
(sampleOnlyCount > 0 ? ' <span style="color:#096dd9;font-weight:500;margin-left:8px">' + sampleOnlyCount + ' 项仅默认</span>' : '') +
|
||||
'</span></div>' +
|
||||
'<button onclick="closeParamComparePopup()" style="background:none;border:none;font-size:22px;cursor:pointer;color:#86909c;padding:0;line-height:1">×</button>' +
|
||||
'</div>' +
|
||||
'<div style="flex:1;overflow:auto;min-height:0">' +
|
||||
'<table style="width:100%;border-collapse:collapse">' +
|
||||
'<thead><tr style="position:sticky;top:0;background:#fafafa;z-index:1">' +
|
||||
'<th style="text-align:left;padding:8px 10px;font-size:12px;color:#86909c;border-bottom:2px solid #e5e6eb;width:60px">状态</th>' +
|
||||
'<th style="text-align:left;padding:8px 10px;font-size:12px;color:#86909c;border-bottom:2px solid #e5e6eb">参数路径</th>' +
|
||||
'<th style="text-align:left;padding:8px 10px;font-size:12px;color:#86909c;border-bottom:2px solid #e5e6eb">默认参考值</th>' +
|
||||
'<th style="text-align:left;padding:8px 10px;font-size:12px;color:#86909c;border-bottom:2px solid #e5e6eb">日志提取值</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody>' + rowsHtml + '</tbody>' +
|
||||
'</table>' +
|
||||
(diffs.length === 0 ? '<div style="text-align:center;padding:40px;color:#86909c">无差异,参数完全一致</div>' : '') +
|
||||
'</div>' +
|
||||
'<div style="padding:14px 24px;border-top:1px solid #e5e6eb;display:flex;gap:10px;flex-shrink:0">' +
|
||||
'<button onclick="applyCompareResult(\'merged\')" style="background:#165DFF;color:#fff;border:none;border-radius:6px;padding:8px 22px;font-size:13px;cursor:pointer">✅ 使用合并值(默认 + 日志)</button>' +
|
||||
'<button onclick="applyCompareResult(\'log\')" style="background:#f2f3f5;border:1px solid #d0d5dd;border-radius:6px;padding:8px 22px;font-size:13px;cursor:pointer;color:#4e5969">📋 仅使用日志值</button>' +
|
||||
'<button onclick="closeParamComparePopup()" style="background:#fff;border:1px solid #d0d5dd;border-radius:6px;padding:8px 22px;font-size:13px;cursor:pointer;color:#4e5969;margin-left:auto">取消</button>' +
|
||||
'</div></div>';
|
||||
document.body.appendChild(overlay);
|
||||
overlay.addEventListener('click', function(e) { if (e.target === overlay) closeParamComparePopup(); });
|
||||
|
||||
// 暂存原始对象
|
||||
overlay._sampleObj = sampleObj;
|
||||
overlay._logObj = logObj;
|
||||
overlay._diffCount = diffs.length;
|
||||
}
|
||||
|
||||
function closeParamComparePopup() {
|
||||
var el = document.getElementById('paramCompareOverlay');
|
||||
if (el) el.remove();
|
||||
}
|
||||
|
||||
/** 比对结果应用 */
|
||||
var _compareResultLogObj = null, _compareResultSampleObj = null;
|
||||
function applyCompareResult(mode) {
|
||||
var overlay = document.getElementById('paramCompareOverlay');
|
||||
var sampleObj = overlay._sampleObj || {};
|
||||
var logObj = overlay._logObj || {};
|
||||
var finalObj;
|
||||
if (mode === 'merged') {
|
||||
finalObj = deepMerge(sampleObj, logObj);
|
||||
} else {
|
||||
finalObj = logObj;
|
||||
}
|
||||
var json = JSON.stringify([finalObj], null, 2);
|
||||
var ta = document.getElementById('dubboArgs');
|
||||
if (ta) { ta.value = json; showToast('✅ 参数已填入(共 ' + (overlay._diffCount || 0) + ' 个字段参与比对)'); }
|
||||
closeParamComparePopup();
|
||||
}
|
||||
|
||||
/** 递归对比两个对象,返回差异列表 [{path, sampleVal, logVal, status}] */
|
||||
function diffObjects(sample, log, path) {
|
||||
var results = [];
|
||||
var sampleIsObj = sample && typeof sample === 'object' && !Array.isArray(sample);
|
||||
var logIsObj = log && typeof log === 'object' && !Array.isArray(log);
|
||||
|
||||
if (!sampleIsObj && !logIsObj) {
|
||||
// 都不是对象,直接比较值
|
||||
var sv = sample;
|
||||
var lv = log;
|
||||
if (sv === undefined && lv !== undefined) results.push({path: path, sampleVal: undefined, logVal: lv, status: 'log_only'});
|
||||
else if (lv === undefined && sv !== undefined) results.push({path: path, sampleVal: sv, logVal: undefined, status: 'sample_only'});
|
||||
else if (!eqVal(sv, lv)) results.push({path: path, sampleVal: sv, logVal: lv, status: 'changed'});
|
||||
return results;
|
||||
}
|
||||
|
||||
// 处理:一个对象一个非对象
|
||||
if (sampleIsObj !== logIsObj) {
|
||||
if (!eqVal(sample, log)) results.push({path: path, sampleVal: sample, logVal: log, status: 'changed'});
|
||||
return results;
|
||||
}
|
||||
|
||||
// 都是对象,递归比较 key
|
||||
var seen = {};
|
||||
for (var key in sample) {
|
||||
if (!sample.hasOwnProperty(key)) continue;
|
||||
seen[key] = true;
|
||||
var childPath = path ? path + '.' + key : key;
|
||||
results = results.concat(diffObjects(sample[key], log ? log[key] : undefined, childPath));
|
||||
}
|
||||
for (var key2 in log) {
|
||||
if (!log.hasOwnProperty(key2) || seen[key2]) continue;
|
||||
var childPath2 = path ? path + '.' + key2 : key2;
|
||||
results.push({path: childPath2, sampleVal: undefined, logVal: log[key2], status: 'log_only'});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function eqVal(a, b) {
|
||||
if (a === b) return true;
|
||||
if (a === undefined || b === undefined) return false;
|
||||
if (typeof a === 'object' || typeof b === 'object') return JSON.stringify(a) === JSON.stringify(b);
|
||||
return String(a) === String(b);
|
||||
}
|
||||
|
||||
function fmtDiffVal(v) {
|
||||
if (v === undefined || v === null) return 'null';
|
||||
if (typeof v === 'object') return JSON.stringify(v);
|
||||
return String(v);
|
||||
}
|
||||
|
||||
/** 解析 DTO 表达式 ClassName(field=value, ...),返回 JS 对象 */
|
||||
function parseDtoExpr(text) {
|
||||
text = text.trim();
|
||||
// 找出最外层 ClassName(...) 的括号范围
|
||||
var m = text.match(/^([\w.]+)\s*\(([\s\S]*)\)$/);
|
||||
if (!m) {
|
||||
var found = findOuterParens(text);
|
||||
if (!found) throw new Error('未识别到 DTO 表达式,期望格式: ClassName(field=value,...)');
|
||||
text = found;
|
||||
m = text.match(/^([\w.]+)\s*\(([\s\S]*)\)$/);
|
||||
if (!m) throw new Error('DTO 格式异常');
|
||||
}
|
||||
var obj = {};
|
||||
var content = m[2];
|
||||
var pairs = splitTopLevel(content, ',');
|
||||
for (var i = 0; i < pairs.length; i++) {
|
||||
var pair = pairs[i].trim();
|
||||
if (!pair) continue;
|
||||
// 在顶层找第一个 '='
|
||||
var eqIdx = findTopLevelChar(pair, '=');
|
||||
if (eqIdx < 0) continue;
|
||||
var key = pair.substring(0, eqIdx).trim();
|
||||
var valStr = pair.substring(eqIdx + 1).trim();
|
||||
obj[key] = parseDtoValue(valStr);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/** 在文本中定位最外层用括号包裹的 ClassName(...) 片段 */
|
||||
function findOuterParens(text) {
|
||||
var m = text.match(/([\w.]+)\s*\(/);
|
||||
if (!m) return null;
|
||||
var start = m.index;
|
||||
var openIdx = start + m[0].length - 1;
|
||||
var depth = 1;
|
||||
for (var i = openIdx + 1; i < text.length; i++) {
|
||||
if (text[i] === '(') depth++;
|
||||
else if (text[i] === ')') depth--;
|
||||
if (depth === 0) return text.substring(start, i + 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 按分隔符拆分字符串,仅拆分括号深度为 0 的位置 */
|
||||
function splitTopLevel(str, sep) {
|
||||
var parts = [];
|
||||
var start = 0;
|
||||
var depth = 0;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
var ch = str[i];
|
||||
if (ch === '(' || ch === '[') depth++;
|
||||
else if (ch === ')' || ch === ']') depth--;
|
||||
else if (ch === sep && depth === 0) {
|
||||
parts.push(str.substring(start, i));
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
parts.push(str.substring(start));
|
||||
return parts;
|
||||
}
|
||||
|
||||
/** 在字符串中找括号深度为 0 的位置 */
|
||||
function findTopLevelChar(str, ch) {
|
||||
var depth = 0;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
if (str[i] === '(' || str[i] === '[') depth++;
|
||||
else if (str[i] === ')' || str[i] === ']') depth--;
|
||||
else if (str[i] === ch && depth === 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** 递归解析 DTO 值:嵌套 DTO / 列表 / 基本值 */
|
||||
function parseDtoValue(val) {
|
||||
val = val.trim();
|
||||
if (!val) return null;
|
||||
// 列表:[item, item, ...]
|
||||
if (val.charAt(0) === '[' && val.charAt(val.length - 1) === ']') {
|
||||
var inner = val.substring(1, val.length - 1).trim();
|
||||
if (!inner) return [];
|
||||
var items = splitTopLevel(inner, ',');
|
||||
var arr = [];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i].trim();
|
||||
if (!item) continue;
|
||||
arr.push(parseDtoValue(item));
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
// 嵌套 DTO:ClassName(...)
|
||||
if (/^[\w.]+\s*\(/.test(val)) return parseDtoExpr(val);
|
||||
// 匿名对象:super=(requestSystem="CF_MIX", ...)
|
||||
if (val.charAt(0) === '(' && val.charAt(val.length - 1) === ')') {
|
||||
return parseAnonymousDto(val.substring(1, val.length - 1));
|
||||
}
|
||||
// null
|
||||
if (val === 'null' || val === 'NULL') return null;
|
||||
// 日志中的 DTO 表达式不区分类型(string/number/boolean),统一按字符串返回,
|
||||
// Dubbo 序列化时由方法签名决定最终类型,避免误判(如 loanNo=211254457192 被当成数字截断)
|
||||
// Date 格式转换:Tue Jul 28 18:10:00 CST 2026 → 2026-07-28 18:10:00
|
||||
if (/^\w{3}\s\w{3}\s\d{1,2}\s\d{2}:\d{2}:\d{2}\s\w+\s\d{4}$/.test(val)) {
|
||||
var d = new Date(val);
|
||||
if (!isNaN(d.getTime())) {
|
||||
return d.getFullYear() + '-' +
|
||||
pad2(d.getMonth() + 1) + '-' +
|
||||
pad2(d.getDate()) + ' ' +
|
||||
pad2(d.getHours()) + ':' +
|
||||
pad2(d.getMinutes()) + ':' +
|
||||
pad2(d.getSeconds());
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
/** 两位数补零 */
|
||||
function pad2(n) { return n < 10 ? '0' + n : '' + n; }
|
||||
|
||||
/** 解析匿名对象 (field=value, ...),没有类名前缀 */
|
||||
function parseAnonymousDto(content) {
|
||||
var obj = {};
|
||||
var pairs = splitTopLevel(content, ',');
|
||||
for (var i = 0; i < pairs.length; i++) {
|
||||
var pair = pairs[i].trim();
|
||||
if (!pair) continue;
|
||||
var eqIdx = findTopLevelChar(pair, '=');
|
||||
if (eqIdx < 0) continue;
|
||||
var key = pair.substring(0, eqIdx).trim();
|
||||
var valStr = pair.substring(eqIdx + 1).trim();
|
||||
obj[key] = parseDtoValue(valStr);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/** 扁平化 super 继承链。
|
||||
* 日志中 super=TradeRequestDTO(super=BaseRequestDTO(...), ...) 嵌套结构
|
||||
* 需要把 super 中的父类字段提升到当前层级,使比对路径与默认值对齐 */
|
||||
function flattenSuperChain(obj) {
|
||||
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj;
|
||||
var result = {};
|
||||
for (var key in obj) {
|
||||
if (!obj.hasOwnProperty(key)) continue;
|
||||
if (key === 'super' && obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
||||
var superFlat = flattenSuperChain(obj[key]);
|
||||
for (var sKey in superFlat) {
|
||||
if (superFlat.hasOwnProperty(sKey)) result[sKey] = superFlat[sKey];
|
||||
}
|
||||
} else if (key !== 'super') {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 深度合并:src 覆盖 base,base 补齐 src 缺失字段。仅处理普通对象,数组直接覆盖 */
|
||||
function deepMerge(base, src) {
|
||||
if (!base || typeof base !== 'object') return src;
|
||||
if (!src || typeof src !== 'object') return base;
|
||||
if (Array.isArray(base) || Array.isArray(src)) return src;
|
||||
var result = {};
|
||||
for (var key in base) {
|
||||
if (base.hasOwnProperty(key)) {
|
||||
if (src.hasOwnProperty(key)) {
|
||||
result[key] = deepMerge(base[key], src[key]);
|
||||
} else {
|
||||
result[key] = base[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var key2 in src) {
|
||||
if (src.hasOwnProperty(key2) && !base.hasOwnProperty(key2)) {
|
||||
result[key2] = src[key2];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** HTML 转义 */
|
||||
function escHtml(s) {
|
||||
if (!s) return '';
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||
}
|
||||
|
||||
/** 获取本地时区的 yyyy-MM-dd HH:mm:ss */
|
||||
function formatLocalDateTime() {
|
||||
var d = new Date();
|
||||
var pad = function(n) { return (n < 10 ? '0' : '') + n; };
|
||||
return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' +
|
||||
pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
|
||||
}
|
||||
|
||||
/** 构建请求 body */
|
||||
function buildDubboBody() {
|
||||
var argsText = document.getElementById('dubboArgs') ? document.getElementById('dubboArgs').value : '{}';
|
||||
var tntId = document.getElementById('dubboTntId') ? document.getElementById('dubboTntId').value : '0101';
|
||||
var env = document.getElementById('dubboEnv') ? document.getElementById('dubboEnv').value : 'STB';
|
||||
var evtEnv = document.getElementById('dubboEvtEnv') ? document.getElementById('dubboEvtEnv').value : 'DEV';
|
||||
var appId = document.getElementById('dubboAppId') ? document.getElementById('dubboAppId').value : '980010000114002';
|
||||
var card = getActiveDubboCard();
|
||||
var iface = card ? card.getAttribute('data-dubbo-interface') : '';
|
||||
var method = card ? card.getAttribute('data-dubbo-method') : '';
|
||||
var paramTypes = card ? card.getAttribute('data-dubbo-param-types') || '[]' : '[]';
|
||||
|
||||
var args;
|
||||
try { args = JSON.parse(stripJsonComments(argsText)); } catch(e) { alert('args JSON 格式错误: ' + e.message); return null; }
|
||||
|
||||
return {
|
||||
data: {
|
||||
interfacePath: iface,
|
||||
method: method,
|
||||
parameterTypes: JSON.parse(paramTypes),
|
||||
args: args,
|
||||
traceId: genTraceId(),
|
||||
zoneType: 'G',
|
||||
eventContext: {
|
||||
tntId: tntId, ipTId:'', ipId:'null', ipRId:'', chlNo: genTraceId(),
|
||||
originalArNo:'', originalEvtCd:'', arNo:'', chlEvtCd:'',
|
||||
prePdCd:'null', preEvtCd:'null', pdCd:'', evtCd:'', pdPath:'',
|
||||
env: evtEnv, tagExtensionInfo:'{}', shadow:'0'
|
||||
}
|
||||
},
|
||||
tnt_id: tntId,
|
||||
app_id: appId,
|
||||
timestamp: formatLocalDateTime(),
|
||||
env: env
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取当前打开调试的那个 method-card */
|
||||
function getActiveDubboCard() {
|
||||
var mask = document.getElementById('dubboMask');
|
||||
if (!mask) return null;
|
||||
// 从触发按钮找:记录最后点击的 card
|
||||
return window._dubboActiveCard || document.querySelector('.method-card[data-dubbo-interface]');
|
||||
}
|
||||
|
||||
/** 生成 traceId */
|
||||
function genTraceId() {
|
||||
return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
var r = Math.random()*16|0, v = c==='x'? r : (r&0x3|0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/** 发送 Dubbo 请求 — 通过本地 Java 代理转发,解决浏览器无法直连内网网关的问题 */
|
||||
function sendDubboRequest() {
|
||||
var body = buildDubboBody();
|
||||
if (!body) return;
|
||||
|
||||
// 发送前保存当前表单状态
|
||||
saveDubboState();
|
||||
|
||||
var gwUrl = document.getElementById('dubboGwUrl') ? document.getElementById('dubboGwUrl').value : getGwUrl();
|
||||
setGwUrl(gwUrl);
|
||||
|
||||
var btn = document.getElementById('dubboSendBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="dubbo-spinner"></span>请求中...';
|
||||
|
||||
// 确保右栏响应面板存在
|
||||
var statusEl = document.getElementById('dubboStatus');
|
||||
var contentEl = document.getElementById('dubboRespContent');
|
||||
var emptyEl = document.querySelector('.dubbo-response-panel-empty');
|
||||
if (emptyEl) emptyEl.style.display = 'none';
|
||||
if (!contentEl) {
|
||||
var rightCol = document.querySelector('.dubbo-modal-right');
|
||||
if (rightCol) {
|
||||
rightCol.innerHTML = '<div class="dubbo-response-panel" id="dubboResponsePanel">' +
|
||||
'<div class="dubbo-response-panel-header"><span>响应结果</span><span class="dubbo-response-status" id="dubboStatus"></span></div>' +
|
||||
'<pre class="dubbo-response-content" id="dubboRespContent"></pre></div>';
|
||||
contentEl = document.getElementById('dubboRespContent');
|
||||
statusEl = document.getElementById('dubboStatus');
|
||||
}
|
||||
}
|
||||
statusEl.textContent = '请求中...';
|
||||
statusEl.className = 'dubbo-response-status';
|
||||
contentEl.textContent = '';
|
||||
|
||||
// 去除旧的 time 行
|
||||
var oldTime = document.querySelector('.dubbo-response-content-time');
|
||||
if (oldTime) oldTime.remove();
|
||||
|
||||
var headers = defaultHeaders();
|
||||
var evtEnv = document.getElementById('dubboEvtEnv') ? document.getElementById('dubboEvtEnv').value : 'DEV';
|
||||
headers['Event-Context'] = buildEventContext(body.tnt_id, body.env, evtEnv);
|
||||
headers['X-Gateway-Url'] = gwUrl;
|
||||
|
||||
// 走本地 Java 代理,避免浏览器 CORS/网络限制
|
||||
fetch(DUBBO_PROXY_URL, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(body)
|
||||
}).then(function(res) {
|
||||
var statusOk = res.ok;
|
||||
var statusText = res.status + ' ' + res.statusText;
|
||||
statusEl.textContent = statusText;
|
||||
statusEl.className = 'dubbo-response-status ' + (statusOk ? 'ok' : 'err');
|
||||
return res.text().then(function(txt) {
|
||||
var displayText;
|
||||
try {
|
||||
var j = JSON.parse(txt);
|
||||
displayText = JSON.stringify(j, null, 2);
|
||||
} catch(e) {
|
||||
displayText = txt;
|
||||
}
|
||||
contentEl.textContent = displayText;
|
||||
// 保存响应结果到 localStorage
|
||||
saveDubboResponse(statusText, displayText, statusOk);
|
||||
// 追加时间戳显示
|
||||
var timeEl = document.querySelector('.dubbo-response-content-time');
|
||||
if (timeEl) timeEl.remove();
|
||||
var timeHtml = '<div class="dubbo-response-content-time">📅 ' + formatLocalDateTime() + '</div>';
|
||||
contentEl.insertAdjacentHTML('beforebegin', timeHtml);
|
||||
btn.disabled = false;
|
||||
btn.textContent = '▶ 发送请求';
|
||||
});
|
||||
}).catch(function(err) {
|
||||
var errMsg = err.message || String(err);
|
||||
statusEl.textContent = '网络错误';
|
||||
statusEl.className = 'dubbo-response-status err';
|
||||
contentEl.textContent = errMsg;
|
||||
saveDubboResponse('网络错误', errMsg, false);
|
||||
btn.disabled = false;
|
||||
btn.textContent = '▶ 发送请求';
|
||||
});
|
||||
}
|
||||
|
||||
/** 复制为 cURL */
|
||||
function copyDubboCurl() {
|
||||
var body = buildDubboBody();
|
||||
if (!body) return;
|
||||
var gwUrl = document.getElementById('dubboGwUrl') ? document.getElementById('dubboGwUrl').value : getGwUrl();
|
||||
var headers = defaultHeaders();
|
||||
var evtEnv = document.getElementById('dubboEvtEnv') ? document.getElementById('dubboEvtEnv').value : 'DEV';
|
||||
headers['Event-Context'] = buildEventContext(body.tnt_id, body.env, evtEnv);
|
||||
var curl = 'curl -X POST \\\n ' + JSON.stringify(gwUrl) + ' \\\n';
|
||||
for (var k in headers) {
|
||||
curl += ' -H ' + JSON.stringify(k + ': ' + headers[k]) + ' \\\n';
|
||||
}
|
||||
curl += ' -d ' + JSON.stringify(JSON.stringify(body, null, 2));
|
||||
copyToClip(curl, null);
|
||||
showToast('cURL 已复制');
|
||||
}
|
||||
|
||||
/** 复制请求体 JSON */
|
||||
function copyDubboBody() {
|
||||
var body = buildDubboBody();
|
||||
if (!body) return;
|
||||
copyToClip(JSON.stringify(body, null, 2), null);
|
||||
showToast('请求体已复制');
|
||||
}
|
||||
|
||||
/** Toast 提示 */
|
||||
function showToast(msg) {
|
||||
var t = document.getElementById('dubboToast');
|
||||
if (!t) { t = document.createElement('div'); t.id = 'dubboToast'; t.style.cssText = 'position:fixed;bottom:32px;left:50%;transform:translateX(-50%);background:#1d2129;color:#fff;padding:8px 24px;border-radius:6px;font-size:13px;z-index:99999;pointer-events:none;transition:opacity .3s'; document.body.appendChild(t); }
|
||||
t.textContent = msg;
|
||||
t.style.opacity = '1';
|
||||
clearTimeout(t._tid);
|
||||
t._tid = setTimeout(function(){ t.style.opacity = '0'; }, 1800);
|
||||
}
|
||||
|
||||
// 修复:track 最后点击的 dubbo card
|
||||
document.addEventListener('click', function(e) {
|
||||
var card = e.target.closest('.method-card[data-dubbo-interface]');
|
||||
if (card) window._dubboActiveCard = card;
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user