vue巧妙使用extend进行表格行内编辑

138 阅读1分钟

工作中需要一个表格可以编辑,其中包含下拉框等各种操作,所以就想到了使用extend进行修改table的cell

具体思路:

  1. 先做一个input组件用来修改数据,并将修改后的数据保留
  2. 再做一个span标签用来替代原始表格的cell 准备起来:
  • input.vue
<template>
  <div class="cell">
    <el-input ref="elInputRef" v-model.trim="cellValue" :placeholder="`请输入${placeholder}`" clearable @clear="handleInputClear" @blur="handleChangePicker" />
  </div>
</template>
<script>
export default {
  name: 'FilterInput',
  props: {
    cellDom: Node,
    cellValue: { type: [String, Number], default: '' },
    placeholder: { type: [String, Number], default: '' },
    saveRowData: { type: Function, default: () => {} },
    row: { type: Object, default: () => {} },
    property: { type: String, default: '' }
  },
  data() {
    return {
      searchData: null
    }
  },
  mounted() {
    this.$refs['elInputRef'].focus()
  },
  methods: {
    handleChangePicker(val) {
      if (this.saveRowData) {
        return this.saveRowData({
          cellValue: this.cellValue,
          cellDom: this.cellDom,
          row: this.row,
          property: this.property
        })
      }
      this.$emit('changeModel', this.cellValue)
    },
    handleInputClear(val) {
      this.$emit('clear', this.cellValue)
    }
  }
}
</script>

<style lang="scss" scoped>
.cell {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  box-sizing: border-box;
  padding: 0 8px;
}
</style>

  • span.vue
<script>
    export default {
      props: {
        cellValue: {
          type: [String, Number],
          default: ''
        }
      },
      render(h) {
            return h('span', { class: 'cell', domProps: { innerHTML: this.cellValue ?? '' }})
      }
    }
</script>

如果是下拉框的话那就需要一个select组件

  • select.vue
<template>
  <el-select v-model="cellValue" :placeholder="`请选择${placeholder}`" clearable @change="handleChangePicker" @clear="handleSelectClear">
    <el-option v-for="item in selectList" :key="item.key " :label="item.label" :value="item.label" />
  </el-select>
</template>
<script>
export default {
  name: 'FilterSelect',
  props: {
    cellDom: Node,
    cellValue: { type: [String, Number], default: '' },
    placeholder: { type: [String, Number], default: '' },
    saveRowData: { type: Function, default: () => {} },
    row: { type: Object, default: () => {} },
    property: { type: String, default: '' },
    selectList: { type: Array, default: () => {} },
    handleChange: { type: Function, default: () => {} }
  },
  data() {
    return {
      searchData: null
    }
  },
  methods: {
    handleChangePicker(val) {
      if (this.saveRowData) {
        return this.saveRowData({
          cellValue: this.cellValue,
          cellDom: this.cellDom,
          row: this.row,
          property: this.property
        })
      }
      this.$emit('changeModel', this.cellValue)
    },
    handleSelectClear(val) {
      this.$emit('clear', this.searchData)
    }
  }
}
</script>

当然不能忘记了推荐搜索功能

  • input-auto.vue
<template>
  <div class="cell">
    <el-autocomplete ref="refAuto" v-model.trim="cellValue" :fetch-suggestions="handleQuerySearch" :debounce="1200" :placeholder="`请输入${placeholder}`" clearable @clear="handleInputClear" @blur="handleChangePicker" @select="handleAutoSel" />
  </div>
</template>
<script>
export default {
  name: 'FilterInput',
  props: {
    cellDom: Node,
    cellValue: { type: [String, Number], default: '' },
    placeholder: { type: [String, Number], default: '' },
    saveRowData: { type: Function, default: () => {} },
    row: { type: Object, default: () => {} },
    property: { type: String, default: '' },
    fetchSuggestions: { type: Function, default: () => {} },
    autoSelect: { type: Function, default: () => {} }
  },
  data() {
    return {
      searchData: null
    }
  },
  mounted() {
    this.$nextTick(_ => this.$refs['refAuto'].focus())
  },
  methods: {
    handleChangePicker(val) {
      if (this.saveRowData) {
        return this.saveRowData({
          cellValue: this.cellValue,
          cellDom: this.cellDom,
          row: this.row,
          property: this.property,
          params: val
        })
      }
      this.$emit('changeModel', this.cellValue)
    },
    handleInputClear(val) {
      this.$emit('clear', this.cellValue)
    },
    handleQuerySearch(str, cb) {
      if (!str) return
      this.fetchSuggestions(str, cb)
    },
    handleAutoSel(val) {
      this.autoSelect(val)
    }
  }
}
</script>

<style lang="scss" scoped>
.cell {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  box-sizing: border-box;
  padding: 0 8px;
}
</style>

  • 还需要准备用来继承的js,extend.js
<script>
import Vue from 'vue'
import FilterInput from '@/components/common/children/table-filter-input'
import definedSpan from '@/components/common/children/table-extend-span'
import InputAuto from '@/components/common/children/table-input-auto'
import defSelect from '@/components/common/children/table-filter-select'

const StarkInput = Vue.extend(FilterInput)
const StarkSpan = Vue.extend(definedSpan)
const StarkAuto = Vue.extend(InputAuto)
const StarkSel = Vue.extend(defSelect)

export default {
  StarkInput,
  StarkSpan,
  StarkAuto,
  StarkSel
}

一切准备就绪,我们开始组合

  • stark-three.vue
<script>
import ExtendComponents from './extend-data'
import tablePrivate from '../children/table-private'
import { guid } from '@/utils/utils'

export default {
  components: { tablePrivate },
  props: {
    tableColumn: { type: Array, default: () => [] },
    tableDataList: { type: Array, default: () => [] },
    tableHeight: { type: Number, default: 90 },
    valueChange: { type: Function, default: () => {} },
    editDic: { type: [Array, String, Boolean], default: false },
    headDic: { type: [Array, String], default: '' },
    headQuery: { type: Function, default: void 0 },
    selection: { type: Boolean, default: false },
    sortable: { type: Boolean, default: false },
    private: { type: Boolean, default: false },
    cellStyle: { type: Function, default: () => {} },
    fetchSuggestions: { type: Function, default: () => {} },
    autoSelect: { type: Function, default: () => {} },
    selectList: { type: Function, default: () => {} }
  },
  data() {
    return {
      oldCellValue: null,
      oldPopover: null,
      dataForm: {}
    }
  },
  methods: {
    handleReset() {
      this.dataForm = this.$options.data.call(this).dataForm
    },
    handleCellClassName({ row, rowIndex }) {
      row._index = rowIndex
    },
    handleDblclick(row, column, cell, event) {
      if (!this.editDic) return
      if (column.type === 'index' || column.type === 'selection') return
      this.oldCellValue = row[column.property]
      const cellValue = row[column.property]
      const dom = this.handleEditDic(column)
      if (dom === false) return
      new ExtendComponents[dom]({
        propsData: {
          cellValue: cellValue,
          saveRowData: this.handleSaveRowData,
          cellDom: cell, row: row, property: column.property,
          selectList: this.selectList(column) ?? void 0,
          fetchSuggestions: (val, cb) => this.fetchSuggestions(val, cb, row, column) ?? void 0,
          autoSelect: (val) => this.autoSelect(val, row, column) ?? void 0
        }
      }).$mount(cell.children[0])
    },
    handleSaveRowData(params) {
      if (params.cellValue === this.oldCellValue) {
        console.log()
      } else {
        params.row[params.property] = params.cellValue
        this.valueChange(params)
        this.$set(this.tableDataList, params.row._index, params.row)
      }
      new ExtendComponents.StarkSpan({
        propsData: {
          cellValue: params?.cellValue
        }
      }).$mount(params.cellDom.children[0])
    },
    handleRowContext(row, column, event) {
      event.preventDefault()
      this.$emit('contextmenu', this.$contextmenu, event, row)
    },
    handleSelectionChange(val) {
      this.$emit('selChange', val)
    },
    handleEditDic({ property }) {
      if (property === this.editDic || (Array.isArray(this.editDic) && this.editDic.includes(property))) return 'StarkInput'
      if (Array.isArray(this.editDic) && this.editDic?.[0]?.['type']) {
        const ar = this.editDic.find(item => { if (item.property === property) return item })
        return ar?.['type'] ?? false
      }
      return false
    },
    handleSift(down, $event) {
      if (this.oldPopover === down.remark) return (this.oldPopover = null)
      this.oldPopover = down.remark
      const sibling = $event.target.parentNode.childNodes?.[0]
      const xy = document.body.offsetWidth - $event.x
      this.$nextTick(_ => {
        let x = -(sibling.offsetWidth / 2)
        if (xy < 200) { x = -200 }
        sibling.style.left = `${$event.x + x}px`
        sibling.style.top = `${$event.y + (sibling.offsetHeight / 4)}px`
      })
    },
    handleInput(event, column) {
      event?.constructor === String ? event = event.replace(/^\s+|\s+$/g, '') : null
      this.$set(this.dataForm, column.property, event)
    },
    handleDatePicker(item) {
      if (!item?.domType.includes('date')) return {}
      const agr = {
        type: item?.dateType,
        defaultTime: item?.dateType === 'daterange' ? ['00:00:00', '23:59:59'] : '',
        valueFormat: item?.valueFormat ?? 'yyyy-MM-dd HH:mm:ss',
        ...item?.place
      }
      return agr
    },
    handleCancel(val) { this.oldPopover = null },
    handleQuery(val) {
      if (this.headQuery) return this.headQuery({ data: this.dataForm })
    },
    handleTh(h, { column }) {
      console.error(this.headDic)
    },
    handleDomTh(h, { column }, down) {
      const labelLong = column.label.length
      const size = 32
      column.minWidth = labelLong * size + 32
      return h('div', [
        h('span', column.label),
        h('el-popover', {
          id: `ref-${down.remark}`,
          ref: `ref-${down.remark}`,
          attrs: { 'popper-class': 'table-popover', 'transition': 'el-zoom-in-top', value: down.remark === this.oldPopover },
          scopedSlots: { reference: () => h('i', {
            class: ['iconfont', 'icon-loudou', 'icon-cursor', `${this.dataForm[column.property] ? 'selected' : ''}`],
            on: { click: (e) => this.handleSift(down, e) }})
          }
        },
        [
          h('el-row', { class: 'popover-body flex-justify-start' }, [
            h('span', `${column.label}:`),
            h(down.domType, {
              attrs: {
                placeholder: `请搜索${column.label}`,
                clearable: true,
                value: this.dataForm?.[column.property],
                ...this.handleDatePicker(down)
              },
              on: { input: (event) => this.handleInput(event, column) }
            })
          ]),
          h('el-row', { class: 'popover-foot' }, [
            h('el-button', { domProps: { innerHTML: '取消' }, on: { click: () => this.handleCancel(column) }}),
            h('el-button', { attrs: { type: 'primary' }, domProps: { innerHTML: '查询' }, on: { click: () => this.handleQuery(column) }})
          ])
        ])
      ])
    }
  },
  render(h) {
    const fc = () => {
      if (!Array.isArray(this.tableColumn)) return []
      return this.tableColumn.map(item => {
        return h('el-table-column', {
          attrs: {
            prop: item.identification, label: item.remark, align: 'center', sortable: item?.sortable ?? false,
            showOverflowTooltip: true, key: item?.remark || item?.id || guid(),
            'render-header': item?.domType ? (h, { column, $index }) => this.handleDomTh(h, { column, $index }, item) : void 0
          },
          scopedSlots: {
            default: (this.private && item.identification?.includes('mobile')) ? ({ row }) => h('table-private', { attrs: { cellValue: row[item.identification] }}) : void 0
          }
        })
      })
    }
    const sel = () => {
      if (!this.selection) return
      return h('el-table-column', { attrs: { type: 'selection' }})
    }
    const sort = () => {
      if (!this.sortable) return
      return h('el-table-column', { attrs: { type: 'index' }})
    }
    return h('div', { attrs: { id: 'stark' }}, [
      h('el-table', {
        ref: 'refTable',
        attrs: {
          cellClassName: this.cellClassName, stripe: true, border: true, lazy: true,
          data: this.tableDataList, height: '100%', 'cell-style': this.cellStyle,
          'row-class-name': this.handleCellClassName
        },
        on: {
          'cell-dblclick': this.handleDblclick,
          'row-contextmenu': this.handleRowContext,
          'selection-change': this.handleSelectionChange
        },
        directives: [{
          name: 'tableHeight', value: { bottomOffset: this.tableHeight }, arg: { bottomOffset: this.tableHeight }
        }]
      },
      [sel(), sort(), ...fc()])
    ])
  }
}
</script>

搞定,这样就可以直接使用了,当然选项必须选择改变才可以,留到下次更新吧,这次累了