修复应用外壳运行时样式与导航契约

This commit is contained in:
张家训
2026-08-01 16:52:08 +08:00
parent ce211c82a4
commit 855195e286
9 changed files with 441 additions and 179 deletions
@@ -114,15 +114,6 @@ public class WorkUtilsApplication implements CommandLineRunner {
settingsService.setNavItemsRefresher(ignoredItems -> Platform.runLater(() -> {
if (mainWindow != null) {
mainWindow.refreshNavItems(buildNavItems());
// 重新绑定插件面板
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);
}
}
}
}));
@@ -116,6 +116,8 @@ public class MainWindow {
Button menuButton = new Button(null, IconFactory.createButtonIcon(AppIcon.MENU, "切换导航栏"));
menuButton.setId("menu-button");
menuButton.setTooltip(new Tooltip("切换导航栏"));
menuButton.setAccessibleText("切换导航栏");
menuButton.setFocusTraversable(true);
menuButton.setOnAction(event -> navigationRail.toggle());
Label brand = new Label("WorkUtils", IconFactory.create(AppIcon.APP, 20));
@@ -135,9 +137,12 @@ public class MainWindow {
HBox.setHgrow(spacerRight, Priority.ALWAYS);
Button settingsButton = new Button(
null, IconFactory.createButtonIcon(AppIcon.SETTINGS, "系统设置"));
"系统设置", IconFactory.createButtonIcon(AppIcon.SETTINGS, "系统设置"));
settingsButton.setId("settings-button");
settingsButton.setTooltip(new Tooltip("系统设置"));
settingsButton.setAccessibleText("系统设置");
settingsButton.setContentDisplay(ContentDisplay.LEFT);
settingsButton.setFocusTraversable(true);
settingsButton.setOnAction(event -> openSettings.run());
bar.getChildren().addAll(
@@ -12,14 +12,11 @@ import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.ArrayList;
@@ -41,7 +38,6 @@ public final class NavigationRail extends VBox {
private final List<ItemView> itemViews = new ArrayList<>();
private final Map<String, ItemView> viewsByPluginId = new LinkedHashMap<>();
private final VBox itemBox = new VBox();
private final Label brandLabel = new Label("WorkUtils");
private final Button collapseButton = new Button();
private MainWindow.NavItem selectedItem;
@@ -53,10 +49,13 @@ public final class NavigationRail extends VBox {
this.navigator = Objects.requireNonNull(navigator, "navigator must not be null");
getStyleClass().addAll(Theme.CSS_NAV_BAR, "navigation-rail");
buildHeader();
itemBox.getStyleClass().add("navigation-rail-items");
VBox.setVgrow(itemBox, Priority.ALWAYS);
getChildren().addAll(new Separator(), itemBox);
buildBottomToggle();
Separator separator = new Separator();
separator.getStyleClass().add(Theme.CSS_NAV_SEP);
getChildren().addAll(itemBox, separator, collapseButton);
expanded.addListener((observable, oldValue, newValue) -> updateAppearance());
setItems(items);
@@ -131,35 +130,31 @@ public final class NavigationRail extends VBox {
return view == null ? null : view.tooltip();
}
private void buildHeader() {
HBox header = new HBox(Theme.S2);
header.setAlignment(Pos.CENTER_LEFT);
header.setMinHeight(DesignTokens.HEADER_HEIGHT);
header.setPrefHeight(DesignTokens.HEADER_HEIGHT);
header.setMaxHeight(DesignTokens.HEADER_HEIGHT);
brandLabel.setGraphic(IconFactory.create(AppIcon.APP, 18));
brandLabel.setContentDisplay(ContentDisplay.LEFT);
brandLabel.setFont(Theme.sectionTitle());
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
collapseButton.getStyleClass().add("navigation-rail-toggle");
private void buildBottomToggle() {
collapseButton.setId("navigation-rail-toggle");
collapseButton.getStyleClass().addAll(Theme.CSS_NAV_CELL, "navigation-rail-toggle");
collapseButton.setContentDisplay(ContentDisplay.LEFT);
collapseButton.setGraphicTextGap(Theme.S3);
collapseButton.setMinHeight(DesignTokens.ROW_HEIGHT);
collapseButton.setPrefHeight(DesignTokens.ROW_HEIGHT);
collapseButton.setMaxHeight(DesignTokens.ROW_HEIGHT);
collapseButton.setMaxWidth(Double.MAX_VALUE);
collapseButton.setFocusTraversable(true);
collapseButton.setOnAction(event -> toggle());
header.getChildren().addAll(brandLabel, spacer, collapseButton);
getChildren().add(header);
}
private void addItem(MainWindow.NavItem item, int index) {
ToggleButton button = new ToggleButton(item.name, IconFactory.createButtonIcon(item.icon, item.name));
button.setToggleGroup(selection);
button.setContentDisplay(ContentDisplay.LEFT);
button.setGraphicTextGap(Theme.S3);
button.setAlignment(Pos.CENTER_LEFT);
button.setMinHeight(DesignTokens.ROW_HEIGHT);
button.setPrefHeight(DesignTokens.ROW_HEIGHT);
button.setMaxHeight(DesignTokens.ROW_HEIGHT);
button.setMaxWidth(Double.MAX_VALUE);
button.setAccessibleText(item.name);
button.setFocusTraversable(true);
button.getStyleClass().add(Theme.CSS_NAV_CELL);
if (item.pluginId != null) {
button.setId("nav-" + item.pluginId);
@@ -201,12 +196,15 @@ public final class NavigationRail extends VBox {
setMaxWidth(width);
pseudoClassStateChanged(COLLAPSED, !isExpanded());
brandLabel.setVisible(isExpanded());
brandLabel.setManaged(isExpanded());
String collapseLabel = isExpanded() ? "收起" : "展开";
String accessibleText = collapseLabel + "导航栏";
collapseButton.setGraphic(IconFactory.createButtonIcon(
isExpanded() ? AppIcon.CHEVRON_LEFT : AppIcon.CHEVRON_RIGHT,
isExpanded() ? "折叠导航" : "展开导航"));
collapseButton.setTooltip(new Tooltip(isExpanded() ? "折叠导航" : "展开导航"));
accessibleText));
collapseButton.setText(isExpanded() ? collapseLabel : null);
collapseButton.setAccessibleText(accessibleText);
collapseButton.setTooltip(new Tooltip(accessibleText));
collapseButton.setAlignment(isExpanded() ? Pos.CENTER_LEFT : Pos.CENTER);
updateItemAppearance();
}
@@ -216,6 +214,7 @@ public final class NavigationRail extends VBox {
ToggleButton button = view.button();
button.setText(isExpanded() ? item.name : null);
button.setTooltip(isExpanded() ? null : view.tooltip());
button.setAccessibleText(item.name);
button.setAlignment(isExpanded() ? Pos.CENTER_LEFT : Pos.CENTER);
}
}
+49 -139
View File
@@ -18,17 +18,8 @@
-workutils-success: #10b981;
-workutils-primary-light: #dbeafe;
-workutils-font-ui: "Segoe UI";
-workutils-font-mono: "Consolas";
-workutils-font-page-title: 20;
-workutils-font-section-title: 15;
-workutils-font-body: 14;
-workutils-font-secondary: 13;
-workutils-row-height: 40;
-workutils-control-height: 36;
-workutils-header-height: 48;
-fx-font-family: -workutils-font-ui;
-fx-font-size: -workutils-font-body;
-fx-font-family: "Segoe UI";
-fx-font-size: 14px;
}
/* ---- 导航栏 ---- */
@@ -38,145 +29,64 @@
-fx-border-width: 0 1 0 0;
}
.nav-brand {
-fx-text-fill: -workutils-text;
-fx-padding: 16 14 16 14;
-fx-font-weight: 600;
}
.nav-separator {
-fx-background-color: -workutils-border;
}
/* ============ ListView 导航(JavaFX 标准化方案) ============ */
.nav-list {
-fx-background-color: transparent;
-fx-background-insets: 0;
-fx-border-width: 0;
-fx-padding: 0;
/* ============ ToggleButton 导航 ============ */
.navigation-rail-items {
-fx-padding: 8px 0;
}
/* 强制 ListView 所有内部容器透明,确保空区域不显示杂色 */
.nav-list,
.nav-list > *,
.nav-list .list-view,
.nav-list .viewport,
.nav-list .virtual-flow,
.nav-list .virtual-flow > *,
.nav-list .clipped-container,
.nav-list .sheet,
.nav-list .corner {
-fx-background-color: transparent;
-fx-background-insets: 0;
}
.nav-list .list-cell,
.nav-list .list-cell:empty,
.nav-list .list-cell:filled:empty {
-fx-background-color: transparent;
}
/* 滚动条美化 */
.nav-list .scroll-bar:vertical {
-fx-background-color: transparent;
-fx-pref-width: 6;
}
.nav-list .scroll-bar .thumb {
-fx-background-color: -workutils-border;
-fx-background-radius: 3;
}
.nav-list .scroll-bar .thumb:hover {
-fx-background-color: -workutils-text-muted;
}
.nav-list .scroll-bar .increment-button,
.nav-list .scroll-bar .decrement-button {
-fx-background-color: transparent;
-fx-padding: 0;
}
/* 每个导航项(Cell) */
.nav-cell {
-fx-background-color: transparent;
-fx-background-radius: 0;
-fx-background-insets: 0;
-fx-padding: 10 14 10 14;
-fx-cursor: hand;
-fx-border-width: 0;
/* 平滑过渡背景色和文字色 */
}
.nav-cell .label {
-fx-border-color: transparent;
-fx-border-width: 0 0 0 3px;
-fx-border-radius: 0;
-fx-padding: 0 14px 0 11px;
-fx-text-fill: -workutils-text-secondary;
/* 深色背景下的文字阴影,提升对比度 */
-fx-font-size: 14px;
-fx-font-weight: 500;
-fx-graphic-text-gap: 12px;
-fx-cursor: hand;
}
/* 移除 JavaFX ListCell 默认焦点环(让选中态边框干净) */
.nav-list .list-cell {
.nav-cell .app-icon {
-fx-icon-color: -workutils-text-secondary;
}
/* 强制空 cell 完全透明(含 placeholder、footer 等场景) */
.nav-list .list-cell:empty,
.nav-list .list-cell:filled:empty {
-fx-background-color: transparent !important;
}
/* hover 状态 */
.nav-cell:hover {
-fx-background-color: -workutils-card;
}
/* 兜底:所有 selected cell(含 empty)默认透明,防止虚拟化复用时空 cell 闪现蓝色 */
.nav-list .list-cell:selected {
-fx-background-color: transparent;
}
/* selected 状态:强制 padding、background-insets、文字色一致
关键:用 :filled:selected 而非自定义 class,避免 JavaFX 内置样式覆盖 */
.nav-list .list-cell:filled:selected {
-fx-background-color: -workutils-primary-light;
-fx-background-insets: 0;
-fx-padding: 10 14 10 14;
}
.nav-list .list-cell:filled:selected .label {
-fx-text-fill: -workutils-primary !important;
-fx-font-weight: 600;
/* 移除 dropshadow,让文字干净锐利 */
}
.nav-list .list-cell:filled:selected .nav-indicator {
-fx-fill: #2563eb;
}
/* 兼容:之前手动加的 .nav-cell-active 类名也支持 */
.nav-cell:selected,
.nav-cell-active {
-fx-background-color: -workutils-primary-light;
-fx-background-insets: 0;
-fx-padding: 10 14 10 14;
-fx-border-color: -workutils-primary;
-fx-border-width: 0 0 0 3px;
-fx-text-fill: -workutils-primary;
-fx-font-weight: 600;
}
.nav-cell:selected .app-icon,
.nav-cell-active .app-icon {
-fx-icon-color: -workutils-primary;
}
/* 按下状态:微妙的反馈 */
.nav-cell:pressed {
-fx-background-color: -workutils-primary-light;
}
/* ListView 自身获得焦点时也保持透明,不显示蓝色环 */
/* 左侧指示条 */
.nav-indicator {
-fx-fill: transparent;
-fx-arc-width: 2;
-fx-arc-height: 2;
.nav-cell:focused {
-fx-border-color: -workutils-primary;
-fx-border-width: 1px 1px 1px 3px;
}
.nav-cell-active .nav-indicator {
-fx-fill: #2563eb;
}
/* 点击涟漪层 */
.nav-version {
-fx-text-fill: -workutils-text-muted;
-fx-padding: 10 14 10 14;
-fx-padding: 10px 14px;
}
/* ---- 顶栏 ---- */
@@ -184,9 +94,9 @@
-fx-background-color: -workutils-surface;
-fx-border-width: 0 0 1 0;
-fx-border-color: -workutils-border;
-fx-pref-height: -workutils-header-height;
-fx-min-height: -workutils-header-height;
-fx-max-height: -workutils-header-height;
-fx-pref-height: 48px;
-fx-min-height: 48px;
-fx-max-height: 48px;
}
/* ---- 内容区 ---- */
@@ -204,18 +114,18 @@
}
.page-title {
-fx-font-size: -workutils-font-page-title;
-fx-font-size: 20px;
-fx-font-weight: 600;
-fx-text-fill: -workutils-text;
}
.body-text {
-fx-font-size: -workutils-font-body;
-fx-font-size: 14px;
-fx-text-fill: -workutils-text;
}
.secondary-text {
-fx-font-size: -workutils-font-secondary;
-fx-font-size: 13px;
-fx-text-fill: -workutils-text-secondary;
}
@@ -226,8 +136,8 @@
.control-default,
.text-field-default,
.combo-default {
-fx-pref-height: -workutils-control-height;
-fx-min-height: -workutils-control-height;
-fx-pref-height: 36px;
-fx-min-height: 36px;
}
/* ---- 按钮增强 ---- */
@@ -306,8 +216,8 @@
-fx-border-width: 1;
-fx-border-radius: 4;
-fx-background-radius: 4;
-fx-font-family: -workutils-font-mono;
-fx-font-size: -workutils-font-body;
-fx-font-family: "Consolas";
-fx-font-size: 13px;
}
/* ---- 状态栏 ---- */
@@ -339,25 +249,25 @@
-fx-border-color: -workutils-border;
-fx-border-width: 1;
-fx-border-radius: 4;
-fx-fixed-cell-size: -workutils-row-height;
-fx-font-size: -workutils-font-body;
-fx-fixed-cell-size: 40px;
-fx-font-size: 13px;
}
/* ---- 标题 ---- */
.section-title {
-fx-font-size: -workutils-font-section-title;
-fx-font-size: 15px;
-fx-font-weight: 600;
-fx-text-fill: -workutils-text;
}
.hint-text {
-fx-text-fill: -workutils-text-secondary;
-fx-font-size: -workutils-font-secondary;
-fx-font-size: 13px;
}
.muted-text {
-fx-text-fill: -workutils-text-muted;
-fx-font-size: -workutils-font-secondary;
-fx-font-size: 13px;
}
.danger-text {
@@ -421,13 +331,13 @@
-fx-border-color: -workutils-border;
-fx-border-width: 1;
-fx-border-radius: 4;
-fx-fixed-cell-size: -workutils-row-height;
-fx-font-size: -workutils-font-body;
-fx-fixed-cell-size: 40px;
-fx-font-size: 13px;
}
/* ---- 代码文本 ---- */
.mono-text {
-fx-font-family: -workutils-font-mono;
-fx-font-family: "Consolas";
}
/* ---- 主题切换按钮 ---- */
@@ -439,7 +349,7 @@
/* ---- 紧凑列表单元格 ---- */
.compact-list .list-cell {
-fx-padding: 3 8 3 8;
-fx-font-size: -workutils-font-secondary;
-fx-font-size: 13px;
}
.compact-list .list-cell:selected {
@@ -473,7 +383,7 @@
}
.apidoc-tab-pane .tab .tab-label {
-fx-text-fill: -workutils-text-secondary;
-fx-font-size: -workutils-font-body;
-fx-font-size: 14px;
-fx-font-weight: normal;
}
.apidoc-tab-pane .tab:selected {
+1 -1
View File
@@ -1,5 +1,5 @@
# ====== WorkUtils 插件配置文件 ======
# 插件顺序(按逗号分隔,顺序即菜单显示顺序)
# 插件状态(disabled 列表,不在列表中的即为启用)
plugins.order=scan,diff,api-doc,git-sync,git-review,mobile-ui-auto,mybatisexport,mybatisgenerator
plugins.order=git-sync,git-review,diff,mybatisexport,mybatisgenerator,scan,api-doc
plugins.disabled=mobile-ui-auto