通用表格二次封装-完善解决方案

引言

前后端配合分页表格的二次封装一直是前端头痛的事情
普遍的UI组件的表格插件实现功能良莠不齐,项目实现代码过多使用插槽和render配置,使代码冗余
一个项目中的表格实现方案不统一,甚至分页参数每个接口都不一致,看上去就急需优化。。
鉴于vxe-table 的表格实现方案全面,这里我们采取vxe-tablevxe-grid 表格组件二次封装,主要解决的分页前后端约定配置问题,灵活定制数据结构,适用于大型金融PC前端管理系统的通用表格方案

简介

  1. 基于vue2+ vxe-tablevxe-grid 表格组件封装
  2. 主要功能如下
  • 外部请求 非分页
  • 外部请求 前端分页
  • 外部请求 后端分页
  • 内部请求 非分页
  • 内部请求 前端分页
  • 内部请求 后端分页
  • render渲染
  • slot渲染
  • 空数据:三个方面限制
    1. formatter
    2. slots slot
    3. slots render
  • 数据自定义格式化
  • 冻结列配置
  • 虚拟滚动配置
  • 筛选配置(前端筛选,后端筛选)待扩展
  • 其他功能参考 https://vxetable.cn/#/grid/api 自行扩展

约定

主要约定如下

  • 非分页请求参数约定
{
  "body":{
    // ...params
  },
  "head":{}
}
  • 非分页返回参数约定
{
  "head":{},
  "body":{
    "data":[
      // {},
    ]
  }
}
  • 分页请求参数约定
{
  "body":{
    "pageNum":1,
    "pageSize":10
    // ...other params
  },
  "head":{}
}
  • 分页返回参数
{
  "head":{},
  "body":{
    "data":[
      // {},
    ],
    "total":1000,
    "pageSize":10
  }
}

说明文档使用示例

  • template
<template>
  <div class="virtual">
    虚拟表格测试
    <button @click="searchData">外部查询</button>
    <button @click="update">内部查询</button>
    <button @click="reset">重置</button>
    <input type="text" v-model="searchText" />

    <!-- 1. 外部请求 非分页 -->
    <!-- <VirtualTable ref="virtualTable" :columns="columns" :data="dataList" getDataListFromBodyKeysStr="data" @change="tableChange">
    </VirtualTable> -->

    <!-- 2. 外部请求 前端分页 -->
    <!-- <VirtualTable ref="virtualTable" :columns="columns" :data="dataList" getDataListFromBodyKeysStr="data" frontPage @change="tableChange">
    </VirtualTable>  -->

    <!-- 3. 外部请求 后端分页 -->
    <!-- <VirtualTable ref="virtualTable" :columns="columns" :data="dataList" getDataListFromBodyKeysStr="data"
      :otherParams="{ name: 'virtualTable' }" endPage @change="tableChange">
    </VirtualTable> -->

    <!-- 4. 内部请求 非分页 -->
    <!-- <VirtualTable ref="virtualTable" :columns="columns" :apiRequestPromiseFun="tablePageListApi" 
       :otherParams="{ name: 'virtualTable' }" :autoRequest="true"
      @change="tableChange">
    </VirtualTable> -->

    <!-- 5. 内部请求 前端分页 -->
    <!-- <VirtualTable ref="virtualTable" :columns="columns" :apiRequestPromiseFun="tablePageListApi" frontPage
       :otherParams="{ name: 'virtualTable' }" :autoRequest="true"
      @change="tableChange">
    </VirtualTable> -->

    <!-- 6. 内部请求 后端分页 -->
    <!-- <VirtualTable ref="virtualTable" :columns="columns" :apiRequestPromiseFun="tablePageListApi" endPage
      :otherParams="{ name: 'virtualTable' }" :autoRequest="true" @change="tableChange">
    </VirtualTable> -->

    <!-- 7. 插槽渲染 -->
    <!-- <VirtualTable ref="virtualTable" :columns="columns" :apiRequestPromiseFun="tablePageListApi" endPage
      :otherParams="{ name: 'virtualTable' }" :autoRequest="true" :showFooter="true" :footerMethod="footerMethod"
      @change="tableChange">
      <template #cell-empty="data"> {{ data.row }} </template>
      <template #name-header="data"> {{ 'name' + data.columnIndex }} </template>
      <template #name-footer="data"> {{ 'footer>>' + JSON.stringify(Object.keys(data)) }} </template>
    </VirtualTable> -->

    <!-- 8. render渲染 -->
    <!-- 略... 查看 https://vxetable.cn/#/grid/api -->

    <!-- 9. 空数据拦截-三种空数据拦截 formatter/slots/render -->
    <!-- <VirtualTable ref="virtualTable" :columns="columns" :apiRequestPromiseFun="tablePageListApi" endPage
      :otherParams="{ name: 'virtualTable' }" :autoRequest="true" @change="tableChange">
      <template #amount10slot="data">
        {{ `slot定义插槽:${data.cellValue}` }}
      </template>
    </VirtualTable> -->

    <!-- 9. 字典转义-三种数据字典结构 -->
    <!-- <VirtualTable ref="virtualTable" :columns="columns" :apiRequestPromiseFun="tablePageListApi" endPage
      :otherParams="{ name: 'virtualTable' }" :autoRequest="true" @change="tableChange">
    </VirtualTable> -->

    <!-- 10. 冻结列配置 fixed / 虚拟滚动配置 scrollX scrollY [已经默认开启] -->
    <!-- <VirtualTable ref="virtualTable" :columns="columns" :apiRequestPromiseFun="tablePageListApi" endPage
      :otherParams="{ name: 'virtualTable' }" :autoRequest="true" @change="tableChange">
    </VirtualTable> -->

    <!-- 11. 外部筛选-适用于前端分页或者不分页的模式,因为后端分页的分页参数取决于后端 -->
    <!-- <VirtualTable ref="virtualTable" :columns="columns" :apiRequestPromiseFun="tablePageListApi" endPage
      :otherParams="{ name: 'virtualTable' }" :autoRequest="true"
      :filterGridOptionsDataMethod="filterGridOptionsDataMethod" @change="tableChange">
    </VirtualTable> -->
    <VirtualTable ref="virtualTable" :columns="columns" :data="dataList" getDataListFromBodyKeysStr="data" frontPage
      :filterGridOptionsDataMethod="filterGridOptionsDataMethod" @change="tableChange">
    </VirtualTable>
  </div>
</template>
  • script
/**
 * [虚拟滚动表格二次封装组件-功能目录]
 * 1. 外部请求 非分页
 * 2. 外部请求 前端分页
 * 3. 外部请求 后端分页
 * 4. 内部请求 非分页
 * 5. 内部请求 前端分页
 * 6. 内部请求 后端分页
 * 7. render渲染
 * 8. slot渲染
 * 9. 空数据:三个方面限制
 *    1. formatter
 *    2. slots slot
 *    3. slots render
 * 10. 数据自定义格式化
 * 11. 冻结列配置
 * 12. 虚拟滚动配置
 * 13. 筛选配置(前端筛选,后端筛选)待扩展
 * ... 其他功能参考 https://vxetable.cn/#/grid/api 自行扩展
 */

import VirtualTable from './Virtual-table-coms';
/**
 * 转义字典配置
 */
const types1 = {
  1: 'one',
  2: 'two',
  3: 'three',
  4: 'four',
}
const types2 = {
  A: { text: '一', value: '1' },
  B: { text: '二', value: '2' },
  C: { text: '三', value: '3' },
  D: { text: '四', value: '4' },
}
const types3 = [
  { text: '1111', value: '1' },
  { text: '2222', value: '2' },
  { text: '333', value: '3' },
  { text: '4444', value: '4' },
]
const formatEmpty = (v) => {
  if (v === '' || v === null || v === undefined || isNaN(v)) {
    return '--';
  } else {
    return v;
  }
}
export default {
  name: 'VirtualTable-test',
  components: {
    VirtualTable
  },
  props: {
  },
  mounted() {
    // let res = this.$HttpTool.post('table-list/', { name: 'abc' });
    // let res2 = this.$HttpTool.post('table-page-list/', { name: 'abc' });
    // console.log(res)
    // console.log(res2)
  },
  data() {
    return {
      searchText: '',
      rowConfig: {
        useKey: true,
        keyField: 'uuid',
      },
      dataList: [],
      columns: [
        {
          field: "id",
          title: "字段名id",
          minWidth: '150px',
          slots: {
            default: ({ row }) => {
              return [<div style={{ color: 'red' }}>{row.id}</div>]
            },
            header: () => {
              return [<span>{"自定义表头字段名ID"}</span>]
            }
          },
          fixed: "left"
        },
        { field: "uuid", title: "字段名uuid" },
        {
          field: "name", title: "字段名name",
          // 自定义格式化渲染
          formatter: ({ cellValue: v }) => {
            return 'formatter>>>' + formatEmpty(v);
          },
          slots: {
            // slot 渲染
            // default: ({ row, column }) => {
            //   const v = row[column.property];
            //   return 'slots>>>'+formatEmpty(v)
            // }
            // 插槽渲染
            // default: 'cell-empty',
            // header: 'name-header',
            // footer: 'name-footer',
          }
        },
        { field: "amount1", title: "字段名amount1", formatterType: 'options', formatterOptions: types1, minWidth: '150px' },
        { field: "amount2", title: "字段名amount2", formatterType: 'options', formatterOptions: types1, minWidth: '150px' },
        { field: "amount3", title: "字段名amount3", minWidth: '150px' },
        { field: "amount4", title: "字段名amount4", formatterType: 'options', formatterOptions: types2, minWidth: '150px' },
        { field: "amount5", title: "字段名amount5", formatterType: 'options', formatterOptions: types2, minWidth: '150px' },
        { field: "amount6", title: "字段名amount6", formatterType: 'options', formatterOptions: types3, minWidth: '150px' },
        { field: "amount7", title: "字段名amount7", formatterType: 'options', formatterOptions: types3, minWidth: '150px' },
        {
          field: "amount8", title: "字段名amount8", formatterType: 'custom', formatter: ({ cellValue: v }) => {
            return '自定义格式化' + v;
          }
        },
        {
          field: "amount9",
          title: "字段名amount9--",
          minWidth: '150px',
          // slotDefaultType:'custom',
          slots: {
            default: ({ ...p }) => {
              const { row, column } = { ...p };
              // console.log({ row, column })
              // console.log(this.columns[columnIndex])
              const v = row[column.property];
              return 'slots-render>>>' + formatEmpty(v)
            }
          }
        },
        {
          field: "amount10", title: "字段名amount10",
          minWidth: '150px',
          // slotDefaultType:'custom',
          slots: {
            default: 'amount10slot'
          }
        },
        { field: "amount11", title: "字段名amount11" },
        { field: "amount12", title: "字段名amount12" },
        { field: "amount13", title: "字段名amount13" },
        { field: "amount14", title: "字段名amount14" },
        { field: "amount15", title: "字段名amount15" },
        { field: "amount16", title: "字段名amount16" },
        { field: "amount17", title: "字段名amount17" },
        { field: "amount18", title: "字段名amount18" },
        { field: "amount19", title: "字段名amount19" },
        { field: "amount20", title: "字段名amount20" },
        { field: "amount21", title: "字段名amount21" },
        { field: "amount22", title: "字段名amount22" },
        { field: "amount23", title: "字段名amount23" },
        { field: "amount24", title: "字段名amount24" },
        { field: "amount25", title: "字段名amount25" },
        { field: "amount26", title: "字段名amount26" },
        { field: "amount27", title: "字段名amount27" },
        { field: "amount28", title: "字段名amount28" },
        { field: "amount29", title: "字段名amount29" },
        { field: "amount30", title: "字段名amount30", minWidth: '150px', fixed: "right" },
      ]
    };
  },
  watch: {
    searchText() {
      this.$refs['virtualTable'].outerFilterUpdate();
    }
  },
  methods: {
    filterGridOptionsDataMethod(item) {
      return item.uuid.includes(this.searchText)
    },
    footerMethod({ columns, data }) {
      return [
        columns.map(column => {
          if (['sex', 'num'].includes(column.field)) {
            return data
          }
          return null
        })
      ]
    },
    tableChange(d, p, r) {
      console.log(d, p, r);
      {/* this.tableUpdateData(v); // 外部数据传入,后端分页,需要在这里再次查询 */ }
    },
    searchData() {
      console.log('查询表格');
      this.tableUpdateData({ name: '外部请求非分页' });
    },
    async tableUpdateData(p) {
      try {
        this.$refs['virtualTable'].setLoading(true);
        // let res = await this.$HttpTool.post('table-list/', p);
        let res = await this.$HttpTool.post('table-page-list/', p);
        this.dataList = res.data.body;
        console.log(this.dataList)
      } catch (e) {
        console.error(e)
      } finally {
        this.$refs['virtualTable'].setLoading(false);
      }
    },
    tableListApi(p) {
      return this.$HttpTool.post('table-list/', p);
    },
    tablePageListApi(p) {
      return this.$HttpTool.post('table-page-list/', p);
    },
    update() {
      console.log('update')
      this.$refs['virtualTable'].requestList(); // 主动跟新
    },
    reset() {
      console.log('resetTable')
      this.$refs['virtualTable'].resetTable(); // 主动重置
    }
  }
}
  • style
.virtual {
  min-width: 1200px;
}
</style>
  1. 介绍和使用文档到此结束,接下来上源码
  • template
<template>
  <div class="virtual-table">
    <vxe-grid ref="vxe-grid" v-bind="{ ...gridOptions, scrollY, scrollX, ...$attrs }" v-on="$listeners">

      <div slot="empty">空白数据定制模版</div>

      <template v-for="defaultName in defaultSlots" v-slot:[defaultName]="{ row, column, rowIndex, columnIndex }">
        <!-- 默认插槽空值处理 -->
        <template v-if="columns[columnIndex]?.slotDefaultType === 'default'">
          <template v-if="!tableCellEmpty(row[column.property])">
            <EmwAmount v-if="defaultName === 'EmwAmount'" :key="`default_${defaultName}_EmwAmount`" slot="EmwAmount"
              v-bind="{ value: row[column.property], ...columns[columnIndex].defaultSlotProps }">
            </EmwAmount>
            <slot v-else :name="defaultName" :row="row" :column="column" :rowIndex="rowIndex" :columnIndex="columnIndex"
              :cellValue="row[column.property]"></slot>
          </template>
          <span v-else :key="`default_${defaultName}_empty`">{{ '--' }}</span>
        </template>
        <!-- 如果为自定义插槽类型没有空值处理 -->
        <template v-else>
          <slot :name="defaultName" :row="row" :column="column" :rowIndex="rowIndex" :columnIndex="columnIndex"
            :cellValue="row[column.property]"></slot>
        </template>
      </template>

      <template v-for="headerName in headerSlots" v-slot:[headerName]="{ row, column, rowIndex, columnIndex }">
        <slot :name="headerName" :row="row" :column="column" :rowIndex="rowIndex" :columnIndex="columnIndex"></slot>
      </template>

      <template v-for="footerName in footerSlots" v-slot:[footerName]="{ row, column, rowIndex, columnIndex }">
        <slot :name="footerName" :row="row" :column="column" :rowIndex="rowIndex" :columnIndex="columnIndex"></slot>
      </template>

      <template #pager>
        <vxe-pager v-if="ifPage && gridOptions?.data?.length" background
          :layouts="['Total', 'Sizes', 'PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'FullJump']"
          :current-page.sync="tablePage.currentPage" :page-size.sync="tablePage.pageSize" :total="tablePage.total"
          :page-sizes="tablePage.pageSizes" align="center" @page-change="handlePageChange">
          <div slot="right">{{ `共${totalPageNum}页` }}</div>
        </vxe-pager>
        <div class="foot-total-tag" v-if="ifPage && footTotalTag && gridOptions?.data?.length">{{
          `共${tablePage.total || 0}条记录` }}</div>
      </template>

    </vxe-grid>
  </div>
</template>
  • script
const getObjectByKeys = (obj, keys) => {
  const len = keys.length;
  if (len === 0) return obj;
  const tK = keys.shift();
  if (tK in obj) {
    return getObjectByKeys(obj[tK], keys);
  } else {
    return undefined;
  }
}
const splitStrByChar = (str, tag = '.') => {
  return str?.split(tag) || [];
}

const tableCellEmpty = (v) => {
  return v === '' || v === undefined || v === null;
}

/**
 * 为了让虚拟滚动表格更好适应项目
 * 在虚拟滚动表格插槽中植入一些高频组件
 * 配置插槽时直接用
 * columns:{
 *  defaultSlotProps:{...组件的props}
 *  slots:{
 *    default:'EmwAmount'
 *  }
 * }
 */
const CommonComNames = ['EmwAmount'];
import EmwAmount from '../EmwAmount.vue'
export default {
  components: {
    EmwAmount
  },
  props: {
    /**
     * 外部传入展示数据
     * 支持数组和对象,如果为对象要使用 getDataListFromBodyKeysStr 取值
     * 最终传给表格的data一定是数组格式
     */
    data: {
      type: [Array, Object],
      default: () => []
    },
    columns: {
      type: Array,
      default: () => []
    },
    loading: {
      type: Boolean,
      default: false
    },
    /**
     * 当需要表格内部查询数据需要传入一个查询函数
     * 返回一个promise,查询成功后会抛出 on-change 事件返回查询结果(body)
     */
    apiRequestPromiseFun: {
      type: [Function, undefined],
      // default: () => Promise.resolve(true)
      default: undefined
    },
    /**
     * 有些时候后端数给的并不规范,如果需要表格内部查询,就必须保证取得正确的key
     * 默认为body.data ,但有些时候 可能是层级比较深的数据 比如说 body.fundsA.list
     * 那么这个字符串应该取 “fundA.list”
     * 前端上送的分页参数也是依据后端制定的,所以如果是前度分页,
     * 那么抛出的分页参数默认为 keysStr 是由 ‘.’ 分割的最后一端字符串
     * 如 'data.body.pageSize' => pageSize 
     */
    getDataListFromBodyKeysStr: {
      type: String,
      default: 'data.body.data'
    },
    getPageNumFromBodyKeysStr: {
      type: String,
      default: 'data.body.pageNum'
    },
    getPageSizeFromBodyKeysStr: {
      type: String,
      default: 'data.body.pageSize'
    },
    getTotalFromBodyKeysStr: {
      type: String,
      default: 'data.body.total'
    },
    /**
     * 是否前端分页
     * 前端处理分页,展示分页组件
     * 后端数据结构不变
     */
    frontPage: {
      type: Boolean,
      default: false
    },
    /**
     * 是否后端分页
     * 后端处理分页,要与前端约定好数据结构
     * 展示分页组件
     */
    endPage: {
      type: Boolean,
      default: false
    },
    /**
     * 非分页时是否展示记录数量
     */
    footTotalTag: {
      type: Boolean,
      default: true
    },
    /**
     * 默认初始化分页设置
     * 一般也就配置 pageSize 和 pageSizes
     */
    pageOptions: {
      type: Object,
      default: () => ({
        pageSize: 23,
        pageSizes: [10, 23, 30, 50, 60, 100, 200]
      })
    },
    /**
     * 除了分页参数以外的其他参数
     */
    otherParams: {
      type: [Object, null],
      default: () => null
    },
    /**
     * 当内部请求时是否自动请求
     * 如果手动请求请使用 requestList
     */
    autoRequest: {
      type: Boolean,
      default: true
    },
    /**
     * 虚拟滚动配置
     * 默认开启
     */
    scrollY: {
      type: Object,
      default: () => ({
        enabled: true,
        gt: 30,
        oSize: 0
      })
    },
    scrollX: {
      type: Object,
      default: () => ({
        enabled: true,
        gt: 20,
        oSize: 0
      })
    },
    /**
     * 外部筛选函数配置
     * 适用于前端自己的筛选显示
     * 外部要配置筛选调教
     * 配合 outerFilterUpdate 对外api使用
     */
    filterGridOptionsDataMethod: {
      type: Function,
      default: () => true
    },
    /**
     * 外部列表处理函数配置
     * 有些接口返回数据不理想,需要表格自动处理成理想的数据列表
     * 最好要求后端处理返回理想的数据,实在不行出此下策
     */
    mapGridOptionsDataListFactory: {
      type: Function,
      default: i => i
    }
  },
  watch: {
    data(v) {
      this.outerRequestDataTableInit(v);
    },
    columns: {
      handler(val) {
        /**
         * 列配置变化,表格变化
         */
        this.gridOptions.columns = val || [];
        this.columnsInit();
      },
      immediate: true
    },
    loading(val) {
      /**
       * 外部配置loading,表格进入加载状态
       */
      // console.log('loading change>>', val);
      this.gridOptions.loading = val;
    }
  },
  computed: {
    /**
     * 根据 page 参数在返回体中的取值字符串获取分页参数
     * 分页参数默认配置 pageSize pageNum, total
     */
    pageNumKey() {
      return this.getPageNumFromBodyKeysStr?.split('.')?.pop() || 'pageNum';
    },
    pageSizeKey() {
      return this.getPageSizeFromBodyKeysStr?.split('.')?.pop() || 'pageSize';
    },
    totalKey() {
      return this.getTotalFromBodyKeysStr?.split('.')?.pop() || 'total';
    },
    ifPage() {
      return this.frontPage || this.endPage;
    },
    totalPageNum() {
      return Math.ceil(this.tablePage?.total / this.tablePage?.pageSize);
    },
    /**
     * 获取数据模式
     * 1. 外部传入data数据
     *    如果data为数组,那么直接渲染表格
     *    如果data为对象,需要传入 getDataListFromBodyKeysStr 由内部取值
     * 2. 内部请求获取列表数据
     *    需要传入 getDataListFromBodyKeysStr 取值,getDataListFromBodyKeysStr默认为 ‘data’
     */
    getDataModal() {
      return typeof this.apiRequestPromiseFun === 'function' ? 'inter_request_data' : 'exter_props_data'
    },
    pageRequestParams() {
      const { currentPage, pageSize } = this.tablePage;
      return { [this.pageNumKey]: currentPage, [this.pageSizeKey]: pageSize, ...this.otherParams };
    },
    /**
     * 插槽名获取 start
     */
    defaultSlots() {
      return this.slotsFilter('default')
    },
    headerSlots() {
      return this.slotsFilter('header')
    },
    footerSlots() {
      return this.slotsFilter('footer')
    },
    /**
     * 插槽名获取 end
     */

  },
  data() {
    return {
      // 内部请求数据
      inerRequestData: null,
      tablePage: {
        total: 0,
        currentPage: 1,
        pageSize: 10,
        pageSizes: [10, 30, 50, 100, 200]
      },
      gridOptions: {
        border: false,
        resizable: true,
        showOverflow: true,
        loading: false,
        height: 560,
        align: 'center',// 表头位置居中
        showHeaderOverflow: true,//表头单行,悬浮显示
        columns: [],
        data: []
      }
    }
  },
  created() {
    this.resetTablePage();
    // 内部请求数据
    if (this.getDataModal === 'inter_request_data' && this.autoRequest) {
      this.requestList();
    }
  },
  mounted() {
    // this.throwCurrentPageParams();
  },
  methods: {
    outerRequestDataTableInit(v) {
      // 外部传入数据
      /**
       * 直接传入数据渲染时
       * 数据变动,表格的数据重新刷新
       * 如果外部传入的是数组,那就直接取
       * 如果外部传入的非数组,就要依赖 getDataListFromBodyKeysStr 取值
       */
      if (!v) return;
      const list = this.tableListData(v);
      !this.frontPage && (this.gridOptions.data = list);
      this.tablePage.total = list.length;
      /**
       * 如果是前端分页需要做数据处理
       */
      this.frontPage && this.seqDataMethod(v);
      this.throwCurrentPageParams();
    },
    interRequestDataTableInit() {
      // 内部请求数据
      /**
       * 内部请求数据渲染
       */
      if (this.inerRequestData === null) return;
      const list = this.tableListData(this.inerRequestData);
      this.gridOptions.data = list;
      if (this.frontPage) {
        this.tablePage.total = list.length;
        this.seqDataMethod(this.inerRequestData);
      }
      if (this.endPage) {
        this.tablePage.total = getObjectByKeys(this.inerRequestData, splitStrByChar(this.getTotalFromBodyKeysStr)) || 0;
      }
      this.throwCurrentPageParams();
    },
    /**
     * 对外API
     * 当配置了 filterGridOptionsDataMethod 时
     * 主动触发筛选数据
     * 无论是内部请求还是外部请求,都不做请求,直接进行数据筛选
     * 但一定要重置分页参数
     */
    outerFilterUpdate() {
      this.resetTablePage();
      if (this.getDataModal === 'inter_request_data') {
        // 内部请求
        this.interRequestDataTableInit();
      } else {
        // 外部请求
        this.outerRequestDataTableInit(this.data);
      }
      this.$refs['vxe-grid'].updateData();
    },
    tableCellEmpty,
    columnsInit() {
      /**
       * 空数据默认格式化
       */
      this.formatEmptyColumns();
      /**
       * 扩展通用组件自定义参数
       */
      this.addSlotPropsColumns();
    },
    formatEmptyColumns() {
      this.columns.forEach(col => {
        /**
         * 没有插槽设置的时候采用的是 formatter 渲染
         * 涉及相关配置项
         * 原生 formatter [function]
         * 拓展 formatterOptions [Object/Array] 下文有注释
         * 拓展 formatterType [enum: 'default'/'custom'/'options']
         */
        if (!col?.slots?.default) {
          const f = col?.formatter;
          /**
           *  格式化类型枚举 
           *  默认 default (如果空值显示 ‘--’)
           *  自定义 custom (不会添加空值 ‘--’ 机制)
           *  枚举 options (根据字典转义码值)
           */
          const formatterOptions = col?.formatterOptions;
          const type = col?.formatterType || 'default';
          const emptyF = ({ cellValue: v }) => tableCellEmpty(v) ? '--' : v;

          if (typeof f === 'function') {
            // 如果定义了格式化,则对格式化函数进行一次包装
            if (type === 'default') {
              col.formatter = ({ ...p }) => {
                const { cellValue: v } = p;
                return tableCellEmpty(v) ? '--' : f({ ...p });
              }
            } else if (type === 'custom') {
              // 如果是自定义直接返回
              col.formatter = ({ ...p }) => {
                return f({ ...p });
              }
            }
          } else if (!f) {
            if (type !== 'options') {
              // 如果没有设置格式化,则设置默认值
              col.formatter = emptyF;
            } else {
              // 定义了options 就根据options进行 码-值 转义
              if (Array.isArray(formatterOptions)) {
                // formatterOptions 兼容 
                // [
                //   {value:<code>,text:<label>},
                //   {value:<code>,text:<label>},
                //   ...
                // ] 
                // 数据格式
                col.formatter = ({ ...p }) => {
                  const { cellValue: v } = p;
                  // console.log(v, formatterOptions)
                  return formatterOptions.find(i => i?.value === v)?.text || '--';
                }
              } else if (typeof formatterOptions === 'object') {
                const keys = Object.keys(formatterOptions);
                col.formatter = ({ ...p }) => {
                  const { cellValue: v } = p;
                  if (tableCellEmpty(v) || !keys.includes(v)) {
                    // return JSON.stringify(keys);
                    return '--';
                  } else {
                    if (typeof formatterOptions[v] === 'object') {
                      // formatterOptions 兼容 
                      // { 
                      //   <code1>:{text:<label>,value:<code>},
                      //   <code2>:{text:<label>,value:<code>},
                      //    ...
                      // } 
                      // 数据格式
                      return formatterOptions[v]?.text || '--';
                    } else {
                      // formatterOptions 兼容 
                      // { 
                      //   <code1>:value1,
                      //   <code2>:value2,
                      //    ...
                      // } 
                      // 数据格式
                      return formatterOptions[v] || '--';
                    }
                  }
                }
              }
            }
          }
        }
        /**
         * 有render函数时涉及配置参数
         * 拓展 slotDefaultType [enum: 'default'/'custom']
         * 当 default 时 数据为空默认 ‘--’
         * 当为 custom 时 自定义
         */
        const slotDefaultType = col?.slotDefaultType || 'default';
        col.slotDefaultType = slotDefaultType; // 这里赋值,模版需要做判断
        if (typeof col?.slots?.default === 'function' && slotDefaultType === 'default') {
          const render_f = col?.slots?.default;
          col.slots.default = ({ ...p }) => {
            const { row, column } = p;
            const v = row[column.property];
            return tableCellEmpty(v) ? '--' : render_f({ ...p });
          }
        }
        /**
         * 有插槽,如果数据为空,数据默认 ‘--’
         */
        // if(typeof col?.slots?.default === 'string' && slotDefaultType === 'default'){
        // 具体实现在模版里实现
        // }

        // if(slotDefaultType === 'custom'){
        // 不做任何操作
        // }
      });
    },
    addSlotPropsColumns() {
      this.columns.forEach(col => {
        /**
         * 如果配置有通用插槽组件的话,默认props为 defaultSlotProps
         */
        if (CommonComNames.includes(col?.slots?.default)) {
          col.defaultSlotProps = col?.defaultSlotProps || {}
        }
      })
    },
    slotsFilter(slotType) {
      return this.columns.filter(i => i?.slots && i?.slots[slotType] && typeof i?.slots[slotType] === 'string').map(x => x?.slots[slotType]);
    },
    async requestList() {
      if (this.getDataModal === 'exter_props_data') throw new Error('参数错误,未配置内部请求api');
      this.gridOptions.loading = true;
      try {
        const params = this.endPage ? this.pageRequestParams : this.otherParams;
        this.inerRequestData = await this.apiRequestPromiseFun(params);
        this.interRequestDataTableInit();
      } catch (e) {
        this.resetTable(false);
        console.log(e);
      } finally {
        this.gridOptions.loading = false;
      }
    },
    resetTable(ifRequestTag = true) {
      this.gridOptions.data = [];
      this.inerRequestData = null;
      this.setLoading(false);
      this.resetTablePage();
      ifRequestTag && this.autoRequest && this.getDataModal === 'inter_request_data' && this.requestList();
    },
    tableListData(v) {
      const originList = Array.isArray(v) ? v : (getObjectByKeys(v, splitStrByChar(this.getDataListFromBodyKeysStr)) || []);
      return originList.filter(this.filterGridOptionsDataMethod).map(this.mapGridOptionsDataListFactory);
    },
    resetTablePage(p = {}) {
      this.tablePage = {
        total: 0,
        currentPage: 1,
        pageSize: this.pageOptions.pageSize[0],
        pageSizes: this.pageOptions.pageSizes,
        ...this.pageOptions,
        ...p
      }
    },
    seqDataMethod(data) {
      this.gridOptions.data = this.tableListData(data).filter((item, rowIndex) => {
        const s = (this.tablePage.currentPage - 1) * this.tablePage.pageSize;
        const e = this.tablePage.currentPage * this.tablePage.pageSize;
        return s <= rowIndex && rowIndex < e;
      });
      // console.log(this.gridOptions.data)
    },
    throwCurrentPageParams() {
      const tableData = this.gridOptions.data;
      let requestParams;
      let pageParams;
      // 分页-前端分页
      if (this.ifPage && this.frontPage) {
        requestParams = this.otherParams;
        pageParams = this.tablePage;
      }
      // 分页-后端分页
      if (this.ifPage && this.endPage) {
        requestParams = this.pageRequestParams;
        pageParams = this.tablePage;
      }
      // 非分页
      if (!this.ifPage) {
        requestParams = this.otherParams;
        pageParams = null;
      }
      this.$emit('change', tableData, requestParams, pageParams);
    },
    async handlePageChange({ currentPage, pageSize }) {
      // console.log({ currentPage, pageSize })
      this.tablePage.currentPage = currentPage;
      this.tablePage.pageSize = pageSize;
      if (this.getDataModal === 'exter_props_data') {
        // 如果是后端分页,外部传入数据的话,直接抛出分页参数即可
        this.frontPage && this.seqDataMethod(this.data);
        this.throwCurrentPageParams();
      } else if (this.getDataModal === 'inter_request_data') {
        if (this.frontPage) {
          this.seqDataMethod(this.inerRequestData);
          this.throwCurrentPageParams();
        }
        // 如果是内部请求数据又是后端分页,那么必须在request响应结果后才能跑出change事件
        this.endPage && await this.requestList();
      }
    },
    getGridTable() {
      return this.$refs['vxe-grid'];
    },
    /**
     * 
     * @param {外部设置loading} loading
     * 支持两种设置loading的模式,还可以直接传入props:loading设置 
     */
    setLoading(loading) {
      this.gridOptions.loading = loading;
    }
  }
}
  • style
.virtual-table {
  width: 100%;

  .vxe-pager .vxe-pager--right-wrapper {
    &>div {
      line-height: 2.2;
    }

    margin: 0;
  }

  .foot-total-tag {
    text-align: right;
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容