likes
comments
collection
share

使用element实现EditTable表格行内编辑

作者站长头像
站长
· 阅读数 21

使用react我们可以用ant.design的EditableProTable,现成的行内编辑组件,并且封装的很好,我之前也有项目使用过,虽然有的地方配置灵活度不够高,但是自己稍微做些改动也能使。那么使用vue的话怎么办呢?vue常用的组件库element不论什么版本并没有提供表格的行内编辑功能,在github也没有搜到符合自己设想的案例,有一些要么就是验证做的不好,要么就是交互方式不是自己想要的,接下来我们就按照 design的EditableProTable来使用vue针对 element 封装一层,完成灵活的行内编辑。

如何编辑?

首先我们要知道什么是行内编辑, 这里我们先来看 antd可编辑表格 是什么样子。

  • 这里是我在项目中使用到的,使用在了业务中的修改以及新增功能中, 其强大的优势就是可以使用jsx 语法作为 render 自己对行内可编辑的组件进行定制化~ 使用element实现EditTable表格行内编辑 使用element实现EditTable表格行内编辑
  • 使用jsx 来对行内编辑 和 非编辑状态渲染进行重置 使用element实现EditTable表格行内编辑 更多详细的功能可以点击上面的超链接,访问antdPro的文档进行查看即可。 那么我们需求就是,想要一个和上面这种差不多的一个行内编辑的功能,但是是使用vue来搞,目前我这个项目使用的是 vite + vue3 + elementPlus, 最后可以到我的代码仓库查看示例
  • 在这之前我在网上搜了下,看有没有别人开源出来的一些可编辑的方案, 要么就是这种,可编辑的交互是点击对应的行,弹出提示编辑 使用element实现EditTable表格行内编辑 要么就是这种只做了最简单的封装,表单验证,插槽等都没有做 使用element实现EditTable表格行内编辑 再有这种可编辑在表头的,属于是可配置的搜索列了 使用element实现EditTable表格行内编辑
  • 当然还有一个最终极的方案,就是使用 ag-grid, 这是属于找到的可编辑表格的天花板了,试了一下他的 demo 性能也优化的很流畅, 不过其功能过于强大,而文档又没有中文文档,社区对其深入研究的案例也不多,而且针对很多功能是要付费的,比如下面的 e 就是付费使用,我们也不是要搞一个多么强大的,所以最终没有选择这个。 使用element实现EditTable表格行内编辑

二次封装

其实我这里应该属于三次封装了哈哈,因为我是在社区上已经实现的把 Element 进行了一次二次封装基础之上进行改造,链接===> juejin.cn/post/716606… , 我没有实现 EditableProTable 的多行编辑,因为多行编辑处理的状态可能更多更复杂,而我的业务对于这类需求又不强烈,暂时先不考虑。

  • 这里最核心的就是,点击编辑按钮,切换到可编辑的组件,如下
<el-table-column
  v-bind="item"
  v-slot="scope"
  :showOverflowTooltip="
    item.showOverflowTooltip ?? item.prop !== 'operation'
  "
  :sortable="item.sortable"
  :sort-method="(a, b) => a.prop - b.prop"
  v-if="!item.type && item.prop && item.prop !== 'operation'"
  :align="item.align ?? 'center'"
>
  <EditTableColumn
    :column="item"
    :realTimeVerification="realTimeVerification"
    :tableData="scope.row"
    :editData="editData"
    @validateHandle="validateHandle"
    v-if="editData && editData.id === scope.row.id"
  />
  <TableColumn v-else :column="item" :tableData="scope.row" />
</el-table-column>

这里我看首先要清楚, 这个 el-table-column 是什么,没错,他就是我们的每一行,其中的item 就是我们 column 中的每一个对象了, 我在这里都是只简述关键业务逻辑,全部代码请 clone 自行查看。 那么我们就需要判定这一列在什么情况下是可编辑的 什么情况下是不可编辑的, 这里使用 editData, 当点击编辑按钮的时候,获取当前这一数据,并且存储, 我们拿当前存储的可编辑这一行的数据去和当前表格对比, 这里使用的是 id, 源于我们的列表数据正常情况下必带的是 id 而且唯一。

  • 第二核心就是表单验证,可编辑组件的模块如下
<!--
 * @Description: 可编辑行部分的单独配置
 * @Autor: codeBo
 * @Date: 2023-05-15 15:37:52
 * @LastEditors: gjzxlihaibo@163.com
 * @LastEditTime: 2023-07-25 15:49:01
-->
<template>
  <div>
    <el-form
      :model="ruleForm"
      ref="ruleFormRef"
      :rules="rules"
      :show-message="false"
      @validate="validateHandle"
    >
      <template v-if="column.valueType">
        <el-form-item :prop="column.prop">
          <el-tooltip
            placement="top"
            effect="dark"
            :visible="realTimeVerification && visible"
            :content="errMsg || '请输入'"
          >
            <el-input
              v-bind="$props.column.editable"
              v-if="column.valueType === ValueType.Input"
              v-model="ruleForm[column.prop]"
            />
            <!-- number 组件有bug, change 事件无法实时捕获, input事件无法捕获到清空以后得错误, 只能使用 blur 事件  -->
            <el-input-number
              v-bind="$props.column.editable"
              v-else-if="column.valueType === ValueType.InputNumber"
              v-model="ruleForm[column.prop]"
            />
            <el-select
              v-bind="$props.column.editable"
              v-else-if="column.valueType === ValueType.Select"
              v-model="ruleForm[column.prop]"
            >
              <el-option
                v-for="item in $props.column.enum"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
            <div v-else-if="column.valueType === ValueType.DateTimePicker">
              <el-date-picker
                style="width: 100%"
                v-bind="$props.column.editable"
                v-model="ruleForm[column.prop]"
                placeholder="请选择时间"
              />
            </div>
          </el-tooltip>
        </el-form-item>
      </template>
      <div v-else>{{ tableData && renderCellData(column, tableData) }}</div>
    </el-form>
  </div>
</template>
<script lang="ts">
import {
  computed,
  defineComponent,
  inject,
  ref,
  reactive,
  onMounted,
} from 'vue'
import { filterEnum, formatValue, handleRowAccordingToProp } from '../utils'
import { ColumnProps, ValueType } from '../types'
import type { FormInstance, FormItemProp } from 'element-plus'
/**
 * 每个组件事件类型 支持两种,一个是 change 一个是 blur ,改变和失焦
 */

export default defineComponent({
  emits: ['validateHandle'],
  props: {
    column: {
      type: Object,
      required: true,
    },
    tableData: {
      type: Object,
    },
    editData: {
      type: Object,
      default: () => null,
    },
    realTimeVerification: {
      type: Boolean,
      required: true,
    },
    editableProps: {
      type: Object,
    },
  },
  /**
   *
   * @param props
   *
   */
  setup(props, { emit }) {
    // 表格部分
    const column = computed(() => props.column)
    const tableData = computed(() => props.tableData)
    const enumMap = inject('enumMap', ref(new Map()))
    // 渲染表格数据, 增加处理层级属性的能力 如 props: config.id
    const renderCellData = (item: ColumnProps, scope: any) => {
      // 增加处理下拉框多选时候, 值是数组的情况
      if (Array.isArray(handleRowAccordingToProp(scope, item.prop!))) {
        return handleRowAccordingToProp(scope, item.prop!)
      }
      return enumMap.value.get(item.prop) && item.isFilterEnum
        ? filterEnum(
            handleRowAccordingToProp(scope, item.prop!),
            enumMap.value.get(item.prop)!,
            item.fieldNames,
          )
        : formatValue(handleRowAccordingToProp(scope, item.prop!))
    }
    // 表单部分
    const ruleFormRef = ref<FormInstance>() // 表单实例
    const rules = reactive<any>(column.value.rules || {}) // 验证规则
    const ruleForm = reactive<Record<string, any>>({
      [column.value.prop]: renderCellData(column.value, tableData.value),
    }) // 表单绑定的数据
    // 可编辑状态的类型 ValueType
    if (column.value.valueType) {
      console.log('prop', column.value, ruleForm)
    }
    const visible = ref(false) // 是否显示错误提示
    const errMsg = ref<any>('') // 错误信息
    const changeFields = () => {
      if (!props.realTimeVerification) return
      ruleFormRef.value?.validateField(column.value.prop, (result) => {
        visible.value = !result
      })
    }
    onMounted(() => {
      // 组件挂载以后 ,验证一次,再分别把可编辑的 实例传给父组件
      ruleFormRef.value?.validateField(column.value.prop, (result) => {
        visible.value = !result
        emit(
          'validateHandle',
          {
            [column.value.prop]: result,
          },
          ruleForm,
          ruleFormRef.value,
        )
      })
    })
    // 表单项被效验以后触发
    const validateHandle = (
      prop: FormItemProp,
      isValid: boolean,
      message: string,
    ) => {
      errMsg.value = message
      visible.value = !isValid
    }
    return {
      renderCellData,
      validateHandle,
      changeFields,
      ValueType,
      visible,
      ruleFormRef,
      errMsg,
      rules,
      ruleForm,
    }
  },
})
</script>
<style lang="scss" scoped>
:deep(.el-form-item) {
  margin-bottom: 0;
}
</style>

因为我们这里的表单是独立的,也就是可编辑行中 每一个可编辑组件就是一个表单,源于我们使用的是二次封装以后的设计,这里面对tableColumns进行了 for 循环, 所以在表单验证的过程中,这里我在组件挂载的时候会主动触发一次验证, 接下来就是侦听 formvalidate 事件, 每一次验证都触发,给父组件进行处理。

// 同步子组件编辑行数据各类状态
const validateHandle = (prop: any, data: any, ref: any) => {
  editRef.value.push(ref)
  Object.assign(editDataValidate.value, prop)
  editAfterData.value.push(data)
}

父组件在验证触发的时候, 需要同步几个数据,1是ref,2是编辑之前的数据,3是编辑之后的数据

  • 第三核心就是保存的时候如何同步
// 保存 需刷新页面 因为 validate 是异步验证,所以用一个 Promise 队列 同步 await
const handleSave = async (oldValue: any) => {
  const validatePromises: Promise<void>[] = []
  const resultObj: any = {
    data: {},
    err: [],
  }
  editRef.value.forEach((item: any) => {
    const validatePromise = new Promise<void>((resolve) => {
      item?.validate((result: boolean, err: any) => {
        if (!result) {
          resultObj.err.push(err)
        }
        resolve()
      })
    })
    validatePromises.push(validatePromise)
  })
  editAfterData.value.forEach((item: any) => {
    Object.assign(resultObj.data, item)
  })
  await Promise.all(validatePromises)
  props.editSave(resultObj, oldValue, clearEditData)
}

这里就是点击保存的事件,因为从点击编辑按钮开始,我们就已经与可编辑组件的每一个数据实时同步,保存的时候只需要依次处理即可。

展示

使用element实现EditTable表格行内编辑 就拿上面这张图简单介绍一下:

  1. 点击编辑以后, 按钮变成 保存和取消, 后面 分配权限的按钮是插槽,增加了配置项,可以控制这里在这两个编辑按钮的前面还是后面。
  2. 只允许当行编辑,当有未保存的数据时,点击其他的编辑按钮会提示请保存。
  3. ruls 规则同 form 相同,验证仅支持两种事件, changeblur
  • 更多详细的介绍我这里就不赘述了,如下面的截图一样,在我项目的代码里面,有更多的解释, 这里有很多 anyts 还没完善,请见谅哈哈, 后面有可能会再次完善和更新,敬请期待。

使用element实现EditTable表格行内编辑demo 在页面中的角色管理部分。

使用element实现EditTable表格行内编辑 项目地址:gitee.com/li-haibo-19… 分支 lowCode。 特别鸣谢: @白哥学前端 我前进路上最大的导师。

修改

  • 2023.07.28
  1. 修复可编辑表格内容溢出省略号不显示问题
  2. 角色管理列表使用 mock 接口 mock/acl,分页信息使用 query 不使用 url 携带