diff --git a/pom.xml b/pom.xml
index 86ec9ef..cd3dd61 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
-Dfile.encoding=UTF-8
- 17.0.14
+ 17.0.15
2.0.1
diff --git a/scripts/build_release.bat b/scripts/build_release.bat
index 77a9cca..eadcf91 100644
--- a/scripts/build_release.bat
+++ b/scripts/build_release.bat
@@ -110,6 +110,16 @@ if %errorlevel% neq 0 (
pause
exit /b 1
)
+
+::: 4.5 补充 JavaFX 原生 DLL(jlink 不会自动从 SDK bin/ 复制)
+echo Copy JavaFX native DLLs...
+if exist "%JFX_SDK_HOME%\bin\glass.dll" (
+ xcopy /y /q "%JFX_SDK_HOME%\bin\*.dll" "%TARGET%\jre\bin\" >nul
+ echo [OK] JavaFX DLLs copied
+) else (
+ echo [WARN] No JavaFX DLLs found in %JFX_SDK_HOME%\bin
+)
+
echo [OK] JRE generated : %TARGET%\jre
::: ============================================================
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 2b204e6..a64bbab 100644
--- a/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java
+++ b/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java
@@ -70,6 +70,8 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
private List lastDamagedRepos;
/** 缓存上一次使用的目标分支 */
private String lastBranch;
+ /** 同步取消标记(供 GUI 终止按钮使用) */
+ private volatile boolean syncCancelled = false;
// ==================== 启动时从日志恢复缓存 ====================
@@ -257,11 +259,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
Map baseUrlMap = new LinkedHashMap<>();
for (FileOperationHelper.ProjectPathInfo proj : projects) {
if (proj.getRemoteBaseUrl() != null && !proj.getRemoteBaseUrl().isEmpty()) {
- String url = proj.getRemoteBaseUrl();
- if (!url.endsWith("/")) {
- url = url + "/";
- }
- baseUrlMap.put(normalizePath(proj.getPath()), url);
+ baseUrlMap.put(normalizePath(proj.getPath()), proj.getRemoteBaseUrl());
}
}
log.info("从项目路径文件加载了 {} 条远程URL映射", baseUrlMap.size());
@@ -290,7 +288,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
int done = completed.incrementAndGet();
String tag;
switch (result) {
- case OK: tag = "成功"; break;
+ case OK: tag = result.cloned ? "拉取" : "成功"; break;
case DAMAGED: tag = "损坏"; break;
default: tag = "失败"; break;
}
@@ -485,11 +483,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
List projects = fileOperationHelper.getProjectPathsWithStatus();
for (FileOperationHelper.ProjectPathInfo proj : projects) {
if (proj.getRemoteBaseUrl() != null && !proj.getRemoteBaseUrl().isEmpty()) {
- String url = proj.getRemoteBaseUrl();
- if (!url.endsWith("/")) {
- url = url + "/";
- }
- map.put(normalizePath(proj.getPath()), url);
+ map.put(normalizePath(proj.getPath()), proj.getRemoteBaseUrl());
}
}
} catch (IOException e) {
@@ -498,15 +492,14 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
return map;
}
- /** 解析 clone URL:优先从项目路径文件的 BASE_URL 拼接,其次用缓存的 remoteUrl */
+ /** 解析 clone URL:BASE_URL 本身就是完整的 clone 地址,直接返回 */
private String resolveCloneUrl(DamagedRepo dr, Map baseUrlMap) {
- // 1. 从项目路径文件的 BASE_URL 拼接(项目路径文件为准)
+ // 1. 从项目路径文件的 BASE_URL(已是完整 clone URL,如 https://user@host/path/repo.git)
if (baseUrlMap != null) {
String normalizedPath = normalizePath(dr.path);
String baseUrl = baseUrlMap.get(normalizedPath);
if (baseUrl != null) {
- String repoName = extractName(dr.path);
- return baseUrl + repoName;
+ return baseUrl;
}
}
// 2. 兜底:缓存的 remoteUrl
@@ -520,9 +513,31 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
private SyncResult syncOneRepo(String repoPath, String branch, Map baseUrlMap) {
File repoDir = new File(repoPath);
- if (!repoDir.exists() || !new File(repoDir, ".git").exists()) {
+
+ // 目录不存在 或 空目录 → 自动 clone
+ if (!repoDir.exists() || (repoDir.isDirectory() && isEmptyDir(repoDir))) {
+ String reason = !repoDir.exists() ? "目录不存在" : "目录为空";
+ log.info("{},自动 clone: {}", reason, repoPath);
+ String cloneUrl = resolveSimpleCloneUrl(repoPath, baseUrlMap);
+ if (cloneUrl != null) {
+ if (doClone(repoPath, branch, cloneUrl)) {
+ SyncResult r = SyncResult.OK;
+ r.output = "auto-cloned";
+ r.cloned = true;
+ return r;
+ }
+ log.warn("自动 clone 失败: {} -> {}", repoPath, "exit code != 0");
+ }
SyncResult r = SyncResult.DAMAGED;
- r.error = "目录不存在或无 .git";
+ r.error = reason + ",自动 clone 失败(远程URL: " + (cloneUrl != null ? cloneUrl : "未知") + ")";
+ r.remoteUrl = cloneUrl;
+ return r;
+ }
+
+ // 目录存在但无 .git → 仓库损坏
+ if (!new File(repoDir, ".git").exists()) {
+ SyncResult r = SyncResult.DAMAGED;
+ r.error = "目录存在但无 .git(仓库损坏)";
r.remoteUrl = getRemoteUrl(repoPath, baseUrlMap);
return r;
}
@@ -583,40 +598,49 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
return result;
}
- // ==================== 重拉(clone)逻辑 ====================
+ // ==================== clone 逻辑 ====================
- private boolean doReclone(DamagedRepo dr, String cloneUrl) {
- String repoPath = dr.path;
- String branch = dr.branch;
-
- // 删除旧目录
+ /** 执行 git clone(删除旧目录后克隆),供 auto-clone 和手动重拉共用 */
+ private boolean doClone(String repoPath, String branch, String cloneUrl) {
File dir = new File(repoPath);
+ // 确保父目录存在
+ if (dir.getParentFile() != null && !dir.getParentFile().exists()) {
+ dir.getParentFile().mkdirs();
+ }
+ // 删除旧目录(如果存在)
if (dir.exists()) {
try {
deleteRecursive(dir.toPath());
log.info("已删除仓库: {}", repoPath);
- System.out.println(" 已删除旧目录");
} catch (IOException e) {
- System.err.println(" 删除失败: " + e.getMessage());
+ log.error("删除失败: {}", repoPath, e);
return false;
}
}
-
// clone
- String parentDir = dir.getParent();
- System.out.println(" clone URL: " + cloneUrl);
+ String parentDir = dir.getParent() != null ? dir.getParent() : ".";
CmdResult cr = runGit(parentDir,
"git clone -b " + branch + " " + cloneUrl + " \"" + repoPath + "\"");
-
if (cr.ok) {
- System.out.println(" clone 成功");
return true;
} else {
- System.err.println(" clone 失败: " + cr.error);
+ log.error("clone 失败: {} -> {}", cloneUrl, cr.error);
return false;
}
}
+ /** 手动重拉(供 DamagedRepo 场景使用) */
+ private boolean doReclone(DamagedRepo dr, String cloneUrl) {
+ System.out.println(" clone URL: " + cloneUrl);
+ boolean ok = doClone(dr.path, dr.branch, cloneUrl);
+ if (ok) {
+ System.out.println(" clone 成功");
+ } else {
+ System.err.println(" clone 失败");
+ }
+ return ok;
+ }
+
// ==================== Git 命令执行 ====================
/**
@@ -721,16 +745,13 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
} catch (IOException ignored) {
}
}
- // 3. 兜底: 从项目路径文件中的 BASE_URL 拼接完整 clone URL
- // 格式: 项目路径文件 key=仓库路径, value=BASE_URL; clone URL = BASE_URL + cf/ + 仓库名
+ // 3. 兜底: 项目路径文件中的 BASE_URL 即完整的 clone URL
if (baseUrlMap != null) {
String normalizedPath = normalizePath(repoPath);
String baseUrl = baseUrlMap.get(normalizedPath);
if (baseUrl != null) {
- String repoName = extractName(repoPath);
- String fullUrl = baseUrl + repoName;
- log.info("从项目路径文件拼接远程URL: {} -> {}", repoPath, fullUrl);
- return fullUrl;
+ log.info("从项目路径文件获取远程URL: {} -> {}", repoPath, baseUrl);
+ return baseUrl;
}
}
return null;
@@ -766,6 +787,19 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
return idx >= 0 ? path.substring(idx + 1) : path;
}
+ /** 判断目录是否为空 */
+ private boolean isEmptyDir(File dir) {
+ if (!dir.isDirectory()) return false;
+ String[] files = dir.list();
+ return files != null && files.length == 0;
+ }
+
+ /** 从 baseUrlMap 获取 clone URL(不拼仓库名,BASE_URL 即完整 clone URL) */
+ private String resolveSimpleCloneUrl(String repoPath, Map baseUrlMap) {
+ if (baseUrlMap == null) return null;
+ return baseUrlMap.get(normalizePath(repoPath));
+ }
+
/** 路径规范化:统一用 / 分隔、小写,用于映射表匹配 */
private String normalizePath(String path) {
if (path == null || path.isEmpty()) return "";
@@ -801,6 +835,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
* 以回调方式同步所有项目(供可视窗口调用)
*/
public void syncWithCallback(String branch, SyncCallback cb) {
+ syncCancelled = false;
List projects;
try {
projects = fileOperationHelper.getProjectPathsWithStatus();
@@ -814,12 +849,12 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
return;
}
+ if (syncCancelled) { cb.onLog("[INFO] 同步已取消"); return; }
+
Map baseUrlMap = new LinkedHashMap<>();
for (FileOperationHelper.ProjectPathInfo proj : projects) {
if (proj.getRemoteBaseUrl() != null && !proj.getRemoteBaseUrl().isEmpty()) {
- String url = proj.getRemoteBaseUrl();
- if (!url.endsWith("/")) url = url + "/";
- baseUrlMap.put(normalizePath(proj.getPath()), url);
+ baseUrlMap.put(normalizePath(proj.getPath()), proj.getRemoteBaseUrl());
}
}
@@ -835,7 +870,13 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
cb.onLog("并行线程: " + PARALLEL);
cb.onLog("开始同步...\n");
+ if (syncCancelled) { cb.onLog("[INFO] 同步已取消"); pool.shutdownNow(); return; }
+
for (int i = 0; i < total; i++) {
+ if (syncCancelled) {
+ // 不再提交新任务,但已提交的会在 latch 中计完
+ break;
+ }
final FileOperationHelper.ProjectPathInfo proj = projects.get(i);
final String repoPath = proj.getPath();
final String repoName = extractName(repoPath);
@@ -850,7 +891,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
int done = completed.incrementAndGet();
String tag;
switch (result) {
- case OK: tag = "成功"; break;
+ case OK: tag = result.cloned ? "拉取" : "成功"; break;
case DAMAGED: tag = "损坏"; break;
default: tag = "失败"; break;
}
@@ -869,10 +910,20 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
pool.shutdown();
try {
- boolean allDone = latch.await(SYNC_TOTAL_TIMEOUT_MINUTES, TimeUnit.MINUTES);
- if (!allDone) {
- pool.shutdownNow();
- cb.onLog("\n[ERROR] 同步超时(超过 " + SYNC_TOTAL_TIMEOUT_MINUTES + " 分钟),强制终止");
+ // 缩短每次 await 时间,以便及时响应取消
+ long checkIntervalMs = 500;
+ long deadline = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(SYNC_TOTAL_TIMEOUT_MINUTES);
+ while (!latch.await(checkIntervalMs, TimeUnit.MILLISECONDS)) {
+ if (syncCancelled) {
+ cb.onLog("\n[WARN] 用户取消了同步,正在终止...");
+ pool.shutdownNow();
+ break;
+ }
+ if (System.currentTimeMillis() > deadline) {
+ pool.shutdownNow();
+ cb.onLog("\n[ERROR] 同步超时(超过 " + SYNC_TOTAL_TIMEOUT_MINUTES + " 分钟),强制终止");
+ break;
+ }
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
@@ -881,6 +932,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
return;
}
+ // 汇总结果
int success = 0, failed = 0;
List damagedRepos = new ArrayList<>();
for (FileOperationHelper.ProjectPathInfo proj : projects) {
@@ -914,6 +966,11 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
}
}
+ /** 取消当前正在执行的同步(供 GUI 终止按钮调用) */
+ public void cancelSync() {
+ this.syncCancelled = true;
+ }
+
/**
* 获取损坏仓库的 clone URL(供窗口使用)
*/
@@ -969,6 +1026,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
String output;
String error;
String remoteUrl;
+ boolean cloned; // 标记是否是通过 auto-clone 完成的,区别于正常 fetch+checkout+pull
}
/** 损坏仓库信息 */
diff --git a/src/main/java/com/example/workutils/biz/git/GitSyncWindow.java b/src/main/java/com/example/workutils/biz/git/GitSyncWindow.java
index 1a615ff..c1bfc56 100644
--- a/src/main/java/com/example/workutils/biz/git/GitSyncWindow.java
+++ b/src/main/java/com/example/workutils/biz/git/GitSyncWindow.java
@@ -20,7 +20,6 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicBoolean;
/**
* Git 仓库同步可视化窗口
@@ -46,7 +45,6 @@ public class GitSyncWindow {
private ProgressBar progressBar;
private Label progressLabel;
private TextArea logArea;
- private final AtomicBoolean stopRequested = new AtomicBoolean(false);
// Tab 3: 损坏仓库处理
private TableView damagedTable;
@@ -204,7 +202,11 @@ public class GitSyncWindow {
stopBtn = StyledButton.danger("终止", Theme.base(14));
stopBtn.setDisable(true);
- stopBtn.setOnAction(e -> stopRequested.set(true));
+ stopBtn.setOnAction(e -> {
+ stopBtn.setDisable(true);
+ progressLabel.setText("正在终止...");
+ plugin.cancelSync();
+ });
branchRow.getChildren().addAll(branchLabel, branchField, syncBtn, stopBtn);
@@ -240,7 +242,6 @@ public class GitSyncWindow {
syncBtn.setDisable(true);
stopBtn.setDisable(false);
- stopRequested.set(false);
logArea.clear();
progressBar.setProgress(0);
progressLabel.setText("正在同步...");
diff --git a/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportWindow.java b/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportWindow.java
index d15effb..d88e23c 100644
--- a/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportWindow.java
+++ b/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportWindow.java
@@ -44,9 +44,16 @@ public class MyBatisExportWindow {
// 操作
private Button exportBtn;
+ private Button stopBtn;
private ProgressBar progressBar;
private Label progressLabel;
+ // 日志
+ private TextArea logArea;
+
+ // 取消标记
+ private volatile boolean exportCancelled = false;
+
// 历史记录
private TableView historyTable;
private final ObservableList historyRows = FXCollections.observableArrayList();
@@ -131,20 +138,40 @@ public class MyBatisExportWindow {
exportBtn = StyledButton.primary("导出", Theme.bold(fsBtn()));
exportBtn.setOnAction(e -> startExport());
- filterRow.getChildren().addAll(modeLabel, filterModeCmb, filterValueField, filterHint, exportBtn);
+ stopBtn = StyledButton.danger("终止", Theme.base(fsBtn()));
+ stopBtn.setDisable(true);
+ stopBtn.setOnAction(e -> {
+ stopBtn.setDisable(true);
+ progressLabel.setText("正在终止...");
+ exportCancelled = true;
+ });
+
+ filterRow.getChildren().addAll(modeLabel, filterModeCmb, filterValueField, filterHint, exportBtn, stopBtn);
// === 进度 ===
HBox progressRow = new HBox(Theme.S2);
progressRow.setAlignment(Pos.CENTER_LEFT);
progressBar = new ProgressBar(0);
- progressBar.setPrefWidth(300);
- progressBar.setPrefHeight(16);
+ progressBar.setPrefWidth(400);
+ progressBar.setPrefHeight(20);
progressBar.setVisible(false);
progressLabel = new Label("");
progressLabel.setFont(Theme.base(fsBody() - 2));
progressLabel.setTextFill(Theme.TEXT_SECONDARY);
progressRow.getChildren().addAll(progressBar, progressLabel);
+ // === 日志区域 ===
+ Label logTitle = new Label("执行日志");
+ logTitle.setFont(Theme.heading(fsBody()));
+ logTitle.setTextFill(Theme.TEXT_PRIMARY);
+ logArea = new TextArea();
+ logArea.setEditable(false);
+ logArea.setFont(Theme.mono(12));
+ logArea.getStyleClass().add(Theme.CSS_LOG_AREA);
+ logArea.setPrefHeight(150);
+ logArea.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_BODY) + ";");
+ VBox.setVgrow(logArea, Priority.NEVER);
+
// === 标题 ===
Label tableTitle = new Label("导出历史记录");
tableTitle.setFont(Theme.heading(fsSection()));
@@ -184,7 +211,7 @@ public class MyBatisExportWindow {
historyTable.getColumns().addAll(colAction, colIndex, colQuery, colFile, colCount, colTime);
VBox.setVgrow(historyTable, Priority.ALWAYS);
- root.getChildren().addAll(filterRow, progressRow, tableTitle, historyTable);
+ root.getChildren().addAll(filterRow, progressRow, logTitle, logArea, tableTitle, historyTable);
stage.setScene(new Scene(root));
}
diff --git a/src/main/java/com/example/workutils/biz/mybatisexport/service/MyBatisExportService.java b/src/main/java/com/example/workutils/biz/mybatisexport/service/MyBatisExportService.java
index 7bbf943..48cb5eb 100644
--- a/src/main/java/com/example/workutils/biz/mybatisexport/service/MyBatisExportService.java
+++ b/src/main/java/com/example/workutils/biz/mybatisexport/service/MyBatisExportService.java
@@ -106,18 +106,28 @@ public class MyBatisExportService {
}
}
+ /** 导出进度回调 */
+ public interface ExportCallback {
+ void onProjectStart(int current, int total, String projectName);
+ void onProjectDone(int current, int total, String projectName, int recordCount);
+ /** 是否已请求取消 */
+ boolean isCancelled();
+ }
+
/**
* 按过滤条件导出并返回结果(含文件路径,供窗口展示 + 保存 CSV)
*/
public ExportResult exportWithResult(String fileNamePrefix, String filterMode, String filterValue) throws Exception {
- SqlFilter filter = null;
- if ("table".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) {
- filter = new TableFilter(filterValue);
- } else if ("keyword".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) {
- filter = new KeywordFilter(filterValue);
- } else if ("tagid".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) {
- filter = new TagIdFilter(filterValue);
- }
+ return exportWithCallback(fileNamePrefix, filterMode, filterValue, null);
+ }
+
+ /**
+ * 带进度回调的导出(供可视化窗口使用)
+ * @param cb 进度回调,传 null 则降级为无回调模式
+ */
+ public ExportResult exportWithCallback(String fileNamePrefix, String filterMode, String filterValue,
+ ExportCallback cb) throws Exception {
+ SqlFilter filter = buildFilter(filterMode, filterValue);
List projectPaths = fileOperationHelper.getProjectPaths();
if (projectPaths.isEmpty()) {
@@ -125,16 +135,30 @@ public class MyBatisExportService {
return new ExportResult(new ArrayList<>(), "");
}
+ int total = projectPaths.size();
List resultList = new ArrayList<>();
+ int current = 0;
for (String projectPath : projectPaths) {
+ current++;
+ String projectName = fileOperationHelper.extractProjectName(projectPath);
+ if (cb != null) {
+ if (cb.isCancelled()) { break; }
+ cb.onProjectStart(current, total, projectName);
+ }
try {
List projectRecords = processProject(projectPath, filter, null);
for (String[] r : projectRecords) {
resultList.add(new SqlExportRow(r[0], r[1], r[2], r[3], r[4]));
}
+ if (cb != null) {
+ cb.onProjectDone(current, total, projectName, projectRecords.size());
+ }
} catch (Exception e) {
log.error("处理项目时出错: " + projectPath, e);
+ if (cb != null) {
+ cb.onProjectDone(current, total, projectName + " (出错)", 0);
+ }
}
}
@@ -150,6 +174,18 @@ public class MyBatisExportService {
return new ExportResult(resultList, fileName);
}
+ /** 根据过滤模式和值构建过滤器 */
+ private SqlFilter buildFilter(String filterMode, String filterValue) {
+ if ("table".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) {
+ return new TableFilter(filterValue);
+ } else if ("keyword".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) {
+ return new KeywordFilter(filterValue);
+ } else if ("tagid".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) {
+ return new TagIdFilter(filterValue);
+ }
+ return null;
+ }
+
private void exportWithFilter(String fileNamePrefix, SqlFilter filter,
ProcessStrategy processStrategy) throws Exception {
List projectPaths = fileOperationHelper.getProjectPaths();
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 7641f11..7180850 100644
--- a/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java
+++ b/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java
@@ -231,7 +231,7 @@ public class WorkUtilsProperties {
* 文件不存在时自动创建空文件。
*/
public Path getProjectPathsFilePath() {
- Path p = Paths.get(projectPathsFile != null ? projectPathsFile : "project_paths.txt");
+ Path p = Paths.get(projectPathsFile != null ? projectPathsFile : "data/project_paths.txt");
if (p.isAbsolute()) {
return p;
}
diff --git a/src/main/java/com/example/workutils/system/ui/MainWindow.java b/src/main/java/com/example/workutils/system/ui/MainWindow.java
index 301e617..d1e26ae 100644
--- a/src/main/java/com/example/workutils/system/ui/MainWindow.java
+++ b/src/main/java/com/example/workutils/system/ui/MainWindow.java
@@ -171,7 +171,6 @@ public class MainWindow {
stage.setScene(scene);
showWelcomePage();
- if (!navItems.isEmpty()) selectNavItem(navItems.get(0));
}
// ==================== 统一顶部栏 ====================