现在插件管理

This commit is contained in:
张家训
2026-07-03 17:09:39 +08:00
parent 3883323f93
commit 10d0f0d312
7 changed files with 291 additions and 20 deletions
@@ -113,6 +113,22 @@ public class WorkUtilsApplication implements CommandLineRunner {
Theme.Mode mode = Theme.Mode.LIGHT; Theme.Mode mode = Theme.Mode.LIGHT;
Platform.runLater(() -> Theme.applyTheme(mode)); Platform.runLater(() -> Theme.applyTheme(mode));
// 设置导航栏刷新回调(插件管理中启用/禁用后刷新侧边栏)
settingsService.setNavItemsRefresher(items -> Platform.runLater(() -> {
if (mainWindow != null) {
mainWindow.refreshNavItems(items);
// 重新绑定插件面板
for (Plugin plugin : pluginManager.getEnabledPlugins()) {
PluginDescriptor desc = plugin.getDescriptor();
VisualPluginFactory factory = plugin.getVisualFactory();
Parent pane = factory != null ? factory.getEmbeddedPane() : null;
if (pane != null) {
mainWindow.embedPlugin(desc.getId(), desc.getIcon(), desc.getName(), pane);
}
}
}
}));
List<MainWindow.NavItem> navItems = buildNavItems(); List<MainWindow.NavItem> navItems = buildNavItems();
CountDownLatch windowClosed = new CountDownLatch(1); CountDownLatch windowClosed = new CountDownLatch(1);
@@ -1,10 +1,14 @@
package com.example.workutils.system.config; package com.example.workutils.system.config;
import com.example.workutils.system.plugin.PluginManager;
import com.example.workutils.system.ui.MainWindow;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Scanner; import java.util.Scanner;
import java.util.function.Consumer;
/** /**
* 系统设置服务 —— 通过可视化 GUI 或控制台修改配置 * 系统设置服务 —— 通过可视化 GUI 或控制台修改配置
@@ -23,12 +27,22 @@ public class SettingsService {
private final WorkUtilsProperties properties; private final WorkUtilsProperties properties;
private final ExternalConfigManager configManager; private final ExternalConfigManager configManager;
private final DatasourceConfigManager datasourceConfigManager; private final DatasourceConfigManager datasourceConfigManager;
private final PluginManager pluginManager;
/** 导航栏刷新回调(插件启用/禁用时刷新侧边栏) */
private Consumer<List<MainWindow.NavItem>> navItemsRefresher;
/** 设置导航栏刷新回调 */
public void setNavItemsRefresher(Consumer<List<MainWindow.NavItem>> refresher) {
this.navItemsRefresher = refresher;
}
/** /**
* 创建设置窗口(不自动 showAndWait),供内嵌模式使用 * 创建设置窗口(不自动 showAndWait),供内嵌模式使用
*/ */
public SettingsWindow createSettingsWindow(Runnable onConfigChanged) { public SettingsWindow createSettingsWindow(Runnable onConfigChanged) {
return new SettingsWindow(properties, configManager, datasourceConfigManager, onConfigChanged); return new SettingsWindow(properties, configManager, datasourceConfigManager,
pluginManager, onConfigChanged, navItemsRefresher);
} }
/** /**
@@ -38,7 +52,8 @@ public class SettingsService {
*/ */
public void showSettings(Scanner scanner, Runnable onConfigChanged) { public void showSettings(Scanner scanner, Runnable onConfigChanged) {
try { try {
SettingsWindow window = new SettingsWindow(properties, configManager, datasourceConfigManager, onConfigChanged); SettingsWindow window = new SettingsWindow(properties, configManager, datasourceConfigManager,
pluginManager, onConfigChanged, null);
window.showAndWait(); window.showAndWait();
} catch (Exception e) { } catch (Exception e) {
log.error("打开设置窗口失败,回退到控制台模式", e); log.error("打开设置窗口失败,回退到控制台模式", e);
@@ -48,4 +63,13 @@ public class SettingsService {
System.out.println("编辑后重启程序即可生效。"); System.out.println("编辑后重启程序即可生效。");
} }
} }
/**
* 通知导航栏刷新(供外部调用)
*/
public void refreshNavItems(List<MainWindow.NavItem> items) {
if (navItemsRefresher != null) {
navItemsRefresher.accept(items);
}
}
} }
@@ -1,7 +1,12 @@
package com.example.workutils.system.config; 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.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.Theme; import com.example.workutils.system.ui.Theme;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.geometry.Insets; import javafx.geometry.Insets;
@@ -12,6 +17,8 @@ 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;
@@ -23,7 +30,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Properties; import java.util.*;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
/** /**
@@ -37,8 +44,12 @@ public class SettingsWindow {
private final WorkUtilsProperties properties; private final WorkUtilsProperties properties;
private final ExternalConfigManager configManager; private final ExternalConfigManager configManager;
private final DatasourceConfigManager datasourceConfigManager; private final DatasourceConfigManager datasourceConfigManager;
private final PluginManager pluginManager;
private final Runnable onConfigChanged; private final Runnable onConfigChanged;
/** 导航栏刷新回调 */
private final java.util.function.Consumer<List<MainWindow.NavItem>> navItemsRefresher;
private Stage stage; private Stage stage;
private Scene scene; private Scene scene;
@@ -65,11 +76,15 @@ public class SettingsWindow {
public SettingsWindow(WorkUtilsProperties properties, public SettingsWindow(WorkUtilsProperties properties,
ExternalConfigManager configManager, ExternalConfigManager configManager,
DatasourceConfigManager datasourceConfigManager, DatasourceConfigManager datasourceConfigManager,
Runnable onConfigChanged) { PluginManager pluginManager,
Runnable onConfigChanged,
java.util.function.Consumer<List<MainWindow.NavItem>> navItemsRefresher) {
this.properties = properties; this.properties = properties;
this.configManager = configManager; this.configManager = configManager;
this.datasourceConfigManager = datasourceConfigManager; this.datasourceConfigManager = datasourceConfigManager;
this.pluginManager = pluginManager;
this.onConfigChanged = onConfigChanged; this.onConfigChanged = onConfigChanged;
this.navItemsRefresher = navItemsRefresher;
this.fontH1 = properties.getFontH1(); this.fontH1 = properties.getFontH1();
this.fontH2 = properties.getFontH2(); this.fontH2 = properties.getFontH2();
this.fontH3 = properties.getFontH3(); this.fontH3 = properties.getFontH3();
@@ -138,6 +153,7 @@ public class SettingsWindow {
createProjectTab(), createProjectTab(),
createDatasourceTab(), createDatasourceTab(),
createDamagedCacheTab(), createDamagedCacheTab(),
createPluginTab(),
createDisplayTab() createDisplayTab()
); );
tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
@@ -596,6 +612,172 @@ public class SettingsWindow {
return tab; return tab;
} }
// ==================== Tab 6: 插件管理 ====================
private Tab createPluginTab() {
VBox root = new VBox(10);
root.setPadding(new Insets(15));
Label title = new Label("插件管理");
title.setFont(Theme.bold(fontH2));
title.getStyleClass().add(Theme.CSS_SECTION_TITLE);
titleLabels.add(title);
root.getChildren().add(title);
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);
List<PluginDescriptor> allDescriptors = pluginManager.listAllPluginDescriptors();
Set<String> enabledIds = new HashSet<>();
for (Plugin p : pluginManager.getEnabledPlugins()) {
enabledIds.add(p.getId());
}
for (PluginDescriptor desc : allDescriptors) {
String id = desc.getId();
boolean isEnabled = enabledIds.contains(id) || pluginManager.getPluginState(id) == PluginState.ACTIVE;
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;");
// 图标 + 名称
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);
Tab tab = new Tab("插件管理");
tab.setContent(root);
tab.setClosable(false);
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());
}
}
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;
}
// ==================== Tab 6: 显示设置 ==================== // ==================== Tab 6: 显示设置 ====================
private Tab createDisplayTab() { private Tab createDisplayTab() {
@@ -4,6 +4,7 @@ import com.example.workutils.system.config.SettingsService;
import com.example.workutils.system.config.WorkUtilsProperties; import com.example.workutils.system.config.WorkUtilsProperties;
import com.example.workutils.system.support.ConsoleHelper; import com.example.workutils.system.support.ConsoleHelper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
@@ -39,7 +40,7 @@ public class PluginManager {
private Map<String, PluginState> pluginStates; private Map<String, PluginState> pluginStates;
public PluginManager(List<Plugin> plugins, WorkUtilsProperties properties, public PluginManager(List<Plugin> plugins, WorkUtilsProperties properties,
SettingsService settingsService) { @Lazy SettingsService settingsService) {
this.allPlugins = plugins; this.allPlugins = plugins;
this.properties = properties; this.properties = properties;
this.settingsService = settingsService; this.settingsService = settingsService;
@@ -27,7 +27,7 @@ import java.util.*;
public class MainWindow { public class MainWindow {
private final Stage stage; private final Stage stage;
private final List<NavItem> navItems; private List<NavItem> navItems;
private final WorkUtilsProperties properties; private final WorkUtilsProperties properties;
private VBox navBox; private VBox navBox;
@@ -100,18 +100,43 @@ public class MainWindow {
} }
private double computeNavMinBaseWidth() { private double computeNavMinBaseWidth() {
double maxTextWidth = 0; // 固定图标区 24px + 间距 8px + 最长文字约 6字符 * 13px ≈ 78px + padding 32px
for (NavItem item : navItems) { return 24 + 8 + 130 + 16;
String text = " " + item.icon + " " + item.name; }
double w = 0;
for (int i = 0; i < text.length(); i++) { /**
char c = text.charAt(i); * 刷新导航栏(插件启用/禁用后调用)
if (c > 127) w += 1.05; */
else w += 0.58; public void refreshNavItems(List<NavItem> newItems) {
} navButtons.clear();
maxTextWidth = Math.max(maxTextWidth, w * 13); navBox.getChildren().clear();
// 品牌区
navBox.getChildren().add(appLabel);
// 分割线
Separator sep = new Separator();
sep.getStyleClass().add(Theme.CSS_NAV_SEP);
sep.setPadding(new Insets(0));
navBox.getChildren().add(sep);
this.navItems.clear();
this.navItems.addAll(newItems);
// 导航按钮
for (NavItem item : newItems) {
Button btn = createNavButton(item);
navButtons.add(btn);
navBox.getChildren().add(btn);
} }
return maxTextWidth + 3 + 16 + 16 + 8;
// 弹性空间
Region spacer = new Region();
VBox.setVgrow(spacer, Priority.ALWAYS);
navBox.getChildren().add(spacer);
// 回到欢迎页
showWelcomePage();
} }
private int fsTitle() { return properties.getFontSectionTitle(); } private int fsTitle() { return properties.getFontSectionTitle(); }
@@ -268,14 +293,25 @@ public class MainWindow {
} }
private Button createNavButton(NavItem item) { private Button createNavButton(NavItem item) {
Button btn = new Button(" " + item.icon + " " + item.name); Button btn = new Button();
btn.setFont(rfont(13));
btn.setMaxWidth(Double.MAX_VALUE); btn.setMaxWidth(Double.MAX_VALUE);
btn.setAlignment(Pos.CENTER_LEFT); btn.setAlignment(Pos.CENTER_LEFT);
btn.setPadding(new Insets(12, 16, 12, 16)); btn.setPadding(new Insets(12, 16, 12, 16));
btn.setTextAlignment(TextAlignment.LEFT);
btn.setCursor(Cursor.HAND); btn.setCursor(Cursor.HAND);
// 使用 HBox 分离图标和文字,图标区域固定宽度确保对齐
HBox content = new HBox(8);
content.setAlignment(Pos.CENTER_LEFT);
Label iconLabel = new Label(item.icon);
iconLabel.setPrefWidth(24); // 固定图标区域宽度
iconLabel.setAlignment(Pos.CENTER_LEFT);
iconLabel.setFont(rfont(13));
Label nameLabel = new Label(item.name);
nameLabel.setFont(rfont(13));
content.getChildren().addAll(iconLabel, nameLabel);
btn.setGraphic(content);
btn.setText("");
btn.getStyleClass().add(Theme.CSS_NAV_BTN); btn.getStyleClass().add(Theme.CSS_NAV_BTN);
btn.setOnMouseEntered(e -> { btn.setOnMouseEntered(e -> {
+6
View File
@@ -50,6 +50,12 @@
-fx-max-width: Infinity; -fx-max-width: Infinity;
} }
.nav-btn .text {
/* 固定图标区域宽度,确保菜单文字左对齐 */
-fx-display: inline-flex;
-fx-alignment: center-left;
}
.nav-btn:hover { .nav-btn:hover {
-fx-background-color: -workutils-nav-hover; -fx-background-color: -workutils-nav-hover;
-fx-text-fill: #e6edf3; -fx-text-fill: #e6edf3;
+6
View File
@@ -51,6 +51,12 @@
-fx-max-width: Infinity; -fx-max-width: Infinity;
} }
.nav-btn .text {
/* 固定图标区域宽度,确保菜单文字左对齐 */
-fx-display: inline-flex;
-fx-alignment: center-left;
}
.nav-btn:hover { .nav-btn:hover {
-fx-background-color: -workutils-nav-hover; -fx-background-color: -workutils-nav-hover;
-fx-text-fill: #f1f5f9; -fx-text-fill: #f1f5f9;