封装组件库-表格

2023, Mar 12    

实现表格的封装

基于Element-plus框架的Table表格组件进行封装。

实现的功能

  • 可配置型,可维护性高
  • 具备element-plus原有表格的所有功能
  • 也可以自行拓展更多的功能

初始配置

  • 注册路由组件table,配置为Container组件的子路由组件
  • 采用路由懒加载,箭头函数异步引入组件。
  • 注册通用组件table,为路由组件的子组件,以便组件间通信。
  • 将通用组件table注册为全局组件

实现基础表格

  • 设计接口类型:
  export interface TableOptions {
    //表头
    label: string,
    //字段名称
    prop?: string,
    //列宽度
    width?: string | number,
    //对齐方式
    align?: 'left' | 'center' | 'right',
    //自定义列表模版名称
    slot?: string
  }
  • 配置基本数据及配置
  tableData.value = [
    {
      date: '2016-05-03',
      name: 'Tom1',
      address: 'No. 189, Grove St, Los Angeles',
    },
    {
      date: '2016-05-02',
      name: 'Tom2',
      address: 'No. 189, Grove St, Los Angeles',
    },
    {
      date: '2016-05-04',
      name: 'Tom3',
      address: 'No. 189, Grove St, Los Angeles',
    },
    {
      date: '2016-05-01',
      name: 'Tom4',
      address: 'No. 189, Grove St, Los Angeles',
    },
  ]
  let options: TableOptions[] = [
    {
      label: '日期',
      prop: 'date',
      align: 'center',
    },
    {
      label: '姓名',
      prop: 'name',
      align: 'center',
    },
    {
      label: '地址',
      prop: 'address',
      align: 'center',
    }
  ]
  • 子组件实现基本表格结构
  let props = defineProps({
    //表格的配置项
    options: {
      type: Array as PropType<TableOptions[]>,
      required: true
    },
    //表格数据
    data:{
      type: Array as PropType<any[]>,
      required: true
    }
  })
  
  <el-table v-bind="$attrs" :data="data">
    <template v-for="(item,index) in options" :key="index">
      <el-table-column
      :label="item.label"
      :prop="item.prop"
      :align="item.align"
      :width="item.width"> 
      </el-table-column>
    </template>
  </el-table>

实现添加操作项

  • 定义接口类型
  export interface TableOptions {
    //是否代表操作项
    action?: boolean,
  }
  • 父组件添加配置项
  let options: TableOptions[] = [
    {
      label: '操作',
      align: 'center',
      action: true
    }
  ]
  • 子组件单独处理操作项:
  //过滤操作选项之后的配置
  let tableOptions = computed(()=>props.options.filter(item=>!item.action))
  //找到操作项的配置
  let actionOptions = computed(()=>props.options.find(item=>item.action))
  <el-table v-bind="$attrs" :data="data">
    <template v-for="(item,index) in tableOptions" :key="index">
        <el-table-column
        :label="item.label"
        :prop="item.prop"
        :align="item.align"
        :width="item.width">
        </el-table-column>
    </template>
    <el-table-column
    :label="actionOptions?.label"
    :align="actionOptions?.align"
    :width="actionOptions?.width">
      <!-- 定义作用域插槽给父组件传递scope -->
      <template #default="scope">
        <slot name="action" :scope="scope" v-else></slot>
      </template>
    </el-table-column>
  </el-table>
  • 父组件编辑操作项插槽:
  <template #action="{scope}">
    <el-button size="small" type="primary">编辑</el-button>
    <el-button size="small" type="danger">删除</el-button>
  </template>

实现加载功能

  • 子组件:props配置项添加,同时绑定配置。
  <el-table v-bind="$attrs" :data="data" v-loading="isLoading" 
  :element-loading-text="elementLoadingText"
  :element-loading-background="elementLoadingBackground"
  :element-loading-spinner="elementLoadingSpinner"
  :element-loading-svg="elementLoadingSvg"
  :element-loading-svg-view-box="elementLoadingSvgViewBox"
  >
  </el-table>
  let props = defineProps({
    //加载文案
    elementLoadingText:{
      type: String,
    },
    //加载图标名
    elementLoadingSpinner:{
      type: String,
    },
    //加载背景颜色
    elementLoadingBackground:{
      type: String,
    },
    //加载svg图标
    elementLoadingSvg:{
      type: String,
    },
    //加载svg图标配置
    elementLoadingSvgViewBox:{
      type: String,
    }
  })
  • 父组件传值
  <my-table :data="tableData" :options="options" 
  elementLoadingText="Loading..." 
  elementLoadingBackground="rgba(122, 122, 122, 0.8)"
  :elementLoadingSvg="svg" 
  elementLoadingSvgViewBox="-10, -10, 50, 50" 
  >
  </my-table>
  
  let svg = `
        <path class="path" d="
          M 30 15
          L 28 17
          M 25.61 25.61
          A 15 15, 0, 0, 1, 15 30
          A 15 15, 0, 1, 1, 27.99 7.5
          L 15 15
        " style="stroke-width: 4px; fill: rgba(0, 0, 0, 0)"/>
      `

实现自定义列功能

  • 为某些列添加slot配置项
  let options: TableOptions[] = [
    {
      label: '日期',
      prop: 'date',
      align: 'center',
      slot: 'date',
    },
    {
      label: '姓名',
      prop: 'name',
      align: 'center',
      slot: 'name'
    }
  ]
  • 子组件: 对数据项遍历过程中,需要对options的配置是否含有slot进行判断,有的话显示插槽,否则显示scope.row[item.prop],即对应值 这里采用动态插槽的形式,对各自定义列进行单独处理
  <el-table v-bind="$attrs" :data="data" v-loading="isLoading" :element-loading-text="elementLoadingText"
  :element-loading-background="elementLoadingBackground"
  :element-loading-spinner="elementLoadingSpinner"
  :element-loading-svg="elementLoadingSvg"
  :element-loading-svg-view-box="elementLoadingSvgViewBox"
  >
    <template v-for="(item,index) in tableOptions" :key="index">
        <el-table-column
        :label="item.label"
        :prop="item.prop"
        :align="item.align"
        :width="item.width">
        <template #default="scope">
            <slot v-if="item.slot" :name="item.slot" :scope="scope"></slot>
            <span v-if="!item.slot && item.prop"></span>
          </template>
        </el-table-column>
    </template>
    <el-table-column
    :label="actionOptions?.label"
    :align="actionOptions?.align"
    :width="actionOptions?.width">
      <template #default="scope">
        <slot name="action" :scope="scope" v-else></slot>
      </template>
    </el-table-column>
  </el-table>
  • 父组件编写插槽,实现自定义列的定义化
  <template #date="{scope}"> 
    <qt-icon-timer></qt-icon-timer>
    
  </template>
  <template #name="{scope}">
    <el-popover effect="light" trigger="hover" placement="top" width="auto">
        <template #default>
          <div>name: </div>
          <div>address: </div>
        </template>
        <template #reference>
          <el-tag></el-tag>
        </template>
      </el-popover>
  </template>

实现可编辑单元格

  • 定义接口类型
  export interface TableOptions {
    //是否是可编辑单元格
    editable?: boolean
  }
  • 为某些列添加editable
  let options: TableOptions[] = [
    {
      label: '日期',
      prop: 'date',
      align: 'center',
      slot: 'date',
      editable: true
    },
    {
      label: '地址',
      prop: 'address',
      align: 'center',
      editable: true
    }
  ]
  • 子组件: 需要实现点击显示输入框及勾、叉功能,如何确定点击的哪个单元格,打印scope发现对应单元格的scope.$index + scope.column.id的值唯一。定义变量currentEdit表示唯一标识。 这里,可以对勾、叉定义插槽,实现自定义的确认和取消的功能。
  <el-table v-bind="$attrs" :data="tableData" v-loading="isLoading" :element-loading-text="elementLoadingText"
  :element-loading-background="elementLoadingBackground"
  :element-loading-spinner="elementLoadingSpinner"
  :element-loading-svg="elementLoadingSvg"
  :element-loading-svg-view-box="elementLoadingSvgViewBox">
    <template v-for="(item,index) in tableOptions" :key="index">
        <el-table-column
        :label="item.label"
        :prop="item.prop"
        :align="item.align"
        :width="item.width">
          <template #default="scope">
            <template>
              <template v-if="scope.$index + scope.column.id === currentEdit">
                <div style="display:flex;">
                  <el-input v-if="item.prop" size="small" v-model="scope.row[item.prop]"></el-input>
                  <div @click.stop="clickEditCell">
                    <!-- 用户定义check和close的内容,插槽写内容则显示 -->
                    <slot name="editCell" :scope="scope" v-if="$slots.editCell"></slot>
                    <div class="icons" v-else>
                      <qt-icon-check class="check" @click="check(scope)"></qt-icon-check>
                      <qt-icon-close class="close" @click="close(scope)"></qt-icon-close>
                    </div>
                  </div>
                </div>
            </template>
            <template v-else>
              <slot v-if="item.slot" :name="item.slot" :scope="scope"></slot>
              <span v-if="!item.slot && item.prop"></span>
              <component :is="`qt-icon-${toLine(editIcon)}`" v-if="item.editable" @click.stop="clickEdit(scope)"/>
            </template>
            </template>
          </template> 
          
        </el-table-column>
    </template>
    <el-table-column
    :label="actionOptions?.label"
    :align="actionOptions?.align"
    :width="actionOptions?.width">
      <template #default="scope">
        <slot name="action" :scope="scope"></slot>
      </template>
    </el-table-column>
  </el-table>
  
  let clickEdit=(scope: any)=>{
    //唯一标识  
    currentEdit.value = scope.$index + scope.column.id
  }

实现可编辑行

  • 子组件添加props配置项,父组件传递isEditRow,表示配置可编辑行。editRowIndex表示确认点击的是编辑还是删除。
  let props = defineProps({
    //是否可以编辑行
    isEditRow: {
      type: Boolean,
      default: false
    },
    //编辑行按钮标识
    editRowIndex: {
      type: String,
      default: ''
    },
  })
  • 子组件:el-table组件配置了@row-click事件,来确定点击的是哪行。为子组件添加该事件。
  • 这里拷贝一份表格数据,为数据添加属性rowEdit,初始值为false,表示不可编辑。同时,要对父组件传递的表格数据进行监听,当变化时对tableData重新赋值。
  • 这里需要对父组件传递的按钮标识和当前表示进行对比判断。
  • 重置按钮标识,修复删除按钮可编辑状态
  // 拷贝一份表格的数据
  let tableData = ref<any[]>(cloneDeep(props.data))
  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})
  
  //拷贝一份按钮标识
  let cloneEditRowIndex = ref<string>(props.editRowIndex)
  //监听父组件传递过来的标识
  watch(()=>props.editRowIndex,val=>{
    if(val) cloneEditRowIndex.value = val
  })
  
  //点击每一行的事件
  let rowClick = (row: any, column: any)=>{
      //判断当前点击的是否是操作项的内容
      if(column.label === actionOptions.value!.label){
        //编辑行的操作
        if(props.isEditRow && cloneEditRowIndex.value === props.editRowIndex){
            //点击的按钮是可编辑的操作
            row.rowEdit = !row.rowEdit
            //重置其他数据的rowEdit
            tableData.value.map(item=>{ 
              if(item !== row) item.rowEdit = false
            })
            //重置按钮标识
            if(!row.rowEdit){
              emits('update:editRowIndex','')
            }
        }
      }
    }

实现分页功能

  • mock生成随机数据:mock生成100条随机数据,传递index、size对数据进行分页
  • 父组件获取数据
  let getData = ()=>{
    axios.post('/api/list',{
      current:current.value,
      pageSize:pageSize.value
    }).then((res: any)=>{
        tableData.value = res.data.data.rows
        total.value = res.data.data.total
    })
  }

  onMounted(()=>{
    getData()
  })
  • 子组件配置props
  let props = defineProps({
    //是否显示分页
    pagination: {
      type: Boolean,
      default: false
    },
    //当前是第几页的数据
    currentPage: {
      type: Number,
      default: 1
    },
    //每页数据的选项
    pageSizes: {
      type: Array as PropType<number[]>,
      default: [10,20,30,40]
    },
    //当前一页多少条数据
    pageSize: {
        type: Number,
        default: 10
    },
    //数据总数
    total: {
      type: Number
    },
    //分页的排列方式
    paginationAlign: {
      type: String as PropType<'left' | 'center' | 'right'>,
      default: 'left'
    }
  })
  • 子组件分页器
  <div class="pagination" :style="{justifyContent:justifyContent}" v-if="pagination">
    <el-pagination
      :current-page="currentPage"
      :page-size="pageSize"
      :page-sizes="pageSizes"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />
  </div>
  • 修改当前页及页面条数
  //分页条数
  let handleSizeChange = (val: number)=>{
    emits('sizeChange',val)
  }
  //分页页数
  let handleCurrentChange = (val:number)=>{
    emits('currentChange',val)
  }
  
  <!-- 父组件   -->
  let sizeChange = (val:number)=>{
    pageSize.value = val
    getData()

  }
  let currentChange = (val:number)=>{
    current.value = val
    getData()

  }