使用element封装动态配置表格

说明:最近几个月负责公司部门vue项目的前端架构任务,该组件的设计实现,主要考虑提高业务开发效率,实现表格的动态化配置,对项目中常见的业务页面,能以较少的代码量,通过简单的json配置实现表格和表单的渲染,实现代码的开发任务。要点有三:
1、表格的动态化配置,动态展现表格内容、配置化显示操作按钮,可扩展操作按钮并提供表格中自定义列模板,使用vue的render原理实现。使用到组件BaseCrud
2、新增、修改的操作弹窗,表单内容动态配置,与表格内操作实现联动。使用组件BaseDialogForm
3、表格列表的数据获取,增删改查的异步请求,都依赖封装的apiService方法,方法中抽象了list、create、update、delete、detail方法,具体代码和实现思路在另外一篇文章使用axios进行apiService的封装中讲解。

注意:crud中的表格自定义列模板依赖于cell组件,源码为下方的expand.js,需要在BaseCrud中引入使用。使用方法和iview框架的表格中自定义方法完全一致(直接使用的iview中的实现源码,哈哈~~~)。

一、BaseCrud的组件源码

<template>
  <div class="crud">
     <!--crud头部,包含可操作按钮-->
    <el-row class="crud-header">
      <el-button type="primary" size="mini" v-if="gridBtnConfig.create" @click="createOrUpdate(null)">新增        
      </el-button>
    </el-row>
    <!--crud主体内容区,展示表格内容-->
    <el-table
      :data="showGridData"
      border
      v-loading="listLoading"
      style="width: 100%">
      <el-table-column
        v-for="(item,index) in gridConfig"
        :key="index"
        :prop="item.prop"
        :label="item.label"
        show-overflow-tooltip
        :width="item.width?item.width:''">
       <template slot-scope="scope">
          <Cell
            v-if="item.render"
            :row="scope.row"
            :column="item"
            :index="scope.$index"
            :render="item.render"></Cell>
          <span v-else>{{scope.row[item.prop]}}</span>
        </template>
      </el-table-column>
      <el-table-column fixed="right" v-if="!hideEditArea" label="操作" :width="gridEditWidth?gridEditWidth:200">
        <template slot-scope="scope">
          <el-button size="mini" v-if="gridBtnConfig.update" type="primary"
                     @click="createOrUpdate(scope.row)">修改
          </el-button>
          <el-button size="mini" v-if="gridBtnConfig.delete" type="danger" @click="remove(scope.row)">删除</el-button>
          <el-button size="mini" v-if="gridBtnConfig.view" type="primary" @click="view(scope.row)">查看</el-button>
          <!--扩展按钮-->
          <el-button size="mini" @click="handleEmit(item.emitName, scope.row)"
                     v-if="gridBtnConfig.expands && gridBtnConfig.expands.length>0"
                     v-for="(item,index) in gridBtnConfig.expands" :key="index" :type="item.type?item.type:'primary'">
            {{item.name}}
          </el-button>
        </template>
      </el-table-column>

    </el-table>

    <!--crud的分页组件-->
    <div class="crud-pagination">
      <!--如果不是异步请求展示数据,需要隐藏分页-->
      <el-pagination
        v-if="isAsync"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="currentPage"
        :page-sizes="[10, 20, 30, 40]"
        :page-size="currentPageSize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="dataTotal">
      </el-pagination>
    </div>

    <!--crud按钮触发的表单弹窗-->
    <BaseDialogForm :title="dialogTitle" ref="dialogForm" :config="formConfig" :form-data="formModel"
                    @submit="dialogSubmit"></BaseDialogForm>
  </div>
</template>

<script>
  import BaseDialogForm from '@/components/BaseDialogForm/index.vue'
  import Cell from './expand';

  export default {
    name: "base-crud",
    components: {
      BaseDialogForm,
      Cell 
    },
    props: [
      // 表单标题,例如用户、角色
      'formTitle',
      // 表单配置
      'formConfig',
      // 表单的model数据
      'formData',
      // 表格配置
      'gridConfig',
      // 表格按钮配置
      'gridBtnConfig',
      // 表格死数据
      'gridData',
      // 数据接口
      'apiService',
      // 判断是否是异步数据
      'isAsync',
      //  表格编辑区域宽度
      'gridEditWidth',
      //  是否隐藏表格操作
      'hideEditArea',
    ],
    data() {
      return {
        // 新增修改模态框title
        dialogTitle: '',
        // 展示的表格数据,数据来源可能是父组件传递的固定数据,可能是接口请求数据
        showGridData: [],
        // 当前页码
        currentPage: 1,
        // 每页显示数量
        currentPageSize: 10,
        // 列表数据总数
        dataTotal: 0,
        // 表单数据
        formModel: {},
        //  表格加载状态
        listLoading: false
      }
    },
    mounted() {
      this.getData();
    },
    methods: {
      // 获取列表数据
      getData() {
        this.listLoading = true;
        let params = {
          page: this.currentPage,
          limit: this.currentPageSize
        };

        this.apiService.list(params).then(res => {
          this.showGridData = res.data.list;
          this.dataTotal = res.data.total;
          this.listLoading = false;
        }, err => {
          this.listLoading = false;
        });
      },
      createOrUpdate(item) {
        this.$refs.dialogForm.resetForm();
        // 新增时,模态框数据需要拷贝基础定义的数据模型,修改时,直接拷贝当前行数据
        this.formModel = item ? Object.assign({}, item) : Object.assign({}, this.formData) ;
        this.dialogTitle = (item ? '修改' : '新增') + this.formTitle;

        this.$refs.dialogForm.showDialog();
      },
      // 处理相应父组件的事件方法
      handleEmit(emitName,row) {
        this.$emit(emitName, row);
      },
      handleCurrentChange(page) {
        this.currentPage = page;
        this.getData();
      },
      handleSizeChange(size) {
        this.currentPageSize = size;
        this.getData();
      },
      // 模态框数据提交
      dialogSubmit(data) {
        this.apiService[data.userId ? 'update' : 'create'](data).then(res => {
          this.getData();
          this.$message.success(this.dialogTitle + '成功!');
        })
      },
      remove(data) {
        //  处理删除逻辑
      },
      view(data){
        // 处理查看详情逻辑
      }
    },
    watch: {
      // 防止表格预置数据不成功,涉及生命周期问题
      gridData() {
        this.showGridData = this.gridData;
      }
    }
  }
</script>

<style rel="stylesheet/scss" lang="scss" scoped>
  .crud {
    .crud-header {
      margin-bottom: 10px;
      line-height: 40px;
    }

    .crud-pagination {
      text-align: right;
      margin-top: 10px;
    }
  }
</style>

该组件主要提供了表格的字段动态配置,操作按钮的自定义配置,包括是否显示新增、修改、删除、查看按钮。操作区域使用属性配置,是因为一般右侧操作区域是固定的,而且如果使用宽度自适应的话,会出现宽度不够,操作按钮换行的难看效果,所以使用gridEditWidth可配置。操作区域可隐藏,部分页面中使用可能只是查询列表。show-overflow-tooltip可以在文字过长时,用省略号显示,我这里默认每个表格都使用该属性。可根据自己项目中的需求,修改代码为该属性可配置。需要注意的是,如果表格字段为空,鼠标hover时该格子有个小黑点的效果。可以考虑在代码中,判断字段为空时显示‘--’进行标识,也防止小黑点的出现。

提供了获取列表数据的方法,新增和修改的方法,依赖于BaseDialogForm组件的应用,新增和修改时会复用同一个模态框,但是通过formTitle展示不一样的标题,最后保存时通过特殊字段id进行业务逻辑的区分。扩展的操作按钮,使用handleEmit方法进行统一处理,使用事件映射完成父子组件的通信。在具体使用时,可在父组件中使用this.$refs.crud.getData()在进行业务操作后,刷新列表数据。

上诉组件源码只是提供了基本的实现思路,我在项目实际使用中,结合公司的定制化业务,实际还结合了很多组件的应用,包括查看详情模态框是根据新增和修改的动态表单进行默认生成展示,配置列表上方动态生成的高级搜索表单进行表格数据查询操作等等。具体的扩展大家可以根据实际业务进行定制化修改。

具体的使用实例会在最下方代码展示

二、表格自定义列模板,依赖的expand.js的源码

export default {
    name: 'TableExpand',
    functional: true,
    props: {
        row: Object,
        render: Function,
        index: Number,
        column: {
            type: Object,
            default: null
        }
    },
    render: (h, ctx) => {
        const params = {
            row: ctx.props.row,
            index: ctx.props.index
        };
        if (ctx.props.column) params.column = ctx.props.column;
        return ctx.props.render(h, params);
    }
};

三、BaseDialogForm组件的源码

<template>
  <el-dialog
    :title="title"
    :visible.sync="dialogVisible"
    :width="width?width:'80%'">
    <el-form :model="formModel" ref="configForm" label-width="100px">
      <el-row :gutter="16">
        <el-col :span="item.span?item.span:8" v-for="(item,index) in config" :key="index">
          <el-form-item
            :prop="item.prop"
            :rules="item.rules"
            :label="item.label"
          >
            <!--输入框表单类型-->
            <el-input v-if="item.type ==='text'" v-model="formData[item.prop]"
                      :placeholder="item.placeholder?item.placeholder:'请输入'"></el-input>
            <!--文本域表单类型-->
            <el-input v-if="item.type === 'textarea'" type="textarea" v-model="formData[item.prop]"
                      :placeholder="item.placeholder?item.placeholder:'请输入'"></el-input>
            <!--checkbox表单类型-->
            <el-checkbox-group v-if="item.type === 'checkbox'" v-model="formData[item.prop]"
                               :placeholder="item.placeholder?item.placeholder:'请选择'">
              <el-checkbox v-for="option in item.data" :label="option.id" :key="option.id">{{option.name}}</el-checkbox>
            </el-checkbox-group>
            <!--radio表单类型-->
            <el-radio-group v-if="item.type === 'radio'" v-model="formData[item.prop]"
                            :placeholder="item.placeholder?item.placeholder:'请选择'">
              <el-radio v-for="option in item.data" :label="option.id" :key="option.id">{{option.name}}</el-radio>
            </el-radio-group>
            <!--下拉选择类型-->
            <el-select v-if="item.type === 'select'" v-model="formData[item.prop]"
                       :placeholder="item.placeholder?item.placeholder:'请选择'">
              <el-option
                v-for="option in item.data"
                :key="option.id"
                :label="option.name"
                :value="option.id">
              </el-option>
            </el-select>

            <el-date-picker
              v-if="item.type === 'datepicker'"
              v-model="formData[item.prop]"
              type="date"
              :placeholder="item.placeholder?item.placeholder:'请选择日期'">
            </el-date-picker>

          </el-form-item>
        </el-col>
      </el-row>
    </el-form>

    <span slot="footer" class="dialog-footer">
    <el-button @click="dialogVisible = false">取 消</el-button>
    <el-button type="primary" @click="submitForm">确 定</el-button>
  </span>
  </el-dialog>
</template>

<script>
  export default {
    name: "base-dialog-form",
    props: [
      'title',
      'width',
      'visible',
      'config',
      'formData'
    ],
    data() {
      return {
        formModel: {},
        dialogVisible: false,
        dialogTitle: '',
      }
    },
    mounted() {
      // 将组件上的属性赋值给当前组件内变量,因为props只能单向绑定,还需要监听属性值变化进行父子组件间交互
      this.formModel = this.formData;
      this.dialogVisible = this.visible;
      this.dialogTitle = this.title;
    },
    methods: {
      // 提交表单数据
      submitForm() {
        this.$refs.configForm.validate((valid) => {
          if (valid) {
            // 让父组件接收到响应数据
            this.$emit('submit', this.formModel);
            // 关闭模态框
            this.dialogVisible = false;
          } else {
            console.log('error submit!!');
            return false;
          }
        });
      },
      // 重置表单状态
      resetForm() {
        if (this.$refs.configForm) {
          this.$refs.configForm.resetFields();
        }
      },
      // 展示模态框
      showDialog() {
        this.dialogVisible = true;
      }
    },
    watch: {
      /*实现表单数据的绑定,实时接收父组件的数据变化*/
      formData() {
        this.formModel = this.formData;
      }
    }
  }
</script>

<style rel="stylesheet/scss" lang="scss" scoped>
  .el-input{
    width: 100% !important;
  }

  .el-select{
    width: 100% !important;
  }
</style>

上诉代码大家应该都能看懂,就是通过简单的config数组提供需要动态生成的表单配置,具体包含表单控件类型、对应的字段、验证规则、placeholder文字等等所需的配置,可根据自身情况进行扩展,此处只做简单的示例

需要注意的是,在使用重置表单状态的方法中,需要预先判断组件实例是否存在,因为el-dialog的底层代码实现用了v-if,在第一次还未显示时,dom实际不存在,如果不进行判断直接使用的话,代码会报错。

四、使用示例之用户列表渲染

页面代码:

<template>
  <div>
    <BaseCrud @download="download" :apiService="userService" :grid-config="configData.gridConfig" :grid-btn-config="configData.gridBtnConfig"
              :grid-data="testData"
              :form-config="configData.formConfig" :form-data="configData.formModel" :grid-edit-width="200"
              form-title="用户" :is-async="true">
    </BaseCrud>
  </div>
</template>

<script>
  import BaseCrud from '@/components/BaseCrud/index.vue'
  import {USER_CONFIG} from './config'
  import {userService} from '@/api/user.js'

  export default {
    components: {
      BaseCrud
    },
    data () {
      return {
        testData: [],
        configData: USER_CONFIG,
        userService: userService
      }
    },
    name: 'user-list',
    mounted () {
      this.testData = [
        {
          id: '1',
          tel: '15184318420',
          name: '小白',
          email: '412412@qq.com',
          status: '1',
          create_time: '2018-04-20',
          expand: '扩展信息一',
          role: ['2']
        },
        {
          id: '2',
          tel: '13777369283',
          name: '小红',
          email: '456465@qq.com',
          status: '0',
          create_time: '2018-03-23',
          expand: 'hashashashas',
          role: ['1']
        }
      ];
    },
    methods: {
      download(row){
        console.log('点击了下载按钮',row);
      }
    }
  }
</script>

config配置:

export const USER_CONFIG = {
  gridConfig: [
    {label: '用户ID', prop: 'id', width: '100'},
    {label: '手机号(登录账号)', prop: 'tel'},
    {label: '邮箱', prop: 'email', width: '100'},
    {label: '中文名', prop: 'name'},
    {
      label: '状态', prop: 'status', render: (h, params) => {
        if(params.row.status === '0'){
          return h('el-tag', {
            props:{
              size:'mini',
              type:'warning'
            }
          },'正常');
        }else {
          return h('el-tag', {
            props:{
              size:'mini',
              type:'success'
            }
          },'禁用');
        }
      }
    },
    {label: '创建时间', prop: 'create_time'},
    {label: '扩展信息', prop: 'expand'}
  ],
  // crud的模态框表单配置,可配置表单类型,验证规则,是否必填,col-span布局可通过span参数配置
  formConfig: [
    {span: 12, label: '手机号', prop: 'tel', type: 'text'},
    {span: 12, label: '中文名', prop: 'name', type: 'text'},
    {span: 12, label: '邮箱', prop: 'email', type: 'text'},
    {
      span: 12, label: '角色',
      prop: 'roleIdList',
      type: 'checkbox',
      data: [{name: '角色一', id: '1'}, {name: '角色二', id: '2'}],
      rules: { type: 'array', required: true, message: '请选择角色', trigger: 'change' }
    },
    {
      span: 12, label: '状态',
      prop: 'status',
      type: 'radio',
      data: [{name: '正常', id: 1}, {name: '禁用', id: 0}],
      rules: {required: true, message: '请选择角色状态', trigger: 'change'}
    },
    {span: 24, label: '备注', prop: 'expand', type: 'textarea'}
  ],
  // crud的操作按钮配置,基础按钮有添加、修改、删除、查看,还可以配置扩展按钮
  gridBtnConfig: {
    create: true, update: true, delete: true, view: false,
    expands: [
      { name: '下载按钮', emitName: 'download', type: 'primary' }
    ]
  },
  // 表单基础数据类型,需要预先赋值
  formModel: {
    id: '',
    tel: '',
    name: '',
    email: '',
    status: '',
    create_time: '',
    expand: '',
    roleIdList: []
  }
};

以上是用户列表的基础配置使用示例,数据使用的是测试数据。在实际使用中应该是通过apiService从服务器端请求数据。下列截图展示了基础的配置使用效果展示。

QQ浏览器截图20180829225231.png
QQ浏览器截图20180829225250.png
QQ浏览器截图20180829225305.png

五、总结

上诉示例是基础业务的实现代码,由于实际业务中代码量稍大,逻辑稍微复杂,并未在此处进行完全展示。此处代码为删减版,可能有一些问题,可以留言进行探讨解决。表格配置实际使用中,应该会有使用过滤器的情况,还有我在项目中实际使用时,有表格多选的情况,表格内操作按钮,和表格外按钮的配置,以及涉及批量操作的如批量删除等业务。若后期大家实际需要,我会陆续分享实现方案。
如果各位大牛有更好的实现方案,也希望不吝赐教,大家共同进步。
最后,希望走过路过的朋友能个给个赞呗,谢谢~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,937评论 3 118
  • 一元做代理1美蒂赫姿眼部护理液137+1=特约代理(一瓶洗眼液137)2美蒂赫姿眼部护理液240+1=分销代理(一...
    Sunshine_girl阅读 270评论 0 0
  • 妍心刚买回来一本书<有本事文艺一辈子>,我在排队等着读,但我心里其实已经有了答案。 那就是:走着瞧! 无数次检视自...
    吕燕阅读 131评论 0 0
  • 【原图为网络,宝宝只是copy】
    团子曼阅读 175评论 0 0