From 145b6559b81ab8a4d8b0510732b3d4476d6db596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Sat, 1 Aug 2026 14:59:08 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=BA=94=E7=94=A8=E9=A1=B6?= =?UTF-8?q?=E9=83=A8=E6=A0=8F=E4=B8=8E=E8=AE=BE=E7=BD=AE=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workutils/WorkUtilsApplication.java | 14 +--- .../workutils/system/ui/MainWindow.java | 49 ++++++++---- .../workutils/system/ui/shell/PageHeader.java | 31 ++++++++ .../DefaultPluginConfigurationTest.java | 1 + .../system/ui/MainWindowArchitectureTest.java | 77 +++++++++++++++++++ 5 files changed, 148 insertions(+), 24 deletions(-) create mode 100644 src/main/java/com/example/workutils/system/ui/shell/PageHeader.java create mode 100644 src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java diff --git a/src/main/java/com/example/workutils/WorkUtilsApplication.java b/src/main/java/com/example/workutils/WorkUtilsApplication.java index 734d07e..370261e 100644 --- a/src/main/java/com/example/workutils/WorkUtilsApplication.java +++ b/src/main/java/com/example/workutils/WorkUtilsApplication.java @@ -111,9 +111,9 @@ public class WorkUtilsApplication implements CommandLineRunner { Platform.runLater(Theme::initTheme); // 设置导航栏刷新回调(插件管理中启用/禁用后刷新侧边栏) - settingsService.setNavItemsRefresher(items -> Platform.runLater(() -> { + settingsService.setNavItemsRefresher(ignoredItems -> Platform.runLater(() -> { if (mainWindow != null) { - mainWindow.refreshNavItems(items); + mainWindow.refreshNavItems(buildNavItems()); // 重新绑定插件面板 for (Plugin plugin : pluginManager.getEnabledPlugins()) { PluginDescriptor desc = plugin.getDescriptor(); @@ -131,7 +131,7 @@ public class WorkUtilsApplication implements CommandLineRunner { CountDownLatch windowClosed = new CountDownLatch(1); Platform.runLater(() -> { try { - mainWindow = new MainWindow(navItems, properties); + mainWindow = new MainWindow(navItems, properties, this::openSettingsEmbedded); mainWindow.showAndWait(); } catch (Exception e) { log.error("主窗口创建失败", e); @@ -164,14 +164,6 @@ public class WorkUtilsApplication implements CommandLineRunner { )); } - // 系统设置 — 内嵌为 Tab - items.add(new MainWindow.NavItem( - "⚙️", "系统设置", - "配置系统路径、数据库连接、项目路径、多数据源、字体大小等", - this::openSettingsEmbedded, - "__settings__" - )); - return items; } 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 2ae146a..29c6469 100644 --- a/src/main/java/com/example/workutils/system/ui/MainWindow.java +++ b/src/main/java/com/example/workutils/system/ui/MainWindow.java @@ -8,7 +8,10 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Parent; import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; @@ -25,6 +28,7 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; /** Main application window and shell-level plugin content host. */ public class MainWindow { @@ -43,11 +47,17 @@ public class MainWindow { private FontIcon toolbarIcon; private Label toolbarTitle; - public MainWindow(List navItems, WorkUtilsProperties properties) { + public MainWindow(List navItems, WorkUtilsProperties properties, Runnable openSettings) { this.navItems = new ArrayList<>(navItems); this.properties = properties; this.stage = new Stage(); - buildUI(); + buildUI(Objects.requireNonNull(openSettings, "openSettings must not be null")); + } + + /** Compatibility constructor for callers that do not expose settings. */ + public MainWindow(List navItems, WorkUtilsProperties properties) { + this(navItems, properties, () -> { + }); } public void refreshNavItems(List newItems) { @@ -74,7 +84,7 @@ public class MainWindow { } } - private void buildUI() { + private void buildUI(Runnable openSettings) { setAppIcon(); stage.setTitle("WorkUtils — 开发工作集"); stage.setWidth(1280); @@ -84,7 +94,7 @@ public class MainWindow { stage.setOnCloseRequest(event -> embeddedCache.clear()); BorderPane root = new BorderPane(); - root.setTop(buildUnifiedHeader()); + root.setTop(buildUnifiedHeader(openSettings)); navigationRail = new NavigationRail(navItems, this::navigateTo); contentArea = new StackPane(); @@ -100,8 +110,9 @@ public class MainWindow { showWelcomePage(); } - private HBox buildUnifiedHeader() { + private HBox buildUnifiedHeader(Runnable openSettings) { HBox bar = new HBox(Theme.S1); + bar.setId("app-header"); bar.setAlignment(Pos.CENTER_LEFT); bar.setPadding(new Insets(0, Theme.S5, 0, Theme.S4)); bar.setMinHeight(DesignTokens.HEADER_HEIGHT); @@ -109,26 +120,38 @@ public class MainWindow { bar.setMaxHeight(DesignTokens.HEADER_HEIGHT); bar.getStyleClass().add(Theme.CSS_HEADER_BAR); - FontIcon brand = IconFactory.create(AppIcon.APP, 20); - bar.getChildren().add(brand); + Button menuButton = new Button(null, IconFactory.createButtonIcon(AppIcon.MENU, "切换导航栏")); + menuButton.setId("menu-button"); + menuButton.setTooltip(new Tooltip("切换导航栏")); + menuButton.setOnAction(event -> navigationRail.toggle()); - Region spacerLeft = new Region(); - bar.getChildren().add(spacerLeft); + Label brand = new Label("WorkUtils", IconFactory.create(AppIcon.APP, 20)); + brand.setContentDisplay(ContentDisplay.LEFT); + brand.setFont(Theme.sectionTitle()); pluginToolbar = new HBox(Theme.S2); pluginToolbar.setAlignment(Pos.CENTER_LEFT); pluginToolbar.setVisible(false); pluginToolbar.setManaged(false); + FontIcon chevron = IconFactory.create(AppIcon.CHEVRON_RIGHT, 14); toolbarIcon = IconFactory.create(AppIcon.APP, 18); toolbarTitle = new Label(); - toolbarTitle.setFont(Theme.body()); - pluginToolbar.getChildren().addAll(toolbarIcon, toolbarTitle); - bar.getChildren().add(pluginToolbar); + toolbarTitle.setFont(Theme.pageTitle()); + toolbarTitle.getStyleClass().add(Theme.CSS_PAGE_TITLE); + pluginToolbar.getChildren().addAll(chevron, toolbarIcon, toolbarTitle); Region spacerRight = new Region(); HBox.setHgrow(spacerRight, Priority.ALWAYS); - bar.getChildren().add(spacerRight); + + Button settingsButton = new Button( + null, IconFactory.createButtonIcon(AppIcon.SETTINGS, "系统设置")); + settingsButton.setId("settings-button"); + settingsButton.setTooltip(new Tooltip("系统设置")); + settingsButton.setOnAction(event -> openSettings.run()); + + bar.getChildren().addAll( + menuButton, brand, pluginToolbar, spacerRight, settingsButton); return bar; } diff --git a/src/main/java/com/example/workutils/system/ui/shell/PageHeader.java b/src/main/java/com/example/workutils/system/ui/shell/PageHeader.java new file mode 100644 index 0000000..a860468 --- /dev/null +++ b/src/main/java/com/example/workutils/system/ui/shell/PageHeader.java @@ -0,0 +1,31 @@ +package com.example.workutils.system.ui.shell; + +import com.example.workutils.system.ui.Theme; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; + +import java.util.Objects; + +/** Semantic title block for plugin content pages. */ +public final class PageHeader extends VBox { + + public PageHeader(String title) { + this(title, null); + } + + public PageHeader(String title, String description) { + super(Theme.S1); + + Label titleLabel = new Label(Objects.requireNonNull(title, "title must not be null")); + titleLabel.setFont(Theme.pageTitle()); + titleLabel.getStyleClass().add(Theme.CSS_PAGE_TITLE); + getChildren().add(titleLabel); + + if (description != null && !description.isBlank()) { + Label descriptionLabel = new Label(description); + descriptionLabel.setFont(Theme.secondary()); + descriptionLabel.getStyleClass().add(Theme.CSS_SECONDARY_TEXT); + getChildren().add(descriptionLabel); + } + } +} diff --git a/src/test/java/com/example/workutils/system/plugin/DefaultPluginConfigurationTest.java b/src/test/java/com/example/workutils/system/plugin/DefaultPluginConfigurationTest.java index 0285af9..d9cff56 100644 --- a/src/test/java/com/example/workutils/system/plugin/DefaultPluginConfigurationTest.java +++ b/src/test/java/com/example/workutils/system/plugin/DefaultPluginConfigurationTest.java @@ -39,6 +39,7 @@ class DefaultPluginConfigurationTest { PluginManager manager = new PluginManager(plugins, properties); manager.init(); + assertEquals(7, manager.getEnabledPlugins().size()); assertEquals( List.of("scan", "diff", "api-doc", "git-sync", "git-review", "mybatisexport", "mybatisgenerator"), diff --git a/src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java b/src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java new file mode 100644 index 0000000..ffc6e7d --- /dev/null +++ b/src/test/java/com/example/workutils/system/ui/MainWindowArchitectureTest.java @@ -0,0 +1,77 @@ +package com.example.workutils.system.ui; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MainWindowArchitectureTest { + + @Test + void settingsIsAUniqueHeaderActionInsteadOfANavigationItem() throws Exception { + String application = source("src/main/java/com/example/workutils/WorkUtilsApplication.java"); + String buildNavItems = between(application, + "private List buildNavItems()", + "// ==================== 内嵌优先打开"); + String mainWindow = source("src/main/java/com/example/workutils/system/ui/MainWindow.java"); + + assertFalse(buildNavItems.contains("__settings__")); + assertFalse(buildNavItems.contains("系统设置")); + assertTrue(application.contains("mainWindow.refreshNavItems(buildNavItems())")); + assertTrue(application.contains( + "new MainWindow(navItems, properties, this::openSettingsEmbedded)")); + assertTrue(mainWindow.contains( + "MainWindow(List navItems, WorkUtilsProperties properties, Runnable openSettings)")); + assertTrue(mainWindow.contains("IconFactory.createButtonIcon(AppIcon.SETTINGS")); + assertTrue(mainWindow.contains("openSettings.run()")); + } + + @Test + void headerUsesDesignTokensAndFeatherSemanticsWithoutCustomWindowChrome() throws Exception { + String mainWindow = source("src/main/java/com/example/workutils/system/ui/MainWindow.java"); + String header = between(mainWindow, "private HBox buildUnifiedHeader", "private void navigateTo"); + + assertTrue(header.contains("DesignTokens.HEADER_HEIGHT")); + assertTrue(header.contains("IconFactory.createButtonIcon(AppIcon.MENU")); + assertTrue(header.contains("navigationRail.toggle()")); + assertTrue(header.contains("IconFactory.create(AppIcon.APP")); + assertTrue(header.contains("IconFactory.create(AppIcon.CHEVRON_RIGHT")); + assertTrue(header.contains("IconFactory.createButtonIcon(AppIcon.SETTINGS")); + assertEquals(1, occurrences(header, "AppIcon.SETTINGS")); + assertEquals(1, occurrences(header, "openSettings.run()")); + assertFalse(header.contains("SVGPath")); + assertFalse(header.contains("= 0, () -> "Missing source marker: " + start); + assertTrue(endIndex > startIndex, () -> "Missing source marker: " + end); + return source.substring(startIndex, endIndex); + } + + private static int occurrences(String source, String needle) { + return source.split(java.util.regex.Pattern.quote(needle), -1).length - 1; + } +}