Vue2在实际项目中的应用——表格组件功能介绍
TableList组件是以ElementUI Table表格组件为主,并封装了一系列其它组件,提供了以下主要功能
- 筛选功能
- 搜索功能
- 分页功能
- 加载过程以及错误信息提示功能
- 行展开功能
- 单选行功能
- switch开关组件功能
- progress进度组件功能
- 分行显示日期时间组件功能
- 动态组件渲染功能
- 自定义列组件功能
表格组件可以分为三个部分:头部(筛选,搜索)、数据部分、底部(汇总,分页)
* 头部
左侧为筛选部分,右侧为搜索部分,可以通过tableConfig的filter和search进行配置是否显示。
可以通过header slot整个自定义头部,通过action slot自定义筛选部分。筛选部分可以通过设置tableData.filters进行自动渲染,数据格式示例如下:
如果想渲染多选,可以设置multiple为true,并且默认的value提供数组形式。
当有筛选动作,或者输入搜索条件按回车后,表格组件将会汇总所有条件,包括所有筛选项,搜索,分页信息,排序等,然后发出reload事件,事件参数为对象,格式如下:
{
origin: {
page:,
per_page:,
search:,
sorts:,
filterkey1:,
filterKey2:,
...
}
query: 'page=&per_page=&search=&....'
}
//sorts格式为列的prop+'|'+asc或者desc,例如'id|asc'
//query就是通过jquery的param方法把对象转成查询参数
头部自定义使用方法如下(header&action slot):
* 数据部分
-
加载过程以及错误信息提示功能
在初始化tableData的items属性时,需要设置成undefined,这样表格组件将会利用el-table的empty slot显示正在加载提示
如果数据加载出错,把items设置成null,将会以红色显示数据加载失败
当没有数据时,设置成空数据,将会提示暂无数据
-
行展开功能
在列定义的时候通过设置type为expand,并设置component属性,示例代码如下:
let tableColumns = [{ 'type': 'expand', 'component': 'PubGroupMgmt-ColumnExpandDetail' }] Vue.component('PubGroupMgmt-ColumnExpandDetail', ColumnExpandDetail);
这样将会提供el-table的行展开功能
-
单选行功能
在列定义的时候通过设置type为radio,示例代码如下:
let tableColumns = [{ 'type': 'radio', 'prop': 'id' }] //这里属性必须是id
并且在dpp-table-list上监听select事件
<dpp-table-list :table-config="tableConfig" :table-columns="tableColumns" :table-data="tableData" @select="select" @reload="reload"> </dpp-table-list> <script> select (selection) { this.selectionId = selection; } </script>
-
switch开关组件功能
在列定义的时候通过设置type为switch,示例代码如下:
let tableColumns = [{ 'type': 'switch', 'prop': 'enabled', 'label': '工作流开关' }] //还可以通过设置disabled属性,表示是否允许用户操作此开关
并且在dpp-table-list上监听switch事件
<script> updateSwitch ({row, prop, mark}) {//mark为数值0或1 WorkflowSrv.patch(row.id, {[prop]: mark}).then(() => { this.loadData(); }); } </script>
-
progress进度组件功能
在列定义的时候通过设置type为progress,示例代码如下:
let tableColumns = [{ 'type': 'progress', 'prop': 'finish_count|unit_count', 'label': '完成数' }] //prop值为'分子|分母'
-
分行显示日期时间组件功能
在列定义的时候通过设置type为datetime,示例代码如下:
let tableColumns = [{ 'prop': 'created_at', 'label': '申请时间', 'type': 'datetime' }]
这一列属性的值应该是timestamp,单位为秒(s),UI上将会强行分两行显示日期和时间
-
动态组件渲染功能
在列定义的时候通过设置type为dynamic,示例代码如下:
let tableColumns = [{ 'type': 'dynamic', 'condition': 'state', 'components': {0: 'UnPublishedProjectList-ColumnAction', 'other': 'PublishedProjectList-ColumnAction'}, 'prop': 'operaion', 'label': '操作' }] Vue.component('PublishedProjectList-ColumnAction', PublishedColumnAction); Vue.component('UnPublishedProjectList-ColumnAction', UnPublishedColumnAction);
condition属性为必设,表示要根据哪个属性的值对组件进行选择。
另外需要设置components属性,格式为对象,对象中key为condition属性值的分布,值为自定义组件。可以设置一个other属性,表示所有其他情况下使用的组件。
-
自定义列组件功能
在列定义的时候通过设置component,示例代码如下:
let tableColumns = [{ 'prop': 'operation_content', 'label': '操作内容', 'component': 'Audit-ColumnContent' }] let ColumnContent = { template: '<div v-html="content"></div>', props: { row: Object }, computed: { content () { return this.row.operation_content.reduce((prev, cur) => prev + cur + ' ', ''); } } }; Vue.component('Audit-ColumnContent', ColumnContent);
如果以上提供的组件都不满足需求,可以自己定义列组件,自定义组件的渲染优先级小于上面特定组件。
自定义组件提供如下属性:prop,row,column,rowIdx,colIdx。通过props接收
自定义组件可以发送如下事件:edit,del,reload,forward,发送的如下事件,除了forward都可以直接在dpp-table-list上监听,forward事件用法文章最后介绍
- el-table的列其它功能同样支持,比如多选,index等,请参照el-table列定义格式
* 底部
左侧为summary slot,可以自定义,右侧为分页组件,可以通过tableConfig.pagination设置显示或者隐藏
当分页组件触发事件时,会发送reload事件,在dpp-table-list上监听,发送的reload事件参数如下:
{
origin: {
page:,
per_page:,
search:,
sorts:,
filterkey1:,
filterKey2:,
...
}
query: 'page=&per_page=&search=&....'
}
//sorts格式为列的prop+'|'+asc或者desc,例如'id|asc'
//query就是通过jquery的param方法把对象转成查询参数
尾部自定义使用方法如下(summary slot):
* forward事件以及其他默认事件使用方法
为了表格组件可以向外暴露事件,表格组件预先定义了一些事件,如edit,del,reload。
在自定义组件中通过this.$emit('edit') 形式可以直接把事件暴露出去。这样就可以在dpp-table-list上监听到这些事件。
edit和del事件的参数就是row,reload事件无参数
如果自定义组件中还想对外发送其他事件,并且想在dpp-table-list上监听,那么可以通过forward事件,表格组件将转发此事件,用法如下:
this.$emit('forward', {event: 'confirm', row: row})
表格组件将转发confirm事件,并把{event: 'confirm', row: row}作为confirm事件的参数值传递
在dpp-table-list上可以监听confirm事件@confirm="joinConfirmation"
<script>
joinConfirmation ({row}) {
}
</script>
* 整体示例代码
组件代码:
<template>
<div class="box table-list">
<div class="box-header">
<!--<h3 class="box-title">-->
<slot name="header-title"></slot>
<slot name="header">
<div class="action-section">
<slot name="action"></slot>
</div>
<div class="filter-section" v-if="tableConfig.filter">
<el-form :inline="true" :model="formFilters" class="table-list-filter-form">
<template v-for="filter in tableData.filters">
<el-form-item :label="filter.label" :key="filter.key" class="filter">
<el-select v-model="formFilters[filter.key]" @change="searchByFilter" placeholder="请选择" :class="'selector-'+filter.key" :popper-class="'selector-popper-'+filter.key" :multiple="!!filter.multiple" :collapse-tags="!!filter.multiple">
<el-option v-for="option in filter.options" :label="option.text" :value="option.value" :key="option.value"></el-option>
</el-select>
</el-form-item>
</template>
<slot name="other-filters"></slot>
</el-form>
</div>
<div class="search-section" v-if="tableConfig.search">
<div class="el-input">
<input autocomplete="off" v-model="searchValue" :placeholder="tableConfig.searchPlaceholder ||
'请输入搜索条件'" type="text" rows="2" validateevent="true" class="el-input__inner" @keyup="searchByInput">
</div>
</div>
</slot>
<!--</h3>-->
</div>
<div class="box-body">
<el-row>
<el-col :span="24">
<el-table
ref="tableList"
:data="tableData.items || tableData.data"
stripe
row-key="id"
@sort-change="handleSortChange"
@selection-change="handleSelectionChange">
<div slot="empty">
<template v-if="typeof tableData.items === 'undefined'">
<i class="fa fa-refresh fa-spin fa-1x fa-fw" aria-hidden="true" style="color:#409EFF"></i>正在加载...
</template>
<template v-else-if="tableData.items === null">
<span style="color:#FA5555">加载数据失败</span>
</template>
<template v-else>
暂无数据
</template>
</div>
<template v-for="(item, $index) in tableColumns">
<el-table-column
v-if="item.type==='expand'"
:key="$index"
:type="item.type"
:label="item.label"
:prop="item.prop"
:align="item.align || 'center'"
:width="item.width">
<template slot-scope="scope">
<component
:is="item.component"
:row="scope.row"
:column="scope.column"
:rowIdx="scope.$index"
:colIdx="$index">
</component>
</template>
</el-table-column>
<el-table-column
v-else-if="item.type==='radio'"
reserve-selection
:key="$index"
:label="item.label"
:prop="item.prop"
:align="item.align || 'center'"
:width="item.width">
<template slot-scope="scope">
<component
:is="'TableList-RadioInTableComponent'"
:table-name="uniqueName"
:row="scope.row"
:column="scope.column"
:rowIdx="scope.$index"
:colIdx="$index"
@forward="handleForwardEvent">
</component>
</template>
</el-table-column>
<el-table-column
v-else-if="item.type==='switch'"
:key="$index"
:label="item.label"
:prop="item.prop"
:align="item.align || 'center'"
:width="item.width">
<template slot-scope="scope">
<component
:is="'TableList-SwitchInTableComponent'"
:row="scope.row"
:column="scope.column"
:rowIdx="scope.$index"
:colIdx="$index"
:disabled="item.disabled"
@forward="handleForwardEvent">
</component>
</template>
</el-table-column>
<el-table-column
v-else-if="item.type==='progress'"
:key="$index"
:label="item.label"
:prop="item.prop"
:align="item.align || 'center'"
:width="item.width">
<template slot-scope="scope">
<component
:is="'TableList-ProgressInTableComponent'"
:prop="item.prop"
:row="scope.row"
:column="scope.column"
:rowIdx="scope.$index"
:colIdx="$index"
@forward="handleForwardEvent">
</component>
</template>
</el-table-column>
<el-table-column
v-else-if="item.type==='datetime'"
:key="$index"
:label="item.label"
:prop="item.prop"
:align="item.align || 'center'"
:width="item.width"
:min-width="100">
<template slot-scope="scope">
<component
:is="'TableList-DateTimeInTableComponent'"
:prop="item.prop"
:row="scope.row"
:column="scope.column"
:rowIdx="scope.$index"
:colIdx="$index"
@forward="handleForwardEvent">
</component>
</template>
</el-table-column>
<el-table-column
v-else-if="item.type==='dynamic'"
:key="$index"
:label="item.label"
:prop="item.prop"
:align="item.align || 'center'"
:width="item.width">
<template slot-scope="scope">
<component
:is="item.components[item.condition] || item.components['other']"
:prop="item.prop"
:row="scope.row"
:column="scope.column"
:rowIdx="scope.$index"
:colIdx="$index"
@edit="handleEditEvent"
@del="handleDelEvent"
@reload="handleReloadEvent"
@forward="handleForwardEvent">
</component>
</template>
</el-table-column>
<el-table-column
v-else-if="!item.component"
reserve-selection
:key="$index"
:type="item.type"
:label="item.label"
:render-header="renderHeader"
:prop="item.prop"
:index="indexMethod"
:sortable="item.sortable && 'custom'"
:class-name="'sort-field-' + (item.sortField || '')"
:formatter="item.formatter"
:align="item.align || 'center'"
:width="item.width">
</el-table-column>
<el-table-column
v-else
:key="$index"
:align="item.align || 'center'"
:label="item.label"
:render-header="renderHeader"
:prop="item.prop"
:sortable="item.sortable && 'custom'"
:class-name="'sort-field-' + (item.sortField || '')"
:width="item.width">
<template slot-scope="scope">
<component
:is="item.component"
:prop="item.prop"
:row="scope.row"
:column="scope.column"
:rowIdx="scope.$index"
:colIdx="$index"
@edit="handleEditEvent"
@del="handleDelEvent"
@reload="handleReloadEvent"
@forward="handleForwardEvent">
</component>
</template>
</el-table-column>
</template>
</el-table>
</el-col>
</el-row>
<slot name="summary"></slot>
<el-pagination
v-if="tableConfig.pagination"
@current-change="handleCurrentChange"
:current-page.sync="currentPage"
:page-size="pageSize"
layout="total, prev, pager, next"
:total="tableData.pagination.pageTotal || tableData.pagination.total"
class="pull-right">
</el-pagination>
</div>
<slot name="footer">
</slot>
</div>
</template>
<script>
import $ from 'jquery';
import _ from 'lodash';
import moment from 'moment';
import Vue from 'vue';
/**
* TableList组件,表格,带有过滤,搜索,分页等功能
* @module TableList
* @fires reload
* @fires select
* @fires edit
* @fires del
* @listens forward 转发列组件发送的事件
* @example
* 具体使用参考{@tutorial 表格组件介绍}
*/
export default {
name: 'TableList',
/**
* Props 接受父组件的传值
* @property {array} tableColumns 必填,列定义,参考elementui table组件列定义
* @property {object} tableConfig 可选,默认全都显示
* @property {boolean} tableConfig.filter 是否显示筛选头
* @property {boolean} tableConfig.search 是否显示搜索框
* @property {string} tableConfig.searchPlaceholder 搜索框placeholder
* @property {boolean} tableConfig.pagination 是否显示分页
* @property {number} tableConfig.pageSize 可选,默认为10,表格一页的数据
* @property {object} tableData 必填,表格数据和分页信息
* @property {array} tableData.items 表格数据,每行必须包含值唯一的id属性,初始可设置成undefined,数据出现错误时设置成null
* @property {array} tableData.filters 筛选器,自动渲染头部筛选部分{items:[{key:,label:,value:,options:[{text:,value:,}]}]}
* @property {object} tableData.pagination 分页信息{total:,}
*/
props: {
tableConfig: {
type: Object,
default: () => ({'filter': true, 'search': true, searchPlaceholder: '请输入搜索条件', 'pagination': true})
},
tableColumns: Array,
tableData: Object,
uniqueName: {
type: String,
default: _.uniqueId('TableList-')
}
},
watch: {
'tableData.filters': function (val, oldVal) { // default select filter after first loaded
if (!oldVal.length) {
for (let filter of this.tableData.filters) {
Vue.set(this.formFilters, filter.key, filter.value);
}
}
}
},
data: function () {
return {
'currentPage': 1,
'pageSize': this.tableConfig.pageSize || 15,
'formFilters': {},
'sortOrder': '',
'sortProp': '',
'searchValue': ''
};
},
methods: {
handleCurrentChange () {
this.emitReload('page');
},
handleSortChange ({column, prop = '', order = ''}) {
if (column) {
let sortFieldIdx = column.className.indexOf('sort-field-'),
sortField = sortFieldIdx !== -1 ? column.className.split(' ').filter(() => sortFieldIdx !== -1).map(f => f.substring(11))[0] : '';
this.sortOrder = order;
this.sortProp = sortField || prop;
} else {
this.sortOrder = '';
this.sortProp = '';
}
this.emitReload('sort');
},
searchByFilter () {
this.currentPage = 1;
this.emitReload('filter');
},
searchByInput ($event) {
if ($event.code === 'Enter' || $event.code === 'NumpadEnter' ||
(($event.code === 'Backspace' || $event.code === 'Delete') && this.searchValue === '')) {
this.currentPage = 1;
this.emitReload('search');
}
},
emitReload (trigger) {
let payload = this.getQueryClause();
payload.trigger = trigger;
this.$emit('reload', payload);
},
getQueryClause () {
let origin = {
'page': this.currentPage,
'per_page': this.pageSize
};
this.sortProp &&
(origin.sorts = this.sortProp + '|' + (this.sortOrder === 'ascending' ? 'asc' : 'desc'));
this.searchValue && (origin.search = this.searchValue);
for (let [key, value] of Object.entries(this.formFilters)) {
(value !== '') && (origin[key] = Array.isArray(value) ? value.join(',') : value);
}
let payload = {
'origin': origin,
'query': $.param(origin)
};
return payload;
},
indexMethod (index) {
return (this.currentPage - 1) * this.pageSize + index + 1;
},
renderHeader (h, { column, $index }) {
return /<[^>]*>/.test(column.label) ? h('div', {//html test
attrs: {
style: 'line-height: initial;'
},
domProps: {
innerHTML: column.label
}
}) : column.label;
},
handleSelectionChange (val) {
this.$emit('select', val);
},
handleEditEvent (row) {
this.$emit('edit', row);
},
handleDelEvent (row) {
this.$emit('del', row);
},
handleReloadEvent (row) {
this.emitReload('child');
},
handleForwardEvent (payload) {
this.$emit(payload.event, payload);
},
/**
* 选中表中的某几行
* @method selectRows
* @param {array} rows tableData中items里面的数据项
*/
selectRows (rows) {
rows.forEach(row => {
this.$refs.tableList.toggleRowSelection(row, true);
});
},
/**
* 取消选中表中的某几行
* @method unselectRows
* @param {array} rows tableData中items里面的数据项
*/
unselectRows (rows) {
rows.forEach(row => {
this.$refs.tableList.toggleRowSelection(row, false);
});
},
/**
* 取消选中的所有行
* @method clearRows
*/
clearRows () {
this.$refs.tableList.clearSelection();
},
/**
* 对表格进行重新布局
* @method doLayout
*/
doLayout () {
this.$refs.tableList.doLayout();
},
/**
* 重置表格,包括筛选条件,搜索框,单选,多选等
* @method reset
*/
reset () {
this.searchValue = '';
this.currentPage = 1;
if (this.tableData.filters) {
for (let filter of this.tableData.filters) {
Vue.set(this.formFilters, filter.key, filter.value);
}
}
this.$refs.tableList.clearSelection();
$('.' + this.uniqueName + ' .el-radio__input').removeClass('is-checked');
$('.' + this.uniqueName).closest('.el-table').data('cachedRadioIdx', '');
}
},
created () {
},
beforeDestroy () {
// delete single selection cached id manually
$('.' + this.uniqueName).closest('.el-table').data('cachedRadioIdx', '');
}
};
Vue.component('TableList-RadioInTableComponent', {
template: '<label :class="\'el-radio radio \'+tableName"><span class="el-radio__input"><span class="el-radio__inner"></span><input @click="selectRadio" type="radio" :name="tableName" class="el-radio__original" :value="row.id"></span><span class="el-radio__label"> </span></label>',
props: {
row: Object,
tableName: String
},
computed: {
radio () {
return this.row.id;
}
},
methods: {
selectRadio () {
$('.' + this.tableName + ' .el-radio__input').closest('.el-table').data('cachedRadioIdx', this.radio);
$('.' + this.tableName + ' .el-radio__input').removeClass('is-checked');
$('input[value=' + this.radio + ']').parent().addClass('is-checked');
this.$emit('forward', {event: 'select', id: this.radio});
}
},
mounted () {
let cachedRadioIdx = $('.' + this.tableName + ' .el-radio__input').closest('.el-table').data('cachedRadioIdx');
if (cachedRadioIdx === this.radio) {
$('input[value=' + this.radio + ']').parent().addClass('is-checked');
}
}
});
Vue.component('TableList-SwitchInTableComponent', {
template: `<el-switch
v-model="mark"
active-value="1"
inactive-value="0"
active-text="ON"
inactive-text="OFF"
:disabled="disabled"
@change="change">
</el-switch>`,
props: {
row: Object,
column: Object,
disabled: {
type: Boolean,
default: false
}
},
computed: {
mark: {
get: function () {
return '' + Number(this.row[this.column.property]);
},
set: function (newVal) {
this.row[this.column.property] = +newVal;
}
}
},
methods: {
change () {
this.$emit('forward', {event: 'switch', row: this.row, prop: this.column.property, mark: +this.mark});
}
}
});
Vue.component('TableList-ProgressInTableComponent', {
template: `<div style="">
<el-progress :show-text="false" :stroke-width="8" :percentage="percentage"></el-progress>
<span class="el-progress-text">{{number}}</span>
</div>`,
props: {
row: Object,
prop: String
},
computed: {
number () {
return _.get(this.row, this.prop.split('|')[0]);
},
percentage () {
let number = _.get(this.row, this.prop.split('|')[0]),
amount = _.get(this.row, this.prop.split('|')[1]);
return amount ? number / amount * 100 : 0;
}
}
});
Vue.component('TableList-DateTimeInTableComponent', {
template: `<div><span>{{date[0]}}</span><br/><span>{{date[1]}}</span></div>`,
props: {
row: Object,
prop: String
},
computed: {
date () {
return moment(this.row[this.prop] * 1000).format('YYYY-MM-DD HH:mm:ss').split(' ');
}
}
});
</script>