Vue2在实际项目中的应用——表格组件功能介绍

TableList组件是以ElementUI Table表格组件为主,并封装了一系列其它组件,提供了以下主要功能

  • 筛选功能
  • 搜索功能
  • 分页功能
  • 加载过程以及错误信息提示功能
  • 行展开功能
  • 单选行功能
  • switch开关组件功能
  • progress进度组件功能
  • 分行显示日期时间组件功能
  • 动态组件渲染功能
  • 自定义列组件功能

Vue2在实际项目中的应用——表格组件功能介绍

表格组件可以分为三个部分:头部(筛选,搜索)、数据部分、底部(汇总,分页)

* 头部

左侧为筛选部分,右侧为搜索部分,可以通过tableConfig的filter和search进行配置是否显示。

可以通过header slot整个自定义头部,通过action slot自定义筛选部分。筛选部分可以通过设置tableData.filters进行自动渲染,数据格式示例如下:

Vue2在实际项目中的应用——表格组件功能介绍

如果想渲染多选,可以设置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):

Vue2在实际项目中的应用——表格组件功能介绍

Vue2在实际项目中的应用——表格组件功能介绍

* 数据部分

  1. 加载过程以及错误信息提示功能

    在初始化tableData的items属性时,需要设置成undefined,这样表格组件将会利用el-table的empty slot显示正在加载提示

    如果数据加载出错,把items设置成null,将会以红色显示数据加载失败

    当没有数据时,设置成空数据,将会提示暂无数据

  2. 行展开功能

    在列定义的时候通过设置type为expand,并设置component属性,示例代码如下:

    let tableColumns = [{
    	'type': 'expand',
    	'component': 'PubGroupMgmt-ColumnExpandDetail'
    }]
    
    Vue.component('PubGroupMgmt-ColumnExpandDetail', ColumnExpandDetail); 
    					

     

    这样将会提供el-table的行展开功能

  3. 单选行功能

    在列定义的时候通过设置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>
    				
  4. 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>					
    				
  5. progress进度组件功能

    在列定义的时候通过设置type为progress,示例代码如下:

    let tableColumns = [{
    	'type': 'progress',
    	'prop': 'finish_count|unit_count',
    	'label': '完成数'
    }]
    //prop值为'分子|分母'
    					

     

    Vue2在实际项目中的应用——表格组件功能介绍
  6. 分行显示日期时间组件功能

    在列定义的时候通过设置type为datetime,示例代码如下:

    let tableColumns = [{
    	'prop': 'created_at',
    	'label': '申请时间',
    	'type': 'datetime'
    }]
    					

     

    这一列属性的值应该是timestamp,单位为秒(s),UI上将会强行分两行显示日期和时间

  7. 动态组件渲染功能

    在列定义的时候通过设置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属性,表示所有其他情况下使用的组件。

  8. 自定义列组件功能

    在列定义的时候通过设置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事件用法文章最后介绍

  9. 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):

Vue2在实际项目中的应用——表格组件功能介绍

* 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>	
		

 

* 整体示例代码

Vue2在实际项目中的应用——表格组件功能介绍

Vue2在实际项目中的应用——表格组件功能介绍

Vue2在实际项目中的应用——表格组件功能介绍

Vue2在实际项目中的应用——表格组件功能介绍

 

组件代码:

<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">&nbsp;</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>