This commit is contained in:
张家训
2026-07-03 17:41:03 +08:00
parent 17ac331e4e
commit 9fdfb05b17
7 changed files with 579 additions and 141 deletions
@@ -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");
@@ -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<PluginDescriptor> allDescriptors = pluginManager.listAllPluginDescriptors();
Set<String> 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<String> 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<MainWindow.NavItem> 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<MainWindow.NavItem> items = buildNavItemsFromPlugins();
navItemsRefresher.accept(items);
}
}
/** 从当前启用的插件构建导航项列表 */
private List<MainWindow.NavItem> buildNavItemsFromPlugins() {
List<MainWindow.NavItem> 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: 显示设置 ====================
@@ -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) {
@@ -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
* <p>
* 更改暂存于内存,点击「保存并刷新菜单」后持久化并生效。
*/
public class PluginManagementWindow {
private final PluginManager pluginManager;
private final Consumer<List<PluginItem>> onSaveCallback;
private Stage stage;
/** 当前内存中的插件条目(同步自 PluginManager + 本地编辑) */
private final List<PluginItem> pluginItems = new ArrayList<>();
/** 行索引 -> HBox 映射,用于拖拽交换 */
private final Map<Integer, HBox> 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<List<PluginItem>> 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<Plugin> enabledPlugins = pluginManager.getEnabledPlugins();
java.util.Set<String> enabledIds = new java.util.HashSet<>();
for (Plugin p : enabledPlugins) {
enabledIds.add(p.getId());
}
// 收集所有插件描述符
Map<String, PluginDescriptor> 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<PluginDescriptor> 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<String> 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<PluginItem> getCurrentItems() {
return new ArrayList<>(pluginItems);
}
// ==================== 显示入口 ====================
public void showAndWait() {
stage.showAndWait();
}
public Stage getStage() {
return stage;
}
}
@@ -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<String> enabledIds = resolveEnabledIds();
// 优先从 data/plugins.config 加载,失败则回退到 application.yml
java.util.Set<String> 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<Plugin> reordered = new ArrayList<>();
Map<String, Plugin> 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<String> orderedIds) {
Map<String, Plugin> allEnabled = new LinkedHashMap<>();
for (Plugin p : enabledPlugins) {
allEnabled.put(p.getId(), p);
}
List<Plugin> 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<PluginManagementWindow.PluginItem> items) {
Path file = getPluginConfigFilePath();
try {
Files.createDirectories(file.getParent());
List<String> orderedIds = new ArrayList<>();
List<String> 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<String> 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<String> disabledList = new ArrayList<>();
if (disabledStr != null && !disabledStr.trim().isEmpty()) {
for (String s : disabledStr.split(",")) {
disabledList.add(s.trim());
}
}
java.util.Set<String> 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) {
+1
View File
@@ -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:
@@ -0,0 +1,5 @@
# ====== WorkUtils 插件配置文件 ======
# 插件顺序(按逗号分隔,顺序即菜单显示顺序)
# 插件状态(disabled 列表,不在列表中的即为启用)
plugins.order=scanner,diff,apidoc,git,gitreview,mobileuiauto,mybatisexport,mybatisgenerator
plugins.disabled=mobileuiauto