diff --git a/src/main/java/com/example/workutils/WorkUtilsApplication.java b/src/main/java/com/example/workutils/WorkUtilsApplication.java index ba6255e..14206bd 100644 --- a/src/main/java/com/example/workutils/WorkUtilsApplication.java +++ b/src/main/java/com/example/workutils/WorkUtilsApplication.java @@ -92,8 +92,6 @@ public class WorkUtilsApplication implements CommandLineRunner { public void run(String... args) throws Exception { if (args.length == 0) { showMainWindow(); - } else if ("--console".equals(args[0])) { - showInteractiveMenu(); } else { processCommand(args); } @@ -104,14 +102,12 @@ public class WorkUtilsApplication implements CommandLineRunner { private void showMainWindow() throws InterruptedException { JfxBootstrap.await(); if (JfxBootstrap.isFailed()) { - System.err.println("JavaFX 初始化失败,回退到控制台模式"); - showInteractiveMenu(); + System.err.println("JavaFX 初始化失败,程序退出。"); return; } - // 始终使用亮色主题 - Theme.Mode mode = Theme.Mode.LIGHT; - Platform.runLater(() -> Theme.applyTheme(mode)); + // 初始化亮色主题 + Platform.runLater(Theme::initTheme); // 设置导航栏刷新回调(插件管理中启用/禁用后刷新侧边栏) settingsService.setNavItemsRefresher(items -> Platform.runLater(() -> { @@ -262,28 +258,7 @@ public class WorkUtilsApplication implements CommandLineRunner { } } - private void openSettingsStandalone() { - if (settingsWindow == null) { - settingsWindow = settingsService.createSettingsWindow(null); - } - javafx.stage.Stage s = settingsWindow.getStage(); - if (s != null) { - s.show(); - } - } - - /** @deprecated 保留兼容旧路径 */ - private void openSettingsWindow() { - new Thread(() -> settingsService.showSettings(null, null), - "SettingsWindow-Thread").start(); - } - - // ==================== 控制台模式(降级) ==================== - - private void showInteractiveMenu() { - Scanner scanner = new Scanner(System.in); - pluginManager.runMainMenu(scanner); - } + // ==================== 命令行模式 ==================== private void processCommand(String... args) throws Exception { String command = args[0].toLowerCase(); diff --git a/src/main/java/com/example/workutils/biz/apidoc/ApiDocPlugin.java b/src/main/java/com/example/workutils/biz/apidoc/ApiDocPlugin.java index e3c3040..16309d5 100644 --- a/src/main/java/com/example/workutils/biz/apidoc/ApiDocPlugin.java +++ b/src/main/java/com/example/workutils/biz/apidoc/ApiDocPlugin.java @@ -1,8 +1,5 @@ package com.example.workutils.biz.apidoc; -import com.example.workutils.biz.apidoc.generator.ApiDocGenerator; -import com.example.workutils.biz.apidoc.model.ApiDocModel; -import com.example.workutils.biz.apidoc.parser.JavaSourceParser; import com.example.workutils.system.config.WorkUtilsProperties; import com.example.workutils.system.plugin.Plugin; import com.example.workutils.system.plugin.PluginDescriptor; @@ -17,10 +14,7 @@ import org.springframework.stereotype.Component; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; import java.util.concurrent.CountDownLatch; -import java.util.stream.Stream; /** * API 接口文档导出插件 @@ -42,22 +36,12 @@ public class ApiDocPlugin implements Plugin { .icon("📄") .build(); - private static final String[] MENU_OPTIONS = { - "1. 自定义路径导出(输入文件或目录路径)", - "2. 全限定名导出 (仅输入类名,自动匹配项目)", - "3. 批量导出 (从配置文件选择项目导出)", - "0. 返回上级菜单" - }; - private final WorkUtilsProperties properties; private final FileOperationHelper fileOperationHelper; @Override public PluginDescriptor getDescriptor() { return DESCRIPTOR; } - @Override - public String[] getMenuOptions() { return MENU_OPTIONS; } - @Override public VisualPluginFactory getVisualFactory() { return new VisualPluginFactory() { @@ -81,7 +65,6 @@ public class ApiDocPlugin implements Plugin { latch = new CountDownLatch(1); window = new ApiDocWindow(fileOperationHelper, properties, latch); } - // 如果之前被内嵌过,Scene root 已被掏空,需要把 embeddedRoot 挂回去 if (embeddedRoot != null && window.getStage().getScene().getRoot() != embeddedRoot) { window.getStage().getScene().setRoot(embeddedRoot); } @@ -117,203 +100,9 @@ public class ApiDocPlugin implements Plugin { return null; } - @Override - public void handleChoice(int choice, Scanner scanner) throws Exception { - if (choice == 1) { - exportCustomPath(scanner); - } else if (choice == 2) { - exportByFqn(scanner); - } else if (choice == 3) { - exportAll(scanner); - } - } + // ==================== 辅助方法(供 ApiDocWindow 使用) ==================== - // ==================== 批量导出(遍历所有项目) ==================== - - private void exportAll(Scanner scanner) throws Exception { - System.out.println("\n=== 批量导出(按项目)==="); - - // 1) 从配置文件读取项目路径 - Path configFile = properties.getProjectPathsFilePath(); - if (!Files.exists(configFile)) { - System.out.println("配置文件不存在: " + configFile.toAbsolutePath()); - return; - } - - List projectPaths; - try { - projectPaths = fileOperationHelper.getProjectPaths(); - } catch (Exception e) { - System.out.println("读取配置文件失败: " + e.getMessage()); - return; - } - - if (projectPaths.isEmpty()) { - System.out.println("配置文件中未配置任何项目路径。"); - return; - } - - // 2) 列出项目,让用户选择 - System.out.println("\n配置项目列表:"); - for (int i = 0; i < projectPaths.size(); i++) { - String projPath = projectPaths.get(i); - String projName = Paths.get(projPath).getFileName().toString(); - System.out.printf(" %d. %s (%s)%n", i + 1, projName, projPath); - } - System.out.println(" 0. 全部导出"); - - System.out.print("\n请选择要导出的项目 (1-" + projectPaths.size() + ",0 全部): "); - String choice = scanner.nextLine().trim(); - - // 筛选要导出的项目路径 - List selectedPaths = new ArrayList<>(); - if ("0".equals(choice)) { - selectedPaths.addAll(projectPaths); - } else { - try { - int idx = Integer.parseInt(choice) - 1; - if (idx < 0 || idx >= projectPaths.size()) { - System.out.println("无效选择。"); - return; - } - selectedPaths.add(projectPaths.get(idx)); - } catch (NumberFormatException e) { - System.out.println("无效输入。"); - return; - } - } - - System.out.print("导出格式 (1-HTML / 2-Markdown / 3-两者都导出,默认 3): "); - String fmt = scanner.nextLine().trim(); - - // 3) 扫描选中项目下所有模块 - Set seenSrcBases = new HashSet<>(); - List modules = new ArrayList<>(); - - for (String projPath : selectedPaths) { - Path projDir = Paths.get(projPath); - if (!Files.isDirectory(projDir)) { - System.out.println(" [跳过] 目录不存在: " + projPath); - continue; - } - - // 先检查自身是否就是模块(有 src/main/java) - Path selfSrcBase = projDir.resolve("src/main/java"); - if (Files.isDirectory(selfSrcBase)) { - String key = selfSrcBase.toString().replace('\\', '/'); - if (seenSrcBases.add(key)) { - modules.add(new ModuleInfo(projDir.getFileName().toString(), - projPath.replace('\\', '/'), selfSrcBase)); - } - } - - // 再递归找子模块(多模块项目) - try (Stream children = Files.list(projDir)) { - children.filter(Files::isDirectory) - .filter(d -> !d.getFileName().toString().startsWith(".")) - .forEach(d -> collectModules(d, modules, seenSrcBases)); - } catch (Exception ignored) {} - } - - if (modules.isEmpty()) { - System.out.println("错误: 未找到任何包含 src/main/java 的模块。"); - return; - } - - System.out.printf("\n找到 %d 个模块:%n", modules.size()); - for (ModuleInfo m : modules) { - System.out.println(" - " + m.name + " (" + m.path + ")"); - } - - // 3) 遍历每个模块,解析所有 Java 文件 - List allDocs = new ArrayList<>(); - int totalClasses = 0, totalMethods = 0; - - for (ModuleInfo module : modules) { - System.out.print("\n[" + module.name + "] 扫描 Java 文件..."); - List javaFiles = new JavaSourceParser().findJavaFilesFrom(module.srcBase); - if (javaFiles.isEmpty()) { - System.out.println(" 无 .java 文件,跳过。"); - continue; - } - System.out.printf(" %d 个文件,正在解析...%n", javaFiles.size()); - - List classes = new ArrayList<>(); - for (Path javaFile : javaFiles) { - JavaSourceParser parser = new JavaSourceParser(); - ApiDocModel.ClassInfo ci = parser.parseFile(javaFile, module.name, true); - if (ci != null && ci.methods != null && !ci.methods.isEmpty()) { - ci.projectName = module.name; - classes.add(ci); - } - } - - if (classes.isEmpty()) { - System.out.println(" [" + module.name + "] 无有效 API 类,跳过。"); - continue; - } - classes.sort(Comparator.comparing(c -> c.className)); - - // DTO 解析(对此模块) - System.out.print(" [" + module.name + "] 解析 DTO..."); - JavaSourceParser dtoParser = new JavaSourceParser(); - for (ApiDocModel.ClassInfo ci : classes) { - dtoParser.resolveDtosForClass(ci, module.srcBase); - } - int reqFields = classes.stream().flatMap(c -> c.methods.stream()) - .filter(m -> m.requestFields != null).mapToInt(m -> m.requestFields.size()).sum(); - int respFields = classes.stream().flatMap(c -> c.methods.stream()) - .filter(m -> m.responseFields != null).mapToInt(m -> m.responseFields.size()).sum(); - int methods = classes.stream().mapToInt(c -> c.methods.size()).sum(); - System.out.printf(" %d 个类, %d 个方法, %d 请求字段, %d 响应字段%n", - classes.size(), methods, reqFields, respFields); - - ApiDocModel.ProjectApiDoc doc = new ApiDocModel.ProjectApiDoc(); - doc.projectName = module.name; - doc.projectPath = module.path; - doc.classes = classes; - allDocs.add(doc); - - totalClasses += classes.size(); - totalMethods += methods; - } - - if (allDocs.isEmpty()) { - System.out.println("\n未找到任何 API 接口。"); - return; - } - - System.out.printf("%n总计: %d 个模块, %d 个类, %d 个方法%n", allDocs.size(), totalClasses, totalMethods); - - // 4) 导出 - ApiDocModel.ExportSelection sel = new ApiDocModel.ExportSelection(); - sel.scope = ApiDocModel.ExportScope.ALL; - sel.projectPath = configFile.toString(); - sel.projectName = "所有项目"; - doExport(sel, allDocs, fmt); - } - - /** - * 递归收集目录下所有包含 src/main/java 的模块 - */ - private void collectModules(Path dir, List modules, Set seenSrcBases) { - if (!Files.isDirectory(dir)) return; - Path srcBase = dir.resolve("src/main/java"); - String key = srcBase.toString().replace('\\', '/'); - if (Files.isDirectory(srcBase) && seenSrcBases.add(key)) { - modules.add(new ModuleInfo(dir.getFileName().toString(), - dir.toString().replace('\\', '/'), srcBase)); - } - // 继续看子目录(可能包含子模块) - try (Stream children = Files.list(dir)) { - children.filter(Files::isDirectory) - .filter(d -> !d.getFileName().toString().startsWith(".")) - .forEach(d -> collectModules(d, modules, seenSrcBases)); - } catch (Exception ignored) {} - } - - /** 模块信息 */ - private static class ModuleInfo { + static class ModuleInfo { final String name; final String path; final Path srcBase; @@ -323,330 +112,4 @@ public class ApiDocPlugin implements Plugin { this.srcBase = srcBase; } } - - // ==================== 全限定名导出 ==================== - - private void exportByFqn(Scanner scanner) throws Exception { - System.out.println("\n=== API 接口文档导出(全限定名)==="); - - System.out.print("请输入全限定类名: "); - String fqn = scanner.nextLine().trim(); - if (fqn.isEmpty()) { - System.out.println("全限定名为空,已取消。"); - return; - } - - // 从配置文件读取项目路径,自动匹配 FQN - List projectPaths; - try { - projectPaths = fileOperationHelper.getProjectPaths(); - } catch (Exception e) { - System.out.println("读取配置文件失败: " + e.getMessage()); - return; - } - if (projectPaths.isEmpty()) { - System.out.println("配置文件中未配置任何项目路径。"); - return; - } - - String relativePath = fqn.replace('.', '/') + ".java"; - Path foundFile = null; - Path foundSrcBase = null; - String foundProjectName = null; - - for (String projPath : projectPaths) { - Path projDir = Paths.get(projPath); - if (!Files.isDirectory(projDir)) continue; - - // 检查项目自身 + 子模块 - List srcBases = new ArrayList<>(); - Path selfSrc = projDir.resolve("src/main/java"); - if (Files.isDirectory(selfSrc)) srcBases.add(selfSrc); - // 递归找子模块 - try (Stream children = Files.list(projDir)) { - children.filter(Files::isDirectory) - .filter(d -> !d.getFileName().toString().startsWith(".")) - .forEach(d -> collectSrcBases(d, srcBases)); - } catch (Exception ignored) {} - - for (Path srcBase : srcBases) { - Path candidate = srcBase.resolve(relativePath); - if (Files.isRegularFile(candidate)) { - foundFile = candidate; - foundSrcBase = srcBase; - foundProjectName = extractProjectName(candidate.toString().replace('\\', '/')); - break; - } - } - if (foundFile != null) break; - } - - if (foundFile == null) { - System.out.println("错误: 在所有配置项目中未找到 " + fqn); - return; - } - - System.out.println("文件: " + foundFile.getFileName()); - System.out.println("全限定名: " + fqn); - System.out.println("项目: " + foundProjectName); - - JavaSourceParser parser = new JavaSourceParser(); - ApiDocModel.ClassInfo ci = parser.parseFile(foundFile, foundProjectName, true); - if (ci == null || ci.methods == null || ci.methods.isEmpty()) { - System.out.println("该文件未找到有效的类/接口声明或公开方法。"); - return; - } - System.out.printf("解析结果: %s %s, %d 个方法%n", - ci.type, ci.className, ci.methods.size()); - - // DTO 深层解析 - System.out.println("正在解析 DTO..."); - parser.resolveDtosForClass(ci, foundSrcBase); - int reqFields = ci.methods.stream().filter(m -> m.requestFields != null).mapToInt(m -> m.requestFields.size()).sum(); - int respFields = ci.methods.stream().filter(m -> m.responseFields != null).mapToInt(m -> m.responseFields.size()).sum(); - System.out.printf("DTO 解析完成: %d 个请求字段, %d 个响应字段%n%n", reqFields, respFields); - - System.out.print("导出格式 (1-HTML / 2-Markdown / 3-两者都导出,默认 3): "); - String fmt = scanner.nextLine().trim(); - - String fqnPath = foundFile.toString().replace('\\', '/'); - ApiDocModel.ExportSelection sel = new ApiDocModel.ExportSelection(); - sel.scope = ApiDocModel.ExportScope.CUSTOM; - sel.projectPath = fqnPath; - sel.projectName = foundProjectName; - sel.classInfo = ci; - - ci.projectName = foundProjectName; - ApiDocModel.ProjectApiDoc doc = new ApiDocModel.ProjectApiDoc(); - doc.projectName = foundProjectName; - doc.projectPath = fqnPath; - doc.classes = Collections.singletonList(ci); - - doExport(sel, Collections.singletonList(doc), fmt); - } - - /** - * 递归收集目录下所有 src/main/java 路径(仅 Path,不创建 ModuleInfo) - */ - private void collectSrcBases(Path dir, List srcBases) { - if (!Files.isDirectory(dir)) return; - Path srcBase = dir.resolve("src/main/java"); - if (Files.isDirectory(srcBase)) srcBases.add(srcBase); - try (Stream children = Files.list(dir)) { - children.filter(Files::isDirectory) - .filter(d -> !d.getFileName().toString().startsWith(".")) - .forEach(d -> collectSrcBases(d, srcBases)); - } catch (Exception ignored) {} - } - - // ==================== 自定义路径导出 ==================== - - private void exportCustomPath(Scanner scanner) throws Exception { - System.out.println("\n=== API 接口文档导出 ==="); - System.out.println("支持输入单个 .java 文件路径 或 目录路径"); - System.out.println("示例: D:\\projects\\Facade.java / D:\\projects\\src\\main\\java\n"); - System.out.print("请输入路径: "); - String inputPath = scanner.nextLine().trim(); - - if (inputPath.isEmpty()) { - System.out.println("路径为空,已取消。"); - return; - } - - Path targetPath = Paths.get(inputPath); - JavaSourceParser parser = new JavaSourceParser(); - - // --- 判断是文件还是目录 --- - if (parser.isJavaFile(targetPath)) { - exportSingleFile(inputPath, targetPath, parser, scanner); - } else if (parser.isDirectory(targetPath)) { - exportDirectory(inputPath, targetPath, parser, scanner); - } else { - System.out.println("路径不存在或不是有效的 Java 文件/目录: " + inputPath); - } - } - - // ==================== 单文件导出 ==================== - - private void exportSingleFile(String inputPath, Path filePath, - JavaSourceParser parser, Scanner scanner) throws Exception { - String projectName = extractProjectName(inputPath); - System.out.println("文件: " + filePath.getFileName()); - System.out.println("项目: " + projectName); - - // includeAll=true:强制解析所有类,不要求有 Dubbo/Spring 注解 - ApiDocModel.ClassInfo ci = parser.parseFile(filePath, projectName, true); - if (ci == null || ci.methods == null || ci.methods.isEmpty()) { - System.out.println("该文件未找到有效的类/接口声明或公开方法。"); - return; - } - System.out.printf("解析结果: %s %s, %d 个方法%n", - ci.type, ci.className, ci.methods.size()); - - // DTO 深层解析 - Path srcBase = resolveSrcBase(inputPath); - if (srcBase != null) { - System.out.println("正在解析 DTO..."); - parser.resolveDtosForClass(ci, srcBase); - int reqFields = ci.methods.stream().filter(m -> m.requestFields != null).mapToInt(m -> m.requestFields.size()).sum(); - int respFields = ci.methods.stream().filter(m -> m.responseFields != null).mapToInt(m -> m.responseFields.size()).sum(); - System.out.printf("DTO 解析完成: %d 个请求字段, %d 个响应字段%n%n", reqFields, respFields); - } else { - System.out.println(); - } - - System.out.print("导出格式 (1-HTML / 2-Markdown / 3-两者都导出,默认 3): "); - String fmt = scanner.nextLine().trim(); - - ApiDocModel.ExportSelection sel = new ApiDocModel.ExportSelection(); - sel.scope = ApiDocModel.ExportScope.CUSTOM; - sel.projectPath = inputPath; - sel.projectName = projectName; - sel.classInfo = ci; - - ci.projectName = projectName; - ApiDocModel.ProjectApiDoc doc = new ApiDocModel.ProjectApiDoc(); - doc.projectName = projectName; - doc.projectPath = inputPath; - doc.classes = Collections.singletonList(ci); - - doExport(sel, Collections.singletonList(doc), fmt); - } - - // ==================== 目录导出 ==================== - - private void exportDirectory(String inputPath, Path dirPath, - JavaSourceParser parser, Scanner scanner) throws Exception { - String projectName = extractProjectName(inputPath); - System.out.println("目录: " + dirPath); - System.out.println("项目: " + projectName); - - System.out.print("导出格式 (1-HTML / 2-Markdown / 3-两者都导出,默认 3): "); - String fmt = scanner.nextLine().trim(); - - System.out.println("\n正在扫描目录..."); - List javaFiles = parser.findJavaFilesFrom(dirPath); - if (javaFiles.isEmpty()) { - System.out.println("该目录下没有找到 .java 文件。"); - return; - } - System.out.printf("找到 %d 个 Java 文件,正在解析...%n", javaFiles.size()); - - List classes = new ArrayList<>(); - int totalMethods = 0; - for (Path javaFile : javaFiles) { - ApiDocModel.ClassInfo ci = parser.parseFile(javaFile, projectName, true); - if (ci != null && ci.methods != null && !ci.methods.isEmpty()) { - classes.add(ci); - totalMethods += ci.methods.size(); - } - } - classes.sort(Comparator.comparing(c -> c.className)); - - if (classes.isEmpty()) { - System.out.println("未找到包含有效类声明和公开方法的 Java 文件。"); - return; - } - System.out.printf("解析完成: %d 个类, %d 个方法%n", classes.size(), totalMethods); - - ApiDocModel.ExportSelection sel = new ApiDocModel.ExportSelection(); - sel.scope = ApiDocModel.ExportScope.CUSTOM; - sel.projectPath = inputPath; - sel.projectName = projectName; - - ApiDocModel.ProjectApiDoc doc = new ApiDocModel.ProjectApiDoc(); - doc.projectName = projectName; - doc.projectPath = inputPath; - doc.classes = classes; - - doExport(sel, Collections.singletonList(doc), fmt); - } - - // ==================== 导出输出 ==================== - - private void doExport(ApiDocModel.ExportSelection sel, List docs, String fmt) throws Exception { - ApiDocGenerator gen = new ApiDocGenerator(properties); - - if ("1".equals(fmt)) { - Path html = gen.generateHtml(sel, docs); - System.out.println("\nHTML 文档已导出: " + html.toAbsolutePath()); - } else if ("2".equals(fmt)) { - Path md = gen.generateMarkdown(sel, docs); - System.out.println("\nMarkdown 文档已导出: " + md.toAbsolutePath()); - } else { - Path html = gen.generateHtml(sel, docs); - Path md = gen.generateMarkdown(sel, docs); - System.out.println("\n文档已导出:"); - System.out.println(" HTML: " + html.toAbsolutePath()); - System.out.println(" Markdown: " + md.toAbsolutePath()); - } - } - - // ==================== 项目名提取 ==================== - - /** - * 从路径中定位 src/main/java 根目录,用于 DTO 文件查找 - */ - private Path resolveSrcBase(String inputPath) { - if (inputPath == null || inputPath.isEmpty()) return null; - String normalized = inputPath.replace('\\', '/'); - int srcIdx = normalized.indexOf("/src/main/java"); - if (srcIdx > 0) { - return Paths.get(normalized.substring(0, srcIdx + "/src/main/java".length())); - } - srcIdx = normalized.indexOf("/src/"); - if (srcIdx > 0) { - return Paths.get(normalized.substring(0, srcIdx + "/src/main/java".length())); - } - return null; - } - - /** - * 从路径中提取项目名。 - * 支持多模块 Maven 项目:同时识别工程名和模块名。 - * - * 例如 D:\xxx\ft-compensatory-core\ft-compensatory-core-api\src\main\java\...\Facade.java - * → ft-compensatory-core / ft-compensatory-core-api - */ - private String extractProjectName(String path) { - if (path == null || path.isEmpty()) return "unknown"; - String normalized = path.replace('\\', '/'); - - // 找 src/main/java 位置 - int srcIdx = normalized.lastIndexOf("/src/main/java"); - if (srcIdx <= 0) { - srcIdx = normalized.lastIndexOf("/src/"); - } - if (srcIdx <= 0) { - // 没有 src 目录,取文件名 - String name = lastSegment(normalized); - if (name.endsWith(".java")) name = name.substring(0, name.length() - 5); - return name; - } - - // src/main/java 前面的部分,即模块根路径 - // 例如 D:/bestpay-srdcloud/ft-compensatory-core/ft-compensatory-core-api - String moduleRoot = normalized.substring(0, srcIdx); - String moduleName = lastSegment(moduleRoot); - - // 尝试识别父工程目录(模块目录的上一级) - int lastSlash = moduleRoot.lastIndexOf('/'); - if (lastSlash > 0) { - String parentDir = moduleRoot.substring(0, lastSlash); - String parentName = lastSegment(parentDir); - // 如果模块名以父工程名开头(如 ft-compensatory-core / ft-compensatory-core-api), - // 说明是多模块结构,返回 "工程名_模块名" - if (!parentName.isEmpty() && moduleName.startsWith(parentName) - && moduleName.length() > parentName.length()) { - return parentName + " / " + moduleName; - } - } - - return moduleName.isEmpty() ? "unknown" : moduleName; - } - - private static String lastSegment(String path) { - int idx = path.lastIndexOf('/'); - return idx >= 0 ? path.substring(idx + 1) : path; - } } diff --git a/src/main/java/com/example/workutils/biz/diff/DiffPlugin.java b/src/main/java/com/example/workutils/biz/diff/DiffPlugin.java index 652f56b..64d22da 100644 --- a/src/main/java/com/example/workutils/biz/diff/DiffPlugin.java +++ b/src/main/java/com/example/workutils/biz/diff/DiffPlugin.java @@ -4,7 +4,6 @@ import com.example.workutils.system.config.WorkUtilsProperties; import com.example.workutils.system.plugin.Plugin; import com.example.workutils.system.plugin.PluginDescriptor; import com.example.workutils.system.plugin.VisualPluginFactory; -import com.example.workutils.system.support.JfxBootstrap; import javafx.scene.Parent; import javafx.scene.layout.Pane; import lombok.RequiredArgsConstructor; @@ -19,11 +18,8 @@ import java.util.concurrent.CountDownLatch; * 文本对比插件 *

* 基于 LCS(最长公共子序列)算法实现行级精确文本对比, - * 输出格式兼容 unified diff,支持交互式和命令行两种模式: - *

    - *
  • 交互模式 — 粘贴文本对比(JavaFX 可视化窗口) / 文件对比 / 目录对比
  • - *
  • 命令行模式 — {@code diff <file1> <file2>} 直接输出差异
  • - *
+ * 输出格式兼容 unified diff,支持命令行模式: + * {@code diff <file1> <file2>} 直接输出差异 */ @Slf4j @Component @@ -39,11 +35,6 @@ public class DiffPlugin implements Plugin { .icon("📊") .build(); - private static final String[] MENU_OPTIONS = { - "1. 粘贴文本对比(弹出窗口)", - "0. 返回上级菜单" - }; - private final WorkUtilsProperties properties; // ==================== Plugin 契约 ==================== @@ -54,9 +45,6 @@ public class DiffPlugin implements Plugin { @Override public String getCommandName() { return "diff"; } - @Override - public String[] getMenuOptions() { return MENU_OPTIONS; } - @Override public VisualPluginFactory getVisualFactory() { return new VisualPluginFactory() { @@ -80,7 +68,6 @@ public class DiffPlugin implements Plugin { latch = new CountDownLatch(1); window = new DiffWindow(DiffPlugin.this, properties, latch); } - // 如果之前被内嵌过,Scene root 已被掏空,需要把 embeddedRoot 挂回去 if (embeddedRoot != null && window.getStage().getScene().getRoot() != embeddedRoot) { window.getStage().getScene().setRoot(embeddedRoot); } @@ -93,50 +80,6 @@ public class DiffPlugin implements Plugin { }; } - @Override - public void handleChoice(int choice, Scanner scanner) throws Exception { - switch (choice) { - case 1: openVisualDiff(); break; - default: printInvalidOption(); break; - } - } - - // ==================== JavaFX 初始化(复用共享启动器) ==================== - - private void openVisualDiff() { - try { - JfxBootstrap.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - if (JfxBootstrap.isFailed()) { - System.err.println("JavaFX 初始化失败,请确认 JDK 8 包含 JavaFX 运行时(OracleJDK 内置;OpenJDK 需安装 OpenJFX)"); - return; - } - - System.out.println("正在打开文本对比窗口..."); - java.util.concurrent.CountDownLatch windowClose = new java.util.concurrent.CountDownLatch(1); - javafx.application.Platform.runLater(() -> { - try { - DiffWindow window = new DiffWindow(DiffPlugin.this, properties, windowClose); - window.show(); - } catch (Exception e) { - System.err.println("窗口创建失败: " + e.getMessage()); - e.printStackTrace(); - windowClose.countDown(); // 异常也释放,避免无限阻塞 - } - }); - // 在控制台线程等待窗口关闭(不阻塞 JavaFX 渲染线程) - try { - windowClose.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - System.out.println("文本对比窗口已关闭"); - } - /** 导出基础目录,供 DiffWindow 调用 */ Path getExportBaseDir() { return properties.getTodayExportPath(); 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 0be70cd..cfedfb4 100644 --- a/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java +++ b/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java @@ -23,7 +23,6 @@ import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Scanner; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -53,21 +52,15 @@ public class GitSyncPlugin implements Plugin, InitializingBean { .icon("🔄") .build(); - private static final String[] MENU_OPTIONS = { - "1. 同步所有项目(切换分支 + 拉取)", - "2. 处理已损坏项目", - "0. 返回上级菜单" - }; - private static final String DEFAULT_BRANCH = "master"; private static final long CMD_TIMEOUT_SECONDS = 120; - private static final long SYNC_TOTAL_TIMEOUT_MINUTES = 30; // 整体同步超时 - private static final int PARALLEL = 6; // 并发数 + private static final long SYNC_TOTAL_TIMEOUT_MINUTES = 30; + private static final int PARALLEL = 6; private final FileOperationHelper fileOperationHelper; private final WorkUtilsProperties properties; - /** 缓存上一次扫描出的损坏仓库,供菜单选项 2 使用 */ + /** 缓存上一次扫描出的损坏仓库 */ private List lastDamagedRepos; /** 缓存上一次使用的目标分支 */ private String lastBranch; @@ -91,7 +84,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { if (lines.isEmpty()) { return; } - // 第一行: branch: String branch = DEFAULT_BRANCH; int startIdx = 0; if (lines.get(0).startsWith("branch:")) { @@ -104,7 +96,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { if (line.isEmpty()) { continue; } - // 格式: name|path|branch|error|remoteUrl String[] parts = line.split("\\|", 5); if (parts.length >= 4) { String remoteUrl = parts.length > 4 ? parts[4] : null; @@ -131,7 +122,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { w.newLine(); if (lastDamagedRepos != null) { for (DamagedRepo dr : lastDamagedRepos) { - // 格式: name|path|branch|error|remoteUrl w.write(dr.name + "|" + dr.path + "|" + dr.branch + "|" + (dr.error != null ? dr.error : "") + "|" + (dr.remoteUrl != null ? dr.remoteUrl : "null")); @@ -150,11 +140,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { return DESCRIPTOR; } - @Override - public String[] getMenuOptions() { - return MENU_OPTIONS; - } - @Override public VisualPluginFactory getVisualFactory() { return new VisualPluginFactory() { @@ -178,7 +163,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { latch = new CountDownLatch(1); window = new GitSyncWindow(GitSyncPlugin.this, fileOperationHelper, properties, latch); } - // 如果之前被内嵌过,Scene root 已被掏空,需要把 embeddedRoot 挂回去 if (embeddedRoot != null && window.getStage().getScene().getRoot() != embeddedRoot) { window.getStage().getScene().setRoot(embeddedRoot); } @@ -214,308 +198,11 @@ public class GitSyncPlugin implements Plugin, InitializingBean { return null; } - @Override - public void handleChoice(int choice, Scanner scanner) throws Exception { - switch (choice) { - case 1: - syncAll(scanner); - break; - case 2: - handleDamaged(scanner); - break; - default: - printInvalidOption(); - } - } - - // ==================== 主流程 ==================== - - private void syncAll(Scanner scanner) { - // 1. 读项目列表 - List projects; - try { - projects = fileOperationHelper.getProjectPathsWithStatus(); - } catch (IOException e) { - log.error("读取项目路径配置文件失败", e); - printError("读取项目路径配置文件失败: " + e.getMessage()); - return; - } - if (projects.isEmpty()) { - System.out.println("未配置任何项目路径,请在 " + properties.getProjectPathsFilePath() + " 中添加。"); - return; - } - - // 2. 选择分支 - System.out.print("请输入目标分支名(默认 " + DEFAULT_BRANCH + "): "); - String branch = scanner.nextLine().trim(); - if (branch.isEmpty()) { - branch = DEFAULT_BRANCH; - } - - System.out.println("\n========== 阶段一:切换分支 + 拉取(并行 " + PARALLEL + " 线程) =========="); - System.out.println("目标分支: " + branch); - System.out.println("项目总数: " + projects.size()); - - // 2.5 从项目路径文件提取 BASE_URL 映射(仓库损坏时兜底用) - Map baseUrlMap = new LinkedHashMap<>(); - for (FileOperationHelper.ProjectPathInfo proj : projects) { - if (proj.getRemoteBaseUrl() != null && !proj.getRemoteBaseUrl().isEmpty()) { - baseUrlMap.put(normalizePath(proj.getPath()), proj.getRemoteBaseUrl()); - } - } - log.info("从项目路径文件加载了 {} 条远程URL映射", baseUrlMap.size()); - - // 3. 并行切换+拉取 - int total = projects.size(); - Map resultMap = new LinkedHashMap<>(); - AtomicInteger completed = new AtomicInteger(0); - CountDownLatch latch = new CountDownLatch(total); - ExecutorService pool = Executors.newFixedThreadPool(PARALLEL); - long startTime = System.currentTimeMillis(); - - for (int i = 0; i < total; i++) { - final FileOperationHelper.ProjectPathInfo proj = projects.get(i); - final String repoPath = proj.getPath(); - final String repoName = extractName(repoPath); - final String targetBranch = branch; - final int taskNum = i + 1; // 固定序号,用于定位卡住的项 - - pool.submit(() -> { - // 开始处理时立即打印,方便定位卡住的项目 - System.out.printf("[%d/%d] 处理中 %s ...%n", taskNum, total, repoName); - try { - SyncResult result = syncOneRepo(repoPath, targetBranch, baseUrlMap); - resultMap.put(repoPath, result); - int done = completed.incrementAndGet(); - String tag; - switch (result) { - case OK: tag = result.cloned ? "拉取" : "成功"; break; - case DAMAGED: tag = "损坏"; break; - default: tag = "失败"; break; - } - System.out.printf("[%d/%d] %s >> %s (完成 %d/%d)%n", taskNum, total, repoName, tag, done, total); - } catch (Exception e) { - SyncResult r = SyncResult.FAILED; - r.error = e.getMessage(); - resultMap.put(repoPath, r); - int done = completed.incrementAndGet(); - System.out.printf("[%d/%d] %s >> 异常: %s (完成 %d/%d)%n", taskNum, total, repoName, e.getMessage(), done, total); - } finally { - latch.countDown(); - } - }); - } - - pool.shutdown(); - try { - boolean allDone = latch.await(SYNC_TOTAL_TIMEOUT_MINUTES, TimeUnit.MINUTES); - if (!allDone) { - pool.shutdownNow(); - System.err.println("\n!!! 同步超时(超过 " + SYNC_TOTAL_TIMEOUT_MINUTES + " 分钟),强制终止剩余任务 !!!"); - // 未完成的任务不再等待,继续输出已有结果 - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - pool.shutdownNow(); - printError("并行处理被中断"); - return; - } - - // 4. 汇总结果(保持与原项目顺序一致) - int success = 0, failed = 0; - List damagedRepos = new ArrayList<>(); - - for (FileOperationHelper.ProjectPathInfo proj : projects) { - SyncResult result = resultMap.get(proj.getPath()); - if (result == null) { - failed++; - continue; - } - switch (result) { - case OK: - success++; - break; - case DAMAGED: { - String err = result.error != null ? result.error : "未知错误"; - damagedRepos.add(new DamagedRepo(proj.getPath(), extractName(proj.getPath()), - branch, err, result.remoteUrl)); - break; - } - case FAILED: - failed++; - break; - } - } - - long elapsed = (System.currentTimeMillis() - startTime) / 1000; - System.out.println("\n---------- 阶段一完成 ----------"); - System.out.printf("总计: %d | 成功: %d | 损坏: %d | 失败: %d | 耗时: %ds%n", - projects.size(), success, damagedRepos.size(), failed, elapsed); - - // 缓存损坏仓库,供菜单选项 2 使用 - this.lastDamagedRepos = damagedRepos; - this.lastBranch = branch; - - // 持久化到日志文件,重启不丢失 - saveDamagedRepos(); - - // 4. 有损坏仓库?进入确认阶段 - if (damagedRepos.isEmpty()) { - System.out.println("\n全部同步成功!"); - return; - } - - processDamaged(scanner); - } - - // ==================== 处理损坏仓库(共享,选项1阶段二 & 选项2) ==================== - - private void handleDamaged(Scanner scanner) { - if (lastDamagedRepos == null || lastDamagedRepos.isEmpty()) { - System.out.println("\n暂无损坏仓库记录。请先执行「同步所有项目」。"); - return; - } - System.out.println("\n使用上次扫描结果(分支: " + lastBranch - + ",损坏: " + lastDamagedRepos.size() + " 个)"); - processDamaged(scanner); - } - - private void processDamaged(Scanner scanner) { - // 从项目路径文件重建 BASE_URL 映射(用于拼接 clone URL) - Map baseUrlMap = buildBaseUrlMap(); - - System.out.println("\n========== 处理损坏仓库 =========="); - System.out.println("以下 " + lastDamagedRepos.size() + " 个仓库无法正常切换/拉取:"); - System.out.println(); - for (int i = 0; i < lastDamagedRepos.size(); i++) { - DamagedRepo dr = lastDamagedRepos.get(i); - String cloneUrl = resolveCloneUrl(dr, baseUrlMap); - System.out.printf(" [%d] %s%n", i + 1, dr.name); - System.out.printf(" 路径: %s%n", dr.path); - System.out.printf(" 错误: %s%n", dr.error); - if (cloneUrl != null) { - System.out.printf(" 远程: %s%n", cloneUrl); - } else { - System.out.println(" 远程: (无法获取)"); - } - } - - System.out.println(); - System.out.println("选择处理方式:"); - System.out.println(" 1 - 逐个选择 确认 是否重新拉取(1重新拉,2跳过,0返回上一级)"); - System.out.println(" 2 - 全部删除重拉(删除本地 -> git clone)"); - System.out.println(" 0 - 跳过,不处理"); - System.out.print("请输入: "); - String choice = scanner.nextLine().trim(); - - if ("0".equals(choice)) { - System.out.println("已跳过,损坏仓库保持不变(可通过菜单选项 2 再次处理)。"); - return; - } - - int recloned = 0, recloneFailed = 0; - long startTime = System.currentTimeMillis(); - - if ("2".equals(choice)) { - // 全部删除重拉 - for (DamagedRepo dr : lastDamagedRepos) { - String cloneUrl = resolveCloneUrl(dr, baseUrlMap); - if (cloneUrl == null) { - System.out.printf(" [跳过] %s - 无法获取远程URL%n", dr.name); - recloneFailed++; - continue; - } - System.out.printf("%n 重拉 %s ...%n", dr.name); - if (doReclone(dr, cloneUrl)) { - recloned++; - } else { - recloneFailed++; - } - } - } else if ("1".equals(choice)) { - // 逐个选择 - for (int i = 0; i < lastDamagedRepos.size(); i++) { - DamagedRepo dr = lastDamagedRepos.get(i); - String cloneUrl = resolveCloneUrl(dr, baseUrlMap); - System.out.printf("%n [%d/%d] %s [%s]%n", i + 1, lastDamagedRepos.size(), dr.name, dr.path); - if (cloneUrl != null) { - System.out.printf(" 远程: %s%n", cloneUrl); - } else { - System.out.println(" 远程: (无法获取)"); - } - System.out.println(" 1 - 重新拉取 2 - 跳过 0 - 返回上一级"); - System.out.print(" 请选择: "); - - String sub = scanner.nextLine().trim(); - if ("0".equals(sub)) { - System.out.println(" 已返回。"); - break; - } - if ("1".equals(sub)) { - if (cloneUrl == null) { - System.out.println(" [跳过] 无法获取远程URL,无法重新拉取"); - recloneFailed++; - continue; - } - if (doReclone(dr, cloneUrl)) { - recloned++; - } else { - recloneFailed++; - } - } else { - // 2 或其他输入视为跳过 - System.out.println(" 已跳过。"); - } - } - } - - long elapsed2 = (System.currentTimeMillis() - startTime) / 1000; - System.out.println("\n---------- 处理完成 ----------"); - System.out.printf("重拉成功: %d | 重拉失败: %d | 耗时: %ds%n", recloned, recloneFailed, elapsed2); - - // 处理完成后重新保存缓存(若有重拉成功,下次全扫会确认状态) - saveDamagedRepos(); - } - - /** 从项目路径文件重建 BASE_URL 映射 */ - private Map buildBaseUrlMap() { - Map map = new LinkedHashMap<>(); - try { - List projects = fileOperationHelper.getProjectPathsWithStatus(); - for (FileOperationHelper.ProjectPathInfo proj : projects) { - if (proj.getRemoteBaseUrl() != null && !proj.getRemoteBaseUrl().isEmpty()) { - map.put(normalizePath(proj.getPath()), proj.getRemoteBaseUrl()); - } - } - } catch (IOException e) { - log.error("读取项目路径文件失败", e); - } - return map; - } - - /** 解析 clone URL:BASE_URL 本身就是完整的 clone 地址,直接返回 */ - private String resolveCloneUrl(DamagedRepo dr, Map baseUrlMap) { - // 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) { - return baseUrl; - } - } - // 2. 兜底:缓存的 remoteUrl - if (dr.remoteUrl != null && !dr.remoteUrl.isEmpty()) { - return dr.remoteUrl; - } - return null; - } - - // ==================== 单仓库同步核心逻辑(不自动重拉) ==================== + // ==================== 单仓库同步核心逻辑 ==================== private SyncResult syncOneRepo(String repoPath, String branch, Map baseUrlMap) { File repoDir = new File(repoPath); - // 目录不存在 或 空目录 → 自动 clone if (!repoDir.exists() || (repoDir.isDirectory() && isEmptyDir(repoDir))) { String reason = !repoDir.exists() ? "目录不存在" : "目录为空"; log.info("{},自动 clone: {}", reason, repoPath); @@ -535,7 +222,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { return r; } - // 目录存在但无 .git → 仓库损坏 if (!new File(repoDir, ".git").exists()) { SyncResult r = SyncResult.DAMAGED; r.error = "目录存在但无 .git(仓库损坏)"; @@ -549,11 +235,10 @@ public class GitSyncPlugin implements Plugin, InitializingBean { log.warn("fetch 失败: {} -> {}", repoPath, r1.error); } - // 2. checkout:先 stash 本地修改,避免因未提交修改导致 checkout 失败 + // 2. 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; @@ -562,7 +247,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { } if (!r2.ok) { log.warn("checkout 失败: {} -> {},标记为损坏", repoPath, r2.error); - // 如果 stash 了但 checkout 仍失败,恢复 stash if (didStash) { runGit(repoPath, "git", "stash", "pop"); } @@ -576,7 +260,6 @@ 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"); } @@ -586,7 +269,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { return r; } - // 4. 恢复之前 stash 的修改(忽略冲突,保留 stash 让用户手动处理) if (didStash) { CmdResult popResult = runGit(repoPath, "git", "stash", "pop"); if (!popResult.ok) { @@ -601,14 +283,11 @@ public class GitSyncPlugin implements Plugin, InitializingBean { // ==================== clone 逻辑 ==================== - /** 执行 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()); @@ -618,7 +297,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { return false; } } - // clone String parentDir = dir.getParent() != null ? dir.getParent() : "."; CmdResult cr = runGit(parentDir, "git", "clone", "-b", branch, cloneUrl, repoPath); if (cr.ok) { @@ -629,7 +307,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { } } - /** 手动重拉(供 DamagedRepo 场景使用) */ private boolean doReclone(DamagedRepo dr, String cloneUrl) { System.out.println(" clone URL: " + cloneUrl); boolean ok = doClone(dr.path, dr.branch, cloneUrl); @@ -643,12 +320,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { // ==================== Git 命令执行 ==================== - /** - * 执行一条 Git 命令,总超时 CMD_TIMEOUT_SECONDS + 10 秒。 - *

- * 关键:进程输出在独立线程中读取,主线程通过 process.waitFor(timeout) 等待。 - * 超时后强制杀进程 + 中断读线程,杜绝僵尸进程 / 永久阻塞。 - */ private CmdResult runGit(String workingDir, String... command) { CmdResult result = new CmdResult(); Process process = null; @@ -658,15 +329,13 @@ public class GitSyncPlugin implements Plugin, InitializingBean { try { ProcessBuilder pb = new ProcessBuilder(command); - if (workingDir != null) { pb.directory(new File(workingDir)); } pb.redirectErrorStream(true); process = pb.start(); - final Process proc = process; // effectively final 副本供 lambda 引用 + final Process proc = process; - // 独立线程读取进程输出,避免 readLine() 阻塞主逻辑 readerThread = new Thread(() -> { try (BufferedReader reader = new BufferedReader( new InputStreamReader(proc.getInputStream(), java.nio.charset.StandardCharsets.UTF_8))) { @@ -675,13 +344,11 @@ public class GitSyncPlugin implements Plugin, InitializingBean { output.append(line).append("\n"); } } catch (IOException ignored) { - // 进程被杀时流关闭会抛异常,正常情况 } }, "git-reader"); readerThread.setDaemon(true); readerThread.start(); - // 等待进程结束(带超时) boolean finished = process.waitFor(CMD_TIMEOUT_SECONDS, TimeUnit.SECONDS); if (!finished) { log.warn("Git 命令超时({}s): {} -> {}", CMD_TIMEOUT_SECONDS, workingDir, commandText); @@ -690,7 +357,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { return result; } - // 进程已结束,等待读线程收尾(最多10秒) readerThread.join(10_000); int exitCode = process.exitValue(); @@ -710,12 +376,9 @@ public class GitSyncPlugin implements Plugin, InitializingBean { result.ok = false; result.error = "执行异常: " + e.getMessage(); } finally { - // 确保进程和读线程都被清理 - if (process != null) { - if (process.isAlive()) { - process.destroyForcibly(); - try { process.waitFor(5, TimeUnit.SECONDS); } catch (InterruptedException ignored) { } - } + if (process != null && process.isAlive()) { + process.destroyForcibly(); + try { process.waitFor(5, TimeUnit.SECONDS); } catch (InterruptedException ignored) { } } if (readerThread != null && readerThread.isAlive()) { readerThread.interrupt(); @@ -726,12 +389,10 @@ public class GitSyncPlugin implements Plugin, InitializingBean { } private String getRemoteUrl(String repoPath, Map baseUrlMap) { - // 1. 尝试 git remote get-url CmdResult r = runGit(repoPath, "git", "remote", "get-url", "origin"); if (r.ok && !r.output.isEmpty()) { return r.output.trim(); } - // 2. 备用: 从 git config 文件直接读 File gitConfig = new File(repoPath, ".git/config"); if (gitConfig.exists()) { try { @@ -744,7 +405,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { } catch (IOException ignored) { } } - // 3. 兜底: 项目路径文件中的 BASE_URL 即完整的 clone URL if (baseUrlMap != null) { String normalizedPath = normalizePath(repoPath); String baseUrl = baseUrlMap.get(normalizedPath); @@ -763,7 +423,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { File[] files = path.toFile().listFiles(); if (files != null) { for (File f : files) { - // Windows 下 .git/objects 文件带只读属性,先去掉 if (!f.canWrite()) { f.setWritable(true); } @@ -771,7 +430,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { } } } - // 目录/文件本身也可能只读 File file = path.toFile(); if (!file.canWrite()) { file.setWritable(true); @@ -786,20 +444,17 @@ 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 ""; String normalized = path.trim().replace('\\', '/'); @@ -818,9 +473,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { // ==================== 供可视化窗口调用的公开 API ==================== - /** - * 同步回调接口 —— 用于 UI 窗口接收实时进度 - */ public interface SyncCallback { void onLog(String message); void onProgress(int current, int total, String repoName, String status, boolean error); @@ -828,7 +480,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { void onDamagedRepos(List repos); } - /** 同步结果汇总 */ public static class SyncSummary { public int total; public int success; @@ -837,9 +488,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { public long elapsedSeconds; } - /** - * 以回调方式同步所有项目(供可视窗口调用) - */ public void syncWithCallback(String branch, SyncCallback cb) { syncCancelled = false; List projects; @@ -879,10 +527,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean { if (syncCancelled) { cb.onLog("[INFO] 同步已取消"); pool.shutdownNow(); return; } for (int i = 0; i < total; i++) { - if (syncCancelled) { - // 不再提交新任务,但已提交的会在 latch 中计完 - break; - } + if (syncCancelled) break; final FileOperationHelper.ProjectPathInfo proj = projects.get(i); final String repoPath = proj.getPath(); final String repoName = extractName(repoPath); @@ -916,7 +561,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { pool.shutdown(); try { - // 缩短每次 await 时间,以便及时响应取消 long checkIntervalMs = 500; long deadline = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(SYNC_TOTAL_TIMEOUT_MINUTES); while (!latch.await(checkIntervalMs, TimeUnit.MILLISECONDS)) { @@ -938,7 +582,6 @@ public class GitSyncPlugin implements Plugin, InitializingBean { return; } - // 汇总结果 int success = 0, failed = 0; List damagedRepos = new ArrayList<>(); for (FileOperationHelper.ProjectPathInfo proj : projects) { @@ -972,29 +615,19 @@ public class GitSyncPlugin implements Plugin, InitializingBean { } } - /** 取消当前正在执行的同步(供 GUI 终止按钮调用) */ public void cancelSync() { this.syncCancelled = true; } - /** - * 获取损坏仓库的 clone URL(供窗口使用) - */ public String getCloneUrlFor(DamagedRepo dr) { Map baseUrlMap = buildBaseUrlMap(); return resolveCloneUrl(dr, baseUrlMap); } - /** - * 执行单个仓库重拉(供窗口调用) - */ public boolean recloneOne(DamagedRepo dr, String cloneUrl) { return doReclone(dr, cloneUrl); } - /** - * 清除损坏仓库缓存 - */ public void clearDamagedCache() { this.lastDamagedRepos = null; this.lastBranch = null; @@ -1004,38 +637,59 @@ public class GitSyncPlugin implements Plugin, InitializingBean { } catch (IOException ignored) {} } - /** - * 获取上次缓存的损坏仓库列表 - */ public List getLastDamagedRepos() { return lastDamagedRepos; } - /** - * 获取上次使用的分支 - */ public String getLastBranch() { return lastBranch; } - /** - * 获取项目路径信息 - */ public List getProjectPaths() throws IOException { return fileOperationHelper.getProjectPathsWithStatus(); } - // ==================== 内部类(公开,供窗口使用) ==================== + // ==================== 内部方法(插件管理用) ==================== + + private Map buildBaseUrlMap() { + Map map = new LinkedHashMap<>(); + try { + List projects = fileOperationHelper.getProjectPathsWithStatus(); + for (FileOperationHelper.ProjectPathInfo proj : projects) { + if (proj.getRemoteBaseUrl() != null && !proj.getRemoteBaseUrl().isEmpty()) { + map.put(normalizePath(proj.getPath()), proj.getRemoteBaseUrl()); + } + } + } catch (IOException e) { + log.error("读取项目路径文件失败", e); + } + return map; + } + + private String resolveCloneUrl(DamagedRepo dr, Map baseUrlMap) { + if (baseUrlMap != null) { + String normalizedPath = normalizePath(dr.path); + String baseUrl = baseUrlMap.get(normalizedPath); + if (baseUrl != null) { + return baseUrl; + } + } + if (dr.remoteUrl != null && !dr.remoteUrl.isEmpty()) { + return dr.remoteUrl; + } + return null; + } + + // ==================== 内部类 ==================== private enum SyncResult { OK, DAMAGED, FAILED; String output; String error; String remoteUrl; - boolean cloned; // 标记是否是通过 auto-clone 完成的,区别于正常 fetch+checkout+pull + boolean cloned; } - /** 损坏仓库信息 */ public static class DamagedRepo { public final String path; public final String name; diff --git a/src/main/java/com/example/workutils/biz/gitreview/GitReviewPlugin.java b/src/main/java/com/example/workutils/biz/gitreview/GitReviewPlugin.java index 1545d92..39fde6c 100644 --- a/src/main/java/com/example/workutils/biz/gitreview/GitReviewPlugin.java +++ b/src/main/java/com/example/workutils/biz/gitreview/GitReviewPlugin.java @@ -4,7 +4,6 @@ import com.example.workutils.system.config.WorkUtilsProperties; import com.example.workutils.system.plugin.Plugin; import com.example.workutils.system.plugin.PluginDescriptor; import com.example.workutils.system.plugin.VisualPluginFactory; -import com.example.workutils.system.support.JfxBootstrap; import javafx.scene.Parent; import javafx.scene.layout.Pane; import lombok.RequiredArgsConstructor; @@ -13,7 +12,6 @@ import org.springframework.stereotype.Component; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Scanner; import java.util.concurrent.CountDownLatch; @Slf4j @@ -30,11 +28,6 @@ public class GitReviewPlugin implements Plugin { .icon("⇄") .build(); - private static final String[] MENU_OPTIONS = { - "1. 打开可视化窗口", - "0. 返回上级菜单" - }; - private final GitReviewService service; private final WorkUtilsProperties properties; @@ -43,11 +36,6 @@ public class GitReviewPlugin implements Plugin { return DESCRIPTOR; } - @Override - public String[] getMenuOptions() { - return MENU_OPTIONS; - } - @Override public VisualPluginFactory getVisualFactory() { return new VisualPluginFactory() { @@ -98,22 +86,4 @@ public class GitReviewPlugin implements Plugin { } return null; } - - @Override - public void handleChoice(int choice, Scanner scanner) throws Exception { - if (choice == 1) { - System.out.println("正在打开 Git 分支评审窗口..."); - JfxBootstrap.await(); - if (JfxBootstrap.isFailed()) { - System.err.println("JavaFX 初始化失败"); - return; - } - CountDownLatch closeLatch = new CountDownLatch(1); - javafx.application.Platform.runLater(() -> new GitReviewWindow(service, closeLatch).show()); - closeLatch.await(); - System.out.println("Git 分支评审窗口已关闭"); - return; - } - printInvalidOption(); - } } diff --git a/src/main/java/com/example/workutils/biz/mobileuiauto/MobileUiAutomationPlugin.java b/src/main/java/com/example/workutils/biz/mobileuiauto/MobileUiAutomationPlugin.java index 3fdfe74..5c3a48b 100644 --- a/src/main/java/com/example/workutils/biz/mobileuiauto/MobileUiAutomationPlugin.java +++ b/src/main/java/com/example/workutils/biz/mobileuiauto/MobileUiAutomationPlugin.java @@ -5,14 +5,11 @@ import com.example.workutils.system.config.WorkUtilsProperties; import com.example.workutils.system.plugin.Plugin; import com.example.workutils.system.plugin.PluginDescriptor; import com.example.workutils.system.plugin.VisualPluginFactory; -import com.example.workutils.system.support.JfxBootstrap; -import javafx.application.Platform; import javafx.scene.Parent; import javafx.scene.layout.Pane; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import java.util.Scanner; import java.util.concurrent.CountDownLatch; @Component @@ -28,11 +25,6 @@ public class MobileUiAutomationPlugin implements Plugin { .icon("📱") .build(); - private static final String[] MENU_OPTIONS = { - "1. 打开手机UI自动化窗口", - "0. 返回上级菜单" - }; - private final MobileAutomationService automationService; private final WorkUtilsProperties properties; @@ -41,11 +33,6 @@ public class MobileUiAutomationPlugin implements Plugin { return DESCRIPTOR; } - @Override - public String[] getMenuOptions() { - return MENU_OPTIONS; - } - @Override public String getCommandName() { return "mobile-ui"; @@ -91,15 +78,6 @@ public class MobileUiAutomationPlugin implements Plugin { }; } - @Override - public void handleChoice(int choice, Scanner scanner) { - if (choice == 1) { - openWindow(); - } else { - printInvalidOption(); - } - } - @Override public void executeCommand(String... args) throws Exception { if (args == null || args.length == 0 || "check-env".equalsIgnoreCase(args[0])) { @@ -113,23 +91,4 @@ public class MobileUiAutomationPlugin implements Plugin { } System.out.println("支持命令: mobile-ui check-env | mobile-ui devices。用例运行请使用可视化窗口。"); } - - private void openWindow() { - try { - JfxBootstrap.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - CountDownLatch closeLatch = new CountDownLatch(1); - Platform.runLater(() -> { - MobileUiAutomationWindow window = new MobileUiAutomationWindow(automationService, properties, closeLatch); - window.show(); - }); - try { - closeLatch.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } } diff --git a/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportPlugin.java b/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportPlugin.java index 4a1c044..f26e51a 100644 --- a/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportPlugin.java +++ b/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportPlugin.java @@ -11,12 +11,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import java.util.Arrays; -import java.util.Scanner; import java.util.concurrent.CountDownLatch; /** - * MyBatis SQL导出插件(实现 Tool 接口) + * MyBatis SQL导出插件 */ @Slf4j @Component @@ -32,15 +30,6 @@ public class MyBatisExportPlugin implements Plugin { .icon("🗃️") .build(); - private static final String[] MENU_OPTIONS = { - "1. 导出所有SQL", - "2. 导出指定表名的所有SQL", - "3. 导出多个指定表名的所有SQL", - "4. 导出包含指定关键字的SQL", - "5. 导出指定SQL标签ID的SQL", - "0. 返回上级菜单" - }; - private final MyBatisExportService myBatisExportService; private final WorkUtilsProperties properties; @@ -54,11 +43,6 @@ public class MyBatisExportPlugin implements Plugin { return "sql"; } - @Override - public String[] getMenuOptions() { - return MENU_OPTIONS; - } - @Override public VisualPluginFactory getVisualFactory() { return new VisualPluginFactory() { @@ -82,7 +66,6 @@ public class MyBatisExportPlugin implements Plugin { latch = new CountDownLatch(1); window = new MyBatisExportWindow(myBatisExportService, properties, latch); } - // 如果之前被内嵌过,Scene root 已被掏空,需要把 embeddedRoot 挂回去 if (embeddedRoot != null && window.getStage().getScene().getRoot() != embeddedRoot) { window.getStage().getScene().setRoot(embeddedRoot); } @@ -129,71 +112,4 @@ public class MyBatisExportPlugin implements Plugin { } } } - - @Override - public void handleChoice(int choice, Scanner scanner) throws Exception { - switch (choice) { - case 1: exportAllSql(); break; - case 2: exportByTableName(scanner); break; - case 3: exportByTableNames(scanner); break; - case 4: exportByKeyword(scanner); break; - case 5: exportByTagId(scanner); break; - default: printInvalidOption(); break; - } - } - - private void exportAllSql() throws Exception { - printMenuTitle("导出所有SQL"); - log.info("开始导出所有SQL"); - myBatisExportService.exportSQL(); - printSuccess("导出所有SQL"); - } - - private void exportByTableName(Scanner scanner) throws Exception { - System.out.print("请输入表名: "); - String tableName = scanner.nextLine().trim(); - if (tableName.isEmpty()) { - System.out.println("表名不能为空"); - return; - } - log.info("按表名导出SQL: {}", tableName); - myBatisExportService.exportSQLByTable(tableName); - printSuccess("按表名导出SQL"); - } - - private void exportByTableNames(Scanner scanner) throws Exception { - System.out.print("请输入表名(多个用逗号分隔): "); - String tableNames = scanner.nextLine().trim(); - if (tableNames.isEmpty()) { - System.out.println("表名不能为空"); - return; - } - log.info("按多个表名导出SQL: {}", tableNames); - myBatisExportService.exportSQLByTables(Arrays.asList(tableNames.split(","))); - printSuccess("按多个表名导出SQL"); - } - - private void exportByKeyword(Scanner scanner) throws Exception { - System.out.print("请输入关键字: "); - String keyword = scanner.nextLine().trim(); - if (keyword.isEmpty()) { - System.out.println("关键字不能为空"); - return; - } - log.info("按关键字导出SQL: {}", keyword); - myBatisExportService.exportSQLByKeyword(keyword); - printSuccess("按关键字导出SQL"); - } - - private void exportByTagId(Scanner scanner) throws Exception { - System.out.print("请输入SQL标签ID: "); - String tagId = scanner.nextLine().trim(); - if (tagId.isEmpty()) { - System.out.println("标签ID不能为空"); - return; - } - log.info("按标签ID导出SQL: {}", tagId); - myBatisExportService.exportSQLByTagId(tagId); - printSuccess("按标签ID导出SQL"); - } } diff --git a/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorPlugin.java b/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorPlugin.java index 5aee249..2c5e2ed 100644 --- a/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorPlugin.java +++ b/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorPlugin.java @@ -6,7 +6,6 @@ import com.example.workutils.system.plugin.Plugin; import com.example.workutils.system.plugin.PluginDescriptor; import com.example.workutils.system.plugin.VisualPluginFactory; import com.example.workutils.system.support.FileOperationHelper; -import com.example.workutils.system.support.JfxBootstrap; import javafx.scene.Parent; import javafx.scene.layout.Pane; import lombok.RequiredArgsConstructor; @@ -18,7 +17,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Properties; -import java.util.Scanner; import java.util.concurrent.CountDownLatch; /** @@ -38,19 +36,11 @@ public class MyBatisGeneratorPlugin implements Plugin { .icon("🗄️") .build(); - private static final String[] MENU_OPTIONS = { - "1. UI可视化操作(选择表+目录)", - "2. 查看数据库所有表", - "3. 查看当前配置信息", - "0. 返回上级菜单" - }; - private final MyBatisGeneratorService generatorService; private final FileOperationHelper fileOperationHelper; private final WorkUtilsProperties properties; @Override public PluginDescriptor getDescriptor() { return DESCRIPTOR; } - @Override public String[] getMenuOptions() { return MENU_OPTIONS; } @Override public VisualPluginFactory getVisualFactory() { @@ -89,7 +79,6 @@ public class MyBatisGeneratorPlugin implements Plugin { latch = new CountDownLatch(1); window = new MyBatisGeneratorWindow(createCallback(), fileOperationHelper, properties, latch); } - // 如果之前被内嵌过,Scene root 已被掏空,需要把 embeddedRoot 挂回去 if (embeddedRoot != null && window.getStage().getScene().getRoot() != embeddedRoot) { window.getStage().getScene().setRoot(embeddedRoot); } @@ -104,13 +93,11 @@ public class MyBatisGeneratorPlugin implements Plugin { @Override public String checkConfig() { - // 1. 优先检查外部配置 String extUrl = properties.getEffectiveMybatisDbUrl(); String extUser = properties.getEffectiveMybatisDbUsername(); if (extUrl != null && !extUrl.isEmpty() && extUser != null && !extUser.isEmpty()) { - return null; // 外部配置完整 + return null; } - // 如果外部只配了部分 if (extUrl != null && !extUrl.isEmpty()) { return "MyBatis 外部配置中数据库用户名未设置。\n请通过「系统设置 > MyBatis 数据库」标签页补充。"; } @@ -118,9 +105,7 @@ public class MyBatisGeneratorPlugin implements Plugin { return "MyBatis 外部配置中数据库 URL 未设置。\n请通过「系统设置 > MyBatis 数据库」标签页补充。"; } - // 2. 回退检查 data/ 外部配置 → classpath 内置配置 Properties props = new Properties(); - // 2a. 先尝试 classpath 内置值(作为基线) try (InputStream is = getClass().getClassLoader() .getResourceAsStream("mybatis-generator.properties")) { if (is != null) { @@ -129,7 +114,6 @@ public class MyBatisGeneratorPlugin implements Plugin { } catch (Exception e) { log.warn("无法读取 classpath 内置配置", e); } - // 2b. 用 data/ 外部配置覆盖 Path dataFile = properties.getMybatisGeneratorFilePath(); if (Files.exists(dataFile)) { try (InputStream is = new java.io.FileInputStream(dataFile.toFile())) { @@ -152,109 +136,4 @@ public class MyBatisGeneratorPlugin implements Plugin { } return null; } - - @Override - public void handleChoice(int choice, Scanner scanner) throws Exception { - switch (choice) { - case 1: openVisualUI(); break; - case 2: showAllTables(); break; - case 3: showConfiguration(); break; - default: printInvalidOption(); break; - } - } - - // ==================== UI 可视化操作 ==================== - - public void openVisualUI() { - try { - JfxBootstrap.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - if (JfxBootstrap.isFailed()) { - System.err.println("JavaFX 初始化失败,请确认 JDK 8 包含 JavaFX 运行时"); - return; - } - - System.out.println("正在打开 MyBatis 逆向工程窗口..."); - - CountDownLatch windowClose = new CountDownLatch(1); - - MyBatisGeneratorWindow.GeneratorCallback cb = new MyBatisGeneratorWindow.GeneratorCallback() { - @Override - public List queryTables(String pattern) throws Exception { - return generatorService.queryTablesByPattern(pattern); - } - @Override - public String getDbUrl() { - return generatorService.getDatabaseUrl(); - } - @Override - public String getDbUsername() { - return generatorService.getDatabaseUsername(); - } - @Override - public String generate(List tables, String modelDir, String xmlDir, String mapperDir) throws Exception { - return generatorService.generateByTableNames(tables, true, modelDir, xmlDir, mapperDir); - } - @Override - public List getDatasourceNames() { - return generatorService.getDatasourceNames(); - } - @Override - public void switchDatasource(String name) { - generatorService.switchDatasource(name); - } - @Override - public String getCurrentDatasourceName() { - return generatorService.getCurrentDatasourceName(); - } - }; - - javafx.application.Platform.runLater(() -> { - try { - MyBatisGeneratorWindow window = new MyBatisGeneratorWindow( - cb, fileOperationHelper, properties, windowClose); - window.show(); - } catch (Exception e) { - System.err.println("窗口创建失败: " + e.getMessage()); - e.printStackTrace(); - windowClose.countDown(); - } - }); - - try { - windowClose.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - System.out.println("MyBatis 逆向工程窗口已关闭"); - } - - private void showAllTables() throws Exception { - List tables = generatorService.queryAllTables(); - System.out.println(); - System.out.println("========== 数据库所有表(共 " + tables.size() + " 张)=========="); - for (int i = 0; i < tables.size(); i++) { - System.out.println(" " + (i + 1) + ". " + tables.get(i)); - } - System.out.println("================================="); - } - - private void showConfiguration() { - System.out.println(); - System.out.println("========== 当前配置信息 =========="); - System.out.println("数据库URL: " + maskPassword(generatorService.getDatabaseUrl())); - System.out.println("数据库用户: " + generatorService.getDatabaseUsername()); - System.out.println("Model包路径: " + generatorService.getModelPackage()); - System.out.println("Mapper接口包路径: " + generatorService.getMapperPackage()); - System.out.println("SQL Mapper包路径: " + generatorService.getSqlMapperPackage()); - System.out.println("================================="); - } - - private String maskPassword(String url) { - if (url == null) return ""; - return url.replaceAll("password=[^&]*", "password=***"); - } } diff --git a/src/main/java/com/example/workutils/biz/scanner/ScannerPlugin.java b/src/main/java/com/example/workutils/biz/scanner/ScannerPlugin.java index ea7f92c..5fac8f8 100644 --- a/src/main/java/com/example/workutils/biz/scanner/ScannerPlugin.java +++ b/src/main/java/com/example/workutils/biz/scanner/ScannerPlugin.java @@ -13,11 +13,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import java.util.Scanner; import java.util.concurrent.CountDownLatch; /** - * 扫描工具插件(实现 Tool 接口) + * 扫描工具插件 */ @Slf4j @Component @@ -33,11 +32,6 @@ public class ScannerPlugin implements Plugin { .icon("🔍") .build(); - private static final String[] MENU_OPTIONS = { - "1. 关键词扫描", - "0. 返回上级菜单" - }; - private final ScannerService scannerService; private final FileOperationHelper fileOperationHelper; private final OutputHelper outputHelper; @@ -53,11 +47,6 @@ public class ScannerPlugin implements Plugin { return "scan"; } - @Override - public String[] getMenuOptions() { - return MENU_OPTIONS; - } - @Override public VisualPluginFactory getVisualFactory() { return new VisualPluginFactory() { @@ -83,7 +72,6 @@ public class ScannerPlugin implements Plugin { window = new ScannerWindow(scannerService, fileOperationHelper, outputHelper, properties, latch); } - // 如果之前被内嵌过,Scene root 已被掏空,需要把 embeddedRoot 挂回去 if (embeddedRoot != null && window.getStage().getScene().getRoot() != embeddedRoot) { window.getStage().getScene().setRoot(embeddedRoot); } @@ -125,34 +113,4 @@ public class ScannerPlugin implements Plugin { System.out.println("扫描完成,结果已保存到: " + outputFile); } - - @Override - public void handleChoice(int choice, Scanner scanner) throws Exception { - switch (choice) { - case 1: - scanByKeyword(scanner); - break; - default: - printInvalidOption(); - break; - } - } - - private void scanByKeyword(Scanner scanner) throws Exception { - printMenuTitle("关键词扫描"); - log.info("开始关键词扫描"); - - System.out.print("请输入关键词: "); - String keyword = scanner.nextLine().trim(); - - if (keyword.isEmpty()) { - System.out.println("关键词不能为空"); - return; - } - - log.info("扫描关键词: {}", keyword); - scannerService.scanKeywordsWithDefaultName(keyword); - - printSuccess("关键词扫描"); - } } 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 a7599f1..e814a3c 100644 --- a/src/main/java/com/example/workutils/system/config/ExternalConfigManager.java +++ b/src/main/java/com/example/workutils/system/config/ExternalConfigManager.java @@ -72,9 +72,6 @@ public class ExternalConfigManager { @Deprecated public static final String KEY_FONT_BUTTON = KEY_FONT_BTN; // KEY_FONT_BODY 和 KEY_FONT_MONO 键名不变,直接复用 - // ---- 主题 ---- - public static final String KEY_THEME = "theme"; // light | dark - // ---- 默认数据源 ---- public static final String KEY_DEFAULT_DATASOURCE = "default.datasource"; @@ -93,9 +90,6 @@ public class ExternalConfigManager { KEY_FONT_BODY, KEY_FONT_SMALL, KEY_FONT_CAPTION, KEY_FONT_MONO, KEY_FONT_BTN_LG, KEY_FONT_BTN, KEY_FONT_BTN_SM }); - SECTIONS.put("===== 界面主题 =====", new String[]{ - KEY_THEME - }); SECTIONS.put("===== 默认数据源 =====", new String[]{ KEY_DEFAULT_DATASOURCE }); 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 6cd29e2..d7a287c 100644 --- a/src/main/java/com/example/workutils/system/config/SettingsService.java +++ b/src/main/java/com/example/workutils/system/config/SettingsService.java @@ -7,15 +7,14 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; -import java.util.Scanner; import java.util.function.Consumer; /** - * 系统设置服务 —— 通过可视化 GUI 或控制台修改配置 + * 系统设置服务 —— 通过可视化 GUI 修改配置 *

* 负责: *

    - *
  • 打开 JavaFX 设置窗口(主方式)
  • + *
  • 打开 JavaFX 设置窗口
  • *
  • 配置变更后通知调用方
  • *
*/ @@ -45,25 +44,6 @@ public class SettingsService { pluginManager, onConfigChanged, navItemsRefresher); } - /** - * 打开系统设置 GUI 窗口 - * @param scanner 控制台 Scanner(GUI 模式不需要) - * @param onConfigChanged 配置变更回调,null 表示不需要通知 - */ - public void showSettings(Scanner scanner, Runnable onConfigChanged) { - try { - SettingsWindow window = new SettingsWindow(properties, configManager, datasourceConfigManager, - pluginManager, onConfigChanged, null); - window.showAndWait(); - } catch (Exception e) { - log.error("打开设置窗口失败,回退到控制台模式", e); - System.out.println("设置窗口打开失败: " + e.getMessage()); - System.out.println("请直接在外部编辑 workutils.config 文件:"); - System.out.println(" " + configManager.getConfigFile().toAbsolutePath()); - System.out.println("编辑后重启程序即可生效。"); - } - } - /** * 通知导航栏刷新(供外部调用) */ 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 e311b8a..ac9e73d 100644 --- a/src/main/java/com/example/workutils/system/config/SettingsWindow.java +++ b/src/main/java/com/example/workutils/system/config/SettingsWindow.java @@ -791,7 +791,7 @@ public class SettingsWindow { titleLabels.add(fontTitle); root.getChildren().add(fontTitle); - Label note = new Label("各层级有独立推荐范围,点击「保存字体 + 主题设置」后写入 workutils.config 并全局生效"); + Label note = new Label("各层级有独立推荐范围,点击「保存字体设置」后写入 workutils.config 并全局生效"); note.setFont(Theme.small(fontSmall)); note.getStyleClass().add(Theme.CSS_HINT_TEXT); hintLabels.add(note); @@ -831,7 +831,7 @@ public class SettingsWindow { root.getChildren().add(new Separator()); - Button btnSaveFont = new Button("保存字体 + 主题设置"); + Button btnSaveFont = new Button("保存字体设置"); btnSaveFont.setFont(font(fontBtn)); btnSaveFont.getStyleClass().add(Theme.CSS_BTN_SUCCESS); allButtons.add(btnSaveFont); @@ -911,21 +911,18 @@ public class SettingsWindow { props.setProperty(ExternalConfigManager.KEY_FONT_BTN_LG, String.valueOf(fontBtnLg)); props.setProperty(ExternalConfigManager.KEY_FONT_BTN, String.valueOf(fontBtn)); props.setProperty(ExternalConfigManager.KEY_FONT_BTN_SM, String.valueOf(fontBtnSm)); - props.setProperty(ExternalConfigManager.KEY_THEME, "light"); try { configManager.saveSectioned(props); properties.mergeExternal(props); - // 应用主题 - Theme.Mode mode = Theme.Mode.LIGHT; - Theme.applyTheme(mode); + // 刷新 CSS if (stage != null && stage.getScene() != null) { - Theme.refreshCustomCss(stage.getScene()); + Theme.applyCustomCss(stage.getScene()); } 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()); 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 1863408..1c36b1a 100644 --- a/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java +++ b/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java @@ -90,9 +90,6 @@ public class WorkUtilsProperties { @Deprecated public int getFontButton() { return fontBtn; } // getFontBody() / getFontMono() 由 Lombok 按新字段名自动生成 - // ---- 主题 ---- - private String theme = "light"; // light | dark - // ---- 默认数据源 ---- private String defaultDatasource; @@ -180,9 +177,6 @@ public class WorkUtilsProperties { applyIntIfPresent(external, ExternalConfigManager.KEY_FONT_BTN, v -> this.fontBtn = v); applyIntIfPresent(external, ExternalConfigManager.KEY_FONT_BTN_SM, v -> this.fontBtnSm = v); - // 主题 - applyIfPresent(external, ExternalConfigManager.KEY_THEME, v -> this.theme = v); - // 默认数据源 applyIfPresent(external, ExternalConfigManager.KEY_DEFAULT_DATASOURCE, v -> this.defaultDatasource = v); @@ -209,7 +203,6 @@ public class WorkUtilsProperties { fontBtnLg = 16; fontBtn = 14; fontBtnSm = 12; - theme = "light"; defaultDatasource = null; log.info("配置已重置为默认值"); } diff --git a/src/main/java/com/example/workutils/system/plugin/Plugin.java b/src/main/java/com/example/workutils/system/plugin/Plugin.java index 1f9f70c..0d9ae3f 100644 --- a/src/main/java/com/example/workutils/system/plugin/Plugin.java +++ b/src/main/java/com/example/workutils/system/plugin/Plugin.java @@ -1,9 +1,5 @@ package com.example.workutils.system.plugin; -import com.example.workutils.system.support.ConsoleHelper; - -import java.util.Scanner; - /** * 工具接口 —— 所有 biz 模块的顶级契约 *

@@ -11,8 +7,8 @@ import java.util.Scanner; *

    *
  • 插件元数据 — {@link #getDescriptor()} 声明 id/name/version/description
  • *
  • 生命周期 — {@link #onLoad(PluginContext)} / {@link #onUnload()}
  • - *
  • 交互模式 — {@link #execute(Scanner)} 模板方法驱动,子类实现 {@link #handleChoice(int, Scanner)}
  • *
  • 命令行模式 — {@link #getCommandName()} + {@link #executeCommand(String...)}
  • + *
  • 可视化窗口 — {@link #getVisualFactory()}
  • *
*/ public interface Plugin { @@ -30,40 +26,11 @@ public interface Plugin { return getDescriptor().getName(); } - /** 子菜单选项,返回空数组表示无子菜单 */ - String[] getMenuOptions(); - /** 命令行命令名(如 "sql", "scan"),返回空字符串表示不支持命令行模式 */ default String getCommandName() { return ""; } - // ==================== 交互模式(模板方法) ==================== - - default void execute(Scanner scanner) { - while (true) { - ConsoleHelper.printMenuTitle(getName()); - ConsoleHelper.displayMenu(getMenuOptions()); - - int choice = ConsoleHelper.readChoice(scanner); - scanner.nextLine(); - - if (choice == 0) { - return; - } - - try { - handleChoice(choice, scanner); - } catch (Exception e) { - ConsoleHelper.printError(e.getMessage()); - } - } - } - - /** 处理子菜单选择,子类实现具体分发逻辑 */ - default void handleChoice(int choice, Scanner scanner) throws Exception { - } - // ==================== 命令行模式 ==================== default void executeCommand(String... args) throws Exception { @@ -100,26 +67,4 @@ public interface Plugin { default VisualPluginFactory getVisualFactory() { return null; } - - // ==================== 控制台输出辅助方法 ==================== - - default void printMenuTitle(String title) { - ConsoleHelper.printMenuTitle(title); - } - - default void displayMenu(String[] options) { - ConsoleHelper.displayMenu(options); - } - - default void printSuccess(String operation) { - ConsoleHelper.printSuccess(operation); - } - - default void printError(String message) { - ConsoleHelper.printError(message); - } - - default void printInvalidOption() { - ConsoleHelper.printInvalidOption(); - } } 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 bc9c9a1..7f94db6 100644 --- a/src/main/java/com/example/workutils/system/plugin/PluginManager.java +++ b/src/main/java/com/example/workutils/system/plugin/PluginManager.java @@ -1,25 +1,20 @@ package com.example.workutils.system.plugin; -import com.example.workutils.system.config.SettingsService; import com.example.workutils.system.config.WorkUtilsProperties; -import com.example.workutils.system.support.ConsoleHelper; import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; -import java.io.IOException; import java.util.*; /** - * 插件管理器 —— 插件生命周期管理 + 注册中心 + 主菜单 + * 插件管理器 —— 插件生命周期管理 + 注册中心 *

* 统一管理所有 {@link Plugin} 实例的: *

    *
  • 生命周期 — load / unload / enable / disable
  • *
  • 注册发现 — 按 id / command 查找
  • *
  • 启用过滤 — 白名单 / 黑名单
  • - *
  • 主菜单 — 交互式菜单循环
  • *
  • 命令行分发 — 按 command 名路由
  • *
*/ @@ -27,11 +22,8 @@ import java.util.*; @Component public class PluginManager { - private static final int EXIT_CODE = 0; - private final List allPlugins; private final WorkUtilsProperties properties; - private final SettingsService settingsService; private PluginContext pluginContext; private List enabledPlugins; @@ -39,11 +31,9 @@ public class PluginManager { private Map pluginByCommandMap; private Map pluginStates; - public PluginManager(List plugins, WorkUtilsProperties properties, - @Lazy SettingsService settingsService) { + public PluginManager(List plugins, WorkUtilsProperties properties) { this.allPlugins = plugins; this.properties = properties; - this.settingsService = settingsService; } @PostConstruct @@ -84,86 +74,11 @@ public class PluginManager { } } - // 按 order 升序排列菜单:1.文本对比 2.MyBatis逆向工程 3.Git仓库同步 4.扫描功能 5.API接口文档 6.MyBatis SQL导出 + // 按 order 升序排列菜单 enabledPlugins.sort(Comparator.comparingInt(p -> p.getDescriptor().getOrder())); log.info("插件加载完成: {}/{} 已激活", enabledPlugins.size(), allPlugins.size()); } - // ==================== 主菜单 ==================== - - /** 启动主菜单交互循环 */ - public void runMainMenu(Scanner scanner) { - log.info("启动主菜单"); - while (true) { - ConsoleHelper.printMenuTitle("主菜单"); - displayMainMenu(); - - int choice = ConsoleHelper.readChoiceOrChar(scanner); - - if (choice == EXIT_CODE) { - log.info("用户退出系统"); - System.out.println("感谢使用,再见!"); - shutdown(); - break; - } - - // '?' 字符(readChoiceOrChar 将单字母转成负ASCII码) - if (choice == -'?') { - settingsService.showSettings(scanner, pluginContext::resetConfigChecks); - // JavaFX 后台线程在 Windows 上可能污染 System.in, - // 清理残留字节并重建 Scanner,确保后续输入正常 - flushSystemIn(); - scanner = new Scanner(System.in); - continue; - } - - dispatchPlugin(choice, scanner); - } - } - - private void displayMainMenu() { - System.out.println("请选择操作:"); - for (int i = 0; i < enabledPlugins.size(); i++) { - PluginDescriptor desc = enabledPlugins.get(i).getDescriptor(); - System.out.println((i + 1) + ". " + desc.getName() + " [v" + desc.getVersion() + "]"); - } - System.out.println("?. 系统设置"); - System.out.println(EXIT_CODE + ". 退出系统"); - } - - private void dispatchPlugin(int choice, Scanner scanner) { - int index = choice - 1; - if (index >= 0 && index < enabledPlugins.size()) { - Plugin plugin = enabledPlugins.get(index); - PluginDescriptor desc = plugin.getDescriptor(); - - // 首次使用时检查配置 - if (!pluginContext.isConfigChecked(desc.getId())) { - String configError = plugin.checkConfig(); - if (configError != null) { - System.out.println(); - System.out.println("========== 配置检查 =========="); - System.out.println("插件 [" + desc.getName() + "] 配置异常:"); - System.out.println(configError); - System.out.println(); - System.out.println("请进入「系统设置(?)」进行配置后再使用。"); - System.out.println("=============================="); - return; - } - pluginContext.markConfigChecked(desc.getId()); - } - - try { - plugin.execute(scanner); - } catch (Exception e) { - log.error("插件执行失败: " + desc, e); - System.out.println("插件执行失败: " + e.getMessage()); - } - } else { - ConsoleHelper.printInvalidOption(); - } - } - // ==================== 查找方法 ==================== /** 获取所有已启用插件(只读) */ @@ -195,7 +110,7 @@ public class PluginManager { return result; } - // ==================== 生命周期管理(预留) ==================== + // ==================== 生命周期管理 ==================== /** 手动启用插件 */ public boolean enablePlugin(String pluginId) { @@ -237,27 +152,6 @@ public class PluginManager { // ==================== 内部方法 ==================== - private void shutdown() { - for (Plugin plugin : allPlugins) { - try { - plugin.onUnload(); - } catch (Exception e) { - log.warn("插件卸载异常: " + plugin.getDescriptor(), e); - } - } - } - - /** 清空 System.in 中的残留字节(JavaFX GUI 可能污染控制台输入流) */ - private static void flushSystemIn() { - try { - if (System.in.available() > 0) { - byte[] buf = new byte[System.in.available()]; - System.in.read(buf); - } - } catch (IOException ignored) { - } - } - private Plugin findPluginById(String id) { for (Plugin p : allPlugins) { if (p.getId().equals(id)) { diff --git a/src/main/java/com/example/workutils/system/support/ConsoleHelper.java b/src/main/java/com/example/workutils/system/support/ConsoleHelper.java deleted file mode 100644 index ec84296..0000000 --- a/src/main/java/com/example/workutils/system/support/ConsoleHelper.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.example.workutils.system.support; - -import java.util.Scanner; - -/** - * 控制台交互工具类 - */ -public final class ConsoleHelper { - - private ConsoleHelper() { - } - - public static void printMenuTitle(String title) { - System.out.println(); - System.out.println("========== " + title + " =========="); - } - - public static void displayMenu(String[] options) { - System.out.println("请选择操作:"); - for (String option : options) { - System.out.println(option); - } - } - - public static int readChoice(Scanner scanner) { - try { - System.out.print("请输入选项编号: "); - return scanner.nextInt(); - } catch (Exception e) { - return -1; - } - } - - /** - * 读取用户输入,支持数字或字母(如 S) - * @return 数字选择(>=0),或 -1 表示字母输入 - */ - public static int readChoiceOrChar(Scanner scanner) { - System.out.print("请输入选项编号: "); - String input = scanner.nextLine().trim(); - if (input.isEmpty()) { - return -1; - } - // 尝试解析为数字 - try { - return Integer.parseInt(input); - } catch (NumberFormatException e) { - // 单字母输入,转为负的 char 值(特殊通道) - if (input.length() == 1) { - return -input.toUpperCase().charAt(0); - } - return -1; - } - } - - public static void printSuccess(String operation) { - System.out.println(operation + "完成!"); - } - - public static void printError(String message) { - System.out.println("操作失败: " + message); - } - - public static void printInvalidOption() { - System.out.println("无效选项"); - } - - /** - * 读取非空字符串 - */ - public static String readLine(Scanner scanner, String prompt) { - System.out.print(prompt); - return scanner.nextLine().trim(); - } -} 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 d1f456a..30aa8c6 100644 --- a/src/main/java/com/example/workutils/system/ui/MainWindow.java +++ b/src/main/java/com/example/workutils/system/ui/MainWindow.java @@ -306,8 +306,10 @@ public class MainWindow { iconLabel.setPrefWidth(24); // 固定图标区域宽度 iconLabel.setAlignment(Pos.CENTER_LEFT); iconLabel.setFont(rfont(13)); + iconLabel.setTextFill(Color.WHITE); Label nameLabel = new Label(item.name); nameLabel.setFont(rfont(13)); + nameLabel.setTextFill(Color.WHITE); content.getChildren().addAll(iconLabel, nameLabel); btn.setGraphic(content); btn.setText(""); @@ -327,23 +329,38 @@ public class MainWindow { private void selectNavItem(NavItem item) { this.activeNavItem = item; + Color white = Color.WHITE; + Color activeColor = Color.web("#60a5fa"); for (int i = 0; i < navItems.size(); i++) { Button btn = navButtons.get(i); if (navItems.get(i) == item) { btn.getStyleClass().remove(Theme.CSS_NAV_BTN); btn.getStyleClass().add(Theme.CSS_NAV_BTN_ACTIVE); - btn.setStyle(""); // 清除内联 + btn.setStyle(""); + setNavButtonTextColor(btn, activeColor); } else { btn.getStyleClass().remove(Theme.CSS_NAV_BTN_ACTIVE); if (!btn.getStyleClass().contains(Theme.CSS_NAV_BTN)) { btn.getStyleClass().add(Theme.CSS_NAV_BTN); } btn.setStyle(""); + setNavButtonTextColor(btn, white); } } showEmbeddedFor(item); } + /** 递归设置导航按钮内所有 Label 的文字颜色 */ + private void setNavButtonTextColor(Button btn, Color color) { + if (btn.getGraphic() instanceof HBox) { + for (javafx.scene.Node child : ((HBox) btn.getGraphic()).getChildren()) { + if (child instanceof Label) { + ((Label) child).setTextFill(color); + } + } + } + } + // ==================== 欢迎页 ==================== private void showWelcomePage() { @@ -441,7 +458,7 @@ public class MainWindow { // ==================== 主题刷新(供设置窗口调用) ==================== public void refreshTheme() { - Theme.refreshCustomCss(stage.getScene()); + Theme.applyCustomCss(stage.getScene()); } // ==================== 内嵌条目 ==================== diff --git a/src/main/java/com/example/workutils/system/ui/Theme.java b/src/main/java/com/example/workutils/system/ui/Theme.java index 859f576..3096781 100644 --- a/src/main/java/com/example/workutils/system/ui/Theme.java +++ b/src/main/java/com/example/workutils/system/ui/Theme.java @@ -1,6 +1,5 @@ package com.example.workutils.system.ui; -import atlantafx.base.theme.PrimerDark; import atlantafx.base.theme.PrimerLight; import javafx.application.Application; import javafx.scene.Scene; @@ -15,40 +14,26 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** - * 工集 UI 设计系统 —— AtlantaFX 主题管理 + 字体/间距 Token + * 工集 UI 设计系统 —— AtlantaFX 亮色主题 *

- * CSS 样式类定义在 resources/css/light-theme.css 和 dark-theme.css 中。 + * 仅支持亮色主题,CSS 样式类定义在 resources/css/light-theme.css 中。 * 使用时通过 {@code getStyleClass().add("class-name")} 引用,而非 setStyle()。 */ public final class Theme { private Theme() {} - // ==================== 主题模式 ==================== - - public enum Mode { LIGHT, DARK } - - private static volatile Mode currentMode = Mode.LIGHT; private static volatile String currentCustomCss = "css/light-theme.css"; /** 缓存已加载的 CSS data: URI,避免重复读取 */ private static final Map cssFileCache = new ConcurrentHashMap<>(); - public static Mode getCurrentMode() { return currentMode; } - /** - * 切换全局主题,同步更新 AtlantaFX 基础主题 + 自定义 CSS + * 初始化全局主题(仅亮色)。 + * 必须在 JavaFX Application Thread 上调用,且在任何 Scene 创建之前。 */ - public static void applyTheme(Mode mode) { - currentMode = mode; - String atlantaTheme; - if (mode == Mode.DARK) { - atlantaTheme = new PrimerDark().getUserAgentStylesheet(); - currentCustomCss = "css/dark-theme.css"; - } else { - atlantaTheme = new PrimerLight().getUserAgentStylesheet(); - currentCustomCss = "css/light-theme.css"; - } + public static void initTheme() { + String atlantaTheme = new PrimerLight().getUserAgentStylesheet(); Application.setUserAgentStylesheet(atlantaTheme); } @@ -63,7 +48,7 @@ public final class Theme { System.err.println("[Theme] 警告: 找不到 CSS 资源 " + currentCustomCss + ",将使用默认样式"); return; } - scene.getStylesheets().removeIf(s -> s.contains("light-theme.css") || s.contains("dark-theme.css")); + scene.getStylesheets().removeIf(s -> s.contains("light-theme.css")); scene.getStylesheets().add(dataUri); } @@ -85,14 +70,6 @@ public final class Theme { } } - /** - * 刷新场景的自定义 CSS(主题切换时调用) - */ - public static void refreshCustomCss(Scene scene) { - if (scene == null) return; - applyCustomCss(scene); - } - // ==================== 色彩系统(供代码中动态着色使用) ==================== public static final Color PRIMARY = Color.web("#2563eb"); @@ -113,7 +90,7 @@ public final class Theme { public static final Color BORDER = Color.web("#e2e8f0"); public static final Color NAV_BG = Color.web("#1e293b"); - public static final Color NAV_TEXT = Color.web("#cbd5e1"); + public static final Color NAV_TEXT = Color.web("#ffffff"); // ==================== CSS 类名常量(统一引用,避免魔法字符串) ==================== @@ -171,16 +148,11 @@ public final class Theme { // ==================== 字体系统(保留,各窗口通过代码动态设置) ==================== - // ---- 标题层级 ---- public static Font h1(int size) { return Font.font("Microsoft YaHei", FontWeight.BOLD, size); } public static Font h2(int size) { return Font.font("Microsoft YaHei", FontWeight.BOLD, size); } public static Font h3(int size) { return Font.font("Microsoft YaHei", FontWeight.BOLD, size); } - - // ---- 正文层级 ---- public static Font body(int size) { return Font.font("Microsoft YaHei", size); } public static Font small(int size) { return Font.font("Microsoft YaHei", size); } - - // ---- 通用工厂(兼容旧调用) ---- public static Font base(int size) { return Font.font("Microsoft YaHei", size); } public static Font bold(int size) { return Font.font("Microsoft YaHei", FontWeight.BOLD, size); } public static Font mono(int size) { return Font.font("Consolas", size); } diff --git a/src/main/resources/css/dark-theme.css b/src/main/resources/css/dark-theme.css deleted file mode 100644 index 2c7eda2..0000000 --- a/src/main/resources/css/dark-theme.css +++ /dev/null @@ -1,293 +0,0 @@ -/* ============================================================ - WorkUtils Dark Theme — 基于 AtlantaFX PrimerDark - 所有颜色变量覆盖为暗色系 - ============================================================ */ - -.root { - -workutils-bg: #0d1117; - -workutils-surface: #161b22; - -workutils-card: #21262d; - -workutils-border: #30363d; - -workutils-text: #c9d1d9; - -workutils-text-secondary: #8b949e; - -workutils-text-muted: #6e7681; - -workutils-primary: #58a6ff; - -workutils-danger: #f85149; - -workutils-warning: #d29922; - -workutils-success: #3fb950; - - -workutils-nav-bg: #0d1117; - -workutils-nav-text: #8b949e; - -workutils-nav-hover: rgba(255,255,255,0.06); - -workutils-nav-active: rgba(88,166,255,0.15); - -workutils-nav-bar: #58a6ff; -} - -/* ---- 导航栏 ---- */ -.nav-bar { - -fx-background-color: -workutils-nav-bg; -} - -.nav-brand { - -fx-text-fill: white; - -fx-padding: 18 14 18 14; - -fx-font-weight: bold; -} - -.nav-separator { - -fx-background-color: rgba(255,255,255,0.08); -} - -.nav-btn { - -fx-background-color: transparent; - -fx-text-fill: -workutils-nav-text; - -fx-background-radius: 0; - -fx-border-width: 0 0 0 3; - -fx-border-color: transparent; - -fx-padding: 12 16 12 16; - -fx-alignment: CENTER-LEFT; - -fx-cursor: hand; - -fx-max-width: Infinity; -} - -.nav-btn .text { - /* 固定图标区域宽度,确保菜单文字左对齐 */ - -fx-display: inline-flex; - -fx-alignment: center-left; -} - -.nav-btn:hover { - -fx-background-color: -workutils-nav-hover; - -fx-text-fill: #e6edf3; -} - -.nav-btn-active { - -fx-background-color: -workutils-nav-active; - -fx-text-fill: white; - -fx-border-color: -workutils-nav-bar; -} - -.nav-version { - -fx-text-fill: -workutils-text-muted; - -fx-padding: 10 14 10 14; -} - -/* ---- 顶栏 ---- */ -.header-bar { - -fx-background-color: -workutils-surface; - -fx-border-width: 0 0 1 0; - -fx-border-color: -workutils-border; - -fx-pref-height: 48; -} - -/* ---- 内容区 ---- */ -.content-area { - -fx-background-color: -workutils-bg; -} - -/* ---- 卡片 ---- */ -.card { - -fx-background-color: -workutils-surface; - -fx-background-radius: 6; - -fx-border-color: -workutils-border; - -fx-border-radius: 6; - -fx-border-width: 1; -} - -/* ---- 按钮增强 ---- */ -.btn-primary { - -fx-background-color: -workutils-primary; - -fx-text-fill: #0d1117; - -fx-font-weight: bold; - -fx-background-radius: 4; - -fx-cursor: hand; -} - -.btn-primary:hover { - -fx-background-color: derive(-workutils-primary, 12%); -} - -.btn-danger { - -fx-background-color: -workutils-danger; - -fx-text-fill: white; - -fx-background-radius: 4; - -fx-cursor: hand; -} - -.btn-danger:hover { - -fx-background-color: derive(-workutils-danger, -12%); -} - -.btn-success { - -fx-background-color: -workutils-success; - -fx-text-fill: #0d1117; - -fx-background-radius: 4; - -fx-cursor: hand; -} - -.btn-warning { - -fx-background-color: -workutils-warning; - -fx-text-fill: #0d1117; - -fx-background-radius: 4; - -fx-cursor: hand; -} - -.btn-small { - -fx-background-color: -workutils-card; - -fx-text-fill: -workutils-text-secondary; - -fx-background-radius: 4; - -fx-padding: 4 12 4 12; - -fx-cursor: hand; -} - -.btn-small:hover { - -fx-background-color: -workutils-border; -} - -/* ---- 文本域 ---- */ -.text-field-default { - -fx-border-color: -workutils-border; - -fx-border-width: 1; - -fx-border-radius: 4; - -fx-background-radius: 4; - -fx-padding: 6 10; -} - -.text-area-default { - -fx-border-color: -workutils-border; - -fx-border-width: 1; - -fx-border-radius: 4; - -fx-background-radius: 4; - -fx-background-insets: 0; - -fx-padding: 8; -} - -/* ---- 日志区 ---- */ -.log-area { - -fx-control-inner-background: #161b22; - -fx-text-fill: -workutils-text; - -fx-border-color: -workutils-border; - -fx-border-width: 1; - -fx-border-radius: 4; - -fx-background-radius: 4; - -fx-background-color: -workutils-surface; -} - -/* ---- 状态栏 ---- */ -.status-bar { - -fx-background-color: -workutils-card; - -fx-border-width: 1 0 0 0; - -fx-border-color: -workutils-border; - -fx-text-fill: -workutils-text-secondary; - -fx-padding: 8 12 8 12; -} - -/* ---- DB 信息栏 ---- */ -.db-info-bar { - -fx-background-color: rgba(88,166,255,0.12); - -fx-background-radius: 6; -} - -.db-info-box { - -fx-border-color: -workutils-border; - -fx-border-width: 1; - -fx-border-radius: 4; - -fx-background-color: -workutils-surface; - -fx-background-radius: 4; -} - -/* ---- 表格 ---- */ -.table-default { - -fx-background-color: -workutils-surface; - -fx-border-color: -workutils-border; - -fx-border-width: 1; - -fx-border-radius: 4; -} - -/* ---- 标题 ---- */ -.section-title { - -fx-font-weight: bold; - -fx-text-fill: -workutils-text; -} - -.hint-text { - -fx-text-fill: -workutils-text-secondary; -} - -.muted-text { - -fx-text-fill: -workutils-text-muted; -} - -.danger-text { - -fx-text-fill: -workutils-danger; -} - -.warning-text { - -fx-text-fill: -workutils-warning; -} - -/* ---- 欢迎页 ---- */ -.welcome-page { - -fx-background-color: -workutils-bg; -} - -.welcome-heading { - -fx-font-weight: bold; - -fx-text-fill: -workutils-text; -} - -.welcome-desc { - -fx-text-fill: -workutils-text-secondary; -} - -/* ---- 分割线 ---- */ -.divider { - -fx-background-color: -workutils-border; -} - -/* ---- 切换按钮组 ---- */ -.toggle-btn { - -fx-background-radius: 4; - -fx-cursor: hand; - -fx-padding: 6 16; -} - -.toggle-btn:selected { - -fx-background-color: -workutils-primary; - -fx-text-fill: #0d1117; -} - -.toggle-btn:not(:selected) { - -fx-background-color: -workutils-card; - -fx-text-fill: -workutils-text; - -fx-border-color: -workutils-border; - -fx-border-width: 1; - -fx-border-radius: 4; -} - -/* ---- ComboBox ---- */ -.combo-default { - -fx-border-color: -workutils-border; - -fx-border-width: 1; - -fx-border-radius: 4; - -fx-background-radius: 4; -} - -/* ---- 项目树 ---- */ -.project-tree { - -fx-background-color: -workutils-surface; - -fx-border-color: -workutils-border; - -fx-border-width: 1; - -fx-border-radius: 4; -} - -/* ---- 代码文本 ---- */ -.mono-text { - -fx-font-family: "Consolas"; -} - -/* ---- 主题切换按钮 ---- */ -.theme-toggle-row { - -fx-padding: 8 0; - -fx-alignment: CENTER_LEFT; -} diff --git a/src/main/resources/css/light-theme.css b/src/main/resources/css/light-theme.css index bd02c7c..ce4a71b 100644 --- a/src/main/resources/css/light-theme.css +++ b/src/main/resources/css/light-theme.css @@ -18,9 +18,9 @@ -workutils-success: #10b981; -workutils-nav-bg: #1e293b; - -workutils-nav-text: #cbd5e1; - -workutils-nav-hover: rgba(255,255,255,0.08); - -workutils-nav-active: rgba(37,99,235,0.2); + -workutils-nav-text: #ffffff; + -workutils-nav-hover: rgba(255,255,255,0.10); + -workutils-nav-active: rgba(37,99,235,0.25); -workutils-nav-bar: #2563eb; } @@ -41,7 +41,7 @@ .nav-btn { -fx-background-color: transparent; - -fx-text-fill: -workutils-nav-text; + -fx-text-fill: white; -fx-background-radius: 0; -fx-border-width: 0 0 0 3; -fx-border-color: transparent; @@ -51,15 +51,15 @@ -fx-max-width: Infinity; } +.nav-btn > .label, +.nav-btn .label, .nav-btn .text { - /* 固定图标区域宽度,确保菜单文字左对齐 */ - -fx-display: inline-flex; - -fx-alignment: center-left; + -fx-text-fill: white; } .nav-btn:hover { -fx-background-color: -workutils-nav-hover; - -fx-text-fill: #f1f5f9; + -fx-text-fill: white; } .nav-btn-active {