在我的《
FastReport报表随笔
》介绍过各种FastReport的报表设计和使用,FastReport报表可以弹性的独立设计格式,并可以在Asp.net网站上、Winform端上使用,由于其主要是使用.net后端生成的报表方式,如果不考虑其在线设计处理,那么可以在在Vue&Element前端项目中使用,通过后端生成报表PDF文件,然后通过前端使用pdf.js来呈现报表最终效果即可。

1、使用FastReport生成自定义报表

我们通过定义自己FastReport报表格式,然后在后端生成PDF文件存储在指定目录中,并返回路径给前端,前端就可以通过pdf.js进行预览了。

关于FastReport报表的设计,这里不再赘述,主要就是通过几个简单的案例展示生成的报表逻辑。

我们来看看其中的报表生成代码,主要分为几个步骤:初始化报表、准备数据、运行并导出报表。

其中从初始化报表如下代码所示。

    #region 初始化报表
    //报表文件路径
    string reportPath = "/Report/Pres.frx";//转换为物理路径
    reportPath =System.Web.Hosting.HostingEnvironment.MapPath(reportPath);//导出PDF/jpg的文件路径
    string exportPdfPath = $"/GenerateFiles/Pres/Report_{id}" + (pdf ? ".pdf":".jpg");//转换为物理路径
    string realPath =System.Web.Hosting.HostingEnvironment.MapPath(exportPdfPath);//确保目录生成
    string parentPath =Directory.GetParent(realPath).FullName;
DirectoryUtil.AssertDirExist(parentPath);
//生成PDF报表文档到具体文件 Report report = newReport();
report.Load(reportPath);
#endregion

而初始化报表后,就需要准备相应的参数和数据等信息了,下面是测试数据代替

    dict.Add("Name", "张三");
dict.Add(
"Gender", "");
dict.Add(
"Age", 32);
dict.Add(
"Telephone", "18620292076");
dict.Add(
"CreateTime", "2019-10-13 22:30:15");
dict.Add(
"CheckDoctor", "张医生");
dict.Add(
"CheckPharmacist", "李药师");
dict.Add(
"SendUser", "王小姐");
dict.Add(
"QrCode", "http:www.iqidi.com");
dict.Add(
"BarCode", "1234567890");for (int i = 0; i < 5; i++)
{
var dr =dt.NewRow();
dr[
"ProductName"] = "阿莫西林" +i;
dr[
"Quantity"] =i;
dr[
"Unit"] = "";
dr[
"Specification"] = "一盒24粒" +i;
dr[
"HowTo"] = "口服";
dr[
"Frequency"] = "一次三次,一次一片";
dt.Rows.Add(dr);
}

准备好数据后,更新相关参数和数据源即可。

    //刷新数据源
    report.RegisterData(dt, "Detail");foreach (string key indict.Keys)
{
report.SetParameterValue(key, dict[key]);
}

然后我们根据是否PDF格式(否则为图片格式)的标志生成文件,如下所示。

    #region 运行并导出报表report.Prepare();if(pdf)
{
//导出PDF报表 var export = newPDFExport();
report.Export(export, realPath);
}
else{//导出JPG报表 var export = newImageExport();//export.JpegQuality = 392;//export.ResolutionY = 226; report.Export(export, realPath);
}
report.Dispose();
#endregion

最后返回路径给前端即可。

2、前端调用pdf.js插件生成并展示自定义报表

后端生成PDF文件后,返回路径给前端,由于是PDF文件,我们可以利用 pdf.js生成并展示自定义报表文件。

首先在前端的API调用类中构建对应的调用函数,如下所示。

然后在页面中增加对应的处理方式即可。

页面中通过按钮的触发调用这个函数,就可以呈现对应的PDF预览窗口了。

在很多项目开发中,我们为了使用方便,一般都会封装一些自定义组件来简化界面的显示处理,例如参照字典的下拉列表显示,是我们项目中经常用到的功能之一,本篇随笔介绍在Vue&Element前端项目中如何使用自定义封装的字典显示处理。

1、字典内容管理

我们在系统中,往往维护着一些系统常用到的字典信息,在我各个框架中都有一个通用的字典管理模块,对于Vue&Element前端项目,也是一样,我们也需要对它进行管理,方便前端使用。本篇随笔介绍的内容适用于ABP开发框架的Vue&Element前端,微信框架和Bootstrap框架中的Vue&Element前端内容。

字典项目一般包括项目大类,字典项的管理,字典项包括显示内容和值,两者可以不一样,也可以一样,如下所示

或者如下所示

字典大类和字典项目的表设计图如下所示。

字典大类由PID构建无穷级的树结构,而字典项目则通过Name和Value来记录显示内容和值内容。

2、后端WebAPI的数据提供

在建立相关的数据表存储数据外,我们后端也需要提供相应的Web API来给各前端提供数据处理,对于显示处理,我们定义了一个适合于Select组件和Tree组件的数据结构,主要就是提供id和label的属性,如下代码所示。

   /// <summary>
    ///通用树节点的定义/// </summary>
[Serializable]public classTreeNodeItem
{
/// <summary> ///默认构造函数/// </summary> publicTreeNodeItem()
{
}
/// <summary> ///构造函数/// </summary> public TreeNodeItem(CListItem item) :this()
{
this.Label =item.Text;this.Id =item.Value;
}
/// <summary> ///参数化构造CListItem对象/// </summary> /// <param name="label">显示的内容</param> /// <param name="id">实际的值内容</param> /// <param name="key">存储额外的键</param> public TreeNodeItem(string id, string label, string key = null) : this()
{
this.Id =id;this.Label =label;this.Key =key;
}
/// <summary> ///参数化构造CListItem对象/// </summary> /// <param name="label">显示的内容</param> /// <param name="id">实际的值内容</param> /// <param name="key">存储额外的键</param> public TreeNodeItem(int id, string label, string key = null) : this()
{
this.Id =id.ToString();this.Label =label;this.Key =key;
}
/// <summary> ///参数化构造CListItem对象/// </summary> /// <param name="text">显示的内容</param> public TreeNodeItem(string text) : this()
{
this.Id =text;this.Label =text;
}
/// <summary> ///实际值内容/// </summary> public string Id { get; set; }/// <summary> ///显示内容/// </summary> public string Label { get; set; }/// <summary> ///用于存储额外的键/// </summary> public string Key { get; set; }/// <summary> ///子节点集合/// </summary> [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling =DefaultValueHandling.Ignore)]public List<TreeNodeItem> children { get; set; }/// <summary> ///返回显示的内容/// </summary> /// <returns></returns> public override stringToString()
{
returnLabel.ToString();
}
}

有了这些数据结构,我们可以在Web API的控制器中提供相应的数据了。

而对于ABP框架后端,它们的调用方式也是类似的。

后端API执行返回的JSON数据结构如下所示。

有了这些数据,就需要在前端进行显示即可。

3、Element 前端组件显示

Vue&Element的前端,需要调用后端接口的时候,需要对API类进行一个简单的封装,这样可以方便通过类的方式进行访问后端接口。

前端界面组件中需要用到这个API调用的时候,import进来即可。

//引入API模块类方法
import dictdata from '@/api/system/dictdata'

有了提供的数据结构和API的封装,我们可以在前端进行展示了,我们来看看Element的Select组件例子代码

<template>
  <el-select v-model="value" placeholder="请选择">
    <el-option
v
-for="item in options":key="item.value":label="item.label":value="item.value"> </el-option> </el-select> </template> <script>exportdefault{
data() {
return{
options: [{
value:
'选项1',
label:
'黄金糕'}, {
value:
'选项2',
label:
'双皮奶'}, {
value:
'选项3',
label:
'蚵仔煎'}, {
value:
'选项4',
label:
'龙须面'}, {
value:
'选项5',
label:
'北京烤鸭'}],
value:
''}
}
}
</script>

如果我们每次都需要用这个原始组件来进行展示,那么就需要编写很多代码,我们希望在编写显示字典列表的代码时候,尽量减少代码,因此我们定义了字典组件,用于接收两个数据参数,一个是字典大类名称,通过字典大类名称获取字典列表,并绑定的select组件中;另一个则是标准的集合列表。

我们来看看使用的界面效果和实际代码。

而使用代码如下所示。

<el-form-itemlabel="付款方式"prop="payType">
        <my-dictdatav-model="searchForm.status"type-name="付款方式" />
</el-form-item>

上面通过type-name来声明字典大类,从而由组件逻辑实现数据源的绑定处理。

另一种方式就是绑定数据列表,通过options变量进行绑定,如下所示。

<el-form-itemlabel="表单分类"prop="category">
       <my-dictdatav-model="searchForm.category":options="FormCategorys" />
</el-form-item>

而其中这个数据源则可以在页面或者组件中实现获取即可。

4、自定义组件处理

上面介绍了如何实现自定义字典组件,那么字典组件如何自定义处理呢,我们来看看一般的处理如下。

<template>
  <el-selectv-model="keyword"filterable :clearable="clearable":multiple="multiple":disabled="disabled":placeholder="placeholder"@change="change">
    <el-optionv-for="(item, index) in dictItems":key="index":label="item.label":value="item.id">
      <spanstyle="float: left;color:yello;">
        <iclass="el-icon-tickets"style="color:blue;" />{{ item.label }}</span>
    </el-option>
  </el-select>
</template>

不过上面这种对于字符型的数据显示没问题,如果对于包含数值型的选项赋值,则会出现不匹配的问题,因此我们改进一下上面的选项处理代码,以便适应字符型和数值型的绑定值处理。

<template>
  <el-selectv-model="keyword"filterable :clearable="clearable":multiple="multiple":disabled="disabled":placeholder="placeholder"@change="change">
    <templatev-for="(item, index) in dictItems">
      <el-optionv-if="typeof(keyword)=='string'":key="index":label="item.label":value="'' + item.id" />
      <el-optionv-else-if="typeof(keyword)=='number'":key="index":label="item.label":value="('' + item.id).trim() == '' ? '' : parseInt(item.id)" />
      <el-optionv-else :key="index":label="item.label":value="item.id" />
    </template>
  </el-select>
</template>

以上代码通过判断选项绑定的值类型,从而进行相应的处理,避免数据格式不一致的问题。

而其中的字典列表,这是通过判断prop参数进行获取处理的。

加载的时候,获取数据进行显示绑定即可。

以上就是我们进行用到的字典处理过程,包括后端提供数据API、前端对API的封装,以及组件封装,然后就是界面组件的调用,这种方式极大的提高了自定义组件的使用效率,简化代码,一旦我们封装好自定义组件,使用起来非常方便。

在Vue前端界面中,自定义组件很重要,也很方便,我们一般是把一些通用的界面模块进行拆分,创建自己的自定义组件,这样操作可以大大降低页面的代码量,以及提高功能模块的开发效率,本篇随笔继续介绍在Vue&Element前端项目中如何使用自定义封装的组件,实现附件的展示场景。

1、界面模块的拆分

Vue的前端界面,对界面内容部分可以根据需要进行适当的拆分,也可以把通用的部分封装为组件进行使用,如我在随笔《
循序渐进VUE+Element 前端应用开发(16)--- 组织机构和角色管理模块的处理
》中介绍过机构或角色信息中,界面内容比较多,可以进行拆分,根据内容的展示不同,拆分为各自的组件模块,然后合并使用即可,如下所示。

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

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

角色界面模块UML类图如下所示。

那么对应界面元素上,我们就应该以不同的Tab来展示这些信息,如下所示。  其中可以看到不同的Tab显示不同的内容。

完成添加成员、添加角色的界面组件后,我们就可以在组织机构界面里面引入使用。

接着加入对应的组件集合里面即可。

然后在界面部分加入对应的组件呈现代码,如下所示。

2、通用组件的拆分

前面介绍了按页面内容进行的组件封装处理,有时候,我们也可以根据特定功能的内容进行组件封装,这样我们就简化了界面的呈现处理逻辑,只需要传递一定的参数给它即可。

如我在随笔《
使用Vue-TreeSelect组件实现公司-部门-人员级联下拉列表的处理
》中对树列表功能的封装,使用起来代码就很简单,赋值相关的参数即可。

<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="所属部门" />

以及在随笔《
自定义Vue&Element组件,实现用户选择和显示
》实现选择用户信息的处理组件。

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

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

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

还有自定义字典组件的处理,在随笔《
在Vue&Element前端项目中,对于字典列表的显示处理
》中介绍。

而使用代码如下所示。

<el-form-itemlabel="付款方式"prop="payType">
        <my-dictdatav-model="searchForm.status"type-name="付款方式" />
</el-form-item>

上面通过type-name来声明字典大类,从而由组件逻辑实现数据源的绑定处理。

另一种方式就是绑定数据列表,通过options变量进行绑定,如下所示。

<el-form-itemlabel="表单分类"prop="category">
       <my-dictdatav-model="searchForm.category":options="FormCategorys" />
</el-form-item>

3、附件展示的自定义组件开发

一般情况下,我们管理的附件都是诸如图片、Excel文件、PDF文件等附件的管理。

附件表是一个综合管理这些文件的记录表,虽然附件一般是独立上传到服务器端的文件系统里面,不过也需要记录这些文件的名称、类别名称、大小、后缀名、创建时间、创建人等信息。

数据库设计表如下所示。

记录明细大概如下所示。

为了管理好这些文件信息,我们在界面提供一些条件供查询,如下是管理界面。

以上是附件的统一管理界面,我们在实际业务中很多业务表都可能用到附件信息,因此可以在业务表中增加一个字符串字段,用来存储附件组的GUID,如下所示。

上面饿AttachGUID对应FileUpload表中的AttachmentGuid,可以用来获得一个获得多个附件列表。

我们先来看看业务表单中展示附件的效果,如下所示。

由于我们要获取附件列表,输入参数应该是附件分组的AttachGUID,输出就是显示文件或者图片列表,图片方便展示即可,文件则提供连接地址下载查看等。

那么我们就需要根据这个场景来定义我们的附件展示组件了。封装好的附件展示界面组件的使用代码如下所示。

<el-form-itemlabel="附件">
  <my-attachment v-modal="viewForm.attachGUID" />
</el-form-item>

代码非常简单,就和使用其他原生组件是差不多的,而我们可以在各个模块用到附件的地方都如此展示,这样处理不仅简化了展示代码,而且不用关心太多的处理逻辑,确实很方便的。

那我们现在来看看,这个附件展示的自定义组件是如何工作的。

附件展示的代码如下所示,主要就是区分是图片,还是常规文件进行不同的展示。

<template>
  <divstyle="box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1)">
    <templatev-if="!attachFiles || attachFiles.length == 0">
      <el-empty:description="placeholder":image-size="50" />
    </template>
    <templatev-else>
      <templatev-for="(item, index) in attachFiles">
        <el-imagev-if="isImage(item)":key="index"style="padding:5px;width: 100px; height: 100px":src="getUrl(item)":preview-src-list="imageList":z-index="3000" />
        <el-linkv-else :key="index":href="getUrl(item)":underline="false"target="_blank"style="padding:5px;">
          <span>{{ item.fileName }}</span>
        </el-link>
      </template>
    </template>
  </div>
</template>

对于附件的API操作,我们引入对应的封装类如下所示。

//引入API模块类方法
import fileupload from '@/api/system/fileupload'

为了传入对应的参数值,我们定义两个prop属性,如下所示。

<script>
//引入API模块类方法
import fileupload from '@/api/system/fileupload'exportdefault{
name:
'MyAttachment', //组件的名称 props: {
placeholder: {
//空白提示 type: String,default: '暂无数据'},
value: {
//接受外部v-model传入的值 type: [String, Number],default: ''}
},

另外,对于传入的v-modal或者value值,需要监控watch它的变化,如果变化,我们触发附件的获取展示逻辑,如下代码所示。

data () {return{
attachFiles: [],
imageList: [],
//预览图片列表 keyword: this.value //选中的值 }
},
watch: {
//判断下拉框的值是否有改变 keyword (val, oldVal) {//console.log(val, oldVal) if (val !==oldVal) {//组件内对值变更后向外部发送事件通知 this.getData()this.$nextTick(() =>{this.$emit('input', val)
})
}
},
value (val, oldVal) {
//监听外部对props属性变更,如通过ResetFields()方法重置值的时候 this.keyword =val
}
},

其中数据的获取,主要是调用服务端API的处理,获得对应的附件列表,并判断是否为图片进行添加即可。

methods: {
getData () {
//console.log(this.value) if (this.value && this.value !== '') {var param = { guid: this.value }
fileupload.GetByAttachGUID(param).then(data
=>{this.attachFiles = data.result//生成并添加预览图片列表 this.imageList =[]this.attachFiles.map(item =>{if (this.isImage(item)) {var imageUrl = this.getUrl(item)this.imageList.push(imageUrl)
}
})
})
}
},

这样我们就把通用的附件管理界面,业务表附件的展示统一起来,实现了比较好的附件通用管理,和通用字典模块一样,一旦完成了组件的封装,使用起来非常简单,代码也比较容易维护了。

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

在基于Vue的工作流项目模块中,我们在查看表单明细的时候,需要包含公用表单信息,特定表单信息两部分内容。前者表单数据可以统一呈现,而后者则是不同业务的表单数据不同。为了实现更好的维护性,把它们分开作为两部分处理,但是页面入口设计为统一的呈现页面,这里介绍使用动态组件的方式统一呈现不同表单数据的处理方式。

1、表单数据分类

刚才提到,表单数据按内容区分分为了两类:通用业务表单、特定业务表单

如果我们要把两者统一在一个通用页面中进行展示,就需要根据不同表单名称,动态加载属于特定表单的展示模块,也就是动态组件的实现方式,大概的业务规则如下所示。

表单查看的实际效果界面如下所示。

2、动态组件的实现

首先我们需要在前端的路由器集合中加入一个路由地址的配置,以便定位到统一查看表单页面明细的路径,如下所示。

  '_viewDetail': {
path:
'/_viewDetail',
meta: {
title:
'查看申请单信息'},
component: Layout,
children: [{
path:
'',
component: ()
=> import('@/views/workflow/system/viewdetail')
}]
},

完成后,我们开始编写页面,包含两部分,一部分是通用的表单信息

一部分是动态组件构建的特定表单信息,如下组件代码所示

    <!--实现动态组件的展示-->
    <component:is="viewType":applyid="applyid" />

在我们平时使用vue中的模板的时候,许多时候都是直接定义成一个固定的模板,但是,vue中提供了一个动态模板,可以在任意模板中切换,就是用vue中<component>用:is来挂载不同的组件。

而其中viewType 是一个属性定义,我们可以通过后台API获取对应的模块名称

methods: {
getViewType () {
if (this.applyid) {//通过申请单的DataTable去掉前缀,转换小写,获得模块名称,如TW_Payment => payment var param = { applyId: this.applyid }
apply.GetModuleName(param).then(data
=>{if(data.result) {this.viewType =data.result.toLowerCase()
}
})
}
},

而其中viewType就是我们组件的名称,这里能够呈现出来的内容,必须是这些组件已经注册到全局中去了,就类似我们在Vue 项目中使用Element组件一样,之所以可以使用它也是它已经全局注册了(或者以组件方式加载到页面)。

而我们定义的表单内容可能很多,如下目录所示。

手工加载这些组件,难免遗漏或者错误,因此需要考虑按一定的规则动态加载进去系统中使用。

我们编写一个js文件,用于动态获取指定目录下的vue组件,并使用Vue.component 动态安装所需组件到系统中,如下代码所示。

export default{
install(Vue) {
//从模块目录中自动载入模块内容
const viewFiles = require.context('./view', true, /\.vue$/)
const viewModules
= viewFiles.keys().reduce((viewModules, modulePath) =>{//set './app.vue' => 'app'
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1').toLowerCase()
const value
=viewFiles(modulePath)
viewModules[moduleName]
= value.default
returnviewModules
}, {})
//console.log(viewModules)
Object.keys(viewModules).forEach(key =>{//console.log(key)
Vue.component(key, viewModules[key])
})

}
}

然后我们在main.js中调用它安装所需组件即可,如下代码所示。

//导入工作流查看、创建等模块
import workflow from './views/workflow/modules'Vue.use(workflow)

这样我们就完成了组件的动态加载安装,在通用的查看页面中,就可以通过component的 is 方式动态呈现不同页面表单的数据了。

以支付申请表单为例,我们的payment.vue组件代码如下所示。

<template>
  <divclass="app-container">
    <el-cardclass="box-card">
      <divslot="header"class="clearfix">
        <span>付款申请单-表单数据</span>
      </div>
      <el-formref="viewForm":model="viewForm"label-width="120px">
        <el-tabsvalue="basicPage"type="border-card">
          <el-tab-panename="basicPage"label="基本信息">
            <el-row>
              <el-col:span="24">
                <el-form-itemlabel="付款事由">
                  <el-inputv-model="viewForm.reason"readonly/>
                </el-form-item>
              </el-col>
              <el-col:span="12">
                <el-form-itemlabel="付款金额">
                  <el-inputv-model="viewForm.payAmount"readonly>
                    <spanslot="suffix"></span>
                  </el-input>
                </el-form-item>
              </el-col>
              <el-col:span="12">
                <el-form-itemlabel="付款方式">
                  <el-inputv-model="viewForm.payType"readonly/>
                </el-form-item>
              </el-col>
              <el-col:span="12">
                <el-form-itemlabel="付款日期">
                  <el-date-pickerv-model="viewForm.payDate"align="right"type="datetime"placeholder="选择日期"value-format="yyyy-MM-dd HH:mm:ss"readonly/>
                </el-form-item>
              </el-col>
              <el-col:span="12">
                <el-form-itemlabel="收款人全称">
                  <el-inputv-model="viewForm.payeeFullName"readonly/>
                </el-form-item>
              </el-col>
              <el-col:span="12">
                <el-form-itemlabel="银行账号">
                  <el-inputv-model="viewForm.bankAccount"readonly/>
                </el-form-item>
              </el-col>
              <el-col:span="12">
                <el-form-itemlabel="开户行">
                  <el-inputv-model="viewForm.bank"readonly/>
                </el-form-item>
              </el-col>
              <el-col:span="24">
                <el-form-itemlabel="备注信息">
                  <el-inputv-model="viewForm.note"readonly type="textarea":rows="5" />
                </el-form-item>
              </el-col>
              <el-col:span="24">
                <el-form-itemlabel="附件">
                  <my-attachmentv-model="viewForm.attachGUID" />
                </el-form-item>
              </el-col>
            </el-row>
          </el-tab-pane>
        </el-tabs>
      </el-form>
    </el-card>

    <divclass="dialog-footer">
      <el-buttontype="success"@click="goback">返回列表页面</el-button>
    </div>
  </div>
</template>

并在js代码中定义了prop属性applyid,如下所示。

props: {
applyid: {
//申请单ID, 接受外部v-model传入的值 type: [String, Number],default: ''}
},

这样我们在页面创建的时候,就可以根据applyid来获得对应的特定表单数据,然后页面内容就会正常展示了。

created () {this.getData() //显示数据
},
methods: {
getData () {
if (this.applyid) {var param = { applyId: this.applyid }
payment.FindByApplyId(param).then(data
=>{//console.log(data)
Object.assign(this.viewForm, data.result)
})
}
}

表单查看的实际效果界面如下所示。

这样就完成了我们通过统一的展示页面,展示不同表单数据的统一操作。

查看页面的操作,都通过路由名称_viewdetail实现了统一的跳转。

      <!--表格行操作按钮-->
      <el-table-columnalign="center"label="操作"width="110">
        <templatescope="scope">
          <el-row>
            <el-button-group>
              <el-tooltipeffect="light"content="查看"placement="top-start">
                <el-buttonicon="el-icon-search"type="success"circle size="mini"@click="showView(scope.row.apply_ID)" />
              </el-tooltip>
              <el-tooltipeffect="light"content="删除"placement="top-start">
                <el-buttonicon="el-icon-delete"type="danger"circle size="mini"@click="showDelete(scope.row.id)" />
              </el-tooltip>
            </el-button-group>
          </el-row>
        </template>
      </el-table-column>

JS代码如下

    showView (id) { //显示查看对话框处理
      this.$router.push(
{
path: `
/_viewDetail`, //查看申请单路径,以_view开始
query: { id: id }
})
},

以上就是在Vue的工作流项目模块中,使用动态组件的方式统一呈现不同表单数据的处理方式,希望对您有所启发,在一些特定的场景使用它,可以极大提高灵活性和开发效率。

在Vue前端项目中,我这里主要是基于Vue+Element的开发,大多数情况下,我们利用Element的表格组件就可以满足大多数情况的要求,本篇随笔针对表格的展示和编辑处理,综合性的介绍几款表格组件的展示和处理效果,其中包括Element的el-table组件,以及其他第三方类组件,如vue-easytable、vue-willtable,以及vxe-table,针对性的对比相关的差异。

1、el-table表格组件

这个是Element的表格组件,使用参考地址如下:
https://element.eleme.cn/#/zh-CN/component/table
这个表格组件也是非常好用的展示数据的组件,提供了很多属性设置,表格列的模板定义可以进行各种各样的转义和处理,是我们使用Element组件经常用到的组件之一。
简单的界面效果如下所示。

它的简单界面代码如下所示,主要就是提供列的定义。

<template>
  <el-table:data="tableData"stripe style="width: 100%">
  <el-table-columnprop="date"label="日期"width="180"> </el-table-column>
    <el-table-columnprop="name"label="姓名"width="180"> </el-table-column>
    <el-table-columnprop="address"label="地址"> </el-table-column>
  </el-table>
</template>

而数据这是一个对象集合,里面对应各种属性记录,如下所示。

<script>exportdefault{
data() {
return{
tableData: [{
date:
'2016-05-02',
name:
'王小虎',
address:
'上海市普陀区金沙江路 1518 弄'}, {
date:
'2016-05-04',
name:
'王小虎',
address:
'上海市普陀区金沙江路 1517 弄'}, {
date:
'2016-05-01',
name:
'王小虎',
address:
'上海市普陀区金沙江路 1519 弄'}, {
date:
'2016-05-03',
name:
'王小虎',
address:
'上海市普陀区金沙江路 1516 弄'}]
}
}
}
</script>

不过一般实际使用的表格比这个会复杂很多,会在列之中提供很多转义信息展示,如下界面效果所示

或者

我们来看看表格头部和表格列的相关定义来进行解析。

<el-tablev-loading="listLoading":data="list"border fit stripe highlight-current-row
:header-cell-style
="{background:'#eef1f6',color:'#606266'}"@selection-change="selectionChange"@row-dblclick="rowDbclick"@sort-change="sortChange">

listLoading用来 展示/隐藏 加载中的状态

list则是提供给表格数据处理的数据源

selectionChange这是选中/取消勾选框的事件触发

rowDbclick是双击表格行出现的事件响应

sortChange是排序表头出现的事件处理

另外,表格数据往往涉及到分页信息,那么必须和分页控件一起使用

        <!--分页部分-->
        <divclass="block"style="height:70px;">
          <el-pagination:current-page="pageinfo.pageindex":page-size="pageinfo.pagesize":total="pageinfo.total":page-sizes="[10,20,30,40]"layout="total, sizes, prev, pager, next"@size-change="sizeChange"@current-change="currentChange" />
        </div>

其中分页组件中涉及到pageinfo对象,包括当前页码和每页的数据记录数量,以及对每页数据大小变化事件sizeChange、页码变化的事件currentChange的处理。

因此我们在<script>中定义相关的变量和方法。

export default{
components: {},
//常见组件已经在main.js中挂载 data () {return{
listLoading:
false, //列表加载状态 list: [], //页面列表数据 pageinfo: {//分页条件 pageindex: 1,
pagesize:
20,
total:
0},
listSelection: [],
//选中记录 sorting: null, //排序条件

获取数据的事件getlist中,我们主要就是根据条件获取数据记录,并赋值给list对象即可。

而其他相应的事件如下定义所示

    search () { //查询列表处理
      this.pageinfo.pageindex = 1;//重置为第一页
      this.searchForm.pid = null;//重置选择类型
      this.getlist()
},
resetSeachForm (formName) {
//重置查询条件处理 this.$refs[formName].resetFields();this.search()
},
sortChange ({ column, prop, order }) {
var direction = (order === 'ascending') ? ' ASC' : ' DESC' this.sorting = !this.isEmpty(prop) ? (prop + direction) : null this.getlist()
},
sizeChange (val) {
//页面大小变化处理 //console.log(`每页 ${val} 条`); this.pageinfo.pagesize =val;this.getlist()
},
currentChange (val) {
//页码变化处理 //console.log(`当前页: ${val}`); this.pageinfo.pageindex =val;this.getlist()
},
selectionChange (val) {
//选中记录 this.listSelection =val;
},
rowDbclick (row, column) {
//列表单击处理 var id =row.idthis.showView(id);
},

这样我们就是实现了对数据的分页处理,和相关的事件处理。剩下的就是对记录列内容的转义处理了,这个通过HTML的代码进行处理即可。

常规的内容处理如下所示。

<el-table-columnalign="center"label="名称"sortable="custom"prop="name"min-width="150">
   <templateslot-scope="scope">{{ scope.row.name }}</template>
</el-table-column>

如果我们需要进行转义,在这里面处理判断就可以了,非常方便。

  <el-table-columnalign="center"label="可见"sortable="custom"prop="visible"width="80">
    <templateslot-scope="scope">
      <el-tagv-if="scope.row.visible"type="success"effect="dark">可见</el-tag>
      <el-tagv-else type="danger"effect="dark">隐藏</el-tag>
    </template>
  </el-table-column>

或者通过formater的函数进行内容的转义处理。

<el-table-columnalign="center"label="创建时间"width="160"sortable="custom"prop="createTime":formatter="timeFormat" />

也可以使用filter进行内容转义

  <el-table-columnalign="center"label="商品类型"width="120"prop="ProductType">
    <templateslot-scope="scope">
      <el-tagtype="danger">  {{ scope.row.ProductType | productTypeFilter }}</el-tag>
    </template>
  </el-table-column>

如果我们需要添加表格列的操作处理,那么添加对应的按钮即可,通过获取对应记录的标识,来进行相关记录的操作即可。

  <!--表格行操作按钮-->
  <el-table-columnalign="center"label="操作"width="110">
    <templatescope="scope">
      <el-row>
        <el-button-group>
          <el-tooltipeffect="light"content="查看"placement="top-start">
            <el-buttonicon="el-icon-search"type="success"circle size="mini"@click="showView(scope.row.id)" />
          </el-tooltip>
          <el-tooltipeffect="light"content="编辑"placement="top-start">
            <el-buttonicon="el-icon-edit"type="primary"circle size="mini"@click="showEdit(scope.row.id)" />
          </el-tooltip>
          <el-tooltipeffect="light"content="删除"placement="top-start">
            <el-buttonicon="el-icon-delete"type="danger"circle size="mini"@click="showDelete(scope.row.id)" />
          </el-tooltip>
        </el-button-group>
      </el-row>
    </template>
  </el-table-column>

Element的表格组件功能虽然很好,不过它的特点是展示处理,并不是很适合进行直接内容的编辑处理,有些人通过变量标识模板的方式控制编辑界面的展示,不过总体太复杂,不方便使用。

2、vue-easytable表格组件

vue-easytable表格组件地址:
npm安装:npm install vue-easytable -S
这个组件相对于Element的表格el-table来说,它的HTML代码比较少,适用于简单快捷的表格数据展示。
简单使用代码如下所示
 <ve-table:columns="columns":table-data="tableData" />

一般它的使用代码如下所示(配合分页数据展示)

    <ve-tablerow-key-field-name="id":checkbox-option="checkboxOption":columns="columns":table-data="list":fixed-header="true":border-around="true":border-x="true":border-y="true" />

    <!--分页部分-->
    <divclass="block"style="height:70px;">
      <el-pagination:current-page="pageinfo.pageindex":page-size="pageinfo.pagesize":total="pageinfo.total":page-sizes="[10,20,30,40]"layout="total, sizes, prev, pager, next"@size-change="sizeChange"@current-change="currentChange" />

常见的界面效果如下所示

一般我们需要在JS中定义列的信息,它的类型可以是expand、checkbox、radio几种之一,默认是字符串方式。

当前列类型: "expand":行展开;"checkbox":行多选;"radio":行单选

      columns: [//表列定义
{
field:
'',
key:
'a',
type:
'checkbox',
title:
'',
width:
50,
align:
'center'},
{
field:
'name',
key:
'name',
title:
'姓名',
width:
120,
columnAlign:
'center'},

这个列的定义除了field外,还需要一个Key的标识,不能为空,一般保留位field的值即可。

如果我们需要进行内容的转义处理,可以使用 renderBodyCell 函数进行转义处理,如下所示。

{
field:
'sex',
key:
'sex',
title:
'性别',
width:
120,
columnAlign:
'center',
renderBodyCell: ({ row, column, rowIndex }, h)
=>{
const text
=row[column.field];//return <input style="width:100%" value={text}></input>; let html = null if (text === '男') {
html
= <el-tag type='primary'>{text}</el-tag> } else{
html
= <el-tag type='success'>{text}</el-tag> }return(<div>{html}</div> )
}
},

数据的处理,和我们前面介绍的类似,就是通过分页条件的方式进行查询分页,获得数据后赋值给对应列表即可。

      //获取列表,绑定到模型上,并修改分页数量
      this.listLoading = truetestUser.GetAll(param).then(data=>{
console.log(data.result.items)
this.list =data.result.itemsthis.pageinfo.total =data.result.totalCountthis.listLoading = false})

通过 row-key-field-name="id" :checkbox-option="checkboxOption" 代码的处理,可以实现选中记录的操作。

checkboxOption: {//row select事件
    selectedRowChange: ({ row, isSelected, selectedRowKeys }) =>{
console.log(row, isSelected, selectedRowKeys);
},
//selected all事件 selectedAllChange: ({ isSelected, selectedRowKeys }) =>{
console.log(isSelected, selectedRowKeys);
}
},

这个表格组件也主要是进行展示,没有直接编辑的处理操作,不过它的特点是快速展示数据,转义的话操作起来会费劲一些,它主要就是通过jsx的方式进行转义处理的,感觉没有在HTML上直接判断的快捷明了。

3、vue-willtable表格组件(可直接编辑)

以上两个表格组件,都没有涉及到编辑的操作,有时候我们在一些表格中希望给客户直接录入数据的场景。
如果采用el-table的方式来处理,非常不直观,也不易处理,因此引入第三方组件的方式来实现数据的直接录入就比较好 了。

vue-willtable表格组件地址:
https://github.com/KevinMint55/vue-willtable

官方案例界面效果如下所示。

表格提供日期,数值、下拉列表,字符串等几种常见方式的录入。

总体上录入没有什么问题,不过对于组件的更新好像有时候有些问题,毕竟github上星星不是很多的项目。

我在这个基础上进行了案例的测试,界面效果如下所示。

主从表展示界面效果

以及表格查询分页处理和录入界面效果

    <divclass="willtable_wrapper">
      <vue-willtableref="table"v-model="list":columns="columns":row-height="38":cell-class-name="cellClassName" />
    </div>

字段定义的JS代码如下所示。

获取数据的JS代码如下所示

  //获取列表,绑定到模型上,并修改分页数量
  this.listLoading = truetestUser.GetAll(param).then(data=>{this.$refs.table.setData(data.result.items)this.pageinfo.total =data.result.totalCountthis.listLoading = false})

不过分页的话,切换页面大小会导致刷新显示有些问题 ,部分空白,首次显示倒是没有问题。

4、vxe-table表格组件(可直接编辑)

vxe-table是github上星星超过4k+的开源组件,这个表格不论是展示数据,以及编辑的处理,都堪称大能之作,非常方便强大。

vxe-table地址:

https://xuliangzhan_admin.gitee.io/vxe-table/#/table/start/install

https://github.com/x-extends/vxe-table

我们来看看它的常规使用代码,以及界面效果

如果需要录入新记录,通事件处理添加一个行即可。

vxe-table的使用HTML代码和Element的el-table类似,通过HTML声明的方式进行定义,如下所示。

<vxe-tableref="xTable"border show-overflow keep-source resizable show-overflow :data="list":edit-config="{trigger: 'click', mode: 'row', showStatus: true}">
  <vxe-columntype="seq"width="60" />
  <vxe-columnfield="name"title="姓名":edit-render="{name: 'input', attrs: {type: 'text'}}" />

  <vxe-columnfield="sex"title="性别":edit-render="{name: '$select', options: sexList}" />

  <vxe-columnfield="emailAddress"title="邮件地址":edit-render="{name: 'input', attrs: {type: 'text', placeholder: '请输入邮件地址'}}" />

  <vxe-columnfield="marriage"title="婚姻状况":edit-render="{name: '$select', options: marriageList, props: {multiple: false}}" />

  <vxe-columnfield="status"title="状态":edit-render="{name: '$select', options: StatusList, props: {multiple: false}}" />

  <vxe-columnfield="height"title="身高":edit-render="{name: '$input', props: {type: 'float', digits: 2}}" />

  <vxe-columnfield="birthDate"title="出生日期":edit-render="{name: '$input', props: {type: 'date', placeholder: '请选择日期'}}":formatter="formatDate" />
</vxe-table>

而数据的获取展示方式,和上面代码操作一样

      //获取列表,绑定到模型上,并修改分页数量
      this.listLoading = truetestUser.GetAll(param).then(data=>{
console.log(data.result.items)
this.list =data.result.itemsthis.pageinfo.total =data.result.totalCountthis.listLoading = false})

其实我们主要关注它的数据的新增,以及保存处理即可,插入或者移除记录的处理如下代码所示。

async insertEvent () {
const $table
= this.$refs.xTable
const newRecord
={}
const { row: newRow }
=await $table.insert(newRecord)
await $table.setActiveRow(newRow)
},
async removeSelectEvent () {
const $table
= this.$refs.xTable
await $table.removeCheckboxRow()
},
async removeRowEvent (row) {
const $table
= this.$refs.xTable
await $table.remove(row)
},

如果是保存,那么需要获取数据记录的更新、删除、新增几个集合的情况,然后统一通过后端进行辨别处理即可。

async saveEvent () {
const $table
= this.$refs.xTable
const { insertRecords, removeRecords, updateRecords }
=$table.getRecordset()if (insertRecords.length <= 0 && removeRecords.length <= 0 && updateRecords.length <= 0) {this.msgSuccess('数据未改动!')return}
const errMap
= await $table.validate().catch(errMap =>errMap)if(errMap) {return}this.listLoading = true try{
const body
={ insertRecords, removeRecords, updateRecords }
console.log(body)
///await XEAjax.post('https://api.xuliangzhan.com:10443/demo/api/pub/save', body) //await this.loadList() this.msgSuccess(`操作成功,新增 ${insertRecords.length} 条,更新 ${updateRecords.length} 条,删除 ${removeRecords.length} 条`)
}
catch(e) {if (e &&e.message) {this.msgSuccess(e.message)
}
}
this.listLoading = false},

通过引用对象的
getRecordset
我们可以获得相关新增、更新、删除的集合,然后通过自己的逻辑处理数据即可。

该表格组件提供了很多方便的处理案例,我们可以通过学习方式进行参考即可。

以上就是几款表格组件的效果,对比它们,一般来说,我们还是倾向于使用el-table的内置表格组件来展示数据,而且也可以通过我们的代码生成工具Database2sharp进行vue界面的快速生成,然后微调即可,而对于数据直接编辑的处理,则建议通过使用vxe-table的组件的方式进行处理,它提供了非常多强大的功能,可以满足我们直接录入数据的处理操作。