wenmo8 发布的文章

在很多时候,我们做一些非常规化的界面的时候,往往需要创建一些用户控件,在其中绘制好一些基础的界面块,作为后续重复使用的一个单元,用户控件同时也可以封装处理一些简单的逻辑。在开发Winform各种类型项目,我都时不时需要定制一些特殊的用户控件,以方便在界面模块中反复使用。我们一般是在自定义的用户控件里面,添加各种各样的界面控件元素,或者封装一些特殊的函数处理共外部调用等。本篇随笔主要介绍基于DevExpress的Winform开发经验,介绍一个类似看板信息的用户控件,并在TabelLayout和StackLayout布局控件中进行展示。

1、用户控件界面的处理

在偶尔的一个场合下,看到一个牙医管家的软件界面做的非常不错,其中有一个预约列表的界面感觉非常好,如下界面所示。

其中它的每个用户信息列表里面,都是一个综合信息的展示,非常直观,估计应该是用户自定义控件做的。

在其中里面,有不同的字体,各式图标,以及内容的信息展示, 这个我在DevExpress的列表控件里面,没有看到可以如此定义列表内容的,在DevExpress的GridView里面有一个看板模板的定义有点接近,但是试了一下,可调性不好,于是放弃寻求其他接近方案,玩遍DevExpress的控件后,发现最好的方式应该是自定义用户控件的方式来解决这个界面问题。

花了一点时间,制作了一个用户控件,在其中添加一个LayoutControl方便控制布局,添加一些标签以及设置了一些图标,得到下图所示。

左侧的颜色条由于使用Group控件,因此宽度暂时无法调整,如果介意大小,我们可以在其中在创建一个LayoutControl,然后在其中方式内容即可。

我们改变布局,然后添加一个颜色块,得到类似界面如下所示。

我们来定义一下用户控件的源码部分,修改其中源码,增加对应的属性,方便动态设置用户控件的相关属性,如颜色块,项目背景色,以及绑定的对象信息等内容。

然后在界面加载完毕后,设置对应的信息和颜色信息,如下代码所示。

        /// <summary>
        ///窗口加载事件/// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void UserItemControl_Load(objectsender, EventArgs e)
{
BindData();
RefreshColor();
}
/// <summary> ///根据用户定义信息,显示不同的内容/// </summary> private voidBindData()
{
if(this.UserItemInfo != null)
{
var info = this.UserItemInfo;this.lblCustomerName.Text =info.CustomerName;this.lblMobile.Text =info.Mobile;this.lblReceiver.Text =info.Receiver;this.lblRecordDate.Text =info.RecordDate;this.lblRecordNo.Text =info.RecordNo;this.lblDealType.Text = "";this.lblStar.ImageOptions.ImageIndex =GetStarImageIndex(info.Stars);if (!info.IsVip)
{
this.lblVip.Visibility =LayoutVisibility.Never;
}
}
}
/// <summary> ///刷新背景色/// </summary> private voidRefreshColor()
{
if (ItemBlockColor !=Color.Empty)
{
this.itemColor.AppearanceItemCaption.BackColor =ItemBlockColor;
}
if (ItemBackColor !=Color.Empty)
{
layoutControl1.BackColor
=ItemBackColor;
}
}

但我们鼠标浮动在项目上或者离开的时候,或者单击某项的时候,我们变换下颜色,方便区分显示。

        private void layoutControl1_MouseLeave(objectsender, EventArgs e)
{
if (!this.IsSelected)
{
this.layoutControl1.ResetBackColor();
}
}
private void layoutControl1_MouseEnter(objectsender, EventArgs e)
{
if (!this.IsSelected)this.layoutControl1.BackColor = Color.FromArgb(192, 255, 192);
}
private void layoutControl1_Click(objectsender, EventArgs e)
{
this.IsSelected = true;
OnItemClick
?.Invoke(this, e);
}

完成这些后,我们需要在窗体上对内容进行初始化。

最后我们看看界面的效果如下所示

或者下面这样的界面布局。

如果嫌弃边框红色不好看,我们 可以修改边框为灰色调一点的,这样总体看起来效果如下所示。

得到和我们最终需要的界面很接近了。

一般除了悬浮鼠标颜色变化外,控件单击后,我们会设置不同的背景色,以示区分。

        /// <summary>
        ///是否选中节点/// </summary>
        public boolIsSelected
{
get{returnm_IsSelected;
}
set{
m_IsSelected
=value;this.ItemBackColor = value ? Color.FromArgb(255, 255, 192) : Color.Transparent;this.RefreshColor();
}
}

一般列表界面中,我们除了支持鼠标移动、单击变色的效果外,我们还希望支持通过键盘箭头上下键进行上下浏览项目。我们如果需要使用键盘的按键,需要设置窗体的KeyPreview属性为True,

然后跟踪按键的事件即可,如下所示。

            this.KeyPreview = true;this.KeyUp += FrmKanBan_KeyUp;

按键事件捕捉处理如下所示,主要就是判断选中的用户控件,并对面板的子控件的选中效果进行处理。

        private void FrmKanBan_KeyUp(objectsender, KeyEventArgs e)
{
//单击鼠标或者切换按键,会触发用户控件获得selectItem对象,可以进行箭头上下移动 if (selectItem != null)
{
var panel =selectItem.Parent;if (panel != null)
{
//获取操作项的索引值 int oldIndex =panel.Controls.IndexOf(selectItem);if (e.KeyCode ==Keys.Up)
{
if (oldIndex > 0)
{
//通过序号获得新的控件,并单击它触发选择事件 var newCtrl = panel.Controls[oldIndex - 1];
Item_OnItemClick(newCtrl,
newEventArgs());
}
}
else if (e.KeyCode ==Keys.Down)
{
if (oldIndex < (panel.Controls.Count - 1))
{
//通过序号获得新的控件,并单击它触发选择事件 var newCtrl = panel.Controls[oldIndex + 1];
Item_OnItemClick(newCtrl,
newEventArgs());
}
}
}
}
}

界面中用户控件的切换选中效果,需要先清空之前所有的选择,然后在设置新的选中控件,所以还需要对控件触发单击事件进行处理,如下所示。

        /// <summary>
        ///选中的用户控件对象实例/// </summary>
        UserItemControl selectItem = null;/// <summary>
        ///单击用户控件,触发清除所有标记后,再次设置选中的项目标记/// </summary>
        private void Item_OnItemClick(objectsender, EventArgs e)
{
//清空所有控件的选中标记 var panel =(PanelControl)((Control)sender).Parent;foreach (Control ctrl inpanel.Controls)
{
var item = ctrl asUserItemControl;if(item != null)
{
item.IsSelected
= false;
}
}
//设置选中控件 selectItem =((UserItemControl)sender);
selectItem.IsSelected
= true;this.Text = selectItem.UserItemInfo.CustomerName + "-选中";//如果在面板中遮挡,移动滚动条,可以查看到完整用户控件界面 panel.ScrollControlIntoView(selectItem);
}

如下效果所示。

2、TabelLayout和StackLayout布局控件的介绍和使用

我们在做Winform开发的时候,一般知道,微软传统Winform的布局提供两个控件,FlowLayoutPanel和TableLayoutPanel,一个是流式布局,一个是表格布局,各有各的用处。流式布局主要就是按照顺序挨个放置控件,表格布局主要按照表格的定义的行列单元格,严格放置控件,表格单元格控制强度更大,而且控件具有拉伸效果。

对于DevExpress,我们一般还是倾向于采用它提供给的控件来做界面,可以很好融合它的皮肤效果,相对于Winform传统两个布局控件,DevExpress提供了两个封装效果相当的控件布局StackPanel和 TablePanel,他们的效果实现大同效果,不过调用的接口不同。

对于两个控件,我们希望里面的内容自动出现滚动条,那么设置属性AutoScroll 为True即可,如下代码所示。

panel.AutoScroll = true;

而StackPanel另外需要LayoutDirection,也就是控件顺序展现的方式,如下代码所示。

panel.LayoutDirection = StackPanelLayoutDirection.TopDown;

使用StackPanel面板来测试展示用户控件列表的界面代码如下所示。

        /// <summary>
        ///使用StackPanel对用户控件布局进行处理/// </summary>
        private voidInitPanel(StackPanel panel)
{
panel.AutoScroll
= true;//面板自动出现滚动条 panel.LayoutDirection = StackPanelLayoutDirection.TopDown;//从上往下展示 panel.Controls.Clear();//清空界面 var list = GetInfoList(); //获取用户控件绑定的对象信息列表 for (int i = 0; i < list.Count; i++)
{
//定义用户控件实例 var item = newUserItemControl();
item.UserItemInfo
= list[i];//绑定对象信息 item.Anchor= AnchorStyles.Top | AnchorStyles.Left |AnchorStyles.Right;
item.ItemBlockColor
= colors[i %10]; //变化颜色 item.OnItemClick += Item_OnItemClick;//触发选中事件 panel.Controls.Add(item);
}
}

对于表格布局TablePanel控件来说,使用初始化控件的方式也差不多,不过有个别地方注意即可。

        /// <summary>
        ///使用TablePanel对用户控件布局进行处理/// </summary>
        private voidInitPanel(TablePanel panel)
{
panel.AutoScroll
= true;//面板自动出现滚动条 panel.Controls.Clear();//清空界面 panel.Rows.Clear();//清空行 panel.Columns.Clear();//清空列//添加列定义(增加一个列即可) panel.Columns.Add(newTablePanelColumn(TablePanelEntityStyle.Relative, 55F));var list = GetInfoList(); //获取用户控件绑定的对象信息列表 for (int i = 0; i < list.Count; i++)
{
//定义行信息 panel.Rows.Add(newTablePanelRow(TablePanelEntityStyle.AutoSize, 100F));//定义用户控件实例 var item = newUserItemControl();
item.UserItemInfo
=list[i];//定义拉伸效果 item.Anchor = AnchorStyles.Top | AnchorStyles.Left |AnchorStyles.Right;
item.ItemBlockColor
= colors[i % 10]; //变化颜色 item.OnItemClick += Item_OnItemClick;//触发选中事件//先添加控件到面板集合中 panel.Controls.Add(item);//设置控件的单元格位置 panel.SetCell(item, i, 0);
}
//添加多一行,确保布局 panel.Rows.Add(newTablePanelRow(TablePanelEntityStyle.AutoSize, 100F));
}

添加控件的时候,需要注意下面的代码,才能正常展示控件信息,否则无法看到用户控件。

                //先添加控件到面板集合中
panel.Controls.Add(item);//设置控件的单元格位置
                panel.SetCell(item, i, 0);

最后对比下效果,左边是TablePanel,右边是StackPanel展现出来的效果。

以上就是界面如何在DevExpress开发中使用各种用户控件进行用户控件的创建、以及实现鼠标进入、移出、单击的不同效果,以及实现键盘上下箭头按键的事件选中效果,并介绍DevExpress中两个布局控件TabelPanel和StackPanel的正常使用,达到展示控件信息的作用。

微信门户应用管理系统,采用基于
MVC + Bootstrap + Ajax + Enterprise Library
的技术路线,界面层采用Boostrap + Metronic组合的前端框架,数据访问层支持Oracle、SQLServer、MySQL、PostgreSQL等数据库。框架以MVC5,C#4.5作为开发基础,能够部署在任何.NET服务器上,建议部署到阿里云服务器。

1、环境准备

本框架采用C#开发,Web管理端采用基于
MVC + Bootstrap + Ajax + Enterprise Library
的技术路线的架构,需要使用VS2017/2019进行开发,默认数据库采用SQL Server,可以安装SQL Server 2008R2及以上。

微信公众号开发,一般需要进行相关的网页页面调试,请下载微信提供的《微信开发者工具》,下载地址为:
https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html

框架开发一般还需要使用到代码生成工具,工具可以从我公司的官网上下载(
http://www.iqidi.com/Download/Database2SharpSetup.rar
)。

2、模块说明

整个框架项目包含了微信公众号、企业微信、小程序三个部分内容,其中三者之间有一些相同的部分抽象出来放在一个公共的项目里面,然后公众号、企业微信、小程序共享一个后台管理系统,这个后台系统主要就是实现账号信息的配置、和微信后台事件的对接、以及权限和系统数据的管理维护等功能。

使用VS打开项目源码后,解决方案的工程如下所示。

下面是对上面项目工程进行说明,方便理解其中各个模块的意义。

编号

名称

说明

01_Framework 目录里面的项目,是框架的基础内容,包括权限、字典、附件模块的管理。

1

Security

权限管理模块业务层。

2

Dictionary

字典管理模块业务层。

3

Attachment

附件管理模块业务层

4

Other

其他系统内容,如图标、产品、测试用户等基础信息

02_Common目录里面的项目,是公众号/企业微信/小程序公用的部分,如账号配置、基础消息对象类,也可以是Web后台管理所需的模块内容。

1

WHC.Common.API

提供一些第三方模块的封装,如邮件处理、短信处理以及一些第三方的应用扩展模块等。

2

WHC.Common.Cache

基于CacheManager的接口处理的缓存接口,可以实现SystemRuntimeCache、Redis等缓存的整合。

3

WHC.Common.Data

提供常用模块,需要对数据库处理的模块的封装,包括一些如账号、菜单、事件、消息内容等公用的数据存储。

4

WHC.Common.Entity

提供常用模块里面设计到的实体类信息。包括公众号和企业微信都共同用到的对象类。

5

WHC.Common.Handler

多个IOC方式的模块,可以在所需系统注册使用。

自定义接口包含有测试处理接口、企业微信消息推送接口、设备信息二维码扫码接口、短信和邮件发送接口。

外部的插件业务接口包括:百度的地理位置解析接口、电影院信息查询、天气信息查询、交通信息查询、旅游信息查询等。

03_Weixin 目录里面的项目,是微信公众号API相关的内容。

1

TestWeixin

是对公众号的API封装的项目模块进行测试。

2

WHC.Weixin.API

针对微信公众号API的单独封装。

3

WHC.Weixin.Data

结合数据库存储内容,对微信公众号模块的管理。这个模块包含对WHC.Weixin.API的使用。

04_Corp目录里面的项目,是对企业微信API的封装和对数据库管理模块的封装。

1

TestCorp

是对企业微信的API封装的项目模块进行测试。

2

WHC.Corp.API

针对微信企业微信API的单独封装。

3

WHC.Corp.Data

结合数据库存储内容,对企业微信模块的管理。这个模块包含对WHC.Corp.API的使用。

05_SmallApp目录里面的项目,是对小程序API的封装和对数据库管理模块的封装。

1

TestSmallApp

是对小程序的API封装的项目模块进行测试。

2

WHC.SmallApp.API

针对小程序API的单独封装。

3

WHC.SmallApp.Data

结合数据库存储内容,对小程序模块的管理。这个模块包含对WHC. SmallApp.API的使用。

06_UI 是微信后台管理系统部分内容,包括后台管理系统及企业微信的管理客户端程序。

1

WHC.Weixin.BootstrapUI

微信管理后台,包括对公众号和企业微信的统一管理,实现各个模块数据的同步和查看管理操作。

2

CorpManagement

基于C/S的Winform企业微信管理客户端,可以方便管理企业微信的通讯录的组织机构、标签、人员信息,以及文本、图片、语音、视频等消息的发送功能。

07_His 是扩展的项目部分

1

WHC.His.Data

包含对医疗设备维修项目、处方审核项目等扩展项目的模块管理。

其他模块

1

公用类库

WHC.Framework.Commons

WHC.Framework.ControlUtil

提供日常各种开发操作的辅助类库。

WHC.Framework.Commons项目是对常规.NET共用类库的封装使用,已达到统一、高效、方便的目的。

WHC.Framework.ControlUtil项目是对第三方组件的封装使用,以及对整个框架各层抽象类的定义和封装。

2

分页类

WHC.Pager.Entity

这个是独立定义的一个分页参数对象,方便框架里面对数据进行分页处理。

4

参数配置管理

SettingsProvider.Net

提供了一个第三方的配置信息的管理类。这个是在GitHub上的一款参数配置组件,能够基于普通配置文件、ProgramData目录文件、独立存储区文件等方式的配置文件存储,它主要是基于Json格式进行的配置保存,因此我们还可以把它存储到数据库

3、模块关系

开发模块划分包括:微信公众号、企业微信、小程序几个部分

这几个部分又有一些基础一样的内容,抽取出来作为公共的项目。公众号、企业微信、小程序公共部分内容如下所示。

公众号、企业微信、小程序几个分类,它们包含了数据存储的模块内容,也包括了对应分类的微信API操作部分。数据管理模块和API模块调用关系如下所示。

微信界面项目,同样是这几个部分的组合。微信界面部分是前面模块组件的综合使用,在微信应用里面,一般需要使用
80端口和微信服务器做交互,而这个同时往往也是我们项目的端口地址。

另外,微信框架还包括基础类库支持部分内容,包括权限管理、字典管理、附件管理等基础内容。

微信开发,我们首先需要利用我们的语言(这里是利用C#语言),为所有用到的API接口实现进一步的封装,方便使用,微信API模块包含的内容很多,其中公众号接口封装包括内容如下所示。

企业微信的API接口包含内容如下所示。

我们在WHC.Weixin.Data模块里面,定义了包含公众号的消息分派处理接口,这个分派接口是对接收来自微信服务器的各种消息事件进行响应;另外该模块还包含一些常规的数据存储,如关注用户、菜单、文章内容等方面数据的存储,如下所示。

同样我们参考微信公众号的做法,也是建立一个数据存储管理的项目,作为微信消息事件的处理入口,同时也管理存储一些必须的数据,包括需要同步的用户、标签、部门等数据。

4、微信公众号配置

不管是微信公众号,还是企业微信,都需要申请账号才能使用,并且最好通过认证,这样才能具有更多的接口权限使用。

微信公众号的注册申请参考文章:
http://www.cnblogs.com/wuhuacong/p/3613826.html

输入账号密码,登录微信公众号后台
https://mp.weixin.qq.com
,然后扫码确认登陆,在左侧【开发】【基本配置】中获取对应的账号配置参数。

在【修改配置】中进行修改内容,如下界面所示。

然后启动微信管理后台系统(一般启动,本地地址为
http://localhost:32973/Login/Index
),如果部署在服务器后,请通过域名进行访问即可。

登陆后台系统后,在【微信管理】【微信账号配置】管理界面里面进行维护对应账号信息,如下界面所示。

修改或者增加相关的公众号账号信息。(根据微信公众号参数,录入红色方框的内容)

再次回到刚才后台的界面,修改对应的URL,提交参数,如提示【提交成功】即可正确完成配置。

5、企业微信配置

微信企业微信的注册申请参考文章:
http://www.cnblogs.com/wuhuacong/p/3991735.html

企业微信后台登录地址为:
https://work.weixin.qq.com
,扫码登录进行获取对应的参数,找到【我的企业】【企业信息】界面,如下界面所示。

然后滚动到底部,找到对应的企业ID,如下界面所示,这个就是后面填写需要用到的CorpID。

再次切换到【应用管理】【应用】选项卡里面。

在应用底部,找到【创建应用】按钮,创建一个新的应用;或者单击进入现有的应用进行查看参数。

查看创建了的应用信息,如下界面所示。

记录下前面的CorpID、CorpSecret、加密Token、加密秘钥等参数。

然后启动Web管理后台,登录后台系统后,在【微信管理】【微信账号配置】管理界面里面进行维护对应账号信息,如下界面所示。

在账号管理界面里面,选择企业微信,修改或者增加相关的账号信息。

再次回到企业微信后台,录入上面获得的接收URL,然后保存成功即可,如下界面所示。

在上篇随笔《
微信门户开发框架-使用指导说明书
》中主要介绍了微信公众号和企业微信的模块说明以及各个模块之间的关系、账号配置以及如何和微信开发框架结合起来使用的内容,由于篇幅局限,因此本篇随笔继续这个方面拓展介绍,介绍基于微信框架的开发过程,包括微信账号菜单的管理,H5页面的开发知识,以及如何整合框架实现公众号应用和企业微信应用的快速开发过程。

微信公众号和企业微信,主要就是利用微信后台管理系统的入口,创建对应应用H5页面。本章节主要介绍基于微信框架的基础上,开发对应的H5页面过程,从而实现微信公众号或者企业应用的菜单对接或者事件对接处理。

1、微信开发者工具

微信公众号或者企业微信开发,一般需要进行相关的网页页面调试,请下载微信提供的《微信开发者工具》,下载地址为:

https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html

软件启动后即可扫码进入,例如我们对其中一个页面进行调试,界面如下所示。

一般情况下,我们的H5页面需要进行重定向获取用户的信息,因此也需要在公众号后台允许该工具重定向获取对应的用户信息,因此需要在后台绑定账号。

然后在下面界面中绑定开发者账号即可。

2、微信H5页面及JSDK开发

在我们开发微信页面的时候,需要大量用到了各种呈现的效果,一般可以使用
Boostrap的效果来设计不同的页面,不过微信团队也提供很多这方面的资源,包括
JSSDK的接口,以及
Weui的页面样式和相关功能页面,给我们提供了很大的便利。

1) JSSDK

微信JS-SDK是
微信公众平台
面向网页开发者提供的基于微信内的网页开发工具包。通过使用微信JS-SDK,网页开发者可借助微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用微信分享、扫一扫、卡券、支付等微信特有的能力,为微信用户提供更优质的网页体验。

目前JSSDK支持的接口分类包括下面几类:基础接口、分享接口、图像接口、音频接口、智能接口、设备信息、地理位置、摇一摇周边、界面操作、微信扫一扫、微信小店、微信卡券、微信支付,随着微信功能的全部整合,估计更多的接口会陆续开放出来。

2

WeUI

Jquery WeUI

WeUI
是一套同微信原生视觉体验一致的基础样式库,由微信官方设计团队为微信内网页开发量身设计,可以令用户的使用感知更加统一。在微信网页开发中使用 WeUI,有如下优势:

  • 同微信客户端一致的视觉效果,令所有微信用户都能更容易地使用你的网站
  • 便捷获取快速使用,降低开发和设计成本
  • 微信设计团队精心打造,清晰明确,简洁大方

该样式库目前包含 button、cell、dialog、progress、toast、article、icon 等各式元素,已经在
GitHub
上开源。访问
http://weui.github.io/weui/
或微信扫码即可预览。

jQuery WeUI
中使用的是官方 WeUI 的 CSS 代码,并提供了 jQuery/Zepto 版本的 API 实现。JQuery WeUI相对于在官方WeUI的基础上做了一些功能扩展,已丰富界面设计和相关功能,因此我们可以考虑直接基于JQuery  WeUI的基础上进行页面开发即可。下载地址:
http://jqweui.com/

在微信框架里面的一些案例中,都利用了We UI样式来进行很多微信H5页面的功能设计,我们主要利用JQuery WeUI库来实现相对应的一些控件处理功能。

3、微信框架中H5控制器关系

我们知道,H5页面也是基于Asp.net的开发模式,因此H5页面的入口也是MVC的控制器。为了实现简单、高效的原则,我们一般采用了一定层次的控制器继承关系,因此MVC的控制器继承关系需要了解一下。

在解决方案控制器目录如下所示。

这些控制器的继承关系如下所示,继承能够极大程度利用基类的公用函数,减少子类的代码臃肿。

通过上面的继承关系,我们可以看到公众号和企业微信部分,有一个基类控制器是实现两者共同的功能的。然后再次分支,分为公众号控制器其或者企业微信控制器,这两个对于用户来说也是一个基类,我们一般在这个基础上创建自己的子类控制器,以便实现差异化的功能。针对公众号和企业号的处理,它们的接口对象不同,因此处理代码也是不同的,这个即使他们分开的原因。

例如,基于上面的继承关系,如果我们需要创建一个新的公众号模块“ABC”,那么我们只需要继承自BaseJSSDKController,创建一个ABCController控制器,在里面实现自己的业务控制器接口即可;对于企业微信也一样,如果我们要创建一个新的企业应用“XYZ”,那么我们只需要继承自BaseQySDKController,创建一个XYZController控制器即可。

4、账号微信菜单管理

然后启动微信管理后台系统(一般启动,本地地址为
http://localhost:32973/Login/Index
),如果部署在服务器后,请通过域名进行访问即可。

进入对应的【微信菜单】模块功能,如下所示。

由于不同账号的菜单不一样,因此注意从右上角选择之前创建好的公众号账号.

默认框架提供了我们标准的一些菜单,如果选择您新建的账号,可能菜单栏是空白的。

注意:
为了方便,我们一般允许多个菜单分组存在,但只有一个是启用的(黑色),其他分组是禁用的(红色),提交菜单到微信,也只是提交当前启用的菜单分组内的菜单列表。

如果是空白的,请先创建一个菜单分组,然后需要单击【进入菜单维护】进行菜单的创建操作。

菜单可以新增,然后指定对应的类型即可,如是Click类型,必须指定事件;如果是View类型,必须指定URL。菜单是树形列表,你可以在某个菜单下面创建子菜单。菜单的文字大小、菜单项目等都有限制,必须遵循微信指定的规则创建。如果菜单设置为不可见,那么该项菜单及其下面的菜单列表都不会提交到微信服务器。菜单排序是用于确定菜单列表的顺序位置的。

常见的一些菜单列表如下所示。

菜单的URL我们要注意填写,例如对于
http://www.iqidi.com/h5/Myinfo
,这个地址前面部分是域名,h5代表是H5Controller,MyInfo代表H5Controller控制器或其基类里面的一个方法入口。这个地址也可以在方法里面进行重定向,从而可以获取用户的openid等身份信息。

菜单维护好后,我们可以通过【提交菜单到微信】功能,发布我们编辑好的菜单列表。

如果发布成功,那么我们公众号的菜单就会及时更新;如果有错误信息,请检查菜单文字的大小、URL格式和大小、菜单项目的数量、事件代码是否填写等是否符合微信的要求。

5、微信公众号开发

公众号功能的开发,除了前面我们在后台创建账号,并录入账号的账号名、AppID、AppSecret、Token、URL、EncodingAESKey并在公众号配置成功后,我们只是完成了第一步和微信服务器的对接工作。接下来需要基于这个账号下面创建对应的菜单,以及实现菜单对应的H5页面控制器和视图代码的处理。这两个步骤都做好了,我们就可以着手对应公账号H5页面的功能开发了。

每开发一个应用功能,我们都需要根据我们的业务场景,设计好对应的数据库表,然后根据业务表删除对应的框架代码BLL/DAL/IDAL/Entity等这些,增量复制到框架对应位置即可,这个和我们开发其他应用的模式一样。下面我们以处方审核项目为例进行介绍。

1)
数据库设计

我们根据业务需要,创建对应的业务表即可。这里涉及一个药品表和一个处方记录表即可,如下截图所示是基于PowerDesigner工具进行的设计视图。表的设计,一般来说需要有一个ID字段,并作为主键,其他根据情况处理即可。

设计好数据库表后,利用工具提供的SQL生成脚本,获取脚本,在数据库上执行即可创建对应的表。

2)
生成框架代码

确保生成对应的数据库表后,我们就可以利用代码生成工具Database2Sharp(代码生成工具下载地址:
http://www.iqidi.com/database2sharp.htm
)进行代码的生成了。

我们这里生成代码,只需要选择“【代码生成】【EnterpriseLibrary代码生成】”即可。

代码生成后把生成的文件复制到VS项目中,然后编译测试是否有错误,有错误则修正即可。

3)
H5
后台代码处理

接下来我们就需要创建一个对应业务的H5控制器了,如果我们业务为ABC,那么你可以定义一个ABCController,继承自公众号控制器 BaseJSSDKController即可。

我处方管理页面则是定义H5Controller控制器名称。

它的控制器定义代码如下所示。

在H5页面里面,如果需要获取用户的OpenID,则需要重定向才能获取,框架里面封装了一个方法AuthOpenId可以实现重定向获取,我们使用如下代码即可。

一旦有了用户的OpenID,就可以通过后台接口获取对应的订阅用户或者系统用户信息(用户关注的时候,订阅用户记录就会写入数据库里面;或者系统用户表绑定对应的OpenID,可以和系统用户关联),如下代码所示是根据OpenID获取绑定的用户记录。

如果我们需要在后台管理系统中绑定用户,那么我们可以在系统用户管理中进行处理,如下界面所示。

在H5页面数据保存的时候,后台需要接受来自页面的数据集合,然后逐一取出进行处理即可,常规的数据可以通过FormCollection进行处理,如下代码所示。

对于记录的保存,一般新增和编辑保存处理都是一个方法,需要我们根据它传过来的ID是否为空判断是新增还是编辑的,如下代码所示。

对于前端需要列表数据,一般通过MVC返回对应的JSON对象给前端使用即可,如下是分页列表展示数据的函数。

5)
H5
页面代码处理

H5页面一般是基于某个MVC控制器下进行的视图页面处理,一旦我们创建某个MVC控制器,VS就会创建一个对应的视图目录,如下是H5Controller控制器下的视图页面。

页面模块说明如下所示。

//通用模块的内容

BindFail.cshtml --- 微信公众号绑定失败处理

BindSuccess.cshtml --- 微信公众号绑定成功处理

Info.cshtml --- 显示一般信息的页面

JSLogin.cshtml --- 使用微信公众号扫码登陆页面

LoginError.cshtml --- 公众号扫码登陆错误页面

Test.cshtml --- 测试页面

Report.cshtml --- 报表测试展示

//医疗设备维修项目页面

DeviceAdd.cshtml --- 设备添加页面

DeviceCheck.cshtml --- 设备检查页面

DeviceInfo.cshtml --- 设备信息页面

DeviceInventory.cshtml --- 设备盘点页面

DeviceList.cshtml --- 设备列表页面

DeviceMaintain.cshtml --- 设备预防性维护页面

Measure.cshtml --- 设备度量页面

Repair.cshtml --- 设备维修工单页面

RepairDeal.cshtml --- 设备微信工单详细页面

//医疗处方审核项目页面

DrugInfo.cshtml --- 药品信息页面

DrugInquiry.cshtml --- 问诊信息页面

DrugInquiry2.cshtml --- 问诊信息页面(使用另一种上传方式的处理)

DrugList.cshtml --- 药品列表页面

DrugShop.cshtml --- 药店信息页面

Prescription.cshtml --- 处方信息

PrescriptionDetail.cshtml --- 处方详细信息

PrescriptionMy.cshtml --- 本店处方页面

MyInfo.cshtml --- 我的信息

PersonalInfo.cshtml --- 个人信息

一般的H5页面文件头部如下所示,没有母版,指定对应的JQueryWeui样式等。

而其他页面所需的CSS样式,我们根据情况参考对应页面Style创建即可。

H5页面底部需要引入对应的JS文件以及编写对应的JS脚本代码函数。

H5页面的表单,参考Jquery WeUI的表单录入处理样式即可,如下所示。

最后可以一个类似下面的表单页面内容。

如对于一些弹出框的选择页面,如选择药品的页面,那么可以通过一个连接进行打开,如下效果所示。

页面代码如下所示。

对于输入框里面即时从服务端获取数据的操作,我们一般通过绑定文本控件的“input propertychange”事件进行响应的,通过获取服务端的JSON数据,然后组装层对应的界面HTML代码后,把它赋值给某个控件就可以实现内容的动态展示了,如下是药品查询并显示在界面上的代码。

上面代码通过绑定input的propertychange事件,根据URL地址获取对应的JSON数据,然后遍历集合进行构建HTML内容,最后赋值HTML给对应控件,实现界面内容的实时变化。

6、
企业微信开发

企业微信的H5页面开发和公众号的开发类似,都需要先在后台管理系统中创建对应账号(注意一个企业微信下不同的应用需要创建不同的账号),然后在对应账号下面创建好对应的菜单资源,并同步到微信上即可更新应用的菜单。具体的账号配置参考前面小节关于【企业微信配置】说明。

同样,每开发一个应用功能,我们都需要根据我们的业务场景,设计好对应的数据库表,然后根据业务表删除对应的框架代码BLL/DAL/IDAL/Entity等这些,增量复制到框架对应位置即可,这个和我们开发其他应用的模式一样。

1)
数据库设计

步骤和公众号类似,此处忽略。

2)
生成框架代码

步骤和公众号类似,此处忽略。

3)
H5
后台代码处理

前面我们介绍过H5控制器类的继承关系,如下所示。这里我们以资产管理项目的企业微信控制器为例介绍。

QyH5Controller的定义代码只需要继承自BaseQySDKController即可。

和公众号有些不一样,企业微信的用户身份获得的是user_id的内容,这个是对应企业微信的标识的。我们也可以通过重定向页面获得用户身份user_id的内容,如下代码所示。

一旦有了用户的user_id,就可以通过后台接口获取对应的系统用户信息(系统用户表绑定对应的user_id,可以和系统用户关联),如下代码所示是根据user_id获取绑定的用户记录。

BLLFactory<User>.Instance.FindByCorpUserId(userId);

如果我们需要在后台管理系统中绑定用户,那么我们可以在系统用户管理中进行处理,如下界面所示。

一般情况下面,对于多个参数的情况,我们也是可以通过FormCollection的集合获取,如下函数定义所示。

对于前端需要列表数据,一般通过MVC返回对应的JSON对象给前端使用即可,如下是分页列表展示数据的函数。

4)
H5
页面代码处理

和公众号类似,创建对应企业应用H5控制器后,也同样会创建对应的视图目录,如我们创建了QyH5Controller控制器,那么对应有QyH5的视图目录,我们创建一些我们需要的视图文件页面,配合QyH5控制器里面的对应入口,如下所示。

企业微信的应用和公众号的H5页面应用本质上没有差异,只需要自己指定对应的样式,创建好对应的HTML代码即可。

同样在H5页面底部引入对应的JS文件,编写对应的JS处理函数来处理页面逻辑。

有一小段时间没有持续升级ABP框架了,最近就因应客户的需要,把ABP框架进行全面的更新,由于我们应用的ABP框架,基础部分还是会使用官方的内容,因此升级的时候需要把官方基础ABP的DLL进行全面的更新,以及对应的引用DLL也同步更新才行。不过在升级过程中还是很多奇奇怪怪的问题,本篇随笔针对出现的情况进行一系列的总结,以便后面有一个对照参考吧。

1、最新案例源码和NugGet程序包更新

ABP官方的基础模块更新速度还是很快的,一段时间过去,就跳过了几个版本号,我是在旧版本的基础上进行手动的NugGet更新,但是基于VS的Nugget总是更新卡顿,不知不觉就没有反应了,严重影响开发的效率。因此先从官方下载的Demo案例中把相关部分源码进行更新。

官方的案例源码下载地址是:
https://aspnetboilerplate.com/Templates

1)最新案例源码结构和部分内容调整

我们从其中下载对应的源码,然后根据项目结构中的对应源码文件,使用Beyond Compare对比文件进行文件逐一对比,原则上除了个人扩展的部分,都以官方的源码做法为准即可。

目前ABP官方最新的DLL版本是5.3.0,可以下载的Demo版本是5.2.0,它们应该差别不大。下载下来的Aspnet-core部分的源码结构如下所示。

而我们的ABP框架是在这个基础上进行一定的结构优化,以更加方便快速的开发,以及结合代码生成工具进行快速的使用。

我们的VS项目结构 如下所示。

以上是VS里面解决方案的项目结构,我根据项目之间的关系,整理了一个架构的图形,如下所示。

上图中,其中橘红色部分就是我们为各个层添加的类或者接口,分层上的序号是我们需要逐步处理的内容。

应用服务层是整个ABP框架的灵魂所在,对内协同仓储对象实现数据的处理,对外配合Web.Core、Web.Host项目提供Web API的服务,而Web.Core、Web.Host项目几乎不需要进行修改,因此应用服务层就是一个非常关键的部分,需要考虑对用户登录的验证、接口权限的认证、以及对审计日志的记录处理,以及异常的跟踪和传递,基本上应用服务层就是一个大内总管的角色,重要性不言而喻。

回顾了解一下我们改造过的ABP开发框架的结构后,我们返回到版本升级的主体上来介绍。

目前我把VS的版本升级到最新,其.net framework支持4.8, 并单独安装了dotnetcore最新版本3.1,因此环境是最新的,而基础的ABP 5.3.0也是采用了.net core3.1。

对比源码,我们可以发现,Web.Host和Web.Core项目里面已经有所差异,IHostingEnvironment已经被抛弃使用,而采用dotnetcore最新对象IWebHostEnvironment来替代了。

替换最新源码为

因此Web.Host项目中的Module类也进行了调整。

相对应的Web.Core项目里面的Module也同时进行调整了。

2)NugGet程序包更新

Nugget程序包的更新,原则上可以选择单个项目进行更新,或者选择整个解决方案进行程序包的更新,前者可能相应速度快一些,后者由于解决方案项目数量问题,可能会较慢。

我早期的基础ABP版本是4.9,因此想一次性整个的解决方案的程序包进行更新,不过尝试多次,花了几个小时,都无法顺利进行项目的全部更新,于是单个项目进行更新,也非常慢。

于是也通过推荐采用Nugget最新地址进行更新,如下设置更新源。

在程序包源中添加:
https://api.nuget.org/v3/index.json

响应速度相对快了一些,没有不经常的出问题了。

我们如果需要单独更新某个项目的程序包,那么需要选择项目,选择【管理Nugget程序包】进入界面更新即可。如下界面所示。

一般情况下,我们推荐对整个解决方案进行全面的程序包更新,如下选择解决方案,然后进入对应Nugget程序包管理界面更新即可。

这样的全部更新解决方案的程序包,如果能够顺利完成,那是皆大欢喜,不过可能会稍微慢一些。如果不行,只有逐个更新程序包了。

我之前选择这样的方式更新的时候,总是有一两个程序包更新出错,因此只有使用npm 控制台进行单独的升级了。

2、ABP框架基类封装接口命名调整

在更新ABP基础模块的时候,发现ABP的基础接口全部调整了命名,如原来的Get变为GetAsync,GetAll变为 GetAllAsync,Delete变为了DeleteAsync,Update变为了UpdateAsync,Create变为了CreateAsync。

也就是说,他们全部采用了异步名称的命名规则,在异步方法后面全部加上了Async作为标识。

我在之前模块介绍过ABP框架的基础接口。IAsyncCrudAppService定义了几个通用的创建、更新、删除、获取单个对象和获取所有对象列表的接口,接口定义如下所示。

namespaceAbp.Application.Services
{
public interface IAsyncCrudAppService<TEntityDto, TPrimaryKey, in TGetAllInput, in TCreateInput, in TUpdateInput, in TGetInput, in TDeleteInput>: IApplicationService, ITransientDependencywhere TEntityDto : IEntityDto<TPrimaryKey> where TUpdateInput : IEntityDto<TPrimaryKey> where TGetInput : IEntityDto<TPrimaryKey> where TDeleteInput : IEntityDto<TPrimaryKey>{
Task
<TEntityDto>Create(TCreateInput input);
Task Delete(TDeleteInput input);
Task
<TEntityDto>Get(TGetInput input);
Task
<PagedResultDto<TEntityDto>>GetAll(TGetAllInput input);
Task
<TEntityDto>Update(TUpdateInput input);
}
}

现在这些接口全部调整如下所示了。

namespaceAbp.Application.Services
{
public interface IAsyncCrudAppService<TEntityDto, TPrimaryKey, in TGetAllInput, in TCreateInput, in TUpdateInput, in TGetInput, in TDeleteInput>: IApplicationService, ITransientDependencywhere TEntityDto : IEntityDto<TPrimaryKey> where TUpdateInput : IEntityDto<TPrimaryKey> where TGetInput : IEntityDto<TPrimaryKey> where TDeleteInput : IEntityDto<TPrimaryKey>{
Task
<TEntityDto>CreateAsync(TCreateInput input);
Task DeleteAsync(TDeleteInput input);
Task
<PagedResultDto<TEntityDto>>GetAllAsync(TGetAllInput input);
Task
<TEntityDto>GetAsync(TGetInput input);
Task
<TEntityDto>UpdateAsync(TUpdateInput input);
}
}

那么这些我们都必须随着ABP框架的调整也同时进行接口和对应类实现的调整了。

例如对应的异步ApplicationService服务的基类封装,我们也需要调整对应的异步接口实现。

针对远程调用的ApiCaller接口和实现,我们也需要进行命名方面的统一调整,这样才能顺利进行异步接口的调用

我在之前随笔《
ABP开发框架前后端开发系列---(10)Web API调用类的简化处理
》有针对API调用层的简化处理做了说明,我们所有的API调用,基本都是通过统一的一个函数进行调用的。

统一调用处理的方法名称是DoActionAsync。

虽然由于前面介绍了应用服务层的接口很多接口增加了Async的后缀字符,但是客户端通过URL调用,这个Async是不需要的,也就是需要移除,我们之前组装的URL地址是根据函数名称,那么函数名称需要移除这个后缀的Async字样了。

这样系统就顺利跑起来了。服务端界面如下所示。

我们启动Winform客户端,界面如下所示。

登陆启动后主体界面如下所示

在微信开发H5页面的时候,往往借助于WeUI或者Jquery WeUI等基础上进行界面效果的开发,由于本人喜欢在Asp.net的Web界面上使用JQuery,因此比较倾向于使用 jQuery WeUI,本篇随笔结合官方案例和自己的项目实际开发过程的经验总结,对在H5页面开发过程中设计到的界面控件进行逐一的分析和总结,以期能够给大家在H5页面开发过程中提供有用的参考。

1、资源参考

WeUI :
https://github.com/Tencent/weui

WeUI for work:
WeUI for Work (企业微信版)

jQuery WeUI:
http://jqweui.com/

一般情况下,我们使用后者Jquery WeUI,在结合一些用于页面的图标和H5页面模板就可以快速开发相关的页面效果了。



2、界面控件的使用

1)文本控件的使用

文本控件是我们实际项目使用最多的界面控件了,文本控件一般包括常规的Input单文本框类型和TextArea多文本框类型。

文本框的录入,一般可以把提示标题放在左侧,如有后缀信息放在右侧,如下所示。

它的定义HTML代码,主要就是设置好布局和样式即可。

<inputid="Title"name="Title"class="weui-input"type="text"placeholder="流程标题"required>

完整加上布局的HTML代码如下所示。

<divclass="weui-cells weui-cells_form">
    <divclass="weui-cell">
        <divclass="weui-cell__hd"><labelclass="weui-label">流程标题</label></div>
        <divclass="weui-cell__bd">
            <inputid="Title"name="Title"class="weui-input"type="text"placeholder="流程标题"required>
        </div>
    </div>
    <divclass="weui-cell">
        <divclass="weui-cell__hd"><labelclass="weui-label">付款事由</label></div>
        <divclass="weui-cell__bd">
            <inputid="Reason"name="Reason"class="weui-input"type="text"placeholder="付款事由"required>
        </div>
    </div>
    <divclass="weui-cell weui-cell_vcode">
        <divclass="weui-cell__hd"><labelclass="weui-label">付款金额</label></div>
        <divclass="weui-cell__bd">
            <inputid="PayAmount"name="PayAmount"type="number"class="weui-input"placeholder="付款金额"required/>
        </div>
        <divclass="weui-cell__ft"><spanclass="weui-vcode-btn"></span></div>
    </div>

其中 weui-cells 是定义单元块的开始,而且 weui-cell 则是一行的开始,而weui-cell__ft 样式则是定义输入信息的后缀部分,如元、岁等单位可以放到后面。其他控件的布局也是遵循这样的方式处理。

使用控件最基本,也是最重要的就是如何通过JS获取和设置对应控件的值了。

文本框是比较简单的,也遵循通用的做法,获取值的JS代码如下所示:

var Title = $("#Title").val();

如果我们需要判断值是否为空,来在保存数据之前提示用户,那么可以利用如下JS代码。

            var Title = $("#Title").val();if (!Title || Title == '') {
$.toast(
'表单标题不能为空', "forbidden");
$(
"#Title").focus();return false;
}

我们如果需要统一对相关控件进行校验,可以编写一个函数来进行统一的校验提示处理。

        //检查界面的输入是否符合要求
        functionCheckData() {var Title = $("#Title").val();if (!Title || Title == '') {
$.toast(
'表单标题不能为空', "forbidden");
$(
"#Title").focus();return false;
}
var Reason = $("#Reason").val();if (!Reason || Reason == '') {
$.toast(
'付款事由不能为空', "forbidden");
$(
"#Reason").focus();return false;
}
var PayAmount = $("#PayAmount").val();if (!PayAmount || PayAmount == '') {
$.toast(
'付款金额不能为空', "forbidden");
$(
"#PayAmount").focus();return false;
}
return true;
}
//保存表单 functionSave() {if (!CheckData()) {return;
}
var flowUser = $("#FlowUser").val();if (!flowUser || flowUser == '') {
$.toast(
'请选定流程处理人', "forbidden");
$(
"#FlowUser").focus();return;
}
//提交信息和上传图片 submitCallback();
}

如果我们需要通过JS进行控件的赋值如下所示。

$("#Title").val(info.Title);//申请单标题

如果我们可以通过Ajax获取对应数据后,把数据赋值到控件显示在界面上的操作如下所示。

//如果是从已有申请单过来,则加载信息
$.getJSON("/Qyh5Flow/FindApplyById?id=" + applyid, function(info) {if (info != null) {
$(
"#Title").val(info.Title);//申请单标题 $("#Apply_ID").val(info.ID);//修改为已有ID }
});

这个就是我们常规对控件的取值和赋值操作,大多数控件处理和这个类似,因此本小节会着重介绍常规的处理方式。

2、多文本框控件和
文本框计数处理

对于多文本控件,一般在界面上,我们通过如下代码定义。和常规单文本的样式不同,这里使用 weui-textarea 样式。

<textareaid="Note"name="Note"class="weui-textarea"placeholder="备注信息"rows="3"style="height:100px"></textarea>

文本框的赋值、取值和普通单文本控件一样,如下所示。

var note = $("#Note").val(); //获取文本框的值

或者赋值处理

$("#Note").val(info.Note);//对多文本控件进行赋值

对于多文本框控件,我们一般会在文本框的基础上增加一个字符计数的处理,如下界面所示。

这部分的HTML定义代码如下所示。

<divclass="weui-cells__title">备注信息</div>
<divclass="weui-cells">
    <divclass="weui-cell">
        <divclass="weui-cell__bd">
            <textareaid="Note"name="Note"class="weui-textarea"placeholder="备注信息"rows="3"style="height:100px"></textarea>
            <divclass="weui-textarea-counter"><span>0</span>/250</div>
        </div>
    </div>
</div>

我们注意,在其下面增加了一个样式为 weui-textarea-counter 的层,这个我们没有定义任何ID,我们准备通过这个样式来定位并显示多文本框的字符信息的。

这个weui-textarea-counter DIV层是在文本框控件的父DIV层下的一个 class="weui-textarea-counter" 的DIV,因此我们可以通过DOM的查询查找进行处理,避免通过ID定位的方式。

在页面初始化的时候,我们绑定一个对这个文本框的 input propertychange 事件,以便在文本框内容变化的时候,实时进行文本框的计数处理,如下JS代码所示。

//绑定字数动态显示
$('#Note').bind('input propertychange', function() {var fizeNum = $(this).val().length;//获取输入字数
    if (fizeNum > 250) {//如果大于250
        var char = $(this).val();char = char.substr(0, 250);//截取字符串(前200)
        $(this).val(char);
fizeNum
= 250;
}
var countText = `${fizeNum}/250`; $(this).parent().find('.weui-textarea-counter').html(countText);
});

文本框内容变化的时候,我们通过$(this).parent().find('.weui-textarea-counter') 这样的DOM元素查找的方式就可以找到对应的文本框计数的层,然后设置它的HTML或者文本进行显示即可。

对于通过样式来定位某个控件,是我们常规一种较为灵活的做法,后面也有很多这样的处理。

如我们需要寻找某个DIV层下面的某个对应样式的元素,我们也可以用同样的方式,如下代码所示。

$("#DispatchReadAction .promotion-sku .sku-default").addClass("active");
$(
"#DispatchReadAction .promotion-sku li").unbind("click");//解除上次的绑定操作 $("#DispatchReadAction .promotion-sku li").click(function() {//单击事件处理代码........... });

3)下拉列表控件(单选和多选)

下拉列表控件,一般用于绑定多个选项,以便从中选择一个或者多个值。这些列表值,有固定的,也有动态从数据库里面获取的,动态的一般是常规的处理。

单选和多选的下拉列表的处理都差不多,只是设置属性不同罢了。

我们先来看看默认的单选的下拉列表控件的效果,官方提供的案例如下所示。

初始化界面的HTML和常规的文本框样式一样。

      <divclass="weui-cell">
        <divclass="weui-cell__hd"><labelfor="name"class="weui-label">职业</label></div>
        <divclass="weui-cell__bd">
          <inputclass="weui-input"id="job"type="text"value="">
        </div>
      </div>

不过JS代码就不一样,JS初始化下拉列表的代码如下所示。

$("#job").select({
title:
"选择职业",
items: [
"法官", "医生", "猎人", "学生", "记者", "其他"]
});

以上是固定的列表,一般情况下,我们需要根据数据库信息,动态进行绑定下拉列表的。

例如对于付款方式,如果我们使用固定列表的方式,那么JS初始化代码如下所示。

var payType = ["银行卡", "现金", "微信"];
$(
"#PayType").select({
title:
"选择支付方式",
items: payType,
//onChange: onChangeDept, //onClose: onClose, //onOpen: onOpen });

如果是采用数据库字典的方式,我们可以通过一个自定义函数进行绑定即可。

BindDictItem("PayType", "付款方式");//绑定字典

这个BindDictItem是我们定义的一个通用的字典获取方式,通过字典类型绑定下拉列表的值,这里主要使用了Ajax方式请求数据,然后遍历数据进行添加到集合里面,进行界面控件的绑定即可。

如果我们想更灵活的处理,也可以自定义一个函数进行处理,如下代码所示,一样的效果。

//选择流程处理人
var flowUsers =[];var url = '/qyh5flow/GetCorpUser';
$.getJSON(url,
function(data) {
$.each(data,
function(i, item) {
flowUsers.push({
title: item.name, value: item.userid
});
});
$(
"#FlowUser").select({
title:
"选择流程处理人",
items: flowUsers,
});
});

我们注意到,绑定的列表里面,每项都有一个title和value的定义,这个是文本和值的两个属性,我们一般在界面上显示的是文本属性,如果需要获取后面的值(一般为某个内容的ID),那么我们需要如何处理才能获取到呢。

如果我们通过常规的获取内容方式获取,如下所示。

var flowUser = $("#FlowUser").val(); //获取列表的文本

那么你会发现获取的是列表的文本值,而不是我们需要获取到的ID值,那么如果我们需要获取ID值,我们可以用以下属性获取即可。

var flowuserId = $("#FlowUser").attr("data-values");//获取列表的值

默认情况下,列表的值是单选的,如果我们需要设置多选,只需要初始化列表控件的时候,指定为multi为true即可。

例如,我们在一个流程中,如果需要会签,那么需要选定多个会签的流程处理人,如下所示。

那么就需要设置可以多选人员了

//流程用户
var flowUsers =[];var url = '/qyh5flow/GetCorpUser';
$.getJSON(url,
function(data) {
$.each(data,
function(i, item) {
flowUsers.push({
title: item.name, value: item.userid
});
});
$(
"#txtSelectSignUser").select({
title:
"选择会签人员",
items: flowUsers,
multi:
true,//min: 2, //max: 3, });
});

4)日期控件

日期控件也是非常常见的一个信息录入的控件,一般我们处理某些表单都需要默认一个日期,可以让用户进行修改设置的。

JQuery WeUI提供了一个Calender和datetimePicker两个不同的控件,两者针对不同的场合,不过一般来说,我倾向于使用datetimePicker,这个选择日期起来更加方便。

我们先来看看Calender的日期控件。

它的定义和常规的处理差不多,如HTML代码如下所示。

      <divclass="weui-cell">
        <divclass="weui-cell__hd"><labelfor="date"class="weui-label">日期</label></div>
        <divclass="weui-cell__bd">
          <inputclass="weui-input"id="date"type="text">
        </div>
      </div>

它的JS初始化代码如下所示。

      $("#date").calendar({
onChange:
function(p, values, displayValues) {
console.log(values, displayValues);
}
});
var currentDate = '2020-03-22';
console.log(currentDate);

$(
"#date2").calendar({
value: [currentDate],
dateFormat:
'yyyy年mm月dd日' //自定义格式的时候,不能通过 input 的value属性赋值 '2016年12月12日' 来定义初始值,这样会导致无法解析初始值而报错。只能通过js中设置 value 的形式来赋值,并且需要按标准形式赋值(yyyy-mm-dd 或者时间戳) });
$(
"#date2").val(currentDate);

而对于datetimePicker控件,这也是提供了多种显示的效果。

例如对于我们常用的审批单,有时候也需要选择默认当前的日期,让用户进行修改调整的。

datetimePicker控件的赋值操作和Calender不太一样,除了设置通过val()函数来设置值,还需要设置各个列的值,否则显示不正常的,datetimePicker控件控件说到底还是一个picker的选择列表控件。

例如对于上面界面的付款日期,我们的初始化代码如下所示。

//付款日期设置
$("#PayDate").datetimePicker({
title:
'付款日期',
times:
function () { return[]; },
parse:
function(str) {return str.split("-");
},
onChange:
function(picker, values, displayValues) {
console.log(values);
}
});
var payDate = GetCurrentDate();//获取当前日期 $("#PayDate").val(payDate);//设置显示值//拆开日期年月日进行设置日期的各个列的显示 var b = payDate.split("-");
$(
"#PayDate").picker("setValue", [b[0], b[1], b[2]]);

其中 GetCurrentDate 函数是我们常用来获取当然日期时间的一个函数,贴出来供参考吧。

//获取日期获取日期+时间的字符串
functionGetCurrentDate(hasTime) {var d = newDate();var year =d.getFullYear();var month = change(d.getMonth() + 1);var day =change(d.getDate());var hour =change(d.getHours());var minute =change(d.getMinutes());var second =change(d.getSeconds());functionchange(t) {if (t < 10) {return "0" +t;
}
else{returnt;
}
}
var time = year + '-' + month + '-' +day;if(hasTime) {
time
+= ' ' + hour + ':' + minute + ':' +second;
}
returntime;
}

5)
省市区级联处理

省市区级联操作,严格意义上来说也是一个类似日期datetimePicker控件的选择列表,也是提供了多列操作。

地址选择器需要引入额外的JS文件:

<scripttype="text/javascript"src="js/city-picker.js"charset="utf-8"></script>

省市区的地址选择控件,可以通过设置属性
showDistrict 来开启是否显示区的列。

HTML代码定义代码和其他控件类似。

<divclass="weui-cell">
    <divclass="weui-cell__hd"><labelclass="weui-label">籍贯</label></div>
    <divclass="weui-cell__bd">
        <inputclass="weui-input"id="NativePlace"type="text"placeholder="籍贯">
    </div>
</div>

JS的初始化代码如下所示。

$("#NativePlace").cityPicker({
title:
"选择籍贯",
showDistrict:
false,
onChange:
function(picker, values, displayValues) {//console.log(values, displayValues); }
});

6)
单选框和复选框

单选框和复选框都是常规的选择出来,在WeUI里面也定义了这两个控件的基本样式,一般情况下,参考使用即可。

以上定义代码如下所示。

    <divclass="weui-cells__title">单选列表项</div>
    <divclass="weui-cells weui-cells_radio">
      <labelclass="weui-cell weui-check__label"for="x11">
        <divclass="weui-cell__bd">
          <p>cell standard</p>
        </div>
        <divclass="weui-cell__ft">
          <inputtype="radio"class="weui-check"name="radio1"id="x11">
          <spanclass="weui-icon-checked"></span>
        </div>
      </label>
      <labelclass="weui-cell weui-check__label"for="x12">

        <divclass="weui-cell__bd">
          <p>cell standard</p>
        </div>
        <divclass="weui-cell__ft">
          <inputtype="radio"name="radio1"class="weui-check"id="x12"checked="checked">
          <spanclass="weui-icon-checked"></span>
        </div>
      </label>
      <ahref="javascript:void(0);"class="weui-cell weui-cell_link">
        <divclass="weui-cell__bd">添加更多</div>
      </a>
    </div>
    <divclass="weui-cells__title">复选列表项</div>
    <divclass="weui-cells weui-cells_checkbox">
      <labelclass="weui-cell weui-check__label"for="s11">
        <divclass="weui-cell__hd">
          <inputtype="checkbox"class="weui-check"name="checkbox1"id="s11"checked="checked">
          <iclass="weui-icon-checked"></i>
        </div>
        <divclass="weui-cell__bd">
          <p>standard is dealt for u.</p>
        </div>
      </label>
      <labelclass="weui-cell weui-check__label"for="s12">
        <divclass="weui-cell__hd">
          <inputtype="checkbox"name="checkbox1"class="weui-check"id="s12">
          <iclass="weui-icon-checked"></i>
        </div>
        <divclass="weui-cell__bd">
          <p>standard is dealicient for u.</p>
        </div>
      </label>
      <ahref="javascript:void(0);"class="weui-cell weui-cell_link">
        <divclass="weui-cell__bd">添加更多</div>
      </a>
    </div>

单选框和复选框,主要就是在块上定义的样式不同,weui-cells_radio 和 weui-cells_checkbox的差异。

或者使用类似开关的界面效果。

这个定义代码如下所示。

    <divclass="weui-cells__title">开关</div>
    <divclass="weui-cells weui-cells_form">
      <divclass="weui-cell weui-cell_switch">
        <divclass="weui-cell__bd">标题文字</div>
        <divclass="weui-cell__ft">
          <inputclass="weui-switch"type="checkbox">
        </div>
      </div>
      <divclass="weui-cell weui-cell_switch">
        <divclass="weui-cell__bd">兼容IE Edge的版本</div>
        <divclass="weui-cell__ft">
          <labelfor="switchCP"class="weui-switch-cp">
            <inputid="switchCP"class="weui-switch-cp__input"type="checkbox"checked="checked">
            <divclass="weui-switch-cp__box"></div>
          </label>
        </div>
      </div>
    </div>

这个样式weui-cell_switch 和weui-switch也就是定义开关的样式的。

除了上面标准的这两个,我们在购物或者流程申请单需要选择选项的时候,都涉及到多个选择选择其一的情景,WeUI里面的单选框占用界面太多内容,不是很理想,应该采用一种更为紧凑的方式进行选择。

类似下面的效果,才是我们实际经常使用到的选择场景。

仿照这个,我们在流程申请单的时候,选择审批意见的时候,会更加美观。

那么以上的代码应该如何处理呢,我们先看看HTML定义代码

<divclass="wy-media-box2 weui-media-box_text"style="margin:0;">
    <divclass="weui-media-box_appmsg">
        <divclass="wy-media-box__title"><spanclass="promotion-label-tit">【审批意见】</span></div>
        <divclass="weui-media-box__bd">
            <divclass="promotion-sku clear">
                <ul>
                    <liclass="sku-default"><ahref="javascript:;">批准申请</a></li>
                    <li><ahref="javascript:;">退回拟稿人重新处理</a></li>
                    <li><ahref="javascript:;">退回上一步处理</a></li>
                </ul>
            </div>
        </div>
    </div>
</div>

这里我们为了操作灵活,采用了样式定义的方式进行处理,这个我在多文本的计数器的时候就介绍过,在这个样式单击的时候,我们进行状态的切换即可实现。

我们看看对应 li 元素单击的时候,触发的JS代码如下所示(由于我们是在DIV层弹出的时候才处理,避免重复绑定,使用前需解除绑定事件:unbind("click") )。

//选项变化
$("#DispatchReadAction .promotion-sku .sku-default").addClass("active");
$(
"#DispatchReadAction .promotion-sku li").unbind("click");//解除上次的绑定操作 $("#DispatchReadAction .promotion-sku li").click(function() {
$(
this).addClass("active").siblings("li").removeClass("active");var action = $(this).text();
console.log(action);
//测试 //console.log($("#DispatchSignAction .promotion-sku .active").text());//测试选中值 //................ });

其中 #DispatchReadAction 是DIV层的定位方式 .promotion-sku .sku-default 和 .promotion-sku li 是样式的过滤选择,以方便选中对应的元素进行操作;而siblings 的操作是遍历处理,类似$.each的处理方式。

这个操作,主要就是移除所有选中的(样式active)的状态,然后重新设置当前选中的元素为选中(样式active)。

这个active样式的选中颜色、以及效果,我们通过样式进行设置好即可。

这样就可以实现之前说的效果了

而我们如果需要获取选中的选项的内容,那么通过样式的选择器就可以获得的了

var action = $("#DispatchSignAction .promotion-sku .active").text();

6)
图片上传和预览

在实际项目上传表单的时候,往往都需要使用附件上传,一般情况下是上传图片文件较多,而图片往往也需要预览进行管理,如果是维护已有记录的编辑界面,还应该可以实现对已有图片的删除操作。

那么就这里涉及到几个问题:图片上传、图片预览、图片删除的几个操作。

例如我们在流程表单里面,需要上传附件图片的界面如下所示。

单击+符号,可以继续上传多个图片,而选择图片就马上进行本地的图片缩略图预览操作,而单击图片缩略图可以实现图片的大图预览,如下所示。

下角有垃圾箱的图片,是用来删除图片的(如果已经上传的,需要删除数据库,如果是刚添加的,则移除本地集合即可)。

那么这样的处理效果,是如何实现的呢,这些同样分为界面的定义和JS代码的处理操作,上传文件还涉及到服务端后台代码对附件的保存处理操作。

图片预览层我们在HTML放入如下定义。

    <!--图片预览层-->
    <divclass="weui-gallery"id="gallery">
        <spanclass="weui-gallery__img"id="galleryImg"style="width:auto"></span>
        <divclass="weui-gallery__opr">
            <ahref="javascript:"class="weui-gallery__del">
                <iclass="weui-icon-delete weui-icon_gallery-delete"></i>
            </a>
        </div>
    </div>

常规的图片附件界面HTML代码如下所示。

<divclass="weui-cells__title">附件</div>
<divclass="weui-cells weui-cells_form">
    <divclass="weui-cell">
        <divclass="weui-cell__bd weui-cell_primary">
            <divclass="weui-uploader">
                <!--预览图片的列表-->
                <ulclass="weui-uploader__files"id="imgAttach">
                </ul>
                <divclass="weui-uploader__input-box">
                    <!--上传图片附件控件-->
                    <inputid="uploaderAttach"class="weui-uploader__input"type="file"accept="image/*"multiple="">
                </div>
            </div>
        </div>
    </div>
</div>

我们在上传图片的附近,放置了一个ul的界面元素,用来承载图片文件缩略图显示的,定义ID为 imgAttach。

下面的JS代码就是常规的图片上传展示缩略图、单击缩略图预览图片、删除图片处理操作等内容,一般情况下,我们直接复制到HTML界面里面即可。

    <script>$(function() {//初始化
initImage();//初始化图片删除操作
});//存放文件图片的集合
        varfileAttach= newArray();functioninitImage() {//预览缩略图模板
            vartmpl= '<li class="weui-uploader__file" style="background-image:url(#url#)"></li>',
$gallery
=$("#gallery"),
$galleryImg
=$("#galleryImg"),

$uploaderAttach
=$("#uploaderAttach"),
$imgAttach
=$("#imgAttach");//图片上传,加入缩略图标 $uploaderAttach.on("change",function(e) {varsrc, url=window.URL||window.webkitURL||window.mozURL,
files
=e.target.files;for(vari= 0, len=files.length; i<len;++i) {varfile=files[i];
fileAttach.push(file);
//加入集合 if(url) {
src
=url.createObjectURL(file);
}
else{
src
=e.target.result;
}
$imgAttach.append($(tmpl.replace(
'#url#', src)));
}
});
varindex;//第几张图片 varcategory;//那个类别 varimgid;//图片ID //图片缩略图元素单击,展示图片预览视图 $imgAttach.on("click","li",function() {
index
=$(this).index();
category
= "payment";
imgid
=$(this).attr("id");
$galleryImg.attr(
"style",this.getAttribute("style"));
$gallery.fadeIn(
100);
});
//预览界面单击退出 $gallery.on("click",function() {
$gallery.fadeOut(
100);
});
//删除图片(根据类别和序号处理) $(".weui-gallery__del").click(function() {
console.log(index
+ ',' +category+ ',' +imgid);//记录显示 //如果是在服务端的图片,确认后移除 if(imgid!=undefined&&imgid!= '') {
$.confirm(
"您确定要永久删除该图片吗?","永久删除?",function() {varurl= "/QYH5/DeleteAttachment";varpostData={
id: imgid.replace(
/img_/,'')//控件id去掉前缀为真正附件ID };

$.post(url, postData,
function(json) {//转义JSON为对象 vardata=$.parseJSON(json);if(data.Success) {
$.toptip(
"删除成功!",'success');//在界面上找到对应控件ID,移除控件 RemoveImg();
}
else{
$.toast(
"操作失败:" +data.ErrorMessage,"forbidden");
}
});
});
}
else{
RemoveImg();
//普通图片快速移除 };
});
//移除对应类别的图片 functionRemoveImg() {if(category== "payment") {
$imgAttach.find(
"li").eq(index).remove();
fileSick.splice(index,
1);
}
else{
console.log(
"没有对应类型");//$imgPres.find("li").eq(index).remove(); //filePres.splice(index, 1); }
};
};
</script>

而上面代码主要处理新增内容的情况下的图片处理,一般情况下,还需要包括编辑现有记录的情况下图片的预览情况,对于现有的图片列表,我们从数据库里面列出,然后展示在缩略图即可。

如下JS代码就是处理编辑情况下的图片缩略图绑定,主要注意,我们这里定义了一个id的规则,和附件在后台数据库里面对应的ID保持一定的规则即可。

//动态处理图片绑定
$.getJSON("/Qyh5Flow/GetImageList2?attachGuid=" + info.AttachGUID, function(data) {
$.each(data,
function(i, item) {
$(
"#imgAttach").append(`<img class='weui-uploader__file' src='${item.Text}' id='img_${item.Value}'/>`); });
});

这个ID,在删除后台附件的时候,我们需要解析出对应的ID,如下是对ID的处理。

var postData ={
id: imgid.replace(
/img_/, '') //控件id去掉前缀为真正附件ID };

我们的图片附件,一般情况下随着表单其他内容一并提交即可,但是由于常规的文本内容和附件内容一并处理,那么我们需要引入FormData来存储复杂对象,从而可以在后台能够接收到各种参数和附件文件信息。

//提交信息和上传图片
functionsubmitCallback() {var formData = new FormData();//构建一个FormData存储复杂对象
    formData.append("ID", $("#ID").val());
formData.append(
"Reason", $("#Reason").val());//.........省略其他内容 //加入附件图片 for (var i = 0; i < fileAttach.length; i++){
formData.append(
"attach_" +i, fileAttach[i]);
};

其中的 fileAttach 的文件集合就是我们加入的文件集合。

有了这些内容,我们就统一处理保存所有的界面录入文本和附件信息了。

//提交表单数据和文件
var url = "/qyH5Flow/PaymentSave?userid=@ViewBag.userid";
$.ajax({
url: url,
type:
'post',
processData:
false,
contentType:
false,
data: formData,
success:
function(json) {//转义JSON为对象 var data =$.parseJSON(json);if(data.Success) {
$.toast(
"保存成功,稍后请到列表界面查看。");
}
else{
$.toast(
"保存失败:" + data.ErrorMessage, "forbidden");
}
}
});

由于使用了混合复杂对象的FormData,我们异步提交的时候,需要设置 processData: false 来处理。

以上就是前端HTML+JS的界面代码部分,要完成附件的上传,还需要后台代码进行附件的处理。

附件保存我们统一采用一个独立的函数处理。

其实后台就是只需要通过Request.Files 的对象即可获得,然后通过我们数据库和文件的处理,就可以实现附件的上传处理了,详细的我们这里就不再赘述。

由于篇幅原因,我们下一篇随笔继续介绍微信开发H5页面控件的各种操作。