2023年2月

在我们实际项目开发过程中,往往需要根据实际情况,对组件进行封装,以更简便的在界面代码中使用,在实际的前端应用中,适当的组件封装,可以减少很多重复的界面代码,并且能够非常简便的使用,本篇随笔介绍基于ElementPlus的上传组件进行封装。

1、El-Upload上传组件的使用场景及数据库设计

在官网地址
https://element-plus.org/zh-CN/component/upload.html
上有关于该组件的详细使用代码案例。

大概有个场景我需要根据需要展示文件的,一个是文件展示方式(非图片格式)。

一种是肖像方式处理。

一种方式是图片缩略图列表方式。

还有就是支持拖动方式上传。

我目前大概就是想到应用这几种方式,细节的地方可以根据需要进行微调即可。

一般来说,我们附件信息是单独存储在一个表里面的,附件则是存储在相应的文件系统或者FTP目录中。如果需要了解后端不同方式的文件上传方式,可以了解随笔《
基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传

而实际我们业务表里面,只需要存在一个字符串字段,它是一串GUID的值,用于引用单个或者多个附件列表的AttahmentGUID值即可。

在前端界面使用的的时候,我则希望自定义的组件使用的时候,尽量简单,因此对于附件上传的URL地址、Upload组件的相关设置,应该屏蔽在自定义组件即可,组件只需要提供一个v-model的绑定值和一些需要的属性即可。

2、文件上传后端处理

在介绍前端上传组件自定义前,我们需要提供一个上传文件的URL地址给前端,接收前端的文件流,以及相关FormData里面的参数值。

后端上传处理的函数定义如下所示

        /// <summary>
        ///多文件上传处理(自动根据配置文件选择合适的上传方式)/// </summary>
        /// <param name="guid">附件组GUID</param>
        /// <param name="folder">指定的上传目录</param>
        /// <returns></returns>
        [RequestSizeLimit(100000000)] //请求正文最大大小100M
[HttpPost]
[Route(
"postupload")]public async Task<List<ResponseFileInfo>> PostUpload()

其中 ResponseFileInfo 是我们定义一个返回给前端使用的对象,记录文件的名称、URL,id信息。

    /// <summary>
    ///上传后文件返回的结果信息/// </summary>
    public classResponseFileInfo
{
/// <summary> ///默认构造函数/// </summary> publicResponseFileInfo() { }/// <summary> ///参数化构造函数/// </summary> /// <param name="id"></param> /// <param name="name"></param> /// <param name="url"></param> public ResponseFileInfo(string id, string name, stringurl)
{
this.Id =id;this.Name =name;this.Url =url;
}
/// <summary> ///记录ID/// </summary> public string Id { get; set; }/// <summary> ///文件名称/// </summary> public string Name { get; set; }/// <summary> ///文件路径/// </summary> public string Url { get; set; }
}

在后端上传处理中,我们可以通过HTTP上下文获得传过来的对应参数和文件列表信息,如下代码所示。

        public async Task<List<ResponseFileInfo>>PostUpload()
{
var httpContext = this.HttpContext;string guid = httpContext.Request.Form["guid"];//input.guid; string folder = httpContext.Request.Form["folder"];//input.folder; var files = httpContext.Request.Form.Files;

最后根据文件信息和相关参数,构建附件数据库信息,以及存储文件就可以了,如下代码所示。

3、文件上传组件的前端封装和处理

对于编辑状态下的前端处理,我们只需要绑定v-model的值,以及指定的一些参数就可以了。

如果是禁止上传的详细展示,可以设置disabled属性为true就可以了

<el-form-itemlabel="资料文档">
     <my-uploadv-model="viewForm.attachGUID":data="{ guid: viewForm.attachGUID, folder: '用户图片' }"disabled/>
 </el-form-item>

用户界面展示如下所示。

通过简单的封装,我们就可以在极少代码的基础上定制自己的功能界面了。易于阅读和维护,同时也有助于我们后面通过代码生成工具的方式快速生成相关的代码。

自定义组件,我们根据需要定义一些属性,并对它设置默认值,如下所示代码。

<scriptsetup lang="ts">import { reactive,  ref,  onMounted,  watch,  computed, getCurrentInstance} from"vue";

import { ElMessage, ElMessageBox } from
"element-plus";
import type { UploadInstance, UploadProps, UploadUserFile } from
"element-plus";
import { Plus, UploadFilled } from
"@element-plus/icons-vue";
import { isNullOrUnDef, isEmpty } from
"/@/utils/is";
import fileupload from
"/@/api/fileupload";

defineOptions({ name:
"MyUpload"});//声明Props的接口类型 interface Props {
modelValue
?: string;//接受外部v-model传入的值,记录的AttachGUID值 data?: Record<string, any>;//上传时附带的额外参数: {a:b} headers?: Headers|Record<string, any>;//设置上传的请求头部 listType?:"text" | "picture" | "picture-card";//文件列表的类型 disabled?:boolean;//是否禁用 multiple?:boolean;//是否支持多选文件 limit?: number;//最大允许上传个数 fileSize?: number;//大小限制(MB) fileType?: Array<string>;//文件类型, 例如['png', 'jpg', 'jpeg'] isShowTip?:boolean;//是否显示提示 showFileList?:boolean;//是否显示文件列表 withCredentials?:boolean;//支持发送 cookie 凭证信息 isAvatarUpload?:boolean;//是否是头像上传 drag?:boolean;//是否启用拖拽上传 buttonText?: string;//按钮文本 }//使用默认值定义Props const props=withDefaults(defineProps<Props>(), {
modelValue:
"",//对应自定义组件的v-model的值 data: ()=>{return null;
},
headers: ()
=>{//用于上传文件的身份认证 return{ Authorization:"Bearer" +getAccessToken() };
},
listType:
"text",//"text" | "picture" | "picture-card" disabled:false,
multiple:
false,//limit: 10, fileSize:2,//MB fileType: ()=>{return["png","jpg","jpeg","gif"];
},
isShowTip:
true,
showFileList:
true,
withCredentials:
true,//支持发送 cookie 凭证信息 isAvatarUpload:false,//头像上传方式 drag:false,//是否启用拖拽上传 buttonText:"单击上传",

});
//定义触发事件 const emit=defineEmits(["error","success","remove","change"]);

在我们展示组件的时候,我们根据v-model绑定的modelValue值进行加载附件列表或者图片信息,

//挂载的时候初始化数据
onMounted(async () =>{if (!isEmpty(props.modelValue)) {//使用字典类型,从服务器请求数据
    await fileupload.GetByAttachGUID(props.modelValue).then(data =>{//console.log(data);
      if (data.length > 0) {var list: Array<UploadUserFile> =[];
data.map(item
=>{
let url
=getUrl(item);
let { id, fileName, fileExtend }
= item; //结构对象属性 list.push({
name: fileName,
url: url,
uid: id
});
});

fileList.value
=list;//如果是头像上传模式,显示最后一个图片 if(props.isAvatarUpload) {
avatarImageUrl.value
= list[0]?.url;
}
}
});
}
});

在预览文件的时候,我们判断,如果是图片文件,就打开窗口展示图片,否则就下载附件即可。

//预览文件
const handlePreview: UploadProps["onPreview"] = uploadFile =>{//console.log(uploadFile);
  //当格式为图片就预览图片,否则下载文件
  let filename =uploadFile.name;
let fileurl
=uploadFile.url;
let fileExtension
= "";//校检文件类型 var imageTypes = ["png", "jpg", "jpeg", "gif"];if (filename.lastIndexOf(".") > -1) {
fileExtension
= filename.slice(filename.lastIndexOf(".") + 1);
}
const isTypeOk
= imageTypes.some(type =>{if (fileExtension && fileExtension.indexOf(type) > -1) return true;return false;
});
if(isTypeOk) {//预览图片 dialogImageUrl.value =fileurl;
dialogTitle.value
=filename;
dialogVisible.value
= true;
}
else{//下载文件 dialogVisible.value = false;//openWindow(fileurl, { target: "_self" }); window.open(fileurl, "_self");
}
};

移除文件的时候,我们根据文件的id,在数据库后端进行删除附件和附件信息。

//移除文件前操作
const beforeRemove: UploadProps["beforeRemove"] = (uploadFile, uploadFiles) =>{//console.log(uploadFile);
  //${uploadFile.name}
  return ElMessageBox.confirm(`确认删除该文件吗?`).then(
async ()
=>{var result = false;var id =uploadFile.uid;if (!isEmpty(id)) {
result
=await fileupload.Delete(id);
}
returnresult;
},
()
=> false);
};

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

在我们开发开发H5程序或者小程序的时候,有时候需要基于内置浏览器或者微信开发者工具进行测试,这个时候可以采用默认的localhost进行访问后端接口,一般来说没什么问题,如果我们需要通过USB基座方式发布到手机进行App测试的时候,那就需要使用局域网的IP地址了,否则就无法访问后端的接口,本篇随笔总结在开发H5程序或者小程序的时候,后端Web API项目在IISExpress调试中使用IP地址和localhost地址并存,便于开发测试。

1、修改VS配置地址文件

默认我们IISExpress是采用localhost进行调试的,我们如果需要使用局域网地址如192.1068.1.*开始的IP端进行调试,那么就需要首先修改VS的配置信息,一般在项目的隐藏文件夹 .vs 中,找到对应项目的解决方案目录下的config目录,如我一个项目是:

打开这个文件,可以看到很多配置信息,定位到binding的配置项目中,找到对应项目的配置信息,如下所示。

<sitename="WebAPI"id="3">
    <applicationpath="/"applicationPool="Clr4IntegratedAppPool">
        <virtualDirectorypath="/"physicalPath="E:\个人应用程序\Web开发框架\Bootstrap开发框架\WebAPI" />
    </application>
    <bindings>
       <bindingprotocol="http"bindingInformation="*:27206:localhost" /> </bindings> </site>

我们可以修改它,增加对应的局域网IP地址的配置信息,如下所示。

<sitename="WebAPI"id="3">
    <applicationpath="/"applicationPool="Clr4IntegratedAppPool">
        <virtualDirectorypath="/"physicalPath="G:\个人应用程序\Web开发框架\Bootstrap开发框架\WebAPI" />
    </application>
    <bindings>
<bindingprotocol="http"bindingInformation="*:27206:192.168.1.103" />
<bindingprotocol="http"bindingInformation="*:27306:localhost" />
</bindings> </site>

这里增加了一个局域网的IP配置项目,同时新建一个本地localhost的端口,因为我们希望保留两个不同的连接,如果只需保留局域网,那么移除其中一个localhost的配置即可。

保存文件就可以继续下一步了。

2、以管理员方式启动VS并进行查看

如果我们需要启动局域网IP的IISExpess端口,一定需要以管理员方式启动VS,否则IISExpess提示出错,无法绑定IP端口的。

然后在C#的Web API项目上,右键出现的菜单中,启动调试或者启动项目即可。

项目顺利启动后,就可以看到有两个不同的端口,一个是局域网IP的,一个是localhost的了。

这样我们在前端项目上,就可以配置基础路径为局域网IP地址了。

baseUrl: 'http://192.168.1.103:27206'



这样我们就可以在HBuilderX中进行扫码测试功能了,或者在微信开发者工具中也可以。

如果通过USB基座发布到手机真机测试也可以正常访问局域网的后端接口数据了。

运行程序可以获得APP的体验效果。

3、后端Web API的CORS跨域处理。

有时候,我们需要在后端的Web API项目上设置CORS的跨域处理,以便于本地调试的访问处理。

如果是常规的Web API项目,我们设置其WebApiConfig文件就可以了,如下所示。

增加CORS的工厂处理,设置相关的参数。

namespaceWebAPI
{
public static classWebApiConfig
{
public static voidRegister(HttpConfiguration config)
{
//Web API 配置和服务//支持CORS//config.EnableCors(); config.SetCorsPolicyProviderFactory(newCorsPolicyFactory());
config.EnableCors();

完整的文件如下所示。

namespaceWebAPI
{
/// <summary> ///JS跨域工厂类/// </summary> public classCorsPolicyFactory : ICorsPolicyProviderFactory
{
ICorsPolicyProvider _provider
= newMyCorsPolicyAttribute();publicICorsPolicyProvider GetCorsPolicyProvider(HttpRequestMessage request)
{
return_provider;
}
}

[AttributeUsage(AttributeTargets.Method
| AttributeTargets.Class, AllowMultiple = false)]public classMyCorsPolicyAttribute : Attribute, ICorsPolicyProvider
{
privateCorsPolicy _policy;publicMyCorsPolicyAttribute()
{
_policy
= newCorsPolicy
{
AllowAnyMethod
= true,
AllowAnyHeader
= true,
SupportsCredentials
= true,
AllowAnyOrigin
= true,//设置所有的都可以 };//Add allowed origins. _policy.Origins.Add("http://localhost:8080");
_policy.Origins.Add(
"http://localhost:8081");
_policy.Origins.Add(
"http://192.168.1.103:8080");
_policy.Origins.Add(
"http://192.168.1.103:8081");//_policy.Origins.Add("http://localhost"); }public Task<CorsPolicy>GetCorsPolicyAsync(HttpRequestMessage request)
{
returnTask.FromResult(_policy);
}
public Task<CorsPolicy>GetCorsPolicyAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
returnTask.FromResult(_policy);
}
}
}

以上就是在开发前后端项目中的一些调试测试细节地方,供参考。

现在的很多程序应用,基本上都是需要多端覆盖,因此基于一个Web API的后端接口,来构建多端应用,如微信、H5、APP、WInForm、BS的Web管理端等都是常见的应用。本篇随笔概括性的介绍基于HBuilderX+UniApp+ThorUI的手机端前端开发处理,总结一下开发工具的设置,以及常见的H5端开发的一些相关知识。

1、前后端分离的后端API架构设计

在我们设计好一个Web  API 的接入层后,Web API根据用户、账号登录后获得相关的令牌,每次访问接口,通过令牌进行识别用户身份,从而提供用户身份相关的数据或者记录用户操作信息。

后端的WebAPI,这里可以是基于我们自己的技术路线,如Asp.net /Asp.net Core 等的.net路线,或者java路线,不管哪种后端技术架构,都是基于对常规业务的分类,横向或者纵向切分业务,并在后端对基础接口的提供进一步的封装处理,以便提供统一的接口接入处理。

后端采用基于Asp.net的Web API技术,可以提供按Area域来管理API的分类,Web API如下架构所示。

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

通过Web API接口返回结果的统一封装处理,我们定义了相关的格式如下所示。

2、Vue 前端H5应用的开发

前端开发,可以是基于Vue&Element的管理后台的前端开发,也可以是Vue + UniApp+手机端组件库的开发H5或者App应用,技术路线都是基于Vue的,我们这里主要介绍UniApp+HBuliderX+Vue+ThorUI来开发H5端的应用。

UniApp
是一个使用
Vue.js
开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台。

使用UniApp的主要原因是它的生态比较完善,而且提供了不同平台的统一的接口调用方法,因此非常方便使用它来统一构建多端应用。

使用UniApp,为了集成方便,一般也会使用HBuilderX开发工具来进行前端的开发。
HBuilderX
编辑器是DCloud全新推出的一款HTML5的Web开发工具。HBuilder目前有两个版本,一个是windows版,一个是mac版。下载的时候根据自己的电脑选择适合自己的版本即可。如果不考虑深度集成的原因,还可以考虑使用更加广泛的VS Code编辑器,毕竟VS Code可是前端界的开发标准工具了。

另外UniApp也有自己的组件库,同时也做的很不错,不过由于参照模板的问题,我们这里使用了会员版的
Thor UI
,可以参考的案例更多,方便做出更好的界面效果。

Thor UI 是一款开发移动端网页应用、小程序的前端 UI 组件库,uni-app 版本还可以编译成为安卓/ iOS APP 应用,特点是轻量、简洁,组件覆盖比较全面,使开发更高效。Thor UI目前有微信小程序原生版本 (opens new window)和uni-app版本。

构建基于Vue的前端项目,我们有一个很重要的部分,就是需要准备好对应Web API的前端JS端类的封装处理,引入了ES6类的概念实现业务基类接口的统一封装,简化代码。

例如常见模块中的权限模块我们涉及到的用户管理、机构管理、角色管理、菜单管理、功能管理等业务类,那么这些类继承BaseApi,就会具有相关的接口了,如下所示继承关系。

其中JS类的BaseApi具有常规的增删改查接口,如下所示。

代码视图如下所示,主要是定义一些通用的操作方法以及常规API接口的操作方法

如前端API模块中,会存在很多类似的JS类,如下图所示。

这样继承的关系,可以尽可能的简化代码的处理,类似类的抽象封装,可以减少大多数的常规代码,我们只需要继承基类就可以获得大类通用的API接口了。

例如,对于常用业务的组织机构类,我们只需要添加一些特殊接口的定义就可以了,其他的会继承自BaseApi的基类方法。

3、注入一些快捷的操作类入口

有时候,为了便于对一些常用类的操作,我们往往会通过Vue.prototype的方式来注入一些操作类入口,便于快捷处理。

import Vue from 'vue'Vue.config.productionTip= falseVue.prototype.http=http
Vue.prototype.tui
=tui
Vue.prototype.$store
= store

或者我们也可以把它独立在一个类文件里面进行引入,这里借鉴了uview项目的做法,把自定义对象$u挂载到uni对象上,便于操作处理。

有了这些快捷的挂载类,我们可以方便的访问一些常用的处理方法。

computed: {
currentUser() {
if (uni.$u.test.isEmpty(this.creator)) {return this.vuex_user?.info?.fullName;
}
else{return this.creator;
}
},
currentDate() {
if (uni.$u.test.isEmpty(this.createTime)) {returnuni.$u.time.timeFormat();
}
else{return uni.$u.time.timeFormat(this.createTime);
}
}
},
checkCode() {var code = this.formData.shopCode;if (uni.$u.test.isEmpty(code)) return;

shop.CheckCodeValid(code).then(res
=>{if(res.success) {this.tui.toast('未被占用, 可以使用!')
}
else{
uni.$u.toast(
"已被占用,请使用其他代码")
}
})
},

4、创建页面组件

我们知道,Vue的强大,其中之一就是它的组件化非常便利,因此我们在设计界面的时候,如果重复出现,尽可能的把它抽取出来作为组件来定义,然后提供相关的Props参数或者处理事件就很好了,而且可以减少主要页面的代码,同时也方便维护组件。

我往往在开发前端项目的时候,会抽取很多共性的地方,作为组件,然后主页面只需要简单的调用即可,随着项目的开发,我们可能抽取更多,完善更好的组件库出来了。

例如对于一个异常信息的处理,我们整合了多个模块的内容进行展示,采用自定义组件的方式,可以减少很多繁杂的前端代码。

上面页面的大部分都是自定义组件的整合使用,如下代码截图所示。

需要使用的组件,在Vue的JS代码中导入组件即可

最后我们来看看其他前端页面的效果,供参考。

登陆界面效果

业务系统的主界面效果

业务字典管理界面

组织机构以树形图的方式进行展示,单击可以查看详细机构信息,或者在下面提供添加组织机构处理。

创建组织机构的界面。

角色管理和角色人员维护

这样通过管理员的权限来处理权限系统的基础信息,也就可以不用在依赖管理前端的操作管理。

现在的很多程序应用,基本上都是需要多端覆盖,因此基于一个Web API的后端接口,来构建多端应用,如微信、H5、APP、WInForm、BS的Web管理端等都是常见的应用。本篇随笔继续分析总结一下项目开发的经验,针对页面组件化开发经验方面进行一些梳理总结,内容包括组件的概念介绍,简单页面组件的抽取开发,以及对控件值进行更改的页面组件的处理等,希望能够对大家有所启发。

1、Vue组件的概念

组件是Vue中的一个重要概念,是一个可以重复使用的Vue实例,它拥有独一无二的组件名称,它可以扩展HTML元素,以组件名称的方式作为自定义的HTML标签。因为组件是可复用的Vue实例,所以它们与new Vue()接收相同的选项,例如data,computed、watch、methods以及生命周期钩子等。

组件是可复用的 Vue 实例, 把一些公共的模块抽取出来,然后写成单独的的工具组件或者页面,在需要的页面中就直接引入即可那么我们可以将其抽出为一个组件进行复用。例如 页面头部、侧边、内容区,尾部,上传图片,等多个页面要用到一样的就可以做成组件,提高了代码的复用率。

Vue的前端界面,对界面内容部分可以根据需要进行适当的拆分,也可以把通用的部分封装为组件进行使用。如果整体界面内容比较多,可以进行拆分,根据内容的展示不同,拆分为各自的组件模块,然后合并使用即可,如下所示。

在对象UML的图例中,应该是如下所示的效果图,组织机构包含组织成员和角色的内容。

在界面上,组织成员还需要添加成员的功能,同理角色也需要添加角色的处理,如下图示。

角色界面模块的内容划分图如下所示。

实际页面组件的开发,也可以按照内容的划分方式进行组件的开发,然后将它们组合起来。

如果我们开发了很多不同业务场景的组件,那么在实际页面中,就可以对它们进行组合使用,提高页面的整洁,同时也便于代码的维护。

组件的拆分和封装,是我们前端开发中非常重要的部分,也是我们快速构建复杂页面功能的,又能轻松应对的必杀技之一。

例如对于一个异常信息的处理,我们整合了多个模块的内容进行展示,采用自定义组件的方式,可以减少很多繁杂的前端代码。

上面页面的大部分都是自定义组件的整合使用,如下代码截图所示。

需要使用的组件,在Vue的JS代码中导入组件即可

2、简单页面组件的抽取开发

例如在一些新录入内容的页面中,我们往往有一些类似人员和时间的信息需要展示,可以把它做成一个简单的页面组件模块

如果用户是创建新记录的,那么显示当前登录的用户名称和当前日期,如果是已有记录,则显示记录中的用户和时间即可,一个很简单的例子。

<!--通用填报信息展示,用于创建新记录或者明细的展示-->
<template>
    <viewclass="">
        <viewclass="tui-order-info">
            <tui-list-cell:hover="false":arrow="showArrow"@click="showDetail">
                <viewclass="tui-order-title">
                    <view>{{title}}</view>
                    <viewclass="tui-order-status":style="{color:subTitleColor}">{{subTitle}}</view>
                </view>
            </tui-list-cell>
            <viewclass="tui-order-content"v-show="visible">
                <viewclass="tui-order-flex">
                    <viewclass="tui-item-title">填报时间:</view>
                    <viewclass="tui-item-content">{{currentDate}}</view>
                    <viewstyle="padding-right: 50rpx;"></view>
                    <viewclass="tui-item-title">填报人:</view>
                    <viewclass="tui-item-content">{{currentUser}}</view>
                </view>
            </view>
        </view>
    </view>
</template>

信息填报的标题中,单击可以切换折叠或者展示模式,通过事件showDetail 进行触发的,而内容这里通过visible属性进行控制,同时接收来时Props属性的detailVisible的初始化设置。

在Props的父传子属性中,我们定义了一些通用数据属性,如标题、副标题、当前时间和填报人等。

<script>exportdefault{
emits: [
'click', 'cancel'],
props: {
title: {
type: String,
default: "信息填报"},
subTitle: {
type: String,
default: ""},
subTitleColor: {
type: String,
default: ""},
showArrow: {
//默认是否展示箭头 type: Boolean,default: false},
creator: {
type: String,
default: ""},
createTime: {
type: String,
default: ""},
detailVisible: {
//默认是否展示详情内容 type: Boolean,default: true},
},

另外,我们在data中添加一个组件属性visbile属性,

data() {return{
visible:
true}
},

这是因为传入的
detailVisible
不能在组件中中修改其属性值,因此接收它的值,并让她赋值给局部的属性,这种做法是常见的处理方式。为了同时保持两个属性的一致,我们通过Created事件中初始化局部变量,并通过Watch监控数据的变化。

created() {this.visible = this.detailVisible;
},
watch: {
detailVisible(val) {
this.visible =val
}
},

当前用户名称和日期,我们判断传递的属性是否为空值,非空则使用传递的值,否则使用当前用户身份和当前日期值,因此我们通过一个计算属性来判断,如下所示。

computed: {
currentUser() {
if (uni.$u.test.isEmpty(this.creator)) {return this.vuex_user?.info?.fullName;
}
else{return this.creator;
}
},
currentDate() {
if (uni.$u.test.isEmpty(this.createTime)) {returnuni.$u.time.timeFormat();
}
else{return uni.$u.time.timeFormat(this.createTime);
}
}
},

这里面的
this.vuex_user?.info?.fullName
是我们通过Vuex的方式存储的一个当前用户身份的信息,通过?来判断是否非空。另外,我们对list-cell的事件进行处理,切换折叠和展开的处理,因此只需要设置局部变量visible的属性即可。

methods: {
showDetail() {
this.visible = !this.visible;
}
}

这样定义好的页面内容后,我们只需要把它加入到页面组件中就可以使用它了

<script>import createinfo from'@/pages/components/createinfo.vue';
export
default{
components: {
createinfo
},

HTML代码和其他简单的HTML常规组件类似。

<createinfo></createinfo>

或者传入数据处理

<createinfo:createTime="entity.createTime":creator="entity.deliverName"></createinfo>

另外,如果我们需要在内部保留一块区域给父页面定义的模板,可以使用Slot进行处理,这样除了添加了人员和日期信息,我们还可以嵌入一个更多内容信息作为区块给组件进行展示了。

<template>
    <viewclass="">
        <viewclass="tui-order-info">
            <tui-list-cell:hover="false":arrow="showArrow"@click="showDetail">
                <viewclass="tui-order-title">
                    <view>{{title}}</view>
                    <viewclass="tui-order-status":style="{color:subTitleColor}">{{subTitle}}</view>
                </view>
            </tui-list-cell>
            <viewclass="tui-order-content"v-show="visible">
               <slot#default>
                    <!--<view class="tui-order-flex">
<view class="tui-item-title">角色编码</view>
<view class="tui-item-content">123456</view>
</view>
--> </slot> </view> </view> </view> </template>

3、对控件值进行更改的页面组件的处理

除了上面的闭合组件,我们有时候也需要一个传递信息和变更v-model的值的处理,或者抛出一些事件给父页面进行通知。

例如,我们在选择一些系统数据字典项目的时候,以及在选择时间的时候,这些是比较常见的操作,我们可以结合我们的组件库,做一些简单的组件封装,从而达到简化页面使用的目的。

如果没有自定义组件的情况,我们使用字典模块的内容,需要请求对应的字典列表,然后绑定在控件Picker中,如下代码所示。

<tui-list-cellarrow padding="0"@click="showPicker = true">
    <tui-inputrequired backgroundColor="transparent":borderBottom="false"label="送货区域"placeholder="请选择送货区域"v-model="formData.deliveryArea"disabled></tui-input>
</tui-list-cell>

<tui-picker:show="showPicker":pickerData="areas"@hide="showPicker = false"@change="changePicker">
</tui-picker>

在页面的JS脚本处理中,还需要对字典类型进行取值、以及选择后修改属性值等操作处理

<script>import dict from'@/api/dictdata.js'exportdefault{
data() {
return{
showPicker:
false,
areas: [
//{text: "中国", value: "1001"}, ]
}
},
created() {
dict.GetListItemByDictType(
"送货区域").then(res =>{this.areas =res;
})
dict.GetListItemByDictType(
"客户等级").then(res =>{this.grades =res;
})
},
methods: {
changePicker(e) {
this.formData.deliveryArea =e.value;
}
}
}
</script>

一顿操作下来还是比较麻烦的,但是如果你使用自定义用户组件来操作,那么代码量迅速降低,如下所示。

<createinfo>
    <viewclass="tui-bg-img"></view>
    <dict-items-pickerv-model="formData.deliveryArea"dictTypeName="送货区域"label="送货区域"></dict-items-picker>
    <dict-items-pickerv-model="formData.grade"dictTypeName="供货档级"label="供货档级"></dict-items-picker>
</createinfo>

获得的界面效果如下所示,是不是感觉更好,代码更整洁了。

而其他部分的代码,则只是包括了引入组件的代码即可。

import dictItemsPicker from '@/pages/components/dict-items-picker.vue'exportdefault{
components: {
dictItemsPicker
},

上面组件的dictTypeName 对应的是数据库后台的字典类型名称,根据名称,从组件中调用api类获得数据即可,页面则不需要干涉这些逻辑了。其中我们自定义组件中使用 v-model 来绑定选择的值到页面的data属性中。

我们来看看整个页面组件的全部代码(内容不多,就一次性贴出来)

<!--通用字典下拉框展示-->
<template>
    <viewclass="">
        <tui-list-cellarrow padding="0"@click="showPicker = true">
            <tui-input:required="required"backgroundColor="transparent":borderBottom="false":label="labelName":placeholder="placeholder"v-model="value"disabled></tui-input>
        </tui-list-cell>
        <tui-picker:show="showPicker":pickerData="dictItems"@hide="showPicker = false"@change="changePicker">
        </tui-picker>
    </view>
</template>

<script>import dict from'@/api/dictdata.js'exportdefault{
emits: [
'click','cancel','update:value','change'],
props: {
required: {
//是否必选 type: Boolean,default:false},
dictTypeName: {
type: String,
default:""},
options: {
type: Array,
default() {return[]
}
},
value: {
type: String,
default:""},
label: {
type: String,
default:""}
},
components: {},
data() {
return{
showPicker:
false,
dictItems: []
}
},
mounted() {
if(!uni.$u.test.isEmpty(this.dictTypeName)) {
dict.GetListItemByDictType(
this.dictTypeName).then(res=>{this.dictItems=res;
})
}
else{this.dictItems= this.options;
}
},
watch: {},
computed: {
placeholder() {
return "请选择" + this.dictTypeName;
},
labelName() {
if(this.label) {return this.label;
}
else if(!uni.$u.test.isEmpty(this.dictTypeName)) {return this.dictTypeName
}
else{return ""}
}
},
methods: {
changePicker(e) {
this.$emit('change', e)this.$emit("input", e.value)this.$emit("update:value", e.value);
}
}
}
</script>

这里在内部组件的值变化的时候,通过事件 this.$emit("update:value", e.value); 进行通知更新父组件的绑定值,而 this.$emit("input", e.value)事件则是更新内部包含的input组件的值,同时提供一个change的事件进行完整的通知处理。

由于我们传递的是DIctTypeName,组件内部在Mounted的事件后进行获取字典的内容,并将返回值更新到内部组件中去。,如果是静态的字典集合,可以通过options 进行赋值即可。

if (!uni.$u.test.isEmpty(this.dictTypeName)) {
dict.GetListItemByDictType(
this.dictTypeName).then(res =>{this.dictItems =res;
})
}
else{this.dictItems = this.options;
}

因此该组件可以接受系统数据字典的字典类型名称,或者接受静态的字典列表作为数据源供选择需要。

整个组件内部封装了读取数据的细节以及展示控件的处理,并将新值通过事件
this.$emit("update:value"
, e.value);
的方式更新父页面的v-modal的绑定值,因此看起来和一个简单的Input的组件使用类似了。

以上就是一些简单组件的封装介绍,我们可以根据实际的需要,把我们 项目中遇到的可以封装的内容提出取出来,然后进行封装为组件的方式,会发现页面维护起来更加方便整洁了。

在组件逐步增多的情况下,我们同步完善一个简单的页面用来测试查看组件的效果,否则组件一多,记起来某个组件的效果就比较困难,我们的一个测试页面例子如下所示。

在我们基于UniApp的H5项目中,需要生成一些二维码进行展示,另外也需要让用户可以扫码进行一定的快捷操作,本篇随笔介绍一下二维码的生成处理和基于H5的扫码进行操作。二维码的生成,使用了JS文件weapp-qrcode.js进行处理,而二维码扫码则是基于一个第三方组件的方式进行支持的,最后通过统一入口来支持不同平台的扫码操作。

1、二维码的生成处理

二维码的生成,使用了JS文件weapp-qrcode.js进行处理的。因此我们在页面或者组件使用它的时候,需要引入JS文件才能调用。

import qrCodefrom '@/libs/weapp-qrcode.js';

我们为二维码的展示,创建一个自定义组件,用来展示二维码信息以及一些说明信息,如下所示。

<viewclass="hidden-box">
    <!--<view class="code-tit">二维码</view>-->
    <viewclass="qrcode-box">
        <tui-no-datav-if="!code":fixed="false"imgUrl="/static/image/img_nodata.png">暂未生成二维码</tui-no-data>
        <viewclass="qrcode"v-else>
            <canvas:style="{ width: qrcode_w + 'px', height: qrcode_w + 'px' }"canvas-id="qrcode"id="qrcode"></canvas>
        </view>
    </view>
    <viewclass="explain"v-if="desc.length > 0">使用说明</view>
    <viewclass="explain-text"v-for="(items, index) in desc":key="items">{{ items }}</view>
</view>

如果没有生成二维码的所示,我们用一个空图片代替,如下效果所示。

二维码生成的时候,接收一个控件id,以及二维码的值,生成函数代码如下所示。

    //二维码生成工具
qrcode(text, canvasId) {//console.log(text, canvasId)
        newqrCode(canvasId, {
text: text,
width:
this.qrcode_w,
height:
this.qrcode_w,
colorDark:
'#000000',
colorLight:
'#ffffff',
correctLevel: qrCode.CorrectLevel.H
});
}

在添加一个方法对展示进行折叠处理,那么具体如下所示。

showDetail() {this.visible = !this.visible;if (this.visible) {
setTimeout(()
=>{this.qrcode(this.code, 'qrcode');
},
60);
}
},

在测试页面中,导入刚才的二维码自定义组件,然后以普通的组件一样使用它即可。

<qrcode-info:code="entity.qrCode"></qrcode-info>

随便弄一个二维码的值,生成二维码后的界面测试效果如下所示。

2、二维码的扫码操作

我们知道,基于UniApp的程序中,内置有扫码操作

uni.scanCode(OBJECT)

地址是:
https://uniapp.dcloud.net.cn/api/system/barcode.html#scancode

不过该接口却不能在H5中调用扫码处理,

如果需要在H5应用中扫码,那么需要另辟蹊径,如果使用基于微信的SDK进行,还需要一系列的操作很麻烦,而且H5扫码一般需要https的支持才能调用摄像头的。

我在GitHub上搜索了一些基于H5扫码的项目,好像效果都不是很理想,估计是没有找到好的案例。在不经意间,发现《
H5调用摄像头识别二维码
》(需要https环境才能使用)效果挺好,就下来整合在项目中使用。

为了方便通用的扫码处理,我们这里定义了一个通用的扫码页面scan-qrcode.vue ,其他地方需要扫码的,统一定位到该页面处理即可。

在页面代码中,我们引入上面的二维码扫描组件即可,如下代码所示。

<template>
    <viewclass="container">
        <get-qrcode@success='qrcodeSucess'@error="qrcodeError"></get-qrcode>
    </view>
</template>

<script>import getQrcode from'@/pages/components/GetQrcode/getQrcode.vue'exportdefault{
components: {
getQrcode
},

另外定义一个to参数,用来判断页面转到那里去的。

data() {return{
to:
'' //页面带过来的to地址 }
},
onLoad(options) {
this.to = options?.to;
}

同时定义两个方法,一个是成功处理的,一个是出错的提示的。

qrcodeSucess(data) {if(uni.$u.test.url(data)) {
let url
=data;
console.log(url)
if (url.indexOf('#') > 0) {
let pageurl
= url.split('#')[1];
console.log(pageurl);
//在页面地址后增加一个to参数 let toUrl = uni.$u.test.isEmpty(this.to) ? pageurl : (pageurl + `&to=${this.to}`)
uni.navigateTo({
url: toUrl
})
}
else{
uni.$u.toast(
"URL格式不符");
}
}
},
qrcodeError(err) {
console.log(err)
uni.showModal({
title:
'摄像头授权失败',
content:
'摄像头授权失败,请检测当前浏览器是否有摄像头权限。',
success: ()
=>{
uni.navigateBack({})
}
})
},

扫码成功后,会自动根据地址进行跳转到具体的二维码URL页面,并带过去一个to的参数给目标页面。扫码的页面效果如下所示。

如果我们扫码的内容是一个URL连接,就会跳转到具体的页面中去,并且带上一个to参数用于区分目标。

这样我们在一些需要的地方,通过按钮或者图片来触发一个扫码行为即可。

处理代码如下所示

uni.navigateTo({
url:
'../common/scan-qrcode'})

如果我们的程序,需要兼容App模式、小程序模式和H5等模式,那么我们可以通过判断来切换不同的扫码处理,如下代码所示。

onScan(to) { //h5扫描二维码并解析
    console.log("不支持H5扫码 走onScan这个方法")
let url
= '../common/scan-qrcode?to=' +to;this.tui.href(url)
},
scanCode(to) {
//允许从相机和相册扫码 //#ifndef H5 uni.scanCode({
scanType: [
'qrCode', "barCode"],
success:
function(res) {
console.log(res)
//微信小程序 if (res.errMsg == "scanCode:ok") {//扫描到的信息 //uni.$u.toast(res.result); var url =res.result;if(uni.$u.test.url(url)) {
console.log(url)
if (url.indexOf('#') > 0) {
let pageurl
= url.split('#')[1];
console.log(pageurl);
//在页面地址后增加一个to参数 uni.navigateTo({
url: pageurl
+ `&to=${to}` //加上功能 })
}
else{
uni.$u.toast(
"URL格式不符");
}
}
else{
uni.$u.toast(
"URL格式不符");
}

}
else{
console.log(
"未识别到二维码,请重新尝试!")
uni.$u.toast(
'未识别到二维码,请重新尝试!')
}
}
});
//#endif //#ifdef H5 this.onScan(to)//#endif }

这样我们就统一处理入口了,如下调用代码所示。

packsign() {this.scanCode('packsign')
},

以上就是二维码生成和在H5中扫码的处理操作供参考。