diff --git a/src/main/java/com/example/workutils/WorkUtilsApplication.java b/src/main/java/com/example/workutils/WorkUtilsApplication.java index 14206bd..734d07e 100644 --- a/src/main/java/com/example/workutils/WorkUtilsApplication.java +++ b/src/main/java/com/example/workutils/WorkUtilsApplication.java @@ -60,7 +60,8 @@ public class WorkUtilsApplication implements CommandLineRunner { public void onReady() { String[] files = { "project_paths.txt", "datasources.json", - "damaged_repos.cache", "mybatis-generator.properties", "workutils.config" + "damaged_repos.cache", "mybatis-generator.properties", + "workutils.config", "plugins.config" }; java.nio.file.Path dataDir = java.nio.file.Paths.get( System.getProperty("user.dir"), "data"); 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 ac9e73d..e801156 100644 --- a/src/main/java/com/example/workutils/system/config/SettingsWindow.java +++ b/src/main/java/com/example/workutils/system/config/SettingsWindow.java @@ -2,8 +2,8 @@ package com.example.workutils.system.config; import com.example.workutils.system.plugin.Plugin; import com.example.workutils.system.plugin.PluginDescriptor; +import com.example.workutils.system.plugin.PluginManagementWindow; import com.example.workutils.system.plugin.PluginManager; -import com.example.workutils.system.plugin.PluginState; import com.example.workutils.system.support.JfxBootstrap; import com.example.workutils.system.support.SystemOpenHelper; import com.example.workutils.system.ui.MainWindow; @@ -17,8 +17,6 @@ import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.control.Alert.AlertType; import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.stage.DirectoryChooser; @@ -624,95 +622,37 @@ public class SettingsWindow { titleLabels.add(title); root.getChildren().add(title); - Label hint = new Label("启用或禁用功能模块。禁用后对应菜单项将从左侧导航栏隐藏,重启后生效。"); + Label hint = new Label("管理插件启用/禁用状态与菜单排序。更改在保存后才生效。"); hint.setFont(Theme.base(fontBody)); hint.getStyleClass().add(Theme.CSS_HINT_TEXT); hintLabels.add(hint); root.getChildren().add(hint); - // 插件列表 - VBox pluginList = new VBox(4); + // 打开插件管理窗口按钮 + VBox infoBox = new VBox(12); + infoBox.setPadding(new Insets(20)); + infoBox.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_CARD) + + "; -fx-background-radius: " + Theme.RADIUS_MD + ";"); - List allDescriptors = pluginManager.listAllPluginDescriptors(); - Set enabledIds = new HashSet<>(); - for (Plugin p : pluginManager.getEnabledPlugins()) { - enabledIds.add(p.getId()); - } + Label infoLine1 = new Label("点击下方按钮打开独立的插件管理窗口:"); + infoLine1.setFont(Theme.base(fontBody)); + Label infoLine2 = new Label("• 拖拽调整插件排序"); + infoLine2.setFont(Theme.base(fontBody)); + Label infoLine3 = new Label("• 启用/禁用功能模块"); + infoLine3.setFont(Theme.base(fontBody)); + Label infoLine4 = new Label("• 配置写入 data/plugins.config"); + infoLine4.setFont(Theme.base(fontBody)); - for (PluginDescriptor desc : allDescriptors) { - String id = desc.getId(); - boolean isEnabled = enabledIds.contains(id) || pluginManager.getPluginState(id) == PluginState.ACTIVE; + infoBox.getChildren().addAll(infoLine1, infoLine2, infoLine3, infoLine4); - HBox row = new HBox(10); - row.setAlignment(Pos.CENTER_LEFT); - row.setPadding(new Insets(6, 0, 6, 0)); - row.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_CARD) - + "; -fx-background-radius: " + Theme.RADIUS_SM + "; -fx-padding: 8 12;"); + Button btnOpen = new Button("打开插件管理"); + btnOpen.setFont(font(fontBtn)); + btnOpen.getStyleClass().add(Theme.CSS_BTN_PRIMARY); + allButtons.add(btnOpen); + btnOpen.setOnAction(e -> openPluginManagement()); - // 图标 + 名称 - Label iconLabel = new Label(desc.getIcon()); - iconLabel.setFont(Theme.base(16)); - Label nameLabel = new Label(desc.getName()); - nameLabel.setFont(Theme.bold(fontBody)); - nameLabel.setPrefWidth(160); - Label descLabel = new Label(desc.getDescription()); - descLabel.setFont(Theme.base(fontSmall)); - descLabel.getStyleClass().add(Theme.CSS_HINT_TEXT); - descLabel.setMaxWidth(Double.MAX_VALUE); - - Region spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); - - // 启用/禁用开关 - ToggleButton toggleBtn = new ToggleButton(isEnabled ? "已启用" : "已禁用"); - toggleBtn.setSelected(isEnabled); - toggleBtn.setFont(font(fontBtnSm)); - toggleBtn.getStyleClass().add(Theme.CSS_TOGGLE_BTN); - allButtons.add(toggleBtn); - - final String pluginId = id; - toggleBtn.setOnAction(e -> { - if (toggleBtn.isSelected()) { - pluginManager.enablePlugin(pluginId); - toggleBtn.setText("已启用"); - } else { - pluginManager.disablePlugin(pluginId); - toggleBtn.setText("已禁用"); - } - // 持久化到配置文件 - savePluginToggleState(); - // 刷新导航栏 - refreshMainWindowNavItems(); - }); - - row.getChildren().addAll(iconLabel, nameLabel, descLabel, spacer, toggleBtn); - pluginList.getChildren().add(row); - } - - ScrollPane scrollPane = new ScrollPane(pluginList); - scrollPane.setFitToWidth(true); - scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); - scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - - root.getChildren().add(scrollPane); - - // 操作按钮行 - HBox btnRow = new HBox(10); - btnRow.setAlignment(Pos.CENTER_LEFT); - btnRow.getChildren().add(new Separator()); - - Button btnSave = new Button("保存配置并刷新菜单"); - btnSave.setFont(font(fontBtn)); - btnSave.getStyleClass().add(Theme.CSS_BTN_SUCCESS); - allButtons.add(btnSave); - btnSave.setOnAction(e -> { - savePluginToggleState(); - refreshMainWindowNavItems(); - showAlert(AlertType.INFORMATION, "插件配置已保存,左侧导航栏已刷新。"); - }); - btnRow.getChildren().add(btnSave); - - root.getChildren().add(btnRow); + root.getChildren().add(infoBox); + root.getChildren().add(btnOpen); Tab tab = new Tab("插件管理"); tab.setContent(root); @@ -720,62 +660,30 @@ public class SettingsWindow { return tab; } - /** 保存插件启用/禁用状态到 workutils.config */ - private void savePluginToggleState() { - try { - Properties props = configManager.load(); - - List disabledList = new ArrayList<>(); - for (PluginDescriptor desc : pluginManager.listAllPluginDescriptors()) { - PluginState state = pluginManager.getPluginState(desc.getId()); - if (state == PluginState.DISABLED) { - disabledList.add(desc.getId()); + /** 打开插件管理窗口,保存后刷新导航栏 */ + private void openPluginManagement() { + PluginManagementWindow pmw = new PluginManagementWindow(pluginManager, items -> { + // 保存后刷新主窗口导航 + if (navItemsRefresher != null) { + // 重建导航项 + List navItems = new ArrayList<>(); + for (Plugin p : pluginManager.getEnabledPlugins()) { + PluginDescriptor desc = p.getDescriptor(); + navItems.add(new MainWindow.NavItem( + desc.getIcon(), desc.getName(), desc.getDescription(), + null, desc.getId() + )); } + navItems.add(new MainWindow.NavItem( + "\u2699\ufe0f", "系统设置", + "配置系统路径、数据库连接、项目路径、多数据源、字体大小等", + null, "__settings__" + )); + navItemsRefresher.accept(navItems); } - - if (disabledList.isEmpty()) { - props.remove("workutils.tools.disabled"); - props.remove("workutils.tools.enabled"); - } else { - props.setProperty("workutils.tools.disabled", String.join(",", disabledList)); - } - - configManager.saveSectioned(props); - properties.mergeExternal(props); - log.info("插件状态已保存: 禁用={}", disabledList); - } catch (Exception ex) { - showAlert(AlertType.ERROR, "保存插件状态失败: " + ex.getMessage()); - } - } - - /** 刷新主窗口导航栏 */ - private void refreshMainWindowNavItems() { - if (onConfigChanged != null) onConfigChanged.run(); - if (navItemsRefresher != null) { - List items = buildNavItemsFromPlugins(); - navItemsRefresher.accept(items); - } - } - - /** 从当前启用的插件构建导航项列表 */ - private List buildNavItemsFromPlugins() { - List items = new ArrayList<>(); - for (Plugin plugin : pluginManager.getEnabledPlugins()) { - PluginDescriptor desc = plugin.getDescriptor(); - String pluginId = desc.getId(); - items.add(new MainWindow.NavItem( - desc.getIcon(), desc.getName(), desc.getDescription(), - null, // action 不在此处设置,由 WorkUtilsApplication 管理 - pluginId - )); - } - // 系统设置固定项 - items.add(new MainWindow.NavItem( - "\u2699\ufe0f", "系统设置", - "配置系统路径、数据库连接、项目路径、多数据源、字体大小等", - null, "__settings__" - )); - return items; + if (onConfigChanged != null) onConfigChanged.run(); + }); + pmw.showAndWait(); } // ==================== Tab 6: 显示设置 ==================== 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 1c36b1a..b5d34b6 100644 --- a/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java +++ b/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java @@ -63,6 +63,9 @@ public class WorkUtilsProperties { /** MyBatis 生成器配置文件(相对路径时相对于 user.dir) */ private String mybatisGeneratorFile; + /** 插件状态配置文件(相对路径时相对于 user.dir) */ + private String pluginsConfigFile; + /** 工具启用/禁用配置 */ private ToolsConfig tools; @@ -283,6 +286,17 @@ public class WorkUtilsProperties { return Paths.get(System.getProperty("user.dir")).resolve(p); } + /** + * 插件状态配置文件的实际路径 + */ + public Path getPluginsConfigFilePath() { + Path p = Paths.get(pluginsConfigFile != null ? pluginsConfigFile : "data/plugins.config"); + if (p.isAbsolute()) { + return p; + } + return Paths.get(System.getProperty("user.dir")).resolve(p); + } + /** * 确保所有数据文件存在于 user.dir 同级 data/ 目录。 * 启动时统一检查:不存在则从 classpath:templates/ 下复制模板。 @@ -294,6 +308,7 @@ public class WorkUtilsProperties { ensureDataFile(getProjectPathsFilePath(), "templates/project_paths.txt", "项目路径文件"); ensureDataFile(getDamagedReposFilePath(), "templates/damaged_repos.cache", "损坏缓存文件"); ensureDataFile(getMybatisGeneratorFilePath(), "templates/mybatis-generator.properties", "MyBatis生成器配置"); + ensureDataFile(getPluginsConfigFilePath(), "templates/plugins.config", "插件配置文件"); } private void ensureDataFile(Path target, String classpathResource, String label) { diff --git a/src/main/java/com/example/workutils/system/plugin/PluginManagementWindow.java b/src/main/java/com/example/workutils/system/plugin/PluginManagementWindow.java new file mode 100644 index 0000000..9e6510b --- /dev/null +++ b/src/main/java/com/example/workutils/system/plugin/PluginManagementWindow.java @@ -0,0 +1,377 @@ +package com.example.workutils.system.plugin; + +import com.example.workutils.system.ui.Theme; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Cursor; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.input.*; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.stage.Modality; +import javafx.stage.Stage; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * 插件管理窗口 —— 支持拖拽排序、启用/禁用、保存到 data/plugins.config + *

+ * 更改暂存于内存,点击「保存并刷新菜单」后持久化并生效。 + */ +public class PluginManagementWindow { + + private final PluginManager pluginManager; + private final Consumer> onSaveCallback; + private Stage stage; + + /** 当前内存中的插件条目(同步自 PluginManager + 本地编辑) */ + private final List pluginItems = new ArrayList<>(); + + /** 行索引 -> HBox 映射,用于拖拽交换 */ + private final Map rowMap = new LinkedHashMap<>(); + + /** 插件列表容器 */ + private VBox pluginListBox; + + /** 拖拽相关 */ + private int dragSourceIndex = -1; + private static final DataFormat DRAG_FORMAT = new DataFormat("plugin-row-index"); + + public PluginManagementWindow(PluginManager pluginManager, + Consumer> onSaveCallback) { + this.pluginManager = pluginManager; + this.onSaveCallback = onSaveCallback; + loadCurrentState(); + buildUI(); + } + + // ==================== 数据模型 ==================== + + public static class PluginItem { + private final String id; + private final String icon; + private final String name; + private final String description; + private boolean enabled; + + public PluginItem(String id, String icon, String name, String description, boolean enabled) { + this.id = id; + this.icon = icon; + this.name = name; + this.description = description; + this.enabled = enabled; + } + + public String getId() { return id; } + public String getIcon() { return icon; } + public String getName() { return name; } + public String getDescription() { return description; } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean e) { this.enabled = e; } + } + + /** 从 PluginManager 加载当前状态 */ + private void loadCurrentState() { + pluginItems.clear(); + List enabledPlugins = pluginManager.getEnabledPlugins(); + java.util.Set enabledIds = new java.util.HashSet<>(); + for (Plugin p : enabledPlugins) { + enabledIds.add(p.getId()); + } + + // 收集所有插件描述符 + Map allDescMap = new LinkedHashMap<>(); + for (PluginDescriptor desc : pluginManager.listAllPluginDescriptors()) { + allDescMap.put(desc.getId(), desc); + } + + // 从 PluginManager 的 enabledPlugins 获取顺序 + for (Plugin p : enabledPlugins) { + PluginDescriptor desc = p.getDescriptor(); + pluginItems.add(new PluginItem(desc.getId(), desc.getIcon(), + desc.getName(), desc.getDescription(), true)); + } + + // 追加已禁用的插件(排在最后,按描述符 order 排序) + List disabledDescs = new ArrayList<>(); + for (PluginDescriptor desc : pluginManager.listAllPluginDescriptors()) { + if (!enabledIds.contains(desc.getId())) { + disabledDescs.add(desc); + } + } + disabledDescs.sort(java.util.Comparator.comparingInt(d -> d.getOrder())); + for (PluginDescriptor desc : disabledDescs) { + pluginItems.add(new PluginItem(desc.getId(), desc.getIcon(), + desc.getName(), desc.getDescription(), false)); + } + } + + // ==================== UI 构建 ==================== + + private void buildUI() { + stage = new Stage(); + stage.setTitle("插件管理"); + stage.initModality(Modality.APPLICATION_MODAL); + stage.setMinWidth(600); + stage.setMinHeight(450); + + VBox root = new VBox(12); + root.setPadding(new Insets(16)); + root.getStyleClass().add(Theme.CSS_CONTENT_AREA); + + // 标题 + Label title = new Label("插件管理"); + title.setFont(Theme.bold(22)); + title.getStyleClass().add(Theme.CSS_SECTION_TITLE); + root.getChildren().add(title); + + Label hint = new Label("拖拽左侧手柄可调整插件顺序。启用/禁用在点击「保存并刷新菜单」后生效。"); + hint.setFont(Theme.base(13)); + hint.getStyleClass().add(Theme.CSS_HINT_TEXT); + root.getChildren().add(hint); + + // 插件列表 + pluginListBox = new VBox(4); + refreshPluginRows(); + + ScrollPane scrollPane = new ScrollPane(pluginListBox); + scrollPane.setFitToWidth(true); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + VBox.setVgrow(scrollPane, Priority.ALWAYS); + root.getChildren().add(scrollPane); + + // 按钮行 + HBox btnRow = new HBox(10); + btnRow.setAlignment(Pos.CENTER_RIGHT); + + Button btnCancel = new Button("取消"); + btnCancel.setFont(Theme.base(14)); + btnCancel.getStyleClass().add(Theme.CSS_BTN_SMALL); + btnCancel.setOnAction(e -> stage.close()); + + Button btnSave = new Button("保存并刷新菜单"); + btnSave.setFont(Theme.base(14)); + btnSave.getStyleClass().add(Theme.CSS_BTN_SUCCESS); + btnSave.setOnAction(e -> saveAndRefresh()); + + // 重置按钮 + Button btnReset = new Button("重置"); + btnReset.setFont(Theme.base(14)); + btnReset.getStyleClass().add(Theme.CSS_BTN_SMALL); + btnReset.setOnAction(e -> { + loadCurrentState(); + refreshPluginRows(); + }); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + btnRow.getChildren().addAll(btnReset, spacer, btnCancel, btnSave); + root.getChildren().add(btnRow); + + Scene scene = new Scene(root, 620, 500); + Theme.applyCustomCss(scene); + stage.setScene(scene); + } + + /** 根据 pluginItems 重建所有行 */ + private void refreshPluginRows() { + rowMap.clear(); + pluginListBox.getChildren().clear(); + + for (int i = 0; i < pluginItems.size(); i++) { + PluginItem item = pluginItems.get(i); + HBox row = createPluginRow(i, item); + rowMap.put(i, row); + pluginListBox.getChildren().add(row); + } + } + + /** 创建单行插件条目 */ + private HBox createPluginRow(int index, PluginItem item) { + HBox row = new HBox(8); + row.setAlignment(Pos.CENTER_LEFT); + row.setPadding(new Insets(6, 10, 6, 10)); + row.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_CARD) + + "; -fx-background-radius: " + Theme.RADIUS_SM + ";"); + + // 拖拽手柄 + Label gripIcon = new Label("≡"); + gripIcon.setFont(Theme.base(18)); + gripIcon.setTextFill(Theme.TEXT_MUTED); + gripIcon.setCursor(Cursor.MOVE); + gripIcon.setPadding(new Insets(0, 2, 0, 0)); + gripIcon.setUserData(index); + + gripIcon.setOnDragDetected(e -> { + Dragboard db = gripIcon.startDragAndDrop(TransferMode.MOVE); + ClipboardContent cc = new ClipboardContent(); + cc.put(DRAG_FORMAT, index); + db.setContent(cc); + dragSourceIndex = index; + row.setOpacity(0.4); + e.consume(); + }); + + gripIcon.setOnDragDone(e -> { + row.setOpacity(1.0); + dragSourceIndex = -1; + // 恢复所有行透明度 + for (HBox r : rowMap.values()) r.setOpacity(1.0); + e.consume(); + }); + + // 图标 + Label iconLabel = new Label(item.getIcon()); + iconLabel.setFont(Theme.base(16)); + + // 名称(悬浮显示描述) + Label nameLabel = new Label(item.getName()); + nameLabel.setFont(Theme.bold(14)); + if (item.getDescription() != null && !item.getDescription().isEmpty()) { + Tooltip tip = new Tooltip(item.getDescription()); + tip.setFont(Theme.base(12)); + tip.setStyle("-fx-background-color: #1e293b; -fx-text-fill: #f1f5f9; -fx-background-radius: 4; -fx-padding: 6 10;"); + nameLabel.setTooltip(tip); + } + + // 禁用状态下的文字变灰 + if (!item.isEnabled()) { + nameLabel.setTextFill(Color.GRAY); + iconLabel.setTextFill(Color.GRAY); + } else { + nameLabel.setTextFill(Theme.TEXT_PRIMARY); + } + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + // 启用/禁用开关 + ToggleButton toggleBtn = new ToggleButton(item.isEnabled() ? "已启用" : "已禁用"); + toggleBtn.setSelected(item.isEnabled()); + toggleBtn.setFont(Theme.base(12)); + toggleBtn.getStyleClass().add(Theme.CSS_TOGGLE_BTN); + + final Label fn = nameLabel; + final Label fi = iconLabel; + toggleBtn.setOnAction(e -> { + item.setEnabled(toggleBtn.isSelected()); + toggleBtn.setText(toggleBtn.isSelected() ? "已启用" : "已禁用"); + if (toggleBtn.isSelected()) { + fn.setTextFill(Theme.TEXT_PRIMARY); + fi.setTextFill(Theme.TEXT_PRIMARY); + } else { + fn.setTextFill(Color.GRAY); + fi.setTextFill(Color.GRAY); + } + }); + + row.getChildren().addAll(gripIcon, iconLabel, nameLabel, spacer, toggleBtn); + + // 拖放接收 + row.setOnDragOver(e -> { + if (e.getGestureSource() != null + && e.getDragboard().hasContent(DRAG_FORMAT) + && !row.equals(e.getGestureSource())) { + e.acceptTransferModes(TransferMode.MOVE); + } + e.consume(); + }); + + row.setOnDragEntered(e -> { + if (e.getDragboard().hasContent(DRAG_FORMAT) + && dragSourceIndex >= 0 + && dragSourceIndex != index) { + row.setStyle("-fx-background-color: " + Theme.toWeb(Theme.PRIMARY_LIGHT) + + "; -fx-background-radius: " + Theme.RADIUS_SM + ";" + + " -fx-border-color: " + Theme.toWeb(Theme.PRIMARY) + + "; -fx-border-radius: " + Theme.RADIUS_SM + "; -fx-border-width: 1;"); + } + e.consume(); + }); + + row.setOnDragExited(e -> { + row.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_CARD) + + "; -fx-background-radius: " + Theme.RADIUS_SM + ";"); + e.consume(); + }); + + row.setOnDragDropped(e -> { + Dragboard db = e.getDragboard(); + if (db.hasContent(DRAG_FORMAT)) { + int sourceIdx = (int) db.getContent(DRAG_FORMAT); + if (sourceIdx >= 0 && sourceIdx != index && sourceIdx < pluginItems.size()) { + // 交换元素 + PluginItem moved = pluginItems.remove(sourceIdx); + int targetIdx = sourceIdx < index ? index - 1 : index; + if (targetIdx > pluginItems.size()) targetIdx = pluginItems.size(); + pluginItems.add(targetIdx, moved); + refreshPluginRows(); + } + } + e.setDropCompleted(true); + e.consume(); + }); + + return row; + } + + /** 保存并刷新 */ + private void saveAndRefresh() { + try { + // 应用启用/禁用 + 顺序到 PluginManager + List orderedIds = new ArrayList<>(); + for (PluginItem item : pluginItems) { + orderedIds.add(item.getId()); + if (item.isEnabled()) { + pluginManager.enablePlugin(item.getId()); + } else { + pluginManager.disablePlugin(item.getId()); + } + } + pluginManager.reorderPlugins(orderedIds); + + // 保存到 data/plugins.config + pluginManager.savePluginConfig(pluginItems); + + // 回调通知外部刷新 + if (onSaveCallback != null) { + onSaveCallback.accept(new ArrayList<>(pluginItems)); + } + + showAlert(AlertType.INFORMATION, "插件配置已保存,菜单即将刷新。"); + stage.close(); + } catch (Exception ex) { + showAlert(AlertType.ERROR, "保存失败: " + ex.getMessage()); + } + } + + private void showAlert(AlertType type, String msg) { + Alert alert = new Alert(type); + alert.setTitle(type == AlertType.ERROR ? "错误" : "提示"); + alert.setHeaderText(null); + alert.setContentText(msg); + alert.showAndWait(); + } + + /** 当前插件的启用/禁用状态列表(用于外部查询当前编辑结果) */ + public List getCurrentItems() { + return new ArrayList<>(pluginItems); + } + + // ==================== 显示入口 ==================== + + public void showAndWait() { + stage.showAndWait(); + } + + public Stage getStage() { + return stage; + } +} 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 7f94db6..8ed5f0d 100644 --- a/src/main/java/com/example/workutils/system/plugin/PluginManager.java +++ b/src/main/java/com/example/workutils/system/plugin/PluginManager.java @@ -5,6 +5,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; /** @@ -44,7 +48,11 @@ public class PluginManager { this.pluginByCommandMap = new LinkedHashMap<>(); this.pluginStates = new LinkedHashMap<>(); - Set enabledIds = resolveEnabledIds(); + // 优先从 data/plugins.config 加载,失败则回退到 application.yml + java.util.Set enabledIds = loadPluginConfigOrder(); + if (enabledIds == null) { + enabledIds = resolveEnabledIds(); + } for (Plugin plugin : allPlugins) { try { @@ -74,11 +82,35 @@ public class PluginManager { } } - // 按 order 升序排列菜单 - enabledPlugins.sort(Comparator.comparingInt(p -> p.getDescriptor().getOrder())); + // 按 data/plugins.config 中的顺序排列菜单 + if (enabledIds != null) { + List reordered = new ArrayList<>(); + Map pluginMap = new LinkedHashMap<>(); + for (Plugin p : enabledPlugins) { + pluginMap.put(p.getId(), p); + } + for (String id : enabledIds) { + Plugin p = pluginMap.remove(id); + if (p != null) { + reordered.add(p); + } + } + reordered.addAll(pluginMap.values()); // 兜底 + enabledPlugins = reordered; + } else { + // 回退:按 order 升序 + enabledPlugins.sort(Comparator.comparingInt(p -> p.getDescriptor().getOrder())); + } log.info("插件加载完成: {}/{} 已激活", enabledPlugins.size(), allPlugins.size()); } + /** + * 获取插件配置文件路径 + */ + public Path getPluginConfigFilePath() { + return properties.getPluginsConfigFilePath(); + } + // ==================== 查找方法 ==================== /** 获取所有已启用插件(只读) */ @@ -150,6 +182,105 @@ public class PluginManager { return plugin != null; } + /** + * 重新排序插件列表,按给定 ID 顺序重排 enabledPlugins + */ + public void reorderPlugins(List orderedIds) { + Map allEnabled = new LinkedHashMap<>(); + for (Plugin p : enabledPlugins) { + allEnabled.put(p.getId(), p); + } + List reordered = new ArrayList<>(); + for (String id : orderedIds) { + Plugin p = allEnabled.remove(id); + if (p != null) { + reordered.add(p); + } + } + // 追加不在顺序列表中的(兜底) + reordered.addAll(allEnabled.values()); + enabledPlugins = reordered; + log.info("插件顺序已更新: {}", orderedIds); + } + + /** + * 将插件启用/禁用状态和顺序保存到 data/plugins.config + */ + public void savePluginConfig(List items) { + Path file = getPluginConfigFilePath(); + try { + Files.createDirectories(file.getParent()); + List orderedIds = new ArrayList<>(); + List disabledIds = new ArrayList<>(); + for (PluginManagementWindow.PluginItem item : items) { + orderedIds.add(item.getId()); + if (!item.isEnabled()) { + disabledIds.add(item.getId()); + } + } + + Properties props = new Properties(); + props.setProperty("plugins.order", String.join(",", orderedIds)); + if (!disabledIds.isEmpty()) { + props.setProperty("plugins.disabled", String.join(",", disabledIds)); + } else { + props.setProperty("plugins.disabled", ""); + } + + try (Writer w = new OutputStreamWriter( + new FileOutputStream(file.toFile()), StandardCharsets.UTF_8)) { + props.store(w, "WorkUtils Plugin Configuration"); + } + log.info("插件配置已保存到: {}", file.toAbsolutePath()); + } catch (IOException e) { + log.error("保存插件配置失败: {}", file.toAbsolutePath(), e); + throw new RuntimeException("无法保存插件配置: " + e.getMessage(), e); + } + } + + /** + * 从 data/plugins.config 加载插件配置,解析为 orderedIds(空=未配置/全部启用) + * 返回启用的插件 ID 集合,null 表示全部启用 + */ + public java.util.Set loadPluginConfigOrder() { + Path file = getPluginConfigFilePath(); + if (!Files.exists(file)) return null; + + try { + Properties props = new Properties(); + try (Reader r = new InputStreamReader( + new FileInputStream(file.toFile()), StandardCharsets.UTF_8)) { + props.load(r); + } + + String orderStr = props.getProperty("plugins.order"); + String disabledStr = props.getProperty("plugins.disabled"); + + if (orderStr == null || orderStr.trim().isEmpty()) return null; + + String[] ids = orderStr.split(","); + List disabledList = new ArrayList<>(); + if (disabledStr != null && !disabledStr.trim().isEmpty()) { + for (String s : disabledStr.split(",")) { + disabledList.add(s.trim()); + } + } + + java.util.Set result = new java.util.LinkedHashSet<>(); + for (String id : ids) { + id = id.trim(); + if (id.isEmpty()) continue; + if (!disabledList.contains(id)) { + result.add(id); + } + } + return result.isEmpty() ? null : result; + } catch (IOException e) { + log.warn("读取插件配置文件失败: {}", file.toAbsolutePath(), e); + return null; + } + } + // ==================== 内部方法 ==================== private Plugin findPluginById(String id) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d71896f..fa8ebc8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -13,6 +13,7 @@ workutils: datasources-file: "data/datasources.json" workutils-config-file: "data/workutils.config" mybatis-generator-file: "data/mybatis-generator.properties" + plugins-config-file: "data/plugins.config" logging: charset: diff --git a/src/main/resources/templates/plugins.config b/src/main/resources/templates/plugins.config new file mode 100644 index 0000000..ca716cb --- /dev/null +++ b/src/main/resources/templates/plugins.config @@ -0,0 +1,5 @@ +# ====== WorkUtils 插件配置文件 ====== +# 插件顺序(按逗号分隔,顺序即菜单显示顺序) +# 插件状态(disabled 列表,不在列表中的即为启用) +plugins.order=scanner,diff,apidoc,git,gitreview,mobileuiauto,mybatisexport,mybatisgenerator +plugins.disabled=mobileuiauto