From 15157acd4ebe70c4d38c536f238fba6e48db1c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Wed, 17 Jun 2026 19:11:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=92=E4=BB=B6=E7=89=88=E6=9C=AC=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=9B=B4=E6=96=B0=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 7 + .../workutils/biz/git/GitSyncPlugin.java | 27 +- .../MyBatisGeneratorWindow.java | 138 ++- .../generator/MyBatisGeneratorService.java | 113 +- .../system/config/ExternalConfigManager.java | 9 +- .../system/config/SettingsService.java | 3 +- .../system/config/SettingsWindow.java | 1015 ++++++++--------- .../system/config/WorkUtilsProperties.java | 20 +- .../system/plugin/PluginManager.java | 8 +- src/main/resources/templates/datasources.json | 22 +- 10 files changed, 715 insertions(+), 647 deletions(-) diff --git a/pom.xml b/pom.xml index edbd34f..53bd980 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,13 @@ 5.1.47 + + + com.oceanbase + oceanbase-client + 2.4.12 + + com.alibaba fastjson diff --git a/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java b/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java index e4a6f07..a900f56 100644 --- a/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java +++ b/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java @@ -493,10 +493,23 @@ public class GitSyncPlugin implements Plugin, InitializingBean { log.warn("fetch 失败: {} -> {}", repoPath, r1.error); } - // 2. checkout + // 2. checkout:先 stash 本地修改,避免因未提交修改导致 checkout 失败 + boolean didStash = false; CmdResult r2 = runGit(repoPath, "git checkout " + branch); + if (!r2.ok) { + // checkout 失败常见原因:有未提交修改 → stash 后重试 + CmdResult stashResult = runGit(repoPath, "git stash push -m \"workutils-auto-stash\""); + if (stashResult.ok) { + didStash = true; + r2 = runGit(repoPath, "git checkout " + branch); + } + } if (!r2.ok) { log.warn("checkout 失败: {} -> {},标记为损坏", repoPath, r2.error); + // 如果 stash 了但 checkout 仍失败,恢复 stash + if (didStash) { + runGit(repoPath, "git stash pop"); + } SyncResult r = SyncResult.DAMAGED; r.error = "checkout 失败: " + r2.error; r.remoteUrl = getRemoteUrl(repoPath, baseUrlMap); @@ -507,12 +520,24 @@ public class GitSyncPlugin implements Plugin, InitializingBean { CmdResult r3 = runGit(repoPath, "git pull origin " + branch); if (!r3.ok) { log.warn("pull 失败: {} -> {},标记为损坏", repoPath, r3.error); + // pull 失败时恢复 stash(如果 stash 过) + if (didStash) { + runGit(repoPath, "git stash pop"); + } SyncResult r = SyncResult.DAMAGED; r.error = "pull 失败: " + r3.error; r.remoteUrl = getRemoteUrl(repoPath, baseUrlMap); return r; } + // 4. 恢复之前 stash 的修改(忽略冲突,保留 stash 让用户手动处理) + if (didStash) { + CmdResult popResult = runGit(repoPath, "git stash pop"); + if (!popResult.ok) { + log.warn("stash pop 有冲突: {} -> {},已保留在 stash 中", repoPath, popResult.error); + } + } + SyncResult result = SyncResult.OK; result.output = r3.output; return result; diff --git a/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorWindow.java b/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorWindow.java index d6ed2ae..1871b46 100644 --- a/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorWindow.java +++ b/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorWindow.java @@ -158,8 +158,9 @@ public class MyBatisGeneratorWindow { ToggleButton btn = new ToggleButton(label); btn.setToggleGroup(fillTargetGroup); btn.setUserData(userData); - btn.setPrefWidth(85); - btn.setStyle("-fx-font-size: " + fsButton() + "px; -fx-cursor: hand;"); + btn.setMinWidth(Region.USE_PREF_SIZE); + btn.setStyle("-fx-font-size: " + fsButton() + "px; -fx-cursor: hand; " + + "-fx-padding: 6 16;"); // 点击按钮时,将当前树节点路径填充到对应字段 btn.setOnAction(e -> { TreeItem selected = projectTree.getSelectionModel().getSelectedItem(); @@ -176,71 +177,103 @@ public class MyBatisGeneratorWindow { boolean active = (btn == cur); btn.setStyle(active ? "-fx-background-color: #4a90d9; -fx-text-fill: white; -fx-font-size: " + fsButton() + "px; " - + "-fx-background-radius: 4; -fx-padding: 6 18;" + + "-fx-background-radius: 4; -fx-padding: 6 16;" : "-fx-background-color: #e0e0e0; -fx-text-fill: #333; -fx-font-size: " + fsButton() + "px; " - + "-fx-background-radius: 4; -fx-padding: 6 18;"); + + "-fx-background-radius: 4; -fx-padding: 6 16;"); }); // 初始化默认选中样式 btn.fire(); return btn; } - private HBox createDbInfoSection() { - HBox box = new HBox(15); - box.setPadding(new Insets(8, 12, 8, 12)); - box.setStyle("-fx-background-color: #e8f0fe; -fx-background-radius: 4;"); - box.setAlignment(Pos.CENTER_LEFT); + private boolean refreshingDatasource = false; + private HBox createDbInfoSection() { + // 与 SplitPane 相同的比例:左侧 28% = 左面板宽度,右侧 72% + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setPadding(new Insets(8, 12, 8, 12)); + grid.setStyle("-fx-background-color: #e8f0fe; -fx-background-radius: 4;"); + + ColumnConstraints colLeft = new ColumnConstraints(); + colLeft.setPercentWidth(28); + ColumnConstraints colRight = new ColumnConstraints(); + colRight.setPercentWidth(72); + grid.getColumnConstraints().addAll(colLeft, colRight); + + // --- 左侧:数据源选择,宽度与下方左面板对齐 --- Label dsLabel = new Label("数据源:"); dsLabel.setFont(Font.font("Microsoft YaHei", fsBody())); + dsLabel.setMinWidth(Region.USE_PREF_SIZE); datasourceCmb = new ComboBox<>(); - datasourceCmb.setPrefWidth(160); datasourceCmb.setStyle("-fx-font-size: " + fsBody() + "px;"); + HBox.setHgrow(datasourceCmb, Priority.ALWAYS); + + Button refreshDsBtn = new Button("刷新"); + refreshDsBtn.setMinWidth(Region.USE_PREF_SIZE); + refreshDsBtn.setStyle("-fx-font-size: " + (fsBody() - 1) + "px; -fx-padding: 2 8;"); - // 加载数据源列表 refreshDatasourceList(); datasourceCmb.setOnAction(e -> { + if (refreshingDatasource) return; String selected = datasourceCmb.getValue(); if (selected != null) { callback.switchDatasource("默认配置".equals(selected) ? null : selected); updateDbInfo(); } }); - - dbUrlLabel = new Label(); - dbUrlLabel.setFont(Font.font("Consolas", fsBody() - 1)); - dbUserLabel = new Label(); - dbUserLabel.setFont(Font.font("Microsoft YaHei", fsBody() - 1)); - updateDbInfo(); - - Button refreshDsBtn = new Button("刷新"); - refreshDsBtn.setStyle("-fx-font-size: " + (fsBody() - 1) + "px; -fx-padding: 2 8;"); refreshDsBtn.setOnAction(e -> { refreshDatasourceList(); updateDbInfo(); }); - VBox infoBox = new VBox(3, dbUrlLabel, dbUserLabel); + HBox leftBox = new HBox(8, dsLabel, datasourceCmb, refreshDsBtn); + leftBox.setAlignment(Pos.CENTER_LEFT); - box.getChildren().addAll(dsLabel, datasourceCmb, refreshDsBtn, infoBox); - return box; + // --- 右侧:连接信息,白底灰框 --- + dbUrlLabel = new Label(); + dbUrlLabel.setFont(Font.font("Consolas", fsBody() - 1)); + dbUrlLabel.setMaxWidth(Double.MAX_VALUE); + dbUrlLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); + dbUserLabel = new Label(); + dbUserLabel.setFont(Font.font("Microsoft YaHei", fsBody() - 1)); + updateDbInfo(); + + VBox infoBox = new VBox(3, dbUrlLabel, dbUserLabel); + infoBox.setPadding(new Insets(4, 10, 4, 10)); + infoBox.setStyle("-fx-border-color: #b0c4de; -fx-border-width: 1; -fx-border-radius: 3; " + + "-fx-background-color: #ffffff; -fx-background-radius: 3;"); + infoBox.setAlignment(Pos.CENTER_LEFT); + infoBox.setMaxWidth(Double.MAX_VALUE); + + grid.add(leftBox, 0, 0); + grid.add(infoBox, 1, 0); + + HBox wrapper = new HBox(grid); + HBox.setHgrow(grid, Priority.ALWAYS); + return wrapper; } private void refreshDatasourceList() { - String current = datasourceCmb.getValue(); - datasourceCmb.getItems().clear(); - datasourceCmb.getItems().add("默认配置"); - datasourceCmb.getItems().addAll(callback.getDatasourceNames()); - if (current != null && datasourceCmb.getItems().contains(current)) { - datasourceCmb.setValue(current); - } else { - String dsName = callback.getCurrentDatasourceName(); - if (dsName != null && datasourceCmb.getItems().contains(dsName)) { - datasourceCmb.setValue(dsName); + refreshingDatasource = true; + try { + String current = datasourceCmb.getValue(); + datasourceCmb.getItems().clear(); + datasourceCmb.getItems().add("默认配置"); + datasourceCmb.getItems().addAll(callback.getDatasourceNames()); + if (current != null && datasourceCmb.getItems().contains(current)) { + datasourceCmb.setValue(current); } else { - datasourceCmb.setValue("默认配置"); + String dsName = callback.getCurrentDatasourceName(); + if (dsName != null && datasourceCmb.getItems().contains(dsName)) { + datasourceCmb.setValue(dsName); + } else { + datasourceCmb.setValue("默认配置"); + } } + } finally { + refreshingDatasource = false; } } @@ -279,22 +312,7 @@ public class MyBatisGeneratorWindow { VBox.setVgrow(projectTree, Priority.ALWAYS); - // 填充目标选择(按钮组) - Label quickLabel = new Label("点击按钮填充到:"); - quickLabel.setFont(Font.font("Microsoft YaHei", fsBody())); - quickLabel.setPadding(new Insets(10, 0, 2, 0)); - - this.fillTargetGroup = new ToggleGroup(); - - ToggleButton btnModel = createFillButton("Model", "model"); - btnModel.setSelected(true); - ToggleButton btnXml = createFillButton("XML", "xml"); - ToggleButton btnMapper = createFillButton("Mapper", "mapper"); - - HBox rbBox = new HBox(10, btnModel, btnXml, btnMapper); - rbBox.setPadding(new Insets(5, 0, 5, 0)); - - panel.getChildren().addAll(treeTitle, projectTree, quickLabel, rbBox); + panel.getChildren().addAll(treeTitle, projectTree); return panel; } @@ -410,9 +428,27 @@ public class MyBatisGeneratorWindow { VBox box = new VBox(8); box.setPadding(new Insets(10, 0, 0, 0)); + // 标题行:左侧"输出目录设置",右侧快速填充按钮 Label dirLabel = new Label("输出目录设置:"); dirLabel.setFont(Font.font("Microsoft YaHei", fsSection())); + this.fillTargetGroup = new ToggleGroup(); + + Label quickLabel = new Label("快速填充:"); + quickLabel.setFont(Font.font("Microsoft YaHei", fsBody())); + quickLabel.setMinWidth(Region.USE_PREF_SIZE); + + ToggleButton btnModel = createFillButton("Model", "model"); + btnModel.setSelected(true); + ToggleButton btnXml = createFillButton("XML", "xml"); + ToggleButton btnMapper = createFillButton("Mapper", "mapper"); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + HBox headerRow = new HBox(10, dirLabel, spacer, quickLabel, btnModel, btnXml, btnMapper); + headerRow.setAlignment(Pos.CENTER_LEFT); + HBox modelRow = createDirRow("Model (PO)", fm -> modelDirField = fm); modelDirField = (TextField) modelRow.getChildren().get(1); HBox xmlRow = createDirRow("XML Mapper", fm -> xmlDirField = fm); @@ -422,7 +458,7 @@ public class MyBatisGeneratorWindow { loadDefaultDirs(); - box.getChildren().addAll(dirLabel, modelRow, xmlRow, mapperRow); + box.getChildren().addAll(headerRow, modelRow, xmlRow, mapperRow); return box; } diff --git a/src/main/java/com/example/workutils/biz/mybatisgenerator/service/generator/MyBatisGeneratorService.java b/src/main/java/com/example/workutils/biz/mybatisgenerator/service/generator/MyBatisGeneratorService.java index 1d45635..5a1094a 100644 --- a/src/main/java/com/example/workutils/biz/mybatisgenerator/service/generator/MyBatisGeneratorService.java +++ b/src/main/java/com/example/workutils/biz/mybatisgenerator/service/generator/MyBatisGeneratorService.java @@ -101,7 +101,7 @@ public class MyBatisGeneratorService { DbConnection db = getDbConnection(); String dbName = extractDatabaseName(db.url); - try (Connection conn = DriverManager.getConnection(db.url, db.username, db.password)) { + try (Connection conn = openConnection(db)) { String sql = "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME LIKE ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, dbName); @@ -124,7 +124,7 @@ public class MyBatisGeneratorService { DbConnection db = getDbConnection(); String dbName = extractDatabaseName(db.url); - try (Connection conn = DriverManager.getConnection(db.url, db.username, db.password)) { + try (Connection conn = openConnection(db)) { String sql = "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, dbName); @@ -138,6 +138,39 @@ public class MyBatisGeneratorService { return tables; } + /** + * 打开数据库连接。先尝试配置的驱动,失败则回退 MySQL 驱动并改写 URL。 + */ + private Connection openConnection(DbConnection db) throws SQLException { + // 第一优先级:尝试用配置的驱动连接 + try { + Class.forName(db.driverClass); + return DriverManager.getConnection(db.url, db.username, db.password); + } catch (Exception e) { + log.warn("驱动 {} 不可用或连接失败,尝试 MySQL 驱动回退: {}", db.driverClass, e.getMessage()); + } + + // 第二优先级:MySQL 驱动回退 + try { + Class.forName("com.mysql.jdbc.Driver"); + } catch (ClassNotFoundException ignored) { + } + + // 第三优先级:尝试 MySQL 5.1 之后的驱动类名 + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + } catch (ClassNotFoundException ignored) { + } + + // 将 jdbc:oceanbase:// 替换为 jdbc:mysql://(OB 4.x MySQL 模式兼容 MySQL 协议) + String fallbackUrl = db.url; + if (fallbackUrl.startsWith("jdbc:oceanbase://")) { + fallbackUrl = fallbackUrl.replace("jdbc:oceanbase://", "jdbc:mysql://"); + log.info("URL 已改写为 MySQL 协议: {}", fallbackUrl); + } + return DriverManager.getConnection(fallbackUrl, db.username, db.password); + } + /** * 获取当前生效的数据库连接信息 * 优先级: 选中的数据源 > 外部配置(workutils.config) > 内置classpath属性 @@ -145,11 +178,12 @@ public class MyBatisGeneratorService { private DbConnection getDbConnection() { DatasourceConfig ds = getCurrentDatasource(); if (ds != null) { - return new DbConnection(ds.getUrl(), ds.getUsername(), ds.getPassword()); + return new DbConnection(ds.getDriver(), ds.getUrl(), ds.getUsername(), ds.getPassword()); } // 回退: data/ 外部配置 → classpath 内置配置 Properties props = loadMybatisProperties(); return new DbConnection( + props.getProperty("db_driver", "com.mysql.jdbc.Driver"), props.getProperty("db_url", ""), props.getProperty("db_username", ""), props.getProperty("db_password", "") @@ -158,10 +192,12 @@ public class MyBatisGeneratorService { /** 内部数据类:数据库连接信息 */ private static class DbConnection { + final String driverClass; final String url; final String username; final String password; - DbConnection(String url, String username, String password) { + DbConnection(String driverClass, String url, String username, String password) { + this.driverClass = driverClass; this.url = url; this.username = username; this.password = password; @@ -264,6 +300,10 @@ public class MyBatisGeneratorService { ""); } + // ★ 关键修复:将 JDBC 连接属性直接注入到 XML 中(绕过 properties 文件), + // 并自动执行 OB→MySQL 驱动/URL 兜底,确保 MBG 总能连上数据库 + content = injectJdbcFallback(content); + // 截掉 之后的内容(防止尾随垃圾导致 XML 解析失败) int endIdx = content.lastIndexOf(""); if (endIdx >= 0) { @@ -335,9 +375,25 @@ public class MyBatisGeneratorService { "$1" + java.util.regex.Matcher.quoteReplacement(mapperPkg) + "$2"); } + // 从 JDBC URL 中提取数据库名作为 schema,避免 MyBatis Generator 找不到表 + String schema = ""; + try { + Properties props = getConfigurationProperties(); + String url = props.getProperty("db_url", ""); + if (!url.isEmpty()) { + schema = extractDatabaseName(url); + } + } catch (Exception ignored) { + } + StringBuilder tableElements = new StringBuilder(); for (String tableName : tableNames) { - tableElements.append(" ", ">"); + } + /** * 根据表名生成实体类名 */ diff --git a/src/main/java/com/example/workutils/system/config/ExternalConfigManager.java b/src/main/java/com/example/workutils/system/config/ExternalConfigManager.java index c1acfed..40b4819 100644 --- a/src/main/java/com/example/workutils/system/config/ExternalConfigManager.java +++ b/src/main/java/com/example/workutils/system/config/ExternalConfigManager.java @@ -58,7 +58,7 @@ public class ExternalConfigManager { public static final String KEY_FONT_BODY = "font.size.body"; // 正文/提示 默认14 public static final String KEY_FONT_MONO = "font.size.mono"; // 等宽/代码 默认14 - /** 配置节定义:名称 → 键列表 */ + /** 配置节定义:名称 → 键列表(MYBATIS 相关配置已迁移到 data/mybatis-generator.properties 独立管理) */ private static final LinkedHashMap SECTIONS = new LinkedHashMap<>(); static { @@ -68,13 +68,6 @@ public class ExternalConfigManager { SECTIONS.put("===== 数据文件路径 =====", new String[]{ KEY_PROJECT_PATHS, KEY_DAMAGED_REPOS }); - SECTIONS.put("===== MyBatis 数据库连接(覆盖内置 mybatis-generator.properties)=====", new String[]{ - KEY_MYBATIS_DRIVER, KEY_MYBATIS_URL, KEY_MYBATIS_USERNAME, - KEY_MYBATIS_PASSWORD - }); - SECTIONS.put("===== MyBatis 代码生成包路径 =====", new String[]{ - KEY_MYBATIS_MODEL_PKG, KEY_MYBATIS_DAO_PKG, KEY_MYBATIS_SQLMAPPER_PKG - }); SECTIONS.put("===== 字体大小(显示设置) =====", new String[]{ KEY_FONT_SECTION_TITLE, KEY_FONT_FIELD_LABEL, KEY_FONT_BUTTON, KEY_FONT_BODY, KEY_FONT_MONO diff --git a/src/main/java/com/example/workutils/system/config/SettingsService.java b/src/main/java/com/example/workutils/system/config/SettingsService.java index e19c1f8..8ddf959 100644 --- a/src/main/java/com/example/workutils/system/config/SettingsService.java +++ b/src/main/java/com/example/workutils/system/config/SettingsService.java @@ -22,6 +22,7 @@ public class SettingsService { private final WorkUtilsProperties properties; private final ExternalConfigManager configManager; + private final DatasourceConfigManager datasourceConfigManager; /** * 打开系统设置 GUI 窗口 @@ -30,7 +31,7 @@ public class SettingsService { */ public void showSettings(Scanner scanner, Runnable onConfigChanged) { try { - SettingsWindow window = new SettingsWindow(properties, configManager, onConfigChanged); + SettingsWindow window = new SettingsWindow(properties, configManager, datasourceConfigManager, onConfigChanged); window.showAndWait(); } catch (Exception e) { log.error("打开设置窗口失败,回退到控制台模式", e); diff --git a/src/main/java/com/example/workutils/system/config/SettingsWindow.java b/src/main/java/com/example/workutils/system/config/SettingsWindow.java index 8191427..a74aac0 100644 --- a/src/main/java/com/example/workutils/system/config/SettingsWindow.java +++ b/src/main/java/com/example/workutils/system/config/SettingsWindow.java @@ -8,31 +8,36 @@ import javafx.geometry.Rectangle2D; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.control.Alert.AlertType; -import javafx.scene.layout.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.stage.DirectoryChooser; import javafx.stage.Screen; import javafx.stage.Stage; import lombok.extern.slf4j.Slf4j; +import java.awt.Desktop; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; import java.util.Properties; import java.util.concurrent.CountDownLatch; /** - * 系统设置可视化窗口(JavaFX) + * 系统设置可视化窗口(JavaFX)—— 重构版 + *

+ * 每个 Tab 直接指向 data 目录下的配置文件,用系统默认编辑器打开编辑, + * 各配置文件独立管理,编辑后点击「重新加载」生效。 *

* Tab 页组织: *

    - *
  • 系统路径 — 基础目录、导出目录、数据文件路径
  • - *
  • MyBatis 数据库 — 覆盖 classpath 内的 mybatis-generator.properties
  • - *
  • 项目管理 — 查看/编辑项目路径文件、损坏仓库缓存
  • + *
  • 系统路径 — 打开 workutils.config(系统路径 + 字体设置)
  • + *
  • MyBatis 数据库 — 打开 data/mybatis-generator.properties
  • + *
  • 项目路径 — 打开 data/project_paths.txt + 批量字符替换工具
  • + *
  • 多数据源 — 打开 data/datasources.json
  • + *
  • 损坏缓存 — 查看 data/damaged_repos.cache(只读)
  • + *
  • 显示设置 — 字体大小滑块预览
  • *
*/ @Slf4j @@ -40,26 +45,14 @@ public class SettingsWindow { private final WorkUtilsProperties properties; private final ExternalConfigManager configManager; + private final DatasourceConfigManager datasourceConfigManager; private final Runnable onConfigChanged; private Stage stage; - // --- 系统路径 Tab --- - private TextField tfBaseDir, tfExportDir, tfProjectPathsFile, tfDamagedReposFile; - private Label lblConfigFile; - - // --- MyBatis Tab --- - private TextField tfDbDriver, tfDbUrl, tfDbUsername; - private PasswordField pfDbPassword; - private TextField tfModelPkg, tfDaoPkg, tfSqlMapperPkg; - - // --- 项目管理 Tab --- - private TextArea taProjectPaths; - private TextArea taDamagedCache; - private Label lblProjectPathsFile, lblDamagedCacheFile; - - // --- 项目路径替换 --- + // --- 项目路径替换工具 --- private TextField tfReplaceParentDir, tfReplaceAccount; + private TextArea taProjectPathsPreview; // --- 字体大小 --- private int fontSectionTitle; @@ -77,9 +70,11 @@ public class SettingsWindow { public SettingsWindow(WorkUtilsProperties properties, ExternalConfigManager configManager, + DatasourceConfigManager datasourceConfigManager, Runnable onConfigChanged) { this.properties = properties; this.configManager = configManager; + this.datasourceConfigManager = datasourceConfigManager; this.onConfigChanged = onConfigChanged; this.fontSectionTitle = properties.getFontSectionTitle(); this.fontFieldLabel = properties.getFontFieldLabel(); @@ -109,7 +104,6 @@ public class SettingsWindow { } }); latch.await(); - // 关闭后显式释放 stage,减少 JavaFX 对 System.in 的干扰 Platform.runLater(() -> { if (stage != null) { stage.hide(); @@ -124,13 +118,15 @@ public class SettingsWindow { createPathTab(), createMyBatisTab(), createProjectTab(), + createDatasourceTab(), + createDamagedCacheTab(), createDisplayTab() ); tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); VBox root = new VBox(10); root.setPadding(new Insets(12)); - root.getChildren().addAll(tabPane, createBottomButtons()); + root.getChildren().add(tabPane); return new Scene(root, screen.getWidth() * 0.75, screen.getHeight() * 0.75); } @@ -139,179 +135,274 @@ public class SettingsWindow { return Screen.getPrimary().getVisualBounds(); } + // ==================== 通用 Tab 构建器 ==================== + + /** + * 创建一个标准配置 Tab:文件路径 + 打开/重新加载按钮 + */ + private Tab createConfigFileTab(String tabName, String fileDescription, + Path filePath, Runnable onReload, + boolean isReadOnly, String extraHint) { + VBox box = new VBox(12); + box.setPadding(new Insets(15)); + + // 标题 + Label title = new Label(fileDescription); + title.setStyle("-fx-font-weight: bold; -fx-font-size: " + fontSectionTitle + "px;"); + titleLabels.add(title); + box.getChildren().add(title); + + // 文件路径 + Label pathLabel = new Label(filePath.toAbsolutePath().toString()); + pathLabel.setStyle("-fx-font-family: 'Consolas'; -fx-font-size: " + fontMono + "px; " + + "-fx-background-color: #f5f5f5; -fx-padding: 8; -fx-background-radius: 4;"); + pathLabel.setWrapText(true); + pathLabel.setMaxWidth(Double.MAX_VALUE); + box.getChildren().add(pathLabel); + + if (!Files.exists(filePath)) { + Label notExist = new Label("⚠ 文件尚未创建,点击下方按钮将自动创建空文件。"); + notExist.setStyle("-fx-text-fill: #e67e22; -fx-font-size: " + fontBody + "px;"); + hintLabels.add(notExist); + box.getChildren().add(notExist); + } + + if (isReadOnly) { + Label readOnly = new Label("⚠ 此文件为只读记录,由系统自动维护。"); + readOnly.setStyle("-fx-text-fill: #c0392b; -fx-font-size: " + fontBody + "px;"); + hintLabels.add(readOnly); + box.getChildren().add(readOnly); + } + + // 按钮行 + HBox btnRow = new HBox(10); + btnRow.setAlignment(Pos.CENTER_LEFT); + + Button btnOpen = new Button(isReadOnly ? "用记事本查看" : "用记事本打开"); + btnOpen.setFont(font(fontButton)); + btnOpen.setStyle("-fx-padding: 10 20; -fx-background-radius: 4; -fx-cursor: hand;"); + allButtons.add(btnOpen); + btnOpen.setOnAction(e -> openFileInEditor(filePath)); + btnRow.getChildren().add(btnOpen); + + if (onReload != null) { + Button btnReload = new Button("重新加载"); + btnReload.setFont(font(fontButton)); + btnReload.setStyle("-fx-padding: 10 20; -fx-background-radius: 4; -fx-cursor: hand;"); + allButtons.add(btnReload); + btnReload.setOnAction(e -> { + onReload.run(); + showAlert(AlertType.INFORMATION, "已重新加载: " + fileDescription); + }); + btnRow.getChildren().add(btnReload); + } + + box.getChildren().add(btnRow); + + // 提示 + String hint = isReadOnly + ? "此文件由系统自动维护,仅可查看。" + : "用外部编辑器编辑后,点击「重新加载」使配置生效,无需重启。"; + if (extraHint != null && !extraHint.isEmpty()) { + hint = hint + "\n" + extraHint; + } + Label hintLabel = new Label(hint); + hintLabel.setStyle("-fx-text-fill: #666; -fx-font-size: " + fontBody + "px;"); + hintLabels.add(hintLabel); + box.getChildren().add(hintLabel); + + Tab tab = new Tab(tabName); + tab.setContent(box); + tab.setClosable(false); + return tab; + } + // ==================== Tab 1: 系统路径 ==================== private Tab createPathTab() { - GridPane grid = new GridPane(); - grid.setHgap(10); - grid.setVgap(12); - grid.setPadding(new Insets(15)); - // 三列:标签 | 输入框(自动伸缩) | 按钮 - ColumnConstraints colLabel = new ColumnConstraints(); - colLabel.setPercentWidth(18); - ColumnConstraints colField = new ColumnConstraints(); - colField.setPercentWidth(72); - ColumnConstraints colBtn = new ColumnConstraints(); - colBtn.setPercentWidth(10); - grid.getColumnConstraints().addAll(colLabel, colField, colBtn); - - int row = 0; - - // 节标题 - addTitleLabel(grid, row++, "系统路径", 3); - tfBaseDir = addBrowseRow(grid, row++, "基础目录:", properties.getBaseDirectory()); - tfExportDir = addBrowseRow(grid, row++, "导出目录:", properties.getExportDirectory()); - - grid.add(new Separator(), 0, row++, 3, 1); - - Label secData = addTitleLabel(grid, row++, "数据文件路径", 3); - secData.setStyle(secData.getStyle() + "-fx-padding: 8 0 8 0;"); - - tfProjectPathsFile = addBrowseRow(grid, row++, "项目路径文件:", - properties.getProjectPathsFilePath().toAbsolutePath().toString()); - tfDamagedReposFile = addBrowseRow(grid, row++, "损坏缓存文件:", - properties.getDamagedReposFilePath().toAbsolutePath().toString()); - - grid.add(new Separator(), 0, row++, 3, 1); - lblConfigFile = new Label("配置文件: " + configManager.getConfigFile().toAbsolutePath() - + (configManager.exists() ? " (已存在)" : " (不存在)")); - lblConfigFile.setStyle("-fx-text-fill: gray; -fx-font-size: " + fontBody + "px;"); - hintLabels.add(lblConfigFile); - grid.add(lblConfigFile, 0, row, 3, 1); - - row++; - Label hint = new Label("提示: 编辑后点击「保存到 workutils.config」生效,无需重启。"); - hint.setStyle("-fx-text-fill: #666; -fx-font-size: " + fontBody + "px;"); - hintLabels.add(hint); - grid.add(hint, 0, row, 3, 1); - - Tab tab = new Tab("系统路径"); - tab.setContent(grid); - tab.setClosable(false); - return tab; + return createConfigFileTab( + "系统路径", + "系统路径配置", + configManager.getConfigFile(), + this::reloadWorkutilsConfig, + false, + "编辑 workutils.config 中的 base-directory、export-directory 等属性。" + + "\n字体大小请在「显示设置」Tab 中调整。" + ); } // ==================== Tab 2: MyBatis 数据库 ==================== private Tab createMyBatisTab() { - VBox wrapper = new VBox(12); - wrapper.setPadding(new Insets(15)); - - Label hint = new Label("以下配置将覆盖 JAR 内置的 mybatis-generator.properties,留空则使用内置值。"); - hint.setStyle("-fx-text-fill: #666; -fx-font-size: " + fontBody + "px;"); - hintLabels.add(hint); - wrapper.getChildren().add(hint); - - GridPane grid = new GridPane(); - grid.setHgap(10); - grid.setVgap(10); - ColumnConstraints colLbl = new ColumnConstraints(); - colLbl.setPercentWidth(22); - ColumnConstraints colFld = new ColumnConstraints(); - colFld.setPercentWidth(78); - grid.getColumnConstraints().addAll(colLbl, colFld); - - int row = 0; - - addTitleLabel(grid, row++, "数据库连接", 2); - - tfDbDriver = addField(grid, row++, "数据库驱动:", ""); - tfDbUrl = addField(grid, row++, "数据库 URL:", ""); - tfDbUsername = addField(grid, row++, "用户名:", ""); - pfDbPassword = new PasswordField(); - textInputs.add(pfDbPassword); - HBox.setHgrow(pfDbPassword, Priority.ALWAYS); - GridPane.setHgrow(pfDbPassword, Priority.ALWAYS); - addLabeled(grid, row++, "密码:", pfDbPassword); - - grid.add(new Separator(), 0, row++, 2, 1); - - addTitleLabel(grid, row++, "代码生成包路径", 2); - - tfModelPkg = addField(grid, row++, "Model 包路径:", ""); - tfDaoPkg = addField(grid, row++, "Mapper 包路径:", ""); - tfSqlMapperPkg = addField(grid, row++, "SQL XML 包路径:", ""); - - wrapper.getChildren().add(grid); - - Button btnLoadBuiltin = new Button("从 data/mybatis-generator.properties 加载"); - btnLoadBuiltin.setFont(font(fontButton)); - btnLoadBuiltin.setStyle("-fx-padding: 6 14;"); - allButtons.add(btnLoadBuiltin); - btnLoadBuiltin.setOnAction(e -> loadMyBatisFromClasspath()); - wrapper.getChildren().add(btnLoadBuiltin); - - refreshMyBatisFields(); - - Tab tab = new Tab("MyBatis 数据库"); - tab.setContent(wrapper); - tab.setClosable(false); - return tab; + return createConfigFileTab( + "MyBatis 数据库", + "MyBatis 生成器数据库配置", + properties.getMybatisGeneratorFilePath(), + this::reloadMyBatisConfig, + false, + "编辑 data/mybatis-generator.properties:db_driver、db_url、db_username、db_password、\n" + + "model_package、dao_package、sqlmapper_package 等属性。" + ); } - // ==================== Tab 3: 项目管理 ==================== + // ==================== Tab 3: 项目路径 ==================== private Tab createProjectTab() { - TabPane innerTabs = new TabPane(); - innerTabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + VBox box = new VBox(12); + box.setPadding(new Insets(15)); - // 子Tab: 项目路径 - VBox ppBox = new VBox(10); - ppBox.setPadding(new Insets(10)); - lblProjectPathsFile = new Label(); - lblProjectPathsFile.setStyle("-fx-text-fill: gray; -fx-font-size: " + fontBody + "px;"); - hintLabels.add(lblProjectPathsFile); - taProjectPaths = new TextArea(); - taProjectPaths.setPrefRowCount(14); - taProjectPaths.setStyle("-fx-font-size: " + fontMono + "px;"); - textInputs.add(taProjectPaths); - taProjectPaths.setPromptText("# 每行一个项目\n# 格式: 项目路径\n# 格式: 项目路径=BASE_URL\n# # 开头为注释"); - Button btnLoadPP = addSmallButton("从文件加载"); - btnLoadPP.setOnAction(e -> loadProjectPaths()); - Button btnSavePP = addSmallButton("保存到文件"); - btnSavePP.setOnAction(e -> saveProjectPaths()); - Button btnOpenPP = addSmallButton("用记事本打开"); - btnOpenPP.setOnAction(e -> openInNotepad(properties.getProjectPathsFilePath())); + Path filePath = properties.getProjectPathsFilePath(); - HBox ppBtns = new HBox(10, btnLoadPP, btnSavePP, btnOpenPP); - ppBox.getChildren().addAll(lblProjectPathsFile, taProjectPaths, ppBtns, createReplaceTool()); + // 标题 + Label title = new Label("项目路径配置"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: " + fontSectionTitle + "px;"); + titleLabels.add(title); + box.getChildren().add(title); - Tab ppTab = new Tab("项目路径"); - ppTab.setContent(ppBox); + // 文件路径 + Label pathLabel = new Label(filePath.toAbsolutePath().toString()); + pathLabel.setStyle("-fx-font-family: 'Consolas'; -fx-font-size: " + fontMono + "px; " + + "-fx-background-color: #f5f5f5; -fx-padding: 8; -fx-background-radius: 4;"); + pathLabel.setWrapText(true); + pathLabel.setMaxWidth(Double.MAX_VALUE); + box.getChildren().add(pathLabel); - // 子Tab: 损坏仓库缓存 - VBox dcBox = new VBox(10); - dcBox.setPadding(new Insets(10)); - lblDamagedCacheFile = new Label(); - lblDamagedCacheFile.setStyle("-fx-text-fill: gray; -fx-font-size: " + fontBody + "px;"); - hintLabels.add(lblDamagedCacheFile); - taDamagedCache = new TextArea(); - taDamagedCache.setPrefRowCount(14); - taDamagedCache.setStyle("-fx-font-size: " + fontMono + "px;"); - textInputs.add(taDamagedCache); - taDamagedCache.setEditable(false); - Button btnLoadDC = addSmallButton("刷新"); - btnLoadDC.setOnAction(e -> loadDamagedCache()); - Button btnClearDC = addSmallButton("清理缓存"); - btnClearDC.setOnAction(e -> clearDamagedCache()); - btnClearDC.setStyle("-fx-text-fill: red; -fx-cursor: hand;"); + // 按钮行 + HBox btnRow = new HBox(10); + btnRow.setAlignment(Pos.CENTER_LEFT); - HBox dcBtns = new HBox(10, btnLoadDC, btnClearDC); - dcBox.getChildren().addAll(lblDamagedCacheFile, taDamagedCache, dcBtns); + Button btnOpen = new Button("用记事本打开"); + btnOpen.setFont(font(fontButton)); + btnOpen.setStyle("-fx-padding: 10 20; -fx-background-radius: 4; -fx-cursor: hand;"); + allButtons.add(btnOpen); + btnOpen.setOnAction(e -> openFileInEditor(filePath)); + btnRow.getChildren().add(btnOpen); - Tab dcTab = new Tab("损坏仓库缓存"); - dcTab.setContent(dcBox); + Button btnReload = new Button("重新加载"); + btnReload.setFont(font(fontButton)); + btnReload.setStyle("-fx-padding: 10 20; -fx-background-radius: 4; -fx-cursor: hand;"); + allButtons.add(btnReload); + btnReload.setOnAction(e -> { + reloadProjectPathsPreview(); + showAlert(AlertType.INFORMATION, "已重新加载项目路径。"); + }); + btnRow.getChildren().add(btnReload); - innerTabs.getTabs().addAll(ppTab, dcTab); - refreshProjectTab(); + box.getChildren().add(btnRow); - Tab tab = new Tab("项目管理"); - tab.setContent(innerTabs); + // 提示 + Label hint = new Label("用外部编辑器编辑 project_paths.txt 后,点击「重新加载」使配置生效。"); + hint.setStyle("-fx-text-fill: #666; -fx-font-size: " + fontBody + "px;"); + hintLabels.add(hint); + box.getChildren().add(hint); + + // --- 批量字符替换工具 --- + box.getChildren().add(createReplaceTool()); + + Tab tab = new Tab("项目路径"); + tab.setContent(box); tab.setClosable(false); return tab; } - // ==================== Tab 4: 显示设置 ==================== + // ==================== Tab 4: 多数据源 ==================== + + private Tab createDatasourceTab() { + return createConfigFileTab( + "多数据源", + "多数据源配置", + properties.getDatasourcesFilePath(), + () -> datasourceConfigManager.reload(), + false, + "编辑 data/datasources.json 中的多套数据源列表。\n" + + "格式为 JSON 数组,每项包含 name、db_driver、db_url、db_username、db_password、\n" + + "model_package、dao_package、sqlmapper_package 等字段。" + ); + } + + // ==================== Tab 5: 损坏缓存 ==================== + + private Tab createDamagedCacheTab() { + VBox box = new VBox(12); + box.setPadding(new Insets(15)); + + Path filePath = properties.getDamagedReposFilePath(); + + // 标题 + Label title = new Label("损坏仓库缓存"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: " + fontSectionTitle + "px;"); + titleLabels.add(title); + box.getChildren().add(title); + + // 文件路径 + Label pathLabel = new Label(filePath.toAbsolutePath().toString()); + pathLabel.setStyle("-fx-font-family: 'Consolas'; -fx-font-size: " + fontMono + "px; " + + "-fx-background-color: #f5f5f5; -fx-padding: 8; -fx-background-radius: 4;"); + pathLabel.setWrapText(true); + pathLabel.setMaxWidth(Double.MAX_VALUE); + box.getChildren().add(pathLabel); + + if (!Files.exists(filePath)) { + Label notExist = new Label("⚠ 缓存文件为空,暂无损坏记录。"); + notExist.setStyle("-fx-text-fill: #e67e22; -fx-font-size: " + fontBody + "px;"); + hintLabels.add(notExist); + box.getChildren().add(notExist); + } + + Label readOnly = new Label("⚠ 此文件由系统自动维护,记录同步过程中损坏的 Git 仓库。"); + readOnly.setStyle("-fx-text-fill: #c0392b; -fx-font-size: " + fontBody + "px;"); + hintLabels.add(readOnly); + box.getChildren().add(readOnly); + + // 按钮行 + HBox btnRow = new HBox(10); + btnRow.setAlignment(Pos.CENTER_LEFT); + + Button btnView = new Button("用记事本查看"); + btnView.setFont(font(fontButton)); + btnView.setStyle("-fx-padding: 10 20; -fx-background-radius: 4; -fx-cursor: hand;"); + allButtons.add(btnView); + btnView.setOnAction(e -> openFileInEditor(filePath)); + btnRow.getChildren().add(btnView); + + Button btnClear = new Button("清理损坏缓存"); + btnClear.setFont(font(fontButton)); + btnClear.setStyle("-fx-text-fill: #c0392b; -fx-padding: 10 20; " + + "-fx-background-radius: 4; -fx-cursor: hand;"); + allButtons.add(btnClear); + btnClear.setOnAction(e -> { + Alert confirm = new Alert(AlertType.CONFIRMATION); + confirm.setTitle("确认清理"); + confirm.setHeaderText("清除损坏仓库缓存?"); + confirm.setContentText("缓存清除后,下次同步时将重新扫描所有仓库。"); + confirm.showAndWait().ifPresent(r -> { + if (r == ButtonType.OK) { + try { + Files.deleteIfExists(filePath); + showAlert(AlertType.INFORMATION, "损坏仓库缓存已清理。"); + } catch (IOException ex) { + showAlert(AlertType.ERROR, "清理失败: " + ex.getMessage()); + } + } + }); + }); + btnRow.getChildren().add(btnClear); + + box.getChildren().add(btnRow); + + // 提示 + Label hint = new Label("此文件由系统自动维护。清除后下次同步时将重新扫描所有仓库。"); + hint.setStyle("-fx-text-fill: #666; -fx-font-size: " + fontBody + "px;"); + hintLabels.add(hint); + box.getChildren().add(hint); + + Tab tab = new Tab("损坏缓存"); + tab.setContent(box); + tab.setClosable(false); + return tab; + } + + // ==================== Tab 6: 显示设置 ==================== private Tab createDisplayTab() { VBox root = new VBox(12); @@ -323,14 +414,13 @@ public class SettingsWindow { titleLabels.add(title); root.getChildren().add(title); - Label note = new Label("可调范围 14px – 24px,点击「保存到 workutils.config」后全局生效"); + Label note = new Label("可调范围 16px – 32px,点击「保存字体设置」后写入 workutils.config 并全局生效"); note.setStyle("-fx-text-fill: #666; -fx-font-size: " + fontBody + "px;"); hintLabels.add(note); root.getChildren().add(note); root.getChildren().add(new Separator()); - // 每个字体类别一行 root.getChildren().add(createFontRow("节标题(如: 系统路径、数据库连接)", fontSectionTitle, f -> { fontSectionTitle = f; applyAllFontSizes(); })); root.getChildren().add(createFontRow("字段标签(如: 基础目录、数据库 URL)", @@ -339,409 +429,128 @@ public class SettingsWindow { fontButton, f -> { fontButton = f; applyAllFontSizes(); })); root.getChildren().add(createFontRow("正文/提示(如: 配置说明、状态信息)", fontBody, f -> { fontBody = f; applyAllFontSizes(); })); - root.getChildren().add(createFontRow("等宽/代码(如: 数据库连接字符串、TextArea 内容)", + root.getChildren().add(createFontRow("等宽/代码(如: 文件路径、TextArea 内容)", fontMono, f -> { fontMono = f; applyAllFontSizes(); })); + root.getChildren().add(new Separator()); + + Button btnSaveFont = new Button("保存字体设置"); + btnSaveFont.setFont(font(fontButton)); + btnSaveFont.setStyle("-fx-padding: 10 24; -fx-background-radius: 4; -fx-cursor: hand;"); + allButtons.add(btnSaveFont); + btnSaveFont.setOnAction(e -> saveFontSettings()); + root.getChildren().add(btnSaveFont); + Tab tab = new Tab("显示设置"); tab.setContent(root); tab.setClosable(false); return tab; } - private HBox createFontRow(String description, int initialValue, - java.util.function.IntConsumer onChanged) { - HBox row = new HBox(12); - row.setAlignment(Pos.CENTER_LEFT); - row.setPadding(new Insets(6, 0, 6, 0)); + // ==================== 重新加载逻辑 ==================== - // 左侧名称 - Label nameLabel = new Label(description); - nameLabel.setPrefWidth(320); - nameLabel.setFont(font(14)); - - // 滑块 14-24 - Slider slider = new Slider(14, 24, initialValue); - slider.setPrefWidth(180); - slider.setMajorTickUnit(2); - slider.setMinorTickCount(1); - slider.setShowTickMarks(true); - slider.setShowTickLabels(false); - slider.setSnapToTicks(true); - - // 当前值标签 - Label valLabel = new Label(String.valueOf(initialValue) + "px"); - valLabel.setPrefWidth(40); - valLabel.setFont(font(14)); - valLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #4a90d9;"); - - // 预览文字 - Label preview = new Label("预览 Abc 123"); - preview.setFont(font(initialValue)); - // 根据类别使用不同预览样式 - if (description.contains("节标题")) { - preview.setStyle("-fx-font-weight: bold;"); - titleLabels.add(preview); - } else if (description.contains("提示")) { - preview.setStyle("-fx-text-fill: #666;"); - hintLabels.add(preview); - } else if (description.contains("等宽")) { - preview.setFont(Font.font("Consolas", initialValue)); + /** 重新加载 workutils.config */ + private void reloadWorkutilsConfig() { + if (!configManager.exists()) { + showAlert(AlertType.INFORMATION, "workutils.config 不存在,当前使用默认值。"); + return; } + Properties external = configManager.load(); + properties.mergeExternal(external); + if (onConfigChanged != null) onConfigChanged.run(); + log.info("已重新加载 workutils.config"); + } - // 滑块变化时更新 - slider.valueProperty().addListener((obs, oldVal, newVal) -> { - int v = (int) Math.round(newVal.doubleValue()); - if (slider.isValueChanging() || Math.abs(newVal.doubleValue() - oldVal.doubleValue()) > 0.1) { - valLabel.setText(v + "px"); - preview.setFont(description.contains("等宽") - ? Font.font("Consolas", v) - : font(v)); - onChanged.accept(v); + /** 重新加载 MyBatis 配置(从 mybatis-generator.properties 读取) */ + private void reloadMyBatisConfig() { + Path file = properties.getMybatisGeneratorFilePath(); + if (!Files.exists(file)) { + showAlert(AlertType.INFORMATION, "mybatis-generator.properties 不存在。"); + return; + } + try { + Properties props = new Properties(); + try (java.io.InputStream is = new java.io.FileInputStream(file.toFile())) { + props.load(is); } - }); - - row.getChildren().addAll(nameLabel, slider, valLabel, preview); - return row; - } - - /** 将所有字体大小重新应用到已创建的 UI 组件上 */ - private void applyAllFontSizes() { - for (Label lbl : titleLabels) { - lbl.setFont(font(fontSectionTitle)); - } - for (Label lbl : fieldLabels) { - lbl.setFont(font(fontFieldLabel)); - } - for (ButtonBase btn : allButtons) { - btn.setFont(font(fontButton)); - } - for (Label lbl : hintLabels) { - lbl.setFont(font(fontBody)); + properties.getMybatisOverrides().clear(); + String[] keys = { + ExternalConfigManager.KEY_MYBATIS_DRIVER, + ExternalConfigManager.KEY_MYBATIS_URL, + ExternalConfigManager.KEY_MYBATIS_USERNAME, + ExternalConfigManager.KEY_MYBATIS_PASSWORD, + ExternalConfigManager.KEY_MYBATIS_MODEL_PKG, + ExternalConfigManager.KEY_MYBATIS_DAO_PKG, + ExternalConfigManager.KEY_MYBATIS_SQLMAPPER_PKG + }; + for (String key : keys) { + String val = props.getProperty(key); + if (val != null && !val.isEmpty()) { + properties.getMybatisOverrides().put(key, val); + } + } + log.info("已重新加载 MyBatis 配置: {} 项", properties.getMybatisOverrides().size()); + } catch (IOException e) { + showAlert(AlertType.ERROR, "加载 MyBatis 配置失败: " + e.getMessage()); } } - private HBox createBottomButtons() { - Button btnSave = new Button("保存到 workutils.config"); - btnSave.setFont(font(fontButton + 1)); - btnSave.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white; -fx-font-weight: bold; " - + "-fx-padding: 8 20; -fx-background-radius: 4; -fx-cursor: hand;"); - btnSave.setOnAction(e -> saveConfig()); + // ==================== 字体设置保存 ==================== - Button btnReload = addSmallButton("重新加载"); - btnReload.setStyle("-fx-padding: 8 16; -fx-background-radius: 4; -fx-cursor: hand;"); - btnReload.setOnAction(e -> reloadConfig()); - - Button btnReset = addSmallButton("重置为默认值"); - btnReset.setStyle("-fx-text-fill: #c0392b; -fx-padding: 8 16; " - + "-fx-background-radius: 4; -fx-cursor: hand;"); - btnReset.setOnAction(e -> resetConfig()); - - Region spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); - - HBox box = new HBox(10, btnSave, btnReload, btnReset, spacer); - box.setAlignment(Pos.CENTER_LEFT); - box.setPadding(new Insets(8, 10, 8, 10)); - box.setStyle("-fx-border-color: #e0e0e0; -fx-border-width: 1 0 0 0;"); - return box; - } - - // ==================== 保存/加载/重置 ==================== - - private void saveConfig() { + private void saveFontSettings() { Properties props = new Properties(); - - putIfNotEmpty(props, ExternalConfigManager.KEY_BASE_DIR, tfBaseDir.getText()); - putIfNotEmpty(props, ExternalConfigManager.KEY_EXPORT_DIR, tfExportDir.getText()); - putIfNotEmpty(props, ExternalConfigManager.KEY_PROJECT_PATHS, tfProjectPathsFile.getText()); - putIfNotEmpty(props, ExternalConfigManager.KEY_DAMAGED_REPOS, tfDamagedReposFile.getText()); - - putIfNotEmpty(props, ExternalConfigManager.KEY_MYBATIS_DRIVER, tfDbDriver.getText()); - putIfNotEmpty(props, ExternalConfigManager.KEY_MYBATIS_URL, tfDbUrl.getText()); - putIfNotEmpty(props, ExternalConfigManager.KEY_MYBATIS_USERNAME, tfDbUsername.getText()); - putIfNotEmpty(props, ExternalConfigManager.KEY_MYBATIS_PASSWORD, pfDbPassword.getText()); - putIfNotEmpty(props, ExternalConfigManager.KEY_MYBATIS_MODEL_PKG, tfModelPkg.getText()); - putIfNotEmpty(props, ExternalConfigManager.KEY_MYBATIS_DAO_PKG, tfDaoPkg.getText()); - putIfNotEmpty(props, ExternalConfigManager.KEY_MYBATIS_SQLMAPPER_PKG, tfSqlMapperPkg.getText()); - - // 字体大小 props.setProperty(ExternalConfigManager.KEY_FONT_SECTION_TITLE, String.valueOf(fontSectionTitle)); props.setProperty(ExternalConfigManager.KEY_FONT_FIELD_LABEL, String.valueOf(fontFieldLabel)); props.setProperty(ExternalConfigManager.KEY_FONT_BUTTON, String.valueOf(fontButton)); props.setProperty(ExternalConfigManager.KEY_FONT_BODY, String.valueOf(fontBody)); props.setProperty(ExternalConfigManager.KEY_FONT_MONO, String.valueOf(fontMono)); - if (props.isEmpty()) { - showAlert(AlertType.WARNING, "没有需要保存的配置项,请至少填写一项。"); - return; - } - try { configManager.saveSectioned(props); - properties.mergeExternal(props); + Properties external = configManager.load(); + properties.mergeExternal(external); if (onConfigChanged != null) onConfigChanged.run(); - showAlert(AlertType.INFORMATION, "配置已保存到:\n" + configManager.getConfigFile().toAbsolutePath() + showAlert(AlertType.INFORMATION, "字体设置已保存到:\n" + configManager.getConfigFile().toAbsolutePath() + "\n\n修改已生效,无需重启。"); } catch (Exception ex) { showAlert(AlertType.ERROR, "保存失败: " + ex.getMessage()); } } - private void reloadConfig() { - if (!configManager.exists()) { - showAlert(AlertType.INFORMATION, "外部配置文件不存在,当前使用 application.yml 默认值。"); - refreshAllFields(); - return; - } - Properties external = configManager.load(); - properties.mergeExternal(external); - if (onConfigChanged != null) onConfigChanged.run(); - refreshAllFields(); - showAlert(AlertType.INFORMATION, "已从 workutils.config 重新加载配置。"); - } + // ==================== 项目路径替换工具 ==================== - private void resetConfig() { - Alert confirm = new Alert(AlertType.CONFIRMATION); - confirm.setTitle("确认重置"); - confirm.setHeaderText("重置为 application.yml 默认值?"); - confirm.setContentText("这会丢弃 workutils.config 中的所有自定义配置。"); - confirm.showAndWait().ifPresent(r -> { - if (r == ButtonType.OK) { - properties.resetToDefaults(); - configManager.delete(); - if (onConfigChanged != null) onConfigChanged.run(); - refreshAllFields(); - showAlert(AlertType.INFORMATION, "已重置为默认值,外部配置文件已删除。"); - } - }); - } - - // ==================== 字段刷新 ==================== - - private void refreshAllFields() { - tfBaseDir.setText(nvl(properties.getBaseDirectory())); - tfExportDir.setText(nvl(properties.getExportDirectory())); - tfProjectPathsFile.setText(properties.getProjectPathsFilePath().toAbsolutePath().toString()); - tfDamagedReposFile.setText(properties.getDamagedReposFilePath().toAbsolutePath().toString()); - refreshMyBatisFields(); - refreshProjectTab(); - lblConfigFile.setText("配置文件: " + configManager.getConfigFile().toAbsolutePath() - + (configManager.exists() ? " (已存在)" : " (不存在)")); - } - - private void refreshMyBatisFields() { - Properties builtin = loadBuiltinMyBatisProps(); - tfDbDriver.setText(extOr(ExternalConfigManager.KEY_MYBATIS_DRIVER, builtin, "db_driver")); - tfDbUrl.setText(extOr(ExternalConfigManager.KEY_MYBATIS_URL, builtin, "db_url")); - tfDbUsername.setText(extOr(ExternalConfigManager.KEY_MYBATIS_USERNAME, builtin, "db_username")); - pfDbPassword.setText(properties.getMybatisOverrides().getOrDefault( - ExternalConfigManager.KEY_MYBATIS_PASSWORD, "")); - - tfModelPkg.setText(extOr(ExternalConfigManager.KEY_MYBATIS_MODEL_PKG, builtin, "model_package")); - tfDaoPkg.setText(extOr(ExternalConfigManager.KEY_MYBATIS_DAO_PKG, builtin, "dao_package")); - tfSqlMapperPkg.setText(extOr(ExternalConfigManager.KEY_MYBATIS_SQLMAPPER_PKG, builtin, "sqlmapper_package")); - } - - private String extOr(String extKey, Properties builtin, String builtinKey) { - String ext = properties.getMybatisOverrides().get(extKey); - if (ext != null && !ext.isEmpty()) return ext; - return builtin.getProperty(builtinKey, ""); - } - - private void refreshProjectTab() { - Path ppFile = properties.getProjectPathsFilePath(); - lblProjectPathsFile.setText("文件: " + ppFile.toAbsolutePath() - + (Files.exists(ppFile) ? " (存在)" : " (不存在)")); - loadProjectPaths(); - - Path dcFile = properties.getDamagedReposFilePath(); - lblDamagedCacheFile.setText("文件: " + dcFile.toAbsolutePath() - + (Files.exists(dcFile) ? " (存在)" : " (不存在)")); - loadDamagedCache(); - } - - // --- 从内置配置加载 --- - - private void loadMyBatisFromClasspath() { - Properties builtin = loadBuiltinMyBatisProps(); - if (builtin.isEmpty()) { - showAlert(AlertType.WARNING, "未找到 mybatis-generator.properties 配置文件。" - + "\n请检查 data/mybatis-generator.properties 是否存在。"); - return; - } - tfDbDriver.setText(builtin.getProperty("db_driver", "")); - tfDbUrl.setText(builtin.getProperty("db_url", "")); - tfDbUsername.setText(builtin.getProperty("db_username", "")); - pfDbPassword.setText(builtin.getProperty("db_password", "")); - tfModelPkg.setText(builtin.getProperty("model_package", "")); - tfDaoPkg.setText(builtin.getProperty("dao_package", "")); - tfSqlMapperPkg.setText(builtin.getProperty("sqlmapper_package", "")); - showAlert(AlertType.INFORMATION, "已从内置配置加载。点击「保存到 workutils.config」可导出。"); - } - - private Properties loadBuiltinMyBatisProps() { - Properties props = new Properties(); - // 1. 先从 classpath 加载内置默认值 - try (InputStream is = getClass().getClassLoader() - .getResourceAsStream("mybatis-generator.properties")) { - if (is != null) { - props.load(is); - } - } catch (IOException ignored) { - } - // 2. 再用 data/ 外部配置覆盖 - Path dataFile = properties.getMybatisGeneratorFilePath(); - if (Files.exists(dataFile)) { - try (InputStream is = new FileInputStream(dataFile.toFile())) { - Properties externalProps = new Properties(); - externalProps.load(is); - props.putAll(externalProps); - } catch (IOException ignored) { - } - } - return props; - } - - // --- 项目路径 --- - - private void loadProjectPaths() { + /** 从文件加载项目路径到预览 TextArea */ + private void loadProjectPathsToPreview() { + if (taProjectPathsPreview == null) return; Path file = properties.getProjectPathsFilePath(); if (Files.exists(file)) { try { - taProjectPaths.setText(String.join("\n", Files.readAllLines(file))); + taProjectPathsPreview.setText(String.join("\n", Files.readAllLines(file))); } catch (IOException e) { - taProjectPaths.setText("# 读取失败: " + e.getMessage()); + taProjectPathsPreview.setText("# 读取失败: " + e.getMessage()); } } else { - taProjectPaths.setText("# 文件不存在\n# 格式: 项目路径\n# 格式: 项目路径=BASE_URL"); + taProjectPathsPreview.setText("# 文件不存在\n# 格式: 项目路径\n# 格式: 项目路径=BASE_URL"); } } - private void saveProjectPaths() { + private void reloadProjectPathsPreview() { + if (taProjectPathsPreview != null) { + loadProjectPathsToPreview(); + } + } + + /** 保存替换后的内容到文件 */ + private void saveProjectPathsFromPreview() { Path file = properties.getProjectPathsFilePath(); try { Files.createDirectories(file.getParent()); - Files.write(file, taProjectPaths.getText().getBytes("UTF-8")); - showAlert(AlertType.INFORMATION, "项目路径文件已保存。"); + Files.write(file, taProjectPathsPreview.getText().getBytes("UTF-8")); } catch (IOException e) { showAlert(AlertType.ERROR, "保存失败: " + e.getMessage()); } } - // --- 损坏缓存 --- - - private void loadDamagedCache() { - Path file = properties.getDamagedReposFilePath(); - if (Files.exists(file)) { - try { - List lines = Files.readAllLines(file); - taDamagedCache.setText(lines.isEmpty() ? "(空)" : String.join("\n", lines)); - } catch (IOException e) { - taDamagedCache.setText("读取失败: " + e.getMessage()); - } - } else { - taDamagedCache.setText("(文件不存在,尚未产生损坏记录)"); - } - } - - private void clearDamagedCache() { - Alert confirm = new Alert(AlertType.CONFIRMATION); - confirm.setTitle("确认清理"); - confirm.setHeaderText("清除损坏仓库缓存?"); - confirm.setContentText("缓存清除后,下次同步时将重新扫描所有仓库。"); - confirm.showAndWait().ifPresent(r -> { - if (r == ButtonType.OK) { - try { - Files.deleteIfExists(properties.getDamagedReposFilePath()); - taDamagedCache.setText("(已清理)"); - showAlert(AlertType.INFORMATION, "损坏仓库缓存已清理。"); - } catch (IOException e) { - showAlert(AlertType.ERROR, "清理失败: " + e.getMessage()); - } - } - }); - } - - // ==================== 工具方法 ==================== - - private Font font(int size) { - return Font.font(size); - } - - /** 添加节标题 Label,自动加入 titleLabels 列表 */ - private Label addTitleLabel(GridPane grid, int row, String text, int colSpan) { - Label lbl = new Label(text); - lbl.setStyle("-fx-font-weight: bold; -fx-font-size: " + fontSectionTitle + "px; -fx-padding: 0 0 8 0;"); - titleLabels.add(lbl); - grid.add(lbl, 0, row, colSpan, 1); - return lbl; - } - - /** 添加小按钮,自动加入 allButtons 列表 */ - private Button addSmallButton(String text) { - Button btn = new Button(text); - btn.setFont(font(fontButton)); - allButtons.add(btn); - return btn; - } - - private TextField addBrowseRow(GridPane grid, int row, String label, String initialValue) { - TextField tf = new TextField(initialValue != null ? initialValue : ""); - HBox.setHgrow(tf, Priority.ALWAYS); - GridPane.setHgrow(tf, Priority.ALWAYS); - textInputs.add(tf); - - Label lbl = new Label(label); - lbl.setFont(font(fontFieldLabel)); - fieldLabels.add(lbl); - - Button btn = new Button("浏览..."); - btn.setFont(font(fontButton)); - allButtons.add(btn); - btn.setOnAction(e -> browseDir(tf)); - - grid.add(lbl, 0, row); - grid.add(tf, 1, row); - grid.add(btn, 2, row); - return tf; - } - - private TextField addField(GridPane grid, int row, String label, String initialValue) { - TextField tf = new TextField(initialValue); - HBox.setHgrow(tf, Priority.ALWAYS); - GridPane.setHgrow(tf, Priority.ALWAYS); - textInputs.add(tf); - addLabeled(grid, row, label, tf); - return tf; - } - - private void addLabeled(GridPane grid, int row, String label, Control field) { - Label lbl = new Label(label); - lbl.setFont(font(fontFieldLabel)); - fieldLabels.add(lbl); - grid.add(lbl, 0, row); - grid.add(field, 1, row); - } - - private void browseDir(TextField target) { - DirectoryChooser chooser = new DirectoryChooser(); - chooser.setTitle("选择目录"); - String current = target.getText(); - if (current != null && !current.isEmpty()) { - File f = new File(current); - if (f.exists()) { - if (f.isFile()) f = f.getParentFile(); - chooser.setInitialDirectory(f); - } - } - File selected = chooser.showDialog(stage); - if (selected != null) { - target.setText(selected.getAbsolutePath()); - } - } - - // ==================== 项目路径替换工具 ==================== - private VBox createReplaceTool() { VBox box = new VBox(8); box.setPadding(new Insets(8, 0, 0, 0)); @@ -786,7 +595,25 @@ public class SettingsWindow { btnApplyAccount.setOnAction(e -> applyReplaceAccount()); accountRow.getChildren().addAll(lblAccount, tfReplaceAccount, btnDetectAccount, btnApplyAccount); - box.getChildren().addAll(parentRow, accountRow); + // 预览区 + taProjectPathsPreview = new TextArea(); + taProjectPathsPreview.setPrefRowCount(6); + taProjectPathsPreview.setStyle("-fx-font-size: " + fontMono + "px;"); + taProjectPathsPreview.setPromptText("# 每行一个项目\n# 格式: 项目路径\n# 格式: 项目路径=BASE_URL\n# # 开头为注释"); + textInputs.add(taProjectPathsPreview); + + HBox previewBtns = new HBox(10); + Button btnLoadFile = addSmallButton("从文件加载"); + btnLoadFile.setOnAction(e -> loadProjectPathsToPreview()); + Button btnSaveFile = addSmallButton("保存到文件"); + btnSaveFile.setOnAction(e -> { + saveProjectPathsFromPreview(); + showAlert(AlertType.INFORMATION, "项目路径文件已保存。"); + }); + previewBtns.getChildren().addAll(btnLoadFile, btnSaveFile); + + box.getChildren().addAll(parentRow, accountRow, new Separator(), taProjectPathsPreview, previewBtns); + loadProjectPathsToPreview(); return box; } @@ -797,13 +624,11 @@ public class SettingsWindow { showAlert(AlertType.WARNING, "项目路径为空,无法检测父目录。"); return; } - // 取 / 分割后的第一个非空部分作为父目录,如 D:/bestpay-srdcloud/ft-xxx 中的 D:/bestpay-srdcloud int slashIdx = firstPath.indexOf('/'); if (slashIdx < 0) { tfReplaceParentDir.setText(firstPath); return; } - // 找到第一个 / 后,继续找 /ft-xxx 的 /,即项目目录前一个 / int nextSlash = firstPath.indexOf('/', slashIdx + 1); if (nextSlash > 0) { tfReplaceParentDir.setText(firstPath.substring(0, nextSlash)); @@ -822,7 +647,6 @@ public class SettingsWindow { if (f.exists()) { chooser.setInitialDirectory(f); } else { - // 尝试取已存在的上级目录 File parent = f.getParentFile(); while (parent != null && !parent.exists()) { parent = parent.getParentFile(); @@ -845,11 +669,9 @@ public class SettingsWindow { showAlert(AlertType.WARNING, "未找到带远程 URL 的项目路径,请确保路径格式为 path=URL。"); return; } - // 格式: path=https://account@code.srdcloud.cn/... int eqIdx = firstLine.indexOf('='); if (eqIdx < 0) return; String url = firstLine.substring(eqIdx + 1).trim(); - // 提取 @ 前面的账号:https://xxx → 取 // 和 @ 之间的部分 int atIdx = url.indexOf('@'); int protoIdx = url.indexOf("://"); if (atIdx > 0 && protoIdx > 0) { @@ -865,7 +687,6 @@ public class SettingsWindow { showAlert(AlertType.WARNING, "请输入新的父目录。"); return; } - // 检测当前父目录 String firstPath = findFirstProjectPath(); if (firstPath == null) { showAlert(AlertType.WARNING, "项目路径为空。"); @@ -879,7 +700,6 @@ public class SettingsWindow { int nextSlash = firstPath.indexOf('/', slashIdx + 1); String oldParent = (nextSlash > 0) ? firstPath.substring(0, nextSlash) : firstPath; - // 规范化:去掉末尾 / if (newParent.endsWith("/") || newParent.endsWith("\\")) { newParent = newParent.substring(0, newParent.length() - 1); } @@ -887,7 +707,7 @@ public class SettingsWindow { oldParent = oldParent.substring(0, oldParent.length() - 1); } - int count = replaceInTextArea(oldParent, newParent); + int count = replaceInPreviewTextArea(oldParent, newParent); showAlert(AlertType.INFORMATION, "已将 " + count + " 条路径中的父目录从\n" + oldParent + "\n替换为\n" + newParent); } @@ -899,7 +719,6 @@ public class SettingsWindow { showAlert(AlertType.WARNING, "请输入新的账号。"); return; } - // 检测当前账号 String firstLine = findFirstLineWithUrl(); if (firstLine == null) { showAlert(AlertType.WARNING, "未找到带远程 URL 的项目路径。"); @@ -916,14 +735,14 @@ public class SettingsWindow { } String oldAccount = url.substring(protoIdx + 3, atIdx); - int count = replaceInTextArea(oldAccount, newAccount); + int count = replaceInPreviewTextArea(oldAccount, newAccount); showAlert(AlertType.INFORMATION, "已将 " + count + " 条URL中的账号从\n" + oldAccount + "\n替换为\n" + newAccount); } - /** 在 TextArea 中替换文本,返回替换行数 */ - private int replaceInTextArea(String oldStr, String newStr) { - String text = taProjectPaths.getText(); + /** 在预览 TextArea 中替换文本,返回替换行数 */ + private int replaceInPreviewTextArea(String oldStr, String newStr) { + String text = taProjectPathsPreview.getText(); int count = 0; String[] lines = text.split("\n"); StringBuilder sb = new StringBuilder(); @@ -935,13 +754,13 @@ public class SettingsWindow { sb.append(line).append("\n"); } } - taProjectPaths.setText(sb.toString()); + taProjectPathsPreview.setText(sb.toString()); return count; } /** 找到第一条有效项目路径(只取路径部分,不含 URL) */ private String findFirstProjectPath() { - String text = taProjectPaths.getText(); + String text = taProjectPathsPreview.getText(); if (text == null || text.trim().isEmpty()) return null; for (String line : text.split("\n")) { line = line.trim(); @@ -957,7 +776,7 @@ public class SettingsWindow { /** 找到第一条同时包含路径和 URL 的行 */ private String findFirstLineWithUrl() { - String text = taProjectPaths.getText(); + String text = taProjectPathsPreview.getText(); if (text == null || text.trim().isEmpty()) return null; for (String line : text.split("\n")) { line = line.trim(); @@ -971,22 +790,110 @@ public class SettingsWindow { return null; } - private void openInNotepad(Path file) { + // ==================== 打开文件编辑器 ==================== + + /** + * 用系统默认编辑器打开文件 + * Windows 优先使用 Desktop.edit(),fallback 到 notepad.exe + */ + private void openFileInEditor(Path file) { try { if (!Files.exists(file)) { Files.createDirectories(file.getParent()); Files.createFile(file); + log.info("自动创建文件: {}", file.toAbsolutePath()); + } + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.EDIT)) { + Desktop.getDesktop().edit(file.toFile()); + } else { + // Windows fallback + Runtime.getRuntime().exec(new String[]{"notepad.exe", file.toAbsolutePath().toString()}); } - Runtime.getRuntime().exec(new String[]{"notepad.exe", file.toAbsolutePath().toString()}); } catch (IOException e) { - showAlert(AlertType.ERROR, "无法打开记事本: " + e.getMessage()); + log.error("打开文件失败: {}", file.toAbsolutePath(), e); + showAlert(AlertType.ERROR, "打开文件失败: " + e.getMessage()); } } - private void putIfNotEmpty(Properties props, String key, String value) { - if (value != null && !value.trim().isEmpty()) { - props.setProperty(key, value.trim()); + // ==================== 字体相关 ==================== + + private HBox createFontRow(String description, int initialValue, + java.util.function.IntConsumer onChanged) { + HBox row = new HBox(12); + row.setAlignment(Pos.CENTER_LEFT); + row.setPadding(new Insets(6, 0, 6, 0)); + + Label nameLabel = new Label(description); + nameLabel.setPrefWidth(320); + nameLabel.setFont(font(14)); + + Slider slider = new Slider(16, 32, initialValue); + slider.setPrefWidth(180); + slider.setMajorTickUnit(2); + slider.setMinorTickCount(1); + slider.setShowTickMarks(true); + slider.setShowTickLabels(false); + slider.setSnapToTicks(true); + + Label valLabel = new Label(String.valueOf(initialValue) + "px"); + valLabel.setPrefWidth(40); + valLabel.setFont(font(14)); + valLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #4a90d9;"); + + Label preview = new Label("预览 Abc 123"); + preview.setFont(font(initialValue)); + if (description.contains("节标题")) { + preview.setStyle("-fx-font-weight: bold;"); + titleLabels.add(preview); + } else if (description.contains("提示")) { + preview.setStyle("-fx-text-fill: #666;"); + hintLabels.add(preview); + } else if (description.contains("等宽")) { + preview.setFont(Font.font("Consolas", initialValue)); } + + slider.valueProperty().addListener((obs, oldVal, newVal) -> { + int v = (int) Math.round(newVal.doubleValue()); + if (slider.isValueChanging() || Math.abs(newVal.doubleValue() - oldVal.doubleValue()) > 0.1) { + valLabel.setText(v + "px"); + preview.setFont(description.contains("等宽") + ? Font.font("Consolas", v) + : font(v)); + onChanged.accept(v); + } + }); + + row.getChildren().addAll(nameLabel, slider, valLabel, preview); + return row; + } + + /** 将所有字体大小重新应用到已创建的 UI 组件上 */ + private void applyAllFontSizes() { + for (Label lbl : titleLabels) { + lbl.setFont(font(fontSectionTitle)); + } + for (Label lbl : fieldLabels) { + lbl.setFont(font(fontFieldLabel)); + } + for (ButtonBase btn : allButtons) { + btn.setFont(font(fontButton)); + } + for (Label lbl : hintLabels) { + lbl.setFont(font(fontBody)); + } + } + + // ==================== 工具方法 ==================== + + private Font font(int size) { + return Font.font(size); + } + + private Button addSmallButton(String text) { + Button btn = new Button(text); + btn.setFont(font(fontButton)); + allButtons.add(btn); + return btn; } private void showAlert(AlertType type, String msg) { @@ -996,8 +903,4 @@ public class SettingsWindow { alert.setContentText(msg); alert.showAndWait(); } - - private static String nvl(String s) { - return s == null ? "" : s; - } } diff --git a/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java b/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java index 7ec9123..1ae9699 100644 --- a/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java +++ b/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java @@ -69,11 +69,11 @@ public class WorkUtilsProperties { private final Map mybatisOverrides = new LinkedHashMap<>(); // ---- 字体大小(显示设置) ---- - private int fontSectionTitle = 20; // 节标题 - private int fontFieldLabel = 18; // 字段标签 - private int fontButton = 18; // 按钮 - private int fontBody = 18; // 正文/提示 - private int fontMono = 18; // 等宽/代码 + private int fontSectionTitle = 24; // 节标题 + private int fontFieldLabel = 20; // 字段标签 + private int fontButton = 20; // 按钮 + private int fontBody = 20; // 正文/提示 + private int fontMono = 20; // 等宽/代码 // ==================== 外部配置加载 ==================== @@ -167,11 +167,11 @@ public class WorkUtilsProperties { projectPathsFile = defaults.get(ExternalConfigManager.KEY_PROJECT_PATHS); damagedReposFile = defaults.get(ExternalConfigManager.KEY_DAMAGED_REPOS); mybatisOverrides.clear(); - fontSectionTitle = 20; - fontFieldLabel = 18; - fontButton = 18; - fontBody = 18; - fontMono = 18; + fontSectionTitle = 24; + fontFieldLabel = 20; + fontButton = 20; + fontBody = 20; + fontMono = 20; log.info("配置已重置为默认值"); } diff --git a/src/main/java/com/example/workutils/system/plugin/PluginManager.java b/src/main/java/com/example/workutils/system/plugin/PluginManager.java index e3bc97a..bce8e7a 100644 --- a/src/main/java/com/example/workutils/system/plugin/PluginManager.java +++ b/src/main/java/com/example/workutils/system/plugin/PluginManager.java @@ -106,8 +106,8 @@ public class PluginManager { break; } - // 'S' / 's' 字符(readChoiceOrChar 将单字母转成负ASCII码) - if (choice == -'S') { + // '?' 字符(readChoiceOrChar 将单字母转成负ASCII码) + if (choice == -'?') { settingsService.showSettings(scanner, pluginContext::resetConfigChecks); // JavaFX 后台线程在 Windows 上可能污染 System.in, // 清理残留字节并重建 Scanner,确保后续输入正常 @@ -126,7 +126,7 @@ public class PluginManager { PluginDescriptor desc = enabledPlugins.get(i).getDescriptor(); System.out.println((i + 1) + ". " + desc.getName() + " [v" + desc.getVersion() + "]"); } - System.out.println("S. 系统设置"); + System.out.println("?. 系统设置"); System.out.println(EXIT_CODE + ". 退出系统"); } @@ -145,7 +145,7 @@ public class PluginManager { System.out.println("插件 [" + desc.getName() + "] 配置异常:"); System.out.println(configError); System.out.println(); - System.out.println("请进入「系统设置(S)」进行配置后再使用。"); + System.out.println("请进入「系统设置(?)」进行配置后再使用。"); System.out.println("=============================="); return; } diff --git a/src/main/resources/templates/datasources.json b/src/main/resources/templates/datasources.json index 89299b8..4118ec7 100644 --- a/src/main/resources/templates/datasources.json +++ b/src/main/resources/templates/datasources.json @@ -1,23 +1,23 @@ [ { - "name": "开发环境", + "name": "本地环境-loacalhost-db", "driver": "com.mysql.jdbc.Driver", "url": "jdbc:mysql://localhost:3306/disburse?useUnicode=true&characterEncoding=utf8&useSSL=false", "username": "root", "password": "1Qaz@4772" }, { - "name": "SIT环境", - "driver": "com.mysql.jdbc.Driver", - "url": "jdbc:mysql://sit-server:3306/disburse?useUnicode=true&characterEncoding=utf8&useSSL=false", - "username": "sit_user", - "password": "" + "name": "ft_loancapital_core_db", + "driver": "com.oceanbase.jdbc.Driver", + "url": "jdbc:oceanbase://ob4x-xyd.bestpay.dev-db:2883/ft_loancapital_core_db?rewriteBatchedStatements=true&allowMultiQueries=true&useLocalSessionState=true&useUnicode=true&characterEncoding=utf-8&socketTimeout=6000000", + "username": "ft_loancapital_core_app@xydcs_gcz_tnt#ob4x_cluster_xyd", + "password": "FT_LOANCAPITAL_CORE@123" }, { - "name": "生产环境(只读)", - "driver": "com.mysql.jdbc.Driver", - "url": "jdbc:mysql://prod-server:3306/disburse?useUnicode=true&characterEncoding=utf8&useSSL=false", - "username": "readonly", - "password": "" + "name": "cf_loan_core_db", + "driver": "com.oceanbase.jdbc.Driver", + "url": "jdbc:oceanbase://ob4x-ky.bestpay.46-db:2883/cf_loan_core_db?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai&socketTimeout=30000&connectTimeout=10000", + "username": "cf_loan_core_app@obtest_tnt#ob4x_cluster_test", + "password": "CF_LOAN_CORE@123" } ]