开发指南

实体定义

QuickAdmin.EntityBaseopen in new window 类库提供了多组基础接口及默认实现, 包括自增值、审计、软删除、显示顺序、树形结构等等,可利用它们来快速定义各种实体,这些实体的主键属性均为 Id,主键字段名称默认也为 Id
QuickAdmin.Net 内置了与这些不同类型实体匹配的实体服务,服务内部会自动处理相关属性。

Sortable 打头的基类表示可指定记录显示顺序的实体(含有一个 DisplayOrderopen in new window 属性),SoftDeletion 打头的基类表示软删除实体, Audit 打头的基类表示包含录入人、更新人信息的审计实体,FullAudit 打头的基类表示包含录入人、更新人、删除人信息的审计实体(即它也是软删除实体)。

TIP

QuickAdmin.Net 内的系统菜单、组织机构、用户等等均是 Sortable 实体以便能够调整显示顺序,可去示例项目或在线演示看看效果。

定义实体时可从这些基类继承,然后书写业务属性即可。以下为实体定义部分示例。

using QuickAdmin.EntityBase;
using QuickAdmin.EntityBase.AuditBaseImplementation;
using FreeSql.DataAnnotations;

// long 类型主键实体
public class MyEntity1 : EntityWithIdKey
{
}

// long 类型自增值主键实体
public class MyEntity2 : EntityWithAutoIdKey
{
}

// int 类型主键实体
public class MyEntity3 : EntityWithIdKey<int>
{
}

// int 类型自增值主键实体
public class MyEntity4 : EntityWithIdKey<int>
{
  [Column(IsPrimary = true, IsIdentity = true)]
  public override int Id { get => base.Id; set => base.Id = value; }
}

// string 类型主键实体
public class MyEntity5 : EntityWithIdKey<string>
{
}

// UUID 类型主键实体(主键数据类型为 string,默认值为 32 位 GUID)
public class MyEntity6 : EntityWithUUIDKey
{
}

// long 类型自增值主键,并可指定显示顺序的实体
public class MyEntity7 : SortableEntityWithAutoIdKey
{
}

// 包含审计字段的 long 类型自增值主键实体
public class MyEntity8 : AuditEntityWithAutoIdKey
{
}

// 可指定显示顺序的,UUID 类型主键的,审计+软删除 实体
public class MyEntity9 : SortableFullAuditEntityWithUUIDKey
{
}

// 与部门相关的业务数据实体
// QuickAdmin.Net 内置了对 与部门相关的业务数据 的关联查询支持,可递归查询某机构下的业务数据,
// 只需实体实现 IDeptRelatedEntity 或 IDeptRelatedEntityWithDeptPath,然后用内置的 DeptRelatedCRUDService 即可进行递归查询
public class MyEntity10 : EntityWithAutoIdKey, IDeptRelatedEntityWithDeptPath
{
  [Column(IsNullable = false, StringLength = 36)]
  public string DeptId { get; set; }

  // 此属性不映射到表字段,通常是在列表里需要显示数据所属部门时使用
  [Column(IsIgnore = true)]
  public string DeptPath { get; set; }
}

// 树形结构示例,比如行政区域实体
// QuickAdmin.Net 内置了可对树形结构实体进行增、删、改、查,并可递归查询的相关服务
public class Area : EntityWithIdKey<string>, ITreeEntity<Area>, IEntityWithNodeLevel
{
  public string Name { get; set; }

  public string ParentId { get; set; }

  [Navigate(nameof(ParentId))]
  [System.Text.Json.Serialization.JsonIgnore]
  public Area Parent { get; set; }

  [Navigate(nameof(ParentId))]
  public List<Area> Childs { get; set; } = new List<Area>();

  // 可在树形结构实体内加入一个节点级别字段,以方便某些查询需求。节点级别从 1 开始,1 表示根节点
  // QuickAdmin.Net 的内置 TreeEntityCRUDService 服务支持在添加记录时自动处理 NodeLevel
  public int NodeLevel { get; set; }
}

当要适配现有数据库去定义实体时(DbFirst),若基类的某些定义与现有数据表不一致,可通过重写去达成适配。
比如该数据库各表的审计字段只有录入人/录入时间,没有修改人/修改时间,且录入人/录入时间字段名称不是 AuditEntity 里的属性名称,此时可自行定义一个基类并重写审计属性,相关实体再从该基类继承:

public abstract class MyAuditEntityBase : AuditEntityWithAutoIdKey
{
  [Column(CanUpdate = false, IsNullable = false, Name = "LrrId")] // 字段名称不是 CreatorId,而是 LrrId
  public override string CreatorId { get => base.CreatorId; set => base.CreatorId = value; }

  [Column(CanUpdate = false, ServerTime = DateTimeKind.Local, Name = "LrShj")] // 字段名称不是 CreatedTime,而是 LrShj
  public override DateTime CreatedTime { get => base.CreatedTime; set => base.CreatedTime = value; }

  [Column(IsIgnore = true)] // 表中没有 UpdaterId 字段
  public override string UpdaterId { get => base.UpdaterId; set => base.UpdaterId = value; }

  [Column(IsIgnore = true)] // 表中没有 UpdatedTime 字段
  public override DateTime? UpdatedTime { get => base.UpdatedTime; set => base.UpdatedTime = value; }
}

[Table(Name = "some_table")]
public class SomeEntity : MyAuditEntityBase
{
}

实体服务

QuickAdmin.Serviceopen in new window 类库提供了通用 CRUD/Paging 服务,可利用它们来快速定义各种实体的 CRUD 和分页查询服务,参照下面的介绍去使用。
使用内置的通用服务不是必须的,你可以完全自行实现自己的实体服务。

设计实体类型

通用 CRUD 服务需要一个实体设计三个类型:实体类型、输入类型和过滤器类型。
输入类型可以直接就用实体类型或者单独设计,视情况而定。单独设计时,必须用 AutoMapper 建立输入类型与实体类型可互相转换的映射关系。
过滤器类型用来在查询数据时构造查询条件,可设计为从 CommonFilteropen in new window 继承的一个类,这样能够充分利用本类库内置的各种查询功能,并简化使用者代码。

以 ASP.NET Core 教程里的电影信息数据表为例,其相关类型可设计如下:

// Movie 实体,将其设计为 FullAuditEntity
public class Movie : FullAuditEntityWithAutoIdKey
{
  [Required]
  public string Title { get; set; }
  public DateTime ReleaseDate { get; set; }
  public string Genre { get; set; }
  public decimal Price { get; set; }
}
// Movie 输入DTO
public class MovieInput
{
  public string Title { get; set; }
  public DateTime ReleaseDate { get; set; }
  public string Genre { get; set; }
  public decimal Price { get; set; }
}

// Movie 过滤器DTO,可以自行设计:
public class MovieFilter
{
  public string Title { get; set; }
  public DateTime? BeginReleaseDate { get; set; }
  public DateTime? EndReleaseDate { get; set; }
  public decimal? MinPrice { get; set; }
  public decimal? MaxPrice { get; set; }
}
// 也可以直接继承自 CommonFilter:
public class MovieFilter : QuickAdmin.Entity.DTO.Common.CommonFilter
{
  // 继承后仍然可以定义更多过滤条件
}

需要用 AutoMapper 建立 MovieMovieInput 可互相转换的映射关系:

// QuickAdmin.Net 已引用了 AutoMapper 相关包
using AutoMapper;

// 定义 AutoMapper Profile
public class MyAutoMapperProfile : AutoMapper.Profile
{
  public MyAutoMapperProfile()
  {
    CreateMap<Movie, MovieInput>().ReverseMap();
    ...
  }
}

// 然后在启动代码加入:
builder.Services.AddAutoMapper(typeof(MyAutoMapperProfile));

定义实体服务

QuickAdmin.Service 类库内通用 CRUD 的核心类型是 ICRUDService<>open in new window 泛型基接口和 CRUDServiceBase<>open in new window 泛型抽象基类, CRUDServiceBase 内实现了增删改查以及分页查询服务,然后针对不同的实体类型又派生出若干泛型基类,如对 ISortableEntity 类型实体,实现对记录显示顺序的调整,对树形结构实体,实现各种树形查询等等。 你可以利用这些接口/基类快速实现自己的实体服务。
各个泛型基类对一些操作尽可能的进行了分解,以方便继承类通过重写进行定制,具体请查阅其参考文档。

使用方法很简单:根据自己实体的类型从 QuickAdmin.Serviceopen in new window 命名空间下选取合适的接口和泛型基类, 然后定义自己的实体的服务接口和服务实现类,服务接口继承一下选取的接口,服务实现类则直接继承对应的泛型基类并实现你的服务接口即可。

以上侧的 Movie 实体为例,当 MovieFilter 完全自定义时其实体服务可设计如下:

using QuickAdmin.Service;

// Movie CRUD服务接口
public interface IMovieService : ICRUDService<Movie, long, MovieInput, MovieFilter>
{
  // 可继续定义对 Movie 的其它操作,并在 MovieService 里去实现
}
// Movie CRUD服务实现类,继承自抽象基类,必须重写应用过滤器的各方法
public class MovieService : CRUDServiceBase<Movie, long, MovieInput, MovieFilter>, IMovieService
{
  protected override ISelect<Movie> ApplyFilter(ISelect<Movie> iSel, MovieFilter filter)
  {
    if (filter == null) return iSel;
    if (filter.Title.NotNullNorEmpty())
      iSel = iSel.Where(a => a.Title.Contains(filter.Title));
    if (filter.BeginReleaseDate.HasValue)
      iSel = iSel.Where(a => a.ReleaseDate >= filter.BeginReleaseDate);
    if (filter.EndReleaseDate.HasValue)
      iSel = iSel.Where(a => a.ReleaseDate <= filter.EndReleaseDate);
    if (filter.MinPrice.HasValue)
      iSel = iSel.Where(a => a.Price >= filter.MinPrice);
    if (filter.MaxPrice.HasValue)
      iSel = iSel.Where(a => a.Price <= filter.MaxPrice);
    return iSel;
  }
  protected override IServiceResult<IUpdate<Movie>> GetFilteredIUpdate(MovieFilter filter)
  {
    if (filter == null)
      throw new ArgumentNullException(nameof(filter));
    IUpdate<Movie> iUpdate = IUpdateObj;
    if (filter.Title.NotNullNorEmpty())
      iUpdate = iUpdate.Where(a => a.Title.Contains(filter.Title));
    if (filter.BeginReleaseDate.HasValue)
      iUpdate = iUpdate.Where(a => a.ReleaseDate >= filter.BeginReleaseDate);
    if (filter.EndReleaseDate.HasValue)
      iUpdate = iUpdate.Where(a => a.ReleaseDate <= filter.EndReleaseDate);
    if (filter.MinPrice.HasValue)
      iUpdate = iUpdate.Where(a => a.Price >= filter.MinPrice);
    if (filter.MaxPrice.HasValue)
      iUpdate = iUpdate.Where(a => a.Price <= filter.MaxPrice);
    return ServiceResult.Ok(iUpdate);
  }
  protected override IServiceResult<IDelete<Movie>> GetFilteredIDelete(MovieFilter filter)
  {
    if (filter == null)
      throw new ArgumentNullException(nameof(filter));
    IDelete<Movie> iDel = IDeleteObj;
    if (filter.Title.NotNullNorEmpty())
      iDel = iDel.Where(a => a.Title.Contains(filter.Title));
    if (filter.BeginReleaseDate.HasValue)
      iDel = iDel.Where(a => a.ReleaseDate >= filter.BeginReleaseDate);
    if (filter.EndReleaseDate.HasValue)
      iDel = iDel.Where(a => a.ReleaseDate <= filter.EndReleaseDate);
    if (filter.MinPrice.HasValue)
      iDel = iDel.Where(a => a.Price >= filter.MinPrice);
    if (filter.MaxPrice.HasValue)
      iDel = iDel.Where(a => a.Price <= filter.MaxPrice);
    return ServiceResult.Ok(iDel);
  }
}

MovieFilter 继承自 CommonFilter 时其实体服务可设计如下:

using QuickAdmin.Service;

// Movie CRUD服务接口
public interface IMovieService : ICRUDService<Movie, long, MovieInput, MovieFilter>
{
  // 可继续定义对 Movie 的其它操作,并在 MovieService 里去实现
}
// Movie CRUD服务实现类,直接使用 CommonCRUDService
public class MovieService : CommonCRUDService<Movie, long, MovieInput, MovieFilter>, IMovieService
{
}

如此定义后,IMovieService 就已经具备了增删改查和分页查询功能。
CRUDServiceBase/CommonCRUDService 泛型基类内对一些操作进行了分解,如果需要,你可在 MovieService 里方便的重写相关方法/属性去定制要执行的逻辑。
Movie 被设计为 FullAuditEntity,CRUDServiceBase 内会自动处理相关审计字段,不需要在继承类 MovieService 里做任何额外实现:添加记录时会自动填充录入人/录入时间, 更新记录时会自动填充修改人/修改时间,删除记录时则是软删除而不会物理删除(填充删除人/删除时间)。

有关内置服务的更多介绍参见通用 CRUD 章节。

注册服务

单个服务注册:

builder.Services.AddQuickAdmin(...);

// 在调用 AddQuickAdmin() 之后去注册
builder.Services.AddSingleton(typeof(IMovieService), typeof(MovieService));

当实体服务位于独立的程序集内或位于某个命名空间下,可利用 QuickAdmin.Net 提供的批量注册方法去注册,只需一行代码,例如:

using QuickAdmin.Common;

...

// 注册类型 IMovieService 所在程序集内的所有满足条件的服务
builder.Services.AddServicesFromAssemblyExceptNotUseDIs(typeof(IMovieService).Assembly);

// 注册类型 SomeType 所在程序集内的,"QHSE.Service" 命名空间下的所有满足条件的服务
builder.Services.AddServicesFromAssemblyExceptNotUseDIs(typeof(SomeType).Assembly, "QHSE.Service.");

参见 AddServicesFromAssemblyExceptNotUseDIsopen in new window 以及其它批量注册扩展方法文档, 以及 UseDIopen in new window/NotUseDIopen in new window 特性说明。

使用服务

用常规做法使用,比如采用构造函数注入的方式,将服务注入到控制器或页面中:

public class MyController : ControllerBase
{
  IMovieService movieService;
  public MyController(IMovieService movieService)
  {
      this.movieService = movieService;
  }
}

也可利用 QuickAdmin.Net 提供的一组获取服务open in new window的方法,如 GetAppService<T>open in new window, 随时获取所需的服务:

using QuickAdmin.Common;

public class MyPageModel : PageModel
{
  public void OnGet()
  {
    var service = G.GetAppService<IMovieService>();
    var pagingOutput = service.GetPagingOutput();
    ...
  }
  public IActionResult OnPostDeleteMovie(long id)
  {
    ...
    var service = G.GetAppService<IMovieService>();
    var serviceResult = service.Delete(id);
    ...
  }
}

操作其它数据库的数据

要操作与系统表不在同一数据库的其它数据库中的数据,你需要构造相应的 IFreeSql 实例,然后重写 fsqlopen in new window 属性即可:

// 构造其它数据库 IFreeSql 实例的类
public class OthDbs
{
  static Lazy<IFreeSql> _sqlServerLazy = new Lazy<IFreeSql>(() => new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.SqlServer, "Data Source=(local);User ID=user;Password=pwd;Initial Catalog=db;Pooling=true;Min Pool Size=1;TrustServerCertificate=true")
    .Build()
  );
  public static IFreeSql SqlServer => _sqlServerLazy.Value;
}

public interface IMovieService : ICRUDService<Movie, long, MovieInput, MovieFilter>
{
}
public class MovieService : CommonCRUDService<Movie, long, MovieInput, MovieFilter>, IMovieService
{
  // 操作指定 SqlServer 数据库里的 Movie 数据
  protected override IFreeSql fsql => OthDbs.SqlServer;
}















 
 

直接使用内置通用服务

你也可以不去自定义实体服务,直接使用内置的通用 CRUD 服务进行增删改查和分页查询操作:创建相应泛型基类实例,调用相应方法即可。

// 创建泛型基类实例
var service = new CommonCRUDService<Movie, long, MovieInput, CommonFilter>(); // 过滤器类型可直接使用内置的 CommonFilter
...
// 添加
var result1 = service.Add(input);
...
// 更新
var result2 = service.Update(1, input);
...
// 删除
var result3 = service.Delete(1);
...
// 分页查询
var pagingOutput = service.GetPagingOutput(new PagingInput<CommonFilter>
{
  PageSize = 20,
  Filter = new CommonFilter
  {
    // 查找 Western 类型电影
    DataItem1Name = nameof(Movie.Genre),
    DataItem1Value = "=Western"
  }
});

IServiceResult 介绍

系统定义了 IServiceResultopen in new windowIServiceResult<T>open in new window 接口以及相应实现类用来表示服务调用结果,前者只有返回代码与返回消息,后者则还包含返回自定义类型数据。其 Code 属性为 0 表示调用成功,非 0 表示调用失败,返回消息放于 Msg 属性,要返回的数据则置于 Data 属性。

在设计实体服务时可按需将服务方法的返回类型定义为此接口,并可使用 ServiceResultopen in new window 类提供的静态方法快速返回各种调用结果。
所有以此接口作为返回结果的服务方法在任何情况下都不应该返回 null,即要么返回表示成功的结果,要么返回表示失败的结果。

// 返回数据类型定义
public class Foo
{
  public long FooId { get; set; }
  public string FooName { get; set; }
}
// 服务接口
public interface IFooService
{
  IServiceResult<Foo> Get(long id);
  IServiceResult Del(long id);
}
// 服务实现
public class FooService : IFooService
{
  public IServiceResult<Foo> Get(long id)
  {
    Foo data = ... // 获取数据
    if (data == null)
      return ServiceResult.NotFound<Foo>();
    return ServiceResult.Ok<Foo>(data);
  }
  public IServiceResult Del(long id)
  {
    ...
    if (failed)
      return ServiceResult.Failed("error msg");
    return ServiceResult.Ok();
  }
}



















 
 





 
 


IServiceResult 还提供了 ToJsonResultopen in new window / ToCamelCaseJsonResultopen in new window 方法,可在 API 控制器内快速将服务调用结果转为 json 结果返回给前端:

[Route("api/[controller]/[action]")]
[ApiController]
public class FooController : ControllerBase
{
  IFooService service;
  public FooController(IFooService service)
  {
    this.service = service;
  }

  [HttpPost]
  public IActionResult GetFoo1(long id)
  {
    return service.Get(id).ToJsonResult(G.InternalJsonSerializerOptions);
    // 或者:
    //return service.Get(id).ToJsonResult(new System.Text.Json.JsonSerializerOptions());
  }

  [HttpPost]
  public IActionResult GetFoo2(long id)
  {
    return service.Get(id).ToCamelCaseJsonResult();
  }
}













 





 




GetFoo1/GetFoo2 返回给前端的 json 将分别为:

// GetFoo1:
{ "Code": 0, "Msg": null, "Data": { "FooId": 123, "FooName": "abc" } }

// GetFoo2:
{ "code": 0, "msg": null, "data": { "fooId": 123, "fooName": "abc" } }

IServiceResult 也不是仅用于实体服务中,也可在其它服务中使用。

Razor 页面

QuickAdmin.Net 内置了布局文件、页面基类以及对 FineUICore 的支持,创建 Razor 页面时你可自行决定是否使用它们。
使用内置的布局文件、页面基类来创建 Razor 页面,你将能得到 QuickAdmin.Net 提供的全部页面功能;只使用二者之一,则得到部分页面功能。
FineUICore 提供了丰富的控件,可快速构建页面,但你也可以选择用 html/css/js 构建页面内容。

内置布局文件

QuickAdmin.Net 内置了布局文件 "_QAdminLayout.cshtml",修改相应 _ViewStart.cshtml 和 _ViewImports.cshtml 文件来使用该布局。

修改 _ViewStart.cshtml:

@{
  Layout = "/Areas/QAdmin/Pages/Shared/_QAdminLayout.cshtml";
}

 

修改 _ViewImports.cshtml:

@using QuickAdminApp
@namespace QuickAdminApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@using QuickAdmin.RCL

@using FineUICore
@addTagHelper *, FineUICore




 

 
 

若是用 QuickAdmin.Net.Templates 里的模板创建的项目,_ViewStart.cshtml 和 _ViewImports.cshtml 文件已改好。

内置页面基类

QAdminBasePageModelopen in new window 是 QuickAdmin.Net 提供的一个页面基类, 新建 Razor 页面后将其改为从该类继承即可使用 QuickAdmin.Net 的各项内置页面功能:登录判断、授权判断、自动加载 css/js 资源(参见 RenderPageJsopen in new window)、 辅助方法等等,具体参考 QAdminBasePageModel 类文档。

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using QuickAdmin.RCL;

namespace QuickAdminApp.Pages
{
  public class SimplePageModel : QAdminBasePageModel
  {
    public IActionResult OnGet()
    {
      return Page();
    }
  }
}






 







不使用 QAdminBasePageModel 页面基类也是支持的,只不过你需要自行处理相关逻辑。

页面结构

"_QAdminLayout.cshtml" 布局文件要求页面的典型结构是这样的:

@page
@model QuickAdminApp.Pages.SimplePageModel
@{
  // 页面标题:
  ViewData["Title"] = "Simple Page"; // 或者在 cs 里重写 PageTitle 属性
  var F = @Html.F(); // 若页面使用 FineUICore,需要加入此行
}

@section head {
  <link rel="stylesheet" href="~/css/foo.css" />
  <style type="text/css">
    div {
      font-size: 14px;
    }
  </style>
}

@section body {
  <div>
    this is a simple page
  </div>
}

@section script {
  <script src="~/js/foo.js"></script>
  <script>
    function foo() {
      ...
    }
  </script>
}

即页面内可包含三个 Section:"head"、"body" 和 "script"。
可只包含 "body" Section:

@page
@model QuickAdminApp.Pages.SimplePageModel
@{
  ViewData["Title"] = "Simple Page";
}

@section body {
  <div>
    this is a simple page
  </div>
}

也可一个 Section 也不包含:

@page
@model QuickAdminApp.Pages.SimplePageModel
@{
  ViewData["Title"] = "Simple Page";
}

<div>
  this is a simple page
</div>

此时所书写的内容会被全部当作 "body" Section。注意若没有 "body" Section,"head" 和 "script" Section 也不能有。

定制登录页

可通过配置文件对登录页进行定制,如 Logo 图片、AppName、是否启用手机号登录、是否启用扫码登录、加载自定义 css/js 等等。

例如某 QHSE 系统使用了自己的 Logo,启用了所有登录方式,且要用户能够始终自动登录,不需要显示 "下次自动登录" 检查框,像如下效果: Login
其部分配置如下:

{
  ...
  "BrowserAutoLoginMode": "Always", // "Always" 表示始终自动登录
  ...
  "RCL": {
    ...
    "AppShortName": "QHSE", // 登录页显示的是应用短名称
    ...
    "LoginPage": {
      ...
      "EnableSmsLogin": true,           // 启用手机号登录
      "EnableWeChatQRCodeLogin": true,  // 启用扫码登录
      "PageCss": "css/login.css",       // 加载自定义 css
      "LogoImg": "img/qhse.png",        // 自定义 Logo 图片
      ...
    },
    ...
  }
}

自定义 login.css 里修改了 Logo 的样式,以适应自己的图片:

#imgLogo img {
  width: 80px;
  height: 60px;
  border-radius: 50%;
  box-shadow: 0px 0px 10px 5px rgba(255, 255, 255, 0.8);
}

QuickAdmin.Net 还内置了另外一种布局的登录页,配置 UseUserLoginPageopen in new window 为 true 即可使用它, 布局如下图(里边的背景图片可利用 BackgroundImageopen in new window 配置替换):

UserLogin

你也可以自行设计实现自己的登录页,只需要在你的登录验证成功后再去调用一个 IAuthServiceopen in new window 服务的执行登录逻辑的方法。
如果仍然是让用户输入账户和密码登录,后端可去调用 Loginopen in new window 方法;如果登录验证完全是在自己的登录逻辑内处理的(比如用了第三方登录等),那么验证成功后再去调用 DirectlyLoginopen in new window 进入登录状态:

...
// 获取 IAuthService
var authService = G.GetAppService<IAuthService>();
// 调用方法
var serviceResult = authService.DirectlyLogin(...);
...

如果你的登录页不是应用根目录下的 Login 页,则还需在配置文件里的 "RCL:LoginPageName" 项去指定,对应属性为 LoginPageNameopen in new window

定制默认页

内置的 Index 也支持充分定制,Logo、单标签还是多标签模式、侧边栏宽度、侧边栏是否折叠、Home 选项卡显示的页面、加载自定义 css/js 等等,参见 IndexPageSettingsopen in new window
左侧功能菜单、顶部快速链接都可动态指定,示例:

Index

若不想用 QuickAdmin.Net 内置的 Index 作为默认页,只需保留应用 Pages 目录下的 Index 页即可,在其中自行设计布局。
IAuthService 包含获取用户可用的系统菜单、快速链接等的方法,如有需要可去调用获取。

如果默认页不是应用根目录下的 Index 页,则还需在配置文件里的 "RCL:DefaultPageName" 项去指定,对应属性为 DefaultPageNameopen in new window

定制内置服务

QuickAdmin.Net 内置的各个服务均可被定制,只需定义继承自内置服务的类,重写需要定制的方法/属性,然后将该类在应用启动时注册到系统服务即可。

例如 ClientInfoParseropen in new window 用来解析客户端信息,重写 GetDevice() 去完善用户设备信息的识别:

public class MyClientInfoParser : ClientInfoParser
{
  protected override string GetDevice(IHeaderDictionary headers)
  {
    // 解析设备信息
  }
}

重写 AuthServiceopen in new window 相应方法执行额外操作:

public class MyAuthService : AuthService
{
  protected override LoginOutput OnLoginSucceed(SysUser user, int? loginWay, LoginInput loginInput, SysAutologinToken autologinToken, string identifier, LoginClientInfo clientInfo)
  {
    LoginOutput loginOutput = base.OnLoginSucceed(user, loginWay, loginInput, autologinToken, identifier, clientInfo);

    // 成功登录后的额外操作
    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.Name, user.AccountId),
        new Claim(ClaimTypes.Role, "SomeRole")
    };
    var claimsIdentity = new ClaimsIdentity(
        claims, CookieAuthenticationDefaults.AuthenticationScheme);
    G.Context.SignInAsync(
        CookieAuthenticationDefaults.AuthenticationScheme,
        new ClaimsPrincipal(claimsIdentity)
    ).GetAwaiter().GetResult();

    return loginOutput;
  }

  protected override void AfterLogout(LoggedOnUser loggedOnUser)
  {
    base.AfterLogout(loggedOnUser);

    // 注销后的额外操作
    G.Context?.SignOutAsync().GetAwaiter().GetResult();
  }
}

重写 UserServiceopen in new window 以下方法可强制用户在修改密码后重新登录:

public class MyUserService : UserService
{
  public override IServiceResult CurrentUserChangePassword(string oldPwd, string newPwd)
  {
    IServiceResult result = base.CurrentUserChangePassword(oldPwd, newPwd);
    if (!result.Success)
      return result;
    return ServiceResult.Ok<bool>(true);
  }
}

在启动时将新的实现类注册到系统服务:

builder.Services.AddQuickAdmin(...);

// 必须在调用 AddQuickAdmin() 之后去注册
builder.Services.AddSingleton(typeof(IClientInfoParser), typeof(MyClientInfoParser));
builder.Services.AddSingleton(typeof(IAuthService), typeof(MyAuthService));
builder.Services.AddSingleton(typeof(IUserService), typeof(MyUserService));

如无特别说明,所有服务均要注册为 Singleton 单例模式。

关于图片验证码

QuickAdmin.Net 内置的 CaptchaServiceopen in new window 生成验证码图片时使用了跨平台的开源图形库 SkiaSharpopen in new window

涉及的配置参数有:CaptchaLettersopen in new window(验证码可用字符)、 CaptchaLengthopen in new window(验证码长度)、 CaptchaNoiseLineCountopen in new window(验证码图片内噪线的数量)。