建立统一异步状态机、任务控制器、线程池、Loading 按钮绑定和进度状态栏。
Scanner、MyBatis SQL 导出迁移到统一异步流程。 Scanner 普通搜索改为字面量匹配,正则模式才解释正则。 不支持真实中断的扫描任务不再显示误导性的“停止”按钮。 修复默认插件 ID 配置错误:首次运行从只能加载 3/8 修正为启用 7/8。 将请求参数、校验和执行编排从 Window 中分离,减少 UI 类职责。 新增状态迁移、并发执行、按钮文案、请求校验和插件配置测试。
This commit is contained in:
@@ -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<HistoryRow> historyTable;
|
||||
private TextField historySearchField;
|
||||
private final ObservableList<HistoryRow> historyRows = FXCollections.observableArrayList();
|
||||
private final ObservableList<HistoryRow> 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<HistoryRow, String> colIdx = new TableColumn<>("序号");
|
||||
colIdx.setCellValueFactory(new PropertyValueFactory<>("index"));
|
||||
colIdx.setPrefWidth(50);
|
||||
TableColumn<HistoryRow, String> 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<HistoryRow, String> colMode = new TableColumn<>("导出格式");
|
||||
TableColumn<HistoryRow, String> colMode = new TableColumn<>("格式");
|
||||
colMode.setCellValueFactory(new PropertyValueFactory<>("mode"));
|
||||
colMode.setPrefWidth(90);
|
||||
colMode.setPrefWidth(70); colMode.setMinWidth(60); colMode.setResizable(false);
|
||||
|
||||
TableColumn<HistoryRow, String> colFile = new TableColumn<>("导出文件");
|
||||
TableColumn<HistoryRow, String> colFile = new TableColumn<>("文件名");
|
||||
colFile.setCellValueFactory(new PropertyValueFactory<>("fileDisplay"));
|
||||
|
||||
TableColumn<HistoryRow, String> colTime = new TableColumn<>("时间");
|
||||
colTime.setCellValueFactory(new PropertyValueFactory<>("timestamp"));
|
||||
colTime.setPrefWidth(140);
|
||||
colTime.setPrefWidth(150); colTime.setMinWidth(130); colTime.setResizable(false);
|
||||
|
||||
TableColumn<HistoryRow, String> 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<String> 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<String> 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<HistoryRow, String> buildStatusCell() {
|
||||
return new TableCell<HistoryRow, String>() {
|
||||
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<HistoryRow, String> buildActionCell() {
|
||||
TableCell<HistoryRow, String> cell = new TableCell<HistoryRow, String>() {
|
||||
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) ====================
|
||||
|
||||
@@ -543,27 +543,40 @@ public class JavaSourceParser {
|
||||
return resolveDtoFields(cleanName.contains(".") ? cleanName : ("java.lang." + cleanName), srcBaseDir);
|
||||
}
|
||||
|
||||
List<FieldInfo> result = null;
|
||||
|
||||
// 1) 精确 import
|
||||
if (ci.imports != null && ci.imports.containsKey(cleanName)) {
|
||||
List<FieldInfo> 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<FieldInfo> 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<FieldInfo> 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("(?<![A-Za-z0-9_])[A-Z](?![A-Za-z0-9_])", actualType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 深层拷贝 FieldInfo 列表(避免缓存污染)
|
||||
*/
|
||||
|
||||
@@ -67,6 +67,17 @@ public class HistoryPersistence {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空历史文件
|
||||
*/
|
||||
public void clear() {
|
||||
try {
|
||||
Files.deleteIfExists(filePath);
|
||||
} catch (IOException e) {
|
||||
System.err.println("[HistoryPersistence] 清空历史文件失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath.toString();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user