2023年2月

在一般系统开发中,我们一般要借助于高度定制化的代码生成工具,用于统一代码风,节省开发时间,提高开发效率。不同的项目,它的项目不同分层的基类定义不同,我们需要在框架基类的基础上扩展我们的业务类代码,尽量根据已有数据库设计的信息,完整、详细的重现我们需要的编码工作,一方面引入必要的关系和代码,一方面保留一定的注释代码供了解使用,站在开发者的角度上思考代码如何编写,这样才能够快速、高效的增量化开发我们的业务表应用模块。

1、数据库的设计

一般情况下,我们往往喜欢基于数据库建模我们的业务表存储内容,数据库表是一切业务的核心,熟练的开发人员,往往都能够迅速根据需求的内容,以及框架所需的信息,准确的定义好所需要的表字段,包括界面展示的字段,以及后台必备的一些关键字段,如创建日期,创建人、所属部门等等。

我们一般推荐使用第三方的建模工具,如PowerDesigner工具来进行数据库表的建模,这样可以方便分享和修改,可以根据需要随时调整生成的数据库脚本。

数据库表一般需要添加备注信息,作为生成实体类字段的备注,或者录入界面的文本标签等等,我们框架使用代码生成工具生成代码的时候,尽可能的多利用备注信息,用来标识或者说明类或者字段的信息。

准确而明了的备注信息,可以降低我们介入代码备注说明信息的有效手段。

2、框架的多数据库支持

对于多数据库的支持,一般是框架底层需要考虑的,好的框架往往很容易支持多种数据库的切换处理的。

在开发不同框架的系统应用的时候,我们往往以其中一个数据库系统为蓝本,先开发完成,然后再行扩展其他数据库的支持即可。

例如对于混合框架中,底层主要使用微软企业库实现多种数据库的切换的,它的底层是通过扩展适配来实现的。

而对于ABP框架来说,它提供EFCore的数据库抽象,因此也是支持多种数据库的处理。

ABP默认的数据库是SQLServer,不过ABP框架底层是EF框架,因此也是很容易支持其他类型的数据库的,如切换到Mysql操作,引入不同的包处理,然后调整下代码即可。

ABP官方文档中推荐使用的Mysql处理包,如下所示。

pomelo
.entityframeworkcore
.mysql

pomelo
.entityframeworkcore
.mysql
.design

修改其中对应的常数定义,切换为我们所需要的Mysql数据库即可,如下所示。

不同的数据库支持,EFCore的切换代码,主要是在EntityFrameworkCore项目的类MyProjectDbContextConfigurer下面。

    /// <summary>
    ///配置使用特定数据库的处理/// </summary>
    public static classMyProjectDbContextConfigurer
{
public static void Configure(DbContextOptionsBuilder<MyProjectDbContext> builder, stringconnectionString)
{
builder.UseSqlServer(connectionString);
//默认SQlServer//builder.UseMySql(connectionString, new MySqlServerVersion(new Version(5, 7)));//MySQL//builder.UseNpgsql(connectionString);//PostgreSQL//builder.UseOracle(connectionString);//Oracle }public static void Configure(DbContextOptionsBuilder<MyProjectDbContext>builder, DbConnection connection)
{
builder.UseSqlServer(connection);
//默认SQlServer//builder.UseMySql(connection, new MySqlServerVersion(new Version(5, 7)));//MySQL//builder.UseNpgsql(connection);//PostgreSQL//builder.UseOracle(connection);//Oracle }
}

对于默认从SQLServer切换到Mysql的处理,详细可以参考随笔《
ABP框架使用Mysql数据库,以及基于SQLServer创建Mysql数据库的架构和数据

如果需要支持Oracle数据库,那么也需要引入Oracle的如下驱动即可,如下所示。

然后调整下MyProjectDbContextConfigurer类的代码即可。

3、框架后端代码生成

一般来说,我们基于框架的构建,基本上把通用的接口或者逻辑尽可能都封装在基类或接口里面,以便减少我们需要维护的代码,基类代码越抽象、统一,那么所需要的开发工作量就会非常少,只需要扩展我们自己的业务接口或函数即可,这方面也可以通过调用公用的处理方法,来进一步降低代码量。

ABP初始框架的各个分层的信息,它主要是分为下面几个项目分层。

Application应用层
:应用层提供一些应用服务(Application Services)方法供展现层调用。一个应用服务方法接收一个DTO(数据传输对象)作为输入参数,使用这个输入参数执行特定的领域层操作,并根据需要可返回另一个DTO。

Core领域核心层
,领域层就是业务层,是一个项目的核心,所有业务规则都应该在领域层实现。这个项目里面,除了定义所需的领域实体类外,其实可以定义我们自己的自定义的仓储对象(类似DAL/IDAL),以及定义自己的业务逻辑层(类似BLL/IBLL),以及基于AutoMapper映射规则等内容。

EntityFrameworkCore
实体框架核心层,这个项目不需要修改太多内容,只需要在DbContext里面加入对应领域对象的仓储对象即可。

Migrator数据迁移层
,这个是一个辅助创建的控制台程序项目,如果基于DB First,我们可以利用它来创建我们项目的初始化数据库。

Web.Core Web核心层
,基于Web或者Web API的核心层,提供了对身份登陆验证的基础处理,没有其他内容。

Web.Core.Host Web API的宿主层
,也是动态发布Web API的核心内容,另外在Web API里面整合了Swagger,使得我们可以方便对Web API的接口进行调试。

Tests 单元测试层
,这个提供了一些应用层对象的模拟测试,其中测试的数据库使用的是Entity Framework 的内存数据库,不影响实际数据库内容。

经过我进行简化和优化处理的框架项目结构如下所示。

以字典模块为介绍的话。

例如对于01-Core模块层,需要增加文件

对于03-Application.Common模块来说,需要增加DTO和应用服务层接口文件

而对于04-Application应用层来说,需要增加对应的接口实现文件

而05、06、07模块,我们不需要加入任何文件,08-Caller层加入对WebAPI的远程调用封装类,给Winform、WPF/UWP、控制台程序等调用。

通过整合相关的生成规则,我们可以增加对应的ABP框架代码的生成,如下代码生成工具界面所示。

代码生成工具Database2Sharp地址:
http://www.iqidi.com/database2sharp.htm

最终根据根据选择数据库表信息,一键生成相关ABP架构分层代码,文件结构如下所示。

我们只需要把这些生成的文件,增量式的合并到我们的项目中即可完成后台代码的整合了。

由于我们发布了ApplicationService层的类,那么在Swagger的接口里面,可以查看我们新增的业务接口了,已经具备了常规标准的增删改查等接口了

ABP+Swagger负责API接口的发布展示管理,如下是API接口的管理界面。

待我们熟练各个业务接口的处理逻辑后,我们就可以进行手工的增加,或者调整逻辑了。

4、ABP框架Winform界面代码的生成

ABP框架的界面包含两个部分,一个是Winform界面部分,这个和我们常规的Winform开发框架或者混合框架界面开发差不多,根据数据库表结构生成对应的列表展示和编辑界面,其中整合了条件查询、分页、导入、导出、查看、新增、编辑、删除等操作功能。

Database2Sharp关于ABP框架的Winform界面配置。

设置好后直接生成,代码工具就可以依照模板来生成所需要的WInform列表界面和编辑界面的内容了,如下是生成的界面代码。

放到VS项目里面,就看到对应的窗体界面效果了。

生成界面后,进行一定的布局调整就可以实际用于生产环境了,省却了很多时间。

在应用业务的模块界面生成好并整合到解决方案后,就需要在后台权限管理系统的菜单管理模块中维护对应的入口,添加对应的菜单/工具栏入口了。

菜单资源管理的列表界面界面如下所示

双击树列表或者右侧的列表,都可以对已有的菜单进行编辑,菜单编辑界面如下所示。

主体框架界面采用的是基于菜单的动态生成,以及多文档的界面布局,具有非常好的美观性和易用性。

5、ABP框架Element前端界面生成

Vue+Element的前端界面,是一套BS的纯前端管理界面,它利用后端ABP提供的API接口作为数据源,展示界面。

引入了前后端分离的Vue + Element 作为前端技术路线,那么前后端的边界则非常清晰,前端可以在通过网络获取对应的JSON就可以构建前端的应用了。

主体框架界面采用的是基于后台配置的菜单动态生成,左侧是菜单,右边顶部是特定导航条和内容区。

菜单可以进一步展开更多内容,包括系统的权限模块(机构、用户、角色、菜单、功能),日志,字典等等模块内容,以及其他展示案例模块。

我们一般做增量开发,只需要完成ABP后台代码开发并整合后,使用代码开发工具进一步开发Element前端界面即可,增量开发好对应业务模块内容后整合在Vue&Element的前端框架中,并配置好菜单入口即可展示使用。

在代码生成工具Database2Sharp界面里面,选择【代码生成】【ABP的Vue+Element界面代码】,如下所示。

按照代码工具一步步选定表进行生成,在最后根据需要选择不同的配置生成即可。

或者选择带有树列表的界面,并自定义指定目录位置。

确认生成代码后,我们可以看到对应的API调用JS类和Vue视图文件,如下所示。

我们把这些整合到我们的Vue&Element前端系统项目中。

然后在路由系统中加入对应的路由信息,路由信息是决定菜单是否能够访问的关键,只有在路由列表集合里面,菜单才放行的,如下所示。

在ABP + Vue + Element 快速开发框架里面,我们BS前端的菜单和其CS的菜单是各自分开的,我们在后台的权限模块系统中维护菜单内容并分配给对应角色用户,在用户登录系统后,动态加载菜单展示,并通过菜单的配置信息,跳转到对应的路由上去进行页面展示处理。

菜单资源管理的列表界面界面如下所示

这样就完成了整个ABP框架,后端和前端的整合处理了。

为了方便读者理解,我列出一下前面几篇随笔的连接,供参考:

循序渐进VUE+Element 前端应用开发(1)--- 开发环境的准备工作

循序渐进VUE+Element 前端应用开发(2)--- Vuex中的API、Store和View的使用

循序渐进VUE+Element 前端应用开发(3)--- 动态菜单和路由的关联处理

循序渐进VUE+Element 前端应用开发(4)--- 获取后端数据及产品信息页面的处理

循序渐进VUE+Element 前端应用开发(5)--- 表格列表页面的查询,列表展示和字段转义处理


循序渐进VUE+Element 前端应用开发(6)--- 常规Element 界面组件的使用

循序渐进VUE+Element 前端应用开发(7)--- 介绍一些常规的JS处理函数

循序渐进VUE+Element 前端应用开发(8)--- 树列表组件的使用


循序渐进VUE+Element 前端应用开发(9)--- 界面语言国际化的处理

循序渐进VUE+Element 前端应用开发(10)--- 基于vue-echarts处理各种图表展示

循序渐进VUE+Element 前端应用开发(11)--- 图标的维护和使用


循序渐进VUE+Element 前端应用开发(12)--- 整合ABP框架的前端登录处理


循序渐进VUE+Element 前端应用开发(13)--- 前端API接口的封装处理

循序渐进VUE+Element 前端应用开发(14)--- 根据ABP后端接口实现前端界面展示

循序渐进VUE+Element 前端应用开发(15)--- 用户管理模块的处理

循序渐进VUE+Element 前端应用开发(16)--- 组织机构和角色管理模块的处理

循序渐进VUE+Element 前端应用开发(17)--- 菜单管理

循序渐进VUE+Element 前端应用开发(18)--- 功能点管理及权限控制

VUE+Element 前端应用开发框架功能介绍

循序渐进VUE+Element 前端应用开发(19)--- 后端查询接口和Vue前端的整合

使用代码生成工具快速生成基于ABP框架的Vue+Element的前端界面

循序渐进VUE+Element 前端应用开发(20)--- 使用组件封装简化界面代码

循序渐进VUE+Element 前端应用开发(21)--- 省市区县联动处理的组件使用

循序渐进VUE+Element 前端应用开发(22)--- 简化main.js处理代码,抽取过滤器、全局界面函数、组件注册等处理逻辑到不同的文件中

循序渐进VUE+Element 前端应用开发(23)--- 基于ABP实现前后端的附件上传,图片或者附件展示管理

循序渐进VUE+Element 前端应用开发(24)--- 修改密码的前端界面和ABP后端设置处理

循序渐进VUE+Element 前端应用开发(25)--- 各种界面组件的使用(1)

循序渐进VUE+Element 前端应用开发(26)--- 各种界面组件的使用(2)

电商商品数据库的设计和功能界面的处理

循序渐进VUE+Element 前端应用开发(27)--- 数据表的动态表单设计和数据存储

循序渐进VUE+Element 前端应用开发(28)--- 附件内容的管理

循序渐进VUE+Element 前端应用开发(29)--- 高级查询条件的界面设计

部署基于.netcore5.0的ABP框架后台Api服务端,以及使用Nginx部署Vue+Element前端应用

循序渐进VUE+Element 前端应用开发(30)--- ABP后端和Vue+Element前端结合的分页排序处理

循序渐进VUE+Element 前端应用开发(31)--- 系统的日志管理,包括登录日志、接口访问日志、实体变化历史日志

循序渐进VUE+Element 前端应用开发(32)--- 手机短信动态码登陆处理

循序渐进VUE+Element 前端应用开发(33)--- 邮件参数配置和模板邮件发送处理

使用代码生成工具快速开发ABP框架项目

使用Vue-TreeSelect组件实现公司-部门-人员级联下拉列表的处理

使用Vue-TreeSelect组件的时候,用watch变量方式解决弹出编辑对话框界面无法触发更新的问题

在我们开发Web API应用的时候,我们可以借鉴ABP框架的过滤器Filter和特性Attribute的应用,实现对Web API返回结果的封装和统一异常处理,本篇随笔介绍利用AuthorizeAttribute实现Web API身份认证,利用ActionFilterAttribute实现对常规Web API返回结果进行统一格式的封装,利用ExceptionFilterAttribute实现对接口异常的统一处理,实现我们Web API常用到的几个通用处理过程。

1、Asp.net的Web API过滤器介绍

过滤器主要有这么几种:AuthorizationFilterAttribute 权限验证、ActionFilterAttribute 日志参数验证等、ExceptionFilterAttribute 异常处理捕获。

ActionFilter 主要实现执行请求方法体之前(覆盖基类方法OnActionExecuting),和之后的事件处理(覆盖基类方法OnActionExecuted);ExceptionFilter主要实现触发异常方法(覆盖基类方法OnException)。

ActionFilterAttrubute提供了两个方法进行拦截:

  • OnActionExecuting和OnActionExecuted,他们都提供了同步和异步的方法。
  • OnActionExecuting方法在Action执行之前执行,OnActionExecuted方法在Action执行完成之后执行。

在使用MVC的时候,ActionFilter提供了一个Order属性,用户可以根据这个属性控制Filter的调用顺序,而Web API却不再支持该属性。Web API的Filter有自己的一套调用顺序规则:

所有Filter根据注册位置的不同拥有三种作用域:Global、Controller、Action:

  • 通过HttpConfiguration类实例下Filters.Add()方法注册的Filter(一般在App_Start\WebApiConfig.cs文件中的Register方法中设置)就属于Global作用域;

  • 通过Controller上打的Attribute进行注册的Filter就属于Controller作用域;

  • 通过Action上打的Attribute进行注册的Filter就属于Action作用域;

他们遵循了以下规则:

  • 在同一作用域下,AuthorizationFilter最先执行,之后执行ActionFilter
  • 对于AuthorizationFilter和ActionFilter.OnActionExcuting来说,如果一个请求的生命周期中有多个Filter的话,执行顺序都是Global->Controller->Action;
  • 对于ActionFilter,OnActionExecuting总是先于OnActionExecuted执行;
  • 对于ExceptionFilter和ActionFilter.OnActionExcuted而言执行顺序为Action->Controller->Global;
  • 对于所有Filter来说,如果阻止了请求:即对Response进行了赋值,则后续的Filter不再执行。

另外,值得注意的是,由于Web API的过滤器无法改变其顺序,那么它是按照 AuthorizationFilterAttribute -> ActionFilterAttribute -> ExceptionFilterAttribute 这个执行顺序来处理的,也就是说授权过滤器执行在前面,再次到自定义的ActionFilter,最后才是异常的过滤器处理。

2、Web API的身份授权过滤器处理

我们通过AuthorizationFilterAttribute 过滤器来处理用户Web API接口身份,比每次在代码上进行验证省事很多。

一般情况下,我们只要定义类继承于AuthorizeAttribute即可,由于AuthorizeAttribute是继承于AuthorizationFilterAttribute,如下所示。

    /// <summary>
    ///验证Web Api接口用户身份/// </summary>
    public classApiAuthorizeAttribute : AuthorizeAttribute
{
...........
}

而一般情况下,我们只需要重写bool IsAuthorized(HttpActionContext actionContext) 方法,实现授权处理逻辑即可。

我们在CheckToken的主要逻辑里面,主要对token令牌进行反向解析,并判断用户身份是否符合,如果不符合抛出异常,就会切换到异常处理器里面了。

然后在Web API控制器中,需要授权访问的Api控制器定义即可,我们可以把它放到基类里面声明这个过滤器特性。

那么所有Api接口的访问,都会检查用户的身份了。

2、自定义过滤器特性ActionFilterAttribute 的处理

这个ActionFilterAttribute 主要用于拦截用户访问控制器方法的处理过程,前面说到,OnActionExecuting方法在Action执行之前执行,OnActionExecuted方法在Action执行完成之后执行。

那么我们可以利用它进行函数AOP的处理了,也就是在执行前,执行后进行日志记录等,还有就是常规的参数检查、结果封装等,都可以在这个自定义过滤器中实现。

我们定义一个类WrapResultAttribute来标记封装结果,定义一个类DontWrapResultAttribute来标记不封装结果。

    /// <summary>
    ///用于判断Web API需要包装返回结果./// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface |AttributeTargets.Method)]public classWrapResultAttribute : ActionFilterAttribute
{

}
/// <summary> ///用于判断Web API不需要包装返回结果./// </summary> [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface |AttributeTargets.Method)]public classDontWrapResultAttribute : WrapResultAttribute
{

}

这个处理方式是借用ABP框架中这两个特性的处理逻辑。

利用,对于获取用户身份令牌的基础操作接口,我们可以不封装返回结果,如下标记所示。

那么执行后,返回的结果如下所示,就是正常的TokenResult对象的JSON信息

{"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIxIiwiaWF0IjoxNjE3MjY0MDQ4LCJqdGkiOiI0NTBjZmY3OC01OTEwLTQwYzUtYmJjMC01OTQ0YzNjMjhjNTUiLCJuYW1lIjoiYWRtaW4iLCJjb3JwaWQiOiI2IiwiY2hhbm5lbCI6IjAiLCJzaGFyZWRrZXkiOiIxMjM0YWJjZCJ9.Umv4j80Sj6BnoCCGO5LrnyddwtfqU5a8Jii92SjPApw","expires_in": 604800}

如果取消这个DontWrapResult的标记,那么它就继承基类BaseApiController的WrapResult的标记定义了。

    /// <summary>
    ///所有接口基类/// </summary>
[ExceptionHandling]
[WrapResult]
public class BaseApiController : ApiController

那么接口定义不变,但是返回的okenResult对象的JSON信息已经经过包装了。

{"result": {"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIxIiwiaWF0IjoxNjE3MjY0NDQ5LCJqdGkiOiJmZTAzYzhlNi03NGVjLTRjNmEtYmMyZi01NTU3MjFiOTM1NDEiLCJuYW1lIjoiYWRtaW4iLCJjb3JwaWQiOiI2IiwiY2hhbm5lbCI6IjAiLCJzaGFyZWRrZXkiOiIxMjM0YWJjZCJ9.9B4dyoE9YTisl36A-w_evLs2o8raopwvDUIr2LxhO1c","expires_in": 604800},"targetUrl": null,"success": true,"error": null,"unAuthorizedRequest": false,"__api": true}

这个JSON格式是我们一个通用的接口返回,其中Result里面定义了返回的信息,而Error里面则定义一些错误信息(如果有错误的话),而success则用于判断是否执行成功,如果有错误异常信息,那么success返回为false。

这个通用返回的定义,是依照ABP框架的返回格式进行调整的,可以作为我们普通Web API的一个通用返回结果的处理。

前面提到过ActionFilterAttribute自定义处理过程,在控制器方法完成后,我们对返回的结果进行进一步的封装处理即可。

我们需要重写逻辑实现OnActionExecuted的函数

在做包装返回结果之前,我们需要判断是否方法或者控制器设置了不包装的标记DontWrapResultAttribute。

        public override voidOnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
//如果有异常,则退出交给Exception的通用处理 if (actionExecutedContext.Exception != null)return;//正常完成,那么判断是否需要包装结果输出,如果不需要则返回 var dontWrap = false;var actionContext =actionExecutedContext.ActionContext;if(actionContext.ActionDescriptor is ReflectedHttpActionDescriptor actionDesc)
{
//判断方法是否包含DontWrapResultAttribute dontWrap = actionDesc.MethodInfo.GetCustomAttributes(inherit: false)
.Any(a
=> a.GetType().Equals(typeof(DontWrapResultAttribute)));if (dontWrap) return;
}
if(actionContext.ControllerContext.ControllerDescriptor is HttpControllerDescriptor controllerDesc)
{
//判断控制器是否包含DontWrapResultAttribute dontWrap = controllerDesc.GetCustomAttributes<Attribute>(inherit: true)
.Any(a
=> a.GetType().Equals(typeof(DontWrapResultAttribute)));if (dontWrap) return;
}

上述代码也就是如果找到方法或者控制器有定义DontWrapResultAttribute的,就不要包装结果,否则下一步就是对结果进行封装了

            //需要包装,那么就包装输出结果
            AjaxResponse result = newAjaxResponse();//状态代码
            var statusCode =actionContext.Response.StatusCode;//读取返回的内容
            var content = actionContext.Response.Content.ReadAsAsync<object>().Result;//请求是否成功
            result.Success =actionContext.Response.IsSuccessStatusCode;//重新封装回传格式
            actionExecutedContext.Response = newHttpResponseMessage(statusCode)
{
Content
= new ObjectContent<AjaxResponse>(newAjaxResponse(content), JsonFomatterHelper.GetFormatter())
};

其中AjaxResponse是参考ABP框架里面返回结果的类定义处理的。

    public abstract classAjaxResponseBase
{
public string TargetUrl { get; set; }public bool Success { get; set; }public ErrorInfo Error { get; set; }public bool UnAuthorizedRequest { get; set; }public bool __api { get; } = true;
}
[Serializable]public class AjaxResponse<TResult>: AjaxResponseBase
{
public TResult Result { get; set; }

}

3、异常处理过滤器ExceptionFilterAttribute

前面介绍到,Web API的过滤器无法改变其顺序,它是按照 AuthorizationFilterAttribute -> ActionFilterAttribute -> ExceptionFilterAttribute 这个执行顺序来处理的,也就是说授权过滤器执行在前面,再次到自定义的ActionFilter,最后才是异常的过滤器处理。

异常处理过滤器,我们定义后,可以统一处理和封装异常信息,而我们只需要实现OnException的方法即可。

    /// <summary>
    ///自定义异常处理/// </summary>
    public classExceptionHandlingAttribute : ExceptionFilterAttribute
{
/// <summary> ///统一对调用异常信息进行处理,返回自定义的异常信息/// </summary> /// <param name="context">HTTP上下文对象</param> public override voidOnException(HttpActionExecutedContext context)
{
}
}

完整的处理过程代码如下所示。

    /// <summary>
    ///自定义异常处理/// </summary>
    public classExceptionHandlingAttribute : ExceptionFilterAttribute
{
/// <summary> ///统一对调用异常信息进行处理,返回自定义的异常信息/// </summary> /// <param name="context">HTTP上下文对象</param> public override voidOnException(HttpActionExecutedContext context)
{
//获取方法或控制器对应的WrapResultAttribute属性 var actionDescriptor =context.ActionContext.ActionDescriptor;var wrapResult = actionDescriptor.GetCustomAttributes<WrapResultAttribute>(inherit: true).FirstOrDefault()?? actionDescriptor.ControllerDescriptor.GetCustomAttributes<WrapResultAttribute>(inherit: true).FirstOrDefault();//如设置,记录异常信息 if (wrapResult != null &&wrapResult.LogError)
{
LogHelper.Error(context.Exception);
}
var statusCode =GetStatusCode(context, wrapResult.WrapOnError);if (!wrapResult.WrapOnError)
{
context.Response
= newHttpResponseMessage(statusCode) {
Content
= newStringContent(context.Exception.Message.ToJson())
};
context.Exception
= null; //Handled! return;
}
//使用AjaxResponse包装结果 var content = new ErrorInfo(context.Exception.Message/*, context.Exception.StackTrace*/);var isAuth = context.Exception isAuthorizationException;
context.Response
= newHttpResponseMessage(statusCode)
{
Content
= new ObjectContent<AjaxResponse>(newAjaxResponse(content, isAuth), JsonFomatterHelper.GetFormatter())
};
context.Exception
= null; //Handled! }

这样我们在BaseApiController里面声明即可。

这样,一旦程序处理过程中,有错误抛出,都会统一到这里进行处理,有异常的返回JSON如下所示。

本篇随笔介绍利用AuthorizeAttribute实现Web API身份认证,利用ActionFilterAttribute实现对常规Web API返回结果进行统一格式的封装,利用ExceptionFilterAttribute实现对接口异常的统一处理,实现我们Web API常用到的几个通用处理过程。

在Asp.net Web API中,对业务数据的分页查询处理是一个非常常见的接口,我们需要在查询条件对象中,定义好相应业务的查询参数,排序信息,请求记录数和每页大小信息等内容,根据这些查询信息,我们在后端的Asp.net Web API中实现对这些数据的按需获取,并排序返回给客户端使用。本篇随笔介绍利用查询条件对象,在Asp.net Web API中实现对业务数据的分页查询处理。

1、Web API控制器基类关系

为了更好的进行相关方法的封装处理,我们把一些常规的接口处理放在BaseApiController里面,而把基于业务表的操作接口放在BusinessController里面定义,如下所示。

在BaseApiController里面,我们使用了结果封装和异常处理的过滤器统一处理,以便简化代码,如下控制器类定义。

    /// <summary>
    ///所有接口基类/// </summary>
[ExceptionHandling]
[WrapResult]
public class BaseApiController : ApiController

其中ExceptionHandling 和WrapResult的过滤器处理,可以参考我的随笔《
利用过滤器Filter和特性Attribute实现对Web API返回结果的封装和统一异常处理
》进行详细了解。

而业务类的接口通用封装,则放在了BusinessController控制器里面,其中使用了泛型定义,包括实体类,业务操作类,分页条件类等内容作为约束参数,如下所示。

    /// <summary>
    ///本控制器基类专门为访问数据业务对象而设的基类/// </summary>
    /// <typeparam name="B">业务对象类型</typeparam>
    /// <typeparam name="T">实体类类型</typeparam>
[ApiAuthorize]public class BusinessController<B, T, TGetAllInput>: BaseApiControllerwhere B : class
        whereTGetAllInput : IPagedAndSortedResultRequestwhere T : BaseEntity, new()

2、分页处理接口

其中IPagedAndSortedResultRequest接口,是借鉴ABP框架中对于分页部分的处理,因此分页函数需要实现这个接口,这个接口包含了请求的数量,偏移量, 以及排序等属性定义的。

而BusinessController的分页查询处理函数GetAll定义如下所示。

        /// <summary>
        ///分页获取记录/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
[HttpGet]public virtual PagedResultDto<T>GetAll([FromUri] TGetAllInput input)
{
var condition =GetCondition(input);var list =GetPagedData(condition, input);returnlist;
}

其中 GetCondition 函数是给子类进行重写,以便处理不同的条件查询的。我们以UserController控制器为例进行说明。

    /// <summary>
    ///用户信息的业务控制器/// </summary>
    public class UserController : BusinessController<User, UserInfo, UserPagedDto>

其中传入的User是BLL业务层类,用来操作数据库;UserInfo是实体类,用来传递记录信息;UserPagedDto 则是分页查询条件类。

    /// <summary>
    ///用户信息的业务查询类/// </summary>
    public classUserPagedDto : PagedAndSortedInputDto, IPagedAndSortedResultRequest
{
/// <summary> ///默认构造函数/// </summary> public UserPagedDto() : base() { }/// <summary> ///参数化构造函数/// </summary> /// <param name="skipCount">跳过的数量</param> /// <param name="resultCount">最大结果集数量</param> public UserPagedDto(int skipCount, int resultCount) : base(skipCount, resultCount)
{
}
/// <summary> ///使用分页信息进行初始化SkipCount 和 MaxResultCount/// </summary> /// <param name="pagerInfo">分页信息</param> public UserPagedDto(PagerInfo pagerInfo) : base(pagerInfo)
{
}
#region Property Members /// <summary> ///所属角色ID/// </summary> public virtual int? Role_ID { get; set; }public virtual int? ID { get; set; }/// <summary> ///用户编码/// </summary> public virtual string HandNo { get; set; }/// <summary> ///用户名/登录名/// </summary> public virtual string Name { get; set; }/// <summary> ///用户密码/// </summary> public virtual string Password { get; set; }/// <summary> ///用户全名/// </summary> public virtual string FullName { get; set; }/// <summary> ///移动电话/// </summary> public virtual string MobilePhone { get; set; }/// <summary> ///邮件地址/// </summary> public virtual string Email { get; set; }/// <summary> ///默认部门ID/// </summary> public virtual string Dept_ID { get; set; }/// <summary> ///所属机构ID/// </summary> public virtual string Company_ID { get; set; }/// <summary> ///父ID/// </summary> public virtual int? PID { get; set; }/// <summary> ///用户呢称/// </summary> public virtual string Nickname { get; set; }/// <summary> ///是否过期/// </summary> public virtual bool? IsExpire { get; set; }/// <summary> ///过期日期/// </summary> public virtual DateTime? ExpireDateStart { get; set; }public virtual DateTime? ExpireDateEnd { get; set; }/// <summary> ///职务头衔/// </summary> public virtual string Title { get; set; }/// <summary> ///身份证号码/// </summary> public virtual string IdentityCard { get; set; }/// <summary> ///办公电话/// </summary> public virtual string OfficePhone { get; set; }/// <summary> ///家庭电话/// </summary> public virtual string HomePhone { get; set; }/// <summary> ///住址/// </summary> public virtual string Address { get; set; }/// <summary> ///办公地址/// </summary> public virtual string WorkAddr { get; set; }/// <summary> ///性别/// </summary> public virtual string Gender { get; set; }/// <summary> ///出生日期/// </summary> public virtual DateTime? BirthdayStart { get; set; }public virtual DateTime? BirthdayEnd { get; set; }/// <summary> ///QQ号码/// </summary> public virtual string QQ { get; set; }/// <summary> ///个性签名/// </summary> public virtual string Signature { get; set; }/// <summary> ///审核状态/// </summary> public virtual string AuditStatus { get; set; }/// <summary> ///备注/// </summary> public virtual string Note { get; set; }/// <summary> ///自定义字段/// </summary> public virtual string CustomField { get; set; }/// <summary> ///默认部门名称/// </summary> public virtual string DeptName { get; set; }/// <summary> ///所属机构名称/// </summary> public virtual string CompanyName { get; set; }/// <summary> ///排序码/// </summary> public virtual string SortCode { get; set; }/// <summary> ///创建人/// </summary> public virtual string Creator { get; set; }/// <summary> ///创建人ID/// </summary> public virtual string Creator_ID { get; set; }/// <summary> ///创建时间/// </summary> public virtual DateTime? CreateTimeStart { get; set; }public virtual DateTime? CreateTimeEnd { get; set; }/// <summary> ///编辑人/// </summary> public virtual string Editor { get; set; }/// <summary> ///编辑人ID/// </summary> public virtual string Editor_ID { get; set; }/// <summary> ///编辑时间/// </summary> public virtual DateTime? EditTimeStart { get; set; }public virtual DateTime? EditTimeEnd { get; set; }/// <summary> ///是否已删除/// </summary> public virtual bool? Deleted { get; set; }/// <summary> ///当前登录IP/// </summary> public virtual string CurrentLoginIP { get; set; }/// <summary> ///当前登录时间/// </summary> public virtual DateTime CurrentLoginTime { get; set; }/// <summary> ///当前Mac地址/// </summary> public virtual string CurrentMacAddress { get; set; }/// <summary> ///微信绑定的OpenId/// </summary> public virtual string OpenId { get; set; }/// <summary> ///微信多平台应用下的统一ID/// </summary> public virtual string UnionId { get; set; }/// <summary> ///公众号状态/// </summary> public virtual string Status { get; set; }/// <summary> ///公众号/// </summary> public virtual string SubscribeWechat { get; set; }/// <summary> ///科室权限/// </summary> public virtual string DeptPermission { get; set; }/// <summary> ///企业微信UserID/// </summary> public virtual string CorpUserId { get; set; }/// <summary> ///企业微信状态/// </summary> public virtual string CorpStatus { get; set; }#endregion}

它的基类属性包括了MaxResultCount,SkipCount,Sorting等分页排序所需的信息。

另外还包含了对条件查询的属性信息,如果是数值的,布尔类型的,则是可空类型,日期则有起始条件的范围属性等等,也可以根据自己需要定义更多属性用户过滤条件。

如对于出生日期,我们定义一个区间范围来进行查询。

        /// <summary>
        ///出生日期/// </summary>
        public virtual DateTime? BirthdayStart { get; set; }public virtual DateTime? BirthdayEnd { get; set; }

最后,我们根据需要进行判断,获得查询条件即可。

        /// <summary>
        ///获取查询条件并转换为SQL/// </summary>
        /// <param name="input">查询条件</param>
        protected override stringGetCondition(UserPagedDto input)
{
//根据条件,构建SQL条件语句 SearchCondition condition = newSearchCondition();if (!input.Role_ID.HasValue)
{
condition.AddCondition(
"ID", input.ID, SqlOperator.Equal)
.AddCondition(
"IdentityCard", input.IdentityCard, SqlOperator.Equal)
.AddCondition(
"Name", input.Name, SqlOperator.Like)
.AddCondition(
"Note", input.Note, SqlOperator.Like)
.AddCondition(
"Email", input.Email, SqlOperator.Like)
.AddCondition(
"MobilePhone", input.MobilePhone, SqlOperator.Like)
.AddCondition(
"Address", input.Address, SqlOperator.Like)
.AddCondition(
"HandNo", input.HandNo, SqlOperator.Like)
.AddCondition(
"HomePhone", input.HomePhone, SqlOperator.Like)
.AddCondition(
"Nickname", input.Nickname, SqlOperator.Like)
.AddCondition(
"OfficePhone", input.OfficePhone, SqlOperator.Like)
.AddCondition(
"OpenId", input.OpenId, SqlOperator.Like)
.AddCondition(
"Password", input.Password, SqlOperator.Like)
.AddCondition(
"PID", input.PID, SqlOperator.Like)
.AddCondition(
"QQ", input.QQ, SqlOperator.Equal)
.AddCondition(
"DeptPermission", input.DeptPermission, SqlOperator.Like)
.AddCondition(
"AuditStatus", input.AuditStatus, SqlOperator.Equal)
.AddCondition(
"FullName", input.FullName, SqlOperator.Like)
.AddCondition(
"Gender", input.Gender, SqlOperator.Equal)
.AddCondition(
"CustomField", input.CustomField, SqlOperator.Like)
.AddCondition(
"IsExpire", input.IsExpire, SqlOperator.Equal)
.AddCondition(
"Signature", input.Signature, SqlOperator.Like)
.AddCondition(
"SortCode", input.SortCode, SqlOperator.Like)
.AddCondition(
"Status", input.Status, SqlOperator.Equal)
.AddCondition(
"CorpStatus", input.CorpStatus, SqlOperator.Equal)
.AddCondition(
"CorpUserId", input.CorpUserId, SqlOperator.Equal)
.AddCondition(
"UnionId", input.UnionId, SqlOperator.Equal)
.AddCondition(
"WorkAddr", input.WorkAddr, SqlOperator.Equal)
.AddCondition(
"SubscribeWechat", input.SubscribeWechat, SqlOperator.Equal)
.AddCondition(
"Title", input.Title, SqlOperator.Like)
.AddCondition(
"CurrentLoginIP", input.CurrentLoginIP, SqlOperator.Like)
.AddCondition(
"CurrentMacAddress", input.CurrentMacAddress, SqlOperator.Like)

.AddCondition(
"Dept_ID", input.Dept_ID, SqlOperator.Equal)
.AddCondition(
"DeptName", input.DeptName, SqlOperator.Like)
.AddCondition(
"CompanyName", input.CompanyName, SqlOperator.Like)
.AddCondition(
"Company_ID", input.Company_ID, SqlOperator.Equal)
.AddCondition(
"Editor_ID", input.Editor_ID, SqlOperator.Equal)
.AddCondition(
"Editor", input.Editor, SqlOperator.Equal)
.AddCondition(
"Creator_ID", input.Creator_ID, SqlOperator.Equal)
.AddCondition(
"Creator", input.Creator, SqlOperator.Equal)

.AddDateCondition(
"CreateTime", input.CreateTimeStart, input.CreateTimeEnd)
.AddDateCondition(
"EditTime", input.EditTimeStart, input.EditTimeEnd)
.AddDateCondition(
"ExpireDate", input.ExpireDateStart, input.ExpireDateEnd)
.AddDateCondition(
"Birthday", input.BirthdayStart, input.BirthdayEnd);
}
return condition.BuildConditionSql().Replace("Where", "");
}

前面介绍到,我们BusinessController基类定义了常规的分页查询GetAll函数,如下所示。

        /// <summary>
        ///分页获取记录/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
[HttpGet]public virtual PagedResultDto<T>GetAll([FromUri] TGetAllInput input)
{
var condition =GetCondition(input);var list =GetPagedData(condition, input);returnlist;
}

其中 GetCondition 是由子类进行重写处理,生成具体的查询条件的。

由于这里的Sorting信息是一个字符串的排序信息,如 Name DESC或者Name ASC类似的信息,前者是字段名,后者是排序降序还是升序的标识,我们在业务里面,需要拆分一下进行组合条件,如下拆分。

            //分页查询条件
            string sortName = null; //排序字段
            bool isDesc = true;if (!string.IsNullOrEmpty(input.Sorting))
{
var sortInput = input asISortedResultRequest;if (sortInput != null)
{
if (!string.IsNullOrWhiteSpace(sortInput.Sorting))
{
List
<string> strNames = sortInput.Sorting.ToDelimitedList<string>(" ");
sortName
= (strNames.Count > 0) ? strNames[0] : null;
isDesc
= sortInput.Sorting.IndexOf("desc", StringComparison.OrdinalIgnoreCase) > 0;
}
}
}

这样我们或者SortName,以及是否降序的判断。

然后根据获得分页信息,并调用业务类的接口函数获取对应记录,构建为分页所需的JSON对象返回。

            //构建分页对象
            var pagerInfo = new PagerInfo() { CurrenetPageIndex = currentPage, PageSize =pageSize };if (!string.IsNullOrWhiteSpace(sortName))
{
list
=baseBLL.FindWithPager(condition, pagerInfo, sortName, isDesc);
}
else{
list
=baseBLL.FindWithPager(condition, pagerInfo);
}
if (list != null)
{
foreach (var item inlist)
{
ConvertDto(item);
//对Dto部分内容进行转义 }
}
//返回常用分页对象 var result = new PagedResultDto<T> { TotalCount = totalCount, Items =list };return result;

其中 PagedResultDto 是一个标准的分页数据返回的对象,定义如下所示。

[Serializable]public class PagedResultDto<T> : ListResultDto<T>, IPagedResult<T>{/// <summary>
        ///Total count of Items./// </summary>
        public int TotalCount { get; set; }
[Serializable]public class ListResultDto<T> : IListResult<T>{/// <summary>
        ///List of items./// </summary>
        public IReadOnlyList<T>Items
{
get { return _items ?? (_items = new List<T>()); }set { _items =value; }
}
private IReadOnlyList<T> _items;

最后返回的结果集合类似如下所示:

展开单条记录明细如下所示。

这个对象使用了Camel样式的属性处理,所以返回的属性全部是Camel的格式。

    /// <summary>
    ///统一处理Json的格式化信息/// </summary>
    public static classJsonFomatterHelper
{
/// <summary> ///获取JSON的格式化信息/// </summary> /// <returns></returns> public staticJsonMediaTypeFormatter GetFormatter()
{
var formatter =GlobalConfiguration.Configuration.Formatters.JsonFormatter;
formatter.SerializerSettings
= newJsonSerializerSettings
{
Formatting
=Formatting.Indented,
ContractResolver
= newCamelCasePropertyNamesContractResolver(),
DateFormatHandling
=DateFormatHandling.IsoDateFormat,
DateFormatString
= "yyyy-MM-dd HH:mm:ss",
};
returnformatter;
}
}

关于统一结果返回的封装处理,这里采用了WrapResultAttribute进行处理,详细可以参考我的随笔《
利用过滤器Filter和特性Attribute实现对Web API返回结果的封装和统一异常处理
》进行详细了解。

            //重新封装回传格式
            actionExecutedContext.Response = newHttpResponseMessage(statusCode)
{
Content
= new ObjectContent<AjaxResponse>(newAjaxResponse(content), JsonFomatterHelper.GetFormatter())
};

最近在改造原有Bootstrap开发框架,增加一个Vue&Element前端的时候,发现需要处理一个级联更新的过程,就是选择公司,然后更新部门,选择部门,或者人员列表,选择作为主管的一个实现,不同于Bootstrap使用Select2的插件,这里前端是Vue&Element,那么我们可以选择下拉列表的方式展现,在Element中可以考虑使用
Cascader 级联选择器
,也可以考虑使用封装
Tree 树形控件
,或者使用第三方组件
Vue-TreeSelect组件
。本篇随笔介绍使用Vue-TreeSelect组件实现公司-部门-人员级联下拉列表的处理过程。

1、Vue-TreeSelect组件的使用

在我早期随笔《
循序渐进VUE+Element 前端应用开发(8)--- 树列表组件的使用
》中也大概介绍了一下Vue-TreeSelect组件,这里我们再来回顾一下它的用法。

GitHub地址:
https://github.com/riophae/vue-treeselect

官网地址:
https://vue-treeselect.js.org/

NPM安装:

npm install --save @riophae/vue-treeselect

界面代码如下所示。

<template>
  <divid="app">
    <treeselectv-model="value":multiple="true":options="options" />
  </div>
</template>

这里的value就是选中的集合,options则是树列表的节点数据,和Element中的Tree组件一样,options的格式也包含id, lable, children这几个属性。

如果常规的数据提供,我们只要准备这些数据格式给options即可。

如下面的数据格式。

      treedata: [//初始化树列表
        { //默认数据
          label: '一级 1',
children: [{
label:
'二级 1-1'}]
}
]

不过我们一般数据是动态从后端接口中提取的,不是静态的,所以需要使用相应的方法来获取,并设置。

如果是后端接口无法满足特定的属性名称,那么Vue-TreeSelect组件也提供了一个 normalizer 属性方法用来重定义节点属性名称

类似下面的javascript代码

export default{
data: ()
=>({
value:
null,
options: [ {
key:
'a',
name:
'a',
subOptions: [ {
key:
'aa',
name:
'aa',
} ],
} ],
normalizer(node) {
return{
id: node.key,
label: node.name,
children: node.subOptions,
}
},
}),
}

通过normalizer 属性方法可以把数据源的属性映射到树列表中去。有时候我们对于空列表,可能还需要判断为空,并移除这个属性,代码如下所示。

normalizer (node) {if (node.children && !node.children.length) {
delete node.children
}
return{
id: node.key,
label: node.name,
children: node.children,
}
},

另外,有时候需要在列表值变化的时候,触发级联更新,那么就需要处理@input事件了。

  <treeselect
:options
="options":value="value":searchable="false"@input="updateValue" />

2、公司-部门-人员级联下拉列表的处理

综合上面的几个特点,我们公司-部门-人员级联下拉列表的处理就需要上面的知识点来处理。

在上面的弹出对话框中,选择所属公司,默认部门,所属经理的操作,级联处理过程效果如下所示。

界面代码如下所示

  <el-col:span="12">
    <el-form-itemlabel="所属公司"prop="company_ID">
      <treeselect:options="myGroupCompany"v-model="addForm.company_ID":searchable="false":default-expand-level="Infinity":open-on-click="true":open-on-focus="true"@input="updateGroupCompany"placeholder="所属公司" />
    </el-form-item>
  </el-col>
  <el-col:span="12">
    <el-form-itemlabel="默认部门"prop="dept_ID">
      <treeselect:options="myDeptTree"v-model="addForm.dept_ID":searchable="false":default-expand-level="Infinity":open-on-click="true":open-on-focus="true"@input="updateDeptUser":normalizer="normalizer"placeholder="所属部门" />
    </el-form-item>
  </el-col>
  <el-col:span="12">
    <el-form-itemlabel="所属经理"prop="pid">
      <treeselect:options="myDeptUser"v-model="addForm.pid":searchable="false":default-expand-level="Infinity":open-on-click="true":open-on-focus="true":normalizer="normalizer"placeholder="所属经理" />
    </el-form-item>
  </el-col>

如第一项公司列表,我们获取列表后设置options的对象即可。这里面需要定义几个变量 myGroupCompany、myDeptTree、myDeptUser的集合属性。

这里保留了normalizer 映射新属性的做法,不过由于属性名称默认和树控件的属性一致,也可以省略。

在其中更新处理,用到了 @input="updateGroupCompany" 、@input="updateDeptUser" 用于触发更新其他关联内容的事件。

另外一点,我们的新增或者编辑框中v-modal中关联的值,需要设置为null即可。

  addForm: {//新建表单
    id: '',
pid:
null,
dept_ID:
null,
company_ID:
null,
................
},

在显示弹出对话框,打开新增用户的时候,需要触发获取公司信息列表,如下所示。

showAdd () {this.resetForm('addForm')this.initData() //打开新增窗体的时候,初始化公司列表
      this.isAdd = true},

而其中initData的函数操作如下所示。

async initData () {var param ={}
await ou.GetMyGroupCompany(param).then(data
=>{
console.log(data.result)
var newTreedata =getJsonTree(data.result, {
id:
'id',
pid:
'pid',
children:
'children',
label:
'name'});this.myGroupCompany =newTreedata
})
},

这里调用ou的api进行获取公司信息的操作

import request from '@/utils/request'import BaseApi from'@/api/base-api'
//业务类自定义接口实现, 通用的接口已经在BaseApi中定义
class Api extends BaseApi {//获取集团公司列表。如果是超级管理员,返回集团+公司节点;如果是公司管理员,返回其公司节点
GetMyGroupCompany(data) {returnrequest({
url:
this.baseurl + 'GetMyGroupCompany',
method:
'get',
params: data
})
}
..........
}

而公司信息触发部门更新,我们用如下函数来处理变化。

async updateGroupCompany (value, instanceId) {//console.log(value + '~' + instanceId)

      this.addForm.dept_ID = null //置空控件内容
      if (!this.isEmpty(value)) {var param ={ parentId: value }
await user.GetDeptJsTreeJson(param).then(data
=>{this.myDeptTree =data.result
})
}
},

由于User的API中 GetDeptJsTreeJson返回的是符合树控件节点属性名称的,因此可以直接赋值给vue-TreeSelect的opition值。

<treeselect:options="myDeptTree"v-model="addForm.dept_ID":searchable="false":default-expand-level="Infinity":open-on-click="true":open-on-focus="true"@input="updateDeptUser":normalizer="normalizer"placeholder="所属部门" />

而部门选择后,则触发部门用户列表的更新,如下代码所示。

async updateDeptUser (value, instanceId) {//console.log(value + '~' + instanceId)
      this.addForm.pid = null //置空控件内容
      if (!this.isEmpty(value)) {var param ={ deptId: value }
await user.GetUserDictJson(param).then(data
=>{this.myDeptUser =data.result
})
}
},

同样,由于由于User的API中 GetUserDictJson 返回的是符合树控件节点属性名称的,因此可以直接赋值给vue-TreeSelect的opition值。

<treeselect:options="myDeptUser"v-model="addForm.pid":searchable="false":default-expand-level="Infinity":open-on-click="true":open-on-focus="true":normalizer="normalizer"placeholder="所属经理" />

3、特殊处理的内容

前面我们介绍了,如果获取内容和树控件的属性不一致,需要进行转义映射,如下代码所示。

normalizer (node) {if (node.children && !node.children.length) {deletenode.children
}
return{
id: node.id,
label: node.label,
children: node.children,
}
},

并在界面代码上指定normalizer处理。

:normalizer="normalizer"

有时候,我们返回的对象集合可能是一个二维列表内容,它本身有id,pid来标识它的层次关系,那么如果我们转换为嵌套列表的话,就可以使用getJsonTree 方法进行转换。

具体操作可以参考:
https://blog.csdn.net/unamattin/article/details/77152451

使用的时候,导入这个类方法即可。

import { getJsonTree } from '@/utils/json-tree.js' //转换二维表数据为树列表数据的辅助类

如果前面介绍的

async initData () {var param ={}
await ou.GetMyGroupCompany(param).then(data
=>{
console.log(data.result)
var newTreedata =getJsonTree(data.result, {
id:
'id',
pid:
'pid',
children:
'children',
label:
'name'});this.myGroupCompany =newTreedata
})
},

如果两个都是嵌套结构的树列表,但是属性名称不同,那么也可以通过map的操作方法,定义一个js函数进行转换即可,转换的代码如下所示。

    getTree () { //树列表数据获取
      var param ={}
user.GetMyDeptJsTreeJson(param).then(data
=>{//console.log(data) this.treedata = [];//树列表清空 var list =data.resultif(list) {this.treedata =list
}
//修改另一个Treedata const ass = (data) =>{
let item
=[];
data.map((list, i)
=>{
let newData
={};
newData.id
=list.id;
newData.label
=list.label;
newData.children
= list.children ? ass(list.children) : null; //如果还有子集,就再次调用自己 //如果列表为空,则移除children if (list.children && !list.children.length) {deletenewData.children;
}
item.push(newData);
});
returnitem;
}
this.selectTreeData =ass(list)
});
},

以上就是数据层次结构相同,属性名称不同的时候,进行转换处理的另外一种方式。

当然,我们定义返回列表数据的时候,如果需要用来绑定在树列表中的,也可以在后端WebAPI进行定义好符合格式的数据,避免在前端额外的代码转换。

        /// <summary>
        ///根据用户获取对应人员层次(给树控件显示的下拉列表)(值为ID)/// </summary>
        /// <param name="deptId">用户所在部门</param>
        /// <returns></returns>
        public List<TreeNodeItem> GetUserDictJson(intdeptId)
{
var itemList = new List<TreeNodeItem>();
itemList.Insert(
0, new TreeNodeItem("-1", ""));var list = BLLFactory<User>.Instance.FindByDept(deptId);foreach (var info inlist)
{
itemList.Add(
newTreeNodeItem(info.ID, info.FullName));
}
returnitemList;
}

其中 TreeNodeItem 类定义了Id, Label,Children的属性,这样前端就可以直接绑定使用了。

另外,在提一下,使用Vue-TreeSelect组件的时候,有时候需要封装它为自定义组件,那么normalizer也会作为prop属性作为配置的,这个时候,可以在自定义组件中定义好默认的normalizer。具体代码如下所示。

<template>
  <div>
    <divclass="flex-container">
      <divclass="flex-item">
        <treeselectref="tree"v-model="svalue":disabled="disabled":options="options":multiple="false":flat="false":default-expand-level="Infinity":open-on-click="true":open-on-focus="true"clearable :max-height="200":placeholder="placeholder":normalizer="normalizer" />
      </div>
      <divv-if="showcheck"class="flex-item">
        <el-checkboxv-model="isTop":label="checkboxLable"border @change="checkChange" />
      </div>
    </div>
  </div>
</template>

那么prop中的normalizer的定义如下所示。

使用这个自定义组件的时候,可以指定它的normalizer。

<MyTreeselectTopv-model="editForm.pid":options="selectTreeData":normalizer="normalizer" />

以上就是前后端树列表的绑定处理,以及使用Vue-TreeSelect组件实现公司-部门-人员级联下拉列表的功能操作,希望大家不吝赐教。

把Bootstrap框架界面改造为Vue+Element前端界面后,

页面列表效果如下所示。

编辑界面效果如下所示。

为了方便读者理解,我列出一下前面几篇随笔的连接,供参考:

循序渐进VUE+Element 前端应用开发(1)--- 开发环境的准备工作

循序渐进VUE+Element 前端应用开发(2)--- Vuex中的API、Store和View的使用

循序渐进VUE+Element 前端应用开发(3)--- 动态菜单和路由的关联处理

循序渐进VUE+Element 前端应用开发(4)--- 获取后端数据及产品信息页面的处理

循序渐进VUE+Element 前端应用开发(5)--- 表格列表页面的查询,列表展示和字段转义处理


循序渐进VUE+Element 前端应用开发(6)--- 常规Element 界面组件的使用

循序渐进VUE+Element 前端应用开发(7)--- 介绍一些常规的JS处理函数

循序渐进VUE+Element 前端应用开发(8)--- 树列表组件的使用


循序渐进VUE+Element 前端应用开发(9)--- 界面语言国际化的处理

循序渐进VUE+Element 前端应用开发(10)--- 基于vue-echarts处理各种图表展示

循序渐进VUE+Element 前端应用开发(11)--- 图标的维护和使用


循序渐进VUE+Element 前端应用开发(12)--- 整合ABP框架的前端登录处理


循序渐进VUE+Element 前端应用开发(13)--- 前端API接口的封装处理

循序渐进VUE+Element 前端应用开发(14)--- 根据ABP后端接口实现前端界面展示

循序渐进VUE+Element 前端应用开发(15)--- 用户管理模块的处理

循序渐进VUE+Element 前端应用开发(16)--- 组织机构和角色管理模块的处理

循序渐进VUE+Element 前端应用开发(17)--- 菜单管理

循序渐进VUE+Element 前端应用开发(18)--- 功能点管理及权限控制

VUE+Element 前端应用开发框架功能介绍

循序渐进VUE+Element 前端应用开发(19)--- 后端查询接口和Vue前端的整合

使用代码生成工具快速生成基于ABP框架的Vue+Element的前端界面

循序渐进VUE+Element 前端应用开发(20)--- 使用组件封装简化界面代码

循序渐进VUE+Element 前端应用开发(21)--- 省市区县联动处理的组件使用

循序渐进VUE+Element 前端应用开发(22)--- 简化main.js处理代码,抽取过滤器、全局界面函数、组件注册等处理逻辑到不同的文件中

循序渐进VUE+Element 前端应用开发(23)--- 基于ABP实现前后端的附件上传,图片或者附件展示管理

循序渐进VUE+Element 前端应用开发(24)--- 修改密码的前端界面和ABP后端设置处理

循序渐进VUE+Element 前端应用开发(25)--- 各种界面组件的使用(1)

循序渐进VUE+Element 前端应用开发(26)--- 各种界面组件的使用(2)

电商商品数据库的设计和功能界面的处理

循序渐进VUE+Element 前端应用开发(27)--- 数据表的动态表单设计和数据存储

循序渐进VUE+Element 前端应用开发(28)--- 附件内容的管理

循序渐进VUE+Element 前端应用开发(29)--- 高级查询条件的界面设计

部署基于.netcore5.0的ABP框架后台Api服务端,以及使用Nginx部署Vue+Element前端应用

循序渐进VUE+Element 前端应用开发(30)--- ABP后端和Vue+Element前端结合的分页排序处理

循序渐进VUE+Element 前端应用开发(31)--- 系统的日志管理,包括登录日志、接口访问日志、实体变化历史日志

循序渐进VUE+Element 前端应用开发(32)--- 手机短信动态码登陆处理

循序渐进VUE+Element 前端应用开发(33)--- 邮件参数配置和模板邮件发送处理

使用代码生成工具快速开发ABP框架项目

使用Vue-TreeSelect组件实现公司-部门-人员级联下拉列表的处理

使用Vue-TreeSelect组件的时候,用watch变量方式解决弹出编辑对话框界面无法触发更新的问题

在前篇随笔《
使用Vue-TreeSelect组件实现公司-部门-人员级联下拉列表的处理
》中介绍了Vue-TreeSelect组件的使用,包括使用v-modal绑定值,normalizer 来映射属性处理,还有一个@input时间处理值变更的关联操作。

在常规的新增界面处理过程中,弹出的对话框是已经构建完成的了,所有界面元素已经渲染,因此能够正常解决级联问题的处理。但在编辑界面中,确无法保证界面渲染完成,导致无法级联更新的问题。我们这里再进一步探讨更新的问题:在使用Vue-TreeSelect组件的时候,用watch变量方式解决弹出编辑对话框界面无法触发更新的问题。

1、问题描述

公司-部门-人员级联下拉列表的处理如下所示。

在编辑界面的时候,如果也是使用@input的事件来处理,则得不到有效的级联关系处理。

如下,我们编辑框绑定和处理更新的界面代码如下所示,这里和新增对话框一样,采用@input事件处理更新的操作。

    <el-col:span="12">
      <el-form-itemlabel="所属公司"prop="company_ID">
        <treeselect:options="myGroupCompany"v-model="editForm.company_ID":searchable="false":default-expand-level="Infinity":open-on-click="true":open-on-focus="true"@input="updateGroupCompany"placeholder="所属公司" />
      </el-form-item>
    </el-col>
    <el-col:span="12">
      <el-form-itemlabel="默认部门"prop="dept_ID">
        <treeselect:options="myDeptTree"v-model="editForm.dept_ID":searchable="false":default-expand-level="Infinity":open-on-click="true":open-on-focus="true"@input="updateDeptUser":normalizer="normalizer"placeholder="所属部门" />
      </el-form-item>
    </el-col>
    <el-col:span="12">
      <el-form-itemlabel="所属经理"prop="pid">
        <treeselect:options="myDeptUser"v-model="editForm.pid":searchable="false":default-expand-level="Infinity":open-on-click="true":open-on-focus="true":normalizer="normalizer"placeholder="所属经理" />
      </el-form-item>
    </el-col>

显示编辑对话框界面的代码也和新增操作类似

    async showEdit (id) { //显示编辑对话框处理
      if (!id || typeof (id) === 'undefined') {this.msgWarning('请选择编辑的记录!');return;
}
this.resetForm('editForm')
await
this.initData()var param ={ id: id }
await user.Get(param).then(data
=>{
Object.assign(
this.editForm, data.result)this.isEdit = true //编辑状态 })//获取列表数据 this.getFunctionsByUser(id)
},

不过打开已有记录的时候,第一次是无法进行级联显示正确的内容的,后面如果变化公司,则可以看到正常级联关系。猜测应该是在渲染顺序的问题,导致无法触发更新。

可以看到选框中的unknown字样,表明没有触发级联关系,没有正确获取到列表数据源。

2、用watch变量方式解决问题

既然无法通过上面的方式处理,我们来变通下,默认也是初始化所属公司列表的数据源,但不在监听它的@input事件,而是通过watch变量的方式,监控editForm里面对应的属性变化,然后在触发更新关联的内容。

我们取消@input事件,模板代码变化如下所示

                <el-col:span="12">
                  <el-form-itemlabel="所属公司"prop="company_ID">
                    <treeselect:options="myGroupCompany"v-model="editForm.company_ID":searchable="false":default-expand-level="Infinity":open-on-click="true":open-on-focus="true"placeholder="所属公司" />
                  </el-form-item>
                </el-col>
                <el-col:span="12">
                  <el-form-itemlabel="默认部门"prop="dept_ID">
                    <treeselect:options="myDeptTree"v-model="editForm.dept_ID":searchable="false":default-expand-level="Infinity":open-on-click="true":open-on-focus="true":normalizer="normalizer"placeholder="所属部门" />
                  </el-form-item>
                </el-col>

然后监听编辑表单的两个属性变化,如下代码所示。

  watch: { //对过滤内容进行监控,实现树列表过滤
    'editForm.company_ID': function(val, oldval) {if(val) {this.updateGroupCompany(val)
}
},
'editForm.dept_ID': function(val, oldval) {if(val) {this.updateDeptUser(val)
}
}
},

这样在编辑框中公司ID变化的时候,触发部门列表的更新;部门ID变化的时候,触发用户列表的更新即可。

再来看看,第一次打开用户信息,可以看到正常的进行展示了。

这样的watch监控变量的变化,还可以在同步处理很多操作,如转换界面组件的值的时候,也可以处理

  watch: { //对过滤内容进行监控,实现树列表过滤
    //键路径必须加上引号
    'addForm.tags_array': function(val, oldval) {if(val) {this.addForm.tags =val.toString()
}
},
'editForm.tags_array': function(val, oldval) {if(val) {this.editForm.tags =val.toString()
}
}
},

又或者在自定义组件的时候,监控某些内容变化,触发界面更新的处理。

watch: {
ouid (value) {
//属性变化触发更新 this.ouId =valuethis.getlist()
},
showaction (value) {
//属性变化触发更新 this.showAction =valuethis.getlist()
}
},

为了方便读者理解,我列出一下前面几篇随笔的连接,供参考:

循序渐进VUE+Element 前端应用开发(1)--- 开发环境的准备工作

循序渐进VUE+Element 前端应用开发(2)--- Vuex中的API、Store和View的使用

循序渐进VUE+Element 前端应用开发(3)--- 动态菜单和路由的关联处理

循序渐进VUE+Element 前端应用开发(4)--- 获取后端数据及产品信息页面的处理

循序渐进VUE+Element 前端应用开发(5)--- 表格列表页面的查询,列表展示和字段转义处理


循序渐进VUE+Element 前端应用开发(6)--- 常规Element 界面组件的使用

循序渐进VUE+Element 前端应用开发(7)--- 介绍一些常规的JS处理函数

循序渐进VUE+Element 前端应用开发(8)--- 树列表组件的使用


循序渐进VUE+Element 前端应用开发(9)--- 界面语言国际化的处理

循序渐进VUE+Element 前端应用开发(10)--- 基于vue-echarts处理各种图表展示

循序渐进VUE+Element 前端应用开发(11)--- 图标的维护和使用


循序渐进VUE+Element 前端应用开发(12)--- 整合ABP框架的前端登录处理


循序渐进VUE+Element 前端应用开发(13)--- 前端API接口的封装处理

循序渐进VUE+Element 前端应用开发(14)--- 根据ABP后端接口实现前端界面展示

循序渐进VUE+Element 前端应用开发(15)--- 用户管理模块的处理

循序渐进VUE+Element 前端应用开发(16)--- 组织机构和角色管理模块的处理

循序渐进VUE+Element 前端应用开发(17)--- 菜单管理

循序渐进VUE+Element 前端应用开发(18)--- 功能点管理及权限控制

VUE+Element 前端应用开发框架功能介绍

循序渐进VUE+Element 前端应用开发(19)--- 后端查询接口和Vue前端的整合

使用代码生成工具快速生成基于ABP框架的Vue+Element的前端界面

循序渐进VUE+Element 前端应用开发(20)--- 使用组件封装简化界面代码

循序渐进VUE+Element 前端应用开发(21)--- 省市区县联动处理的组件使用

循序渐进VUE+Element 前端应用开发(22)--- 简化main.js处理代码,抽取过滤器、全局界面函数、组件注册等处理逻辑到不同的文件中

循序渐进VUE+Element 前端应用开发(23)--- 基于ABP实现前后端的附件上传,图片或者附件展示管理

循序渐进VUE+Element 前端应用开发(24)--- 修改密码的前端界面和ABP后端设置处理

循序渐进VUE+Element 前端应用开发(25)--- 各种界面组件的使用(1)

循序渐进VUE+Element 前端应用开发(26)--- 各种界面组件的使用(2)

电商商品数据库的设计和功能界面的处理

循序渐进VUE+Element 前端应用开发(27)--- 数据表的动态表单设计和数据存储

循序渐进VUE+Element 前端应用开发(28)--- 附件内容的管理

循序渐进VUE+Element 前端应用开发(29)--- 高级查询条件的界面设计

部署基于.netcore5.0的ABP框架后台Api服务端,以及使用Nginx部署Vue+Element前端应用

循序渐进VUE+Element 前端应用开发(30)--- ABP后端和Vue+Element前端结合的分页排序处理

循序渐进VUE+Element 前端应用开发(31)--- 系统的日志管理,包括登录日志、接口访问日志、实体变化历史日志

循序渐进VUE+Element 前端应用开发(32)--- 手机短信动态码登陆处理

循序渐进VUE+Element 前端应用开发(33)--- 邮件参数配置和模板邮件发送处理

使用代码生成工具快速开发ABP框架项目

使用Vue-TreeSelect组件实现公司-部门-人员级联下拉列表的处理

使用Vue-TreeSelect组件的时候,用watch变量方式解决弹出编辑对话框界面无法触发更新的问题