@@ -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: han d; "
+ " -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: bol d; "
+ " -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) ====================