提交
This commit is contained in:
@@ -60,7 +60,8 @@ public class WorkUtilsApplication implements CommandLineRunner {
|
|||||||
public void onReady() {
|
public void onReady() {
|
||||||
String[] files = {
|
String[] files = {
|
||||||
"project_paths.txt", "datasources.json",
|
"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(
|
java.nio.file.Path dataDir = java.nio.file.Paths.get(
|
||||||
System.getProperty("user.dir"), "data");
|
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.Plugin;
|
||||||
import com.example.workutils.system.plugin.PluginDescriptor;
|
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.PluginManager;
|
||||||
import com.example.workutils.system.plugin.PluginState;
|
|
||||||
import com.example.workutils.system.support.JfxBootstrap;
|
import com.example.workutils.system.support.JfxBootstrap;
|
||||||
import com.example.workutils.system.support.SystemOpenHelper;
|
import com.example.workutils.system.support.SystemOpenHelper;
|
||||||
import com.example.workutils.system.ui.MainWindow;
|
import com.example.workutils.system.ui.MainWindow;
|
||||||
@@ -17,8 +17,6 @@ import javafx.scene.Scene;
|
|||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Alert.AlertType;
|
import javafx.scene.control.Alert.AlertType;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import javafx.stage.DirectoryChooser;
|
import javafx.stage.DirectoryChooser;
|
||||||
@@ -624,95 +622,37 @@ public class SettingsWindow {
|
|||||||
titleLabels.add(title);
|
titleLabels.add(title);
|
||||||
root.getChildren().add(title);
|
root.getChildren().add(title);
|
||||||
|
|
||||||
Label hint = new Label("启用或禁用功能模块。禁用后对应菜单项将从左侧导航栏隐藏,重启后生效。");
|
Label hint = new Label("管理插件启用/禁用状态与菜单排序。更改在保存后才生效。");
|
||||||
hint.setFont(Theme.base(fontBody));
|
hint.setFont(Theme.base(fontBody));
|
||||||
hint.getStyleClass().add(Theme.CSS_HINT_TEXT);
|
hint.getStyleClass().add(Theme.CSS_HINT_TEXT);
|
||||||
hintLabels.add(hint);
|
hintLabels.add(hint);
|
||||||
root.getChildren().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();
|
Label infoLine1 = new Label("点击下方按钮打开独立的插件管理窗口:");
|
||||||
Set<String> enabledIds = new HashSet<>();
|
infoLine1.setFont(Theme.base(fontBody));
|
||||||
for (Plugin p : pluginManager.getEnabledPlugins()) {
|
Label infoLine2 = new Label("• 拖拽调整插件排序");
|
||||||
enabledIds.add(p.getId());
|
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) {
|
infoBox.getChildren().addAll(infoLine1, infoLine2, infoLine3, infoLine4);
|
||||||
String id = desc.getId();
|
|
||||||
boolean isEnabled = enabledIds.contains(id) || pluginManager.getPluginState(id) == PluginState.ACTIVE;
|
|
||||||
|
|
||||||
HBox row = new HBox(10);
|
Button btnOpen = new Button("打开插件管理");
|
||||||
row.setAlignment(Pos.CENTER_LEFT);
|
btnOpen.setFont(font(fontBtn));
|
||||||
row.setPadding(new Insets(6, 0, 6, 0));
|
btnOpen.getStyleClass().add(Theme.CSS_BTN_PRIMARY);
|
||||||
row.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_CARD)
|
allButtons.add(btnOpen);
|
||||||
+ "; -fx-background-radius: " + Theme.RADIUS_SM + "; -fx-padding: 8 12;");
|
btnOpen.setOnAction(e -> openPluginManagement());
|
||||||
|
|
||||||
// 图标 + 名称
|
root.getChildren().add(infoBox);
|
||||||
Label iconLabel = new Label(desc.getIcon());
|
root.getChildren().add(btnOpen);
|
||||||
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);
|
|
||||||
|
|
||||||
Tab tab = new Tab("插件管理");
|
Tab tab = new Tab("插件管理");
|
||||||
tab.setContent(root);
|
tab.setContent(root);
|
||||||
@@ -720,62 +660,30 @@ public class SettingsWindow {
|
|||||||
return tab;
|
return tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保存插件启用/禁用状态到 workutils.config */
|
/** 打开插件管理窗口,保存后刷新导航栏 */
|
||||||
private void savePluginToggleState() {
|
private void openPluginManagement() {
|
||||||
try {
|
PluginManagementWindow pmw = new PluginManagementWindow(pluginManager, items -> {
|
||||||
Properties props = configManager.load();
|
// 保存后刷新主窗口导航
|
||||||
|
if (navItemsRefresher != null) {
|
||||||
List<String> disabledList = new ArrayList<>();
|
// 重建导航项
|
||||||
for (PluginDescriptor desc : pluginManager.listAllPluginDescriptors()) {
|
List<MainWindow.NavItem> navItems = new ArrayList<>();
|
||||||
PluginState state = pluginManager.getPluginState(desc.getId());
|
for (Plugin p : pluginManager.getEnabledPlugins()) {
|
||||||
if (state == PluginState.DISABLED) {
|
PluginDescriptor desc = p.getDescriptor();
|
||||||
disabledList.add(desc.getId());
|
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 (onConfigChanged != null) onConfigChanged.run();
|
||||||
if (disabledList.isEmpty()) {
|
});
|
||||||
props.remove("workutils.tools.disabled");
|
pmw.showAndWait();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Tab 6: 显示设置 ====================
|
// ==================== Tab 6: 显示设置 ====================
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ public class WorkUtilsProperties {
|
|||||||
/** MyBatis 生成器配置文件(相对路径时相对于 user.dir) */
|
/** MyBatis 生成器配置文件(相对路径时相对于 user.dir) */
|
||||||
private String mybatisGeneratorFile;
|
private String mybatisGeneratorFile;
|
||||||
|
|
||||||
|
/** 插件状态配置文件(相对路径时相对于 user.dir) */
|
||||||
|
private String pluginsConfigFile;
|
||||||
|
|
||||||
/** 工具启用/禁用配置 */
|
/** 工具启用/禁用配置 */
|
||||||
private ToolsConfig tools;
|
private ToolsConfig tools;
|
||||||
|
|
||||||
@@ -283,6 +286,17 @@ public class WorkUtilsProperties {
|
|||||||
return Paths.get(System.getProperty("user.dir")).resolve(p);
|
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/ 目录。
|
* 确保所有数据文件存在于 user.dir 同级 data/ 目录。
|
||||||
* 启动时统一检查:不存在则从 classpath:templates/ 下复制模板。
|
* 启动时统一检查:不存在则从 classpath:templates/ 下复制模板。
|
||||||
@@ -294,6 +308,7 @@ public class WorkUtilsProperties {
|
|||||||
ensureDataFile(getProjectPathsFilePath(), "templates/project_paths.txt", "项目路径文件");
|
ensureDataFile(getProjectPathsFilePath(), "templates/project_paths.txt", "项目路径文件");
|
||||||
ensureDataFile(getDamagedReposFilePath(), "templates/damaged_repos.cache", "损坏缓存文件");
|
ensureDataFile(getDamagedReposFilePath(), "templates/damaged_repos.cache", "损坏缓存文件");
|
||||||
ensureDataFile(getMybatisGeneratorFilePath(), "templates/mybatis-generator.properties", "MyBatis生成器配置");
|
ensureDataFile(getMybatisGeneratorFilePath(), "templates/mybatis-generator.properties", "MyBatis生成器配置");
|
||||||
|
ensureDataFile(getPluginsConfigFilePath(), "templates/plugins.config", "插件配置文件");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureDataFile(Path target, String classpathResource, String label) {
|
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 org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
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.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +48,11 @@ public class PluginManager {
|
|||||||
this.pluginByCommandMap = new LinkedHashMap<>();
|
this.pluginByCommandMap = new LinkedHashMap<>();
|
||||||
this.pluginStates = 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) {
|
for (Plugin plugin : allPlugins) {
|
||||||
try {
|
try {
|
||||||
@@ -74,11 +82,35 @@ public class PluginManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按 order 升序排列菜单
|
// 按 data/plugins.config 中的顺序排列菜单
|
||||||
enabledPlugins.sort(Comparator.comparingInt(p -> p.getDescriptor().getOrder()));
|
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());
|
log.info("插件加载完成: {}/{} 已激活", enabledPlugins.size(), allPlugins.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件配置文件路径
|
||||||
|
*/
|
||||||
|
public Path getPluginConfigFilePath() {
|
||||||
|
return properties.getPluginsConfigFilePath();
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 查找方法 ====================
|
// ==================== 查找方法 ====================
|
||||||
|
|
||||||
/** 获取所有已启用插件(只读) */
|
/** 获取所有已启用插件(只读) */
|
||||||
@@ -150,6 +182,105 @@ public class PluginManager {
|
|||||||
return plugin != null;
|
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) {
|
private Plugin findPluginById(String id) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ workutils:
|
|||||||
datasources-file: "data/datasources.json"
|
datasources-file: "data/datasources.json"
|
||||||
workutils-config-file: "data/workutils.config"
|
workutils-config-file: "data/workutils.config"
|
||||||
mybatis-generator-file: "data/mybatis-generator.properties"
|
mybatis-generator-file: "data/mybatis-generator.properties"
|
||||||
|
plugins-config-file: "data/plugins.config"
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
charset:
|
charset:
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# ====== WorkUtils 插件配置文件 ======
|
||||||
|
# 插件顺序(按逗号分隔,顺序即菜单显示顺序)
|
||||||
|
# 插件状态(disabled 列表,不在列表中的即为启用)
|
||||||
|
plugins.order=scanner,diff,apidoc,git,gitreview,mobileuiauto,mybatisexport,mybatisgenerator
|
||||||
|
plugins.disabled=mobileuiauto
|
||||||
Reference in New Issue
Block a user