表格封装:让我们轻松的coding~

1,352 阅读4分钟

表格复用

在后端管理项目中,我们通常有着许多重复利用的组件,尤其是表格与分页器是用得最多的。我们采用的是 element-ui所以我们对其进行了封装。

封装组件

com-list.vue

<template>
  <section class="com-list">

    <el-table
      v-loading="loading"
      :data="sourceData"
      @selection-change="selectionChange"
      class="com-list__table">

      <!-- 选择框 -->
      <el-table-column
        v-if="selectVisible"
        type="selection"
        width="45"
        align="center"
      />

      <!-- 主体数据 -->
      <template v-for="(column, idx) in columns">
        <el-table-column
          v-if="column.prop"
          :key="idx"
          :prop="column.prop"
          :label="column.label"
          :formatter="column.formatter"
          :sortable="column.sortable"
          :align="column.align"
          :width="column.width">
        </el-table-column>
        <slot
          v-else-if="column.slot"
          :name="column.slot"
        />
      </template>

      <!-- 分页 -->
      <div v-if="needPage" class="com-list__footer" slot="append">
        <el-row type="flex" justify="left">
          <el-col>
            <el-button v-if="deleteVisible" type="danger" size="small">批量删除</el-button>
            <el-button v-if="setGroupVisible" size="small">设置分组</el-button>
            <slot name="read" />
          </el-col>
          <el-col class="pagination" v-if="needPage" :span="6">
            <el-pagination
              small
              background
              layout="prev, pager, next"
              :total="pageTotal"
              :page-size="pageRequest.limit"
              :current-page.sync="pageRequest.page"
              @current-change="page => $emit('pageChange', page)"
              @size-change="size => $emit('pageChange', size)"
            >
            </el-pagination>
          </el-col>
        </el-row>
      </div>
    </el-table>

  </section>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'

@Component
export default class ComList extends Vue {

  @Prop({default: false}) loading!: boolean
  @Prop() sourceData!: any // 表格数据
  @Prop() columns!: Array<object> // 表格项
  @Prop({default: false}) selectVisible?: boolean // 选择框
  @Prop({default: false}) deleteVisible?: boolean // 批量删除
  @Prop({default: false}) setGroupVisible?: boolean // 设置分组
  @Prop({default: true}) needPage?: boolean // 是否有分页
  @Prop({default: () => {return {page: 1, limit: 10, total: 0} }}) pageRequest?: object // 分页数据
  @Prop({default: 0}) pageTotal?: number // 分页总数

  private selections: Array<object> = []

  private selectionChange(selections: Array<object>) {
    this.selections = selections
    this.$emit('selectionChange', { selections })
  }

}
</script>

<style lang="scss" scoped>
.com-list {

  ::v-deep &__table {
    border: 1px solid #EBF3FA;
    thead {
      font-size: 14px;
      color: #333;
      th {
        height: 70px;
        background: #EBF3FA;
      }
    }
    thead th, tbody, td {
      &:first-child:not(.el-table-column--selection) {
        .cell {
          padding-left: 36px;
        }
      }
    }
  }

  &__footer {
    margin: 22px;
    text-align: left;
    .pagination {
      margin-top: 6px;
      text-align: right;
    }
    .el-pagination {
      text-align: right;
      padding: 0;
    }
  }
}
</style>

其中的slot是很关键的一点,它能让我们自定义每一项,其他的倒无非是增加相关属性而已。

使用组件

<com-list 
      :source-data="tableData" 
      :columns="columns"
      :selectVisible="true"
      :page-request="listQuery"
      :page-total="total"
      :loading="listLoading"
      :page-sizes="[10, 25, 50]"
      @pageChange="pageChange"
      @sizeChange="sizeChange"
      @selectionChange="selectionChange"
    >
      <template #status>
        <el-table-column label="状态">
          <template slot-scope="scope">
            <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
              {{scope.row.status === 1 ? '已处理' : '待处理'}}
            </el-tag>
          </template>
        </el-table-column>
      </template>
      <template #operation>
        <el-table-column label="操作">
          <template slot-scope="scope">
            <el-button type="text" size="small" @click="onLock(scope.row.cate_id)">查看</el-button>
          </template>
        </el-table-column>
      </template>
      <template #read>
        <el-button size="small" @click="setRead">标记已读</el-button>
      </template>
</com-list>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import ComList from '@/components/ComList/index.vue'
import Page from '@/mixins/pageMixins'
    
@Component({
  components: {
    ComList
  }
})

export default class Message extends Page {

  public fetchList = this.$api.Message.getList
  private columns: Array<object> = [
    {
      label: 'ID',
      prop: 'id'
    },
    {
      label: '消息标题',
      prop: 'title'
    },
    {
      slot: 'status'
    },
    {
      label: '时间',
      prop: 'create_time'
    },
    {
      slot: 'operation'
    }
  ]
  private selections: Array<object> = []
  private selectionChange (parmars: any) {
    const arr: Array<object> = []
    parmars.selections.forEach((el: any) => {
      arr.push(el.cate_id)
    })
    this.selections = arr
  }
}
</script>

需要注意的是,我们在其中引入了一个minxins它能让我们将一些属性以及方法抽离出来,达到复用的目的。在vue-property-decorator中我们使用的是 extends来继承 mixins

抽取可复用的方法以及属性

mixins/pageMinxis.ts

import { Component, Vue, Watch } from 'vue-property-decorator'

@Component
export default class Page extends Vue {
  // 页码及每页条数
  listQuery = {
    page: 1,
    limit: 10
  }
  total = 0
  listLoading = false // 是否加载中
  fetchList: any = null // 请求接口函数
  tableData = [] // 获取数据
  searchForm = {} // 筛选条件

  @Watch('listQuery', { immediate: true, deep: true })
  listQueryChange () {
    this.getList()
  }

  // 页码切换
  pageChange(page: number) {
    this.listQuery.page = page
  }
  // 每页条数切换
  sizeChange(size: number) {
    this.listQuery.limit = size
  }

  // 获取列表数据
  async getList() {
    if (!this.fetchList || typeof this.fetchList !== 'function') {
      this.$message.error('请把列表接口函数赋值给fetchList')
      return
    }
    this.listLoading = true
    const query = this.$filterEmptyValue(this.listQuery, this.searchForm)
    const RESULT = await this.fetchList(query)
    if (RESULT.code === 1) {
      this.tableData = RESULT.data.data
      this.total = RESULT.data.total
      this.listLoading = false
    }
  }

  /**
   * @description: 操作项目及回调
   * fn: 需要执行的函数
   * cb: 回调(可传空)
   * ...rest: 需要执行函数的参数
   */
  async mixinHandleItem(fn: Function, cb: Function, ...rest: any) {
    const { code, data } = await fn(...rest)
    if (code === 1) {
      this.$message.success('操作成功')
      if (cb && typeof cb === 'function') {
        cb()
      }
    } else {
      this.$message.error(data || '操作失败,请重试')
    }
  }
}

我们可以复用方法以及覆盖属性,即可高效利用代码

对以上代码的优化

components/ComList.vue

<template>
  <section class="com-list">

    <el-table
      v-loading="loading"
      :data="sourceData"
      @selection-change="selectionChange"
      class="com-list__table">

      <!-- 选择框 -->
      <el-table-column
        v-if="selectVisible"
        type="selection"
        width="45"
        align="center"
      />

      <!-- 主体数据 -->
      <template v-for="(column, index) in columns">
        <el-table-column
          v-if="column.render"
          :key="column.prop" 
          :label="column.label"
        >
          <template slot-scope="scope">
            <Render
              :row="scope.row"
              :index="index"
              :render="column.render"
            />
          </template>
        </el-table-column>
        <slot
          v-else-if="column.slot"
          :name="column.slot"
        />
        <el-table-column
          v-else
          :key="column.prop"
          v-bind="setAttrs(column)"
        />
      </template>

      <!-- 分页 -->
      <div v-if="needPage" class="com-list__footer" slot="append">
        <el-row type="flex" justify="left">
          <el-col>
            <el-button v-if="deleteVisible" type="danger" size="small">批量删除</el-button>
            <el-button v-if="setGroupVisible" size="small">设置分组</el-button>
            <slot name="read" />
          </el-col>
          <el-col class="pagination" v-if="needPage" :span="6">
            <el-pagination
              small
              background
              layout="prev, pager, next"
              :total="pageTotal"
              :page-size="pageRequest.limit"
              :current-page.sync="pageRequest.page"
              @current-change="page => $emit('pageChange', page)"
              @size-change="size => $emit('pageChange', size)"
            >
            </el-pagination>
          </el-col>
        </el-row>
      </div>
    </el-table>

  </section>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'
import Render from './render'

@Component({
  components: {
    Render
  }
})
export default class ComList extends Vue {

  @Prop({default: false}) loading!: boolean
  @Prop() sourceData!: object // 表格数据
  @Prop() columns!: Array<object> // 表格项
  @Prop({default: false}) selectVisible?: boolean // 选择框
  @Prop({default: false}) deleteVisible?: boolean // 批量删除
  @Prop({default: false}) setGroupVisible?: boolean // 设置分组
  @Prop({default: true}) needPage?: boolean // 是否有分页
  @Prop({default: () => {return {page: 1, limit: 10, total: 0} }}) pageRequest?: object // 分页数据
  @Prop({default: 0}) pageTotal?: number // 分页总数

  private selections: Array<object> = []

  private selectionChange(selections: Array<object>) {
    this.selections = selections
    this.$emit('selectionChange', { selections })
  }

  private setAttrs (params: object) {
    // eslint-disable-next-line
    const { slot, ...options }: any = params // 排除 slot
    return { ...options }
  }

}
</script>

<style lang="scss" scoped>
.com-list {

  ::v-deep &__table {
    border: 1px solid #EBF3FA;
    thead {
      font-size: 14px;
      color: #333;
      th {
        height: 70px;
        background: #EBF3FA;
      }
    }
    thead th, tbody, td {
      &:first-child:not(.el-table-column--selection) {
        .cell {
          padding-left: 36px;
        }
      }
    }
  }

  &__footer {
    margin: 22px;
    text-align: left;
    .pagination {
      margin-top: 6px;
      text-align: right;
    }
    .el-pagination {
      text-align: right;
      padding: 0;
    }
  }
}
</style>

components/render.ts

import { CreateElement, RenderContext } from 'vue/types/umd'

export default {
  functional: true,
  props: {
    row: Object,
    render: Function,
    index: Number,
    column: {
      type: Object,
      default: null
    }
  },
  render: (h: CreateElement, ctx: RenderContext) => {
    const params: any = {
      row: ctx.props.row,
      index: ctx.props.index
    }
    if (ctx.props.column) params.column = ctx.props.column
    return ctx.props.render(h, params)
  }
}

利用 render函数定制表格内的内容

使用

import { Component } from 'vue-property-decorator'
import { CreateElement } from 'vue/types/umd'
import ComList from '@/components/ComList/index.vue'
import Page from '@/mixins/pageMixins'

@Component({
  components: {
    ComList
  }
})
export default class Work extends Page {
  
  fetchList = this.$api.WorkOrder.getList

  columns: Array<object> = [
    {
      label: '工单编号',
      prop: 'id'
    },
    {
      label: '问题类型',
      prop: 'type',
      formatter: this.formatterType
    },
    {
      label: '问题标题',
      prop: 'title'
    },
    {
      label: '状态',
      prop: 'status',
      render: (h: CreateElement, { row }: any) => {
        return h('el-tag', {
          props: {
            type: row.status === 1 ? 'success' : 'danger'
          }
        }, row.status === 1 ? '启用' : '停用')
      }
    },
    {
      label: '问题详情',
      prop: 'content'
    },
    {
      label: '提交时间',
      prop: 'create_time'
    },
    {
      slot: 'operation'
    }
  ]

  typeOption: Array<object> = []

  formatterType (row: any) {
    let str = ''
    this.typeOption.forEach((el: any) => {
      if (el.id === row.type) str = el.group_name 
    })
    return str
  }
}

不知为何这个语法只能渲染 element-ui里的组件,如果div等则无法放入内容。