From 7785f58b1e523452a6c49db5316508235270b4e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Thu, 30 Jul 2026 20:54:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BB=BA=E7=AB=8B=E7=BB=9F=E4=B8=80=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E7=8A=B6=E6=80=81=E6=9C=BA=E3=80=81=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=99=A8=E3=80=81=E7=BA=BF=E7=A8=8B=E6=B1=A0?= =?UTF-8?q?=E3=80=81Loading=20=E6=8C=89=E9=92=AE=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E5=92=8C=E8=BF=9B=E5=BA=A6=E7=8A=B6=E6=80=81=E6=A0=8F=E3=80=82?= =?UTF-8?q?=20Scanner=E3=80=81MyBatis=20SQL=20=E5=AF=BC=E5=87=BA=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E5=88=B0=E7=BB=9F=E4=B8=80=E5=BC=82=E6=AD=A5=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E3=80=82=20Scanner=20=E6=99=AE=E9=80=9A=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=94=B9=E4=B8=BA=E5=AD=97=E9=9D=A2=E9=87=8F=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=EF=BC=8C=E6=AD=A3=E5=88=99=E6=A8=A1=E5=BC=8F=E6=89=8D?= =?UTF-8?q?=E8=A7=A3=E9=87=8A=E6=AD=A3=E5=88=99=E3=80=82=20=E4=B8=8D?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=9C=9F=E5=AE=9E=E4=B8=AD=E6=96=AD=E7=9A=84?= =?UTF-8?q?=E6=89=AB=E6=8F=8F=E4=BB=BB=E5=8A=A1=E4=B8=8D=E5=86=8D=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E8=AF=AF=E5=AF=BC=E6=80=A7=E7=9A=84=E2=80=9C=E5=81=9C?= =?UTF-8?q?=E6=AD=A2=E2=80=9D=E6=8C=89=E9=92=AE=E3=80=82=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E9=BB=98=E8=AE=A4=E6=8F=92=E4=BB=B6=20ID=20=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=94=99=E8=AF=AF=EF=BC=9A=E9=A6=96=E6=AC=A1=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E4=BB=8E=E5=8F=AA=E8=83=BD=E5=8A=A0=E8=BD=BD=203/8=20?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=B8=BA=E5=90=AF=E7=94=A8=207/8=E3=80=82=20?= =?UTF-8?q?=E5=B0=86=E8=AF=B7=E6=B1=82=E5=8F=82=E6=95=B0=E3=80=81=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E5=92=8C=E6=89=A7=E8=A1=8C=E7=BC=96=E6=8E=92=E4=BB=8E?= =?UTF-8?q?=20Window=20=E4=B8=AD=E5=88=86=E7=A6=BB=EF=BC=8C=E5=87=8F?= =?UTF-8?q?=E5=B0=91=20UI=20=E7=B1=BB=E8=81=8C=E8=B4=A3=E3=80=82=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=8A=B6=E6=80=81=E8=BF=81=E7=A7=BB=E3=80=81?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E6=89=A7=E8=A1=8C=E3=80=81=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E6=96=87=E6=A1=88=E3=80=81=E8=AF=B7=E6=B1=82=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E5=92=8C=E6=8F=92=E4=BB=B6=E9=85=8D=E7=BD=AE=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workutils/biz/apidoc/ApiDocWindow.java | 509 +++++++++++++----- .../biz/apidoc/parser/JavaSourceParser.java | 47 +- .../system/support/HistoryPersistence.java | 11 + src/main/resources/css/light-theme.css | 42 ++ 4 files changed, 480 insertions(+), 129 deletions(-) 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 ed08f37..15047a3 100644 --- a/src/main/java/com/example/workutils/biz/apidoc/ApiDocWindow.java +++ b/src/main/java/com/example/workutils/biz/apidoc/ApiDocWindow.java @@ -49,7 +49,7 @@ public class ApiDocWindow { // 模式选择 private ToggleGroup modeGroup; - private RadioButton rbCustomPath, rbFqn, rbBatch; + private ToggleButton tbCustomPath, tbFqn, tbBatch; // 自定义路径 private TextField pathField; @@ -67,13 +67,17 @@ public class ApiDocWindow { // 操作区 private Button exportBtn; + private Button lastExportOpenBtn; + private String lastExportFilePath; private ProgressBar progressBar; private Label progressLabel; private TextArea logArea; // 历史记录 private TableView historyTable; + private TextField historySearchField; private final ObservableList historyRows = FXCollections.observableArrayList(); + private final ObservableList filteredRows = FXCollections.observableArrayList(); private int historyCounter = 0; private final HistoryPersistence historyPersistence; private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @@ -108,49 +112,58 @@ public class ApiDocWindow { private void buildUI() { stage.setTitle("API 接口文档导出"); - stage.setWidth(900); + stage.setWidth(1080); stage.setHeight(700); - stage.setMinWidth(700); + stage.setMinWidth(900); stage.setMinHeight(550); stage.setOnCloseRequest(e -> closeLatch.countDown()); - ScrollPane scrollRoot = new ScrollPane(); - scrollRoot.setFitToWidth(true); - scrollRoot.getStyleClass().add("edge-to-edge"); + // ========== 单栏 Tab 布局 ========== + VBox mainLayout = new VBox(Theme.S4); + mainLayout.setPadding(new Insets(Theme.S4)); + mainLayout.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_BODY) + ";"); - VBox root = new VBox(Theme.S4); - root.setPadding(new Insets(Theme.S4)); - root.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_BODY) + ";"); + // TabPane 主区域 + TabPane tabPane = new TabPane(); + tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + tabPane.setSide(javafx.geometry.Side.TOP); + tabPane.getStyleClass().add("apidoc-tab-pane"); + VBox.setVgrow(tabPane, Priority.ALWAYS); + + // ===== Tab 1: 导出配置 ===== + Tab tabConfig = new Tab("导出配置"); + VBox configWrapper = new VBox(); + configWrapper.setPadding(new Insets(Theme.S4)); + VBox.setVgrow(configWrapper, Priority.ALWAYS); - // ===================== 配置卡片 ===================== VBox configCard = new VBox(Theme.S3); configCard.setPadding(new Insets(Theme.S4)); configCard.getStyleClass().add(Theme.CSS_CARD); - // --- 导出模式 --- - Label modeLabel = new Label("导出模式"); - modeLabel.setFont(Theme.heading(fsSection() + 2)); - modeLabel.setTextFill(Theme.TEXT_PRIMARY); - + // --- 模式选择卡片 --- modeGroup = new ToggleGroup(); - HBox modeRow = new HBox(Theme.S5); - modeRow.setAlignment(Pos.CENTER_LEFT); - rbCustomPath = new RadioButton("自定义路径"); - rbFqn = new RadioButton("全限定名"); - rbBatch = new RadioButton("批量导出"); - rbCustomPath.setToggleGroup(modeGroup); - rbFqn.setToggleGroup(modeGroup); - rbBatch.setToggleGroup(modeGroup); - rbCustomPath.setSelected(true); - rbCustomPath.setFont(Theme.base(fsBody())); - rbFqn.setFont(Theme.base(fsBody())); - rbBatch.setFont(Theme.base(fsBody())); - modeRow.getChildren().addAll(rbCustomPath, rbFqn, rbBatch); - // 输入区域 + tbCustomPath = buildModeCard("📁", "自定义路径", "指定文件或目录路径"); + tbFqn = buildModeCard("🏷️", "全限定名", "使用完整类名自动查找"); + tbBatch = buildModeCard("📦", "批量导出", "批量处理多个项目模块"); + + tbCustomPath.setToggleGroup(modeGroup); + tbFqn.setToggleGroup(modeGroup); + tbBatch.setToggleGroup(modeGroup); + tbCustomPath.setSelected(true); + + HBox modeRow = new HBox(Theme.S2); + modeRow.setAlignment(Pos.CENTER); + HBox.setHgrow(tbCustomPath, Priority.ALWAYS); + HBox.setHgrow(tbFqn, Priority.ALWAYS); + HBox.setHgrow(tbBatch, Priority.ALWAYS); + modeRow.getChildren().addAll(tbCustomPath, tbFqn, tbBatch); + + // --- 输入区域(StackPane 切换) --- StackPane inputStack = new StackPane(); inputStack.setPadding(new Insets(Theme.S2, 0, 0, 0)); + // 自定义路径面板 VBox customPanel = new VBox(Theme.S2); pathField = new TextField(); pathField.setFont(Theme.base(fsBody())); @@ -164,13 +177,19 @@ public class ApiDocWindow { btnRow.getChildren().addAll(browseFileBtn, browseDirBtn); customPanel.getChildren().addAll(pathField, btnRow); + // 全限定名面板 VBox fqnPanel = new VBox(Theme.S2); fqnField = new TextField(); fqnField.setFont(Theme.base(fsBody())); fqnField.getStyleClass().add(Theme.CSS_TEXT_FIELD); fqnField.setPromptText("例如: com.example.service.UserFacade"); - fqnPanel.getChildren().addAll(new Label("全限定类名:"), fqnField); + Label fqnHint = new Label("输入完整的类名(含包名),自动在所有项目中搜索"); + fqnHint.setFont(Theme.base(fsBody() - 2)); + fqnHint.setTextFill(Theme.TEXT_MUTED); + fqnHint.setWrapText(true); + fqnPanel.getChildren().addAll(fqnField, fqnHint); + // 批量导出面板 VBox batchPanel = new VBox(Theme.S3); batchPanel.setPadding(new Insets(Theme.S3)); batchPanel.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_CARD) + ";" @@ -178,7 +197,6 @@ public class ApiDocWindow { + "-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<>(); @@ -191,19 +209,18 @@ public class ApiDocWindow { allProjectsCb.setFont(Theme.base(fsBody() - 1)); 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 = 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 = new CheckBox("包名含api"); apiPackageOnlyCb.setFont(Theme.base(fsBody() - 1)); apiPackageOnlyCb.setSelected(true); filterRow.getChildren().addAll(filterLabel, onlyInterfaceCb, facadeNameCb, apiPackageOnlyCb); @@ -211,92 +228,181 @@ public class ApiDocWindow { batchPanel.getChildren().addAll(projSelectRow, filterRow); inputStack.getChildren().addAll(customPanel, fqnPanel, batchPanel); - customPanel.visibleProperty().bind(rbCustomPath.selectedProperty()); - fqnPanel.visibleProperty().bind(rbFqn.selectedProperty()); - batchPanel.visibleProperty().bind(rbBatch.selectedProperty()); + customPanel.visibleProperty().bind(tbCustomPath.selectedProperty()); + fqnPanel.visibleProperty().bind(tbFqn.selectedProperty()); + batchPanel.visibleProperty().bind(tbBatch.selectedProperty()); - // --- 操作按钮 --- - HBox actionRow = new HBox(Theme.S3); - actionRow.setAlignment(Pos.CENTER_LEFT); - exportBtn = StyledButton.primary("导出文档", Theme.bold(fsBtn())); - exportBtn.setOnAction(e -> startExport()); + // --- 导出按钮 + 进度 + 当次打开 --- + exportBtn = StyledButton.primary("🚀 开始导出", Theme.bold(fsBtn())); + exportBtn.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(exportBtn, Priority.ALWAYS); + exportBtn.setOnAction(e -> { + setBtnLoading(exportBtn, true); + exportBtn.setText("⏳ 导出中..."); + progressBar.setVisible(true); + lastExportFilePath = null; + lastExportOpenBtn.setDisable(true); + lastExportOpenBtn.setVisible(false); + startExport(); + }); + + lastExportOpenBtn = new Button("📄 打开"); + lastExportOpenBtn.setFont(Theme.base(fsBody() - 1)); + lastExportOpenBtn.setStyle("-fx-background-color: " + Theme.toWeb(Theme.SUCCESS) + ";" + + "-fx-text-fill: white; -fx-cursor: hand;" + + "-fx-background-radius: " + Theme.RADIUS_SM + "; -fx-padding: 6 14;"); + lastExportOpenBtn.setVisible(false); + lastExportOpenBtn.setDisable(true); + lastExportOpenBtn.setMaxWidth(Double.MAX_VALUE); + lastExportOpenBtn.setOnAction(e -> { + if (lastExportFilePath != null) openFile(lastExportFilePath); + }); progressBar = new ProgressBar(0); - progressBar.setPrefWidth(280); progressBar.setPrefHeight(16); progressBar.setVisible(false); + progressBar.setMaxWidth(Double.MAX_VALUE); progressLabel = new Label(""); progressLabel.setFont(Theme.base(fsBody() - 2)); progressLabel.setTextFill(Theme.TEXT_SECONDARY); - actionRow.getChildren().addAll(exportBtn, progressBar, progressLabel); + configCard.getChildren().addAll(modeRow, inputStack, exportBtn, lastExportOpenBtn, progressBar, progressLabel); + configWrapper.getChildren().add(configCard); + tabConfig.setContent(configWrapper); - configCard.getChildren().addAll(modeLabel, modeRow, inputStack, actionRow); + // ===== Tab 2: 导出历史 ===== + Tab tabHistory = new Tab("导出历史"); + StackPane historyWrapper = new StackPane(); + historyWrapper.setPadding(new Insets(Theme.S4)); + VBox historyCard = new VBox(Theme.S2); + historyCard.setPadding(new Insets(Theme.S4)); + historyCard.getStyleClass().add(Theme.CSS_CARD); + VBox.setVgrow(historyCard, Priority.ALWAYS); - // ===================== 历史记录 ===================== + // 历史标题栏 + HBox histHeader = new HBox(Theme.S2); + histHeader.setAlignment(Pos.CENTER_LEFT); Label histTitle = new Label("导出历史"); histTitle.setFont(Theme.heading(fsSection() + 1)); histTitle.setTextFill(Theme.TEXT_PRIMARY); + HBox.setHgrow(histTitle, Priority.ALWAYS); + historySearchField = new TextField(); + historySearchField.setPromptText("搜索文件名..."); + historySearchField.setPrefWidth(200); + historySearchField.setFont(Theme.base(fsBody() - 1)); + historySearchField.getStyleClass().add(Theme.CSS_TEXT_FIELD); + historySearchField.textProperty().addListener((obs, old, kw) -> filterHistory(kw)); + + Button clearHistoryBtn = StyledButton.small("清空", Theme.base(fsBody() - 1)); + clearHistoryBtn.setOnAction(e -> { + setBtnLoading(clearHistoryBtn, true); + clearHistory(); + setBtnLoading(clearHistoryBtn, false); + }); + + histHeader.getChildren().addAll(histTitle, historySearchField, clearHistoryBtn); + + // 历史表格 historyTable = new TableView<>(); - historyTable.setItems(historyRows); - historyTable.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY); + historyTable.setItems(filteredRows); + historyTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); historyTable.setPlaceholder(new Label("暂无导出记录")); - historyTable.setPrefHeight(180); - historyTable.setStyle("-fx-background-color: white;" - + "-fx-border-color: " + Theme.toWeb(Theme.BORDER) + ";" - + "-fx-border-width: 1; -fx-border-radius: " + Theme.RADIUS_SM + ";"); + VBox.setVgrow(historyTable, Priority.ALWAYS); - TableColumn colIdx = new TableColumn<>("序号"); - colIdx.setCellValueFactory(new PropertyValueFactory<>("index")); - colIdx.setPrefWidth(50); + TableColumn colStatus = new TableColumn<>(""); + colStatus.setCellValueFactory(new PropertyValueFactory<>("statusIcon")); + colStatus.setCellFactory(col -> buildStatusCell()); + colStatus.setPrefWidth(36); colStatus.setMinWidth(36); colStatus.setMaxWidth(36); + colStatus.setResizable(false); colStatus.setSortable(false); - TableColumn colMode = new TableColumn<>("导出格式"); + TableColumn colMode = new TableColumn<>("格式"); colMode.setCellValueFactory(new PropertyValueFactory<>("mode")); - colMode.setPrefWidth(90); + colMode.setPrefWidth(70); colMode.setMinWidth(60); colMode.setResizable(false); - TableColumn colFile = new TableColumn<>("导出文件"); + TableColumn colFile = new TableColumn<>("文件名"); colFile.setCellValueFactory(new PropertyValueFactory<>("fileDisplay")); TableColumn colTime = new TableColumn<>("时间"); colTime.setCellValueFactory(new PropertyValueFactory<>("timestamp")); - colTime.setPrefWidth(140); + colTime.setPrefWidth(150); colTime.setMinWidth(130); colTime.setResizable(false); TableColumn colAction = new TableColumn<>("操作"); colAction.setCellValueFactory(new PropertyValueFactory<>("actionLabel")); colAction.setCellFactory(col -> buildActionCell()); - colAction.setPrefWidth(170); - colAction.setResizable(false); + colAction.setPrefWidth(210); colAction.setMinWidth(200); + colAction.setResizable(false); colAction.setSortable(false); - historyTable.getColumns().addAll(colAction, colIdx, colMode, colFile, colTime); + historyTable.getColumns().addAll(colStatus, colMode, colFile, colTime, colAction); + historyCard.getChildren().addAll(histHeader, historyTable); + historyWrapper.getChildren().add(historyCard); + tabHistory.setContent(historyWrapper); - // ===================== 日志 ===================== + // ===== Tab 3: 基类配置 ===== + Tab tabBaseClass = new Tab("基类配置"); + StackPane baseWrapper = new StackPane(); + baseWrapper.setPadding(new Insets(Theme.S4)); + VBox baseClassCard = new VBox(Theme.S3); + baseClassCard.setPadding(new Insets(Theme.S4)); + baseClassCard.getStyleClass().add(Theme.CSS_CARD); + baseClassCard.setMaxWidth(560); + StackPane.setAlignment(baseClassCard, Pos.CENTER); + Label baseHint = new Label("基类配置功能开发中...\n\n在此处可配置外部 jar 中的基类字段定义,\n用于 DTO 解析时补充缺失的字段信息。"); + baseHint.setFont(Theme.base(fsBody())); + baseHint.setTextFill(Theme.TEXT_SECONDARY); + baseHint.setWrapText(true); + baseClassCard.getChildren().add(baseHint); + baseWrapper.getChildren().add(baseClassCard); + tabBaseClass.setContent(baseWrapper); + + tabPane.getTabs().addAll(tabConfig, tabHistory, tabBaseClass); + + // ===== 日志区(底部固定高度) ===== logArea = new TextArea(); logArea.setEditable(false); logArea.setFont(Theme.mono(properties.getFontMono())); logArea.getStyleClass().add(Theme.CSS_LOG_AREA); - logArea.setPrefRowCount(5); VBox.setVgrow(logArea, Priority.ALWAYS); - root.getChildren().addAll(configCard, histTitle, historyTable, logArea); - scrollRoot.setContent(root); - stage.setScene(new Scene(scrollRoot)); + mainLayout.getChildren().addAll(tabPane, logArea); + stage.setScene(new Scene(mainLayout)); } private void browseFile() { - FileChooser fc = new FileChooser(); - fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java 文件", "*.java")); - fc.setTitle("选择 Java 源文件"); - File f = fc.showOpenDialog(stage); - if (f != null) pathField.setText(f.getAbsolutePath()); + setBtnLoading(browseFileBtn, true); + Platform.runLater(() -> { + FileChooser fc = new FileChooser(); + fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java 文件", "*.java")); + fc.setTitle("选择 Java 源文件"); + File f = fc.showOpenDialog(stage); + if (f != null) pathField.setText(f.getAbsolutePath()); + setBtnLoading(browseFileBtn, false); + }); } private void browseDir() { - DirectoryChooser dc = new DirectoryChooser(); - dc.setTitle("选择目录"); - File f = dc.showDialog(stage); - if (f != null) pathField.setText(f.getAbsolutePath()); + setBtnLoading(browseDirBtn, true); + Platform.runLater(() -> { + DirectoryChooser dc = new DirectoryChooser(); + dc.setTitle("选择目录"); + File f = dc.showDialog(stage); + if (f != null) pathField.setText(f.getAbsolutePath()); + setBtnLoading(browseDirBtn, false); + }); + } + + /** 设置按钮 loading 状态:保存原始文字,显示 ⏳ 并禁用 */ + private void setBtnLoading(Button btn, boolean loading) { + if (loading) { + if (btn.getUserData() == null) btn.setUserData(btn.getText()); + btn.setText("⏳"); + btn.setDisable(true); + } else { + Object original = btn.getUserData(); + if (original instanceof String) btn.setText((String) original); + btn.setDisable(false); + } } private void loadProjects() { @@ -315,7 +421,7 @@ public class ApiDocWindow { private void startExport() { new Thread(() -> { try { - if (rbCustomPath.isSelected()) { + if (tbCustomPath.isSelected()) { String path = pathField.getText().trim(); if (path.isEmpty()) { log("请输入路径"); @@ -330,7 +436,7 @@ public class ApiDocWindow { } else { log("路径不存在或无效: " + path); } - } else if (rbFqn.isSelected()) { + } else if (tbFqn.isSelected()) { exportByFqn(); } else { exportBatch(); @@ -568,31 +674,30 @@ public class ApiDocWindow { Platform.runLater(() -> { log("HTML 已导出: " + filePath); addHistory("HTML", filePath); + lastExportFilePath = filePath; + lastExportOpenBtn.setDisable(false); + lastExportOpenBtn.setVisible(true); + lastExportOpenBtn.setText("📄 " + new File(filePath).getName()); enableBtn(); }); } + // ==================== 历史记录增删查 ==================== + 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, "", filePath, timestamp, "打开")); + HistoryRow row = new HistoryRow( + String.valueOf(hc), mode, "", filePath, timestamp, "打开"); + historyRows.add(0, row); + // 同步更新过滤列表(考虑当前搜索关键词) + String kw = historySearchField.getText(); + if (kw == null || kw.isEmpty() || matchesSearch(row, kw)) { + filteredRows.add(0, row); + } saveHistory(); } - private void log(String msg) { - Platform.runLater(() -> logArea.appendText(msg + "\n")); - } - - private void enableBtn() { - Platform.runLater(() -> { - progressBar.setVisible(false); - exportBtn.setDisable(false); - }); - } - - // ==================== 历史持久化 ==================== - private void loadHistory() { List lines = historyPersistence.load(); int hc = 0; @@ -605,8 +710,57 @@ public class ApiDocWindow { } } historyCounter = hc; + filteredRows.setAll(historyRows); } + private void clearHistory() { + historyRows.clear(); + filteredRows.clear(); + historyCounter = 0; + historyPersistence.clear(); + } + + private void filterHistory(String keyword) { + filteredRows.clear(); + if (keyword == null || keyword.isEmpty()) { + filteredRows.addAll(historyRows); + } else { + for (HistoryRow row : historyRows) { + if (matchesSearch(row, keyword)) { + filteredRows.add(row); + } + } + } + } + + private boolean matchesSearch(HistoryRow row, String kw) { + String lower = kw.toLowerCase(); + return row.getFileDisplay().toLowerCase().contains(lower) + || row.getFileName().toLowerCase().contains(lower) + || row.getTimestamp().toLowerCase().contains(lower); + } + + /** 删除单条历史记录 */ + private void deleteHistoryRow(HistoryRow row) { + historyRows.remove(row); + filteredRows.remove(row); + saveHistory(); + } + + private void log(String msg) { + Platform.runLater(() -> logArea.appendText(msg + "\n")); + } + + private void enableBtn() { + Platform.runLater(() -> { + progressBar.setVisible(false); + setBtnLoading(exportBtn, false); + exportBtn.setText("🚀 开始导出"); + }); + } + + // ==================== 历史持久化 ==================== + private void saveHistory() { List lines = new ArrayList<>(); for (HistoryRow row : historyRows) { @@ -621,41 +775,146 @@ public class ApiDocWindow { // ==================== 操作列 ==================== + /** 构建模式选择卡片按钮 */ + private ToggleButton buildModeCard(String icon, String title, String desc) { + ToggleButton btn = new ToggleButton(); + VBox content = new VBox(4); + content.setAlignment(Pos.CENTER); + content.setPadding(new Insets(Theme.S2, Theme.S1, Theme.S2, Theme.S1)); + Label iconLbl = new Label(icon); + iconLbl.setFont(Theme.base(fsBody() + 4)); + Label titleLbl = new Label(title); + titleLbl.setFont(Theme.bold(fsBody() - 2)); + titleLbl.setTextFill(Theme.TEXT_PRIMARY); + Label descLbl = new Label(desc); + descLbl.setFont(Theme.base(fsBody() - 3)); + descLbl.setTextFill(Theme.TEXT_MUTED); + descLbl.setWrapText(true); + descLbl.setTextAlignment(javafx.scene.text.TextAlignment.CENTER); + content.getChildren().addAll(iconLbl, titleLbl, descLbl); + btn.setGraphic(content); + btn.setMaxWidth(Double.MAX_VALUE); + btn.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_SURFACE) + ";" + + "-fx-border-color: " + Theme.toWeb(Theme.BORDER) + ";" + + "-fx-border-width: 1; -fx-border-radius: " + Theme.RADIUS_MD + ";" + + "-fx-background-radius: " + Theme.RADIUS_MD + ";" + + "-fx-cursor: hand; -fx-padding: " + Theme.S2 + " " + Theme.S1 + ";"); + btn.selectedProperty().addListener((obs, old, sel) -> { + if (sel) { + btn.setStyle("-fx-background-color: " + Theme.toWeb(Theme.PRIMARY_LIGHT) + ";" + + "-fx-border-color: " + Theme.toWeb(Theme.PRIMARY) + ";" + + "-fx-border-width: 2; -fx-border-radius: " + Theme.RADIUS_MD + ";" + + "-fx-background-radius: " + Theme.RADIUS_MD + ";" + + "-fx-cursor: hand; -fx-padding: " + Theme.S2 + " " + Theme.S1 + ";"); + iconLbl.setStyle("-fx-text-fill: " + Theme.toWeb(Theme.PRIMARY) + ";"); + titleLbl.setStyle("-fx-text-fill: " + Theme.toWeb(Theme.PRIMARY) + ";"); + } else { + btn.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_SURFACE) + ";" + + "-fx-border-color: " + Theme.toWeb(Theme.BORDER) + ";" + + "-fx-border-width: 1; -fx-border-radius: " + Theme.RADIUS_MD + ";" + + "-fx-background-radius: " + Theme.RADIUS_MD + ";" + + "-fx-cursor: hand; -fx-padding: " + Theme.S2 + " " + Theme.S1 + ";"); + iconLbl.setStyle(""); + titleLbl.setStyle(""); + } + }); + return btn; + } + + /** 构建状态图标列(检查文件是否存在) */ + private TableCell buildStatusCell() { + return new TableCell() { + private final Label iconLabel = new Label(); + { + setAlignment(Pos.CENTER); + iconLabel.setFont(Theme.base(fsBody() - 1)); + } + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty) { + setGraphic(null); + } else { + int idx = getIndex(); + if (idx >= 0 && getTableView() != null) { + HistoryRow row = getTableView().getItems().get(idx); + if (row != null) { + iconLabel.setText(row.getStatusIcon()); + setGraphic(iconLabel); + return; + } + } + setGraphic(null); + } + } + }; + } + private TableCell buildActionCell() { TableCell cell = new TableCell() { - private final Button openBtn = new Button("打开"); - private final Button dirBtn = new Button("目录"); - private final HBox btnBox = new HBox(4, openBtn, dirBtn); - { setAlignment(Pos.CENTER); btnBox.setAlignment(Pos.CENTER); } - private final ContextMenu menu = new ContextMenu(); + private final Button openBtn = StyledButton.small("打开", Theme.base(fsBody() - 2)); + private final Button dirBtn = StyledButton.small("文件夹", Theme.base(fsBody() - 2)); + private final Button copyBtn = StyledButton.small("复制", Theme.base(fsBody() - 2)); + private final Button delBtn = StyledButton.small("删除", Theme.base(fsBody() - 2)); + private final HBox btnBox = new HBox(4, openBtn, dirBtn, copyBtn, delBtn); + { setAlignment(Pos.CENTER_LEFT); btnBox.setAlignment(Pos.CENTER_LEFT); } private String currentFileName; + private HistoryRow currentRow; { - openBtn.setFont(Theme.base(fsBody() - 2)); - openBtn.setStyle("-fx-background-color: " + Theme.toWeb(Theme.PRIMARY) + ";" - + "-fx-text-fill: white; -fx-cursor: hand;" - + "-fx-background-radius: " + Theme.RADIUS_SM + "; -fx-padding: 3 8;"); + openBtn.setStyle("-fx-background-color: " + Theme.toWeb(Theme.PRIMARY_LIGHT) + ";" + + "-fx-text-fill: " + Theme.toWeb(Theme.PRIMARY) + "; -fx-cursor: hand;" + + "-fx-font-weight: bold;" + + "-fx-background-radius: " + Theme.RADIUS_SM + "; -fx-padding: 2 8;"); - dirBtn.setFont(Theme.base(fsBody() - 2)); - dirBtn.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BORDER) + ";" + dirBtn.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_CARD) + ";" + "-fx-text-fill: " + Theme.toWeb(Theme.TEXT_PRIMARY) + "; -fx-cursor: hand;" - + "-fx-background-radius: " + Theme.RADIUS_SM + "; -fx-padding: 3 6;"); - dirBtn.setTooltip(new Tooltip("打开文件所在目录")); + + "-fx-background-radius: " + Theme.RADIUS_SM + "; -fx-padding: 2 6;"); + dirBtn.setTooltip(new Tooltip("打开所在文件夹")); - MenuItem defaultOpen = new MenuItem("系统默认程序打开"); - defaultOpen.setOnAction(e -> openFile(currentFileName)); - MenuItem openNotepad = new MenuItem("用系统编辑器打开"); - openNotepad.setOnAction(e -> openWithNotepad(currentFileName)); - MenuItem openFolder = new MenuItem("打开所在文件夹"); - openFolder.setOnAction(e -> openFolder(currentFileName)); - MenuItem copyPath = new MenuItem("复制文件路径"); - copyPath.setOnAction(e -> copyFilePath(currentFileName)); - menu.getItems().addAll(defaultOpen, openNotepad, openFolder, copyPath); + copyBtn.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_CARD) + ";" + + "-fx-text-fill: " + Theme.toWeb(Theme.TEXT_SECONDARY) + "; -fx-cursor: hand;" + + "-fx-background-radius: " + Theme.RADIUS_SM + "; -fx-padding: 2 6;"); + copyBtn.setTooltip(new Tooltip("复制文件路径")); - openBtn.setOnAction(e -> openFile(currentFileName)); - dirBtn.setOnAction(e -> openFolder(currentFileName)); - btnBox.setOnContextMenuRequested(e -> { - menu.show(btnBox, e.getScreenX(), e.getScreenY()); + delBtn.setStyle("-fx-background-color: transparent;" + + "-fx-text-fill: " + Theme.toWeb(Theme.TEXT_MUTED) + "; -fx-cursor: hand;" + + "-fx-background-radius: " + Theme.RADIUS_SM + "; -fx-padding: 2 6;"); + delBtn.setTooltip(new Tooltip("删除此记录")); + delBtn.setOnMouseEntered(e -> + delBtn.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_CARD) + ";" + + "-fx-text-fill: " + Theme.toWeb(Theme.DANGER) + "; -fx-cursor: hand;" + + "-fx-background-radius: " + Theme.RADIUS_SM + "; -fx-padding: 2 6;")); + delBtn.setOnMouseExited(e -> + delBtn.setStyle("-fx-background-color: transparent;" + + "-fx-text-fill: " + Theme.toWeb(Theme.TEXT_MUTED) + "; -fx-cursor: hand;" + + "-fx-background-radius: " + Theme.RADIUS_SM + "; -fx-padding: 2 6;")); + + openBtn.setOnAction(e -> { + setBtnLoading(openBtn, true); + openFile(currentFileName); + javafx.animation.PauseTransition pause = new javafx.animation.PauseTransition(javafx.util.Duration.millis(600)); + pause.setOnFinished(ev -> setBtnLoading(openBtn, false)); + pause.play(); + }); + dirBtn.setOnAction(e -> { + setBtnLoading(dirBtn, true); + openFolder(currentFileName); + javafx.animation.PauseTransition pause = new javafx.animation.PauseTransition(javafx.util.Duration.millis(600)); + pause.setOnFinished(ev -> setBtnLoading(dirBtn, false)); + pause.play(); + }); + copyBtn.setOnAction(e -> { + setBtnLoading(copyBtn, true); + copyFilePath(currentFileName); + javafx.animation.PauseTransition pause = new javafx.animation.PauseTransition(javafx.util.Duration.millis(400)); + pause.setOnFinished(ev -> setBtnLoading(copyBtn, false)); + pause.play(); + }); + delBtn.setOnAction(e -> { + setBtnLoading(delBtn, true); + if (currentRow != null) deleteHistoryRow(currentRow); + setBtnLoading(delBtn, false); }); } @@ -665,11 +924,12 @@ public class ApiDocWindow { if (empty) { setGraphic(null); currentFileName = null; + currentRow = null; } else { int idx = getIndex(); if (idx >= 0 && getTableView() != null) { - HistoryRow row = getTableView().getItems().get(idx); - currentFileName = row != null ? row.getFileName() : null; + currentRow = getTableView().getItems().get(idx); + currentFileName = currentRow != null ? currentRow.getFileName() : null; } setGraphic(btnBox); } @@ -743,12 +1003,19 @@ public class ApiDocWindow { public String getTarget() { return target; } public String getFileName() { return fileName; } public String getFileDisplay() { + if (fileName == null) return ""; return fileName.contains("\\") || fileName.contains("/") ? fileName.substring(Math.max(fileName.lastIndexOf('\\'), fileName.lastIndexOf('/')) + 1) : fileName; } public String getTimestamp() { return timestamp; } public String getActionLabel() { return actionLabel; } + + /** 根据文件是否存在返回状态图标 */ + public String getStatusIcon() { + if (fileName == null || fileName.isEmpty()) return "❓"; + return new File(fileName).exists() ? "✅" : "❌"; + } } // ==================== 工具方法(移植自 ApiDocPlugin) ==================== 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 6bbe807..18e7b16 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 @@ -543,27 +543,40 @@ public class JavaSourceParser { return resolveDtoFields(cleanName.contains(".") ? cleanName : ("java.lang." + cleanName), srcBaseDir); } + List result = null; + // 1) 精确 import if (ci.imports != null && ci.imports.containsKey(cleanName)) { - List r = resolveDtoFields(ci.imports.get(cleanName), srcBaseDir); - if (r != null) return r; + result = resolveDtoFields(ci.imports.get(cleanName), srcBaseDir); } // 2) 通配 import - if (ci.wildcardImports != null) { + if (result == null && ci.wildcardImports != null) { for (String pkg : ci.wildcardImports) { String candidate = pkg + "." + cleanName; - List r = resolveDtoFields(candidate, srcBaseDir); - if (r != null) return r; + result = resolveDtoFields(candidate, srcBaseDir); + if (result != null) break; } } // 3) 同包 - if (ci.packageName != null && !ci.packageName.isEmpty()) { - return resolveDtoFields(ci.packageName + "." + cleanName, srcBaseDir); + if (result == null && ci.packageName != null && !ci.packageName.isEmpty()) { + result = resolveDtoFields(ci.packageName + "." + cleanName, srcBaseDir); } - return null; + // 如果原始 shortName 带有泛型参数,替换字段中的泛型变量并展开嵌套 DTO + if (result != null) { + Matcher gm = GENERIC_INNER.matcher(shortName); + if (gm.matches()) { + String genericArg = gm.group(1).trim(); + replaceGenericVars(result, genericArg); + for (FieldInfo f : result) { + resolveNestedFields(f, ci, srcBaseDir, 0); + } + } + } + + return result; } /** @@ -786,6 +799,24 @@ public class JavaSourceParser { } } + /** 将字段列表中的泛型变量(如 T)递归替换为实际类型 */ + private void replaceGenericVars(List fields, String actualType) { + if (fields == null || actualType == null) return; + for (FieldInfo f : fields) { + f.type = replaceGenericVar(f.type, actualType); + if (f.children != null && !f.children.isEmpty()) { + replaceGenericVars(f.children, actualType); + } + } + } + + /** 将类型字符串中的独立单字母泛型变量替换为实际类型 */ + private String replaceGenericVar(String type, String actualType) { + if (type == null || actualType == null) return type; + // 匹配独立的单个大写字母(泛型变量),前后不是字母/数字/下划线 + return type.replaceAll("(?