userMenu 用户菜单

userMenu 组件显示一个按钮,当点击时,打开一个下拉信息菜单。

XML 元素

userMenu

Java 类

UserMenu

XML 属性

id - alignSelf - classNames - colspan - css - enabled - focusShortcut - openOnHover - overlayClass - tabIndex - themeNames - title - visible

事件和处理器

AttachEvent - BlurEvent - DetachEvent - FocusEvent - UserChangedEvent - buttonRenderer - headerRenderer

XML 内部元素

actionItem - componentItem - separator - textItem - viewItem

基本用法

主按钮的布局中显示了当前用户的信息。 用户点击此按钮可打开下拉菜单。

下拉菜单包含一系列操作,每个操作由一个可点击的菜单项或子菜单表示。

user menu basic

以下是一个 userMenu 的示例,使用了默认按钮渲染器和多个菜单项:

<userMenu>
    <items>
        <viewItem id="profileMenuItem" text="msg://profileMenuItem.text" icon="USER"
                  viewId="ProfileView"
                  themeNames="non-checkable"/>
        <viewItem id="settingsMenuItem" text="msg://settingsMenuItem.text" icon="COG"
                  viewClass="com.company.onboarding.view.settings.SettingsView"
                  themeNames="non-checkable"/>
        <actionItem id="themeMenuItem"
                    themeNames="non-checkable">
            <action id="themeMenuItem" type="userMenu_themeSwitch"/>
        </actionItem>
        <separator/>
        <actionItem id="logoutMenuItem" ref="logoutAction"
                    themeNames="non-checkable"/>
    </items>
</userMenu>

XML 内部元素

在 XML 中,userMenu 可以包含内部元素:

actionItem

actionItem 元素用于关联用户菜单项与点击该项时的特定 操作

可以声明式地定义一个操作,也可以使用 ref 属性指向已定义的 actionid

<actions>
    <action id="logoutAction" text="msg:///actions.logout.text" type="logout"/>
</actions>
<layout>
    <userMenu id="userMenuActions">
        <items>
            <actionItem id="aboutMenuItem">
                <action id="aboutAction" text="msg://aboutAction.text" icon="INFO_CIRCLE_O"/> (1)
            </actionItem>
            <actionItem id="logoutMenuItem" ref="logoutAction"/> (2)
        </items>
    </userMenu>
</layout>
1 声明式定义的操作。
2 引用已有操作。

当用户点击下拉菜单中的 actionItem 时,Jmix 会自动触发该 action。 可以为某个操作生成 ActionPerformedEvent 处理方法来实现其逻辑:

@Autowired
private Notifications notifications;

@Subscribe("userMenuActions.aboutMenuItem.aboutAction")
public void onUserMenuActionsAboutMenuItemAboutAction(final ActionPerformedEvent event) {
    notifications.show("About");
}

框架提供了一些内置的 用户菜单操作

componentItem

componentItem 元素可以为 userMenu 添加自定义的内部组件。

<userMenu id="userMenuComponent">
    <items>
        <componentItem id="emailItMenuItem">
            <hbox padding="false">
                <icon icon="MAILBOX"/>
                <span text="E Mail"/>
            </hbox>
        </componentItem>
    </items>
</userMenu>

在 Jmix Studio 中,可以为 componentItem 添加一个 UserMenuItem.HasClickListener.ClickEvent 事件处理的桩代码:

@Autowired
private Notifications notifications;

@Subscribe("userMenuComponent.emailItMenuItem")
public void onUserMenuComponentEmailItMenuItemClick(final UserMenuItem.HasClickListener.ClickEvent<ComponentUserMenuItem> event) {
    notifications.show("Email: test@river.net");
}

textItem

textItem 元素可以添加文本和图标。

<userMenu id="userMenuText">
    <items>
        <textItem id="contactUsMenuItem" text="msg://contactUsItem.text" icon="PHONE"/>
    </items>
</userMenu>

在 Jmix Studio 中,可以为 textItem 添加一个 UserMenuItem.HasClickListener.ClickEvent 事件处理的桩代码:

@Autowired
private Notifications notifications;

@Subscribe("userMenuText.contactUsMenuItem")
public void onUserMenuTextContactUsMenuItemClick(final UserMenuItem.HasClickListener.ClickEvent<TextUserMenuItem> event) {
    notifications.show("Phone number: +6(876)5463");
}

viewItem

viewItem 元素支持打开特定的视图。

<userMenu id="userMenuView">
    <items>
        <viewItem id="profileMenuItem" text="msg://profileMenuItem.text" icon="USER"
                  viewId="ProfileView"/> (1)
        <viewItem id="settingsMenuItem" text="msg://settingsMenuItem.text" icon="COG"
                  viewClass="com.company.onboarding.view.settings.SettingsView"
                  openMode="DIALOG"/> (2)
    </items>
</userMenu>
1 用指定的 id 打开特定视图。
2 DIALOG 模式打开指定类的视图。

separator

separator 元素用于在下拉菜单中分隔菜单项。

分隔符在满足以下条件时会自动隐藏:

  1. 多个分隔符连续出现。此时仅保留一个分隔符可见。

  2. 分隔符是第一个子元素且未定义 header 渲染器。

  3. 分隔符是最后一个子元素。

子菜单

textItemcomponentItem 元素支持定义内部菜单项,从而可以定义具有层级结构的菜单。

user menu nested
<userMenu>
    <items>
        <textItem id="helpMenuItem"
                  text="msg://helpMenuItem.text" icon="QUESTION_CIRCLE"
                  themeNames="non-checkable">
            <items>
                <textItem id="documentationMenuItem" text="msg://documentationMenuItem.text"/>
                <textItem id="aboutMenuItem" text="msg://aboutMenuItem.text"/>
            </items>
        </textItem>
    </items>
</userMenu>

样式版本

使用 themeNames 属性设置组件的样式。

样式 描述 支持的主题

tertiary

移除按钮背景

Aura, Lumo

non-checkable

移除用于显示对勾的区域

Aura, Lumo

non-checkable 样式可以在 userMenu 和子菜单级别单独设置。

如果子菜单有可以勾选的菜单,最好在一级菜单中单独使用 non-checkable 样式。

渲染器

框架为按钮和可选的菜单 header 内容的自定义功能提供饿了灵活的方案。

按钮渲染器

可以通过 setButtonRenderer() 方法或 @Install 注解设置按钮渲染器。

user menu button renderer
import com.company.onboarding.entity.User;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.avatar.AvatarVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.server.streams.DownloadHandler;
import com.vaadin.flow.server.streams.DownloadResponse;
import io.jmix.core.FileRef;
import io.jmix.core.FileStorage;
import io.jmix.flowui.UiComponents;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;

// class declaration and annotations omitted

    @Autowired
    private UiComponents uiComponents;
    @Autowired
    private FileStorage fileStorage;

    @Install(to = "userMenu", subject = "buttonRenderer")
    private Component userMenuButtonRenderer(final UserDetails userDetails) {
        if (!(userDetails instanceof User user)) {
            return null;
        }

        String userName = generateUserName(user);
        Avatar avatar = createAvatar(userName, user.getPicture());
        Span name = uiComponents.create(Span.class);
        name.setText(userName);
        name.addClassName(LumoUtility.TextColor.BODY);

        HorizontalLayout content = uiComponents.create(HorizontalLayout.class);
        content.setAlignItems(FlexComponent.Alignment.CENTER);
        content.add(avatar, name);
        content.addClassNames( (1)
                LumoUtility.Padding.Horizontal.MEDIUM,
                LumoUtility.Padding.Vertical.SMALL);

        return content;
    }

    private String generateUserName(User user) {
        String userName = String.format("%s %s",
                        Strings.nullToEmpty(user.getFirstName()),
                        Strings.nullToEmpty(user.getLastName()))
                .trim();

        return userName.isEmpty() ? user.getUsername() : userName;
    }

    private Avatar createAvatar(String fullName, @Nullable FileRef fileRef) {
        Avatar avatar = uiComponents.create(Avatar.class); (2)
        avatar.setName(fullName); (3)
        avatar.getElement().setAttribute("tabindex", "-1"); (4)

        if (fileRef != null) {
            avatar.setImageHandler( (5)
                    DownloadHandler.fromInputStream(event ->
                            new DownloadResponse(
                                    fileStorage.openStream(fileRef),
                                    fileRef.getFileName(),
                                    fileRef.getContentType(),
                                    -1
                            )
                    )
            );
        }

        return avatar;
    }
1 默认 HorizontalLayout 的内边距太大,我们通过样式添加内边距。
2 使用 UiComponents 工厂创建 Avatar 组件实例。
3 将用户全名传递给 Avatar,以便在没有图片时生成备选首字母缩写。
4 avatar 默认获得焦点,但不提供禁用焦点的 Java API。移除 tab 索引,只有 userMenu 组件可以接收焦点。
5 DownloadHandler 根据 User 实体的 picture 属性中存储信息,从文件存储中流式传输头像图片。

Header 渲染器

Header 渲染器用于定义菜单顶部的信息。可以通过 setHeaderRenderer() 方法或 @Install 注解设置 header 渲染器。

user menu header renderer
import com.company.onboarding.entity.User;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.avatar.AvatarVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.server.streams.DownloadHandler;
import com.vaadin.flow.server.streams.DownloadResponse;
import io.jmix.core.FileRef;
import io.jmix.core.FileStorage;
import io.jmix.flowui.UiComponents;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;

// class declaration and annotations omitted

    @Autowired
    private UiComponents uiComponents;
    @Autowired
    private FileStorage fileStorage;

    @Install(to = "userMenu", subject = "headerRenderer")
    private Component userMenuHeaderRenderer(final UserDetails userDetails) {
        if (!(userDetails instanceof User user)) {
            return null;
        }

        String name = generateUserName(user);

        Avatar avatar = createAvatar(name, user.getPicture());
        avatar.addThemeVariants(AvatarVariant.LUMO_LARGE);
        avatar.addClassName("user-menu-avatar");

        Span text = uiComponents.create(Span.class);
        text.setText(name);
        text.setClassName("user-menu-text");

        Div content = uiComponents.create(Div.class);
        content.setClassName("user-menu-header-content"); (1)
        content.add(avatar, text);

        if (name.equals(user.getUsername())) {
            text.addClassNames("user-menu-text-subtext");
        } else {
            Span subtext = uiComponents.create(Span.class);
            subtext.setText(user.getUsername());
            subtext.setClassName("user-menu-subtext");

            content.add(subtext);
        }

        return content;
    }

    private String generateUserName(User user) {
        String userName = String.format("%s %s",
                        Strings.nullToEmpty(user.getFirstName()),
                        Strings.nullToEmpty(user.getLastName()))
                .trim();

        return userName.isEmpty() ? user.getUsername() : userName;
    }

    private Avatar createAvatar(String fullName, @Nullable FileRef fileRef) {
        Avatar avatar = uiComponents.create(Avatar.class); (2)
        avatar.setName(fullName); (3)
        avatar.getElement().setAttribute("tabindex", "-1"); (4)

        if (fileRef != null) {
            avatar.setImageHandler( (5)
                    DownloadHandler.fromInputStream(event ->
                            new DownloadResponse(
                                    fileStorage.openStream(fileRef),
                                    fileRef.getFileName(),
                                    fileRef.getContentType(),
                                    -1
                            )
                    )
            );
        }

        return avatar;
    }
1 组件位置通过样式定义。
2 使用 UiComponents 工厂创建 Avatar 组件实例。
3 将用户全名传递给 Avatar,以便在没有图片时生成备选首字母缩写。
4 avatar 默认可获得焦点,但不提供禁用焦点的 Java API。移除 tab 索引,以便只有 userMenu 组件可以接收焦点。
5 DownloadHandler 根据 User 实体的 picture 属性中存储的引用,从文件存储中流式传输头像图片。
.user-menu-header-content {
    display: grid;
    grid-template: "avatar text"
                   "avatar subtext";
    grid-template-columns: auto 1fr;
    column-gap: var(--lumo-space-s);

    width: 100%;
    box-sizing: border-box;

    color: var(--lumo-body-text-color);
    padding: var(--lumo-space-xs) var(--lumo-space-l) var(--lumo-space-xs) var(--lumo-space-s);
}

.user-menu-header-content > .user-menu-avatar {
    grid-area: avatar;
    align-self: center;
}

.user-menu-header-content > .user-menu-text {
    grid-area: text;

    color: var(--lumo-body-text-color);
    font-weight: 700;
    font-size: var(--lumo-font-size-m);
}

.user-menu-header-content > .user-menu-text-subtext {
    grid-row: text / subtext;
}

.user-menu-header-content > .user-menu-text {
    align-self: center;
    text-align: start;

    width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
}

.user-menu-header-content > .user-menu-subtext {
    grid-area: subtext;
    align-self: center;
    text-align: start;

    color: var(--lumo-secondary-text-color);
    font-size: var(--lumo-font-size-xs);

    width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
}

XML 属性

通用属性 对所有组件都是一样的配置。

下面是 userMenu 的特殊属性:

名称

描述

默认值

openOnHover

如果 openOnHover = true,则当该组件被鼠标或触摸聚焦时,会自动打开下拉列表。

false

事件和处理器

通用事件和处理器 对所有组件都是一样的配置。 下面是 userMenu 的特殊事件和处理器:

在 Jmix Studio 生成处理器桩代码时,可以使用 Jmix UI 组件面板的 Handlers 标签页或者视图类顶部面板的 Generate Handler 添加,也可以通过 CodeGenerate 菜单(Alt+Insert / Cmd+N)生成。

名称

描述

UserChangedEvent

io.jmix.flowui.kit.component.usermenu.JmixUserMenu.UserChangedEvent 当切换用户时发出。

buttonRenderer

设置基于当前用户信息渲染按钮的 Renderer。参阅 按钮渲染器

headerRenderer

设置基于当前用户信息渲染 header 的 Renderer 参阅 Header 渲染器