From 10d0f0d3120fad278ca0a6f0fef1b4a369e162cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Fri, 3 Jul 2026 17:09:39 +0800 Subject: [PATCH] =?UTF-8?q?=E7=8E=B0=E5=9C=A8=E6=8F=92=E4=BB=B6=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workutils/WorkUtilsApplication.java | 16 ++ .../system/config/SettingsService.java | 28 ++- .../system/config/SettingsWindow.java | 186 +++++++++++++++++- .../system/plugin/PluginManager.java | 3 +- .../workutils/system/ui/MainWindow.java | 66 +++++-- src/main/resources/css/dark-theme.css | 6 + src/main/resources/css/light-theme.css | 6 + 7 files changed, 291 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/example/workutils/WorkUtilsApplication.java b/src/main/java/com/example/workutils/WorkUtilsApplication.java index 34ae493..ba6255e 100644 --- a/src/main/java/com/example/workutils/WorkUtilsApplication.java +++ b/src/main/java/com/example/workutils/WorkUtilsApplication.java @@ -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 navItems = buildNavItems(); CountDownLatch windowClosed = new CountDownLatch(1); diff --git a/src/main/java/com/example/workutils/system/config/SettingsService.java b/src/main/java/com/example/workutils/system/config/SettingsService.java index 2a3c429..6cd29e2 100644 --- a/src/main/java/com/example/workutils/system/config/SettingsService.java +++ b/src/main/java/com/example/workutils/system/config/SettingsService.java @@ -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> navItemsRefresher; + + /** 设置导航栏刷新回调 */ + public void setNavItemsRefresher(Consumer> 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 items) { + if (navItemsRefresher != null) { + navItemsRefresher.accept(items); + } + } } diff --git a/src/main/java/com/example/workutils/system/config/SettingsWindow.java b/src/main/java/com/example/workutils/system/config/SettingsWindow.java index c6f658e..e311b8a 100644 --- a/src/main/java/com/example/workutils/system/config/SettingsWindow.java +++ b/src/main/java/com/example/workutils/system/config/SettingsWindow.java @@ -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> 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> 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 allDescriptors = pluginManager.listAllPluginDescriptors(); + Set 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 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 items = buildNavItemsFromPlugins(); + navItemsRefresher.accept(items); + } + } + + /** 从当前启用的插件构建导航项列表 */ + private List buildNavItemsFromPlugins() { + List items = new ArrayList<>(); + for (Plugin plugin : pluginManager.getEnabledPlugins()) { + PluginDescriptor desc = plugin.getDescriptor(); + String pluginId = desc.getId(); + items.add(new MainWindow.NavItem( + desc.getIcon(), desc.getName(), desc.getDescription(), + null, // action 不在此处设置,由 WorkUtilsApplication 管理 + pluginId + )); + } + // 系统设置固定项 + items.add(new MainWindow.NavItem( + "\u2699\ufe0f", "系统设置", + "配置系统路径、数据库连接、项目路径、多数据源、字体大小等", + null, "__settings__" + )); + return items; + } + // ==================== Tab 6: 显示设置 ==================== private Tab createDisplayTab() { diff --git a/src/main/java/com/example/workutils/system/plugin/PluginManager.java b/src/main/java/com/example/workutils/system/plugin/PluginManager.java index bce8e7a..bc9c9a1 100644 --- a/src/main/java/com/example/workutils/system/plugin/PluginManager.java +++ b/src/main/java/com/example/workutils/system/plugin/PluginManager.java @@ -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 pluginStates; public PluginManager(List plugins, WorkUtilsProperties properties, - SettingsService settingsService) { + @Lazy SettingsService settingsService) { this.allPlugins = plugins; this.properties = properties; this.settingsService = settingsService; diff --git a/src/main/java/com/example/workutils/system/ui/MainWindow.java b/src/main/java/com/example/workutils/system/ui/MainWindow.java index 2c9caa2..d1f456a 100644 --- a/src/main/java/com/example/workutils/system/ui/MainWindow.java +++ b/src/main/java/com/example/workutils/system/ui/MainWindow.java @@ -27,7 +27,7 @@ import java.util.*; public class MainWindow { private final Stage stage; - private final List navItems; + private List 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; - } - maxTextWidth = Math.max(maxTextWidth, w * 13); + // 固定图标区 24px + 间距 8px + 最长文字约 6字符 * 13px ≈ 78px + padding 32px + return 24 + 8 + 130 + 16; + } + + /** + * 刷新导航栏(插件启用/禁用后调用) + */ + public void refreshNavItems(List 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 -> { diff --git a/src/main/resources/css/dark-theme.css b/src/main/resources/css/dark-theme.css index 1de6b7f..2c7eda2 100644 --- a/src/main/resources/css/dark-theme.css +++ b/src/main/resources/css/dark-theme.css @@ -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; diff --git a/src/main/resources/css/light-theme.css b/src/main/resources/css/light-theme.css index df8fc6f..bd02c7c 100644 --- a/src/main/resources/css/light-theme.css +++ b/src/main/resources/css/light-theme.css @@ -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;