Writing a common management platform components common table

Now the company to a year, a year in handling projects do six or seven, but then most of them are some of the management functions of the platform, while the management platform to do most is to show a variety of forms, so development process in order to improve development efficiency, packaging some common functional components is very necessary, where I put my spreadsheet component in the development process of the package share, of course, there is definitely a lot less, because the current So far I still have not achieved some of the ideas, but also hope to communicate with each other about it when the initiate, and hit nobody could blame me ah 0.0

Development environment

I am here using vue family bucket + ElementUI, after all management platform, not so high requirements, then random version, after all, merely describes a design method, common

demand analysis

Management platform table page typically contains these parts functions: operation button (add, batch delete, etc.); 2 form data screening items (commonly used with select filter, time filter, search filters, etc.); 3 form the main body;... 4. paging.

1. Operation button design

There are two ways to add operating here I think of a button, first: vue provide direct use of slots , direct-defined buttons, styles, and event operation buttons on the outside, through the insertion of the components inside the slot; the second species: define a target component external button, to pass through internal communication Sons assembly table assembly, the internal components of buttons for parsing, rendering, probably in the following format:

[
    {
        name: 'addBtn',
        text: '新增', // 按钮文案
        icon: 'el-icon-plus', // 按钮图标
        style: 'primary', // 按钮样式(这里取element的按钮样式)
        class: 'addBtn', // 自定义按钮class
        func: 'toAdd' // 按钮点击事件
    }, {
        name: 'multiDelBtn',
        text: '批量删除',
        icon: 'el-icon-delete',
        style: 'danger',
        class: 'multiDel',
        func: 'toMultiDel'
    }
]
复制代码

2. Form data filtering items

Design screening items did not think there is any good, it will say a few common features of those above, then determine whether to show the parameters, if there are other needs can be customized external definition, and then inserted through the slot

3. Form the body

Table consists of two parts: header and body form, were analyzed separately

I form the first side is designed to parse the outer assembly within the internal configuration item defined transfer assembly, assembly, the following format:

[
    {
        prop: 'name', // 表格数据对应的字段
        label: '用户姓名', // 表格头展示的信息
        sortable: false,  // 这一列是否支持排序,true|false|'custom',可以不传(为true是前端排序,不过前端排序没什么意义,一半排序的话还是传‘custom’进行服务端排序)
        minWidth: '100', // 这一列的最小宽度(用minWidth是因为在表格宽度不够的时候有个限制不会变形,在宽度过大的时候又能够按照各列的比例进行伸展,perfect!)
    }, {
        prop: 'address',
        label: '住址',
        minWidth: '170'
    }, {
        prop: 'age',
        label: '年龄',
        sortable: 'custom',
        minWidth: '80'
    }
]
复制代码

Form body nothing, you see the element-ui on the line

4. Paging

Paging has nothing to say, built-in components, less than a page is not displayed in a page element selected component library functions they need

Component Development

The demand component is encapsulated

1. Operation button design

<div class="header-operate">
    <!-- 操作按钮插槽 -->
    <slot name="operateBtn"></slot>
    <el-button
      v-for="(btnItem, btnIndex) in propConfig.btnList"
      :key="btnIndex"
      :class="item.class"
      :type="item.style"
      :icon="item.icon"
      @click="toEmitFunc(item.func)"
    >{{item.text}}</el-button>
</div>
复制代码

2. Screening of the project design

<div class="header-filter">
    <!-- 筛选项插槽 -->
    <slot name="filter"></slot>
    <el-select
      class="filter-iterm"
      v-if="propConfig.showFilter"
      v-model="filter"
      size="small"
      :placeholder="propConfig.filterPlaceholder"
      :clearable="true"
      filterable
      @change="handleSelect"
    >
      <el-option
        v-for="(item, index) in propConfig.filterOptions"
        :key="index"
        :label="item.label"
        :value="item.value"
      ></el-option>
    </el-select>
    <el-date-picker
      v-if="propConfig.showDatePick"
      v-model="dateRange"
      class="filter-iterm"
      align="right"
      size="small"
      format="timestamp"
      value-format="timestamp"
      :type="propConfig.datePickType"
      :clearable="true"
      :start-placeholder="propConfig.startPlaceholder"
      :end-placeholder="propConfig.endPlaceholder"
      @change="handleTimerange"
    ></el-date-picker>
    <el-input
      class="table-search filter-iterm"
      v-if="propConfig.showSearch"
      size="small"
      :placeholder="propConfig.searchPlaceholder"
      v-model="search"
      @keyup.13.native="toSearch"
    >
      <i slot="suffix" class="el-input__icon el-icon-search" @click="toSearch"></i>
    </el-input>
</div>
复制代码

3.table main design

<el-table
    :data="tableData.tbody"
    style="width: 100%"
    @selection-change="handleSelection"
    @sort-change="handleSort"
    >
    <el-table-column v-if="tableData.isMulti" type="selection" width="50"></el-table-column>
    <template v-for="(item, index) in tableData.thead">
      <el-table-column
        :key="index"
        :prop="item.prop"
        :label="item.label"
        :width="item.width"
        :min-width="item.minWidth"
        :sortable="item.sortable"
      >
        <template slot-scope="scope">
          <span class="default-cell" :title="scope.row[item.prop]">{{scope.row[item.prop]}}</span>
        </template>
      </el-table-column>
    </template>
</el-table>
复制代码

4. Page Design

<el-pagination
    class="table-pagination"
    v-if="tableData.pageInfo.total > tableData.pageInfo.size"
    layout="total, prev, pager, next, jumper"
    :current-page.sync="tableData.pageInfo.page"
    :page-size="tableData.pageInfo.size"
    :total="tableData.pageInfo.total"
    @current-change="pageChange"
></el-pagination>
复制代码

5. Accept the transfer involved methods

props: {
    tableConfig: {
      type: Object,
      default: () => {
        return {}
      }
    },
    tableData: {
      type: Object,
      default: () => {
        return {
          thead: [],
          tbody: [],
          isMulti: false, // 是否展示多选
          pageInfo: { page: 1, size: 10, total: 0 } // 默认一页十条数据
        }
      }
    }
},
  methods: {
    toEmitFunc (funName, params) {
      this.$emit(funName, params)
    },
    toSearch () {
      this.toEmitFunc('setFilter', { search: this.search, page: 1 })
    },
    pageChange (val) {
      this.toEmitFunc('setFilter', { page: val })
    },
    handleSelection (val) {
      let cluster = {
        id: [],
        status: [],
        grantee: [],
        rows: []
      }
      val.forEach(function (element) {
        cluster.id.push(element.id)
        cluster.status.push(element.status)
        cluster.rows.push(element)
        if (element.grantee) cluster.grantee.push(element.grantee)
      })
      this.toEmitFunc('selectionChange', cluster)
    },
    handleSort (value) {
      this.toEmitFunc('setFilter', {
        prop: value.prop,
        order: value.order
      })
    },
    handleTimerange () {
        if (this.dateRange) {
            this.eventBus('setFilter', {
              startTime: this.dateRange[0],
              endTime: this.dateRange[1]
            })
        } else {
            this.eventBus('setFilter', {
              startTime: '',
              endTime: ''
            })
        }
    },
    handleSelect () {
      this.toEmitFunc('setFilter', {
        filter: this.filter
      })
    }
  }
复制代码

See this, you say for sure that this is not the element using the table do? Ah ... you said makes sense, I actually can not refute 0.0, the following I will add some of their own design ideas bar (づ ¯3¯) づ

The first to write demo, in the process of writing to a step by step to improve inadequate

<!--外部引用文件-->
<template>
  <div class="home">
    <TableComponent :loading="loading" :tableConfig="tableConfig" :tableData="tableData"/>
  </div>
</template>

<script>
import TableComponent from '../components/TableComponent'
export default {
  name: 'home',
  components: { TableComponent },
  data () {
    return {
      tableConfig: {
        btnList: [
          {
            name: 'addBtn',
            text: '新增',
            icon: 'el-icon-plus',
            style: 'primary',
            class: 'addBtn',
            func: 'toAdd'
          }, {
            name: 'multiDelBtn',
            text: '批量删除',
            icon: 'el-icon-delete',
            style: 'danger',
            class: 'multiDel',
            func: 'toMultiDel'
          }
        ],
        showFilter: true,
        filterOptions: [],
        showDatePick: true
      },
      tableData: {
        thead: [],
        tbody: [],
        isMulti: false,
        pageInfo: { page: 1, size: 10, total: 0 }
      },
      loading: false
    }
  },
  created () {
    this.toSetTdata()
  },
  methods: {
    toSetTdata () {
      let that = this
      that.loading = true
      that.tableData.thead = [
        {
          prop: 'name',
          label: '用户姓名',
          minWidth: '100'
        }, {
          prop: 'age',
          label: '年龄',
          sortable: 'custom',
          minWidth: '92'
        }, {
          prop: 'address',
          label: '家庭住址',
          minWidth: '130'
        }, {
          prop: 'status',
          label: '账号状态',
          minWidth: '100'
        }, {
          prop: 'email',
          label: '邮箱地址',
          minWidth: '134'
        }, {
          prop: 'createdTime',
          label: '添加时间',
          minWidth: '128'
        }
      ]
      setTimeout(() => {
        that.tableData.tbody = [
          { "id": "810000199002137628", "name": "邓磊", "age": 23, "address": "勐海县", "status": "offline", "email": "[email protected]", "createdTime": 1560218008 },
          { "id": "650000197210064188", "name": "蔡刚", "age": 26, "address": "四方台区", "status": "online", "email": "[email protected]", "createdTime": 1500078008 },
          { "id": "450000199109254165", "name": "蔡明", "age": 22, "address": "其它区", "status": "online", "email": "[email protected]", "createdTime": 1260078008 },
          { "id": "440000198912192761", "name": "曹明", "age": 25, "address": "其它区", "status": "online", "email": "[email protected]", "createdTime": 1260078008 },
          { "id": "310000198807038763", "name": "侯静", "age": 21, "address": "莱城区", "status": "offline", "email": "[email protected]", "createdTime": 1560078008 },
          { "id": "310000198406163029", "name": "谭涛", "age": 29, "address": "闸北区", "status": "offline", "email": "[email protected]", "createdTime": 1500078008 },
          { "id": "220000199605161598", "name": "罗秀兰", "age": 27, "address": "其它区", "status": "online", "email": "[email protected]", "createdTime": 1560078008 },
          { "id": "37000019810301051X", "name": "黎敏", "age": 27, "address": "其它区", "status": "online", "email": "[email protected]", "createdTime": 1560078008 },
          { "id": "440000201411267619", "name": "黄强", "age": 24, "address": "合浦县", "status": "offline", "email": "[email protected]", "createdTime": 1560218008 },
          { "id": "440000200504038626", "name": "叶艳", "age": 25, "address": "大渡口区", "status": "offline", "email": "[email protected]", "createdTime": 1560078008 }
        ]
        that.tableData.isMulti = true
        that.tableData.pageInfo = { page: 1, size: 10, total: 135 }
        that.loading = false
      }, 500) // 模拟请求延时
    }
  }
}
</script>
复制代码

demo finished discovered that many features are what have ah, function is very simple, find the problem:

  • Time that column, the back-end is not necessarily passed over data can be directly displayed, if a timestamp pass over it? This time you need to do at the front end of the formatting process
  • States that column, mere words are not conspicuous, often you need a status label or other styles to show
  • I also met many times in the interactive design requirements Click the name of the list (not necessarily the name, click on an item that is able to enter the details page) to enter the user's details page
  • If there is a list of action items within it? How items in the configuration to the internal components?

There are problems, one by one 0.0

The first one: the time that a required format, this column need to be formatted in the same way in the table, then we can put the configuration information in the header, and then table element analysis process, defined formatFn :

{
  prop: 'createdTime',
  label: '添加时间',
  minWidth: '128',
  formatFn: 'timeFormat'
}
复制代码

Analyzing the components added formatFn

<template slot-scope="scope">
  <span
    class="default-cell"
    v-if="!item.formatFn || item.formatFn === ''"
    :title="scope.row[item.prop]"
  >{{scope.row[item.prop]}}</span>
  <span
    v-else
    :class="item.formatFn"
    :title="formatFunc(item.formatFn, scope.row[item.prop], scope.row)"
  >{{formatFunc(item.formatFn, scope.row[item.prop], scope.row)}}</span>
</template>
复制代码

Add utils class, write data formatting method, and register globals

// 格式化方法文件(format.js)(记得要在main.js注册啊)
export default {
  install (Vue, options) {
    Vue.prototype.formatFunc = (fnName = 'default', data = '', row = {}) => {
      const fnMap = {
        default: data => data,
        /**
         * 时间戳转换时间
         * 接受两个参数需要格式化的数据
         * 为防止某些格式化的规则需要表格当前行其他数据信息扩展传参row(可不传)
         */
        timeFormat: (data, row) => {
          return unixToTime(data) // unixToTime是我书写的一个时间戳转时间的方法,如果你项目有引用其他类似方法插件,在这里返回格式化后的数据就可以
        }
      }
      return fnMap[fnName](data, row)
    }
  }
}
复制代码

So if there are other formatting rules can also be custom formatting method, and the method defined in the header again need to call on it, but this formatting method can be used not only in the table, whatever else you want to formatting places can be defined in this file, and then directly use it, but do not have to use another method to introduce, it is not very convenient (づ ¯3¯) づ

See here should understand the core components of the table but it is still using this method of formatting

The second problem continues, the state of the row, not just the change in the data required, it is necessary to convert the state data into the appropriate tag corresponding bit table extension assembly which requires adding a new determination logic

{
    prop: 'status',
    label: '账号状态',
    minWidth: '100',
    formatFn: 'formatAccountStatus',
    formatType: 'dom'
}
复制代码
<template slot-scope="scope">
  <!--不需要处理-->
  <span
    class="default-cell"
    v-if="!item.formatFn || item.formatFn === ''"
    :title="scope.row[item.prop]"
  >{{scope.row[item.prop]}}</span>
  <!--需要处理为标签-->
  <span
    v-else-if="item.formatType === 'dom'"
    :class="item.formatFn"
    v-html="formatFunc(item.formatFn, scope.row[item.prop], scope.row)"
  ></span>
  <!--单纯的数据处理-->
  <span
    v-else
    :class="item.formatFn"
    :title="formatFunc(item.formatFn, scope.row[item.prop], scope.row)"
  >{{formatFunc(item.formatFn, scope.row[item.prop], scope.row)}}</span>
</template>
复制代码

I think the intention is to element-ui format of labels, such as

But we found that the direct return to el-tag labels, and then through the v-html parsed as html tags but later found not in accordance with label element as resolved to be expected, what better way to think about it when writing format found no method, no only way to return to their native label, the definition of class and then modify their own writing style for the label look

The definition of the state table, add the format method

const accountStatusMaps = {
  status: {
    online: '在线',
    offline: '离线'
  },
  type: {
    online: 'success',
    offline: 'warning'
  }
}
// 用户账号状态转标签
formatAccountStatus: (data, row) => {
  return `<span class="status-tag ${accountStatusMaps.type[data]}">${accountStatusMaps.status[data]}</span>`
}
复制代码

Such is the general style format to meet, but some of the more complex needs of their own handwriting style is more inconvenient, but I did not expect his momentary good solution, coupled with this is more downright demand is relatively small, so has been stood up (and perhaps this is my limited technical reasons for it to grow 0.0), Tell me if you have any good suggestions method, then welcome to put ah

The third continues, it is easy to meet this demand management platform in the background, click on the name to enter the details, had I still want to use format, format a a label, then href want to jump to the address, but the function is realized though , but the use of a label there is a big problem is the page jump too strong, only to abandon this approach, then no good way to think of a fuss at the head table, define a new formatType => 'link', optional parameters defined linkUrl hoplinks modified template table assembly

<span
    v-if="item.formatType === 'link'"
    :class="item.formatClass || 'to-detail-link'"
    :title="scope.row[item.prop]"
    @click="toLink(item.linkUrl, scope.row)"
>{{scope.row[item.prop]}}</span>
复制代码
toLink (url, row) {
  if (url) {
    this.$router.push(`${url}/${row.id}`)
  } else {
    this.$router.push(this.$route.path + '/detail/' + row.id)
  }
}
复制代码
.to-detail-link {
  color: #1c92ff;
  cursor: pointer;
  &:hover {
    color: #66b1ff;
  }
}
复制代码

To meet the demand, but only bind a compromise measure, how to return to the label in the format string above method does, if the idea should be grateful, to solve this problem to make a big step to improve the functions of this component, because the details function fairly common, can be compatible component, but if you click on other methods? You can not have a little bit compatible, so meaningless package assembly, as is compatible with compatible endless.

Continue continue operating items in the background management platform is essential, the main problem lies in how transfer, throw click method, my side is so achievable

To modify the template

<span class="table-operation" v-if="item.prop === 'operation' && scope.row.hasOwnProperty('operation')">
    <span
      class="text-btn"
      v-for="(item, index) in scope.row.operation"
      :class="item.type"
      :key="index"
      @click="toEmitFunc(item.event, scope.row)"
    >{{item.text}}</span>
</span>
复制代码

Data setting operation item

computed: {
    operateConfig () {
      return {
        optType: {
          toEdit: {
            event: 'toEdit', // 操作按钮调用的方法
            text: '编辑', // 操作按钮展示的文案
            type: 'primary' // 操作按钮展示的样式
          },
          toDel: {
            event: 'toDel',
            text: '删除',
            type: 'danger'
          }
        },
        optFunc: function (row) {
        // 在线状态用户不能删除
          if (row.status === 'offline') {
            return ['toEdit', 'toDel']
          } else {
            return ['toEdit']
          }
        }
      }
    }
}
复制代码

Some common properties, methods, pulled out on tableMixins inside, reducing write each call

// tableMixins.js
// 表格方法的mixins
export default {
  data() {
    return {
      // 表格数据,具体参考接口数据
      tableData: {
        thead: [],
        tbody: [],
        isMulti: false,
        pageInfo: { page: 1, size: 10, total: 0 }
      },
      // 表格是否处于loading状态
      loading: true,
      // 多选,已选中数据
      selection: [],
      // 查询条件,包括排序、搜索以及筛选
      searchCondition: {}
    }
  },
  mounted: function () { },
  methods: {
    // 多选事件, 返回选中的行及每行的当前状态
    selectionChange(value) {
      this.selection = value
    },
    接口请求到数据后将数据传入这个方法进行thead、tbody、pageInfo等信息的赋值
    afterListSet(res) {
      let formData = this.setOperation(res)
      if (formData.thead) {
        this.tableData.thead = JSON.parse(JSON.stringify(formData.thead))
      }
      this.tableData.tbody = formData.tbody
      if (formData.pageInfo) {
        this.tableData.pageInfo = JSON.parse(JSON.stringify(formData.pageInfo))
      }
      formData.isMulti && (this.tableData.isMulti = formData.isMulti)
      let query = JSON.parse(JSON.stringify(this.$route.query))
      this.$router.replace({
        query: Object.assign(query, { page: this.tableData.pageInfo.page })
      })
      this.loading = false
    },
    // 遍历原始数据,塞入前端定义好的操作项方法序列,设置操作项
    setOperation(res) {
      let that = this
      let tdata = JSON.parse(JSON.stringify(res))
      if (that.operateConfig && that.operateConfig.optFunc) {
        for (let i in tdata.tbody) {
          let temp = that.operateConfig.optFunc(tdata.tbody[i])
          let operation = []
          for (let j in temp) {
            operation.push(that.operateConfig.optType[temp[j]])
          }
          that.$set(tdata.tbody[i], 'operation', operation)
        }
      }
      return tdata
    }
  }
}
复制代码

Such a combination of boxing down, common management platform to achieve a basic spreadsheet functionality, stickers Results Figure (style did not how to write, this would adjust it according to your project style)

I put this source components uploaded to my GitHub, and while there wrote demo, interested friends can clone to local pondering, while welcome insufficient or bug in the code, be grateful 0.0, paste the source address

Think

Components finished, the article also finished water, reflect some shortcomings, is how a label UI rendering component, and the other is how the format out of the label on the binding method, component development process is true that there are a lot of compromise, which is itself a compromise failed to reach the level of vision or consequences caused to be thrown component is to brainstorm this article helpful to you, it can help you improve then I will be very happy if you help solve the difficulties I encountered to help me improve then I will be more happy, this is not my thing, so welcome to help us to find solutions for the

Of course, component development process, I still do some very user-friendly design, is to provide a multiple filter criteria simultaneously punching method acting, is to maintain a searchCondition, each filter function is added to the searchCondition, and then to pass searchCondition data query method, which can try the demo, the console will output, another small feature would have to Tucao about a lot of open source components against humanity design, rich text editor, many people have used it, there will be a configuration menu item, if the configuration item you wear nothing he would show all the default menu item, but if you do not want one of the menu item, you put a set in the configuration menu is false .... ... then all of the menu was no more, I've seen some of the source code is delivered directly into the menu of configuration override the default menu item, so I just want to cancel one of them, I have to put all of the configuration menu items all over again and all set to true, this does not pit father do ... Well, so when I pass parameters to the internal components adds a method, but With deconstruction assignment, maintaining a computed, code is as follows:

computed: {
    propConfig () {
      // defaultConfig是默认配置项,tableConfig是父组件传递进来的配置项
      return { ...this.defaultConfig, ...this.tableConfig }
    }
}
复制代码

Such items will only change the default configuration, the other unchanged when an item has a parent component changes

On the right, we hope everyone will help

Reproduced in: https: //juejin.im/post/5ce1507551882525d05f7bed

Guess you like

Origin blog.csdn.net/weixin_33967071/article/details/91422002