菜单重构完成
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# WorkUtils UI 开发规则
|
||||
|
||||
## 1. CSS 优先级铁律(AtlantaFX 环境)
|
||||
|
||||
### 文本颜色
|
||||
- **禁止**纯 CSS 设置导航栏选中文字色,AtlantaFX `.label` 会覆盖(含 `!important`)。
|
||||
- **正确做法**:Java 代码中 `label.setTextFill(Color.WHITE)` 直接设置。
|
||||
|
||||
### 背景透明
|
||||
- ListView 内部容器透明需覆盖完整链路:`.list-view`, `.viewport`, `.virtual-flow`, `.virtual-flow > *`, `.clipped-container`, `.sheet`, `.corner`。
|
||||
- 同时用 Java 代码 `setStyle("-fx-background-color: transparent;")` 做 inline 兜底。
|
||||
|
||||
### 选中态
|
||||
- **必须同时覆盖** `.list-cell:selected` 和 `.list-cell:filled:selected`。
|
||||
- `.list-cell:selected` 设置透明背景兜底 → 防止虚拟化复用时空 cell 闪现蓝色。
|
||||
- `.list-cell:filled:selected` 设置激活背景 → CSS 优先级更高,非空选中 cell 生效。
|
||||
|
||||
## 2. ListView 虚拟化陷阱
|
||||
|
||||
- ListView 使用 VirtualFlow,cell 会被复用。每次 `updateItem(empty=true)` 时:
|
||||
- 必须清理自定义 style class(先 removeAll 再按需 add)。
|
||||
- 禁止 empty 分支中直接用 `setAll()`(会误删 JavaFX 内置 `list-cell` class)。
|
||||
- `selectedProperty()` listener 中调用 `updateStyleClass()` 时,要兼容 empty 状态。
|
||||
|
||||
## 3. 布局规则
|
||||
|
||||
- ListView 导航栏使用 `VBox.setVgrow(navList, Priority.ALWAYS)` 撑满剩余空间。
|
||||
- **不要**在 `refreshNavItems()` 中重新设置 prefHeight/minHeight/maxHeight,会与 grow 冲突。
|
||||
- 滚动条自然出现在 ListView 右侧底部,无需额外处理。
|
||||
|
||||
## 4. 修改验证流程
|
||||
|
||||
修改 UI 样式后:
|
||||
1. 先检查 AtlantaFX/modena.css 是否有同名规则覆盖。
|
||||
2. 优先使用 Java 端代码设置(`setStyle`/`setTextFill`),再补充 CSS。
|
||||
3. 对虚拟化组件(ListView/TableView),验证 resize 窗口场景。
|
||||
@@ -69,6 +69,51 @@ rules:
|
||||
action: "suggest"
|
||||
suggestion: "echo 'export/' >> .gitignore"
|
||||
|
||||
# 7. 复杂重构分步提交 —— 避免会话中断丢失工作
|
||||
- id: refactor-step-by-step
|
||||
description: |
|
||||
涉及 ≥4 个文件 或 ≥200 行变更 的复杂重构,必须分多个 commit 完成,避免单次会话中断丢失全部工作。
|
||||
典型场景:
|
||||
- 字段/类重命名(如 navButtons → navList)会触发多文件交叉引用更新
|
||||
- 组件替换(Button → ListView)涉及字段、方法、引用、样式、测试多文件
|
||||
- 框架/库升级涉及配置文件、依赖、调用点、文档
|
||||
推荐步骤:
|
||||
① 第一步:加新常量/CSS/配置项(与旧实现共存)
|
||||
② 第二步:替换实现/切换调用点
|
||||
③ 第三步:删除旧代码(清理 commit)
|
||||
每步完成后提交,便于回滚。
|
||||
condition: "涉及文件数 >= 4 OR 变更行数 >= 200"
|
||||
action: "warn"
|
||||
severity: high
|
||||
|
||||
# 8. AtlantaFX UI 防中断规则 —— 修改 MainWindow/Theme/CSS 时必读
|
||||
- id: atlantafx-ui-pitfalls
|
||||
description: |
|
||||
修改 MainWindow.java、Theme.java 或 light-theme.css 时,必须遵守以下规则,避免反复修改同一问题导致会话中断:
|
||||
|
||||
【CSS 优先级】
|
||||
- AtlantaFX 的 .label 规则优先级极高,!important 也会被覆盖
|
||||
- 导航栏选中文字色:必须用 Java 代码 label.setTextFill(Color.WHITE),不能靠 CSS
|
||||
- ListView 选中态:必须同时覆盖 .list-cell:selected(为空 cell 兜底)和 .list-cell:filled:selected(为非空 cell 设置激活背景)
|
||||
- 背景透明:需要完整覆盖 .list-view → .virtual-flow → .clipped-container → .sheet 全链路
|
||||
|
||||
【ListView 虚拟化】
|
||||
- cell 会被 VirtualFlow 复用,updateItem(empty=true) 时必须清理自定义 style class
|
||||
- 不要用 getStyleClass().setAll(),会误删 JavaFX 内置 list-cell class;用 removeAll + add
|
||||
- 窗口 resize 时 cell 复用会暴露 empty cell 被 selected 的问题
|
||||
|
||||
【布局】
|
||||
- 导航 ListView 用 VBox.setVgrow(ALWAYS) 撑满
|
||||
- refreshNavItems() 中不要重新设置 prefHeight/minHeight/maxHeight
|
||||
|
||||
【修改后自检清单】
|
||||
① 小窗口(菜单项少)→ 无滚动条,无底部蓝块
|
||||
② 选中菜单 → 文字白色,背景蓝色,左侧蓝条可见
|
||||
③ 最大化窗口 → 无额外蓝色块闪现,滚动条在导航栏底部
|
||||
files: ["MainWindow.java", "Theme.java", "light-theme.css"]
|
||||
action: "warn"
|
||||
severity: high
|
||||
|
||||
# ===== 触发提示 =====
|
||||
# 当规则被触发时,AI 会看到这些提示
|
||||
messages:
|
||||
@@ -79,3 +124,10 @@ messages:
|
||||
large-file-check: |
|
||||
⚠️ 目标文件 >500KB,可能是压缩产物或大型导出文件。
|
||||
建议使用 head/tail 检查结构,或通过单元测试验证行为。
|
||||
refactor-step-by-step: |
|
||||
⚠️ 检测到复杂重构(≥4 文件 或 ≥200 行)。
|
||||
建议分步提交:
|
||||
① 第一步:加新常量/CSS(与旧实现共存)
|
||||
② 第二步:替换实现/切换调用点
|
||||
③ 第三步:删除旧代码
|
||||
每步完成后 git commit,避免会话中断丢失工作。
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.example.workutils.system.ui;
|
||||
|
||||
import com.example.workutils.system.config.WorkUtilsProperties;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -10,11 +14,15 @@ import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
@@ -22,7 +30,7 @@ import java.util.*;
|
||||
/**
|
||||
* 工作工具集主窗口 —— AtlantaFX 重构版
|
||||
* <p>
|
||||
* 左侧导航栏 + 右侧内容区,使用 CSS 类控制样式。
|
||||
* 左侧导航栏(ListView)+ 右侧内容区,使用 CSS 类控制样式。
|
||||
*/
|
||||
public class MainWindow {
|
||||
|
||||
@@ -31,6 +39,7 @@ public class MainWindow {
|
||||
private final WorkUtilsProperties properties;
|
||||
|
||||
private VBox navBox;
|
||||
private ListView<NavItem> navList;
|
||||
private StackPane contentArea;
|
||||
private NavItem activeNavItem;
|
||||
private String activePluginId;
|
||||
@@ -44,7 +53,6 @@ public class MainWindow {
|
||||
private Label appLabel;
|
||||
private Label welcomeIcon, welcomeHeading, welcomeDesc;
|
||||
|
||||
private final List<Button> navButtons = new ArrayList<>();
|
||||
private final Map<String, EmbeddedEntry> embeddedCache = new LinkedHashMap<>();
|
||||
|
||||
/** 响应式缩放因子 */
|
||||
@@ -71,15 +79,23 @@ public class MainWindow {
|
||||
private void updateScale(double width) {
|
||||
if (Double.isNaN(width) || width <= 0) return;
|
||||
double newScale = Math.max(0.85, Math.min(1.4, width / 1280.0));
|
||||
if (Math.abs(newScale - responsiveScale) > 0.025) {
|
||||
responsiveScale = newScale;
|
||||
refreshResponsive();
|
||||
}
|
||||
// debounce: resize 时频繁触发,延迟 150ms 才真正刷新,避免 UI 卡顿
|
||||
if (scaleDebounce != null) scaleDebounce.stop();
|
||||
scaleDebounce = new javafx.animation.Timeline(
|
||||
new javafx.animation.KeyFrame(javafx.util.Duration.millis(150),
|
||||
e -> {
|
||||
if (Math.abs(newScale - responsiveScale) > 0.025) {
|
||||
responsiveScale = newScale;
|
||||
refreshResponsive();
|
||||
}
|
||||
}));
|
||||
scaleDebounce.play();
|
||||
}
|
||||
|
||||
private javafx.animation.Timeline scaleDebounce;
|
||||
|
||||
private void refreshResponsive() {
|
||||
appLabel.setFont(rbold(14));
|
||||
for (Button btn : navButtons) btn.setFont(rfont(13));
|
||||
|
||||
double navMin = computeNavMinBaseWidth() * responsiveScale;
|
||||
navBox.setMinWidth(navMin);
|
||||
@@ -97,6 +113,9 @@ public class MainWindow {
|
||||
|
||||
toolbarIcon.setFont(rfont(18));
|
||||
toolbarTitle.setFont(rbold(14));
|
||||
|
||||
// ListView 自动处理 cell 渲染;cell 内部用 rfont 在 updateItem 中读取最新 scale
|
||||
if (navList != null) navList.refresh();
|
||||
}
|
||||
|
||||
private double computeNavMinBaseWidth() {
|
||||
@@ -108,34 +127,12 @@ public class MainWindow {
|
||||
* 刷新导航栏(插件启用/禁用后调用)
|
||||
*/
|
||||
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);
|
||||
if (navList != null) {
|
||||
navList.setItems(FXCollections.observableArrayList(navItems));
|
||||
navList.getSelectionModel().clearSelection();
|
||||
}
|
||||
|
||||
// 弹性空间
|
||||
Region spacer = new Region();
|
||||
VBox.setVgrow(spacer, Priority.ALWAYS);
|
||||
navBox.getChildren().add(spacer);
|
||||
|
||||
// 回到欢迎页
|
||||
showWelcomePage();
|
||||
}
|
||||
|
||||
@@ -277,87 +274,144 @@ public class MainWindow {
|
||||
sep.setPadding(new Insets(0));
|
||||
navBox.getChildren().add(sep);
|
||||
|
||||
// 导航按钮
|
||||
for (NavItem item : navItems) {
|
||||
Button btn = createNavButton(item);
|
||||
navButtons.add(btn);
|
||||
navBox.getChildren().add(btn);
|
||||
}
|
||||
// ====== 标准化 ListView 导航 ======
|
||||
navList = new ListView<>(FXCollections.observableArrayList(navItems));
|
||||
navList.getStyleClass().add(Theme.CSS_NAV_LIST);
|
||||
navList.setStyle("-fx-background-color: transparent;"); // inline 兜底
|
||||
navList.setCellFactory(lv -> new NavListCell());
|
||||
navList.getSelectionModel().selectedItemProperty().addListener(
|
||||
(obs, old, newVal) -> {
|
||||
if (newVal != null) {
|
||||
this.activeNavItem = newVal;
|
||||
if (newVal.action != null) newVal.action.run();
|
||||
showEmbeddedFor(newVal);
|
||||
// 阻止 ListView 默认把选中项滚到中部
|
||||
// 延迟到当前事件循环结束后,让我们的滚动覆盖默认行为
|
||||
Platform.runLater(() -> {
|
||||
int idx = navList.getSelectionModel().getSelectedIndex();
|
||||
if (idx > 0) navList.scrollTo(idx - 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 弹性空间
|
||||
Region spacer = new Region();
|
||||
VBox.setVgrow(spacer, Priority.ALWAYS);
|
||||
navBox.getChildren().add(spacer);
|
||||
// ListView 撑满 VBox 剩余空间,内容溢出时自动滚动
|
||||
VBox.setVgrow(navList, Priority.ALWAYS);
|
||||
navBox.getChildren().add(navList);
|
||||
|
||||
return navBox;
|
||||
}
|
||||
|
||||
private Button createNavButton(NavItem item) {
|
||||
Button btn = new Button();
|
||||
btn.setMaxWidth(Double.MAX_VALUE);
|
||||
btn.setAlignment(Pos.CENTER_LEFT);
|
||||
btn.setPadding(new Insets(12, 16, 12, 16));
|
||||
btn.setCursor(Cursor.HAND);
|
||||
/**
|
||||
* 自定义导航 Cell —— 包含图标、文本、左侧指示条、点击涟漪。
|
||||
* 配合 ListView 自带的选择/键盘/滚动行为。
|
||||
*/
|
||||
private class NavListCell extends ListCell<NavItem> {
|
||||
|
||||
// 使用 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));
|
||||
iconLabel.setTextFill(Color.WHITE);
|
||||
Label nameLabel = new Label(item.name);
|
||||
nameLabel.setFont(rfont(13));
|
||||
nameLabel.setTextFill(Color.WHITE);
|
||||
content.getChildren().addAll(iconLabel, nameLabel);
|
||||
btn.setGraphic(content);
|
||||
btn.setText("");
|
||||
private final Rectangle indicator;
|
||||
private final Label iconLabel;
|
||||
private final Label nameLabel;
|
||||
private final Circle ripple;
|
||||
private final StackPane cellRoot;
|
||||
|
||||
btn.getStyleClass().add(Theme.CSS_NAV_BTN);
|
||||
NavListCell() {
|
||||
super();
|
||||
|
||||
btn.setOnMouseEntered(e -> {
|
||||
// AtlantaFX 默认 hover 效果 + 我们增强
|
||||
});
|
||||
btn.setOnMouseExited(e -> {
|
||||
// AtlantaFX 恢复
|
||||
});
|
||||
btn.setOnAction(e -> { selectNavItem(item); if (item.action != null) item.action.run(); });
|
||||
indicator = new Rectangle(4, 28);
|
||||
indicator.getStyleClass().add(Theme.CSS_NAV_INDICATOR);
|
||||
indicator.setMouseTransparent(true);
|
||||
|
||||
return btn;
|
||||
}
|
||||
iconLabel = new Label();
|
||||
iconLabel.setPrefWidth(24);
|
||||
iconLabel.setAlignment(Pos.CENTER_LEFT);
|
||||
iconLabel.setMouseTransparent(true);
|
||||
|
||||
private void selectNavItem(NavItem item) {
|
||||
this.activeNavItem = item;
|
||||
Color white = Color.WHITE;
|
||||
Color activeColor = Color.web("#60a5fa");
|
||||
for (int i = 0; i < navItems.size(); i++) {
|
||||
Button btn = navButtons.get(i);
|
||||
if (navItems.get(i) == item) {
|
||||
btn.getStyleClass().remove(Theme.CSS_NAV_BTN);
|
||||
btn.getStyleClass().add(Theme.CSS_NAV_BTN_ACTIVE);
|
||||
btn.setStyle("");
|
||||
setNavButtonTextColor(btn, activeColor);
|
||||
} else {
|
||||
btn.getStyleClass().remove(Theme.CSS_NAV_BTN_ACTIVE);
|
||||
if (!btn.getStyleClass().contains(Theme.CSS_NAV_BTN)) {
|
||||
btn.getStyleClass().add(Theme.CSS_NAV_BTN);
|
||||
nameLabel = new Label();
|
||||
nameLabel.setMouseTransparent(true);
|
||||
|
||||
// 涟漪层(点击时扩散)
|
||||
ripple = new Circle(0);
|
||||
ripple.getStyleClass().add(Theme.CSS_NAV_RIPPLE);
|
||||
ripple.setMouseTransparent(true);
|
||||
// 关键:不参与 layout,防止动画过程撑大 StackPane 高度
|
||||
ripple.setManaged(false);
|
||||
|
||||
HBox content = new HBox(10, indicator, iconLabel, nameLabel);
|
||||
content.setAlignment(Pos.CENTER_LEFT);
|
||||
content.setMouseTransparent(true);
|
||||
|
||||
cellRoot = new StackPane(ripple, content);
|
||||
cellRoot.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
setGraphic(cellRoot);
|
||||
setText(null);
|
||||
getStyleClass().add(Theme.CSS_NAV_CELL);
|
||||
|
||||
// 监听 selected 变化以更新 CSS class(必须在 ripple 初始化之后)
|
||||
// 直接设置 textFill 绕过 AtlantaFX CSS 优先级问题
|
||||
selectedProperty().addListener((obs, old, isSelected) -> {
|
||||
updateStyleClass();
|
||||
if (isSelected) {
|
||||
ripple.setOpacity(0);
|
||||
}
|
||||
btn.setStyle("");
|
||||
setNavButtonTextColor(btn, white);
|
||||
}
|
||||
// 强制 text-fill:深色背景统一用白色
|
||||
nameLabel.setTextFill(Color.WHITE);
|
||||
iconLabel.setTextFill(Color.WHITE);
|
||||
});
|
||||
|
||||
// 点击涟漪动画
|
||||
addEventHandler(MouseEvent.MOUSE_PRESSED, this::onCellPressed);
|
||||
}
|
||||
showEmbeddedFor(item);
|
||||
}
|
||||
|
||||
/** 递归设置导航按钮内所有 Label 的文字颜色 */
|
||||
private void setNavButtonTextColor(Button btn, Color color) {
|
||||
if (btn.getGraphic() instanceof HBox) {
|
||||
for (javafx.scene.Node child : ((HBox) btn.getGraphic()).getChildren()) {
|
||||
if (child instanceof Label) {
|
||||
((Label) child).setTextFill(color);
|
||||
}
|
||||
@Override
|
||||
protected void updateItem(NavItem item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
updateStyleClass();
|
||||
return;
|
||||
}
|
||||
iconLabel.setText(item.icon);
|
||||
iconLabel.setFont(rfont(13));
|
||||
nameLabel.setText(item.name);
|
||||
nameLabel.setFont(rfont(13));
|
||||
// 直接在代码中强制 text-fill,深色导航栏统一用白色
|
||||
nameLabel.setTextFill(Color.WHITE);
|
||||
iconLabel.setTextFill(Color.WHITE);
|
||||
setGraphic(cellRoot);
|
||||
updateStyleClass();
|
||||
}
|
||||
|
||||
private void updateStyleClass() {
|
||||
getStyleClass().removeAll(Theme.CSS_NAV_CELL, Theme.CSS_NAV_CELL_ACTIVE);
|
||||
if (isEmpty()) return;
|
||||
boolean active = isSelected();
|
||||
getStyleClass().add(active ? Theme.CSS_NAV_CELL_ACTIVE : Theme.CSS_NAV_CELL);
|
||||
}
|
||||
|
||||
/** 点击涟漪扩散动画 */
|
||||
private void onCellPressed(MouseEvent e) {
|
||||
if (isEmpty()) return;
|
||||
double x = e.getX();
|
||||
double y = e.getY();
|
||||
ripple.setCenterX(x);
|
||||
ripple.setCenterY(y);
|
||||
ripple.setRadius(0);
|
||||
ripple.setOpacity(0.6);
|
||||
|
||||
double maxRadius = Math.max(getWidth(), getHeight());
|
||||
Timeline anim = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(ripple.radiusProperty(), 0)),
|
||||
new KeyFrame(Duration.millis(500),
|
||||
new KeyValue(ripple.radiusProperty(), maxRadius),
|
||||
new KeyValue(ripple.opacityProperty(), 0))
|
||||
);
|
||||
// 关键:动画结束必须重置 radius 为 0,否则 Circle 会撑大 StackPane 高度
|
||||
anim.setOnFinished(ev -> {
|
||||
ripple.setRadius(0);
|
||||
ripple.setOpacity(0);
|
||||
});
|
||||
anim.play();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,8 +97,11 @@ public final class Theme {
|
||||
public static final String CSS_NAV_BAR = "nav-bar";
|
||||
public static final String CSS_NAV_BRAND = "nav-brand";
|
||||
public static final String CSS_NAV_SEP = "nav-separator";
|
||||
public static final String CSS_NAV_BTN = "nav-btn";
|
||||
public static final String CSS_NAV_BTN_ACTIVE = "nav-btn-active";
|
||||
public static final String CSS_NAV_LIST = "nav-list";
|
||||
public static final String CSS_NAV_CELL = "nav-cell";
|
||||
public static final String CSS_NAV_CELL_ACTIVE = "nav-cell-active";
|
||||
public static final String CSS_NAV_INDICATOR = "nav-indicator";
|
||||
public static final String CSS_NAV_RIPPLE = "nav-ripple";
|
||||
public static final String CSS_NAV_VERSION = "nav-version";
|
||||
|
||||
public static final String CSS_HEADER_BAR = "header-bar";
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
-workutils-nav-bg: #1e293b;
|
||||
-workutils-nav-text: #ffffff;
|
||||
-workutils-nav-hover: rgba(255,255,255,0.10);
|
||||
-workutils-nav-active: rgba(37,99,235,0.25);
|
||||
-workutils-nav-active: rgba(37,99,235,0.35);
|
||||
-workutils-nav-bar: #2563eb;
|
||||
}
|
||||
|
||||
@@ -39,33 +39,152 @@
|
||||
-fx-background-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
/* ============ ListView 导航(JavaFX 标准化方案) ============ */
|
||||
|
||||
.nav-list {
|
||||
-fx-background-color: transparent;
|
||||
-fx-background-insets: 0;
|
||||
-fx-border-width: 0;
|
||||
-fx-padding: 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: rgba(255,255,255,0.15);
|
||||
-fx-background-radius: 3;
|
||||
}
|
||||
.nav-list .scroll-bar .thumb:hover {
|
||||
-fx-background-color: rgba(255,255,255,0.25);
|
||||
}
|
||||
.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-text-fill: white;
|
||||
-fx-background-radius: 0;
|
||||
-fx-border-width: 0 0 0 3;
|
||||
-fx-border-color: transparent;
|
||||
-fx-padding: 12 16 12 16;
|
||||
-fx-alignment: CENTER-LEFT;
|
||||
-fx-background-insets: 0;
|
||||
-fx-padding: 10 14 10 14;
|
||||
-fx-cursor: hand;
|
||||
-fx-max-width: Infinity;
|
||||
-fx-border-width: 0;
|
||||
/* 平滑过渡背景色和文字色 */
|
||||
-fx-transition: -fx-background-color 150ms ease-out;
|
||||
}
|
||||
|
||||
.nav-btn > .label,
|
||||
.nav-btn .label,
|
||||
.nav-btn .text {
|
||||
.nav-cell .label {
|
||||
-fx-text-fill: white;
|
||||
/* 深色背景下的文字阴影,提升对比度 */
|
||||
-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.45), 3, 0, 0, 1);
|
||||
-fx-transition: -fx-text-fill 150ms ease-out,
|
||||
-fx-effect 150ms ease-out;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
/* 移除 JavaFX ListCell 默认焦点环(让选中态边框干净) */
|
||||
.nav-list .list-cell {
|
||||
-fx-focus-color: transparent;
|
||||
-fx-faint-focus-color: transparent;
|
||||
-fx-border-color: transparent;
|
||||
-fx-border-width: 0;
|
||||
}
|
||||
|
||||
/* 强制空 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-nav-hover;
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.nav-btn-active {
|
||||
/* 兜底:所有 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-nav-active;
|
||||
-fx-text-fill: white;
|
||||
-fx-border-color: -workutils-nav-bar;
|
||||
-fx-background-insets: 0;
|
||||
-fx-padding: 10 14 10 14;
|
||||
}
|
||||
|
||||
.nav-list .list-cell:filled:selected .label {
|
||||
-fx-text-fill: white !important;
|
||||
-fx-font-weight: 600;
|
||||
/* 移除 dropshadow,让文字干净锐利 */
|
||||
-fx-effect: null !important;
|
||||
}
|
||||
|
||||
.nav-list .list-cell:filled:selected .nav-indicator {
|
||||
-fx-fill: #2563eb;
|
||||
-fx-effect: dropshadow(gaussian, #60a5fa, 6, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* 兼容:之前手动加的 .nav-cell-active 类名也支持 */
|
||||
.nav-cell-active {
|
||||
-fx-background-color: -workutils-nav-active;
|
||||
-fx-background-insets: 0;
|
||||
-fx-padding: 10 14 10 14;
|
||||
}
|
||||
|
||||
/* 按下状态:微妙的反馈 */
|
||||
.nav-cell:pressed {
|
||||
-fx-background-color: rgba(96,165,250,0.35);
|
||||
}
|
||||
|
||||
/* ListView 自身获得焦点时也保持透明,不显示蓝色环 */
|
||||
.nav-list:focused {
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: transparent;
|
||||
}
|
||||
|
||||
/* 左侧指示条 */
|
||||
.nav-indicator {
|
||||
-fx-fill: transparent;
|
||||
-fx-arc-width: 2;
|
||||
-fx-arc-height: 2;
|
||||
-fx-transition: -fx-fill 150ms ease-out;
|
||||
}
|
||||
|
||||
.nav-cell-active .nav-indicator {
|
||||
-fx-fill: #2563eb;
|
||||
-fx-effect: dropshadow(gaussian, #60a5fa, 6, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* 点击涟漪层 */
|
||||
.nav-ripple {
|
||||
-fx-fill: rgba(96,165,250,0.4);
|
||||
-fx-opacity: 0;
|
||||
}
|
||||
|
||||
.nav-version {
|
||||
|
||||
Reference in New Issue
Block a user