现在插件管理
This commit is contained in:
@@ -113,6 +113,22 @@ public class WorkUtilsApplication implements CommandLineRunner {
|
||||
Theme.Mode mode = Theme.Mode.LIGHT;
|
||||
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();
|
||||
|
||||
CountDownLatch windowClosed = new CountDownLatch(1);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
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.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 系统设置服务 —— 通过可视化 GUI 或控制台修改配置
|
||||
@@ -23,12 +27,22 @@ public class SettingsService {
|
||||
private final WorkUtilsProperties properties;
|
||||
private final ExternalConfigManager configManager;
|
||||
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),供内嵌模式使用
|
||||
*/
|
||||
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) {
|
||||
try {
|
||||
SettingsWindow window = new SettingsWindow(properties, configManager, datasourceConfigManager, onConfigChanged);
|
||||
SettingsWindow window = new SettingsWindow(properties, configManager, datasourceConfigManager,
|
||||
pluginManager, onConfigChanged, null);
|
||||
window.showAndWait();
|
||||
} catch (Exception e) {
|
||||
log.error("打开设置窗口失败,回退到控制台模式", e);
|
||||
@@ -48,4 +63,13 @@ public class SettingsService {
|
||||
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;
|
||||
|
||||
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.SystemOpenHelper;
|
||||
import com.example.workutils.system.ui.MainWindow;
|
||||
import com.example.workutils.system.ui.Theme;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -12,6 +17,8 @@ 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;
|
||||
@@ -23,7 +30,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Properties;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
/**
|
||||
@@ -37,8 +44,12 @@ public class SettingsWindow {
|
||||
private final WorkUtilsProperties properties;
|
||||
private final ExternalConfigManager configManager;
|
||||
private final DatasourceConfigManager datasourceConfigManager;
|
||||
private final PluginManager pluginManager;
|
||||
private final Runnable onConfigChanged;
|
||||
|
||||
/** 导航栏刷新回调 */
|
||||
private final java.util.function.Consumer<List<MainWindow.NavItem>> navItemsRefresher;
|
||||
|
||||
private Stage stage;
|
||||
private Scene scene;
|
||||
|
||||
@@ -65,11 +76,15 @@ public class SettingsWindow {
|
||||
public SettingsWindow(WorkUtilsProperties properties,
|
||||
ExternalConfigManager configManager,
|
||||
DatasourceConfigManager datasourceConfigManager,
|
||||
Runnable onConfigChanged) {
|
||||
PluginManager pluginManager,
|
||||
Runnable onConfigChanged,
|
||||
java.util.function.Consumer<List<MainWindow.NavItem>> navItemsRefresher) {
|
||||
this.properties = properties;
|
||||
this.configManager = configManager;
|
||||
this.datasourceConfigManager = datasourceConfigManager;
|
||||
this.pluginManager = pluginManager;
|
||||
this.onConfigChanged = onConfigChanged;
|
||||
this.navItemsRefresher = navItemsRefresher;
|
||||
this.fontH1 = properties.getFontH1();
|
||||
this.fontH2 = properties.getFontH2();
|
||||
this.fontH3 = properties.getFontH3();
|
||||
@@ -138,6 +153,7 @@ public class SettingsWindow {
|
||||
createProjectTab(),
|
||||
createDatasourceTab(),
|
||||
createDamagedCacheTab(),
|
||||
createPluginTab(),
|
||||
createDisplayTab()
|
||||
);
|
||||
tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
|
||||
@@ -596,6 +612,172 @@ public class SettingsWindow {
|
||||
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: 显示设置 ====================
|
||||
|
||||
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.support.ConsoleHelper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
@@ -39,7 +40,7 @@ public class PluginManager {
|
||||
private Map<String, PluginState> pluginStates;
|
||||
|
||||
public PluginManager(List<Plugin> plugins, WorkUtilsProperties properties,
|
||||
SettingsService settingsService) {
|
||||
@Lazy SettingsService settingsService) {
|
||||
this.allPlugins = plugins;
|
||||
this.properties = properties;
|
||||
this.settingsService = settingsService;
|
||||
|
||||
@@ -27,7 +27,7 @@ import java.util.*;
|
||||
public class MainWindow {
|
||||
|
||||
private final Stage stage;
|
||||
private final List<NavItem> navItems;
|
||||
private List<NavItem> navItems;
|
||||
private final WorkUtilsProperties properties;
|
||||
|
||||
private VBox navBox;
|
||||
@@ -100,18 +100,43 @@ public class MainWindow {
|
||||
}
|
||||
|
||||
private double computeNavMinBaseWidth() {
|
||||
double maxTextWidth = 0;
|
||||
for (NavItem item : navItems) {
|
||||
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;
|
||||
// 固定图标区 24px + 间距 8px + 最长文字约 6字符 * 13px ≈ 78px + padding 32px
|
||||
return 24 + 8 + 130 + 16;
|
||||
}
|
||||
maxTextWidth = Math.max(maxTextWidth, w * 13);
|
||||
|
||||
/**
|
||||
* 刷新导航栏(插件启用/禁用后调用)
|
||||
*/
|
||||
public void refreshNavItems(List<NavItem> newItems) {
|
||||
navButtons.clear();
|
||||
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(); }
|
||||
@@ -268,14 +293,25 @@ public class MainWindow {
|
||||
}
|
||||
|
||||
private Button createNavButton(NavItem item) {
|
||||
Button btn = new Button(" " + item.icon + " " + item.name);
|
||||
btn.setFont(rfont(13));
|
||||
Button btn = new Button();
|
||||
btn.setMaxWidth(Double.MAX_VALUE);
|
||||
btn.setAlignment(Pos.CENTER_LEFT);
|
||||
btn.setPadding(new Insets(12, 16, 12, 16));
|
||||
btn.setTextAlignment(TextAlignment.LEFT);
|
||||
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.setOnMouseEntered(e -> {
|
||||
|
||||
@@ -50,6 +50,12 @@
|
||||
-fx-max-width: Infinity;
|
||||
}
|
||||
|
||||
.nav-btn .text {
|
||||
/* 固定图标区域宽度,确保菜单文字左对齐 */
|
||||
-fx-display: inline-flex;
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
-fx-background-color: -workutils-nav-hover;
|
||||
-fx-text-fill: #e6edf3;
|
||||
|
||||
@@ -51,6 +51,12 @@
|
||||
-fx-max-width: Infinity;
|
||||
}
|
||||
|
||||
.nav-btn .text {
|
||||
/* 固定图标区域宽度,确保菜单文字左对齐 */
|
||||
-fx-display: inline-flex;
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
-fx-background-color: -workutils-nav-hover;
|
||||
-fx-text-fill: #f1f5f9;
|
||||
|
||||
Reference in New Issue
Block a user