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

豫見陳公子發表於2021-02-05

前一段時間,有博友在我那篇封裝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)封裝的程式碼中還有這麼一個判斷<t-text v-if="!x.edit" :row="{x, row}" />,這個判斷也有點意思。大家在使用可編輯表格的時候,每一行的資料並不都是需要編輯的,例如本文開頭貼出的效果圖中表格的前兩列就不需要編輯,就只是文字展示,但其他列基本都是需要編輯的,那麼這個判斷就是用在這裡的。

3)也許有同學已經注意到了,在本次封裝所用到的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: '谷歌上海', 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函式的方式來獲取表格中表單的輸入值。

相關文章