Abp Vnext Blazor替換UI元件 整合BootstrapBlazor(詳細過程)

tchivs發表於2021-11-29

Abp Vnext自帶的blazor專案使用的是 Blazorise,但是試用後發現不支援多標籤。於是想替換為BootstrapBlazor
過程比較複雜,本人已經把模組寫好了只需要替換掉即可。

點選檢視原始碼

demo也在原始碼裡面

建立一個Abp模組

從官網下載

Q:為什麼不選擇應用程式?

因為模組中包含Blazor的ssr和Wasm的host。可以直接使用,而建立應用程式的話只能從ssr或wasm的host中二選一,雖然可以建立兩次再把host複製合併但太麻煩了。

精簡模組

刪除以下無用目錄:

  • angular(前端)
  • host/DemoApp.Web.Host (mvc使用)
  • host/DemoApp.Web.Unified (mvc使用)
  • host/DemoApp.Web (mvc使用)

專案結構與如何啟動專案

  • IdentityServer應用程式是其他應用程式使用的身份驗證伺服器,它有自己的appsettings.json包含資料庫連線字串和其他配置,需要初始化資料庫
  • HttpApi.Host託管模組的HTTP API. 它有自己的appsettings.json包含資料庫連線字串和其他配置
    先把專案跑起來
  • Blazor.HostBlazor WebAssembly模式的啟動程式,它有自己的appsettings.json(位於wwwroot中)包含HTTP API伺服器地址和IdentityServer等配置,前後端分離,需要先啟動前面兩個程式才能正常使用
  • Blazor.Server.HostBlazor Server模式的啟動程式,它有自己的appsettings.json包含資料庫連線字串和其他配置,但是它內部預設整合了IdentityServer和HttpApi.Host模組,相當於前後端不分離,所以它可以直接用。

啟動專案(WebAssembly模式)

因為專案預設資料庫為MSSQLLocalDB所以不需要另外修改配置,直接初始化資料庫即可。

首先在控制檯中切換到DemoApp.IdentityServer專案所在目錄,執行

dotnet ef database update

按順序開啟如下專案:

  • DemoApp.IdentityServer
  • DemoApp.HttpApi.Host
  • DemoApp.Blazor.Host

開啟https://localhost:44307/正常載入wasm頁面,點選右上角登入會跳轉到identityServer認證中心(https://localhost:44364/),輸入使用者名稱admin密碼1q2w3E*登入完成跳轉回wasm

啟動專案(Server模式)

由於Server.Host預設整合了IdentityServer和HttpApi(需要改造,後文有)
初始化資料庫
首先在控制檯中切換到DemoApp.Blazor.Server.Host專案所在目錄,執行

dotnet ef database update

直接啟動後開啟https://localhost:44313/即可

可以看到登入的時候也是https://localhost:44313/,不像wasm一樣會跳到identityserver(因為它自己就整合了)。

替換模組主題

DemoApp.Blazor

這是模組的Blazor公共專案,一般在這裡面編寫相關頁面和元件

  1. 移除依賴Volo.Abp.AspNetCore.Components.Web.Theming,替換為Tchivs.Abp.AspNetCore.Blazor.Theme.Bootstrap
  2. 開啟DemoAppBlazorModule
    2.1 把DependsOn中依賴的模組名AbpAspNetCoreComponentsWebThemingModule改為AbpAspNetCoreBlazorThemeBootstrapModule
    2.2 引用Tchivs.Abp.AspNetCore.Blazor.Theme.Bootstrap Tchivs.Abp.AspNetCore.Blazor.Theme名稱空間
  3. 開啟_Imports.razor,刪除@using Volo.Abp.BlazoriseUI @using Blazorise @using Blazorise.DataGrid,新增@using BootstrapBlazor.Components @using Tchivs.Abp.AspNetCore.Blazor.Theme

DemoApp.Blazor-DemoAppBlazorModule

DemoApp.Blazor-_Imports.razor

DemoApp.Blazor.Server

這個是模組的ssr模式下引用的類庫,這個簡單,只需要替換依賴就行。

  1. 移除依賴Volo.Abp.AspNetCore.Components.Server.Theming,替換為Tchivs.Abp.AspNetCore.Blazor.Theme.Bootstrap.Server
  2. 開啟DemoAppBlazorServerModule
    2.1 把DependsOn中依賴的模組名AbpAspNetCoreComponentsServerThemingModule改為AbpAspNetCoreBlazorThemeBootstrapServerModule
    2.2 引用Tchivs.Abp.AspNetCore.Blazor.Theme.Bootstrap名稱空間

DemoApp.Blazor.Server-DemoAppBlazorServerModule

DemoApp.Blazor.WebAssembly

這個是模組的wasm模式下引用的類庫,由上。

  1. 移除依賴Volo.Abp.AspNetCore.Components.WebAssembly.Theming,替換為Tchivs.Abp.AspNetCore.Blazor.Theme.Bootstrap.WebAssembly
  2. 開啟DemoAppBlazorWebAssemblyModule
    2.1 把DependsOn中依賴的模組名AbpAspNetCoreComponentsWebAssemblyThemingModule改為AbpAspNetCoreBlazorThemeBootstrapWebAssemblyModule
    2.2 引用Tchivs.Abp.AspNetCore.Blazor.Theme.Bootstrap名稱空間

替換Host主題

Blazor.Host

首先我們替換WebAssembly Host的主題,它比Server整合更簡單一點

移除依賴

由於自帶的使用者管理、許可權管理、租戶管理等UI模組都是依賴了Blazorise的,所以需要從專案依賴中移除這幾項:

  • Volo.Abp.Identity.Blazor.WebAssembly
  • Volo.Abp.TenantManagement.Blazor.WebAssembly
  • Volo.Abp.SettingManagement.Blazor.WebAssembly
  • Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme(主題)
  • Blazorise.Bootstrap
  • Blazorise.Icons.FontAwesome

修改DemoAppBlazorHostModule

using System;
using System.Net.Http;
using Tchivs.Abp.AspNetCore.Blazor.Theme;
using Tchivs.Abp.AspNetCore.Blazor.Theme.Bootstrap;
using DemoApp.Blazor.WebAssembly;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Account;
using Volo.Abp.Autofac.WebAssembly;
using Volo.Abp.AutoMapper;
using Volo.Abp.Modularity;
using Volo.Abp.UI.Navigation;
namespace DemoApp.Blazor.Host
{
    [DependsOn(
        typeof(AbpAutofacWebAssemblyModule),
        typeof(AbpAccountApplicationContractsModule), 
        typeof(DemoAppBlazorWebAssemblyModule)
    )]
    public class DemoAppBlazorHostModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            var environment = context.Services.GetSingletonInstance<IWebAssemblyHostEnvironment>();
            var builder = context.Services.GetSingletonInstance<WebAssemblyHostBuilder>();
            ConfigureAuthentication(builder);
            ConfigureHttpClient(context, environment);
            ConfigureRouter(context);
            ConfigureUI(builder);
            ConfigureMenu(context);
            ConfigureAutoMapper(context);
        }

        private void ConfigureRouter(ServiceConfigurationContext context)
        {
            Configure<AbpRouterOptions>(options =>
            {
                //options.AppAssembly = typeof(DemoAppBlazorHostModule).Assembly;這裡要註釋掉
                options.AdditionalAssemblies.Add(this.GetType().Assembly);
            });
        }

        private void ConfigureMenu(ServiceConfigurationContext context)
        {
            Configure<AbpNavigationOptions>(options =>
            {
                options.MenuContributors.Add(new DemoAppHostMenuContributor(context.Services.GetConfiguration()));
            });
        }

        
        private static void ConfigureAuthentication(WebAssemblyHostBuilder builder)
        {
            builder.Services.AddOidcAuthentication(options =>
            {
                builder.Configuration.Bind("AuthServer", options.ProviderOptions);
                options.ProviderOptions.DefaultScopes.Add("DemoApp");
            });
        }

        private static void ConfigureUI(WebAssemblyHostBuilder builder)
        {
            builder.RootComponents.Add<App>("#ApplicationContainer");
        }

        private static void ConfigureHttpClient(ServiceConfigurationContext context, IWebAssemblyHostEnvironment environment)
        {
            context.Services.AddTransient(sp => new HttpClient
            {
                BaseAddress = new Uri(environment.BaseAddress)
            });
        }

        private void ConfigureAutoMapper(ServiceConfigurationContext context)
        {
            Configure<AbpAutoMapperOptions>(options =>
            {
                options.AddMaps<DemoAppBlazorHostModule>();
            });
        }
    }
}

修改_Imports.razor

刪除

@using Blazorise
@using Blazorise.DataGrid

新增

@using BootstrapBlazor.Components
@using Tchivs.Abp.AspNetCore.Blazor.Theme

重新生成樣式

因為修改了主題需要重新bundle

先生成DemoApp.Blazor.Host專案,然後在控制檯中轉到DemoApp.Blazor.Host所在目錄
執行:

abp bundle

如果顯示abp不是命令則需要安裝abp-cli

登入後顯示 :

Blazor.Server.Host

1.移除與替換依賴

移除以下包

  • Blazorise.Bootstrap
  • Blazorise.Icons.FontAwesome
  • Microsoft.EntityFrameworkCore.Tools
  • Volo.Abp.EntityFrameworkCore.SqlServer
  • Volo.Abp.AspNetCore.Authentication.JwtBearer
  • Volo.Abp.AspNetCore.Components.Server.BasicTheme
  • Volo.Abp.AuditLogging.EntityFrameworkCore
  • Volo.Abp.Account.Web.IdentityServer
  • Volo.Abp.Account.Application
  • Volo.Abp.FeatureManagement.EntityFrameworkCore
  • Volo.Abp.FeatureManagement.Application
  • Volo.Abp.Identity.Blazor.Server
  • Volo.Abp.Identity.EntityFrameworkCore
  • Volo.Abp.Identity.Application
  • Volo.Abp.TenantManagement.Blazor.Server
  • Volo.Abp.TenantManagement.EntityFrameworkCore
  • Volo.Abp.TenantManagement.Application
  • Volo.Abp.SettingManagement.Blazor.Server
  • Volo.Abp.SettingManagement.EntityFrameworkCore
  • Volo.Abp.SettingManagement.Application
  • Volo.Abp.PermissionManagement.Application
  • Volo.Abp.PermissionManagement.EntityFrameworkCore
  • DemoApp.EntityFrameworkCore\DemoApp.EntityFrameworkCore
  • DemoApp.HttpApi

新增以下包

  • Volo.Abp.AspNetCore.Authentication.OpenIdConnect
  • Volo.Abp.AspNetCore.Mvc.Client
  • Volo.Abp.AspNetCore.Authentication.OAuth
  • Volo.Abp.Http.Client.IdentityModel.Web
  • Volo.Abp.PermissionManagement.HttpApi.Client
  • Volo.Abp.Identity.HttpApi.Client
  • Volo.Abp.TenantManagement.HttpApi.Client
  • Volo.Abp.FeatureManagement.HttpApi.Client
  • DemoApp.HttpApi.Client

2.修改Module.cs

1.刪除DependsOn中已移除的模組

還要刪除

  • DemoAppEntityFrameworkCoreModule(因為不需要直接讀取資料庫了)

  • DemoAppApplicationModule

  • DemoAppHttpApiModule
    新增以下模組

  • AbpAspNetCoreMvcClientModule

  • AbpAspNetCoreAuthenticationOAuthModule

  • AbpAspNetCoreAuthenticationOpenIdConnectModule

  • AbpHttpClientIdentityModelWebModule

  • AbpAspNetCoreMvcUiBasicThemeModule

  • AbpAspNetCoreSerilogModule

  • AbpIdentityHttpApiClientModule

  • AbpFeatureManagementHttpApiClientModule

  • AbpTenantManagementHttpApiClientModule

  • AbpPermissionManagementHttpApiClientModule

2.ConfigureServices
   public override void ConfigureServices(ServiceConfigurationContext context)
        {
            var hostingEnvironment = context.Services.GetHostingEnvironment();
            var configuration = context.Services.GetConfiguration();
            Configure<AbpBundlingOptions>(options =>
            {
                // MVC UI
                options.StyleBundles.Configure(
                    BasicThemeBundles.Styles.Global,
                    bundle =>
                    {
                        bundle.AddFiles("/global-styles.css");
                    }
                );

                //BLAZOR UI
                options.StyleBundles.Configure(
                    BlazorBootstrapThemeBundles.Styles.Global,
                    bundle =>
                    {
                        bundle.AddFiles("/blazor-global-styles.css");
                        //You can remove the following line if you don't use Blazor CSS isolation for components
                        bundle.AddFiles("/DemoApp.Blazor.Server.Host.styles.css");
                    }
                );
            });
            
            context.Services.AddAuthentication(options =>
                {
                    options.DefaultScheme = "Cookies";
                    options.DefaultChallengeScheme = "oidc";
                })
                .AddCookie("Cookies", options => { options.ExpireTimeSpan = TimeSpan.FromDays(365); })
                .AddAbpOpenIdConnect("oidc", options =>
                {
                    options.Authority = configuration["AuthServer:Authority"];
                    options.ClientId = configuration["AuthServer:ClientId"];
                    options.ClientSecret = configuration["AuthServer:ClientSecret"];
                    options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
                    options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
                    options.SaveTokens = true;
                    options.GetClaimsFromUserInfoEndpoint = true;
                    options.Scope.Add("role");
                    options.Scope.Add("email");
                    options.Scope.Add("phone");
                    options.Scope.Add("DemoApp");
                });
            if(hostingEnvironment.IsDevelopment())
            {
                Configure<AbpVirtualFileSystemOptions>(options =>
                {
                    options.FileSets.ReplaceEmbeddedByPhysical<DemoAppDomainSharedModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}src{0}DemoApp.Domain.Shared", Path.DirectorySeparatorChar)));
                    options.FileSets.ReplaceEmbeddedByPhysical<DemoAppDomainModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}src{0}DemoApp.Domain", Path.DirectorySeparatorChar)));
                    options.FileSets.ReplaceEmbeddedByPhysical<DemoAppApplicationContractsModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}src{0}DemoApp.Application.Contracts", Path.DirectorySeparatorChar)));
                    options.FileSets.ReplaceEmbeddedByPhysical<DemoAppApplicationModule>(Path.Combine(hostingEnvironment.ContentRootPath, string.Format("..{0}..{0}src{0}DemoApp.Application", Path.DirectorySeparatorChar)));
                    options.FileSets.ReplaceEmbeddedByPhysical<DemoAppBlazorHostModule>(hostingEnvironment.ContentRootPath);
                });
            }

            context.Services.AddAbpSwaggerGen(
                options =>
                {
                    options.SwaggerDoc("v1", new OpenApiInfo { Title = "DemoApp API", Version = "v1" });
                    options.DocInclusionPredicate((docName, description) => true);
                    options.CustomSchemaIds(type => type.FullName);
                });

            Configure<AbpLocalizationOptions>(options =>
            {
                options.Languages.Add(new LanguageInfo("cs", "cs", "Čeština"));
                options.Languages.Add(new LanguageInfo("en", "en", "English"));
                options.Languages.Add(new LanguageInfo("en-GB", "en-GB", "English (UK)"));
                options.Languages.Add(new LanguageInfo("fi", "fi", "Finnish"));
                options.Languages.Add(new LanguageInfo("fr", "fr", "Français"));
                options.Languages.Add(new LanguageInfo("hi", "hi", "Hindi", "in"));
                options.Languages.Add(new LanguageInfo("it", "it", "Italian", "it"));
                options.Languages.Add(new LanguageInfo("hu", "hu", "Magyar"));
                options.Languages.Add(new LanguageInfo("pt-BR", "pt-BR", "Português (Brasil)"));
                options.Languages.Add(new LanguageInfo("ru", "ru", "Русский"));
                options.Languages.Add(new LanguageInfo("sk", "sk", "Slovak"));
                options.Languages.Add(new LanguageInfo("tr", "tr", "Türkçe"));
                options.Languages.Add(new LanguageInfo("zh-Hans", "zh-Hans", "簡體中文"));
                options.Languages.Add(new LanguageInfo("zh-Hant", "zh-Hant", "繁體中文"));
            });

            Configure<AbpMultiTenancyOptions>(options =>
            {
                options.IsEnabled = MultiTenancyConsts.IsEnabled;
            });

            context.Services.AddTransient(sp => new HttpClient
            {
                BaseAddress = new Uri("/")
            });

          

            Configure<AbpNavigationOptions>(options =>
            {
                options.MenuContributors.Add(new DemoAppMenuContributor());
            });

// Configure<AbpRouterOptions>(options => { options.AppAssembly = typeof(DemoAppBlazorHostModule).Assembly; });
            Configure<AbpRouterOptions>(options => { options.AdditionalAssemblies .Add(typeof(DemoAppBlazorHostModule).Assembly); });//要改成這個
        }
3.OnApplicationInitialization
 public override void OnApplicationInitialization(ApplicationInitializationContext context)
        {
            var env = context.GetEnvironment();
            var app = context.GetApplicationBuilder();

            app.UseAbpRequestLocalization();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseCorrelationId();
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthentication();
            //app.UseJwtTokenMiddleware();

            if (MultiTenancyConsts.IsEnabled)
            {
                app.UseMultiTenancy();
            }

            // app.UseUnitOfWork();
            //app.UseIdentityServer();
            app.UseAuthorization();
            app.UseSwagger();
            app.UseAbpSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "DemoApp API"); });
            app.UseConfiguredEndpoints();

            using (var scope = context.ServiceProvider.CreateScope())
            {
                AsyncHelper.RunSync(async () =>
                {
                    await scope.ServiceProvider
                        .GetRequiredService<IDataSeeder>()
                        .SeedAsync();
                });
            }
        }

3.修改_Imports.razor

刪除

@using Blazorise
@using Blazorise.DataGrid
@using Volo.Abp.BlazoriseUI
@using Volo.Abp.BlazoriseUI.Components

新增

@using BootstrapBlazor.Components
@using Tchivs.Abp.AspNetCore.Blazor.Theme

4.刪除EntityFrameworkCore和Migrations目錄

因為我們直接呼叫httpApi獲取資料所以不需要host去讀取資料庫,所以把這兩個目錄刪除

5._Host.cshtml

@page "/"
@namespace DemoApp.Blazor.Server.Host.Pages
@using System.Globalization
@using Tchivs.Abp.AspNetCore.Blazor.Theme.Bootstrap
@using Tchivs.Abp.AspNetCore.Blazor.Theme.Bootstrap.Components
@using Tchivs.Abp.AspNetCore.Blazor.Theme.Server
@using Volo.Abp.Localization
@{
    Layout = null;
    var rtl = CultureHelper.IsRtl ? "rtl" : string.Empty;
}

<!DOCTYPE html>
<html lang="@CultureInfo.CurrentCulture.Name" dir="@rtl">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>DemoApp.Blazor.Server</title>
    <base href="~/" />

    <abp-style-bundle name="@BlazorThemeBundles.Styles.Global" />
</head>
<body class="abp-application-layout bg-light @rtl">
    <component type="typeof(App)" render-mode="Server" />

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment>
        <a href="" class="reload">Reload</a>
        <a class="dismiss">?</a>
    </div>

    <abp-script-bundle name="@BlazorThemeBundles.Scripts.Global" />
</body>
</html>

6.DemoAppMenuContributor

註釋ConfigureMainMenuAsync方法體,因為我們沒有那幾個模組了

7.修改appsettings.json配置

刪除ConnectionStrings節點

修改AuthServer為:

 "AuthServer": {
    "Authority": "https://localhost:44364",
    "RequireHttpsMetadata": "true",
    "ClientId": "DemoApp_Blazor_Server",
    "ClientSecret": "1q2w3e*"
  }

其中Authority配置項為IdentityServer的uri,ClientId需要記住,等會還要用到

新增:

  "RemoteServices": {
    "Default": {
      "BaseUrl": "https://localhost:44364/"
    },
    "DemoApp": {
      "BaseUrl": "https://localhost:44396/"
    }
  }

這裡配置的是DemoApp httpapi的uri和identityserver許可權、賬號管理相關API

5.新增登入控制器

建立Controllers目錄,新增AccountController

    public class AccountController : ChallengeAccountController
    {

    }

6.新增identityServer配置

開啟DemoApp.IdentityServer專案

1.修改appsettings.json

在IdentityServer的Clients中新增

      "DemoApp_Blazor_Server": {
        "ClientId": "DemoApp_Blazor_Server",
        "RootUrl": "https://localhost:44313/"
        "ClientSecret": "1q2w3e*",
      }

定位到IdentityServer/IdentityServerDataSeedContributor.cs,新增IdentityServer配置。

修改CreateClientsAsync方法,新增

      var blazorServerTieredClientId = configurationSection["DemoApp_Blazor_Server:ClientId"];
            if (!blazorServerTieredClientId.IsNullOrWhiteSpace())
            {
                var blazorServerTieredClientRootUrl = configurationSection["DemoApp_Blazor_Server:RootUrl"].EnsureEndsWith('/');

                /* Admin_BlazorServerTiered client is only needed if you created a tiered blazor server
                 * solution. Otherwise, you can delete this client. */

                await CreateClientAsync(
                    name: blazorServerTieredClientId,
                    scopes: commonScopes,
                    grantTypes: new[] { "hybrid" },
                    secret: (configurationSection["DemoApp_Blazor_Server:ClientSecret"] ?? "1q2w3e*").Sha256(),
                    redirectUri: $"{blazorServerTieredClientRootUrl}signin-oidc",
                    postLogoutRedirectUri: $"{blazorServerTieredClientRootUrl}signout-callback-oidc",
                    frontChannelLogoutUri: $"{blazorServerTieredClientRootUrl}Account/FrontChannelLogout",
                    corsOrigins: new[] { blazorServerTieredClientRootUrl.RemovePostFix("/") }
                );
            }

修改完成後需要重新開啟IdentityServer配置即可生效。

7.修改選單

定位到Menus>DemoAppMenuContributor.cs

using System.Threading.Tasks;
using DemoApp.MultiTenancy;
using Volo.Abp.UI.Navigation;

namespace DemoApp.Blazor.Server.Host.Menus
{
    public class DemoAppMenuContributor : IMenuContributor
    {
        public async Task ConfigureMenuAsync(MenuConfigurationContext context)
        {
            if (context.Menu.Name == StandardMenus.Main)
            {
                await ConfigureMainMenuAsync(context);
            }
            
            
              
        }

        private Task ConfigureMainMenuAsync(MenuConfigurationContext context)
        {
            var administration = context.Menu.GetAdministration();
            context.Menu.Items.Insert(0,
                new ApplicationMenuItem("Index", displayName: "Index", "/", icon: "fa fa-home"));
            // if (MultiTenancyConsts.IsEnabled)
            // {
            //     administration.SetSubItemOrder(TenantManagementMenuNames.GroupName, 1);
            // }
            // else
            // {
            //     administration.TryRemoveMenuItem(TenantManagementMenuNames.GroupName);
            // }
            //
            // administration.SetSubItemOrder(IdentityMenuNames.GroupName, 2);
            // administration.SetSubItemOrder(SettingManagementMenus.GroupName, 3);

            return Task.CompletedTask;
        }
    }
}

結語

以上為替換詳細步驟,如果嫌麻煩或者報錯可以下載demo原始碼自行編譯檢視

未完成的

由於移除了abp中的幾個頁面模組,所以需要重寫使用者管理、角色管理、租戶管理等頁面,這些模組我完善之後會放出來。還有identityServer的登入頁面也應該重寫。

相關文章