项目

ASP.NET Core MVC / Razor Pages: UI 主题化

引言

ABP 提供了一套完整的 UI 主题化 系统,旨在实现以下目标:

  • 可复用的 应用程序模块 被开发为独立于主题,因此它们可以与任何 UI 主题配合工作。
  • UI 主题由最终的应用程序决定。
  • 主题通过 NuGet/NPM 包分发,因此易于升级
  • 最终应用程序可以自定义选定的主题。

为了实现这些目标,ABP 做了以下工作:

  • 确定一组所有主题都使用和适配的基础库。这样,模块和应用程序开发者可以依赖并使用这些库,而无需依赖特定的主题。
  • 提供一个由所有主题实现的系统,包括导航菜单工具栏布局钩子等。这样,模块和应用程序可以参与到布局中来,构建出一致的应用程序 UI。

当前主题

目前,官方提供四个主题:

  • 基础主题 是采用纯 Bootstrap 风格的极简主题。它是开源且免费的
  • LeptonX Lite 主题 是一个现代、时尚的 Bootstrap UI 主题。如果您想要一个可用于生产环境的 UI 主题,它是理想选择。它也是开源且免费的
  • Lepton 主题 是由核心 ABP 团队开发的商业主题,是 ABP 许可证的一部分。
  • LeptonX 主题 同样是由核心 ABP 团队开发的商业主题,是 ABP 许可证的一部分。这是 ABP v6.0.0 之后的默认主题。

还有一些社区驱动的 ABP 主题(您可以在网上搜索)。

概述

基础库

所有主题必须依赖于 @abp/aspnetcore.mvc.ui.theme.shared NPM 包,因此它们间接依赖于以下库:

这些库被选为基础库,可供应用程序和模块使用。

抽象/包装器

ABP 中也提供了一些抽象,使您的代码也能独立于这些库中的某些库。例如:

  • 标签助手 使生成 Bootstrap UI 变得容易。
  • JavaScript 消息通知 API 提供了使用 Sweetalert 和 Toastr 的抽象。
  • 表单与验证 系统自动处理验证,因此您通常不需要直接编写任何验证代码。

标准布局

主题的主要职责是提供布局。所有主题都必须实现三种预定义的布局

  • 应用程序布局:主应用程序页面使用的默认布局。
  • 账户布局:主要由账户模块用于登录、注册、忘记密码...等页面。
  • 空布局:完全没有布局组件的最小布局。

布局名称是在 Volo.Abp.AspNetCore.Mvc.UI.Theming.StandardLayouts 类中定义的常量。

应用程序布局

这是主应用程序页面使用的默认布局。下图显示了基础主题应用程序布局中的用户管理页面:

basic-theme-application-layout

同一页面在 Lepton 主题 应用程序布局下的显示如下:

lepton-theme-application-layout

如您所见,页面是相同的,但在上面的主题中,外观完全不同。

应用程序布局通常包含以下部分:

某些主题可能提供更多部分,如面包屑导航、页面标题和工具栏...等。请参阅布局部件部分。

账户布局

账户布局主要由账户模块用于登录、注册、忘记密码...等页面。

basic-theme-account-layout

此布局通常提供以下部分:

  • 语言切换下拉菜单
  • 租户切换区域(如果应用程序是 多租户 的,并且当前租户是通过 Cookie 解析的)
  • 页面警报
  • 页面内容(即 RenderBody()
  • 布局钩子

基础主题也为此布局渲染了顶部导航栏(如上图所示)。

这里是 Lepton 主题的账户布局:

lepton-theme-account-layout

Lepton 主题在此布局中显示了应用程序徽标和页脚。

您可以在应用程序中完全或部分覆盖主题布局以自定义它。

空布局

空布局提供一个空页面。它通常包含以下部分:

实现一个主题

最简单的方法

创建新主题最简单的方法是添加带有源代码的基础主题源代码模块并自定义它。

abp add-package Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic --with-source-code --add-to-solution-file

ITheme 接口

ABP 使用 ITheme 接口为当前页面选择布局。主题必须实现此接口以提供请求的布局路径。

这是基础主题ITheme 实现。

using Volo.Abp.AspNetCore.Mvc.UI.Theming;
using Volo.Abp.DependencyInjection;

namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic
{
    [ThemeName(Name)]
    public class BasicTheme : ITheme, ITransientDependency
    {
        public const string Name = "Basic";

        public virtual string GetLayout(string name, bool fallbackToDefault = true)
        {
            switch (name)
            {
                case StandardLayouts.Application:
                    return "~/Themes/Basic/Layouts/Application.cshtml";
                case StandardLayouts.Account:
                    return "~/Themes/Basic/Layouts/Account.cshtml";
                case StandardLayouts.Empty:
                    return "~/Themes/Basic/Layouts/Empty.cshtml";
                default:
                    return fallbackToDefault
                        ? "~/Themes/Basic/Layouts/Application.cshtml"
                        : null;
            }
        }
    }
}
  • [ThemeName] 属性是必需的,并且主题必须有一个唯一的名称,本例中为 Basic
  • 如果主题提供了请求的布局(name),GetLayout 方法应返回其路径。如果主题旨在供标准应用程序使用,则应实现标准布局。它还可以实现其他布局。

一旦主题实现了 ITheme 接口,就应该在模块的 ConfigureServices 方法中将主题添加到 AbpThemingOptions 中。

Configure<AbpThemingOptions>(options =>
{
    options.Themes.Add<BasicTheme>();
});

IThemeSelector 服务

ABP 允许同时使用多个主题。这就是为什么 options.Themes 是一个列表。IThemeSelector 服务在运行时选择主题。应用程序开发者可以设置 AbpThemingOptions.DefaultThemeName 来设置要使用的主题,或者替换 IThemeSelector 服务实现(默认实现是 DefaultThemeSelector)以完全控制运行时的主题选择。

捆绑包

捆绑与压缩系统 提供了将样式和脚本文件导入页面的标准方式。ABP 定义了两个标准捆绑包:

  • StandardBundles.Styles.Global:全局捆绑包,包含所有页面使用的样式文件。通常包括基础库的 CSS 文件。
  • StandardBundles.Scripts.Global:全局捆绑包,包含所有页面使用的脚本文件。通常包括基础库的 JavaScript 文件。

主题通常通过添加特定于主题的 CSS/JavaScript 文件来扩展这些标准捆绑包。

定义新捆绑包的最佳方式是从标准捆绑包继承并添加到 AbpBundlingOptions 中,如下所示(这段代码来自基础主题):

Configure<AbpBundlingOptions>(options =>
{
    options
        .StyleBundles
        .Add(BasicThemeBundles.Styles.Global, bundle =>
        {
            bundle
                .AddBaseBundles(StandardBundles.Styles.Global)
                .AddContributors(typeof(BasicThemeGlobalStyleContributor));
        });

    options
        .ScriptBundles
        .Add(BasicThemeBundles.Scripts.Global, bundle =>
        {
            bundle
                .AddBaseBundles(StandardBundles.Scripts.Global)
                .AddContributors(typeof(BasicThemeGlobalScriptContributor));
        });
});

BasicThemeGlobalStyleContributorBasicThemeGlobalScriptContributor 是捆绑包贡献者。例如,BasicThemeGlobalStyleContributor 定义如下:

public class BasicThemeGlobalStyleContributor : BundleContributor
{
    public override void ConfigureBundle(BundleConfigurationContext context)
    {
        context.Files.Add("/themes/basic/layout.css");
    }
}

然后主题可以在布局中渲染这些捆绑包。例如,您可以像下面这样渲染全局样式:

<abp-style-bundle name="@BasicThemeBundles.Styles.Global" />

请参阅 捆绑与压缩 文档以更好地理解捆绑系统。

布局部件

典型的布局由几个部分组成。主题应在每个布局中包含必要的部分。

示例:基础主题的应用程序布局包含以下部分

basic-theme-application-layout-parts

应用程序代码和模块只能在"页面内容"部分显示内容。如果它们需要更改其他部分(添加菜单项、添加工具栏项、更改品牌区域中的应用程序名称...),它们应使用 ABP API。

以下各节解释了由 ABP 预定义的、主题可以实现的基本部分。

最佳实践是将布局拆分为组件/部分视图,以便最终应用程序可以部分覆盖它们以实现自定义。

品牌

应使用 IBrandingProvider 服务获取应用程序的名称和徽标 URL,以便在"品牌"部分渲染。

应用程序启动模板 提供了此接口的实现,供应用程序开发者设置这些值。

主菜单

IMenuManager 服务用于获取主菜单项并在布局上渲染。

示例:在视图组件中获取主菜单以进行渲染

public class MainNavbarMenuViewComponent : AbpViewComponent
{
    private readonly IMenuManager _menuManager;

    public MainNavbarMenuViewComponent(IMenuManager menuManager)
    {
        _menuManager = menuManager;
    }

    public async Task<IViewComponentResult> InvokeAsync()
    {
        var menu = await _menuManager.GetAsync(StandardMenus.Main);
        return View("~/Themes/Basic/Components/Menu/Default.cshtml", menu);
    }
}

请参阅 导航 / 菜单 文档以了解有关导航系统的更多信息。

主工具栏

IToolbarManager 服务用于获取主工具栏项并在布局上渲染。此工具栏的每个项都是一个视图组件,因此它可以包含任何类型的 UI 元素。注入 IToolbarManager 并使用 GetAsync 获取工具栏项:

var toolbar = await _toolbarManager.GetAsync(StandardToolbars.Main);

请参阅 工具栏 文档以了解更多关于工具栏系统的信息。

主题有责任向主工具栏添加两个预定义的项:语言选择和用户菜单。为此,创建一个实现 IToolbarContributor 接口的类,并将其添加到 AbpToolbarOptions 中,如下所示:

Configure<AbpToolbarOptions>(options =>
{
    options.Contributors.Add(new BasicThemeMainTopToolbarContributor());
});
语言选择

"语言选择"工具栏项通常是一个下拉菜单,用于在语言之间切换。ILanguageProvider 用于获取可用语言列表,CultureInfo.CurrentUICulture 用于了解当前语言。

可以使用 /Abp/Languages/Switch 端点切换语言。此端点接受以下查询字符串参数:

  • culture:选定的区域性,如 en-USen
  • uiCulture:选定的 UI 区域性,如 en-USen
  • returnUrl(可选):可用于在切换语言后返回给定的 URL。

cultureuiCulture 应与可用语言之一匹配。ABP 在 /Abp/Languages/Switch 端点设置一个区域性 Cookie。

用户菜单

用户菜单包含与用户帐户相关的链接。IMenuManager 的使用方式与主菜单类似,但这次使用 StandardMenus.User 参数,如下所示:

var menu = await _menuManager.GetAsync(StandardMenus.User);

可以使用 ICurrentUserICurrentTenant 服务获取当前用户和租户名称。

页面警报

IAlertManager 服务用于获取当前页面警报以在布局上渲染。使用 IAlertManagerAlerts 列表。它通常在页面内容(RenderBody())之前渲染。

请参阅 页面警报 文档以了解更多信息。

布局钩子

由于布局在主题包中,最终的应用程序或任何模块无法直接操作布局内容。布局钩子 系统允许向布局的某些特定点注入组件。

主题负责在正确的位置渲染钩子。

示例:在应用程序布局中渲染 LayoutHooks.Head.First 钩子

<head>
    @await Component.InvokeLayoutHookAsync(LayoutHooks.Head.First, StandardLayouts.Application)
    ...

请参阅 布局钩子 文档以了解标准布局钩子。

脚本/样式区域

每个布局都应渲染以下可选区域:

  • styles 区域在 head 的末尾、LayoutHooks.Head.Last 之前渲染。
  • scripts 区域在 body 的末尾、LayoutHooks.Body.Last 之前渲染。

这样,页面可以向布局导入样式和脚本。

示例:渲染 styles 区域

@await RenderSectionAsync("styles", required: false)

内容工具栏区域

另一个预定义的区域是"内容工具栏"区域,页面可以使用它在页面内容之前添加代码。基础主题按如下方式渲染它:

<div id="AbpContentToolbar">
    <div class="text-end mb-2">
        @RenderSection("content_toolbar", false)
    </div>
</div>

容器 div 的 id 必须为 AbpContentToolbar。此部分应在 RenderBody() 之前。

小部件资源

小部件系统 允许定义具有自己样式/脚本文件的可重用小部件。所有布局都应渲染小部件的样式和脚本。

小部件样式 如下所示渲染,在 styles 区域之前,全局样式捆绑包之后:

@await Component.InvokeAsync(typeof(WidgetStylesViewComponent))

小部件脚本 如下所示渲染,在 scripts 区域之前,全局脚本捆绑包之后:

@await Component.InvokeAsync(typeof(WidgetScriptsViewComponent))

ABP 脚本

ABP 有一些特殊的脚本,应包含在每个布局中。它们不包含在全局捆绑包中,因为它们是动态创建的,基于当前用户。

ABP 脚本(ApplicationConfigurationScriptServiceProxyScript)应紧接在全局脚本捆绑包之后添加,如下所示:

<script src="~/Abp/ApplicationConfigurationScript"></script>
<script src="~/Abp/ServiceProxyScript"></script>

页面标题、选定的菜单项和面包屑导航

任何页面都可以注入 IPageLayout 服务来设置页面标题、选定的菜单项名称和面包屑导航项。然后主题可以使用此服务获取这些值并在 UI 上渲染。

基础主题没有实现此服务,但 Lepton 主题实现了:

breadcrumbs-example

更多信息请参阅 页面标题 文档。

租户切换

如果应用程序是多租户的,并且租户是从 Cookie 解析的,则账户布局应允许用户切换当前租户。请参阅 基础主题账户布局 作为示例实现。

布局类

标准布局(ApplicationAccountEmpty)应向 body 标签添加以下 CSS 类:

  • Application 布局使用 abp-application-layout
  • Account 布局使用 abp-account-layout
  • Empty 布局使用 abp-empty-layout

这样,应用程序或模块可以根据当前布局进行选择。

RTL(从右到左)

为了支持从右到左的语言,布局应检查当前区域性,并向 html 标签添加 dir="rtl",并向 body 标签添加 rtl CSS 类。

您可以检查 CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft 来判断当前语言是否为 RTL 语言。

NPM 包

主题应具有一个 NPM 包,该包依赖于 @abp/aspnetcore.mvc.ui.theme.shared 包。这样,它就继承了所有基础库。如果主题需要额外的库,那么它也应该定义这些依赖项。

应用程序使用 客户端包管理 系统将客户端库添加到项目中。因此,如果应用程序使用您的主题,它应该添加对您主题的 NPM 包的依赖,以及 NuGet 包依赖。

在本文档中