再不用写查询列表页面了 | 减少低端重复劳动 vue+iview

8,584 阅读7分钟

在群里请教了一下这样的页面有没有标准的称谓,有的叫标准的CRUD页面,有的说是表格信息页,我也不知道叫什么,上边是查询下边是列表,索性就叫查询列表页了,借助罗永浩的口头禅:”少废话,先看东西“。 演示地址

高频出现的列表页面

从去年9月份换工作以后,参与了几个项目,有数据BI平台,有可视化平台,有大屏项目,有JIRA的二次开发,种类繁多,技术栈也丰富多彩,学习到很多。

  • 基于vue的BI平台用iview搭了一套后台,
  • 基于react的熟悉了antDesign\umi\dva\bizcharts
  • 基于fis3+yog2的JIRA的二次开发
  • 最近后端同事又给过来三个基于vue-element-admin的中台系统。

说了这么多,想表达的是”查询列表页面出现的频率太高了“,不管是BI系统、可视化系统,还是toB的中台系统,就前端岗位而言,不可避免的要开发很多这样的查询列表页面,没有什么技术要求门槛,但就是得做,所谓的”低端重复劳动“。

分页组件的迭代

之前BI的项目里用iview的table和Page组件封装了一个分页组件,只需要传入表头、api地址、查询条件,就完成了,所有分页列表都是用它实现的。

接到重构之前一个中台系统的需求,拆分成三个应用,后端给过来三个基于vue-element-admin的git项目地址,粗略的瞄了一下之前的系统,几乎所有左侧频道点进去都是查询列表页面,直接引入之前的分页组件开发v0.1版本,v1.0需求越来越多,索性抽了两天时间把查询条件也一块封装了吧😃。

五分钟生成查询列表页面

查询条件一般都是key:value的形式,比如后端给出的文档如下图。

我们把查询条件和列表封装成一个组件,查询条件其实就是一个option配置,,组件必不可少的三个入参,接口地址、查询条件配置、自定义表头,开发过程中往往都是先新建.vue组件、引入查询列表组件、编写入参,然后再引入必要的工具方法,其实也很繁琐。

于是我们自己写了个生成工具,按照文档把入参填写一下,点击导出,vue组件代码直接就复制到剪切板,新建.vue文件,然后ctrl+v即可。

工具地址

日光之下无新事,页面可视化的工具很多,借用之前可视化文章里的观点,大家为什么不用可视化,可视化工具为什么没火起来。

  1. 学习成本高于开发难度
  2. 工具简单,不能满足业务需求

这么简单的小东西,能满足业务需求吗?入参类型够全吗?可以加权限判断吗? 怎么自定义表头?

组件功能设计

这个组件也是在实际开发过程中封装出来的,不同的业务场景、开发的体验、测试的反馈才能使这个组件更稳定、更好用,我们也是在一个一个的实际需求版本迭代中才把api给确定下来。

先看看组件做什么,很简单,组件分为两部分,上边是查询条件、下边分页表格。

这个组件就是在点击查询,把查询条件的结果拼成入参传入列表分页,上半部分的未来就是封装更多类型,代码也很简单,后续可以在组件代码里自己增加。

主要还是在下半部分,也可以单独使用列表组件,一起看看列表分页的主要入参吧。url是接口地址,每个列表的接口地址肯定不一样,灵活配置;searchData是要查询条件,Columns是iview的表格组件表头,在巨人的肩膀子真的是有益,具体见iview的表格api文档 可以自定义列的展示;allDate是一个钩子,可以修改后端返回的结果,比如根据权限判断是否增加操作按钮等。

剩下的就是上一页、下一页、跳转、修改页数的操作了,和后端订好格式就OK了。

列表的数据格式也有一定要求,dataCount总条数,tableData表体数据,Columns表头,pageSize每页条数,如下

{
	"code": 1,
	"msg": "",
	"data": {
		"dataCount": 444,
		"tableData": [{
			"id": 443,
			"order_no": "1016410719705984",
			"source": 10,
			"order_type": 1,
			"order_status": 1,
			"car_name": "奥迪 A6 2006款 1.8 手动"
		}, {
			"id": 443,
			"order_no": "1016410719705984",
			"source": 10,
			"order_type": 1,
			"order_status": 1,
			"car_name": "奥迪 A6 2006款 1.8 手动"
		}],
		"Columns": [{
			"key": "order_no",
			"title": "订单ID"
		}, {
			"key": "order_type",
			"title": "订单类型"
		}, {
			"key": "source",
			"title": "来源"
		}, {
			"key": "car_name",
			"title": "车型"
		}, {
			"key": "order_status",
			"title": "状态"
		}],
		"pageSize": 10
	}
}

我们再回顾一下,整个组件的功能。

  1. 查询条件组件根据文档列成配置文件并展示
  2. 用户输入后把查询条件封装,给到列表分页组件
  3. 分页根据自定义表头展示内容,完成机械的上下页、跳转等操作

说了这么多,到底有哪些api可以讲下吧?

数据格式与api

template部分

除了组件上的props,需要提一下的就是buttons这个solt了,除了页面上的搜索和重置按钮,往往需要增加其他的按钮,最常见的比如新建

url 接口地址

字符串类型,每个列表都有自己不同的地址

apiType 全局地址

字符串类型,我们ajax使用的是axios,如果是微服务的项目会有多个域名配置,把axios的实例名称填上即可,默认为http,在main.js中给vue原型指向。

// 引入微服务apis
import { http, OderApi, ReportApi} from '@/lib/apis.js'
Vue.prototype.$http = http            // 默认地址
Vue.prototype.$OderApi = OderApi      // 订单服务
Vue.prototype.$reportApi = ReportApi  // 报表服务
fromOption 表单配置

数组类型,每一个查询项为一个对象,目前仅支持四种输入项,后续会增加其他类型,本着小步快跑原则,先把文章发出来,省的要夭折在自己的笔记软件中(我太懒)。

select:下拉项 date-picker:开始-结束日期 input:输入框 selectAndInput:下拉+输入

select

{
  label:'沿途城市',     // 标签名称
  name:['cityId'],     // *入参名称  必须唯一
  type:'select',       // *元素类型  selectAndInput
  dataType:'arr',      // 数据格式    arr/json
  arrKey:{             // arr数组格式下为必填
    value:'city_id',
    key:'name'
  },
  value:'',            // *默认选中值
  options:{            // *element组件属性 
    data:[             // 列表数据 仅在select类型下可用
      {
        city_id:'',
        name:'全部'
      },
      {
        city_id:1001,
        name:'北京'
      },
      {
        city_id:1002,
        name:'上海'
      }
    ]
  }
},

下拉菜单的数据支持数组和对象两种格式,分别为arrjson表示,后续可能会优化掉这一入参,直接用类型判断。 dataTypejson时的数据格式,推荐使用json,可以减少遍历操作。

 options:{             // *element组件属性 
    data:[             // 列表数据 仅在select类型下可用
      {
       全部:'',
       北京:1001,
       上海:1002
      }
    ]
  }

date-picker

使用的elementUI的日期组件,options可以传入elementUI组件的官方属性,type目前仅支持daterangeelementUI 日期date-picker文档

注:因为目前项目是基于vue-element-admin,封装时没有考虑,结果上半部分是elementUI的表单组件,下半部分为iviewUI的表格组件,后期会统一。

{
    label:'操作时间',
    name:['start_time','end_time'],     // *入参名称2个
    type:'date-picker',
    value:'',
    options:{
        'value-format':"yyyy-MM-dd",
        type:"daterange"
    }
}

input

{
    label:'车牌号',
    name:['car_no'],    // *入参名称2个
    value:'',           // *默认值
    type:'input'
}

selectAndInput

{
    label: "ID",
    type: "selectAndInput",
    value: "",
    defaulType:"id",       // 默认选中的下拉项值
    name:                  // 下拉菜单数据
      {
        运单:'transport_order_id',
        主运单:'transport_order_main_id'
      },
    selectOption:{},       // 下拉框官网api设置
    inputOtions:{}         // 输入框官网api设置
}

以往我们在生成这类查询条件时,会特别检查一下后端的接口。 谨防接口设计为类型字段和id字段,不利于拆分为单个查询条件。

// 错误类型入参实例
{
    type:'transport_order_id',
    id:'99234i88845'
}

// 正确入参实例
{
   transport_order_main_id:'99234i88845' 
}
// 或
{
   transport_order_id:'99234i88845' 
}
Columns 自定义表头

数组类型

使用过iview的表格组件应该就很熟悉了文档

[
    { key: "order_no", minWidth:200, fixed: 'left'},
    {
      key: "order_status",
      minWidth:90,
      render:(h, params) => {
          let text = this.matchType('order_status',params)
          return h('div',text)
      }
    },
    {
      key: "source",
      render:(h, params) => {
          let text = this.matchTypeArr('source',params)
          return h('div',text)
      }
    },
    {title: "操作", key: "action",align:'center', minWidth:200, fixed: 'right',
        render:(h, params) => {

          let setingBtn = h('Button', {
                    props: {
                        type: 'primary',
                        size: 'small'
                    },
                    on: {
                        click: () => {
                            this.showSet(params.row)
                        }
                    }
                }, '发运设置')


          let editBtn = h('Button', {
                    props: {
                        type: 'primary',
                        size: 'small'
                    },
                    style:{
                        marginLeft:'10px'
                    },
                    on: {
                        click: () => {
                          this.$router.push('/constExplorer/editorLine/' + params.row.id)
                        }
                    }
                }, '班车设置')

            return h('div',[setingBtn,editBtn])
        }
    }
]
allDate 修改返回结果

函数类型

其实就是一个钩子函数,在每次翻页、搜索的时候,拿到数据并做一些修改,比如给每列加一个最小宽度,或者根据权限判断是否展示操作按钮。

addWidth(data){

    // 列最小宽度
    data.data.Columns.forEach(item => {
        item.minWidth = 170
    })

    // 操作按钮权限判断
    if(_.get(this.powers, 'oderList')){
        data.data.Columns.push({title: "操作", key: "action"})
    }
}

常用方法

日常的实际开发中,除了api,还有很多我们常用的操作,比如下拉菜单是接口,格式化表格数据等,我们来一起过一下吧,把代码贴一下,提高我们的开发效率。

下拉菜单调接口数据在组件初始化时获取下拉菜单的数据

created(){

    this.$http.get('/mock/city.json').then(res => {

        // 城市列表 数组格式
        let formIndex = _.findIndex(this.form, ['name', ['cityid']]);
        this.form[formIndex].options.data = this.form[formIndex].options.data.concat(res.data)

          })
        //  城市列表 对象格式 
        let formIndex = _.findIndex(this.form, ['name', ['cityid']]);
        this.form[formIndex].options.data = Object.assign(this.form[formIndex].options.data,res.data)

          })

        },

表格列数据格式化 比如这样的场景,查询条件中存储了状态码,我们需要根据查询条件中的数据格式化表格的列数据。

查询条件

列数据中返回的是1100这样的格式,我们封装了两个方法,分别进行格式化,后期会优化为一个方法。

// 表头格式
{
  key: "order_type",
  render:(h, params) => {
      let text = this.matchType('order_type',params)
      return h('div',text)
  }
},
{
  key: "source",
  render:(h, params) => {
      let text = this.matchTypeArr('source',params)
      return h('div',text)
  }
},

matchType(key,params){

  let formIndex = _.findIndex(this.form, ['name', [key]]);
  let formType = this.form[formIndex].options.data

  let text = _.findKey(formType, item => {
    return item == params.row[key]
  });

  return text
}

matchTypeArr(key,params){

  let formIndex = _.findIndex(this.form, ['name', [key]]);
  let formType = this.form[formIndex].options.data
  let keyName = this.form[formIndex].arrKey.value
  let text = _.find(formType, item => {
    return item[keyName] == params.row[key]
  });

  return text.name
}

按钮权限

权限管理有一些需要精确到按钮,我们有专门的接口获取权限并存在store里,在allData里进行判断,是否展示操作按钮。

addWidth(data){
    // 操作按钮权限判断
    if(_.get(this.powers, 'oderList')){
        data.data.Columns.push({title: "操作", key: "action"})
    }
}

未解决问题列表

  • 获取筛选项下拉后再请求列表接口
  • 下拉菜单数据格式判断
  • 列数据格式化方法封装
  • 权限多按钮的判断
  • 查询条件单日期类型配置项目

演示地址与源码

码云 gitee.com/nihaojob/fo…

表单演示 nihaojob.gitee.io/formandlist…

可视化生成 nihaojob.gitee.io/formandlist…