diff --git a/pom.xml b/pom.xml index cd3dd61..eb8940d 100644 --- a/pom.xml +++ b/pom.xml @@ -232,9 +232,10 @@ package launch4j - console + gui ${project.build.directory}/${project.build.finalName}.jar ${project.build.directory}/workutils.exe + ${project.basedir}/src/main/resources/app_icon.ico false org.springframework.boot.loader.JarLauncher diff --git a/src/main/java/com/example/workutils/biz/apidoc/ApiDocWindow.java b/src/main/java/com/example/workutils/biz/apidoc/ApiDocWindow.java index 1d51c01..091c84f 100644 --- a/src/main/java/com/example/workutils/biz/apidoc/ApiDocWindow.java +++ b/src/main/java/com/example/workutils/biz/apidoc/ApiDocWindow.java @@ -59,10 +59,13 @@ public class ApiDocWindow { // 批量 private ComboBox 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 colMode = new TableColumn<>("导出模式"); + TableColumn colMode = new TableColumn<>("导出格式"); colMode.setCellValueFactory(new PropertyValueFactory<>("mode")); colMode.setPrefWidth(90); - TableColumn colTarget = new TableColumn<>("目标"); - colTarget.setCellValueFactory(new PropertyValueFactory<>("target")); - TableColumn 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 seenSrcBases = new HashSet<>(); List modules = new ArrayList<>(); @@ -513,10 +543,19 @@ public class ApiDocWindow { List 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 docs) throws Exception { - doExport(name, docs, "单文件", ""); - } - - private void doExport(String name, List 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(); } diff --git a/src/main/java/com/example/workutils/biz/apidoc/generator/ApiDocGenerator.java b/src/main/java/com/example/workutils/biz/apidoc/generator/ApiDocGenerator.java index 9f8e7b7..4105b83 100644 --- a/src/main/java/com/example/workutils/biz/apidoc/generator/ApiDocGenerator.java +++ b/src/main/java/com/example/workutils/biz/apidoc/generator/ApiDocGenerator.java @@ -61,183 +61,406 @@ public class ApiDocGenerator { StringBuilder sb = new StringBuilder(); sb.append("\n\n\n"); sb.append("\n"); + sb.append("\n"); sb.append("API 接口文档\n"); - sb.append("\n\n\n"); + appendCss(sb); + sb.append("\n\n"); - sb.append("

API 接口文档

\n"); - sb.append("

生成时间: ").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("

\n"); + // 顶部栏 + sb.append("
\n"); + sb.append("
📄 API 接口文档
\n"); + sb.append("
\n"); + sb.append(" 接口文档\n"); + sb.append("
\n"); + sb.append("
生成时间: ").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("
\n"); + sb.append("
\n"); - // 目录 - sb.append("

目录

\n
    \n"); + // 左侧导航 + 右侧内容 + sb.append("
    \n"); + sb.append("
\n"); - - // 正文 - for (ApiDocModel.ProjectApiDoc doc : docs) { - sb.append("

").append(escapeHtml(doc.projectName)).append(" (") - .append(doc.classes.size()).append(" 接口)

\n"); - - for (ApiDocModel.ClassInfo ci : doc.classes) { - sb.append("
\n"); - sb.append("
").append(ci.type).append(": ").append(escapeHtml(ci.className)).append("
\n"); - sb.append("
").append(escapeHtml(ci.fullClassName)).append("
\n"); - - if (ci.annotations != null && !ci.annotations.isEmpty()) { - for (String a : ci.annotations) { - sb.append("").append(escapeHtml(a)).append(" "); + String anchor = anchorId(doc.projectName, ci.fullClassName); + sb.append(" ") + .append("").append("interface".equals(ci.type) ? "🔌" : "📦").append("") + .append(escapeHtml(ci.className)) + .append("\n"); + // 方法级子项 + if (ci.methods != null) { + for (ApiDocModel.MethodInfo mi : ci.methods) { + String methodAnchor = anchor + "_" + safeId(mi.name); + String http = extractHttpMethod(mi.annotations); + sb.append(" ") + .append("●") + .append("").append(http.isEmpty() ? "—" : http).append("") + .append(escapeHtml(mi.name)) + .append("\n"); } - sb.append("
\n"); } + } + sb.append("
\n \n"); + } + sb.append(" \n\n"); - if (ci.javadoc != null && !ci.javadoc.isEmpty()) { - sb.append("
").append(escapeHtml(ci.javadoc)).append("
\n"); + // 右侧内容区 + sb.append("
\n"); + + for (ApiDocModel.ProjectApiDoc doc : docs) { + sb.append("
\n"); + sb.append("

").append(escapeHtml(doc.projectName)).append("

\n"); + + for (ApiDocModel.ClassInfo ci : doc.classes) { + String anchor = anchorId(doc.projectName, ci.fullClassName); + sb.append("
\n"); + + // 基本信息区 + sb.append("
\n"); + sb.append("
\n"); + String typeLabel = "interface".equals(ci.type) ? "接口" : "class".equals(ci.type) ? "类" : ci.type; + sb.append(" ").append(typeLabel).append("\n"); + sb.append(" ").append(escapeHtml(ci.className)).append("\n"); + sb.append("
\n"); + if (ci.methods != null) { + sb.append("
").append(ci.methods.size()).append(" 个方法
\n"); } + sb.append("
\n"); + + // 基本信息表格 + sb.append("
\n"); + sb.append("
全限定名").append(escapeHtml(ci.fullClassName)).append("
\n"); + if (ci.superClass != null) { + sb.append("
父类").append(escapeHtml(ci.superClass)).append("
\n"); + } + if (ci.interfaces != null && !ci.interfaces.isEmpty()) { + sb.append("
实现接口").append(escapeHtml(String.join(", ", ci.interfaces))).append("
\n"); + } + if (ci.annotations != null && !ci.annotations.isEmpty()) { + sb.append("
注解"); + for (String a : ci.annotations) { + sb.append("").append(escapeHtml(a)).append(""); + } + sb.append("
\n"); + } + if (ci.javadoc != null && !ci.javadoc.isEmpty()) { + sb.append("
描述").append(escapeHtml(ci.javadoc)).append("
\n"); + } + sb.append("
\n"); // 方法列表 if (ci.methods != null) { + sb.append("
\n"); + sb.append("
📋 接口方法
\n"); for (ApiDocModel.MethodInfo mi : ci.methods) { - sb.append("
\n"); - sb.append("
").append(escapeHtml(mi.name)).append("
\n"); + String methodId = anchor + "_" + safeId(mi.name); + sb.append("
\n"); - if (mi.annotations != null && !mi.annotations.isEmpty()) { - sb.append("
"); - for (String a : mi.annotations) { - sb.append("").append(escapeHtml(a)).append(" "); - } - sb.append("
\n"); + // 方法头 + sb.append("
\n"); + sb.append(" ▼\n"); + String httpMethod = extractHttpMethod(mi.annotations); + if (!httpMethod.isEmpty()) { + sb.append(" ").append(httpMethod).append("\n"); } - - sb.append("
").append(escapeHtml(mi.fullSignature())).append("
\n"); - - if (mi.javadoc != null && !mi.javadoc.isEmpty()) { - sb.append("
").append(escapeHtml(mi.javadoc)).append("
\n"); - } - - // 参数表 - if (mi.params != null && !mi.params.isEmpty()) { - sb.append("\n"); - for (ApiDocModel.ParamInfo pi : mi.params) { - sb.append("\n"); - } - sb.append("
参数名类型
").append(escapeHtml(pi.name)).append("") - .append(escapeHtml(pi.type)).append("
\n"); - } - - // 请求参数 DTO 展开 - if (mi.requestFields != null && !mi.requestFields.isEmpty()) { - renderFieldsHtml(sb, mi.requestFields, "请求参数", 0); - } - - // 返回类型信息 + sb.append(" ").append(escapeHtml(mi.name)).append("\n"); if (mi.returnType != null) { - sb.append("
"); - sb.append("返回: ").append(escapeHtml(mi.returnType)).append(""); - sb.append("
\n"); + sb.append(" → ").append(escapeHtml(mi.returnType)).append("\n"); + } + sb.append("
\n"); + + // 方法体 + sb.append("
\n"); + + // 注解 + if (mi.annotations != null && !mi.annotations.isEmpty()) { + sb.append("
\n"); + for (String a : mi.annotations) { + sb.append(" ").append(escapeHtml(a)).append("\n"); + } + sb.append("
\n"); } - // 响应 DTO 展开 + // 描述 + if (mi.javadoc != null && !mi.javadoc.isEmpty()) { + sb.append("
").append(escapeHtml(mi.javadoc)).append("
\n"); + } + + // 签名 + sb.append("
").append(escapeHtml(mi.fullSignature())).append("
\n"); + + // 方法参数 + if (mi.params != null && !mi.params.isEmpty()) { + sb.append("
\n"); + sb.append("
方法参数
\n"); + sb.append(" \n"); + for (ApiDocModel.ParamInfo pi : mi.params) { + sb.append(" ") + .append("") + .append("\n"); + } + sb.append("
参数名类型全路径
").append(escapeHtml(pi.name)).append("").append(escapeHtml(pi.type)).append("").append(escapeHtml(pi.fullQualifiedName != null ? pi.fullQualifiedName : "")).append("
\n
\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("
throws: "); - sb.append(escapeHtml(String.join(", ", mi.throwsTypes))); - sb.append("
\n"); + sb.append("
⚠ throws: ") + .append(escapeHtml(String.join(", ", mi.throwsTypes))) + .append("
\n"); } - sb.append("
\n"); // .method + sb.append("
\n"); // .method-body + sb.append("
\n"); // .method-card } + sb.append("
\n"); // .methods-section } - sb.append("
\n"); // .card + sb.append(" \n"); // .api-card } + sb.append("
\n"); } + sb.append("
\n"); + sb.append("\n"); // .main-layout + + // JS: 折叠/展开 + 侧栏导航 + appendJs(sb); + sb.append("\n"); return sb.toString(); } - /** 递归渲染字段表(HTML) */ - private void renderFieldsHtml(StringBuilder sb, List fields, String title, int depth) { + /** 新版本字段渲染 —— 可折叠卡片 */ + private void renderFieldsHtmlV2(StringBuilder sb, List 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("
\n"); - sb.append("").append(escapeHtml(title)).append("\n"); - sb.append("\n"); - sb.append("\n"); + sb.append("
\n"); + sb.append("
") + .append("▼ ") + .append(titleIcon).append(" ").append(escapeHtml(title)) + .append(" (").append(fields.size()).append(" 字段)") + .append("
\n"); + sb.append("
\n"); + sb.append("
参数名参数类型描述全路径
\n"); for (ApiDocModel.FieldInfo f : fields) { - sb.append(""); - sb.append(""); + sb.append(" "); + sb.append(""); sb.append(""); - sb.append(""); - sb.append(""); + sb.append(""); sb.append("\n"); } - sb.append("
参数名参数类型描述
").append(escapeHtml(f.name)).append("
").append(escapeHtml(f.name)).append("").append(escapeHtml(f.type)).append("").append(escapeHtml(f.javadoc != null ? f.javadoc : "")).append("").append(escapeHtml(f.fullQualifiedName != null ? f.fullQualifiedName : "")).append("").append(escapeHtml(f.javadoc != null ? f.javadoc : "—")).append("
\n"); - sb.append("
\n"); + sb.append(" \n"); + sb.append(" \n"); + sb.append(" \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 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("\n"); + } + + /** JS 交互 */ + private void appendJs(StringBuilder sb) { + sb.append("\n"); + } + /** 递归渲染字段表(Markdown) */ private void renderFieldsMd(StringBuilder sb, List 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 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("\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("### ").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"); } diff --git a/src/main/java/com/example/workutils/biz/apidoc/parser/JavaSourceParser.java b/src/main/java/com/example/workutils/biz/apidoc/parser/JavaSourceParser.java index e066a41..29304ce 100644 --- a/src/main/java/com/example/workutils/biz/apidoc/parser/JavaSourceParser.java +++ b/src/main/java/com/example/workutils/biz/apidoc/parser/JavaSourceParser.java @@ -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; } diff --git a/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportWindow.java b/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportWindow.java index d88e23c..ba8cdb8 100644 --- a/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportWindow.java +++ b/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportWindow.java @@ -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(); diff --git a/src/main/java/com/example/workutils/system/ui/MainWindow.java b/src/main/java/com/example/workutils/system/ui/MainWindow.java index d1e26ae..2c9caa2 100644 --- a/src/main/java/com/example/workutils/system/ui/MainWindow.java +++ b/src/main/java/com/example/workutils/system/ui/MainWindow.java @@ -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