Vue 表格封装 BasicTableVue

本文最后更新于:2019年3月26日 凌晨

Vue 表格封装 BasicTableVue

场景

后台项目中大量使用表格,我们使用的 element-ui 中的表格并不足以满足吾辈的需求,而且使用起来重复的代码实在太多,所以吾辈便对数据表格进行了二次封装。

实现

API 列表

  • [el]: 绑定的选择器。默认为 '#app'
  • data: 数据对象
    • form: 搜索表单绑定对象
    • columns: 表格的列数组。每个列定义参考 TableColumn
    • [formShow]: 是否显示搜索表单
    • [page]: 分页信息,包含分页的数据。具体参考 Page
    • [selectedIdList]: 选中项的 id 列表
    • [fileSelectorShow]: 是否显示导入 Excel 的文件选择器
  • methods: 绑定的函数
    • createForm: 初始化 form 表单,主要是为了自定义初始化逻辑
    • getPage: 获取分页信息
    • exportFile: 导出文件
    • importFile: 导入文件
    • deleteData: 删除选择的数据
    • [init]: 初始化函数,如果可能请使用该函数而非重写 mounted 生命周期函数,该函数会在 mounted 中调用
    • [resetFile]: 重置导入选择的文件,必须为 input:file 绑定属性 ref="fileInput"
    • [searchPage]: 搜索分页信息
    • [resetPage]: 重置分页信息
    • [toggle]: 切换搜索表单显示
    • [selection]: 选择的 id
    • [changeSize]: 改变一页的大小
    • [goto]: 跳转到指定页数
    • [deleteSelected]: 删除选择的数据项
    • [showFileSelector]: 是否显示导入文件选择器
    • [initCommon]: 初始化功能,如果重写了 mounted 生命周期函数,请务必调用它!

自定义表格组件

/**
 * 自定义表格组件
 */
Vue.component('my-table', {
  /**
   * 列
   */
  props: {
    columns: {
      type: Array,
      default: [],
    },
    data: {
      type: Array,
      default: [],
    },
  },
  template: `<el-table
  :data="data"
  tooltip-effect="dark"
  style="width: 100%"
  border
  @selection-change="handleSelectionChange"
>
  <template v-for="column in columns">

    <el-table-column
      :type="column.type"
      :prop="column.prop"
      :label="column.title"
      :align="column.align"
      :sortable="column.sortable"
      :width="column.width"
      :formatter="column.formatter"
      v-if="column.customComponent"
    >
      <!--suppress HtmlUnknownAttribute -->
      <template #default="scope">
        <!--这里将传递给模板当前行的数据-->
        <slot :name="humpToLine(column.prop)" :row="scope.row"></slot>
      </template>
    </el-table-column>
    <el-table-column
      :type="column.type"
      :prop="column.prop"
      :label="column.title"
      :align="column.align"
      :sortable="column.sortable"
      :width="column.width"
      :formatter="column.formatter"
      v-else
    >
    </el-table-column>
  </template>

</el-table>`,
  methods: {
    handleSelectionChange(val) {
      this.$emit('handle-selection-change', val)
    },
    humpToLine(data) {
      return toLine(data)
    },
  },
})

定义一些公共的实体

/**
 * 分页信息,多次使用到所以定义一个公共的
 */
class Page {
  /**
   * 构造函数
   * @param {Number} current 当前页数,从 1 开始
   * @param {Number} size 每页的数量
   * @param {Number} total 数据总条数
   * @param {Number} pages 数据总页数
   * @param {Array} records 一页的数据记录
   * @param {...Object} [args] 其他的参数项,这里只是为了避免遗漏
   * @returns {Page} 分页对象
   */
  constructor({
    current = 1,
    size = 10,
    total = 0,
    pages = 0,
    records = [],
    ...args
  } = {}) {
    this.current = current
    this.size = size
    this.total = total
    this.pages = pages
    this.records = records
    Object.assign(this, args)
  }
}

/**
 * 表格的列
 */
class TableColumn {
  /**
   * 格式化日期事件
   * @param value 字段的值
   * @returns {String|*} 格式化得到的日期时间字符串 TableColumn.datetimeFormat()
   */
  static datetimeFormat(_row, _column, value, _index) {
    return !value ? '' : rx.dateFormat(new Date(value), 'yyyy-MM-dd hh:mm:ss')
  }

  /**
   * 构造函数
   * @param {String} [prop] 字段名
   * @param {String} [title] 标题
   * @param {'selection'} [type] 列类型,可以设置为选择列
   * @param {Boolean} [sortable=true] 排序方式
   * @param {Number} [width] 宽度
   * @param {'center'} [align='center'] 水平对齐方式
   * @param {Function} [formatter] 格式化列
   * @param {Boolean} [customComponent] 是否自定义组件
   * @param {...Object} [args] 其他的参数项,这里只是为了避免遗漏
   */
  constructor({
    prop,
    type,
    width,
    title,
    sortable = true,
    align = 'center',
    formatter,
    customComponent,
    ...args
  } = {}) {
    this.prop = prop
    this.type = type
    this.width = width
    this.align = align
    this.title = title
    this.sortable = sortable
    this.align = align
    this.formatter = formatter
    this.customComponent = customComponent
    Object.assign(this, args)
  }
}

定义一个 BasicTableVue 继承 Vue

/**
 * 基本的表格数据配置
 */
class BasicTableData {
  /**
   * 构造函数
   * @param {Object} [form={}] 搜索表单,子类一般需要覆盖(不覆盖的话可能在 html 中没有提示)
   * @param {Array<TableColumn>} [columns=[]] 列信息列表,子类必须覆盖
   * @param {Boolean} [formShow=false] 是否显示搜索表单
   * @param {Page} [page=new Page()] 分页信息,包含数据列表
   * @param {Array} [selectedIdList=[]] 选择的列表 id
   * @param {Boolean} [fileSelectorShow=false] 导入文件选择器是否需要
   */
  constructor({
    form = {},
    columns = [],
    formShow = false,
    page = new Page(),
    selectedIdList = [],
    fileSelectorShow = false,
  } = {}) {
    this.form = form
    this.columns = columns
    this.formShow = formShow
    this.page = page
    this.selectedIdList = selectedIdList
    this.fileSelectorShow = fileSelectorShow
  }
}

/**
 * 基本的表格方法
 */
class BasicTableMethods {
  /**
   * 构造函数
   * @param {Function} createForm 初始化 form 表单,主要是为了自定义初始化逻辑
   * @param {Function} getPage 获取分页信息,需要覆盖
   * @param {Function} exportFile 导出文件,需要覆盖
   * @param {Function} importFile 导入文件,需要覆盖
   * @param {Function} deleteData 删除选择的数据,需要覆盖
   * @param {Function} init 初始化函数,如果可能请使用该函数而非重写 mounted 生命周期函数,该函数会在 mounted 中调用
   * @param {Function} [resetFile] 重置导入选择的文件,必须为 input:file 绑定属性 ref="fileInput"
   * @param {Function} [searchPage] 搜索分页信息
   * @param {Function} [resetPage] 重置分页信息
   * @param {Function} [toggle] 切换搜索表单显示
   * @param {Function} [selection] 选择的 id
   * @param {Function} [changeSize] 改变一页的大小
   * @param {Function} [goto] 跳转到指定页数
   * @param {Function} [deleteSelected] 删除选择的数据项
   * @param {Function} [showFileSelector] 是否显示导入文件选择器
   * @param {Function} [initCommon] 初始化功能,如果重写了 mounted 生命周期函数,请务必调用它!
   */
  constructor({
    createForm = function() {
      throw new Error('如果需要搜索条件,请重写 initForm() 方法')
    },
    getPage = async function(page, entity) {
      throw new Error('如果需要自动分页,请重写 getPage() 方法')
    },
    exportFile = async function() {
      throw new Error('如果需要导出数据,请重写 exportFile() 方法')
    },
    importFile = function() {
      throw new Error('如果需要导入数据,请重写 importFile() 方法')
    },
    deleteData = async function(idList) {
      throw new Error('如果需要删除数据,请重写 deleteData 方法')
    },
    init = async function() {},
    resetFile = function() {
      const $el = this.$refs['fileInput']
      if (!$el) {
        throw new Error(
          '如果需要清空选择文件,请为 input:file 绑定属性 ref 的值为 fileInput',
        )
      }
      $el.value = ''
    },
    searchPage = async function() {
      try {
        this.page = await this.getPage(this.page, this.form)
      } catch (e) {
        console.error(e)
        await rxPrompt.dangerMsg('查询数据失败,请刷新页面')
      }
    },
    resetPage = async function() {
      this.form = this.createForm()
      await this.searchPage()
    },
    toggle = function() {
      this.formShow = !this.formShow
    },
    selection = function(data) {
      this.selectedIdList = data.map(({ id }) => id)
    },
    changeSize = function(size) {
      this.page.current = 1
      this.page.size = size
      this.searchPage()
    },
    goto = function(current) {
      if (!current) {
        current = this.page.current
      }
      if (current < 1) {
        return
      }
      if (current > this.page.pages) {
        return
      }
      this.page.current = current
      this.searchPage()
    },
    deleteSelected = async function() {
      const result = await this.deleteData(this.selectedIdList)
      if (result.code !== 200 || !result.data) {
        await rxPrompt.msg('')
        return
      }
      // noinspection JSIgnoredPromiseFromCall
      rxPrompt.msg('删除成功')
      this.page.current = 1
      await this.searchPage()
    },
    showFileSelector = function() {
      this.fileSelectorShow = !this.fileSelectorShow
    },
    initCommon = async function() {
      this.form = this.createForm()
      this.searchPage()
    },
  } = {}) {
    this.createForm = createForm
    this.getPage = getPage
    this.searchPage = searchPage
    this.resetPage = resetPage
    this.toggle = toggle
    this.selection = selection
    this.changeSize = changeSize
    this.goto = goto
    this.exportFile = exportFile
    this.importFile = importFile
    this.resetFile = resetFile
    this.deleteData = deleteData
    this.init = init
    this.deleteSelected = deleteSelected
    this.showFileSelector = showFileSelector
    this.initCommon = initCommon
  }
}

/**
 * 基本的 vue 表格配置信息
 */
class BasicTableOption {
  /**
   * 构造函数
   * @param {String} [el='#app'] 标签选择器
   * @param {BasicTableData} data 数据
   * @param {BasicTableMethods} methods 方法
   * @param {Function} mounted 初始化方法
   */
  constructor({
    el = '#app',
    data = new BasicTableData(),
    methods = new BasicTableMethods(),
    mounted = async function() {
      await this.initCommon()
      await this.init()
    },
  } = {}) {
    this.el = el
    this.data = data
    this.methods = methods
    this.mounted = mounted
  }
}

/**
 * 基本的表格 vue 类
 */
class BasicTableVue extends Vue {
  /**
   * 构造函数
   * @param {BasicTableOption} option 初始化选项
   * @param {BasicTableData|Function} option.data vue 的 data 数据,如果是 {@link Function} 类型,则必须返回 {@link BasicTableData} 的结构
   * @param {BasicTableMethods} option.methods vue 中的 methods 属性
   * @param {Function} option.mounted 初始化方法,如果覆盖则必须手动初始化表格
   */
  constructor({ data, methods, mounted, ...args } = {}) {
    //注:这里为了应对 data 既有可能是对象,又有可能是函数的情况
    super(
      _.merge(new BasicTableOption(), {
        data: function() {
          return _.merge(
            new BasicTableData(),
            typeof data === 'function' ? data.call(this) : data,
          )
        },
        methods,
        mounted,
        ...args,
      }),
    )
  }
}

注:这里分开这么多的类是因为便于 IDE 进行提示

使用

下面简单的使用一下 BasicTableVue

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <main>
      <h1>用户列表</h1>
      <!-- 使用内置函数 toggle 切换表单是否显示 -->
      <button @click="toggle">高级搜索</button>
      <!-- 使用 formShow 属性控制表单是否显示 -->
      <form v-show="formShow">
        <div>
          <label for="name">名字:</label>
          <input v-model="form.name" name="name" type="text" />
        </div>
        <div>
          <label for="age">年龄:</label>
          <input v-model="form.age" name="age" type="number" />
        </div>
        <div>
          <!-- 使用 searchPage 查询 -->
          <button @click="searchPage">查询</button>
          <!-- 使用 resetPage 重置条件并搜索 -->
          <button @click="resetPage">重置</button>
        </div>
      </form>
      <div>
        <!-- 
          分页数据绑定 page 对象的 records 属性
          表格的列绑定 columns 属性(需要自定义覆盖)
          选中的项需要将 selection 属性绑定到 @handle-selection-change 事件
         -->
        <my-table
          :data="page.records"
          :columns="columns"
          @handle-selection-change="selection"
        >
          <!-- 
            定义自定义操作列
            scope 指代当前行的信息
           -->
          <template #operating="scope">
            <span>
              <!-- 将自定义的函数绑定到 @click.stop.prevent 上 -->
              <button @click.stop.prevent="() => viewInfo(scope.row)">
                查看信息
              </button>
            </span>
          </template>
        </my-table>
        <!-- 
          分页组件
          将内置的属性或函数绑定到 el-pagination 组件上
          changeSize(): 改变一页数据大小的函数
          goto(): 跳转指定页的函数
          page: 具体参考 Page 对象
         -->
        <el-pagination
          background
          @size-change="changeSize"
          @current-change="goto"
          :current-page="page.current"
          :page-sizes="[10, 20, 30]"
          :page-size="page.size"
          layout="total, sizes, prev, pager, next, jumper"
          :total="page.total"
        >
        </el-pagination>
      </div>
    </main>
    <script src="/user-info.js"></script>
  </body>
</html>

JavaScript 部分

class UserInfo {
  constructor({ id, name, age, ...args }) {
    this.id = id
    this.name = name
    this.age = age
    Object.assign(this, args)
  }
}

const app = new BasicTableVue({
  data: {
    columns: [
      new TableColumn({ width: 30, type: 'selection' }),
      new TableColumn({ prop: 'name', title: '姓名' }),
      new TableColumn({ prop: 'age', title: '年龄' }),
      new TableColumn({
        prop: 'operating',
        title: '操作',
        customComponent: true,
      }),
    ],
  },
  methods: {
    createForm() {
      return new UserInfo()
    },
    async getPage(page, entity) {
      return await baseUserInfoApi.page(page, entity)
    },
    deleteData(idList) {
      return baseCustomerApi.delete(idList)
    },
    viewInfo(row) {
      forward('/user_info_detail', row)
    },
    async init() {
      console.log('这里想做一些自定义的初始化操作')
    },
  },
})

这里需要注意一些要点

  1. 如果需要在 data 中调用 methods 中的函数,则 data 必须是一个函数并返回对象
  2. 不要直接重写 mounted() 生命周期函数,而是在重写的 init() 中进行自定义操作
  3. 任何实体都需要有 ...args 属性以避免一些没有声明的属性找不到

那么,关于 BasicTableVue 的封装便到此结束了。这是一个相当简陋的封装,如果有什么更好的方式,后面也会更新。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!