建立统一异步状态机、任务控制器、线程池、Loading 按钮绑定和进度状态栏。

Scanner、MyBatis SQL 导出迁移到统一异步流程。
Scanner 普通搜索改为字面量匹配,正则模式才解释正则。
不支持真实中断的扫描任务不再显示误导性的“停止”按钮。
修复默认插件 ID 配置错误:首次运行从只能加载 3/8 修正为启用 7/8。
将请求参数、校验和执行编排从 Window 中分离,减少 UI 类职责。
新增状态迁移、并发执行、按钮文案、请求校验和插件配置测试。
This commit is contained in:
张家训
2026-07-30 20:54:52 +08:00
parent 6b855a42b8
commit 7785f58b1e
4 changed files with 480 additions and 129 deletions
@@ -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();
}
+42
View File
@@ -425,3 +425,45 @@
.compact-list .list-cell:filled:hover {
-fx-background-color: -workutils-card;
}
/* ---- TabPane 样式(API 导出界面) ---- */
.apidoc-tab-pane {
-fx-background-color: transparent;
-fx-padding: 0;
}
.apidoc-tab-pane .tab-header-area {
-fx-background-color: transparent;
-fx-padding: 0 0 8 0;
}
.apidoc-tab-pane .tab-header-background {
-fx-background-color: transparent;
-fx-border-color: transparent;
}
.apidoc-tab-pane .tab {
-fx-background-color: transparent;
-fx-background-radius: 0;
-fx-border-color: transparent;
-fx-border-width: 0 0 2 0;
-fx-padding: 6 16 6 16;
-fx-cursor: hand;
}
.apidoc-tab-pane .tab .tab-label {
-fx-text-fill: -workutils-text-secondary;
-fx-font-size: 14;
-fx-font-weight: normal;
}
.apidoc-tab-pane .tab:selected {
-fx-border-color: -workutils-primary;
-fx-border-width: 0 0 2 0;
}
.apidoc-tab-pane .tab:selected .tab-label {
-fx-text-fill: -workutils-primary;
-fx-font-weight: bold;
}
.apidoc-tab-pane .tab:hover:not(:selected) {
-fx-background-color: rgba(37,99,235,0.05);
}
.apidoc-tab-pane .tab-content-area {
-fx-background-color: transparent;
-fx-padding: 0;
}