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>