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

* 數據部分

  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值爲'分子|分母'
    					

     

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

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章