api生成器优化
This commit is contained in:
@@ -65,10 +65,6 @@ public class ApiDocWindow {
|
||||
private CheckBox apiPackageOnlyCb;
|
||||
private CheckBox facadeNameCb;
|
||||
|
||||
// 导出格式
|
||||
private ToggleGroup fmtGroup;
|
||||
private RadioButton rbHtml, rbMd;
|
||||
|
||||
// 操作区
|
||||
private Button exportBtn;
|
||||
private ProgressBar progressBar;
|
||||
@@ -219,28 +215,6 @@ public class ApiDocWindow {
|
||||
fqnPanel.visibleProperty().bind(rbFqn.selectedProperty());
|
||||
batchPanel.visibleProperty().bind(rbBatch.selectedProperty());
|
||||
|
||||
// --- 分割线 ---
|
||||
Separator sep1 = new Separator();
|
||||
|
||||
// --- 导出格式 ---
|
||||
HBox fmtRow = new HBox(Theme.S4);
|
||||
fmtRow.setAlignment(Pos.CENTER_LEFT);
|
||||
Label fmtLabel = new Label("导出格式:");
|
||||
fmtLabel.setFont(Theme.base(fsBody()));
|
||||
fmtLabel.setTextFill(Theme.TEXT_PRIMARY);
|
||||
fmtGroup = new ToggleGroup();
|
||||
rbHtml = new RadioButton("HTML");
|
||||
rbMd = new RadioButton("Markdown");
|
||||
rbHtml.setToggleGroup(fmtGroup);
|
||||
rbMd.setToggleGroup(fmtGroup);
|
||||
rbHtml.setSelected(true);
|
||||
rbHtml.setFont(Theme.base(fsBody() - 1));
|
||||
rbMd.setFont(Theme.base(fsBody() - 1));
|
||||
fmtRow.getChildren().addAll(fmtLabel, rbHtml, rbMd);
|
||||
|
||||
// --- 分割线 ---
|
||||
Separator sep2 = new Separator();
|
||||
|
||||
// --- 操作按钮 ---
|
||||
HBox actionRow = new HBox(Theme.S3);
|
||||
actionRow.setAlignment(Pos.CENTER_LEFT);
|
||||
@@ -258,7 +232,7 @@ public class ApiDocWindow {
|
||||
|
||||
actionRow.getChildren().addAll(exportBtn, progressBar, progressLabel);
|
||||
|
||||
configCard.getChildren().addAll(modeLabel, modeRow, inputStack, sep1, fmtRow, sep2, actionRow);
|
||||
configCard.getChildren().addAll(modeLabel, modeRow, inputStack, actionRow);
|
||||
|
||||
// ===================== 历史记录 =====================
|
||||
Label histTitle = new Label("导出历史");
|
||||
@@ -371,10 +345,6 @@ public class ApiDocWindow {
|
||||
}, "ApiDoc-Worker").start();
|
||||
}
|
||||
|
||||
private String getFmt() {
|
||||
return rbHtml.isSelected() ? "1" : "2";
|
||||
}
|
||||
|
||||
// ==================== 单文件导出 ====================
|
||||
|
||||
private void exportSingle(String inputPath, Path filePath, JavaSourceParser parser) throws Exception {
|
||||
@@ -593,24 +563,13 @@ public class ApiDocWindow {
|
||||
sel.scope = ApiDocModel.ExportScope.CUSTOM;
|
||||
sel.projectName = name;
|
||||
|
||||
String fmt = getFmt();
|
||||
if ("1".equals(fmt)) {
|
||||
Path p = gen.generateHtml(sel, docs);
|
||||
String filePath = p.toAbsolutePath().toString();
|
||||
Platform.runLater(() -> {
|
||||
log("HTML 已导出: " + filePath);
|
||||
addHistory("HTML", filePath);
|
||||
enableBtn();
|
||||
});
|
||||
} else {
|
||||
Path p = gen.generateMarkdown(sel, docs);
|
||||
String filePath = p.toAbsolutePath().toString();
|
||||
Platform.runLater(() -> {
|
||||
log("Markdown 已导出: " + filePath);
|
||||
addHistory("Markdown", filePath);
|
||||
enableBtn();
|
||||
});
|
||||
}
|
||||
Path p = gen.generateHtml(sel, docs);
|
||||
String filePath = p.toAbsolutePath().toString();
|
||||
Platform.runLater(() -> {
|
||||
log("HTML 已导出: " + filePath);
|
||||
addHistory("HTML", filePath);
|
||||
enableBtn();
|
||||
});
|
||||
}
|
||||
|
||||
private void addHistory(String mode, String filePath) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
||||
package com.example.workutils.biz.apidoc.generator;
|
||||
|
||||
/**
|
||||
* API 文档渲染上下文,封装传入模板的所有数据。
|
||||
*
|
||||
* AI 定位指引:
|
||||
* - 新增模板变量 → 在本类添加字段,然后在 buildHtml() 中使用
|
||||
* - 修改文档标题/元信息 → 修改本类对应字段的赋值
|
||||
*/
|
||||
public class DocRenderContext {
|
||||
|
||||
/** 文档标题 */
|
||||
private String title;
|
||||
|
||||
/** CSS 样式内容(从 api-doc.css 加载) */
|
||||
private String css;
|
||||
|
||||
/** JS 脚本内容(从 api-doc.js 加载) */
|
||||
private String js;
|
||||
|
||||
/** 文档主体 HTML */
|
||||
private String bodyHtml;
|
||||
|
||||
/** 生成时间 */
|
||||
private String generationTime;
|
||||
|
||||
/** 接口总数 */
|
||||
private int totalClasses;
|
||||
|
||||
/** 方法总数 */
|
||||
private int totalMethods;
|
||||
|
||||
// ===== 构造器 =====
|
||||
|
||||
public DocRenderContext() {
|
||||
}
|
||||
|
||||
// ===== Getter / Setter =====
|
||||
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
|
||||
public String getCss() { return css; }
|
||||
public void setCss(String css) { this.css = css; }
|
||||
|
||||
public String getJs() { return js; }
|
||||
public void setJs(String js) { this.js = js; }
|
||||
|
||||
public String getBodyHtml() { return bodyHtml; }
|
||||
public void setBodyHtml(String bodyHtml) { this.bodyHtml = bodyHtml; }
|
||||
|
||||
public String getGenerationTime() { return generationTime; }
|
||||
public void setGenerationTime(String generationTime) { this.generationTime = generationTime; }
|
||||
|
||||
public int getTotalClasses() { return totalClasses; }
|
||||
public void setTotalClasses(int totalClasses) { this.totalClasses = totalClasses; }
|
||||
|
||||
public int getTotalMethods() { return totalMethods; }
|
||||
public void setTotalMethods(int totalMethods) { this.totalMethods = totalMethods; }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.example.workutils.biz.apidoc.generator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* API 文档资源加载器。
|
||||
* 从 classpath 加载分离的 CSS / JS / HTML 模板资源,
|
||||
* 替换原先在 {@link ApiDocGenerator} 中硬编码字符串拼接的方式。
|
||||
*
|
||||
* AI 定位指引:
|
||||
* - CSS 样式修改 → src/main/resources/templates/apidoc/api-doc.css
|
||||
* - JS 交互修改 → src/main/resources/templates/apidoc/api-doc.js
|
||||
* - HTML 结构修改 → src/main/resources/templates/apidoc/api-doc.html
|
||||
* - 新增资源类型 → 在本类添加对应加载方法
|
||||
*/
|
||||
public class HtmlResourceLoader {
|
||||
|
||||
private static final String RESOURCE_BASE = "templates/apidoc/";
|
||||
|
||||
/**
|
||||
* 加载 HTML 模板。
|
||||
* 对应资源文件:resources/templates/apidoc/api-doc.html。
|
||||
* 模板中使用 {{PLACEHOLDER}} 格式标记动态替换位。
|
||||
*/
|
||||
public String loadTemplate() {
|
||||
return loadResource(RESOURCE_BASE + "api-doc.html");
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 CSS 样式。
|
||||
* 对应资源文件:resources/templates/apidoc/api-doc.css
|
||||
*/
|
||||
public String loadCss() {
|
||||
return loadResource(RESOURCE_BASE + "api-doc.css");
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 JS 脚本。
|
||||
* 对应资源文件:resources/templates/apidoc/api-doc.js
|
||||
*/
|
||||
public String loadJs() {
|
||||
return loadResource(RESOURCE_BASE + "api-doc.js");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 classpath 读取资源文件内容。
|
||||
* 所有资源必须以 UTF-8 编码。
|
||||
*/
|
||||
private String loadResource(String path) {
|
||||
ClassLoader cl = getClass().getClassLoader();
|
||||
if (cl == null) {
|
||||
cl = ClassLoader.getSystemClassLoader();
|
||||
}
|
||||
try (InputStream is = cl.getResourceAsStream(path)) {
|
||||
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() : "";
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("加载资源失败: " + path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/* ============================================================
|
||||
* API 接口文档样式
|
||||
* 从 ApiDocGenerator.appendCss() 提取,便于独立维护
|
||||
* ============================================================ */
|
||||
|
||||
/* ===== 基础重置 ===== */
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:'Microsoft YaHei',-apple-system,sans-serif;color:#1d1d1f;background:#f2f3f5;display:flex;flex-direction:column;min-height:100vh}
|
||||
|
||||
/* ===== 顶部栏 ===== */
|
||||
.topbar{display:flex;align-items:center;height:48px;background:#fff;border-bottom:1px solid #e5e6eb;padding:0 20px;position:sticky;top:0;z-index:100}
|
||||
.topbar-brand{font-weight:700;font-size:16px;color:#1d1d1f;margin-right:32px}
|
||||
.topbar-tabs{display:flex;gap:0}
|
||||
.tab{padding:12px 20px;font-size:14px;color:#86909c;cursor:pointer;border-bottom:2px solid transparent;transition:.2s}
|
||||
.tab.active{color:#1677ff;border-bottom-color:#1677ff;font-weight:600}
|
||||
.topbar-meta{margin-left:auto;font-size:12px;color:#86909c;white-space:nowrap}
|
||||
|
||||
/* ===== 搜索框 ===== */
|
||||
.search-input{padding:5px 12px;border:1px solid #e5e6eb;border-radius:4px;font-size:13px;outline:none;width:200px;transition:border .2s,box-shadow .2s;font-family:inherit}
|
||||
.search-input:focus{border-color:#1677ff;box-shadow:0 0 0 2px rgba(22,119,255,.1)}
|
||||
|
||||
/* ===== 统计摘要栏 ===== */
|
||||
.stats-bar{display:flex;align-items:center;gap:20px;padding:6px 20px;background:#fff;border-bottom:1px solid #e5e6eb;font-size:12px;color:#86909c}
|
||||
.stats-item{display:flex;align-items:center;gap:3px}
|
||||
.stats-num{font-weight:700;font-size:14px;color:#1677ff}
|
||||
.stats-actions{margin-left:auto;display:flex;gap:6px}
|
||||
.ctrl-btn{padding:3px 10px;border:1px solid #e5e6eb;border-radius:3px;background:#fff;color:#4e5969;font-size:11px;cursor:pointer;transition:.15s;font-family:inherit}
|
||||
.ctrl-btn:hover{border-color:#1677ff;color:#1677ff;background:#e8f3ff}
|
||||
|
||||
/* ===== 布局 ===== */
|
||||
.main-layout{display:flex;flex:1}
|
||||
|
||||
/* ===== 侧栏导航 ===== */
|
||||
.sidebar{width:280px;min-width:280px;background:#fff;border-right:1px solid #e5e6eb;overflow-y:auto;padding:16px 0;position:sticky;top:78px;height:calc(100vh - 78px)}
|
||||
.sidebar-title{font-size:14px;font-weight:700;color:#1d1d1f;padding:0 16px 12px;border-bottom:1px solid #f2f3f5;margin-bottom:8px}
|
||||
.nav-group{margin-bottom:4px}
|
||||
.nav-group-title{padding:8px 16px;font-size:13px;font-weight:600;color:#4e5969;cursor:pointer;display:flex;align-items:center;gap:6px;user-select:none;transition:.15s}
|
||||
.nav-group-title:hover{background:#f2f3f5}
|
||||
.nav-group-title .arrow{font-size:10px;transition:transform .2s;display:inline-block}
|
||||
.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-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}
|
||||
.nav-method{padding-left:48px;font-size:11px;gap:6px}
|
||||
.nav-method-dot{font-size:6px;color:#c9cdd4;flex-shrink:0;line-height:1}
|
||||
.nav-http-tag{display:inline-block;min-width:28px;text-align:center;font-size:10px;font-weight:600;padding:0 4px;border-radius:2px;line-height:16px;flex-shrink:0}
|
||||
.nav-http-tag.get{background:#e8f3ff;color:#1677ff}
|
||||
.nav-http-tag.post{background:#e8ffea;color:#00b42a}
|
||||
.nav-http-tag.put{background:#fff7e6;color:#ff7d00}
|
||||
.nav-http-tag.delete{background:#ffece8;color:#f53f3f}
|
||||
.nav-http-tag.req{background:#f2f3f5;color:#86909c}
|
||||
.nav-http-tag.rpc{background:#f3e8ff;color:#722ed1}
|
||||
|
||||
/* ===== 内容区 ===== */
|
||||
.content{flex:1;padding:24px;overflow-y:auto;max-width:calc(100vw - 280px)}
|
||||
.project-section{margin-bottom:32px}
|
||||
.project-title{font-size:22px;font-weight:700;color:#1d1d1f;margin-bottom:16px;padding-bottom:8px;border-bottom:1px solid #e5e6eb}
|
||||
|
||||
/* ===== 接口卡片 ===== */
|
||||
.api-card{background:#fff;border-radius:8px;margin-bottom:16px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.06)}
|
||||
.api-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #f2f3f5}
|
||||
.api-header-left{display:flex;align-items:center;gap:10px}
|
||||
.api-name{font-size:16px;font-weight:700;color:#1d1d1f}
|
||||
.api-header-right{font-size:13px;color:#86909c}
|
||||
.type-badge{display:inline-block;padding:2px 10px;border-radius:4px;font-size:12px;font-weight:600}
|
||||
.type-badge.interface{background:#e8f3ff;color:#1677ff}
|
||||
.type-badge.class{background:#e8ffea;color:#00b42a}
|
||||
|
||||
/* ===== 信息网格 ===== */
|
||||
.info-grid{padding:16px 20px;display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||||
.info-item{display:flex;align-items:baseline;gap:8px;font-size:13px}
|
||||
.info-label{color:#86909c;white-space:nowrap;min-width:64px}
|
||||
.info-value{color:#1d1d1f;word-break:break-all}
|
||||
.info-value.mono{font-family:'Consolas','Courier New',monospace;font-size:12px;color:#4e5969}
|
||||
|
||||
/* ===== 注解标签 ===== */
|
||||
.anno-tag{display:inline-block;padding:1px 8px;border-radius:3px;font-size:11px;margin:1px 3px;background:#fff7e6;color:#d46b08;border:1px solid #ffd591}
|
||||
.method-anno{background:#f6ffed;color:#389e0d;border-color:#b7eb8f}
|
||||
|
||||
/* ===== 方法列表 ===== */
|
||||
.methods-section{padding:0 20px 16px}
|
||||
.section-label{font-size:14px;font-weight:700;color:#1d1d1f;padding:8px 0;border-top:1px solid #f2f3f5}
|
||||
|
||||
/* ===== 方法卡片 ===== */
|
||||
.method-card{border:1px solid #f2f3f5;border-radius:8px;margin:10px 0;overflow:hidden}
|
||||
.method-header{display:flex;align-items:center;gap:10px;padding:12px 16px;background:#fafbfc;cursor:pointer;user-select:none;transition:background .15s}
|
||||
.method-header:hover{background:#f2f3f5}
|
||||
.method-arrow{font-size:10px;transition:transform .2s;color:#86909c}
|
||||
.method-header.collapsed .method-arrow{transform:rotate(-90deg)}
|
||||
.method-name{font-size:14px;font-weight:700;color:#1677ff;font-family:'Consolas','Courier New',monospace;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.method-return{font-size:12px;color:#86909c;flex-shrink:0}
|
||||
.method-return code{color:#00b42a;background:#e8ffea;padding:2px 6px;border-radius:3px;font-size:11px}
|
||||
.http-badge{display:inline-block;padding:2px 8px;border-radius:3px;font-size:11px;font-weight:700;color:#fff}
|
||||
.http-badge.get{background:#1677ff}
|
||||
.http-badge.post{background:#00b42a}
|
||||
.http-badge.put{background:#ff7d00}
|
||||
.http-badge.delete{background:#f53f3f}
|
||||
.http-badge.req{background:#86909c}
|
||||
.http-badge.rpc{background:#722ed1}
|
||||
|
||||
/* ===== 方法体 ===== */
|
||||
.method-body{padding:12px 16px}
|
||||
.method-header.collapsed+.method-body{display:none}
|
||||
.method-annotations{margin-bottom:8px}
|
||||
.method-desc{font-size:13px;color:#4e5969;padding:8px 12px;background:#fafbfc;border-radius:4px;margin-bottom:10px;border-left:3px solid #e5e6eb;white-space:pre-wrap}
|
||||
.method-signature{padding:10px 14px;background:#1e1e1e;border-radius:6px;margin-bottom:12px;overflow-x:auto;position:relative;display:flex;align-items:center}
|
||||
.method-signature code{font-family:'Consolas','Courier New',monospace;font-size:13px;color:#d4d4d4;flex:1}
|
||||
.copy-btn{position:absolute;right:8px;top:50%;transform:translateY(-50%);padding:3px 10px;border:1px solid #555;border-radius:3px;background:transparent;color:#999;font-size:11px;cursor:pointer;transition:.15s;font-family:inherit}
|
||||
.copy-btn:hover{color:#fff;border-color:#888}
|
||||
.copy-btn.copied{color:#52c41a;border-color:#52c41a}
|
||||
|
||||
/* ===== 复制按钮 ===== */
|
||||
.api-copy-btn{padding:4px 12px;border:1px solid #1677ff;border-radius:4px;background:#e8f3ff;color:#1677ff;font-size:12px;cursor:pointer;transition:.15s;font-family:inherit}
|
||||
.api-copy-btn:hover{background:#1677ff;color:#fff}
|
||||
.api-copy-btn.copied{background:#52c41a;border-color:#52c41a;color:#fff}
|
||||
.method-copy-btn{padding:3px 10px;border:1px solid #e5e6eb;border-radius:3px;background:#fff;color:#4e5969;font-size:11px;cursor:pointer;transition:.15s;font-family:inherit;flex-shrink:0}
|
||||
.method-copy-btn:hover{border-color:#1677ff;color:#1677ff;background:#e8f3ff}
|
||||
.method-copy-btn.copied{border-color:#52c41a;color:#52c41a;background:#f6ffed}
|
||||
.api-copy-btn.txt{background:#fff;color:#1677ff}
|
||||
.api-copy-btn.txt:hover{background:#e8f3ff}
|
||||
.method-copy-btn.txt{color:#1677ff;border-color:#91caff;background:#f0f7ff}
|
||||
.method-copy-btn.img{background:#1677ff;color:#fff;border-color:#1677ff}
|
||||
.method-copy-btn.img:hover{background:#0958d9;border-color:#0958d9}
|
||||
|
||||
/* ===== 参数块 ===== */
|
||||
.params-block{margin-bottom:12px}
|
||||
.params-block-title{font-size:13px;font-weight:600;color:#4e5969;margin-bottom:6px}
|
||||
|
||||
/* ===== DTO 块(可折叠卡片) ===== */
|
||||
.dto-block{margin-bottom:10px;border:1px solid #e5e6eb;border-radius:6px;overflow:hidden}
|
||||
.dto-block-title{padding:8px 12px;background:#fafbfc;font-size:13px;font-weight:600;cursor:pointer;border-left:3px solid #1677ff;user-select:none;display:flex;align-items:center;gap:6px;transition:background .15s}
|
||||
.dto-block-title:hover{background:#f2f3f5}
|
||||
.dto-block-title small{font-weight:400;color:#86909c}
|
||||
.dto-arrow{font-size:10px;transition:transform .2s;color:#86909c}
|
||||
.dto-block-title.collapsed .dto-arrow{transform:rotate(-90deg)}
|
||||
.dto-block-body{padding:0}
|
||||
.dto-block-title.collapsed+.dto-block-body{display:none}
|
||||
|
||||
/* ===== 未配置包装器警告 ===== */
|
||||
.dto-block-warn{border-color:#ff7d00;background:#fff7e6}
|
||||
.dto-title-warn{border-left-color:#ff7d00!important;background:#fff7e6!important;color:#d46b08!important}
|
||||
.warn-hint{color:#ff7d00!important;font-weight:600;font-size:11px;background:#fff0d6;padding:2px 8px;border-radius:3px;margin-left:6px}
|
||||
|
||||
/* ===== 表格 ===== */
|
||||
.data-table{width:100%;border-collapse:collapse;font-size:12px}
|
||||
.data-table th{background:#fafbfc;text-align:left;padding:7px 10px;border:1px solid #e5e6eb;font-weight:600;color:#4e5969;white-space:nowrap}
|
||||
.data-table td{padding:7px 10px;border:1px solid #e5e6eb;color:#1d1d1f;vertical-align:top}
|
||||
.data-table td:first-child{min-width:200px}
|
||||
.data-table tr:hover td{background:#f7f8fa}
|
||||
.data-table code{font-family:'Consolas','Courier New',monospace;font-size:11px;background:#f2f3f5;padding:1px 5px;border-radius:2px;color:#1677ff}
|
||||
.data-table code.field-name{color:#d46b08}
|
||||
|
||||
/* ===== 字段行层级样式 ===== */
|
||||
.field-indent{display:inline-block;min-width:0}
|
||||
.field-expand-toggle{display:inline-block;font-size:10px;color:#86909c;cursor:pointer;width:12px;text-align:center;transition:transform .15s;user-select:none}
|
||||
.field-expand-toggle.open{transform:rotate(90deg)}
|
||||
.field-row.d0 td{background:#fff}
|
||||
.field-row.d1 td{background:#f9fbff}
|
||||
.field-row.d2 td{background:#f6f9ff}
|
||||
.field-row.d3 td{background:#f3f6ff}
|
||||
.field-row.d1 td:first-child{border-left:3px solid #52c41a}
|
||||
.field-row.d2 td:first-child{border-left:3px solid #fa8c16}
|
||||
.field-row.d3 td:first-child{border-left:3px solid #722ed1}
|
||||
.type-complex{color:#722ed1!important;background:#f3e8ff!important}
|
||||
.type-custom{color:#0958d9!important;background:#e6f4ff!important;border:1px solid #91caff}
|
||||
|
||||
/* ===== 全限定名与继承 ===== */
|
||||
.fqn{font-size:11px;color:#86909c;font-family:'Consolas','Courier New',monospace}
|
||||
.inherit-tag{font-size:11px;color:#86909c;font-style:italic;margin-left:4px}
|
||||
.inherit-section{border-top:1px dashed #e5e6eb;padding-top:8px;margin-top:8px}
|
||||
.inherit-header{display:inline-block;font-size:11px;font-weight:600;color:#86909c;padding:2px 8px;background:#f7f8fa;border-radius:3px;margin-bottom:6px}
|
||||
.inherit-header code{font-size:11px;color:#1677ff;background:#e8f3ff;padding:1px 5px;border-radius:2px}
|
||||
|
||||
/* ===== throws 异常 ===== */
|
||||
.throws-block{font-size:12px;color:#f53f3f;padding:6px 0}
|
||||
.throws-block code{background:#fff2f0;padding:2px 6px;border-radius:2px}
|
||||
|
||||
/* ===== 搜索高亮 ===== */
|
||||
.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}
|
||||
.search-match{box-shadow:0 0 0 2px rgba(22,119,255,.25)}
|
||||
|
||||
/* ===== 响应式 ===== */
|
||||
@media(max-width:900px){.sidebar{display:none}.content{max-width:100vw}}
|
||||
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API 接口文档</title>
|
||||
<style>
|
||||
{{CSS}}
|
||||
</style>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="topbar">
|
||||
<div class="topbar-brand">📄 API 接口文档</div>
|
||||
<div class="topbar-tabs">
|
||||
<span class="tab active">接口文档</span>
|
||||
</div>
|
||||
<input type="text" id="searchInput" class="search-input" placeholder="搜索接口/方法名..." oninput="filterDocs()">
|
||||
<div class="topbar-meta">生成时间: {{GENERATE_TIME}}</div>
|
||||
</header>
|
||||
|
||||
{{STATS_BAR}}
|
||||
|
||||
<div class="main-layout">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-title">接口列表</div>
|
||||
<nav class="nav-tree">
|
||||
{{SIDEBAR}}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
{{CONTENT}}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
{{JS}}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,272 @@
|
||||
/* ============================================================
|
||||
* API 接口文档交互脚本
|
||||
* 从 ApiDocGenerator.appendJs() 提取,版块化便于独立维护
|
||||
*
|
||||
* AI 定位指引:
|
||||
* - 搜索过滤 → 搜索 "filterDocs"
|
||||
* - 展开/折叠 → 搜索 "expandAll" / "collapseAll"
|
||||
* - 复制功能 → 搜索 "copyToClip" / "copyImage" / "copyRtfFromEl"
|
||||
* - DTO/字段展开 → 搜索 "toggleDto" / "toggleChildren"
|
||||
* ============================================================ */
|
||||
|
||||
/* ===== 导航分组折叠 ===== */
|
||||
document.querySelectorAll('.nav-group-title').forEach(function(el) {
|
||||
el.classList.add('open');
|
||||
});
|
||||
function toggleGroup(el) {
|
||||
el.classList.toggle('open');
|
||||
document.querySelectorAll('.nav-group-title.open + .nav-group-items').forEach(function(i) {
|
||||
i.style.maxHeight = i.scrollHeight + 'px';
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('.nav-group-title.open + .nav-group-items').forEach(function(i) {
|
||||
i.style.maxHeight = i.scrollHeight + 'px';
|
||||
});
|
||||
|
||||
/* ===== 方法卡片折叠 ===== */
|
||||
function toggleMethod(el) {
|
||||
el.classList.toggle('collapsed');
|
||||
}
|
||||
function toggleDto(el) {
|
||||
el.classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
/* ===== 字段子行折叠 ===== */
|
||||
function toggleChildren(el) {
|
||||
el.classList.toggle('open');
|
||||
var row = el.closest('tr');
|
||||
if (!row) return;
|
||||
var d = parseInt(row.dataset.depth) || 0;
|
||||
var hide = el.classList.contains('open');
|
||||
var next = row.nextElementSibling;
|
||||
while (next && next.classList.contains('field-row')) {
|
||||
var nd = parseInt(next.dataset.depth) || 0;
|
||||
if (nd <= d) break;
|
||||
next.style.display = hide ? '' : 'none';
|
||||
next = next.nextElementSibling;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 展开/折叠全部 ===== */
|
||||
function expandAll() {
|
||||
document.querySelectorAll('.method-header').forEach(function(h) { h.classList.remove('collapsed'); });
|
||||
document.querySelectorAll('.dto-block-title').forEach(function(t) { t.classList.remove('collapsed'); });
|
||||
}
|
||||
function collapseAll() {
|
||||
document.querySelectorAll('.method-header').forEach(function(h) { h.classList.add('collapsed'); });
|
||||
document.querySelectorAll('.dto-block-title').forEach(function(t) { t.classList.add('collapsed'); });
|
||||
}
|
||||
|
||||
/* ===== 导航高亮(IntersectionObserver) ===== */
|
||||
var observer = new IntersectionObserver(function(entries) {
|
||||
entries.forEach(function(e) {
|
||||
if (e.isIntersecting) {
|
||||
document.querySelectorAll('.nav-item.active').forEach(function(a) { a.classList.remove('active'); });
|
||||
var n = document.querySelector('.nav-item[href="#' + e.target.id + '"]');
|
||||
if (n) n.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.api-card[id]').forEach(function(c) { observer.observe(c); });
|
||||
|
||||
/* ===== 侧栏导航点击展开对应方法 ===== */
|
||||
document.querySelectorAll('.nav-method').forEach(function(link) {
|
||||
link.addEventListener('click', function(e) {
|
||||
var targetId = this.getAttribute('href');
|
||||
if (!targetId || !targetId.startsWith('#')) return;
|
||||
var el = document.getElementById(targetId.substring(1));
|
||||
if (!el) return;
|
||||
var methodCard = el.closest('.method-card');
|
||||
if (!methodCard) return;
|
||||
var header = methodCard.querySelector('.method-header');
|
||||
if (header) header.classList.remove('collapsed');
|
||||
var body = methodCard.querySelector('.method-body');
|
||||
if (body) body.style.display = '';
|
||||
var apiCard = methodCard.closest('.api-card');
|
||||
if (apiCard) { apiCard.classList.remove('collapsed'); var apiBody = apiCard.querySelector('.api-body'); if (apiBody) apiBody.style.display = ''; }
|
||||
});
|
||||
});
|
||||
|
||||
/* ===== 复制到剪贴板(纯文本) ===== */
|
||||
function copyToClip(text, btn) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
markCopied(btn, '✔ 已复制');
|
||||
}).catch(function() {
|
||||
fallbackCopy(text, btn);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(text, btn);
|
||||
}
|
||||
}
|
||||
function fallbackCopy(text, btn) {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.left = '-999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
try { document.execCommand('copy'); markCopied(btn, '✔ 已复制'); } catch (e) { markCopied(btn, '⚠ 失败'); }
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
function markCopied(btn, msg) {
|
||||
var orig = btn.textContent;
|
||||
btn.textContent = msg;
|
||||
btn.classList.add('copied');
|
||||
setTimeout(function() { btn.textContent = orig; btn.classList.remove('copied'); }, 1500);
|
||||
}
|
||||
|
||||
/* ===== 复制代码签名 ===== */
|
||||
function copyCode(btn) {
|
||||
var pre = btn.closest('.method-signature');
|
||||
if (!pre) { var card = btn.closest('.method-card'); if (card) pre = card.querySelector('.method-signature'); }
|
||||
var codeEl = pre ? pre.querySelector('code') : null;
|
||||
var text = codeEl ? codeEl.textContent : (pre ? pre.textContent.replace('复制', '') : '');
|
||||
copyToClip(text.trim(), btn);
|
||||
}
|
||||
|
||||
/* ===== 复制为图片(需要 html2canvas) ===== */
|
||||
function copyImage(el, btn) {
|
||||
if (!el || typeof html2canvas === 'undefined') { markCopied(btn, '⚠ 不可用'); return; }
|
||||
var clone = el.cloneNode(true);
|
||||
clone.querySelectorAll('.method-header').forEach(function(h) { h.classList.remove('collapsed'); });
|
||||
clone.querySelectorAll('.method-body').forEach(function(b) { b.style.display = ''; });
|
||||
clone.querySelectorAll('.dto-block-title').forEach(function(t) { t.classList.remove('collapsed'); });
|
||||
clone.style.position = 'fixed';
|
||||
clone.style.left = '-9999px';
|
||||
clone.style.top = '0';
|
||||
clone.style.zIndex = '-1';
|
||||
document.body.appendChild(clone);
|
||||
html2canvas(clone, { scale: 2, useCORS: true, allowTaint: true, backgroundColor: '#ffffff', windowWidth: clone.scrollWidth, windowHeight: clone.scrollHeight }).then(function(canvas) {
|
||||
canvas.toBlob(function(blob) {
|
||||
if (blob && navigator.clipboard && navigator.clipboard.write) {
|
||||
var item = new ClipboardItem({ 'image/png': blob });
|
||||
navigator.clipboard.write([item]).then(function() {
|
||||
markCopied(btn, '✔ 图片已复制');
|
||||
}).catch(function() {
|
||||
markCopied(btn, '⚠ 请粘贴');
|
||||
});
|
||||
} else { markCopied(btn, '⚠ 不支持'); }
|
||||
}, 'image/png');
|
||||
}).catch(function() {
|
||||
markCopied(btn, '⚠ html2canvas 失败');
|
||||
}).finally(function() {
|
||||
if (clone.parentNode) document.body.removeChild(clone);
|
||||
});
|
||||
}
|
||||
|
||||
/* ===== 复制为 Word 富文本 (RTF) ===== */
|
||||
function copyRtfFromEl(el, btn) {
|
||||
if (!el) { markCopied(btn, '⚠ 无内容'); return; }
|
||||
var wrap = document.createElement('div');
|
||||
wrap.innerHTML = el.innerHTML;
|
||||
// 清除按钮
|
||||
wrap.querySelectorAll('button').forEach(function(b) { b.remove(); });
|
||||
wrap.querySelectorAll('.copy-btn,.api-copy-btn,.method-copy-btn').forEach(function(b) { b.remove(); });
|
||||
wrap.querySelectorAll('.field-expand-toggle').forEach(function(b) { b.remove(); });
|
||||
// 展开所有折叠区域
|
||||
wrap.querySelectorAll('.method-header').forEach(function(h) { h.classList.remove('collapsed'); });
|
||||
wrap.querySelectorAll('.method-body').forEach(function(b) { b.style.display = ''; });
|
||||
wrap.querySelectorAll('.dto-block-title').forEach(function(t) { t.classList.remove('collapsed'); });
|
||||
// 清除 script/style
|
||||
wrap.querySelectorAll('script,style').forEach(function(s) { s.remove(); });
|
||||
var html = '<html><head><meta charset="utf-8"><style>' + getDocStyles() + '</style></head><body>' + wrap.innerHTML + '</body></html>';
|
||||
if (navigator.clipboard && navigator.clipboard.write) {
|
||||
var blob = new Blob([html], { type: 'text/html' });
|
||||
navigator.clipboard.write([new ClipboardItem({ 'text/html': blob, 'text/plain': new Blob([wrap.textContent || ''], { type: 'text/plain' }) })])
|
||||
.then(function() { markCopied(btn, '✔ 富文本已复制'); })
|
||||
.catch(function() { legacyRich(html, btn); markCopied(btn, '⚠ 已复制富文本'); });
|
||||
} else {
|
||||
legacyRich(html, btn);
|
||||
markCopied(btn, '⚠ 已复制富文本');
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 获取文档样式 ===== */
|
||||
function getDocStyles() {
|
||||
var css = '';
|
||||
document.querySelectorAll('style').forEach(function(s) { css += s.textContent; });
|
||||
return css;
|
||||
}
|
||||
|
||||
/* ===== 旧版富文本复制兼容 ===== */
|
||||
function legacyRich(html, btn) {
|
||||
var listener = function(e) {
|
||||
e.clipboardData.setData('text/html', html);
|
||||
e.clipboardData.setData('text/plain', '');
|
||||
e.preventDefault();
|
||||
document.removeEventListener('copy', listener);
|
||||
};
|
||||
document.addEventListener('copy', listener);
|
||||
document.execCommand('copy');
|
||||
document.removeEventListener('copy', listener);
|
||||
}
|
||||
|
||||
/* ===== 搜索过滤 ===== */
|
||||
function filterDocs() {
|
||||
var kw = (document.getElementById('searchInput') || {}).value;
|
||||
if (!kw || !kw.trim()) { resetSearch(); return; }
|
||||
kw = kw.trim().toLowerCase();
|
||||
var visible = 0;
|
||||
// 导航过滤
|
||||
document.querySelectorAll('.nav-group').forEach(function(g) {
|
||||
var hasVisible = false;
|
||||
g.querySelectorAll('.nav-item').forEach(function(item) {
|
||||
var txt = (item.textContent || '').toLowerCase();
|
||||
if (txt.indexOf(kw) >= 0) { item.classList.remove('search-hidden'); hasVisible = true; visible++; }
|
||||
else { item.classList.add('search-hidden'); }
|
||||
});
|
||||
if (hasVisible) {
|
||||
g.classList.remove('search-hidden');
|
||||
var t = g.querySelector('.nav-group-title');
|
||||
if (t) t.classList.add('open');
|
||||
var items = g.querySelector('.nav-group-items');
|
||||
if (items) items.style.maxHeight = items.scrollHeight + 'px';
|
||||
} else { g.classList.add('search-hidden'); }
|
||||
});
|
||||
// 内容过滤
|
||||
document.querySelectorAll('.api-card').forEach(function(card) {
|
||||
var txt = (card.textContent || '').toLowerCase();
|
||||
var match = txt.indexOf(kw) >= 0;
|
||||
if (!match) {
|
||||
card.querySelectorAll('.method-card').forEach(function(mc) {
|
||||
var mt = (mc.textContent || '').toLowerCase();
|
||||
if (mt.indexOf(kw) >= 0) { match = true; mc.classList.remove('search-hidden'); mc.querySelector('.method-header').classList.remove('collapsed'); mc.querySelector('.method-body').style.display = ''; }
|
||||
else { mc.classList.add('search-hidden'); }
|
||||
});
|
||||
}
|
||||
if (match) { card.classList.remove('search-hidden'); var body = card.querySelector('.api-body'); if (body) { body.style.display = ''; card.classList.remove('collapsed'); } }
|
||||
else { card.classList.add('search-hidden'); }
|
||||
});
|
||||
var searchCount = document.getElementById('searchCount');
|
||||
if (searchCount) searchCount.textContent = visible > 0 ? '匹配 ' + visible + ' 项' : '无匹配';
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
document.querySelectorAll('.search-hidden').forEach(function(el) { el.classList.remove('search-hidden'); });
|
||||
document.querySelectorAll('.search-highlight').forEach(function(el) { var p = el.parentNode; if (p) p.replaceChild(document.createTextNode(el.textContent), el); });
|
||||
var searchCount = document.getElementById('searchCount');
|
||||
if (searchCount) searchCount.textContent = '';
|
||||
}
|
||||
|
||||
/* ===== ESC 清空搜索 ===== */
|
||||
(function() {
|
||||
var si = document.getElementById('searchInput');
|
||||
if (si) { si.addEventListener('keydown', function(e) { if (e.key === 'Escape') { this.value = ''; resetSearch(); this.blur(); } }); }
|
||||
})();
|
||||
|
||||
/* ===== 手动选择区域复制富文本兼容 ===== */
|
||||
document.querySelectorAll('.api-card .api-body, .method-body').forEach(function(el) {
|
||||
el.addEventListener('copy', function(e) {
|
||||
var sel = window.getSelection();
|
||||
if (!sel.rangeCount) return;
|
||||
var range = sel.getRangeAt(0);
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.appendChild(range.cloneContents());
|
||||
wrapper.querySelectorAll('button,.copy-btn,.api-copy-btn,.method-copy-btn').forEach(function(b) { b.remove(); });
|
||||
var html = wrapper.innerHTML;
|
||||
var css = getDocStyles();
|
||||
e.clipboardData.setData('text/html', '<html><head><meta charset="utf-8"><style>' + css + '</style></head><body>' + html + '</body></html>');
|
||||
e.preventDefault();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user