2023年2月

DevExpress是一家全球知名的控件开发公司, DevExpress 也特指此公司出品的控件集合或某系列控件或其中某控件。我们应用最为广泛的是基于Winform的DevExpress控件组,本篇随笔主要总结笔者在Winform开发框架中应用到的各种基于DevExpress的Winform界面效果。

1、SplashScreen控件

我们在开发桌面应用程序的时候,由于程序启动比较慢或者展示公司产品Logo,往往为了提高用户的体验,增加一个闪屏,也就是SplashScreen,好处有:1、让用户看到加载的过程,提高程序的交互响应;2.可以简短展示或者介绍程序的功能或者展示Logo,给客户较深的印象。

在DevExpress程序中使用SplashScreenManager控件实现启动闪屏和等待信息窗口,这个SplashScreenManager很好的封装了日常WInform程序中用到的启动闪屏窗口和在耗时操作中进行等待提示的信息窗口,合理使用可以增加程序界面的友好交换。

2、GridControl控件的封装分页控件

在Winform开发中,一直离不开分页处理,好的分页控件封装,能为开发节省很多时间和繁琐工作,对分页控件一直的改进和完善,也是我的兴趣之一。

可以通过菜单设置显示的列内容。

3、基于Ribbon样式的界面

为了更多放置一些开发功能,我们可以使用树列表的方式,也可以使用Ribbon的方式,更加紧凑的展示功能按钮。

或者参考框架备件仓库管理系统的界面

应该会比放在树列表中展示的比较好一点

有时候,我们需要分类展示不同的菜单功能,而且功能比较多的时候,可以结合Ribbon工具栏和树列表的方式展示功能点。

4、使用布局控件约束内容展示

在录入数据的界面的设计中, 我一般倾向于使用LayoutControl控件实现布局功能,这个控件对我们排版各种控件非常方便。

我们在展示详细界面的时候,一般为了整洁美观,一般使用LayoutControl布局控件来约束内容的展示。

或者如下详细界面所示

下面是我在其中的一个界面中使用该控件调整后得到的一个多样化一点的界面表达方式,有点类似于分组分类的方式,使得输入的内容更加易读。

这个调整好的界面布局的设计模式下的效果如下所示。

5、SearchLookUpEdit控件的使用

可以在GridControl中整合SearchLookupEdit控件,以便快速选择录入数据

6、内容比较多则封装控件实现多页面处理

我们在做Winform项目开发的时候,经常会发现有一些数据很多,需要通过不同的Tab页面分类来实现数据的录入和现实,例如体检数据,可能包含外科、内科、眼科、耳鼻喉科、口腔科、以及其他的检查等等内容,如果一次性放在一个窗口中现实,不太合理也不好看,如果通过多个Tab分类进行管理,则用户体验好很多。

如上分析,我们把每个模块独立出来做一个控件,如眼科的作为一个独立的用户控件进行展现,如下所示。

或者类似下面客户关系管理系统中的内容关系

7、GridControl样式控制

通过控制GridControl的列表展示样式,可以着重强调某个字段的信息,也可以进行内容的转义处理。

GridView表头多行显示(折行),表头及行内容居中操作

8、RichEditControl控件的封装使用

传统.NET界面有一个RichTextBox控件,这个是一个富文本控件,可以存储图片文字等内容,它有自己的文件格式RTF,在DevExpress控件组里面也有一个同等的控件,他的名字是RichEditControl,这个控件功能很强大。基于RichEditControl控件我们可以增加自定义按钮,实现定制工具条或者自定义的处理功能。

9、图表控件

有时候,基于对一些年份、月份的统计,需要集成多个数值指标进行分析,因此就需要把多种数据放到一个图形里面展现,也成为多重坐标轴,多重坐标轴可以是多个X轴,也可以是Y轴,它们的处理方式类似。

或者饼图展示

10、附件管理的自定义组件

在很多病人资料管理模块中,都需要管理影像学资料,这个在我的Winfrom开发框架中,提供了一个附件管理的通用模块供重复使用,因此对这些操作并不会增加太多的工作量,统一使用即可,界面效果如下所示。

附件管理模块提供了分类的附件管理,图片缩略图预览和图片预览,多文件上传、下载、删除等附件操作,满足我们对附件管理的大多数要求。

11、 树列表展示

我们为了直观显示的需要,一般把菜单用树列表控件进行展示,其中就会用到我们说的TreeList控件,如下界面所示。

其中TreeList和一个输入SearchControl来一起协同使用,可以提高界面的友好性,我们可以通过输入关键字进行节点的过滤显示。

如输入过滤内容后查询过滤树列表节点,如下所示,这样可以给用户快速模糊检索指定的树节点。

下面也是树列表的展示案例界面

或者如下树形列表界面所示

整体结合多文档展示内容,界面效果如下所示。

12、 GridControl数据导入导出处理

GridControl内容简单的导出很容易,只需要把相关的数据表,利用Aspose.cell导出到指定的文件即可。简单二维表导出Excel数据的效果如下所示。

由于工作的需要,在我的
Winform开发框架
中引入了一个通用的数据导入模块,来实现这个既是统一,又是变化的业务需求,首先我们来看看能大致的模块功能介绍图,如下所示。

然后我们再来看看实际的导入模块操作界面,如下图所示

在最底的状态栏里面,但我们保存数据的时候,会调用后台线程进行数据保存,并显示数据导入的进度状态,由于是采用后台线程处理,不会阻塞当前的界面,在多文档的
Winform开发框架
界面中,可以切换到其他业务界面进行其他处理,不影响整体界面操作。

下面我们就来介绍内容比较多的时候,分选项卡展示一个记录内容的操作,如图所示。

导入的数据,是一个Excel,它要求包含几个不同表的数据,导入操作一次性完成数据的导入,Excel文件的格式如下所示。

导出操作,我们根据用户的选择,可以一次性导出多个Excel文件,每个Excel文件包含客户的基础信息,也包含相关数据,它们的格式和导入的格式保持一致即可,这样方便数据的交换处理。

导出操作,我们需要把客户的选择信息转换为需要导出的对象列表数据,然后绑定到Excel里面即可,因此我们的Excel里面,可以通过自定义模板,指定列的数据属性就可以绑定好数据了。

13、SplitContainerControl控件的分拆界面

左右分拆的界面布局效果如下所示。

或者主从表的界面效果

14、利用DevExpress的控件实现对PDF、Word、Excel文档的预览和操作处理

为了演示这些控件的处理,我单独编写了一个例子,用来实现对PDF、Word、Excel等文档的处理。

为了显示PDF文档,我们需要在界面里面添加一个XtraPdfViewer.PdfViewer的控件,这个主要是用来显示PDF的,它有很多属性方法,用来实现对PDF的处理操作

其中RichEditControl能够较好显示Word文档, 加载文档后,界面显示内容如下所示:

文档控件很容易支持打印预览功能,打印预览的界面如下所示

对于Excel文档的预览和操作,DevExpress控件在最近版本中增加的XtraSpreadsheet.SpreadsheetControl控件就可以实现Excel的显示和处理操作,这个控件很强大,可以处理很复杂格式的Excel文档,虽然我原来使用了另外一个FarPoint Spread控件组,不过这个XtraSpreadsheet控件组,如果集成在DevExpress也就很方便了。

这个DevExpress的控件,可以在其中进行Excel的新建、保存、打印预览等操作,当然也可以打开我们已有的Excel文件了。

打开文件后,界面效果如下所示。

预览也很方便,和Word的预览操作类似。

如我在我的《Winform开发框架》、《混合式开发框架里面》使用的通用附件管理模块,就是基于这些特点,实现图片、Excel文档、Word文档和PDF等文档的在线预览和管理操作,界面截图如下所示。

15、WizardControl向导控件

在一些界面操作里面,我们可能把它拆分为几部进行处理,这个时候引入WizardControl向导控件应该是比较不错的选择了。多步的处理方式,可以让用户避免一次性输入太多内容的烦躁心情,也可以针对性的校验部分内容。

我们一般在DevExpress的VS工具栏里面选择导航布局选项卡,就可以找到对应的WizardControl向导控件了。

最终我们实现的效果如下所示。

16、日程控件XtraScheduler

在一些应用场景中,我们可能需要记录某一天,某个时段的日程安排,那么这个时候就需要引入了DevExpress的日程控件XtraScheduler了,这个控件功能非常强大,提供了很好的界面展现方式,以及很多的事件、属性给我们定制修改,能很好满足我们的日程计划安排的需求。

整个日程控件,可以分为日视图、周视图、月视图等等,当然还有一些不常用的时间线、甘特图等,本例我们来关注控件的使用以及这几个视图的处理。先来看看他们的界面效果,如下所示。

日视图:

在视图里面,默认可以打开响应的日程事件进行编辑的。

周视图:

月视图:

由于控件,默认也是提供右键菜单,对几种控件视图进行切换的,如下菜单所示。

17、背景加按钮排版,实现更加场景化的功能指引

在一些场景里面,如一些进销存的系统里面,我们往往把一些相关的模块处理放在一起,如进货、退货、库存调入、调出、产品、库存、盘点等这些信息,就是一个场景里面的内容,有时候可以把它们放在一起,但是如果单纯的放几个按钮,可能会显得比较单调一些,我们为了丰富界面效果,把界面设计效果如下所示。

这个界面虽说不是特别华丽,但是也是比仅仅放几个标准的按钮来的美观一些,而且按钮之间也设置了一些箭头来指示他们的流程或者从属关系,因此在一些场合,我们更倾向使用这种直观、清晰的界面表达方式,同时也增加了一定的美感。

利用图元的绘图工具如EDraw Max等等这些来绘制背景,预先留出放置图标按钮的位置,我们绘制一个背景如下所示。

绘制好这些背景,截图出来待用,记住要图片要预留一定的位置,并且具有一定的画布大小,我们想图片按正常方式设为背景即可,这样我们在窗口放大的时候,不至于直接看完图片了,截取出来的图片,左上角预留一定的位置即可。

在我们预先新建的窗体上面,设置它的BackgroundImage背景图片,如下图所示。

18、Winform开发框架介绍

Winform开发框架详细了解可以访问:
http://www.iqidi.com/Framework/info.html

ABP框架的数据访问底层是基于EFCore(Entity Framework Core)的,是微软标志性且成熟的ORM,因此它本身是支持多种主流数据库MySQL,SqlServer,Oracle,SQLite等等的,我在上篇随笔《
ABP框架使用Mysql数据库,以及基于SQLServer创建Mysql数据库的架构和数据
》已经详细介绍过如何从SQLServer迁移支持Mysql数据库的操作,同时介绍如何从SQLServer基础数据,通过Navicat工具,实现数据库迁移到Mysql上去。本篇随笔继续介绍ABP框架实现Oracle的适配和数据库的迁移处理。

1、ABP框架中Oracle数据库的适配处理

ABP框架底层是使用EFCore来实现数据处理的,框架默认是使用SQLServer数据库的,如果需要切换到Oracle数据库上去,使用EF Core操作Oracle数据库,首先需要安装Oracle.EntityFrameworkCore,可以直接在NuGet上直接搜索安装即可。

首先在ABP框架的EntityFrameworkCore项目右键上选择【管理NuGet程序包】,然后 搜索Oracle,选择Oracle.ManagedDataAccess.Core和Oracle.EntityFrameworkCore即可,如下所示。

Oracle.ManagedDataAccess.Core是基于ODP的.netcore的驱动程序,是我们访问Oracle的时候,摆脱X86,X64的繁琐限制。

这个Oracle的ODP.NET方式,之前在微软企业库的相关框架就已经用过,非常不错,有兴趣可以参考随笔《
在开发框架中扩展微软企业库,支持使用ODP.NET(Oracle.ManagedDataAccess.dll)访问Oracle数据库
》了解一下。

它的连接字符串类似下面的格式

<addname="oracle3"providerName="OracleManaged"connectionString="Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orcl)));User ID=win;Password=win" />

如果是我们目前ABP框架,这是采用appSetting.json的方式了,格式如下所示。

  "ConnectionStrings": {"Default": "Server=.\\SQL2014; Database=MyProjectDb; Trusted_Connection=True;","Oracle": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orcl)));User ID=C##ABP;Password=abp","MySql": "Server=localhost;Database=myprojectdb;Uid=root;Pwd=123456;","PostgreSQL": "Server=localhost;Port=5432;Database=myprojectdb;User Id=postgres;Password=123456"},

安装了Oracle的两个驱动文件,我们就需要调整一下对应的代码了,参考随笔《
ABP框架使用Mysql数据库,以及基于SQLServer创建Mysql数据库的架构和数据
》的处理,我们调整非常方便。

支持,我们感觉好像Oracle的应该调整的差不多了,尝试把项目编译运行,会发现有错误出现:
ORA-00942: 表或视图不存在

这个问题开始挺困惑的,而且ABP框架切换到Oracle的相关文章介绍也很少,网上很多做法后来回想起来,他们处理都是不太正确的,至少对于当前EFCore的处理来说,不是正确的。

要了解这个问题:
ORA-00942: 表或视图不存在
,我们需要来看看EFCore底层对表名和字段的限定符处理逻辑了,我们查看host启动项目的错误日志,得到如下提示。

ERROR 2021-08-15 16:12:46,603 [1    ] oft.EntityFrameworkCore.Database.Command - Failed executing DbCommand (114ms) [Parameters=[], CommandType='Text', CommandTimeout='0']
SELECT "a"."Id", "a"."CreationTime", "a"."CreatorUserId", "a"."DeleterUserId", "a"."DeletionTime", "a"."Discriminator", "a"."DisplayName", "a"."IsDeleted", "a"."LastModificationTime", "a"."LastModifierUserId", "a"."Name", "a"."AnnualPrice", "a"."DailyPrice", "a"."ExpiringEditionId", "a"."MonthlyPrice", "a"."TrialDayCount", "a"."WaitingDayAfterExpire", "a"."WeeklyPrice"
FROM "AbpEditions" "a"
WHERE "a"."Name" = N'Standard'
FETCH FIRST 1 ROWS ONLY

我们可以看到生成的访问SQL,它使用了双引号作为限定符。

生成的Sql中是将所有的表名和字段名都用双引号修饰的,而Oracle中存储字段名默认都是采用全大写的形式的,它们是不对应的,使用双引号的对象名称,是大小写敏感的,如下在PLSQL中执行语句。

而不使用限定符的字符串对象,默认是按大写方式来处理的,如下结果所示。

所以如果我们不想将实体类所有的属性名称写成大写,就需要显式的指定映射的column的名称。可以使用DataAnnotations的特性标注所有属性,或者在Context中指定列映射。

显示指定每个访问类的处理方式如下所示。

[Table("EMPLOYEE")]  //指定数据库对应表名
public classEmployee
{
[Key]
//主键 [Column("ID")] //指定数据库对应表主键名称 public long Id { get; set; }

[Column(
"EMPLOYEENO")]public int EmployeeNo { get; set; }

[Column(
"NAME")]public string Name { get; set; }

[Column(
"BIRTHDAY")]public DateTime BirthDay { get; set; }

[Column(
"DEPARTMENT")]public string Department { get; set; }

[Column(
"ISVALID")]public bool IsValid { get; set; }
}

这种方式肯定不是很好的方法,这种方式处理起来很累赘,而且ABP框架很多基类我们是通过官方DLL来引用的,没有或者很麻烦去修改相关的映射关系。

那么我们考虑动态公共映射的方式处理,尽量避免这种劳而无功的方式。

我们知道ABP框架中的EntityFrameworkCore项目里面,我们可以通过代码的方式修改它们和表、字段的映射关系,我们的规则就是让它(表名、字段名)变为大写,这样即使它们使用了限定符,也不改变SQL的处理结果。

我们修改EF模型OnModelCreating的处理逻辑,如下代码所示,红框部分就是我们改变问题所在的核心。

运行Host启动项目,我们发现后端的Swagger能够正常打开,成功了第一步了。

2、使用Oracle的序列和触发器解决自增ID的问题

不过登陆进入的时候,会发现出现
违反唯一约束条件
的错误,这个是由于登录的时候,无法写入日志,因为无法让这个表的自增ID为NULL,违反了数据约束规则。

到这里,我们Oracle方面基本上就解决了大问题了,不过就是还有一个自增长的问题。

INSERT INTO "C##ABP"."ABPUSERTOKENS" ("EXPIREDATE", "LOGINPROVIDER", "NAME", "TENANTID", "USERID", "VALUE")
VALUES (:p0, :p1, :p2, :p3, :p4, :p5)
RETURNING "ID" INTO "lABPUSERTOKENS_0"(1)."ID";
OPEN :cur1 FOR SELECT "lABPUSERTOKENS_0"(1)."ID" FROM DUAL;
END;
ERROR 2021-08-14 17:27:18,636 [52   ] osoft.EntityFrameworkCore.Infrastructure - 2021-08-14 17:27:18.636902 ThreadID:52  (ERROR)   OracleExecutionStrategy.ExecuteAsync() :  Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.
 ---> Oracle.ManagedDataAccess.Client.OracleException (0x80004005): ORA-00001: 违反唯一约束条件 (C##ABP.PK_ABPUSERTOKENS)
ORA-06512: 在 line 13

Oracle插入数据倒是和其他数据库差别不大,但是由于Oracle天生是没有自增Id的,如果我们设计的表的主键需要采用自增键就需要使用Sequence序列代替,通过序列和触发器的方式结合,我们可以顺利解决自增长的问题。

我们一般通过序列和触发器的结合,自动为自增长ID写入对应的起步值,定义Oracle序列和触发器的代码如下所示。

--表 WEB_SITEARTICLE 的自增序列和触发器
CREATESEQUENCE WEB_SITEARTICLE_ID_AUTO
MINVALUE
1 --定义序列的初始值(即产生的第一个值),默认为1。 MAXVALUE 99999999999STARTWITH 70 --因为表中已有3条数据,所以要从第4开始,如果尚未插入数据,那写1即可。 INCREMENT BY 1 --用于定义序列的步长,如果省略,则默认为1,如果出现负值,则代表序列的值是按照此步长递减的。 NOCYCLE --CYCLE代表循环,NOCYCLE代表不循环。 NOCACHE; --CACHE建立缓冲区,NOCACHE不建缓冲区 CREATE OR REPLACE TRIGGERWEB_SITEARTICLE_ID_TRIGGER
BEFORE
INSERT ON WEB_SITEARTICLE --before:表示在数据库动作之前触发器执行; after:表示在数据库动作之后触发器执行。 FOR EACH ROW --对表的每一行触发器执行一次。如果不写,则只对整个表执行一次。 BEGIN SELECT WEB_SITEARTICLE_ID_AUTO.NEXTVAL INTO :NEW.ID FROM DUAL; --:new 为一个引用最新的列值;:old 为一个引用以前的列值; 这两个变量只有在使用了关键字 "FOR EACH ROW"时才存在.且update语句两个都有,而insert只有:new ,delect 只有:old; END WEB_SITEARTICLE_ID_TRIGGER;

下面就是我们实际的文件中,为ABP框架的Oracle表创建序列和触发器的SQL代码截图。

执行后,我们就可以通过PLSQL查看到对应的序列和触发器列表,并可以进行适当的调整了。

序列如下:

触发器列表如下所示。

弄完这些,如果我们的Oracle表和数据都准备好了,那么就没有什么问题了,已经可以正常使用ABP框架在Oracle上跑了。

由于我们之前已经通过Navicat的方式迁移传输了SQLserver的数据,我们可以顺利跑起来ABP框架了,整套框架包括了WebAPI后端的Swagger管理、Vue&Element的管理前端、Winform管理前端,以及公司门户网站几个部分。

ABP 框架Swagger接口端

ABP框架之Vue&Element端

ABP框架之Winform端

ABP框架之公司门户网站

3、SQLServer结构和数据迁移到Oracle

我在上篇随笔《
ABP框架使用Mysql数据库,以及基于SQLServer创建Mysql数据库的架构和数据
》也详细介绍过使用Navicat工具实现SQLserver 数据库迁移到Mysql的处理方式,这里依旧使用这个方式,实现SQLserver 数据库迁移到Oracle。

首先我们需要创建对应的Oracle用户和表空间,用来承载表的结构和数据的存储。如果我们是基于Oracle的DMP导出文件,那么可以不用提前创建用户和表控件也行。

注意,由于ABP框架使用的数据库表和键名称等的标识超过了30个字符,而Oracle12c以下版本不支持超过30个字符的标识,因此本ABP项目以Oracle12c起步的数据库,用于解决这个标识问题。

由于Oracle12C的创建全局用户,需要以C##开始,因此创建用户的代码和Oracle11的有所差异。

下面是基于Oracle12C的创建用户和表空间SQL代码。用户名为C##ABP,密码为abp

create tablespace abp_tbs datafile 'C:\app\Administrator\virtual\oradata\orcl\whcdb\abp_tbs.dbf'size 100M;--DROP TABLESPACE abp_tbs INCLUDING CONTENTS AND DATAFILES CASCADE CONSTRAINTS;
create user C##ABP identified by abp defaulttablespace abp_tbs;grant connect,resource toC##ABP;grant dba toC##ABP;--Revoke dba from C##ABP;

弄完这些准备工作,就到Navicat工具出场了。

通过数据传输的方式,我们构建Oracle数据的处理,分别设置源和目标的配置。

并注意通过选项的方式,设置好传输的规则。

其中我们需要设置让标识转换为大写,这个符合Oracle的处理规则,否则用了双引号的标识符,就不正确了。

另外遇到错误继续,是因为Oracle 的迁移,有些记录无法顺利处理,我们可以通过手工的方式补齐它,特别是NCLOB的内容,有些限制。

选定好相关的表内容,就可以继续一步步完成即可创建和导出表数据了。

最后我们通过PLSQL查看相关的表内容如下所示。

到这里,切换Oracle的后端代码,以及数据库的数据,包括创建对应自增记录字段ID的序列和触发器的处理也完成了,这样就可以顺利完成整个Oracle的迁移和处理了。

在我们很多前端业务开发中,往往为了方便,都需要自定义一些用户组件,一个是减少单一页面的代码,提高维护效率;二个也是方便重用。本篇随笔介绍在任务管理操作中,使用自定义Vue&Element组件,实现系统用户选择和显示。

1、系统用户的选择需求

在我们一些业务系统中,可能需要选中系统用户进行一些业务处理,如本篇介绍的任务系统中,如在新增或者编辑界面中,需要选择任务的执行人、参与人等人员操作。

而在查看详细数据的时候,可能需要展示相关的人员名称,如下界面所示。

前者需要弹出界面中选择用户,可以设置多选、单选、过滤用户数据的操作;后者则需要根据用户的ID显示正常的名称。

一般来说,我们用户隶属于某个部门、角色、或者岗位的,我们可以根据这些条件进行开始展示,同时也可以输入一定的条件进行查询定位用户。

选择用户的弹出界面如下所示,其中可以根据部门分类、岗位分类进行快速的查询,同时也可以根据用户名进行查询。

我们可以通过定义用户组件,允许开启/禁用限制用户多选的操作,如果选择多个,则自动添加在列表中,如下界面所示。

确认后,界面的人员进行更新显示即可。

以上就是大概的用户组件的界面效果和逻辑处理。

2、实现用户选择和显示的组件开发

我们先定义一个用户组件,命名为selectPostUser.vue其中HTML模板代码的结构如下所示。

主要就是一个标签显示、按钮、和一个弹出对话框。

在编辑状态下,显示按钮,并可以触发弹出对话框的展示,对话框主要就是显示系统系统的内容。

而这个组件,我们定义了几个prop属性,用来配置显示不同的界面效果,是否可以选择,是否可以移除标签、是否可以多选人员等设置。

export default{
props: {
value: {
//接受外部v-model传入的值 type: [String],default: ''},
closable: {
//是否可以关闭标签 type: Boolean,default: () => { return true}
},
selectable: {
//是否可选人员 type: Boolean,default: () => { return true}
},
multiple: {
//是否多选,默认可以多选 type: Boolean,default: true}
},

我们定义了几个data变量,以及对几个变量进行watch监控,以便控制它的值的更新和返回,如下代码所示。

这里注意,如果我们需要使用.sync的方式来更新属性的值,那么需要使用update:value的格式进行处理,如下所示。

则代码更新这个visible则如下

this.$emit('update:isVisible', val)

而如果我们这个组件通过v-modal绑定的值,如下界面代码所示

<select-post-userv-if="isEdit"ref="editForm.participant"v-model="editForm.participant" />

如果更新返回,则使用下面方式

this.$emit('input', val)

使用自定义的用户组件,需要在父页面中引入相关的对象组件,如下代码所示。

import selectPostUser from './components/selectPostUser'

然后加入父页面的组件列表中

import selectPostUserfrom './components/selectPostUser'import tasklog from'./components/tasklog'let that;
export
default{
components: { selectPostUser, tasklog },
//导入组件

这样我们才能在界面上正常使用了

  <el-col:span="12">
    <el-form-itemlabel="执行人"prop="executor">
      <select-post-userref="add_executor"v-model="addForm.executor":multiple="false" />
    </el-form-item>
  </el-col>
  <el-col:span="12">
    <el-form-itemlabel="参与人"prop="participant">
      <select-post-userref="add_participant"v-model="addForm.participant" />
    </el-form-item>
  </el-col>
  <el-col:span="12">
    <el-form-itemlabel="验收人"prop="accepter">
      <select-post-userref="add_accepter"v-model="addForm.accepter" />
    </el-form-item>
  </el-col>

如果我们需要在限制不能多加超过一个用户的,那么设置 :multiple="false" 即可限制即可,

用户组件中对该变量进行处理,限制添加多于一个人员的情况即可。

通过,选择确认内容后,我们更新对应的值,并触发一个change事件给父页面使用即可。

    async handleSubmit () { //表单提交
      this.isVisible = false
      this.change() //触发值变化

      this.$emit('submit', this.tags)
},
change (data) {if (this.tags) {this.dataValue = this.tags.map(e => e.id).join(',')
console.log(
this.dataValue)this.$emit('change', this.dataValue)
}
},

如果我们是查看明细状态下,不允许用户编辑或者修改人员,那么可以通过修改其他两个属性进行控制显示,如下代码所示。

<el-col:span="12">
  <el-form-itemlabel="执行人">
    <select-post-userref="viewForm.executor"v-model="viewForm.executor":closable="false":selectable="false" />
  </el-form-item>
</el-col>
<el-col:span="12">
  <el-form-itemlabel="参与人">
    <select-post-userref="viewForm.participant"v-model="viewForm.participant":closable="false":selectable="false" />
  </el-form-item>
</el-col>
<el-col:span="12">
  <el-form-itemlabel="验收人">
    <select-post-userref="viewForm.accepter"v-model="viewForm.accepter":closable="false":selectable="false" />
  </el-form-item>
</el-col>

通过设置 :
closable
="false"和  :
selectable
="false" 两个属性变量,就可以控制不出现移除标签人员和选择按钮的出现了。

以上就是自定义用户组件的主要逻辑和处理操作,我们自定义用户组件后,在各个页面需要的地方,引入使用即可,可以非常方便的重用,且可以减少单一页面的代码,非常方便。

学会如何自定义用户组件的处理过程,是我们前端开发所必须掌握的基本技能之一,熟练拆分各种重复模块变为自定义组件的过程,可以使得我们的代码更方便维护开发,增强可读性。

在前面随笔的介绍中,我们已经为各种框架,已经准备了Web API、Winform端、Bootstrap-Vue的公司动态网站前端、Vue&Element的管理前端等内容,基本都是基于Web API基础的。完成这些基础准备和布局后,我们继续将技术的触角放到使用Vue语言开发小程序的场景中,本篇随笔介绍使用uView UI+UniApp开发微信小程序,介绍使用准备过程中的一些注意点和经验总结。

1、小程序的开发准备工作

我们在开发小程序之前,需要了解一些基本的知识,以及掌握一些常规的开发工具,相关知识最好能够在着手开始前有所掌握,然后在开发过程中逐步加强巩固即可。

1)Vue语言掌握

我们通过Vue官网
https://cn.vuejs.org/v2/guide/index.html
,了解相关的语言基础只是,这个部分我们利用Vue开发小程序的基础,必须先有所掌握相关基础知识,以及开发组件中涉及到的知识点。

Vue可以是我们后续前端开发的强力助手,如果我们掌握的好,对很多相关的处理会一目了然,否则可能不明所以,如Mixin混入、以及组件的事件属性的通讯关系、Vuex的存取处理、常用的JS模块、ES6的函数定义及Promise的处理等等。

随着我们对Vue开发的逐步熟悉,各种特性我们会一一记在脑中并能够熟练运用。

2)UniApp官网

我们需要了解UniApp的基础,包括它的相关理念,下面介绍的uViewUI本身也是基于它的进一步封装,使用UniApp和HbuliderX 工具可以开发各种不同的小程序,APP、H5等场景,不过我们这里着重考虑微信小程序。

3)uView UI官网

通过官网
https://www.uviewui.com/components/intro.html
,了解组件的使用,以及一些基础的知识,如easycom的约定、全局变量特别是Vuex的混入及使用过程、JS类库等处理。

我们本次开发小程序,需要利用uView UI的组件代码下载下来进行项目的整合使用,或者利用HBuilder X的工具,直接下载使用UniApp官网的uView UI插件模块。

uView UI提供了各种界面上用到的组件,几乎封装了我们常见到的各种界面元素,极大的方便了我们小程序界面的开发工作,如果有些特殊的功能界面,也可以从UniApp官网的插件列表中寻找进行整合。

4)官方小程序网站

通过官网
https://developers.weixin.qq.com/miniprogram/dev/framework/
了解微信小程序的基础知识,我们利用uView UI+UniApp开发微信小程序,本身还是对官方小程序接口的封装处理,我们有时候还是需要对其中的相关生命周期,底层函数等有一定的了解,才能更好的理解微信小程序的各种机制和处理方式。

5)开发工具

我们这里开发UniApp程序,推荐还是使用HBuilderX来开发,相对通用的前端开发工具VS Code来说,它的有些处理更特性化,然后熟悉配合《微信开发者工具》进行小程序的调试和部署即可。

2、uView UI的使用

以Web API为业务数据的接口基石,我们可以扩展很多业务管理端,包括Winform端、Vue&Element业务管理端、动态门户网站、微信小程序等等。

我们循例介绍一下uView UI的基础使用步骤,首先在项目中main.js引入对应的组件-引入uView主JS库。

//main.js
import uView from "uview-ui";
Vue.use(uView);

检查uni.scss中已经加入了uView UI的样式,引入uView的全局SCSS主题文件

/*uni.scss*/@import'uview-ui/theme.scss';

检查app.vue中加入了对应的样式,在
App.vue
中首行的位置引入

<stylelang="scss">@import "uview-ui/index.scss";
@import "common/demo.scss";
</style>

一般来说,我们创建示例项目,都有这些基础的设置了,我这里只是循例介绍一下,让我们有所了解它的工作原理。

我这里还是主要介绍uView的this.$u的相关对象处理,它是通过Vue.prototype进行挂载进去的,也就是我们使用这些,都是uView加入的,如下main.js部分内容所示。

//引入uView提供的对vuex的简写法文件
let vuexStore = require('@/store/$u.mixin.js')
Vue.mixin(vuexStore)
//引入后自动将Vuex里面的键作为Computed的属性 //引入uView对小程序分享的mixin封装 let mpShare = require('uview-ui/libs/mixin/mpShare.js')
Vue.mixin(mpShare)

在项目中,我们可以找到对应项目的store实现,以及uview对象Mixin混入的部分

它把登录信息相关的用户和令牌信息,通过storage的方式进行存储起来,并处理好相关的逻辑,这样混入后我们可以方便的通过混入的Computed属性获取到对应的值,或者快速的设置存储起来。

这里的SaveStateKeys就是设置存储到storage中的键,只要存在这两个键的内容,都可以快速的使用Vuex的处理,如下是获取内容。

<viewclass="u-flex-1">
    <viewclass="u-font-16 u-p-b-20">用户名称:{{vuex_user.fullName}}</view>
    <viewclass="u-font-12 u-p-b-20">手机号:{{vuex_user.mobilePhone}}</view>
    <viewclass="u-font-12 ">邮箱:{{vuex_user.email}}</view>
</view>

由于是页面对象的自动混入,我们甚至在JS代码里面都没有定义这两个对象,只需要记得这个键是我们的全局存储的对象即可。

例如我们在JS的模块里面,通过VM获得this的参数

即可直接调用对象存储处理,如下代码所示。

//缓存其他信息
vm.$u.vuex('vuex_user.name', name)
vm.$u.vuex(
'vuex_user.fullName', fullName)
vm.$u.vuex(
'vuex_user.mobilePhone', mobilePhone)
vm.$u.vuex(
'vuex_user.email', email)
vm.$u.vuex(
'vuex_user.roles', roles)
vm.$u.vuex(
'vuex_user.roles', roleNames)

如果是在页面组件中,我们则使用this代替vm的变量进行调用

this.$u.vuex('vuex_user.name', name)

storage的信息,可以通过小程序的调试工具进行查看到,如下截图所示。

3、小程序的登录状态判断及跳转

在业务系统中,我们需要根据登录用户的身份获取对应的数据,如果用户没有登录,这些信息是无法获到的,那么我们可以在app.vue中判断用户是否登录,然后调准到对应的页面,如下所示。

跳转判断在app.vue的程序启动逻辑中进行处理,如下代码所示。

<script>exportdefault{
globalData: {
username:
''},
onLaunch() {
//如果用户没有登录或令牌失效,跳转到登录界面 //console.log(this.vuex_token) if(!this.vuex_token) {this.$u.route({
url:
'pages/template/login/password'});
}
else{
uni.switchTab({
url:
'/pages/example/myinfo'});
}
},
}
</script> <stylelang="scss">@import "uview-ui/index.scss";
@import "common/demo.scss";
</style>

其中 uni.switchTab 是跳转到首页的某个tab页面,如果我们的页面有tabbar页面的话。

用户登录的时候,需要输入用户名,密码,构建相关的参数后,进行登录处理

submit() {this.$refs.uForm.validate(valid =>{if(valid) {this.$u.api.User.login(this.model).then(data =>{//登陆成功跳转到Tab页面
uni.switchTab({
url:
'/pages/example/myinfo'});
});
}
else{
console.log(
'验证失败');
}
});
},

其中
this.$u.api.User
是用户API接口的统一调用方式

我们在main.js代码里面看到安装了两个不同的JS模块,如下代码所示。

//http拦截器,将此部分放在new Vue()和app.$mount()之间,才能App.vue中正常使用
import httpInterceptor from '@/common/http.interceptor.js'Vue.use(httpInterceptor, app)//http接口API抽离,免于写url或者一些固定的参数
import httpApi from '@/common/http.api.js'Vue.use(httpApi, app)

其中第一个是统一http调用的设置,第二个这是引入一个api对象,方便调用api对应的接口,如下:
this.$u.api.User

以及统一整合各个API对象

import User from '../api/user.js'

//此处第二个参数vm,就是我们在页面使用的this,你可以通过vm获取vuex等操作,更多内容详见uView对拦截器的介绍部分://https://uviewui.com/js/http.html#%E4%BD%95%E8%B0%93%E8%AF%B7%E6%B1%82%E6%8B%A6%E6%88%AA%EF%BC%9F
const install = (Vue, vm) =>{//将各个定义的接口名称,统一放进对象挂载到vm.$u.api(因为vm就是this,也即this.$u.api)下
    vm.$u.api = { //将 vm 对象传递到模块中
User: User(vm)
}
}

export
default { install }

其中在http.interceptor.js里面,统一设置了api的Url的baseUrl,这样可以不用配置反向代理的转义,就可以简化API中URL的定义了。

const install = (Vue, vm) =>{
Vue.prototype.$u.http.setConfig({
baseUrl:
'http://localhost:27206/', //接口访问基础路径 showLoading: true, //是否显示请求中的loading loadingText: '请求中...', //请求loading中的文字提示 loadingTime: 800, //在此时间内,请求还没回来的话,就显示加载中动画,单位ms //如果将此值设置为true,拦截回调中将会返回服务端返回的所有数据response,而不是response.data //设置为true后,就需要在this.$u.http.interceptor.response进行多一次的判断,请打印查看具体值 //originalData: true, //设置自定义头部content-type //header: { //'content-type': 'application/json;charset=UTF-8' //} })

另外我们返回的Response对象里面,有统一定义对象的,相关的数据可以参考随笔《
利用过滤器Filter和特性Attribute实现对Web API返回结果的封装和统一异常处理
》了解。

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

其中结果result是用于检查是否执行成功的标识,这里用来判断是否有错误即可,有错误,则显示自定义错误信息

    //响应拦截,判断状态码是否通过
    Vue.prototype.$u.http.interceptor.response = res =>{//console.log(res)
        //如果把originalData设置为了true,这里得到将会是服务器返回的所有的原始数据
        //判断可能变成了res.statueCode,或者res.data.code之类的,请打印查看结果
        if (!vm.$u.test.isEmpty(res.success)) {//res为服务端返回值,可能有code,result等字段
            //这里对res.result进行返回,将会在this.$u.post(url).then(res => {})的then回调中的res的到
            //如果配置了originalData为true,请留意这里的返回值
            returnres.result
}
else{var msg = '' var data =res.resultif (data && data.error &&data.error.message) {
msg
=data.error.message
vm.$u.toast(msg)
}
//如果返回false,则会调用Promise的reject回调, //并将进入this.$u.post(url).then().catch(res=>{})的catch回调中,res为服务端的返回值 return false}
}

上面的拦截默认是基于状态码200的,如果我们自定义一些异常,指定了状态码的也需要拦截,需改uview-ui/libs/request/index.js 根据状态码自己去加入,如下代码所示。

用户完成登录,并成功获取身份信息后,切换到指定的页面,如下界面效果所示

在《
使用uView UI+UniApp开发微信小程序
》的随笔中,介绍了基于uView UI+UniApp开发微信小程序的一些基础知识和准备工作,其中也大概介绍了一下基本的登录过程,本篇随笔详细介绍一下微信小程序的相关登录处理以及登录后设置用户身份信息,并跳转到相应页面的处理过程。

1、令牌判断和登录方式

在之前介绍过,在业务系统中,我们需要根据登录用户的身份获取对应的数据,如果用户没有登录,这些信息是无法获到的,那么我们可以在app.vue中判断用户是否登录,然后跳转到对应的页面,如下所示。

也就是系统启动的进入的时候,我们需要对系统用户的身份做一次判断,判断token是否存在,并且是否有效(因为token是有时效的)。

我们先来介绍下如何如何判断token是否存在的处理过程,由于token本身在登陆的时候,设置了存储,因此只需要通过uView的token获取操作即可读取出来,并进行判断即可。

我们只需要简单判断this.vuex_token 是否存在值即可,因为this.vuex_token是由于uView在加载Mixin的时候,已经自动映射了存储的键值,因此我们可以通过this.vuex_token访问到对应的值。

如果是简单的判断,我们在app.vue的如下代码即可处理

<script>exportdefault{
globalData: {
username:
''},
onLaunch() {
//如果用户没有登录或令牌失效,跳转到登录界面//console.log(this.vuex_token) if(!this.vuex_token) {this.$u.route({
url:
'pages/template/login/password'});
}
else{
uni.switchTab({
url:
'/pages/example/myinfo'});
}
},
}
</script>

不过我们需要通过判断它的时限有效性,那么通过判断失效日期进行处理,如下代码所示。

<script>exportdefault{
onLaunch() {
//如果用户没有登录或令牌失效,跳转到登录界面//console.log(this.vuex_token) console.log(this.$u.http.config.baseUrl)var authed = this.checkToken()if(!authed) {this.$u.route({
url:
'/pages/task/login/index'});
}
else{this.$u.route({
type:
'tab',
url:
'pages/task/login/myinfo'})
}
},
methods: {
checkToken() {
if(this.vuex_token && this.vuex_user) {var expired = new Date(this.vuex_user.expired) //token过期时间
const now =Date.now()if(expired - now > 0) {return true}
}
return false}
}
}
</script>

其中expired是我们获取到token的时间,并加上token的失效时间的。

我们这里使用了一个checkToken的函数,用来判断是否正常登录并且有效的,如果令牌无效,那么跳转到登陆界面,否则直接跳到个人信息页面下。

我们先来看看登录界面,我们这里提供了几种登录方式,账号密码登录、短信验证码登陆、微信授权登录几种方式。

这几种不同的登录方式,都是在验证成功后,需要获取用户的身份信息,并设置到Storage存储中去,逻辑上有相同之处。

2、登录的处理逻辑

用户登录的时候,需要输入用户名,密码,构建相关的参数后,进行登录处理,处理过程代码如下所示。

submit() {this.$refs.uForm.validate(valid =>{if(valid) {this.$u.api.User.login(this.model).then(data =>{//登陆成功跳转到Tab页面
uni.switchTab({
url:
'/pages/task/login/myinfo'});
});
}
else{
console.log(
'验证失败');
}
});
},

其中 this.$u.api.User 是用户API接口的统一调用方式,其中http.api.js的代码如下所示。

import User from '../api/user.js'import Taskfrom '../api/task.js'

const install = (Vue, vm) =>{//将各个定义的接口名称,统一放进对象挂载到vm.$u.api(因为vm就是this,也即this.$u.api)下
    vm.$u.api = { //将 vm 对象传递到模块中
User: User(vm),
Task: Task(vm)
}
}

export
default { install }

其中api/user.js里面定义了访问远程WebAPI的操作,同时也是我们封装一些处理逻辑的操作函数,我们可以通过ES6的Promise进行封装一个简单的登录函数。

由于我们这里登录过程,除了用户名密码外,还需要appid、时间戳以及签名参数等信息组合,因此构建参数比较多一点。

登录成功后,我们就调用resolve的执行即可,如果失败,调用reject的处理。

这样我们就可以直接通过Promise的操作处理登录成功后的操作了,如下代码所示。

this.$u.api.User.login(this.model).then(data =>{
uni.switchTab({
url:
'/pages/task/login/myinfo'});
});

而其中setUserToken函数,主要是便于重用的目的抽取出来,因为设置令牌和用户信息,是其他两个登录方式(短信登陆、微信登陆)所通用的操作。

短信验证码的登录方式也是类似,需要后端配合判断短信的有效性即可,前端先调用后端的发送短信操作,如下代码所示

//发送短信验证码
var params = { PhoneNumber: this.tel }this.$u.api.User.SendPhoneLoginSmsCode(params).then(res =>{if(res.success) {this.$u.toast(`验证码已发送至手机 ${this.tel},请注意查收!`)this.$u.route({
url:
'pages/task/login/code',params: {
mobile:
this.tel
}
});
}
else{this.$u.toast('发送出现错误:' +res.errorMessage)
}
})

而前端调用的函数也就是在api/user.js中简单封装一下对API的调用即可。

SendPhoneLoginSmsCode(data) { //发送登录动态码
    return vm.$u.post('/api/framework/User/SendPhoneLoginSmsCode', data)
},

发送的后端代码如下所示,主要就是放在缓存中一段时间供验证即可。

        /// <summary>
        /// 发送登录动态码
        /// </summary>
        /// <returns></returns>
[AllowAnonymous]
public CommonResult SendPhoneLoginSmsCode(PhoneCaptchaModel model)
{
//获取随机6位数字动态验证码 var code = RandomChinese.GetRandomNumber(6);//使用自定义模板处理短信发送 string message =string.Format(ConfigData.MySmsCodeTemplate, code);var smsSender = newMySmsSender();var result =smsSender.Send(model.PhoneNumber, message);if(result.Success)
{
var cacheKey = model.PhoneNumber;//以手机号码作为键存储验证码缓存 var cacheItem = new SmsLoginCodeCacheItem { Code = code, PhoneNumber =model.PhoneNumber };var cache = CacheManagerHelper.GetCacheItem(cacheKey, () =>{returncacheItem;
}, TimeSpan.FromMinutes(ConfigData.SmsCodeExpiredMinutes));
}returnresult;
}

验证的时候,只需要判断缓存里面是否存在记录和对应验证码是否匹配即可,如果顺利通过,那么构建用户的token信息返回给前端就是。

如果前端发送验证码成功,那么登陆界面跳转到等待输入验证码的界面,如下所示。

输入正确的验证码即可顺利登陆,否则过期则要求重新发送短信验证码。

submit() {var params = { mobile: this.mobile, smscode: this.smscode };
console.log(params);
this.$u.api.User.dynamiclogin(params)
.then(res
=>{this.$u.toast('验证成功');

uni.switchTab({
url:
'/pages/task/login/myinfo'});
})
.
catch(error =>{
console.log(
'验证失败' +error);this.$u.toast(error);
});
},

如果顺利登陆,则跳转到我的页面里面去,展示一些常用的信息汇总,以及常见处理操作。

这里面涉及一个退出登录的操作,主要就是注销当前用户的身份,只要清空身份信息,并跳转到登录首页即可。

logout() {this.$u.vuex('vuex_token', null) //重置
    this.$u.vuex('vuex_user', null) //重置
    this.$u.toast('退出成功,请重新登录!')this.second = 0setTimeout(()=>{this.$u.route({ url: '/pages/task/login/index'})
},
1500)
},

鉴于篇幅原因,先介绍到这里,关于微信授权登陆及绑定的操作过程,后续再介绍。