功能已稳定

This commit is contained in:
张家训
2026-06-22 13:32:18 +08:00
parent dbc9902716
commit 699a8697bf
9 changed files with 541 additions and 250 deletions
+2 -1
View File
@@ -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,11 +543,20 @@ 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()) {
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");
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("</ul>\n");
}
sb.append("</ul>\n");
}
sb.append(" </div>\n </div>\n");
}
sb.append(" </nav>\n</aside>\n");
// 右侧内容区
sb.append("<main class=\"content\">\n");
// 正文
for (ApiDocModel.ProjectApiDoc doc : docs) {
sb.append("<h2>").append(escapeHtml(doc.projectName)).append(" <small>(")
.append(doc.classes.size()).append(" 接口)</small></h2>\n");
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) {
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");
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\">").append(escapeHtml(a)).append("</span> ");
sb.append("<span class=\"anno-tag\">").append(escapeHtml(a)).append("</span>");
}
sb.append("<br>\n");
sb.append("</span></div>\n");
}
if (ci.javadoc != null && !ci.javadoc.isEmpty()) {
sb.append("<div class=\"class-doc\">").append(escapeHtml(ci.javadoc)).append("</div>\n");
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 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>\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(() -> {
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);
// 添加历史记录行(最新在前)
if (result != null && result.fileName != null && !result.fileName.isEmpty()) {
historyRows.add(0, new HistoryRow(
String.valueOf(hc), fQueryDisplay, result.fileName,
result.rows.size(), timestamp, "打开"));
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