封裝Vue Element的可編輯table表格組件

前一段時間,有博友在我那篇封裝Vue Element的table表格組件的博文下邊留言說有沒有那種「表格行內編輯」的封裝組件,我當時說我沒有封裝過這樣的組件,因為一直以來在實際開發中也沒有遇到過這樣的需求,但我當時給他提供了一個思路。

時間過去了這麼久,公司的各種需求也不停地往外冒,什麼地圖圖表、表格行內編輯、動態新增表單等等,只有你做不到,沒有產品想不到,賊雞兒累。再加上很快又要過年了,大家工作的心態基本呈直線下滑趨勢而玩忽職守、尸位素餐以致飽食終日。只是話雖如此,但越是到年底,需求開發卻越是緊急,平時可能一兩周的開發任務,現在卻要壓縮到一周左右就要完成,苦不堪言。這不公司剛剛評完了需求,年前就讓開發完成並提測,說是等年後來了,測試同學搞定後就上線。

話說這表格行內編輯,不光要在表格一行內實現文字的編輯,而且還要實現可新增一行或多行表格行內編輯的功能,同時還希望實現表格行內表單的正則驗證。聽著複雜,想著實現起來也複雜,其實不然,我們完全可以參照element動態增減表單項的模式來搞。原理大概其就是利用每一行的索引來設置每一個表單所需的prop和v-model,如果需要新增一行可編輯的表格行,只需往數據源數組中push一行相同的數據即可。

多說一句,年底了,這馬上就要放假了,公司里很多人已經回老家了,我們這些留下來的人有一個算一個實在是沒心思工作,但你以為這就可以放鬆了?可以摸摸魚、劃划水了?美得你。聽沒聽說過一個女人:亞里士多德的妹妹,珍妮瑪士多?

不過話又說回來,我們作為打工人,本職工作就是打工。你不工作,你還有臉稱自己是打工人嗎?你不是打工人,你連飯都吃不到嘴裡,你還有臉說自己是「乾飯人」?你還真想「十年一覺揚州夢」?東方不亮西方亮,二哈啥樣你啥樣。好好乾活吧你!!!

這兩天,趁著中午休息的時候,就把前一段時間加班加點完成的開發需求中的一個表格行內編輯的封裝組件給發出來,茲當是給大家又提供了一個輪子亦或是多了一種選擇吧。

照例先來張效果圖:

1、封裝的可編輯表格組件TableForm.vue

<template>
  <el-form :model="form" ref="form" size="small">
    <el-form-item v-if="!isDetail">
      <el-button type="primary" @click="add">新增</el-button>
    </el-form-item>
    <el-table :data="form.list" border>
      <el-table-column v-for="x in columns" :key="x.prop" :label="x.label" :prop="x.prop" v-bind="x.attr">
        <template slot-scope="{row, $index}">
          <t-text v-if="!x.edit" :row="{x, row}" />
          <template v-else>
            <t-text v-if="isDetail" :row="{x, row}" />
            <t-input v-else v-model="row[`${x.prop}`]" v-bind="componentAttrs(x, $index)" class="width100" />
          </template>
        </template>
      </el-table-column>
    </el-table>
    <el-form-item v-if="!isDetail" style="margin-top:18px;">
      <el-button type="primary" @click="submit">提交</el-button>
      <el-button @click="reset">重置</el-button>
    </el-form-item>
  </el-form>
</template>

<script>
export default {
  props: {config: Object},
  computed: {
    isDetail(){
      const { isDetail = false } = this.config || {}
      return isDetail
    }
  },
  components: {
    TInput: {
      functional: true,
      props: ['prop', 'rules', 'type', 'options'],
      render: (h, {props: { prop, rules, type, options = [] }, data, listeners: {input = _.identity}}) => {
        const children = h => {
          if(type == 'select') return h('el-select', {class: 'width100', props: {...data.attrs}, on: {change(v){input(v)}}}, options.map(o => h('el-option', {props: {...o, key: o.value}})))
          else if(type == 'checkbox') return h('el-checkbox-group', {props: {...data.attrs}, on: {input(v) {input(v)}}}, options.map(o => h('el-checkbox', {props: {...o, label: o.value, key: o.value}}, [o.label])))
          else if(type == 'switch') return h('el-switch', {props: {activeColor: '#13ce66'}, ...data})
          else if(type == 'date') return h('el-date-picker', {props: {type: 'date', valueFormat: 'yyyy-MM-dd',}, ...data})
          return h('el-input', data)
        }

        return h('el-form-item', {props: {prop, rules}}, [children(h)])
      }
    },
    TText: {
      functional: true,
      props: ['row'],
      render: (h, {props: { row: { x, row } }}) => {
        if(!row[`${x.prop}`]) return h('span', '-')
        else if(x.format && typeof x.format == 'function') return h('span', x.format(row))
        else return h('span', row[`${x.prop}`])
      }
    },
  },
  data(){
    const { columns = [], data = [] } = this.config || {}
    return {
      form: {
        list: (data && data.length > 0) ? data.map(n => columns.reduce((r, c) => ({...r, [c.prop]: n[c.prop] == false ? n[c.prop] : (n[c.prop] || (c.type == 'checkbox' ? [] : ''))}), {})) : [columns.reduce((r, c) => ({...r, [c.prop]: c.type == 'checkbox' ? [] : (c.type == 'switch' ? false : '')}), {})]
      },
      columns,
      rules: columns.reduce((r, c) => ({...r, [c.prop]: { required: c.required == false ? false : true, message: c.label + '必填'}}), {})
    }
  },
  methods: {
    componentAttrs(item, idx){
      const {type, label} = item, attrs = Object.fromEntries(Object.entries(item).filter(n => !/^(prop|edit|label|attr|format)/.test(n[0]))),
      placeholder = (/^(select|el-date-picker)/.test(type) ? '請選擇' : '請輸入') + label
      Object.assign(attrs, {prop: `list.${idx}.${item.prop}`, rules: this.rules[item.prop]})
      return {...attrs, placeholder}
    },
    add(){
      const { columns = [] } = this.config || {}, obj = columns.reduce((r, c) => ({...r, [c.prop]: c.type == 'checkbox' ? [] : (c.type == 'switch' ? false : '')}), {})
      this.form.list.push(obj)
    },
    submit(){
      this.$refs.form.validate((valid) => {
        if (valid) {
          this.$emit('submit', this.form.list)
        }
      })
    },
    reset(){
      this.$refs.form.resetFields();
    },
  }
}
</script>
<style scoped>
.width100{width: 100%;}
</style>

本次封裝的可編輯的表格組件,基本把大家在表格中內嵌的一些常用表單如:input輸入框、select下拉框/選擇器、日期選擇器、checkbox複選框、switch開關等都封裝進去了,大家只需根據自己的實際需求去添加不同的type就可以了,如果你還有其他的表單組件需要加進去,你自己按照我這個套路給封裝進去就完事了。

另外,本次封裝有幾個點,大家注意下:

1)本次封裝的組件,不光可以實現表格行內的編輯,同樣當你編輯完成下次需要回顯這些數據的時候,你只需多傳一個isDetail參數就可以了,該參數默認為false。你往這兒看:

computed: {
  isDetail(){
    const { isDetail = false } = this.config || {}
    return isDetail
  }
}

頁面當中也加了這個isDetail計算屬性的判斷。

2)也許有同學已經注意到了,在本次封裝所用到的data數據對象中,有一串很長的實現方法:

list: (data && data.length > 0) ? data.map(n => columns.reduce((r, c) => ({...r, [c.prop]: n[c.prop] == false ? n[c.prop] : (n[c.prop] || (c.type == 'checkbox' ? [] : ''))}), {})) : [columns.reduce((r, c) => ({...r, [c.prop]: c.type == 'checkbox' ? [] : (c.type == 'switch' ? false : '')}), {})]

這段程式碼是幹嘛滴的呢?

它兩種用途:

  • 沒有數據需要回顯時的可編輯表格的數據源

  • 有數據需要回顯時的可編輯表格的數據源

n[c.prop] == false ? n[c.prop] : ...這段程式碼的判斷或許有人會疑惑。大家要知道element的switch組件的值是非false既true的,不加這個判斷,當數據回顯時,switch為false的值就回顯不出來。

2、使用方法:

<template>
  <TableForm :config="config" @submit="submit" style="margin:20px;" />
</template>

<script>
import TableForm from "./TableForm";

const repayTypeList = {
   averageCapital: '等額本金',
   averageInterest: '等額本息'
},
columns = [
  { prop: 'repaymentMethod', label: '還款方式', attr: {width: '180'}, format: ({ repaymentMethod }) => repayTypeList[repaymentMethod]},
  { prop: 'productPer', label: '期數', attr: {width: '180'}, format: ({ productPer }) => `${+ productPer + 1}期(${productPer}個月)` },
  { prop: 'costRate', label: '成本利率', attr: {minWidth: '110'}, edit: true, type: 'select', options: [{label: '5%', value: '5'}, {label: '10%', value: '10'}] },
  { prop: 'price', label: '單價', attr: {minWidth: '110'}, edit: true },
  { prop: 'company', label: '所屬公司', attr: {minWidth: '110'}, edit: true },
  { prop: 'product', label: '產品', attr: {minWidth: '110'}, edit: true, type: 'checkbox', options: [{label: '橘子', value: 'orange'}, {label: '蘋果', value: 'apple'}] },
  { prop: 'date', label: '日期', attr: {minWidth: '110'}, edit: true, type: 'date', required: false, },
  { prop: 'opt', label: '鎖定', attr: {minWidth: '110'}, edit: true, type: 'switch' },
]

export default {
  components: {
    TableForm,
  },
  data(){
    return {
      config: {
        columns,
        data: [],
      },
    }
  },
  created(){
    this.config.data = [
      {repaymentMethod: 'averageCapital', productPer: '1', price: '5', company: 'Google上海', date: '2021-01-03', opt: false},
      {repaymentMethod: 'averageInterest', productPer: '3', costRate: '10', price: '', company: '雅虎北京', opt: true},
      {repaymentMethod: 'averageInterest', productPer: '5', costRate: '5', price: '100', company: '上海你真美', opt: false},
    ]
  },
  methods: {
    submit(res){
      console.log(res)
    }
  }
}
</script>

在使用的過程中,有一個需要注意的地方就是在columns數組中有一個required的屬性,該屬性默認為true,這個屬性主要是用來控制當前的表單是否需要必填的校驗的。還有需要說明的是,本次封裝只是封裝了每個表單是否需要必填的正則校驗rules,沒有對其他的正則驗證如數字類型、小數位數等加以封裝,如有需要你可以自行添加。

最後,通過子組件觸發父組件的submit函數的方式來獲取表格中表單的輸入值。