手把手带你用Vue-ElementUI实现一个类似i-View的配置类table组件

2,156 阅读8分钟

本文主要是帮助提炼业务封装能力。

ElementUI和i-View应该是前端使用较多的两个UI框架了吧~

然而在使用ElementUI中的table组件的时候很多小伙伴肯定是这样:

<el-table :data="data" stripe border style="width: 100%">      
    <el-table-column prop="id" label="ID"/>      
    <el-table-column prop="name" label="名称"/>      
    <el-table-column prop="price" label="金额"/>
</el-table>

显示字段不多,乍一看好像没什么问题~~

现在产品告诉你,在金额前面加个¥,然后你一顿巴拉巴拉:

<el-table :data="data" stripe border style="width: 100%">      
    <el-table-column prop="id" label="ID"/>      
    <el-table-column prop="name" label="名称"/>      
    <el-table-column label="金额">
        <template slot-scope="scope">
            ¥{{scope.row.price}}
        </template>
    </el-table-column>
</el-table>

乍一看好像也没什么问题~~

那么现在看这个:

<el-table :data="dataList" stripe border style="width: 100%">      
    <el-table-column prop="date" label="订单ID"/>      
    <el-table-column prop="name" label="名称" width="150"/>      
    <el-table-column label="班级名称" width="150"/>      
    <el-table-column label="期别">        
        <template slot-scope="scope">¥{{ scope.row.xxx }}</template>      
    </el-table-column>      
    <el-table-column label="老师">        
        <template slot-scope="scope">¥{{ scope.row.xxx }}</template>      
    </el-table-column>      
    <el-table-column label="重复周期">        
        <template slot-scope="scope">¥{{ scope.row.xxx}}</template>      
    </el-table-column>       
    <el-table-column prop="xxx" label="开课时间"/>      
    <el-table-column prop="xxx" label="操作时间"/>      
    <el-table-column prop="xxx" label="操作人"/> 
    ......
</el-table>

是不是有超级多的代码冗余?

有的小伙伴肯定会说用v-for循环下不就行了,但是如果需要在字段上加前缀、后缀、或者一个 el-table-column里有多个字段显示呢?每次使用都各种判断?不,这显然不是我们想看到的。

我们首先看i-view是怎么样的实现:

<template>
    <Table :columns="columns" :data="data1"></Table>
</template>
<script>
    export default {
        data () {
            return {
                columns: [
                    {
                        title: 'Name',
                        key: 'name'
                    },
                    {
                        title: 'Age',
                        key: 'age'
                    },
                    {
                        title: 'Address',
                        key: 'address'
                    }
                ]
            }
        }
    }
</script>

可以看到是通过一个columns的props数组对象自定义表头~

实现基本配置功能

我们来用Element初步实现下:

首先创建一个CommTable.vue文件。
这个文件有两个props,分别是data(table的数据)columns(配置表头及显示内容)。

// CommTable.vue
<template>  
    <el-table :data="data">    
        <template v-for="({label, prop, width, fixed}, index) in columns" :key="index">      
            <el-table-column 
                :key="index" 
                :prop="prop" 
                :label="label" 
                :width="width || 'auto'" 
                :fixed="fixed || false"      
            />  
    </el-table>
</template>
<script>
export default {  
    props: {    
        data: {      
            type: Array,      
            default: () => []   
        },    
        columns: {      
            type: Array,      
            default: () => []    
        }  
    }
}
</script>

使用:

<template>
  <comm-table :data="data" >
</template>

<script>
import CommTable from './CommTable'
export default {
    components: {
        CommTable    
    }
    data(){
        return {
            data: [],
            columns: [
                { label: '名称', prop: 'name', width: 200, fixed: 'left'}
                ...
            ]
        }
    }
}
</script>

怎么样,是不是非常简单?上文已经配置好了基本的width、fixed属性的使用

实现前缀、后缀功能

接下来实现前缀和后缀,首先上配置:

// 省略部分代码
columns: [
     { label: '金额', prop: 'price', prefix: '¥', suffix: '元'}
 ]

最常见的就是在金额前面加“¥”符号,后面加单位“元”这种需求了。

代码实现:

// CommTable.vue
<template>  
    <el-table :data="data">    
        <template v-for="({label, prop, width, fixed, prefix, suffix}, index) in columns">      
            <el-table-column 
                v-if="!prefix && !suffix"
                :key="index" 
                :prop="prop" 
                :label="label" 
                :width="width || 'auto'" 
                :fixed="fixed || false"      
            />
            <el-table-column
                v-else
                :key="index"
                :label="label" 
                :width="width || 'auto'" 
                :fixed="fixed || false"      
            >
                <template slot-scope="{row, index}">
                    <!-- 当存在前缀或后缀的时候就显示出对应的前缀或后缀 -->
                    <span>{{ prefix }}{{ row[prop] }}{{ suffix }}</span>
                </template>
            </el-table-column>
        </template>
    </el-table>
</template>
<script>
export default {  
    props: {    
        data: {      
            type: Array,      
            default: () => []   
        },    
        columns: {      
            type: Array,      
            default: () => []    
        }  
    }
}
</script>

使用:

    // 省略部分代码
    data: [
        { name: '张三', price: 50 }, 
        { name: '李四', price: 80 }
    ],
    columns: [
        { label: '名称', prop: 'name', width: 200},
        { label: "金额", prop: "price", prefix: "¥", suffix: "元" }
    ]

效果:

avatar
是不是很简单?接下来实现多字段配置~

要实现的效果:

avatar
就是这样一个多字段的的单列显示需求 上配置:

    // 省略部分代码
    data: [
        { name: "小学一年级", price: 50, signup: 6, payment: 5, total: 30 },
        { name: "初中一年级", price: 80, signup: 10, payment: 15, total: 50 },
    ],
    columns: [
        { label: "班级", prop: "name", width: 200 },
        { label: "金额", prop: "price", prefix: "¥", suffix: "元" },
        { label: "已报名/已缴费/总人数", prop: ['signup', 'payment', 'total'] }
    ]

可以看到在要多字段显示的columns里prop的值变成了一个数组,大家肯定猜到要怎么实现了

实现单列多字段功能

上代码

<template>
  <div id="app">
    <el-table :data="data">
      <template
        v-for="({label, prop, width, fixed, prefix, suffix, separator = '/'}, index) in columns"
      >
        <el-table-column
          v-if="!prefix && !suffix && !Array.isArray(prop)"
          :key="index"
          :prop="prop"
          :label="label"
          :width="width || 'auto'"
          :fixed="fixed || false"
        />
        <el-table-column
          v-else
          :key="index"
          :label="label"
          :width="width || 'auto'"
          :fixed="fixed || false"
        >
          <template slot-scope="{row}">
            <!-- 当存在前缀或后缀的时候就显示出对应的前缀或后缀 -->
            <template v-if="Array.isArray(prop)">
              <span
                v-for="(item, index) in prop"
                :key="index"
              >{{ prefix }}{{ row[item] }}{{ suffix }}{{ index === prop.length - 1 ? '' : separator }}</span>
            </template>
            <template v-else>
              <span>{{ prefix }}{{ row[prop] }}{{ suffix }}</span>
            </template>
          </template>
        </el-table-column>
      </template>
    </el-table>
  </div>
</template>
    // 省略部分代码

新加了一个配置属性 separator 分隔符的配置,默认为 / 。然后对prop判断是否为一个数组,是的话进行遍历显示即可。

好了,以上就是今天的全......不,怎么可能这么简单!下面来看看这个效果:

avatar
是否见过这样的需求呢?

看配置:

data() {
    return {
      data: [
        {
          name: "小学一年级",
          price: 50,
          signup: 6,
          payment: 5,
          total: 30,
          children: [
            { studentName: '小王', sex: '男', age: 18, phone: '13800000000' },
            { studentName: '小董', sex: '男', age: 18, phone: '13800000000' },
            { studentName: '小朱', sex: '男', age: 18, phone: '13800000000' }
          ]
        }
      ],
      columns: [
        { label: "班级", prop: "name" },
        { label: "金额", prop: "price", prefix: "¥", suffix: "元" },
        { label: "已报名/已缴费/总人数", prop: ['signup', 'payment', 'total'], width: 160 },
        [
          { label: "学生", prop: "studentName" },
          { label: "性别", prop: "sex" },
          { label: "年龄", prop: "age" },
          { label: "电话", prop: "phone" },
        ]
      ]
    };
}

可以看到columns里面的需要子集显示的数据被一个数组包裹起来了
而后端返回的子集数据也是被一个children字段所包裹住的。

实现子列功能

实现:

<template>
  <div id="app">
    <el-table :data="data">
      <template
        v-for="({label, prop, width, fixed, prefix, suffix, separator = '/', _child}, index) in columns"
      >
        <el-table-column
          v-if="!prefix && !suffix && !Array.isArray(prop) && !_child"
          :key="index"
          :prop="prop"
          :label="label"
          :width="width || 'auto'"
          :fixed="fixed || false"
        />
        <el-table-column
          v-else
          :key="index"
          :label="label"
          :width="width || 'auto'"
          :fixed="fixed || false"
        >
            <template slot-scope="{row}">
                <!-- 当是子集的时候 -->
                <template v-if="_child">
                    <!-- 循环当前的children数据 -->
                    <div v-for="(child, index) in row[childType]" :key="index" class="comm-table-child">
                        <!-- 判断是否多字段显示,是则遍历否则直接显示 -->
                        <template v-if="Array.isArray(prop)">
                            <span
                            v-for="(item, index) in prop"
                            :key="index"
                            >{{ prefix }}{{ child[item] }}{{ suffix }}{{ index === prop.length - 1 ? '' : separator }}</span>
                        </template>
                        <span v-else>{{ prefix }}{{child[prop]}}{{ suffix }}</span>
                    </div>
                </template>
                <template v-else>
                    <template v-if="Array.isArray(prop)">
                        <span
                        v-for="(item, index) in prop"
                        :key="index"
                        >{{ prefix }}{{ row[item] }}{{ suffix }}{{ index === prop.length - 1 ? '' : separator }}</span>
                    </template>
                    <span v-else>{{ prefix }}{{ row[prop] }}{{ suffix }}</span>
                </template>
            </template>
        </el-table-column>
      </template>
    </el-table>
  </div>
</template>

<script>
export default {
    props: {    
        data: {      
            type: Array,      
            default: () => []   
        },    
        columns: {      
            type: Array,      
            default: () => []    
        },
        childType: {
            type: String,
            default: 'children'
        }
    },
    created() {
        this.columns.forEach((item, index) => {
            if (Array.isArray(item)) {
                item.forEach(item => item._child = true)
                this.columns.splice(index, 1, ...item)
            }
        })
    }
};
</script>
<style>
.comm-table-child {
  position: relative;
}
.comm-table-child:not(:first-child) {
  padding-top: 10px;
}
.comm-table-child:not(:first-child)::after {
  content: "";
  position: absolute;
  left: -10px;
  top: 5px;
  background: #ebeef5;
  width: calc(100% + 20px);
  height: 1px;
}
</style>

这里就需要CSS的支持了。在初始化阶段先遍历columns这个数组,判断item是否为数组,是的话展开并替换掉原来的数组。
并且(重点)给定一个其为子数组的标识(这里我习惯在自定义添加的属性前面加上_来区分)。
这里props还新增了一个childType属性,并给定其默认值为children对应data数据里的子集字段children。

tips:这里在子组件修改了父组件的数据,但是由于其引用类型不会报错,但还是推荐深拷一份进行操作~

实现render功能

来看看i-view的columns配置里:

// 部分代码省略
columns: [
    {
        title: 'Name',
        key: 'name',
        render: (h, params) => {
            return h('div', [
                h('Icon', {
                    props: {
                        type: 'person'
                    }
                }),
                h('strong', params.row.name)
            ]);
        }
    }
]

可以看到直接把render的写在了配置文件,第一个参数h就是this.$createElement了!知道这样就好实现了。

上配置:

columns: [
    {
      label: "班级",
      prop: "name",
      render: (h, row) => {
        return h('strong', row.name)
      }
    },
    { label: "金额", prop: "price", prefix: "¥", suffix: "元" },
    { label: "已报名/已缴费/总人数", prop: ['signup', 'payment', 'total'], width: 160 },
    [
      { label: "学生", prop: "studentName" },
      { label: "性别/年龄", prop: ['sex', 'age'] },
      { label: "电话", prop: "phone" },
    ]
]

这里只实现一个加粗的name字段。

实现代码:

<template>
  <div id="app">
    <el-table :data="data">
      <template
        v-for="({label, prop, width, fixed, prefix, suffix, separator = '/', _child, _render}, index) in columns"
      >
        <el-table-column
          v-if="!prefix && !suffix && !Array.isArray(prop) && !_child && !_render"
          :key="index"
          :prop="prop"
          :label="label"
          :width="width || 'auto'"
          :fixed="fixed || false"
        />
        <el-table-column
          v-else
          :key="index"
          :label="label"
          :width="width || 'auto'"
          :fixed="fixed || false"
        >
          <template slot-scope="{row}">
            <template v-if="_child">
              <div v-for="(child, index) in row.children" :key="index" class="comm-table-child">
                <template v-if="Array.isArray(prop)">
                  <span
                    v-for="(item, index) in prop"
                    :key="index"
                  >{{ prefix }}{{ child[item] }}{{ suffix }}{{ index === prop.length - 1 ? '' : separator }}</span>
                </template>
                <span v-else>{{ prefix }}{{child[prop]}}{{ suffix }}</span>
              </div>
            </template>
            <!-- 当存在前缀或后缀的时候就显示出对应的前缀或后缀 -->
            <template v-else>
              <template v-if="Array.isArray(prop)">
                <span
                  v-for="(item, index) in prop"
                  :key="index"
                >{{ prefix }}{{ row[item] }}{{ suffix }}{{ index === prop.length - 1 ? '' : separator }}</span>
              </template>
              <template v-else>
                <span v-if="_render">
                <!-- 重点!!! 调用一个函数并把render方法处理后的VNODE以参数形式传入,然后在slot里显示出来 -->
                  {{ createNode('_render_'+index, _render(row)) }}
                  <slot :name="'_render_'+index"></slot>
                </span>
                <span v-else>{{ prefix }}{{ row[prop] }}{{ suffix }}</span>
              </template>
            </template>
          </template>
        </el-table-column>
      </template>
    </el-table>
  </div>
</template>
// 省略部分代码
created() {
    this.columns.forEach((item, index) => {
      if (Array.isArray(item)) {
        item.forEach(item => item._child = true)
        this.columns.splice(index, 1, ...item)
      }
      if (item.render && typeof item.render === 'function') {
        item._render = row => item.render(this.$createElement, row)
      }
    })
  },
  methods: {
    createNode(key, vnode) {
      this.$slots[key] = vnode
    }
  }

效果:

avatar
好了,大功告成!

以上只实现了非多级字段的render函数,还有操作按钮、子列的checkbox拓展等更多功能可以实现,可拓展性非常高~

结语

Element、i-view这类框架只是提供了一些常用的组件功能,项目中不能只单纯的做各种搬运工作!要做到按需封装,解决项目里存在的各类痛点、代码冗余等问题才是一个合格的前端工程师应该做的。

最后,2019~一起加油!