【代码背景】

最近写了10+表,上线以后时不时要改需求,一旦改动就是10+的工作量,所以决定把表格抽离出来单独写个组件,方便以后复用修改,这应该是写过最复杂的组件封装了,特此记录,小白一枚,接触vue也没多久,欢迎大家学习交流哦。

开发环境:vue + element ui

【准备工作】

封装组件的目的是方便复用,也就是多个地方引用相同相似的组件功能,所以在封装之前一定要弄清楚,针对目前的Table需要封装哪些功能,有哪些共性可以抽离出来。这部分的思考非常重要,是影响组件封装质量的重要权衡点,所以多花一点时间是非常有必要的。

针对目前所有的表进行了分析,从样式上大致可分为4个部分:

->1. 序号列,每张表都有。

->2. 单位列,每张表都有,但是字段值label标签是变化的,label会根据后续查询操作进行更改,比如用户点击“险种”后,这里的label会从“单位”变成“险种”。

->3. 查询操作,这个字段是抽取共性最难的部分,几乎没有相似的地方,每张表的查询操作都不太一样,且各自查询逻辑也不一样。

->4. 其他字段渲染,有单层/多层表头之分。

相对应的解决办法如下:

->1. 序号列通过el-table可以自动完成渲染。

<el-table-column label="序号" type="index" align="center" fixed />

->2. 在渲染可变字段的时候,可以通过props传入变量。

<!-- 单位/可变字段 -->
<el-table-column
    :label="changeable.label"
    :prop="changeable.prop"
    align="center"
    fixed
    sortable
    :sort-by="changeable.sort"
    :min-width="flexColumnWidth(changeable.label,changeable.prop)"
/>

->3. 针对这个变化最多的字段,之前也考虑过用循环的方式把查询按钮渲染出来,尝试写过固定格式,然后添加条件判断渲染,但是无法达到预期的效果,后来在查找资料的时候突然看到了“插槽”的概念,具体可以参考这篇文章:https://vue-js.com/topic/5f6d68f24590fe0031e591f5 ,瞬间解决了这个问题。把个性化的部分抽出来作为“插槽”部分完美和组件结合使用。

<!--  子组件  -->
<!-- 查询操作 使用template插槽进行自定义组装 -->
<slot name="child" />

<!--  父组件 -->
<!-- 自定义具名插槽 -->
<template v-slot:child>
    <el-table-column label="查询操作" align="center" width="130px" fixed>
        <template slot-scope="scope">
            <!-- 按机构下挖 -->
            <el-tooltip
                v-if="scope.row.sale_no === ''"
                class="item"
                effect="dark"
                :content="scope.row.branch_no.endsWith('00')?'按机构查询':'按业务员查询'"
                placement="top"
                :enterable="false"
            >
                <el-button
                  type="success"
                  size="mini"
                  class="btn-column-query"
                  @click="digByType(scope.row,'机构')"
                >{{ scope.row.branch_no.endsWith('00')?'机构':'业务员' }}
                </el-button>
            </el-tooltip>
            <!-- 按险种下挖 -->
            <el-tooltip
                v-if="queryInfo.select_poltype==='0'"
                class="item"
                effect="dark"
                content="按险种查询"
                placement="top"
                :enterable="false"
            >
              <el-button
                  type="warning"
                  size="mini"
                  class="btn-column-query"
                  @click="digByType(scope.row,'险种')"
              >险种
              </el-button>
            </el-tooltip>
        </template>
    </el-table-column>
</template>

->4. 通过v-for循环渲染表头数据 

    <!-- v-for 循环渲染表头 -->
    <template v-for="item in headerData">
      <!-- 多层表头 -->
      <el-table-column
        v-if="item.children && item.children.length"
        :key="item.prop"
        :label="item.label"
        :prop="item.prop"
        align="center"
      >
        <el-table-column
          v-for="obj in item.children"
          :key="obj.prop"
          :label="obj.label"
          :prop="obj.prop"
          align="center"
          sortable
          :formatter="handleFormatter"
          :min-width="flexColumnWidth(obj.label,obj.prop)"
        />
      </el-table-column>
      <!-- 单层表头 -->
      <el-table-column
        v-else
        :key="item.prop"
        :label="item.label"
        :prop="item.prop"
        align="center"
        :fixed="item.fixed"
        sortable
        :formatter="handleFormatter"
        :min-width="flexColumnWidth(item.label,item.prop)"
      />
    </template>

【完整代码】 

子组件 ReportTabel.vue

<template>
  <!-- 报表模板 -->
  <el-table
    ref="tableRef"
    v-loading="loading"
    :data="tableData"
    border
    stripe
    :header-cell-style="MyHeaderCellStyle"
    :cell-style="MyCellStyle"
    show-summary
    :summary-method="accountSummaries"
    :height="TableHeight"
    style="margin-top: 0px"
  >
    <el-table-column label="序号" type="index" align="center" fixed />
    <!-- 单位/可变字段 -->
    <el-table-column
      :label="changeable.label"
      :prop="changeable.prop"
      align="center"
      fixed
      sortable
      :sort-by="changeable.sort"
      :min-width="flexColumnWidth(changeable.label,changeable.prop)"
    />
    <!-- 查询操作 使用template插槽进行自定义组装 -->
    <slot name="child" />
    <!-- v-for 循环渲染表头 -->
    <template v-for="item in headerData">
      <!-- 多层表头 -->
      <el-table-column
        v-if="item.children && item.children.length"
        :key="item.prop"
        :label="item.label"
        :prop="item.prop"
        align="center"
      >
        <el-table-column
          v-for="obj in item.children"
          :key="obj.prop"
          :label="obj.label"
          :prop="obj.prop"
          align="center"
          sortable
          :formatter="handleFormatter"
          :min-width="flexColumnWidth(obj.label,obj.prop)"
        />
      </el-table-column>
      <!-- 单层表头 -->
      <el-table-column
        v-else
        :key="item.prop"
        :label="item.label"
        :prop="item.prop"
        align="center"
        :fixed="item.fixed"
        sortable
        :formatter="handleFormatter"
        :min-width="flexColumnWidth(item.label,item.prop)"
      />
    </template>
  </el-table>
</template>

<script>
  export default {
    name: 'Index',
    props: {
      // 表头数据
      headerData: {
        type: Array,
        default: () => [],
        require: true
      },
      // 表头可变字段:单位/业务员/....
      changeable: {
        type: Object,
        default: () => {
          return {
            label: '单位',
            prop: 'branch_name',
            sort: 'branch_no'
          }
        }
      },
      // 表格数据
      tableData: {
        type: Array,
        default: () => [],
        require: true
      },
      // 自定义合计
      accountSummaries: {
        type: Function,
        default: () => {
          return null
        }
      },
      // 控制数据刷新
      loading: {
        type: Boolean,
        default: false
      }
    },
    data () {
      return {
        TableHeight: 550 // 默认表格高度
      }
    },
    watch: {
      // tableData是el-table绑定的数据
      tableData: {
        // 解决表格显示错位问题
        handler () {
          this.$nextTick(() => {
            // tableRef是el-table绑定的ref属性值
            this.$refs.tableRef.doLayout() // 对 Table 进行重新布局
          })
        },
        deep: true
      }
    },
    created () {
      // 动态计算表格高度
      const windowHeight = document.documentElement.clientHeight || document.bodyclientHeight
      this.TableHeight = windowHeight - 150 // 数值"140"根据需要调整
    },
    methods: {
      /**
       * 遍历列的所有内容,获取最宽一列的宽度
       * @param arr
       */
      getMaxLength (arr) {
        return arr.reduce((acc, item) => {
          if (item) {
            const calcLen = this.getTextWidth(item)
            if (acc < calcLen) {
              acc = calcLen
            }
          }
          return acc
        }, 0)
      },
      /**
       * 使用span标签包裹内容,然后计算span的宽度 width: px
       * @param valArr
       */
      getTextWidth (str) {
        let width = 0
        const html = document.createElement('span')
        html.innerText = str
        html.className = 'getTextWidth'
        document.querySelector('body').appendChild(html)
        width = document.querySelector('.getTextWidth').offsetWidth
        document.querySelector('.getTextWidth').remove()
        return width
      },
      /**
       * el-table-column 自适应列宽
       * @param prop_label: 表名
       * @param table_data: 表格数据
       */
      flexColumnWidth (label, prop) {
        // console.log('label', label)
        // console.log('prop', prop)
        // console.log(this.tableData)
        // 1.获取该列的所有数据
        const arr = this.tableData.map(x => x[prop])
        arr.push(label) // 把每列的表头也加进去算
        // console.log(arr)
        // 2.计算每列内容最大的宽度 + 表格的内间距(依据实际情况而定)
        return (this.getMaxLength(arr) + 25) + 'px'
      },
      // 格式化字段 string->number 用于排序
      handleFormatter (row, column, cellValue, index) {
        // console.log('row', row)
        // console.log('column', column)
        // console.log('cellValue', cellValue)
        // console.log('index', index)
        row[column.property] = Number(row[column.property])
        return row[column.property]
      },
      // [表头]设置样式
      MyHeaderCellStyle ({ row, column, rowIndex, columnIndex }) {
        // console.log('column', column.property)
        let style = null
        this.headerData.forEach(item => {
          if (item.prop === column.property) {
            style = item.header_style
          }
          // 存在子节点
          if (item.children && item.children.length) {
            for (let i = 0; i < item.children.length; i++) {
              if (item.children[i].prop === column.property) {
                style = item.header_child_style
              }
            }
          }
        })
        return style
      },
      // [表格]设置样式
      MyCellStyle ({ row, column, rowIndex, columnIndex }) {
        // console.log(column)
        let style = null
        this.headerData.forEach(item => {
          if (item.prop === column.property) {
            style = item.cell_style
          }
          // 存在子节点
          if (item.children && item.children.length) {
            for (let i = 0; i < item.children.length; i++) {
              if (item.children[i].prop === column.property) {
                style = item.cell_style
              }
            }
          }
        })
        return style
      }
    }
  }
</script>

<style scoped>
  .el-table /deep/ th {
    padding: 0;
    white-space: nowrap;
    min-width: fit-content;
  }

  .el-table /deep/ td {
    padding: 1px;
    white-space: nowrap;
    width: fit-content;
  }

  /** 修改el-card默认paddingL:20px-内边距 **/
  >>> .el-card__body {
    padding: 10px;
  }

  .el-table /deep/ .cell {
    white-space: nowrap;
    width: fit-content;
  }
</style>

父组件引用组件

// 引入组件
import ReportTable from '@/components/ReportTable'
<template>
  <!--报表数据-->
      <report-table
        :header-data="headerData"
        :changeable="changeable"
        :table-data="tableData"
        :account-summaries="accountSummaries2"
        :loading="loading"
      >
        <!-- 自定义具名插槽 -->
        <template v-slot:child>
          <el-table-column label="查询操作" align="center" width="130px" fixed>
            <template slot-scope="scope">
              <!-- 按机构下挖 -->
              <el-tooltip
                v-if="scope.row.sale_no === ''"
                class="item"
                effect="dark"
                :content="scope.row.branch_no.endsWith('00')?'按机构查询':'按业务员查询'"
                placement="top"
                :enterable="false"
              >
                <el-button
                  type="success"
                  size="mini"
                  class="btn-column-query"
                  @click="digByType(scope.row,'机构')"
                >{{ scope.row.branch_no.endsWith('00')?'机构':'业务员' }}
                </el-button>
              </el-tooltip>
              <!-- 按险种下挖 -->
              <el-tooltip
                v-if="queryInfo.select_poltype==='0'"
                class="item"
                effect="dark"
                content="按险种查询"
                placement="top"
                :enterable="false"
              >
                <el-button
                  type="warning"
                  size="mini"
                  class="btn-column-query"
                  @click="digByType(scope.row,'险种')"
                >险种
                </el-button>
              </el-tooltip>
            </template>
          </el-table-column>
        </template>
      </report-table>
</template>
<script>
  // 引入组件
  import ReportTable from '@/components/ReportTable'

  // 表格头数据
  const tableHeaderData = [
    {
      label: '保费合计',
      prop: 'sum_amnt',
      fixed: true,
      header_style: 'background-color: #FFCCCC;color: #333;',
      cell_style: 'background-color: #FFE6E5'
    },
    {
      label: '大个险',
      prop: 'gx',
      children: [
        { label: '首年保费', prop: 'gx_snbf' },
        { label: '首年标保', prop: 'gx_snbb' },
        { label: '首年期交', prop: 'gx_snqj' },
        { label: '10年及以上期交', prop: 'gx_10qj' },
        { label: '保障型保费', prop: 'gx_bzxbf' },
        { label: '续期保费', prop: 'gx_xqbf' },
        { label: '短险保费', prop: 'gx_dxbf' }
      ],
      header_style: 'background-color: #CCCCFF;color: #333;font-size: 14px',
      header_child_style: 'background-color: #CCCCFF;color: #333;',
      cell_style: 'background-color: #DBD9FF'
    },
    {
      label: '银保',
      prop: 'yb',
      children: [
        { label: '首年保费', prop: 'yb_snbf' },
        { label: '首年标保', prop: 'yb_snbb' },
        { label: '首年期交', prop: 'yb_snqj' },
        { label: '保障型保费', prop: 'yb_bzxbf' },
        { label: '续期保费', prop: 'yb_xqbf' },
        { label: '短险保费', prop: 'yb_dxbf' }
      ],
      header_style: 'background-color: #FFCC99;color: #333;font-size: 14px',
      header_child_style: 'background-color: #FFCC99;color: #333;',
      cell_style: 'background-color: #FFE4AB'
    },
    {
      label: '团险',
      prop: 'tx',
      children: [
        { label: '首年保费', prop: 'tx_snbf' },
        { label: '首年期交', prop: 'tx_snqj' },
        { label: '续期保费', prop: 'tx_xqbf' },
        { label: '短险保费', prop: 'tx_dxbf' }
      ],
      header_style: 'background-color: #CCFFCC;color: #333;font-size: 14px',
      header_child_style: 'background-color: #CCFFCC;color: #333;',
      cell_style: 'background-color: #E4FFD9'
    }
  ]

  export default {
    name: 'Index',
    components: { // 组件注册
      'report-table': ReportTable
    },
    data () {
      return {
        headerData: tableHeaderData, // 表头数据
        tableData: [], // 表格数据
        tableTotal: [], // 表格合计数组
        changeable: { // 可变字段:单位/业务员/险种
          label: '单位',
          prop: 'branch_name',
          sort: 'branch_no'
        },
        loading: false,
        queryInfo: {} // 表格的查询条件
      }
    },
    created () {
      this.checkReportData() // 渲染表格
    },
    methods: {
      // 渲染表格数据
      checkReportData () {
          this.loading = true
          checkReport01tb1(this.queryInfo).then(res => {
            if (res.data === null) {
              this.$message.info('当前查询结果为空')
              this.tableData = [] // 清空当前列表
              this.tableTotal = [] // 清空合计数组
            } else {
              this.handleChangeable(res.data.body[0]) // 处理可变字段
              this.tableData = res.data.body // 渲染表格数据
              this.tableTotal = res.data.total[0] // 渲染表格合计数据
            }
            this.loading = false
          })
      },
      // 按类型下挖
      digByType (rowInfo, type) {
        // 根据type 修改queryInfo 查询不同的返回结果
        this.checkReportData()
      },
      // 根据list结果渲染changeable字段
      handleChangeable (rowData) {
        if (rowData.branch_no !== '' && rowData.branch_name !== '') {
          this.changeable = {
            label: '单位',
            prop: 'branch_name',
            sort: 'branch_no'
          }
        }
        if (rowData.sale_no !== '' && rowData.sale_name !== '') {
          this.changeable = {
            label: '业务员',
            prop: 'sale_name',
            sort: 'sale_no'
          }
        }
        if (rowData.pol_code !== '' && rowData.pol_name !== '') {
          this.changeable = {
            label: '险种名称',
            prop: 'pol_name',
            sort: 'pol_code'
          }
        }
      },
      // 自定义合计(后台获取total数据)
      accountSummaries2 (param) {
        // console.log('param', param)
        const { columns } = param
        const sums = []
        columns.forEach((column, index) => {
          if (index === 0) {
            sums[index] = '合计'
            return
          }
          if (index === 1 || index === 2) {
            sums[index] = '-'
            return
          }
          // 如果查询结果为空
          if (this.tableTotal.length === 0) {
            sums[index] = '0'
          } else {
            sums[index] = this.tableTotal[column.property]
          }
        })
        return sums
      }
    }
  }
</script>

 【几点说明】

 (1)在渲染表头数据时把当前表头和单元格的样式也加进去了 

 <el-table>的表头跟单元格的样式是通过header-cell-stylecell-style实现的,具体请参考Element - The world's most popular Vue UI framework

 

双层的表头为了区别字号大小所以写了2个style,实际上只需要header_stylecell_style就行了

(2) 在渲染单层/多层表头的时候,这里只写了针对两层的情况,如果需要渲染三层及以上可以参考之前写过的一个demo:【vue】基于ElementUI实现动态表格_爱吃香草冰淇淋的阿喵的博客-CSDN博客_vue动态表格

 (3)flexColumnWidth是为了自适应表格列宽,关于这个可以参考上一篇文:【vue】ElementUI el-table自适应列宽实现_爱吃香草冰淇淋的阿喵的博客-CSDN博客_vue表格宽度自适应

【参考】
https://vue-js.com/topic/5f6d68f24590fe0031e591f5

Logo

前往低代码交流专区

更多推荐