功能已稳定
This commit is contained in:
@@ -232,9 +232,10 @@
|
||||
<phase>package</phase>
|
||||
<goals><goal>launch4j</goal></goals>
|
||||
<configuration>
|
||||
<headerType>console</headerType>
|
||||
<headerType>gui</headerType>
|
||||
<jar>${project.build.directory}/${project.build.finalName}.jar</jar>
|
||||
<outfile>${project.build.directory}/workutils.exe</outfile>
|
||||
<icon>${project.basedir}/src/main/resources/app_icon.ico</icon>
|
||||
<dontWrapJar>false</dontWrapJar>
|
||||
<classPath>
|
||||
<mainClass>org.springframework.boot.loader.JarLauncher</mainClass>
|
||||
|
||||
@@ -59,10 +59,13 @@ public class ApiDocWindow {
|
||||
// 批量
|
||||
private ComboBox<String> projectCmb;
|
||||
private CheckBox allProjectsCb;
|
||||
private CheckBox onlyInterfaceCb;
|
||||
private CheckBox apiPackageOnlyCb;
|
||||
private CheckBox facadeNameCb;
|
||||
|
||||
// 导出格式
|
||||
private ToggleGroup fmtGroup;
|
||||
private RadioButton rbHtml, rbMd, rbBoth;
|
||||
private RadioButton rbHtml, rbMd;
|
||||
|
||||
// 操作区
|
||||
private Button exportBtn;
|
||||
@@ -170,14 +173,43 @@ public class ApiDocWindow {
|
||||
fqnPanel.getChildren().addAll(new Label("全限定类名:"), fqnField);
|
||||
|
||||
VBox batchPanel = new VBox(Theme.S3);
|
||||
batchPanel.setPadding(new Insets(Theme.S3));
|
||||
batchPanel.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_CARD) + ";"
|
||||
+ "-fx-background-radius: " + Theme.RADIUS_SM + ";"
|
||||
+ "-fx-border-color: " + Theme.toWeb(Theme.PRIMARY) + ";"
|
||||
+ "-fx-border-width: 0 0 0 3; -fx-border-radius: 0 " + Theme.RADIUS_SM + " " + Theme.RADIUS_SM + " 0;");
|
||||
|
||||
// 项目选择行
|
||||
HBox projSelectRow = new HBox(Theme.S2);
|
||||
projSelectRow.setAlignment(Pos.CENTER_LEFT);
|
||||
projectCmb = new ComboBox<>();
|
||||
projectCmb.getStyleClass().add(Theme.CSS_COMBO);
|
||||
projectCmb.setStyle("-fx-font-family: 'Microsoft YaHei'; -fx-font-size: " + fsBody() + "px;");
|
||||
projectCmb.setMaxWidth(Double.MAX_VALUE);
|
||||
projectCmb.setPromptText("选择项目");
|
||||
HBox.setHgrow(projectCmb, Priority.ALWAYS);
|
||||
allProjectsCb = new CheckBox("全部项目");
|
||||
allProjectsCb.setFont(Theme.base(fsBody() - 1));
|
||||
batchPanel.getChildren().addAll(projectCmb, allProjectsCb);
|
||||
projSelectRow.getChildren().addAll(projectCmb, allProjectsCb);
|
||||
|
||||
// 过滤选项(默认勾选)
|
||||
HBox filterRow = new HBox(Theme.S4);
|
||||
filterRow.setAlignment(Pos.CENTER_LEFT);
|
||||
Label filterLabel = new Label("筛选:");
|
||||
filterLabel.setFont(Theme.base(fsBody() - 1));
|
||||
filterLabel.setTextFill(Theme.TEXT_SECONDARY);
|
||||
onlyInterfaceCb = new CheckBox("仅导出接口");
|
||||
onlyInterfaceCb.setFont(Theme.base(fsBody() - 1));
|
||||
onlyInterfaceCb.setSelected(true);
|
||||
facadeNameCb = new CheckBox("Facade结尾");
|
||||
facadeNameCb.setFont(Theme.base(fsBody() - 1));
|
||||
facadeNameCb.setSelected(true);
|
||||
apiPackageOnlyCb = new CheckBox("包名含 api");
|
||||
apiPackageOnlyCb.setFont(Theme.base(fsBody() - 1));
|
||||
apiPackageOnlyCb.setSelected(true);
|
||||
filterRow.getChildren().addAll(filterLabel, onlyInterfaceCb, facadeNameCb, apiPackageOnlyCb);
|
||||
|
||||
batchPanel.getChildren().addAll(projSelectRow, filterRow);
|
||||
|
||||
inputStack.getChildren().addAll(customPanel, fqnPanel, batchPanel);
|
||||
customPanel.visibleProperty().bind(rbCustomPath.selectedProperty());
|
||||
@@ -196,15 +228,12 @@ public class ApiDocWindow {
|
||||
fmtGroup = new ToggleGroup();
|
||||
rbHtml = new RadioButton("HTML");
|
||||
rbMd = new RadioButton("Markdown");
|
||||
rbBoth = new RadioButton("两者都导出");
|
||||
rbHtml.setToggleGroup(fmtGroup);
|
||||
rbMd.setToggleGroup(fmtGroup);
|
||||
rbBoth.setToggleGroup(fmtGroup);
|
||||
rbBoth.setSelected(true);
|
||||
rbHtml.setSelected(true);
|
||||
rbHtml.setFont(Theme.base(fsBody() - 1));
|
||||
rbMd.setFont(Theme.base(fsBody() - 1));
|
||||
rbBoth.setFont(Theme.base(fsBody() - 1));
|
||||
fmtRow.getChildren().addAll(fmtLabel, rbHtml, rbMd, rbBoth);
|
||||
fmtRow.getChildren().addAll(fmtLabel, rbHtml, rbMd);
|
||||
|
||||
// --- 分割线 ---
|
||||
Separator sep2 = new Separator();
|
||||
@@ -246,13 +275,10 @@ public class ApiDocWindow {
|
||||
colIdx.setCellValueFactory(new PropertyValueFactory<>("index"));
|
||||
colIdx.setPrefWidth(50);
|
||||
|
||||
TableColumn<HistoryRow, String> colMode = new TableColumn<>("导出模式");
|
||||
TableColumn<HistoryRow, String> colMode = new TableColumn<>("导出格式");
|
||||
colMode.setCellValueFactory(new PropertyValueFactory<>("mode"));
|
||||
colMode.setPrefWidth(90);
|
||||
|
||||
TableColumn<HistoryRow, String> colTarget = new TableColumn<>("目标");
|
||||
colTarget.setCellValueFactory(new PropertyValueFactory<>("target"));
|
||||
|
||||
TableColumn<HistoryRow, String> colFile = new TableColumn<>("导出文件");
|
||||
colFile.setCellValueFactory(new PropertyValueFactory<>("fileDisplay"));
|
||||
|
||||
@@ -266,7 +292,7 @@ public class ApiDocWindow {
|
||||
colAction.setPrefWidth(170);
|
||||
colAction.setResizable(false);
|
||||
|
||||
historyTable.getColumns().addAll(colAction, colIdx, colMode, colTarget, colFile, colTime);
|
||||
historyTable.getColumns().addAll(colAction, colIdx, colMode, colFile, colTime);
|
||||
|
||||
// ===================== 日志 =====================
|
||||
logArea = new TextArea();
|
||||
@@ -343,9 +369,7 @@ public class ApiDocWindow {
|
||||
}
|
||||
|
||||
private String getFmt() {
|
||||
if (rbHtml.isSelected()) return "1";
|
||||
if (rbMd.isSelected()) return "2";
|
||||
return "3";
|
||||
return rbHtml.isSelected() ? "1" : "2";
|
||||
}
|
||||
|
||||
// ==================== 单文件导出 ====================
|
||||
@@ -373,7 +397,7 @@ public class ApiDocWindow {
|
||||
doc.projectName = projName; doc.projectPath = inputPath;
|
||||
doc.classes = Collections.singletonList(ci);
|
||||
|
||||
doExport(projName, Collections.singletonList(doc), "单文件", inputPath);
|
||||
doExport(projName, Collections.singletonList(doc));
|
||||
}
|
||||
|
||||
// ==================== 目录导出 ====================
|
||||
@@ -401,7 +425,7 @@ public class ApiDocWindow {
|
||||
doc.projectName = projName; doc.projectPath = inputPath;
|
||||
doc.classes = classes;
|
||||
|
||||
doExport(projName, Collections.singletonList(doc), "目录", inputPath);
|
||||
doExport(projName, Collections.singletonList(doc));
|
||||
}
|
||||
|
||||
// ==================== 全限定名导出 ====================
|
||||
@@ -480,7 +504,13 @@ public class ApiDocWindow {
|
||||
selectedPaths.add(sel);
|
||||
}
|
||||
|
||||
log("开始批量导出,共 " + selectedPaths.size() + " 个项目");
|
||||
boolean onlyInterface = onlyInterfaceCb.isSelected();
|
||||
boolean apiPackageOnly = apiPackageOnlyCb.isSelected();
|
||||
boolean facadeName = facadeNameCb.isSelected();
|
||||
log("开始批量导出,共 " + selectedPaths.size() + " 个项目"
|
||||
+ (onlyInterface ? ",仅接口" : "")
|
||||
+ (facadeName ? ",Facade结尾" : "")
|
||||
+ (apiPackageOnly ? ",包名含api" : ""));
|
||||
|
||||
Set<String> seenSrcBases = new HashSet<>();
|
||||
List<ModuleInfo> modules = new ArrayList<>();
|
||||
@@ -513,10 +543,19 @@ public class ApiDocWindow {
|
||||
List<ApiDocModel.ClassInfo> classes = new ArrayList<>();
|
||||
for (Path f : javaFiles) {
|
||||
ApiDocModel.ClassInfo ci = parser.parseFile(f, m.name, true);
|
||||
if (ci != null && ci.methods != null && !ci.methods.isEmpty()) {
|
||||
ci.projectName = m.name;
|
||||
classes.add(ci);
|
||||
}
|
||||
if (ci == null || ci.methods == null || ci.methods.isEmpty()) continue;
|
||||
|
||||
// 批量过滤:仅接口
|
||||
if (onlyInterface && !"interface".equals(ci.type)) continue;
|
||||
// 批量过滤:Facade结尾
|
||||
if (facadeName && ci.className != null
|
||||
&& !ci.className.endsWith("Facade")) continue;
|
||||
// 批量过滤:包名含 api
|
||||
if (apiPackageOnly && ci.packageName != null
|
||||
&& !ci.packageName.toLowerCase().contains("api")) continue;
|
||||
|
||||
ci.projectName = m.name;
|
||||
classes.add(ci);
|
||||
}
|
||||
if (classes.isEmpty()) continue;
|
||||
classes.sort(Comparator.comparing(c -> c.className));
|
||||
@@ -536,17 +575,12 @@ public class ApiDocWindow {
|
||||
}
|
||||
|
||||
log("总计: " + allDocs.size() + " 个模块, " + totalClasses + " 个类");
|
||||
doExport("批量导出", allDocs, "批量", String.valueOf(selectedPaths.size()) + "个项目");
|
||||
doExport("批量导出", allDocs);
|
||||
}
|
||||
|
||||
// ==================== 导出输出 ====================
|
||||
|
||||
private void doExport(String name, List<ApiDocModel.ProjectApiDoc> docs) throws Exception {
|
||||
doExport(name, docs, "单文件", "");
|
||||
}
|
||||
|
||||
private void doExport(String name, List<ApiDocModel.ProjectApiDoc> docs,
|
||||
String mode, String target) throws Exception {
|
||||
ApiDocGenerator gen = new ApiDocGenerator(properties);
|
||||
ApiDocModel.ExportSelection sel = new ApiDocModel.ExportSelection();
|
||||
sel.scope = ApiDocModel.ExportScope.CUSTOM;
|
||||
@@ -558,36 +592,25 @@ public class ApiDocWindow {
|
||||
String filePath = p.toAbsolutePath().toString();
|
||||
Platform.runLater(() -> {
|
||||
log("HTML 已导出: " + filePath);
|
||||
addHistory(mode, target, filePath);
|
||||
addHistory("HTML", filePath);
|
||||
enableBtn();
|
||||
});
|
||||
} else if ("2".equals(fmt)) {
|
||||
} else {
|
||||
Path p = gen.generateMarkdown(sel, docs);
|
||||
String filePath = p.toAbsolutePath().toString();
|
||||
Platform.runLater(() -> {
|
||||
log("Markdown 已导出: " + filePath);
|
||||
addHistory(mode, target, filePath);
|
||||
enableBtn();
|
||||
});
|
||||
} else {
|
||||
Path html = gen.generateHtml(sel, docs);
|
||||
Path md = gen.generateMarkdown(sel, docs);
|
||||
String htmlPath = html.toAbsolutePath().toString();
|
||||
String mdPath = md.toAbsolutePath().toString();
|
||||
Platform.runLater(() -> {
|
||||
log("HTML: " + htmlPath);
|
||||
log("Markdown: " + mdPath);
|
||||
addHistory(mode, target, htmlPath);
|
||||
addHistory("Markdown", filePath);
|
||||
enableBtn();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void addHistory(String mode, String target, String filePath) {
|
||||
private void addHistory(String mode, String filePath) {
|
||||
int hc = ++historyCounter;
|
||||
String timestamp = SDF.format(new Date());
|
||||
historyRows.add(0, new HistoryRow(
|
||||
String.valueOf(hc), mode, target, filePath, timestamp, "打开"));
|
||||
String.valueOf(hc), mode, "", filePath, timestamp, "打开"));
|
||||
saveHistory();
|
||||
}
|
||||
|
||||
|
||||
@@ -61,183 +61,406 @@ public class ApiDocGenerator {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n");
|
||||
sb.append("<meta charset=\"UTF-8\">\n");
|
||||
sb.append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n");
|
||||
sb.append("<title>API 接口文档</title>\n");
|
||||
sb.append("<style>\n");
|
||||
sb.append("body{font-family:'Microsoft YaHei',sans-serif;margin:20px;color:#333;background:#f5f5f5}\n");
|
||||
sb.append("h1{color:#1890ff;border-bottom:2px solid #1890ff;padding-bottom:8px}\n");
|
||||
sb.append("h2{color:#333;margin-top:30px;background:#e6f7ff;padding:8px 12px;border-left:4px solid #1890ff}\n");
|
||||
sb.append("h3{color:#555;margin-top:20px}\n");
|
||||
sb.append(".meta{color:#999;font-size:13px;margin-bottom:15px}\n");
|
||||
sb.append(".card{background:#fff;border-radius:6px;padding:16px 20px;margin:12px 0;box-shadow:0 1px 4px rgba(0,0,0,.08)}\n");
|
||||
sb.append(".class-name{font-size:18px;font-weight:bold;color:#222}\n");
|
||||
sb.append(".class-path{color:#888;font-size:12px;margin:4px 0}\n");
|
||||
sb.append(".class-doc{color:#666;margin:8px 0;padding:8px;background:#fafafa;border-radius:4px;white-space:pre-wrap}\n");
|
||||
sb.append(".anno{display:inline-block;background:#fff7e6;color:#d46b08;border:1px solid #ffd591;border-radius:3px;padding:1px 6px;margin:2px;font-size:12px}\n");
|
||||
sb.append(".method{border-top:1px solid #f0f0f0;padding:10px 0}\n");
|
||||
sb.append(".method-name{font-weight:bold;color:#722ed1;font-size:15px}\n");
|
||||
sb.append(".method-sig{font-family:'Consolas','Courier New',monospace;background:#f6f8fa;padding:6px 10px;border-radius:4px;margin:6px 0;font-size:13px;overflow-x:auto}\n");
|
||||
sb.append(".method-doc{color:#666;margin:6px 0;padding:4px 8px;border-left:3px solid #e8e8e8;white-space:pre-wrap}\n");
|
||||
sb.append(".method-anno .anno{background:#f6ffed;color:#389e0d;border-color:#b7eb8f}\n");
|
||||
sb.append(".param-table{width:100%;border-collapse:collapse;margin:8px 0;font-size:13px}\n");
|
||||
sb.append(".param-table th{background:#fafafa;text-align:left;padding:6px 10px;border:1px solid #e8e8e8}\n");
|
||||
sb.append(".param-table td{padding:6px 10px;border:1px solid #e8e8e8}\n");
|
||||
sb.append(".toc{margin:0;padding:0;list-style:none}\n");
|
||||
sb.append(".toc li{margin:4px 0}\n");
|
||||
sb.append(".toc a{color:#1890ff;text-decoration:none}\n");
|
||||
sb.append(".toc a:hover{text-decoration:underline}\n");
|
||||
sb.append("</style>\n</head>\n<body>\n");
|
||||
appendCss(sb);
|
||||
sb.append("</head>\n<body>\n");
|
||||
|
||||
sb.append("<h1>API 接口文档</h1>\n");
|
||||
sb.append("<p class=\"meta\">生成时间: ").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("</p>\n");
|
||||
// 顶部栏
|
||||
sb.append("<header class=\"topbar\">\n");
|
||||
sb.append(" <div class=\"topbar-brand\">📄 API 接口文档</div>\n");
|
||||
sb.append(" <div class=\"topbar-tabs\">\n");
|
||||
sb.append(" <span class=\"tab active\">接口文档</span>\n");
|
||||
sb.append(" </div>\n");
|
||||
sb.append(" <div class=\"topbar-meta\">生成时间: ").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("</div>\n");
|
||||
sb.append("</header>\n");
|
||||
|
||||
// 目录
|
||||
sb.append("<h2>目录</h2>\n<ul class=\"toc\">\n");
|
||||
// 左侧导航 + 右侧内容
|
||||
sb.append("<div class=\"main-layout\">\n");
|
||||
sb.append("<aside class=\"sidebar\" id=\"sidebar\">\n");
|
||||
sb.append(" <div class=\"sidebar-title\">接口列表</div>\n");
|
||||
sb.append(" <nav class=\"nav-tree\">\n");
|
||||
for (ApiDocModel.ProjectApiDoc doc : docs) {
|
||||
sb.append("<li><b>").append(escapeHtml(doc.projectName)).append("</b> (").append(doc.classes.size())
|
||||
.append(" 个接口, ").append(doc.totalMethods()).append(" 个方法)</li>\n");
|
||||
sb.append("<ul>\n");
|
||||
String projId = "proj_" + safeId(doc.projectName);
|
||||
sb.append(" <div class=\"nav-group\">\n");
|
||||
sb.append(" <div class=\"nav-group-title\" onclick=\"toggleGroup(this)\">")
|
||||
.append("<span class=\"arrow\">▶</span> ")
|
||||
.append(escapeHtml(doc.projectName))
|
||||
.append(" <small>(").append(doc.classes.size()).append("接口/").append(doc.totalMethods()).append("方法)</small>")
|
||||
.append("</div>\n");
|
||||
sb.append(" <div class=\"nav-group-items\">\n");
|
||||
for (ApiDocModel.ClassInfo ci : doc.classes) {
|
||||
sb.append("<li><a href=\"#").append(anchorId(doc.projectName, ci.fullClassName)).append("\">")
|
||||
.append(escapeHtml(ci.className)).append("</a> (")
|
||||
.append(ci.methods != null ? ci.methods.size() : 0).append(" 个方法)</li>\n");
|
||||
}
|
||||
sb.append("</ul>\n");
|
||||
}
|
||||
sb.append("</ul>\n");
|
||||
|
||||
// 正文
|
||||
for (ApiDocModel.ProjectApiDoc doc : docs) {
|
||||
sb.append("<h2>").append(escapeHtml(doc.projectName)).append(" <small>(")
|
||||
.append(doc.classes.size()).append(" 接口)</small></h2>\n");
|
||||
|
||||
for (ApiDocModel.ClassInfo ci : doc.classes) {
|
||||
sb.append("<div class=\"card\" id=\"").append(anchorId(doc.projectName, ci.fullClassName)).append("\">\n");
|
||||
sb.append("<div class=\"class-name\">").append(ci.type).append(": ").append(escapeHtml(ci.className)).append("</div>\n");
|
||||
sb.append("<div class=\"class-path\">").append(escapeHtml(ci.fullClassName)).append("</div>\n");
|
||||
|
||||
if (ci.annotations != null && !ci.annotations.isEmpty()) {
|
||||
for (String a : ci.annotations) {
|
||||
sb.append("<span class=\"anno\">").append(escapeHtml(a)).append("</span> ");
|
||||
String anchor = anchorId(doc.projectName, ci.fullClassName);
|
||||
sb.append(" <a class=\"nav-item\" href=\"#").append(anchor).append("\" title=\"").append(escapeHtml(ci.fullClassName)).append("\">")
|
||||
.append("<span class=\"nav-item-icon\">").append("interface".equals(ci.type) ? "🔌" : "📦").append("</span>")
|
||||
.append(escapeHtml(ci.className))
|
||||
.append("</a>\n");
|
||||
// 方法级子项
|
||||
if (ci.methods != null) {
|
||||
for (ApiDocModel.MethodInfo mi : ci.methods) {
|
||||
String methodAnchor = anchor + "_" + safeId(mi.name);
|
||||
String http = extractHttpMethod(mi.annotations);
|
||||
sb.append(" <a class=\"nav-item nav-method\" href=\"#").append(methodAnchor).append("\" title=\"").append(escapeHtml(mi.name)).append("\">")
|
||||
.append("<span class=\"nav-method-dot\">●</span>")
|
||||
.append("<span class=\"nav-http-tag\">").append(http.isEmpty() ? "—" : http).append("</span>")
|
||||
.append(escapeHtml(mi.name))
|
||||
.append("</a>\n");
|
||||
}
|
||||
sb.append("<br>\n");
|
||||
}
|
||||
}
|
||||
sb.append(" </div>\n </div>\n");
|
||||
}
|
||||
sb.append(" </nav>\n</aside>\n");
|
||||
|
||||
if (ci.javadoc != null && !ci.javadoc.isEmpty()) {
|
||||
sb.append("<div class=\"class-doc\">").append(escapeHtml(ci.javadoc)).append("</div>\n");
|
||||
// 右侧内容区
|
||||
sb.append("<main class=\"content\">\n");
|
||||
|
||||
for (ApiDocModel.ProjectApiDoc doc : docs) {
|
||||
sb.append("<section class=\"project-section\">\n");
|
||||
sb.append(" <h2 class=\"project-title\">").append(escapeHtml(doc.projectName)).append("</h2>\n");
|
||||
|
||||
for (ApiDocModel.ClassInfo ci : doc.classes) {
|
||||
String anchor = anchorId(doc.projectName, ci.fullClassName);
|
||||
sb.append(" <div class=\"api-card\" id=\"").append(anchor).append("\">\n");
|
||||
|
||||
// 基本信息区
|
||||
sb.append(" <div class=\"api-header\">\n");
|
||||
sb.append(" <div class=\"api-header-left\">\n");
|
||||
String typeLabel = "interface".equals(ci.type) ? "接口" : "class".equals(ci.type) ? "类" : ci.type;
|
||||
sb.append(" <span class=\"type-badge ").append(ci.type).append("\">").append(typeLabel).append("</span>\n");
|
||||
sb.append(" <span class=\"api-name\">").append(escapeHtml(ci.className)).append("</span>\n");
|
||||
sb.append(" </div>\n");
|
||||
if (ci.methods != null) {
|
||||
sb.append(" <div class=\"api-header-right\">").append(ci.methods.size()).append(" 个方法</div>\n");
|
||||
}
|
||||
sb.append(" </div>\n");
|
||||
|
||||
// 基本信息表格
|
||||
sb.append(" <div class=\"info-grid\">\n");
|
||||
sb.append(" <div class=\"info-item\"><span class=\"info-label\">全限定名</span><span class=\"info-value mono\">").append(escapeHtml(ci.fullClassName)).append("</span></div>\n");
|
||||
if (ci.superClass != null) {
|
||||
sb.append(" <div class=\"info-item\"><span class=\"info-label\">父类</span><span class=\"info-value mono\">").append(escapeHtml(ci.superClass)).append("</span></div>\n");
|
||||
}
|
||||
if (ci.interfaces != null && !ci.interfaces.isEmpty()) {
|
||||
sb.append(" <div class=\"info-item\"><span class=\"info-label\">实现接口</span><span class=\"info-value mono\">").append(escapeHtml(String.join(", ", ci.interfaces))).append("</span></div>\n");
|
||||
}
|
||||
if (ci.annotations != null && !ci.annotations.isEmpty()) {
|
||||
sb.append(" <div class=\"info-item\"><span class=\"info-label\">注解</span><span class=\"info-value\">");
|
||||
for (String a : ci.annotations) {
|
||||
sb.append("<span class=\"anno-tag\">").append(escapeHtml(a)).append("</span>");
|
||||
}
|
||||
sb.append("</span></div>\n");
|
||||
}
|
||||
if (ci.javadoc != null && !ci.javadoc.isEmpty()) {
|
||||
sb.append(" <div class=\"info-item\"><span class=\"info-label\">描述</span><span class=\"info-value\">").append(escapeHtml(ci.javadoc)).append("</span></div>\n");
|
||||
}
|
||||
sb.append(" </div>\n");
|
||||
|
||||
// 方法列表
|
||||
if (ci.methods != null) {
|
||||
sb.append(" <div class=\"methods-section\">\n");
|
||||
sb.append(" <div class=\"section-label\">📋 接口方法</div>\n");
|
||||
for (ApiDocModel.MethodInfo mi : ci.methods) {
|
||||
sb.append("<div class=\"method\">\n");
|
||||
sb.append("<div class=\"method-name\">").append(escapeHtml(mi.name)).append("</div>\n");
|
||||
String methodId = anchor + "_" + safeId(mi.name);
|
||||
sb.append(" <div class=\"method-card\" id=\"").append(methodId).append("\">\n");
|
||||
|
||||
if (mi.annotations != null && !mi.annotations.isEmpty()) {
|
||||
sb.append("<div class=\"method-anno\">");
|
||||
for (String a : mi.annotations) {
|
||||
sb.append("<span class=\"anno\">").append(escapeHtml(a)).append("</span> ");
|
||||
}
|
||||
sb.append("</div>\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");
|
||||
}
|
||||
|
||||
sb.append("<div class=\"method-sig\">").append(escapeHtml(mi.fullSignature())).append("</div>\n");
|
||||
|
||||
if (mi.javadoc != null && !mi.javadoc.isEmpty()) {
|
||||
sb.append("<div class=\"method-doc\">").append(escapeHtml(mi.javadoc)).append("</div>\n");
|
||||
}
|
||||
|
||||
// 参数表
|
||||
if (mi.params != null && !mi.params.isEmpty()) {
|
||||
sb.append("<table class=\"param-table\"><tr><th>参数名</th><th>类型</th></tr>\n");
|
||||
for (ApiDocModel.ParamInfo pi : mi.params) {
|
||||
sb.append("<tr><td>").append(escapeHtml(pi.name)).append("</td><td><code>")
|
||||
.append(escapeHtml(pi.type)).append("</code></td></tr>\n");
|
||||
}
|
||||
sb.append("</table>\n");
|
||||
}
|
||||
|
||||
// 请求参数 DTO 展开
|
||||
if (mi.requestFields != null && !mi.requestFields.isEmpty()) {
|
||||
renderFieldsHtml(sb, mi.requestFields, "请求参数", 0);
|
||||
}
|
||||
|
||||
// 返回类型信息
|
||||
sb.append(" <span class=\"method-name\">").append(escapeHtml(mi.name)).append("</span>\n");
|
||||
if (mi.returnType != null) {
|
||||
sb.append("<div style=\"margin:6px 0;font-size:14px\">");
|
||||
sb.append("<strong>返回:</strong> <code>").append(escapeHtml(mi.returnType)).append("</code>");
|
||||
sb.append("</div>\n");
|
||||
sb.append(" <span class=\"method-return\">→ <code>").append(escapeHtml(mi.returnType)).append("</code></span>\n");
|
||||
}
|
||||
sb.append(" </div>\n");
|
||||
|
||||
// 方法体
|
||||
sb.append(" <div class=\"method-body\">\n");
|
||||
|
||||
// 注解
|
||||
if (mi.annotations != null && !mi.annotations.isEmpty()) {
|
||||
sb.append(" <div class=\"method-annotations\">\n");
|
||||
for (String a : mi.annotations) {
|
||||
sb.append(" <span class=\"anno-tag method-anno\">").append(escapeHtml(a)).append("</span>\n");
|
||||
}
|
||||
sb.append(" </div>\n");
|
||||
}
|
||||
|
||||
// 响应 DTO 展开
|
||||
// 描述
|
||||
if (mi.javadoc != null && !mi.javadoc.isEmpty()) {
|
||||
sb.append(" <div class=\"method-desc\">").append(escapeHtml(mi.javadoc)).append("</div>\n");
|
||||
}
|
||||
|
||||
// 签名
|
||||
sb.append(" <div class=\"method-signature\"><code>").append(escapeHtml(mi.fullSignature())).append("</code></div>\n");
|
||||
|
||||
// 方法参数
|
||||
if (mi.params != null && !mi.params.isEmpty()) {
|
||||
sb.append(" <div class=\"params-block\">\n");
|
||||
sb.append(" <div class=\"params-block-title\">方法参数</div>\n");
|
||||
sb.append(" <table class=\"data-table\"><thead><tr><th>参数名</th><th>类型</th><th>全路径</th></tr></thead><tbody>\n");
|
||||
for (ApiDocModel.ParamInfo pi : mi.params) {
|
||||
sb.append(" <tr><td><code>").append(escapeHtml(pi.name)).append("</code></td>")
|
||||
.append("<td><code>").append(escapeHtml(pi.type)).append("</code></td>")
|
||||
.append("<td class=\"fqn\">").append(escapeHtml(pi.fullQualifiedName != null ? pi.fullQualifiedName : "")).append("</td></tr>\n");
|
||||
}
|
||||
sb.append(" </tbody></table>\n </div>\n");
|
||||
}
|
||||
|
||||
// 请求 DTO
|
||||
if (mi.requestFields != null && !mi.requestFields.isEmpty()) {
|
||||
renderFieldsHtmlV2(sb, mi.requestFields, "请求参数", 0);
|
||||
}
|
||||
|
||||
// 返回 DTO
|
||||
if (mi.responseFields != null && !mi.responseFields.isEmpty()) {
|
||||
renderFieldsHtml(sb, mi.responseFields, "返回结果", 0);
|
||||
renderFieldsHtmlV2(sb, mi.responseFields, "返回结果", 0);
|
||||
}
|
||||
|
||||
// throws
|
||||
if (mi.throwsTypes != null && !mi.throwsTypes.isEmpty()) {
|
||||
sb.append("<div style=\"color:#cf1322;font-size:13px\">throws: ");
|
||||
sb.append(escapeHtml(String.join(", ", mi.throwsTypes)));
|
||||
sb.append("</div>\n");
|
||||
sb.append(" <div class=\"throws-block\">⚠ throws: <code>")
|
||||
.append(escapeHtml(String.join(", ", mi.throwsTypes)))
|
||||
.append("</code></div>\n");
|
||||
}
|
||||
|
||||
sb.append("</div>\n"); // .method
|
||||
sb.append(" </div>\n"); // .method-body
|
||||
sb.append(" </div>\n"); // .method-card
|
||||
}
|
||||
sb.append(" </div>\n"); // .methods-section
|
||||
}
|
||||
|
||||
sb.append("</div>\n"); // .card
|
||||
sb.append(" </div>\n"); // .api-card
|
||||
}
|
||||
sb.append("</section>\n");
|
||||
}
|
||||
|
||||
sb.append("</main>\n");
|
||||
sb.append("</div>\n"); // .main-layout
|
||||
|
||||
// JS: 折叠/展开 + 侧栏导航
|
||||
appendJs(sb);
|
||||
|
||||
sb.append("</body>\n</html>");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** 递归渲染字段表(HTML) */
|
||||
private void renderFieldsHtml(StringBuilder sb, List<ApiDocModel.FieldInfo> fields, String title, int depth) {
|
||||
/** 新版本字段渲染 —— 可折叠卡片 */
|
||||
private void renderFieldsHtmlV2(StringBuilder sb, List<ApiDocModel.FieldInfo> fields, String title, int depth) {
|
||||
if (fields == null || fields.isEmpty()) return;
|
||||
String margin = depth > 0 ? "margin-left:" + (depth * 20) + "px;" : "";
|
||||
String headerColor = depth == 0 ? "#1890ff" : depth == 1 ? "#52c41a" : "#fa8c16";
|
||||
String indent = depth > 0 ? " style=\"margin-left:" + (depth * 20) + "px;\"" : "";
|
||||
String borderColor = depth == 0 ? "#1677ff" : depth == 1 ? "#52c41a" : "#fa8c16";
|
||||
String titleIcon = depth == 0 ? "📥" : "└";
|
||||
|
||||
sb.append("<div style=\"").append(margin).append("margin-top:8px;\">\n");
|
||||
sb.append("<strong style=\"color:").append(headerColor).append("\">").append(escapeHtml(title)).append("</strong>\n");
|
||||
sb.append("<table class=\"param-table\">\n");
|
||||
sb.append("<tr><th>参数名</th><th>参数类型</th><th>描述</th><th>全路径</th></tr>\n");
|
||||
sb.append(" <div class=\"dto-block\"").append(indent).append(">\n");
|
||||
sb.append(" <div class=\"dto-block-title\" style=\"border-left-color:").append(borderColor)
|
||||
.append("\" onclick=\"toggleDto(this)\">")
|
||||
.append("<span class=\"dto-arrow\">▼</span> ")
|
||||
.append(titleIcon).append(" ").append(escapeHtml(title))
|
||||
.append(" <small>(").append(fields.size()).append(" 字段)</small>")
|
||||
.append("</div>\n");
|
||||
sb.append(" <div class=\"dto-block-body\">\n");
|
||||
sb.append(" <table class=\"data-table\"><thead><tr><th>参数名</th><th>参数类型</th><th>描述</th></tr></thead><tbody>\n");
|
||||
for (ApiDocModel.FieldInfo f : fields) {
|
||||
sb.append("<tr>");
|
||||
sb.append("<td><code>").append(escapeHtml(f.name)).append("</code></td>");
|
||||
sb.append(" <tr>");
|
||||
sb.append("<td><code class=\"field-name\">").append(escapeHtml(f.name)).append("</code></td>");
|
||||
sb.append("<td><code>").append(escapeHtml(f.type)).append("</code></td>");
|
||||
sb.append("<td>").append(escapeHtml(f.javadoc != null ? f.javadoc : "")).append("</td>");
|
||||
sb.append("<td style=\"font-size:11px;color:#888\">").append(escapeHtml(f.fullQualifiedName != null ? f.fullQualifiedName : "")).append("</td>");
|
||||
sb.append("<td>").append(escapeHtml(f.javadoc != null ? f.javadoc : "—")).append("</td>");
|
||||
sb.append("</tr>\n");
|
||||
}
|
||||
sb.append("</table>\n");
|
||||
sb.append("</div>\n");
|
||||
sb.append(" </tbody></table>\n");
|
||||
sb.append(" </div>\n");
|
||||
sb.append(" </div>\n");
|
||||
|
||||
// 递归渲染嵌套字段
|
||||
for (ApiDocModel.FieldInfo f : fields) {
|
||||
if (f.children != null && !f.children.isEmpty()) {
|
||||
renderFieldsHtml(sb, f.children, f.type + " 字段", depth + 1);
|
||||
renderFieldsHtmlV2(sb, f.children, f.type, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 提取 HTTP 方法注解(@GetMapping → GET) */
|
||||
private String extractHttpMethod(List<String> annotations) {
|
||||
if (annotations == null) return "";
|
||||
for (String a : annotations) {
|
||||
if (a.contains("GetMapping")) return "GET";
|
||||
if (a.contains("PostMapping")) return "POST";
|
||||
if (a.contains("PutMapping")) return "PUT";
|
||||
if (a.contains("DeleteMapping")) return "DELETE";
|
||||
if (a.contains("RequestMapping")) return "REQ";
|
||||
if (a.contains("FeignClient") || a.contains("RpcMethod")) return "RPC";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String safeId(String s) {
|
||||
if (s == null) return "x";
|
||||
return s.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5_-]", "_");
|
||||
}
|
||||
|
||||
/** CSS 样式 */
|
||||
private void appendCss(StringBuilder sb) {
|
||||
sb.append("<style>\n");
|
||||
sb.append("*{margin:0;padding:0;box-sizing:border-box}\n");
|
||||
sb.append("body{font-family:'Microsoft YaHei',-apple-system,sans-serif;color:#1d1d1f;background:#f2f3f5;display:flex;flex-direction:column;min-height:100vh}\n");
|
||||
|
||||
// 顶部栏
|
||||
sb.append(".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}\n");
|
||||
sb.append(".topbar-brand{font-weight:700;font-size:16px;color:#1d1d1f;margin-right:32px}\n");
|
||||
sb.append(".topbar-tabs{display:flex;gap:0}\n");
|
||||
sb.append(".tab{padding:12px 20px;font-size:14px;color:#86909c;cursor:pointer;border-bottom:2px solid transparent;transition:.2s}\n");
|
||||
sb.append(".tab.active{color:#1677ff;border-bottom-color:#1677ff;font-weight:600}\n");
|
||||
sb.append(".topbar-meta{margin-left:auto;font-size:12px;color:#86909c}\n");
|
||||
|
||||
// 布局
|
||||
sb.append(".main-layout{display:flex;flex:1}\n");
|
||||
|
||||
// 侧栏
|
||||
sb.append(".sidebar{width:280px;min-width:280px;background:#fff;border-right:1px solid #e5e6eb;overflow-y:auto;padding:16px 0;position:sticky;top:48px;height:calc(100vh - 48px)}\n");
|
||||
sb.append(".sidebar-title{font-size:14px;font-weight:700;color:#1d1d1f;padding:0 16px 12px;border-bottom:1px solid #f2f3f5;margin-bottom:8px}\n");
|
||||
sb.append(".nav-group{margin-bottom:4px}\n");
|
||||
sb.append(".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}\n");
|
||||
sb.append(".nav-group-title:hover{background:#f2f3f5}\n");
|
||||
sb.append(".nav-group-title .arrow{font-size:10px;transition:transform .2s;display:inline-block}\n");
|
||||
sb.append(".nav-group-title.open .arrow{transform:rotate(90deg)}\n");
|
||||
sb.append(".nav-group-title small{font-weight:400;color:#86909c;font-size:11px}\n");
|
||||
sb.append(".nav-group-items{overflow:hidden;max-height:0;transition:max-height .3s ease}\n");
|
||||
sb.append(".nav-group-title.open+.nav-group-items{max-height:2000px}\n");
|
||||
sb.append(".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}\n");
|
||||
sb.append(".nav-item:hover,.nav-item.active{color:#1677ff;background:#e8f3ff}\n");
|
||||
sb.append(".nav-item-icon{font-size:14px;flex-shrink:0}\n");
|
||||
sb.append(".nav-method{padding-left:48px;font-size:11px;gap:6px}\n");
|
||||
sb.append(".nav-method-dot{font-size:6px;color:#c9cdd4;flex-shrink:0;line-height:1}\n");
|
||||
sb.append(".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}\n");
|
||||
sb.append(".nav-http-tag.get{background:#e8f3ff;color:#1677ff}\n");
|
||||
sb.append(".nav-http-tag.post{background:#e8ffea;color:#00b42a}\n");
|
||||
sb.append(".nav-http-tag.put{background:#fff7e6;color:#ff7d00}\n");
|
||||
sb.append(".nav-http-tag.delete{background:#ffece8;color:#f53f3f}\n");
|
||||
sb.append(".nav-http-tag.req{background:#f2f3f5;color:#86909c}\n");
|
||||
sb.append(".nav-http-tag.rpc{background:#f3e8ff;color:#722ed1}\n");
|
||||
|
||||
// 内容区
|
||||
sb.append(".content{flex:1;padding:24px;overflow-y:auto;max-width:calc(100vw - 280px)}\n");
|
||||
sb.append(".project-section{margin-bottom:32px}\n");
|
||||
sb.append(".project-title{font-size:22px;font-weight:700;color:#1d1d1f;margin-bottom:16px;padding-bottom:8px;border-bottom:1px solid #e5e6eb}\n");
|
||||
|
||||
// 卡片
|
||||
sb.append(".api-card{background:#fff;border-radius:8px;margin-bottom:16px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.06)}\n");
|
||||
sb.append(".api-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #f2f3f5}\n");
|
||||
sb.append(".api-header-left{display:flex;align-items:center;gap:10px}\n");
|
||||
sb.append(".api-name{font-size:16px;font-weight:700;color:#1d1d1f}\n");
|
||||
sb.append(".api-header-right{font-size:13px;color:#86909c}\n");
|
||||
sb.append(".type-badge{display:inline-block;padding:2px 10px;border-radius:4px;font-size:12px;font-weight:600}\n");
|
||||
sb.append(".type-badge.interface{background:#e8f3ff;color:#1677ff}\n");
|
||||
sb.append(".type-badge.class{background:#e8ffea;color:#00b42a}\n");
|
||||
|
||||
// 信息网格
|
||||
sb.append(".info-grid{padding:16px 20px;display:grid;grid-template-columns:1fr 1fr;gap:10px}\n");
|
||||
sb.append(".info-item{display:flex;align-items:baseline;gap:8px;font-size:13px}\n");
|
||||
sb.append(".info-label{color:#86909c;white-space:nowrap;min-width:64px}\n");
|
||||
sb.append(".info-value{color:#1d1d1f;word-break:break-all}\n");
|
||||
sb.append(".info-value.mono{font-family:'Consolas','Courier New',monospace;font-size:12px;color:#4e5969}\n");
|
||||
|
||||
// 注解标签
|
||||
sb.append(".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}\n");
|
||||
sb.append(".method-anno{background:#f6ffed;color:#389e0d;border-color:#b7eb8f}\n");
|
||||
|
||||
// 方法列表
|
||||
sb.append(".methods-section{padding:0 20px 16px}\n");
|
||||
sb.append(".section-label{font-size:14px;font-weight:700;color:#1d1d1f;padding:8px 0;border-top:1px solid #f2f3f5}\n");
|
||||
sb.append(".method-card{border:1px solid #f2f3f5;border-radius:8px;margin:10px 0;overflow:hidden}\n");
|
||||
sb.append(".method-header{display:flex;align-items:center;gap:10px;padding:12px 16px;background:#fafbfc;cursor:pointer;user-select:none;transition:background .15s}\n");
|
||||
sb.append(".method-header:hover{background:#f2f3f5}\n");
|
||||
sb.append(".method-arrow{font-size:10px;transition:transform .2s;color:#86909c}\n");
|
||||
sb.append(".method-header.collapsed .method-arrow{transform:rotate(-90deg)}\n");
|
||||
sb.append(".method-name{font-size:14px;font-weight:700;color:#1677ff;font-family:'Consolas','Courier New',monospace}\n");
|
||||
sb.append(".method-return{font-size:12px;color:#86909c;margin-left:auto}\n");
|
||||
sb.append(".method-return code{color:#00b42a;background:#e8ffea;padding:2px 6px;border-radius:3px;font-size:11px}\n");
|
||||
sb.append(".http-badge{display:inline-block;padding:2px 8px;border-radius:3px;font-size:11px;font-weight:700;color:#fff}\n");
|
||||
sb.append(".http-badge.get{background:#1677ff}\n");
|
||||
sb.append(".http-badge.post{background:#00b42a}\n");
|
||||
sb.append(".http-badge.put{background:#ff7d00}\n");
|
||||
sb.append(".http-badge.delete{background:#f53f3f}\n");
|
||||
sb.append(".http-badge.req{background:#86909c}\n");
|
||||
sb.append(".http-badge.rpc{background:#722ed1}\n");
|
||||
|
||||
sb.append(".method-body{padding:12px 16px}\n");
|
||||
sb.append(".method-header.collapsed+.method-body{display:none}\n");
|
||||
sb.append(".method-annotations{margin-bottom:8px}\n");
|
||||
sb.append(".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}\n");
|
||||
sb.append(".method-signature{padding:10px 14px;background:#1e1e1e;border-radius:6px;margin-bottom:12px;overflow-x:auto}\n");
|
||||
sb.append(".method-signature code{font-family:'Consolas','Courier New',monospace;font-size:13px;color:#d4d4d4}\n");
|
||||
|
||||
// 参数块
|
||||
sb.append(".params-block{margin-bottom:12px}\n");
|
||||
sb.append(".params-block-title{font-size:13px;font-weight:600;color:#4e5969;margin-bottom:6px}\n");
|
||||
|
||||
// DTO 块
|
||||
sb.append(".dto-block{margin-bottom:10px;border:1px solid #e5e6eb;border-radius:6px;overflow:hidden}\n");
|
||||
sb.append(".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}\n");
|
||||
sb.append(".dto-block-title:hover{background:#f2f3f5}\n");
|
||||
sb.append(".dto-block-title small{font-weight:400;color:#86909c}\n");
|
||||
sb.append(".dto-arrow{font-size:10px;transition:transform .2s;color:#86909c}\n");
|
||||
sb.append(".dto-block-title.collapsed .dto-arrow{transform:rotate(-90deg)}\n");
|
||||
sb.append(".dto-block-body{padding:0}\n");
|
||||
sb.append(".dto-block-title.collapsed+.dto-block-body{display:none}\n");
|
||||
|
||||
// 表格
|
||||
sb.append(".data-table{width:100%;border-collapse:collapse;font-size:12px}\n");
|
||||
sb.append(".data-table th{background:#fafbfc;text-align:left;padding:7px 10px;border:1px solid #e5e6eb;font-weight:600;color:#4e5969;white-space:nowrap}\n");
|
||||
sb.append(".data-table td{padding:7px 10px;border:1px solid #e5e6eb;color:#1d1d1f}\n");
|
||||
sb.append(".data-table tr:hover td{background:#f7f8fa}\n");
|
||||
sb.append(".data-table code{font-family:'Consolas','Courier New',monospace;font-size:11px;background:#f2f3f5;padding:1px 5px;border-radius:2px;color:#1677ff}\n");
|
||||
sb.append(".data-table code.field-name{color:#d46b08}\n");
|
||||
sb.append(".fqn{font-size:11px;color:#86909c;font-family:'Consolas','Courier New',monospace}\n");
|
||||
|
||||
// throws
|
||||
sb.append(".throws-block{font-size:12px;color:#f53f3f;padding:6px 0}\n");
|
||||
sb.append(".throws-block code{background:#fff2f0;padding:2px 6px;border-radius:2px}\n");
|
||||
|
||||
// 响应式
|
||||
sb.append("@media(max-width:900px){.sidebar{display:none}.content{max-width:100vw}}\n");
|
||||
sb.append("</style>\n");
|
||||
}
|
||||
|
||||
/** JS 交互 */
|
||||
private void appendJs(StringBuilder sb) {
|
||||
sb.append("<script>\n");
|
||||
// 侧栏分组折叠
|
||||
sb.append("document.querySelectorAll('.nav-group-title').forEach(function(el){el.classList.add('open')});\n");
|
||||
sb.append("function toggleGroup(el){el.classList.toggle('open')}\n");
|
||||
// 方法折叠
|
||||
sb.append("function toggleMethod(el){el.classList.toggle('collapsed')}\n");
|
||||
// DTO 折叠
|
||||
sb.append("function toggleDto(el){el.classList.toggle('collapsed')}\n");
|
||||
// 侧栏项目激活高亮
|
||||
sb.append("var navItems=document.querySelectorAll('.nav-item');\n");
|
||||
sb.append("navItems.forEach(function(a){a.addEventListener('click',function(){navItems.forEach(function(x){x.classList.remove('active')});a.classList.add('active')})});\n");
|
||||
// 默认展开第一个项目组
|
||||
sb.append("var firstGroup=document.querySelector('.nav-group-title');if(firstGroup)firstGroup.classList.add('open');\n");
|
||||
sb.append("</script>\n");
|
||||
}
|
||||
|
||||
/** 递归渲染字段表(Markdown) */
|
||||
private void renderFieldsMd(StringBuilder sb, List<ApiDocModel.FieldInfo> fields, String title, int depth) {
|
||||
if (fields == null || fields.isEmpty()) return;
|
||||
String prefix = depth > 0 ? "**" + title + "**" : "**" + title + "**";
|
||||
sb.append(prefix).append("\n\n");
|
||||
sb.append("| 参数名 | 参数类型 | 描述 | 全路径 |\n");
|
||||
sb.append("|--------|----------|------|--------|\n");
|
||||
String indent = depth > 0 ? " ".repeat(depth) : "";
|
||||
String icon = depth == 0 ? "📥 " : " └ ";
|
||||
sb.append(indent).append("**").append(icon).append(title).append("**(").append(fields.size()).append(" 字段)\n\n");
|
||||
sb.append(indent).append("| 参数名 | 参数类型 | 描述 |\n");
|
||||
sb.append(indent).append("|--------|----------|------|\n");
|
||||
for (ApiDocModel.FieldInfo f : fields) {
|
||||
sb.append("| `").append(f.name).append("` | `").append(f.type).append("` | ")
|
||||
.append(f.javadoc != null ? f.javadoc.replace("\n", " ") : "").append(" | ")
|
||||
.append(f.fullQualifiedName != null ? f.fullQualifiedName : "").append(" |\n");
|
||||
sb.append(indent).append("| `").append(f.name).append("` | `").append(f.type).append("` | ")
|
||||
.append(f.javadoc != null ? f.javadoc.replace("\n", " ").replace("|", "\\|") : "—").append(" |\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
|
||||
// 递归渲染嵌套字段
|
||||
for (ApiDocModel.FieldInfo f : fields) {
|
||||
if (f.children != null && !f.children.isEmpty()) {
|
||||
renderFieldsMd(sb, f.children, f.type + " 字段", depth + 1);
|
||||
renderFieldsMd(sb, f.children, f.type, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,77 +469,72 @@ public class ApiDocGenerator {
|
||||
|
||||
private String buildMarkdown(ApiDocModel.ExportSelection selection, List<ApiDocModel.ProjectApiDoc> docs) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# API 接口文档\n\n");
|
||||
sb.append("# 📄 API 接口文档\n\n");
|
||||
sb.append("> 生成时间: ").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("\n\n");
|
||||
|
||||
// 目录
|
||||
sb.append("## 目录\n\n");
|
||||
int classIdx = 1;
|
||||
for (ApiDocModel.ProjectApiDoc doc : docs) {
|
||||
sb.append("- **").append(doc.projectName).append("** (").append(doc.classes.size())
|
||||
.append(" 个接口, ").append(doc.totalMethods()).append(" 个方法)\n");
|
||||
sb.append("- **").append(doc.projectName).append("**(").append(doc.classes.size())
|
||||
.append(" 接口 / ").append(doc.totalMethods()).append(" 方法)\n");
|
||||
for (ApiDocModel.ClassInfo ci : doc.classes) {
|
||||
sb.append(" - [").append(classIdx).append(". ").append(ci.className).append("](#")
|
||||
.append(anchorId(doc.projectName, ci.fullClassName)).append(") (")
|
||||
.append(ci.methods != null ? ci.methods.size() : 0).append(" 个方法)\n");
|
||||
classIdx++;
|
||||
sb.append(" - [").append(ci.className).append("](#")
|
||||
.append(anchorId(doc.projectName, ci.fullClassName)).append(")(")
|
||||
.append(ci.methods != null ? ci.methods.size() : 0).append(" 方法)\n");
|
||||
}
|
||||
}
|
||||
sb.append("\n---\n\n");
|
||||
|
||||
// 正文
|
||||
classIdx = 1;
|
||||
for (ApiDocModel.ProjectApiDoc doc : docs) {
|
||||
sb.append("## ").append(doc.projectName).append("\n\n");
|
||||
sb.append("## 📦 ").append(doc.projectName).append("\n\n");
|
||||
sb.append("> ").append(doc.classes.size()).append(" 个接口 | ")
|
||||
.append(doc.totalMethods()).append(" 个方法\n\n");
|
||||
|
||||
for (ApiDocModel.ClassInfo ci : doc.classes) {
|
||||
sb.append("### ").append(classIdx).append(". ").append(ci.className).append("\n\n");
|
||||
sb.append("<a id=\"").append(anchorId(doc.projectName, ci.fullClassName)).append("\"></a>\n\n");
|
||||
|
||||
sb.append("- **类型**: `").append(ci.type).append("`\n");
|
||||
sb.append("- **全限定名**: `").append(ci.fullClassName).append("`\n");
|
||||
String typeLabel = "interface".equals(ci.type) ? "🔌 接口" : "📦 类";
|
||||
String anchor = anchorId(doc.projectName, ci.fullClassName);
|
||||
sb.append("### <a id=\"").append(anchor).append("\"></a>").append(typeLabel).append(":").append(ci.className).append("\n\n");
|
||||
|
||||
// 基本信息
|
||||
sb.append("**基本信息**\n\n");
|
||||
sb.append("| 属性 | 值 |\n");
|
||||
sb.append("|------|----|\n");
|
||||
sb.append("| 全限定名 | `").append(ci.fullClassName).append("` |\n");
|
||||
if (ci.superClass != null) sb.append("| 父类 | `").append(ci.superClass).append("` |\n");
|
||||
if (ci.interfaces != null && !ci.interfaces.isEmpty())
|
||||
sb.append("| 实现接口 | `").append(String.join(", ", ci.interfaces)).append("` |\n");
|
||||
if (ci.annotations != null && !ci.annotations.isEmpty()) {
|
||||
sb.append("- **注解**: ");
|
||||
for (String a : ci.annotations) {
|
||||
sb.append("`").append(a).append("` ");
|
||||
sb.append("| 注解 | ");
|
||||
for (String a : ci.annotations) sb.append("`").append(a).append("` ");
|
||||
sb.append("|\n");
|
||||
}
|
||||
if (ci.javadoc != null && !ci.javadoc.isEmpty())
|
||||
sb.append("| 描述 | ").append(ci.javadoc.replace("\n", " ")).append(" |\n");
|
||||
sb.append("\n");
|
||||
|
||||
// 方法列表
|
||||
if (ci.methods != null && !ci.methods.isEmpty()) {
|
||||
// 方法概览
|
||||
sb.append("**方法概览**(").append(ci.methods.size()).append(" 个)\n\n");
|
||||
sb.append("| HTTP | 方法 | 返回类型 |\n");
|
||||
sb.append("|------|------|----------|\n");
|
||||
for (ApiDocModel.MethodInfo mi : ci.methods) {
|
||||
String http = extractHttpMethod(mi.annotations);
|
||||
sb.append("| ").append(http.isEmpty() ? "—" : "`" + http + "`")
|
||||
.append(" | `").append(mi.name).append("` | `").append(mi.returnType).append("` |\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
if (ci.javadoc != null && !ci.javadoc.isEmpty()) {
|
||||
sb.append("\n> ").append(ci.javadoc.replace("\n", "\n> ")).append("\n");
|
||||
}
|
||||
|
||||
// 方法
|
||||
if (ci.methods != null) {
|
||||
sb.append("\n#### 方法\n\n");
|
||||
sb.append("| # | 方法 | 返回类型 | 参数 |\n");
|
||||
sb.append("|---|------|---------|------|\n");
|
||||
int midx = 1;
|
||||
for (ApiDocModel.MethodInfo mi : ci.methods) {
|
||||
sb.append("| ").append(midx++).append(" | `").append(mi.name).append("` | `")
|
||||
.append(mi.returnType).append("` | ");
|
||||
if (mi.params != null && !mi.params.isEmpty()) {
|
||||
for (int i = 0; i < mi.params.size(); i++) {
|
||||
if (i > 0) sb.append(", ");
|
||||
sb.append("`").append(mi.params.get(i).type).append(" ").append(mi.params.get(i).name).append("`");
|
||||
}
|
||||
} else {
|
||||
sb.append("(无)");
|
||||
}
|
||||
sb.append(" |\n");
|
||||
}
|
||||
|
||||
// 方法详情
|
||||
sb.append("\n#### 方法详情\n\n");
|
||||
for (ApiDocModel.MethodInfo mi : ci.methods) {
|
||||
sb.append("##### `").append(mi.name).append("`\n\n");
|
||||
sb.append("#### `").append(mi.name).append("`\n\n");
|
||||
|
||||
if (mi.javadoc != null && !mi.javadoc.isEmpty()) {
|
||||
sb.append("> ").append(mi.javadoc.replace("\n", "\n> ")).append("\n\n");
|
||||
}
|
||||
|
||||
if (mi.annotations != null && !mi.annotations.isEmpty()) {
|
||||
sb.append("注解: ");
|
||||
for (String a : mi.annotations) {
|
||||
sb.append("`").append(a).append("` ");
|
||||
}
|
||||
@@ -325,37 +543,31 @@ public class ApiDocGenerator {
|
||||
|
||||
sb.append("```java\n").append(mi.fullSignature()).append("\n```\n\n");
|
||||
|
||||
if (mi.javadoc != null && !mi.javadoc.isEmpty()) {
|
||||
sb.append("> ").append(mi.javadoc.replace("\n", "\n> ")).append("\n\n");
|
||||
}
|
||||
|
||||
// 方法参数
|
||||
if (mi.params != null && !mi.params.isEmpty()) {
|
||||
sb.append("**方法参数**\n\n");
|
||||
sb.append("| 参数名 | 类型 | 全路径 |\n");
|
||||
sb.append("|--------|------|--------|\n");
|
||||
for (ApiDocModel.ParamInfo pi : mi.params) {
|
||||
sb.append("| ").append(pi.name).append(" | `").append(pi.type).append("` | ")
|
||||
.append(pi.fullQualifiedName != null ? pi.fullQualifiedName : "").append(" |\n");
|
||||
sb.append("| `").append(pi.name).append("` | `").append(pi.type).append("` | `")
|
||||
.append(pi.fullQualifiedName != null ? pi.fullQualifiedName : "—").append("` |\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
// 请求参数 DTO 展开
|
||||
// 请求 DTO
|
||||
if (mi.requestFields != null && !mi.requestFields.isEmpty()) {
|
||||
renderFieldsMd(sb, mi.requestFields, "请求参数", 0);
|
||||
}
|
||||
|
||||
// 返回值
|
||||
if (mi.returnType != null) {
|
||||
sb.append("**返回:** `").append(mi.returnType).append("`\n\n");
|
||||
}
|
||||
|
||||
// 响应 DTO 展开
|
||||
// 返回 DTO
|
||||
if (mi.responseFields != null && !mi.responseFields.isEmpty()) {
|
||||
renderFieldsMd(sb, mi.responseFields, "返回结果", 0);
|
||||
}
|
||||
|
||||
// throws
|
||||
if (mi.throwsTypes != null && !mi.throwsTypes.isEmpty()) {
|
||||
sb.append("throws: `").append(String.join(", ", mi.throwsTypes)).append("`\n\n");
|
||||
sb.append("⚠ **throws:** `").append(String.join(", ", mi.throwsTypes)).append("`\n\n");
|
||||
}
|
||||
|
||||
sb.append("---\n\n");
|
||||
@@ -363,7 +575,6 @@ public class ApiDocGenerator {
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
classIdx++;
|
||||
}
|
||||
sb.append("\n---\n\n");
|
||||
}
|
||||
|
||||
@@ -360,10 +360,12 @@ public class JavaSourceParser {
|
||||
for (int i = startIdx; i < lines.length; i++) {
|
||||
String line = lines[i].trim();
|
||||
if (JAVADOC_END.matcher(line).find()) {
|
||||
// 最后一行的 */ 内容
|
||||
Matcher lm = JAVADOC_LINE.matcher(line);
|
||||
if (lm.find() && !lm.group(1).isEmpty()) {
|
||||
sb.append(lm.group(1));
|
||||
// 最后一行(含 */),去掉尾部 */
|
||||
String cleaned = line.replaceFirst("\\s*\\*/\\s*$", "");
|
||||
// 去掉开头的 *
|
||||
cleaned = cleaned.replaceFirst("^\\s*\\*\\s?", "");
|
||||
if (!cleaned.isEmpty()) {
|
||||
sb.append(cleaned);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -344,36 +344,78 @@ public class MyBatisExportWindow {
|
||||
String prefix = "SQL " + (val.isEmpty() ? "全部" : val);
|
||||
final String fQueryDisplay = queryDisplay;
|
||||
|
||||
// 重置状态
|
||||
exportCancelled = false;
|
||||
exportBtn.setDisable(true);
|
||||
stopBtn.setDisable(false);
|
||||
logArea.clear();
|
||||
progressBar.setVisible(true);
|
||||
progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
|
||||
progressBar.setProgress(0);
|
||||
progressLabel.setText("正在导出...");
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
MyBatisExportService.ExportResult result =
|
||||
exportService.exportWithResult(prefix, filterMode, val);
|
||||
MyBatisExportService.ExportResult result = exportService.exportWithCallback(
|
||||
prefix, filterMode, val, new MyBatisExportService.ExportCallback() {
|
||||
@Override
|
||||
public void onProjectStart(int current, int total, String projectName) {
|
||||
Platform.runLater(() -> {
|
||||
double pct = (double)(current - 1) / total;
|
||||
progressBar.setProgress(pct);
|
||||
progressLabel.setText(String.format("%d/%d 处理中 %s ...", current, total, projectName));
|
||||
logArea.appendText(String.format("[%d/%d] 处理中 %s ...\n", current, total, projectName));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProjectDone(int current, int total, String projectName, int recordCount) {
|
||||
Platform.runLater(() -> {
|
||||
double pct = (double) current / total;
|
||||
progressBar.setProgress(pct);
|
||||
progressLabel.setText(String.format("%d/%d %s → %d 条SQL", current, total, projectName, recordCount));
|
||||
logArea.appendText(String.format("[%d/%d] %s → %d 条\n", current, total, projectName, recordCount));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return exportCancelled;
|
||||
}
|
||||
});
|
||||
|
||||
int hc = ++historyCounter;
|
||||
String timestamp = SDF.format(new Date());
|
||||
int totalRows = result != null ? result.rows.size() : 0;
|
||||
boolean wasCancelled = exportCancelled;
|
||||
|
||||
Platform.runLater(() -> {
|
||||
progressBar.setProgress(1);
|
||||
if (wasCancelled) {
|
||||
progressBar.setProgress(1);
|
||||
progressLabel.setText("已取消");
|
||||
logArea.appendText("\n[WARN] 导出已被用户取消\n");
|
||||
} else {
|
||||
progressBar.setProgress(1);
|
||||
progressLabel.setText("完成 (" + totalRows + " 条)");
|
||||
logArea.appendText(String.format("\n========== 导出完成:%d 条记录 ==========\n", totalRows));
|
||||
}
|
||||
progressBar.setVisible(false);
|
||||
progressLabel.setText("完成");
|
||||
exportBtn.setDisable(false);
|
||||
stopBtn.setDisable(true);
|
||||
|
||||
// 添加历史记录行(最新在前)
|
||||
historyRows.add(0, new HistoryRow(
|
||||
String.valueOf(hc), fQueryDisplay, result.fileName,
|
||||
result.rows.size(), timestamp, "打开"));
|
||||
saveHistory();
|
||||
if (result != null && result.fileName != null && !result.fileName.isEmpty()) {
|
||||
historyRows.add(0, new HistoryRow(
|
||||
String.valueOf(hc), fQueryDisplay, result.fileName,
|
||||
totalRows, timestamp, "打开"));
|
||||
saveHistory();
|
||||
}
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
Platform.runLater(() -> {
|
||||
progressBar.setVisible(false);
|
||||
progressLabel.setText("导出失败");
|
||||
exportBtn.setDisable(false);
|
||||
stopBtn.setDisable(true);
|
||||
logArea.appendText("[ERROR] " + ex.getMessage() + "\n");
|
||||
});
|
||||
}
|
||||
}, "SQLExport-Worker").start();
|
||||
|
||||
@@ -9,12 +9,14 @@ import javafx.scene.Cursor;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
@@ -39,7 +41,7 @@ public class MainWindow {
|
||||
private Label toolbarIcon, toolbarTitle;
|
||||
|
||||
/** 导航栏控件引用(响应式刷新用) */
|
||||
private Label appLabel, versionLabel;
|
||||
private Label appLabel;
|
||||
private Label welcomeIcon, welcomeHeading, welcomeDesc;
|
||||
|
||||
private final List<Button> navButtons = new ArrayList<>();
|
||||
@@ -77,7 +79,6 @@ public class MainWindow {
|
||||
|
||||
private void refreshResponsive() {
|
||||
appLabel.setFont(rbold(14));
|
||||
versionLabel.setFont(rcaption(11));
|
||||
for (Button btn : navButtons) btn.setFont(rfont(13));
|
||||
|
||||
double navMin = computeNavMinBaseWidth() * responsiveScale;
|
||||
@@ -124,7 +125,18 @@ public class MainWindow {
|
||||
|
||||
// ==================== UI 构建 ====================
|
||||
|
||||
/** 从 classpath 加载应用图标并设置到 Stage */
|
||||
private void setAppIcon() {
|
||||
try (InputStream is = getClass().getClassLoader().getResourceAsStream("app_icon.png")) {
|
||||
if (is != null) {
|
||||
stage.getIcons().add(new Image(is));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
setAppIcon();
|
||||
stage.setTitle("WorkUtils — 开发工作集");
|
||||
stage.setWidth(1280);
|
||||
stage.setHeight(800);
|
||||
@@ -252,12 +264,6 @@ public class MainWindow {
|
||||
VBox.setVgrow(spacer, Priority.ALWAYS);
|
||||
navBox.getChildren().add(spacer);
|
||||
|
||||
// 版本
|
||||
versionLabel = new Label("v1.3.0 — AtlantaFX");
|
||||
versionLabel.setFont(rcaption(11));
|
||||
versionLabel.getStyleClass().add(Theme.CSS_NAV_VERSION);
|
||||
navBox.getChildren().add(versionLabel);
|
||||
|
||||
return navBox;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,12 @@ public final class Theme {
|
||||
* 将自定义 CSS 样式表添加到 Scene
|
||||
*/
|
||||
public static void applyCustomCss(Scene scene) {
|
||||
String css = Theme.class.getClassLoader().getResource(currentCustomCss).toExternalForm();
|
||||
var cssUrl = Theme.class.getClassLoader().getResource(currentCustomCss);
|
||||
if (cssUrl == null) {
|
||||
System.err.println("[Theme] 警告: 找不到 CSS 资源 " + currentCustomCss + ",将使用默认样式");
|
||||
return;
|
||||
}
|
||||
String css = cssUrl.toExternalForm();
|
||||
scene.getStylesheets().removeIf(s -> s.contains("light-theme.css") || s.contains("dark-theme.css"));
|
||||
scene.getStylesheets().add(css);
|
||||
}
|
||||
@@ -56,6 +61,7 @@ public final class Theme {
|
||||
* 刷新场景的自定义 CSS(主题切换时调用)
|
||||
*/
|
||||
public static void refreshCustomCss(Scene scene) {
|
||||
if (scene == null) return;
|
||||
applyCustomCss(scene);
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Reference in New Issue
Block a user