封装table

主要讲解思路

效果图

请添加图片描述

数据格式

types.ts

export interface TableOptions {
  // 字段名称
  prop?: string
  // 表头
  label: string
  // 对应列的宽度
  width?: string | number
  // 对齐方式
  align?: 'left' | 'center' | 'right'
  // 自定义列模板的插槽名
  slot?: string
  // 是否是操作项
  action?: boolean
  // 是否可以编辑
  editable?: boolean
}

props接收的数据

    options: {
      type: Array as PropType<TableOptions[]>,
      required: true,
    },
    data: {
      type: Array,
      required: true,
    },
基本样式

接下来创建数据,通过template v-for循环options,使用el-table-column形成表头,再绑定data形成数据,这样就完成了一个最基础的表格

操作项和自定义列数据

操作项

大部分table都具有button用于编辑删除等操作,我们在传入options的中定义操作项的action为true,因为操作的特殊性,我们选择单独编写,所以要先处理传入的数据,将操作项与数据部分分离

    // 传入options的数据
    let options: TableOptions[] = [
      {
        prop: 'date',
        label: '日期',
        // width: '180',
        align: 'center',
        slot: 'date',
        editable: true,
      },
      {
        label: '操作',
        action: true,
        align: 'center',
      },
    ]

通过computed过滤数组获取数据tableOption

    let tableOption = computed(() =>
      props.options.filter((item) => !item.action)
    )

同理获取操作项

let actionOption = computed(() => props.options.find((item) => item.action))

v-if="actionOption"的条件下单独写一个el-table-column,传入对应参数,并且因为点击按钮需要获取点击对应行的数据,我们使用elementplus的默认插槽,这是一个作用域插槽,会为我们传入数据;同时在使用默认插槽后,我们再为自己的组件创造一个插槽,供外部使用

<template #default="scope">
  <slot name="action" :scope="scope"></slot>
</template>

这样外部就能用下面的方式使用我们的插槽并获得数据

<template #action="{ scope }">
  <el-button type="primary" @click="edit(scope)">编辑</el-button>
  <el-button type="danger">删除</el-button>
</template>

自定义列

自定义列是通过分发插槽的方式,根据传入的slot属性创建对应插槽

<slot v-if="item.slot" :name="item.slot" :scope="scope"></slot>
<!--没有slot就显示默认数据-->
<span v-else-if="item.prop">{{ scope.row[item.prop] }}</span>

直接使用对应的插槽即可

<template #date="{ scope }">
  <el-icon-timer></el-icon-timer>
  <span style="margin-left: 10px">{{ scope.row.date }}</span>
</template>
loading加载

计算属性判断是否生成data,使用v-loading显示加载条,同时加载的其他属性也接收父组件传来的即可

  <el-table
    :data="tableData"
    v-loading="isLoading"
    :element-loading-text="elementLoadingText"
    :element-loading-spinner="elementLoadingSpinner"
    :element-loading-background="elementLoadingBackground"
    :element-loading-svg="elementLoadingSvg"
    :element-loading-svg-view-box="elementLoadingSvgViewBox"
  >
  </el-table>
可编辑单元格

options中传入editable为true代表可编辑,当v-if="item.editable"时,使用动态组件显示编辑按钮的图标,并触发点击事件传入当前行的值scope。输出scope,我们可以发现,scope的$index和column.id两个值可以确认一个唯一的单元格

请添加图片描述

因此我们使用一个数据来存放这个唯一的标识

currentEdit.value = scope.$index + scope.column.id

此时就可以新建一个template来判断当前单元格的值是否等于currentEdit.value,如果不等于就正常渲染原始值,等于就渲染成一个input输入框,同时在输入框旁边生成一个确认和取消按钮,点击后将currentEdit.value设置为空,并将事件和数据分发到父组件

为了继续加强可配置性,预留editCell插槽用来自定义可编辑单元格显示的内容

<slot
  name="editCell"
  :scope="scope"
  v-if="$slots.editCell"
></slot>
可编辑行功能

我们希望在点击后面的编辑按钮的时候,可以一次性将整行变成输入框进行编辑

首先通过props接收是否可编辑行以及编辑行按钮的标识

   isEditRow: {
      type: Boolean,
      default: false,
    },
    // 编辑行按钮的标识
    editRowIndex: {
      type: String,
      default: '',
    },

这个标识可以理解为布尔值,只要不为空,对应按钮就具有编辑行的功能

这样子可能会觉得很奇怪,我们有按钮也通过scope传入了数据,直接修改就可以了,为什么要这么麻烦,还定义两个参数用来接收呢?主要是因为在封装组件时,除了基本配置项外,我们要尽量控制数据为单向数据流,避免组件之间数据相互修改引起的麻烦。

安装lodash引入cloneDeep方法,深拷贝一份数据

    // 拷贝一份表格数据
    let tableData = ref<any[]>(cloneDeep(props.data))
    // 拷贝一份按钮标识,用于之后比对
    let cloneEditRowIndex = ref<string>(props.editRowIndex)

在onMounted周期为拷贝的数据添加一个是否可编辑属性

    onMounted(() => {
      tableData.value.map((item) => {
        // 代表当前是否是可编辑的状态
        // 为数据添加是否可以编辑行的属性
        item.rowEdit = false
      })
    })

同时为了还要监听父组件是否改变了数据,如果改变了也要重复这一步操作

    watch(
      () => props.data,
      (val) => {
        tableData.value = cloneDeep(val)
        tableData.value.map((item) => {
          item.rowEdit = false
        })
      },
      { deep: true }
    )

还要监听父组件editRowIndex标识符的变化,这样当用户修改配置项editRowIndex后,不需要手动刷新

    watch(
      () => props.editRowIndex,
      (val) => {
        if (val) cloneEditRowIndex.value = val
      }
    )

因为我们是通过插槽将操作模块分发出去的,所以我们无法判断是否点击了按钮,好在elementplus自带row-click事件,可以返回我们点击单元格行与列的数据,此时就可以根据数据判断点击的按钮是否符合要求

let rowClick = (row: any, column: any) => {
  // 判断当前点击的是否是操作项
  if (column.label === actionOption.value!.label) {
    // 用户是否开启整行编辑,点击的是否是对应标识符
    if (
      props.isEditRow &&
      props.editRowIndex &&
      cloneEditRowIndex.value === props.editRowIndex
    ) {
      row.rowEdit = !row.rowEdit
      // 重置其他数据(每次只能编辑一行)
      tableData.value.map((item) => {
        if (item != row) item.rowEdit = false
      })
      // 重置按钮的标识
      if (!row.rowEdit) context.emit('update:editRowIndex', '')
    }
  }
}

在上面的代码中,我们先判断了当前点击的是否是操作项,又判断了是否开启了整行编辑功能且要有标识符且标识符必须和暂存的相同,并在实现一次功能后重置标识符

rowEdit用于判断我们是否将原始的样式改为input框

<template v-if="scope.row.rowEdit">
  <el-input
    size="small"
    v-if="item.prop"
    v-model="scope.row[item.prop]"
  ></el-input>
</template>

父组件传递标识符

v-model:editRowIndex="editRowIndex"

// 点击按钮后,为editRowIndex添加数据
let edit = (scope: any) => {
  // 将标识改为编辑
  // 标识符名称可以是任意值,因为最后都会被存入组件中
  editRowIndex.value = 'abc'
  console.log(scope)
}
表格分页

使用elementplus的pagination分页

设置对应的props,分发sizeChange和currentChange事件的值到父组件,供父组件获取数据

注册组件

如果想想elementplus一样使用具有自己命名特色的组件,可以自己注册

在table文件夹下创建index.ts

import { App } from 'vue'
import Table from './table.vue'
// 让这个组件可以用个use的形式使用
export default {
  install(app: App) {
    app.component('mk-table', Table)
  },
}

在整个组件的总文件夹下(当然在其他地方也可以),创建index.ts,引入上面的ts文件,注册到全局

import { App } from 'vue'
import Table from './table'

const components = [
  Table,
]
export default {
  install(app: App) {
    components.map((item) => {
      app.use(item)
    })
  },
}

项目的main.ts文件夹中

import MKBird from '@/mycomponents'
app.use(MKBird)

就可以直接使用啦

本例中的table组件就可以用

<mk-table></mk-table>

组件仓库参考

Logo

前往低代码交流专区

更多推荐