來如今這公司一年了,一年時間裏經手作的項目有六七個,不過呢大部分都是一些管理平臺的功能,而管理平臺作的最多的就是各類表格的展現了,因此在開發過程當中,爲了提升開發效率,封裝一些通用的功能組件是十分有必要的,在這裏我就把我在開發過程當中所封裝的表格組件分享一下,固然確定是有不少不足的,由於到目前爲止我仍是有一些想法沒有實現的,也但願能夠互相交流一下,就當拋磚引玉了,砸到誰也別怨我啊0.0javascript
我這邊使用的是vue全家桶+ElementUI,畢竟管理平臺,沒那麼高的要求,版本的話隨意,畢竟只是說明一種設計方法,通用的css
管理平臺的表格頁面通常包含這幾部分功能:1.操做按鈕(添加,批量刪除等);2.表格數據篩選項(經常使用的有select過濾,時間過濾,搜索過濾等);3.表格主體;4.分頁。html
操做按鈕的添加我這邊想到的有兩種方法,第一種:直接使用vue提供的插槽功能,直接在外部定義按鈕、樣式,以及按鈕的操做事件,經過插槽插入組件內部;第二種:組件外部定義一個按鈕的對象,經過父子組件通訊傳遞到table組件內部,組件內部對按鈕的對象進行解析、渲染,大概格式以下:前端
[
{
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'
}
]
複製代碼
篩選項的設計沒想到有什麼好的,就內設幾個經常使用的就上面說的那些,而後經過參數判斷是否展現,其餘若是有定製需求能夠外部定義,而後經過插槽插入vue
表格主要包括兩部分:表頭和表格體,分開分析java
表格頭我這邊設計是在組件外部定義配置項傳遞組件內部,組件內進行解析,格式以下:git
[
{
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'
}
]
複製代碼
表格體沒什麼,就參看element-ui的就行github
分頁也沒什麼好說的,內置在組件內,少於一頁不顯示,在element組件庫選一個本身須要的功能的分頁element-ui
根據需求對組件進行了封裝後端
<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>
複製代碼
<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>
複製代碼
<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>
複製代碼
<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>
複製代碼
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
})
}
}
複製代碼
看到這,你確定說這不就是element的table的使用嗎?嗯...你說的頗有道理,我竟沒法反駁0.0,下面我就加入一些本身的想法設計吧(づ ̄3 ̄)づ
先寫demo,在寫的過程當中才能一步步完善不足
<!--外部引用文件-->
<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": "y.rbuuy@ndtccqms.lv", "createdTime": 1560218008 }, { "id": "650000197210064188", "name": "蔡剛", "age": 26, "address": "四方臺區", "status": "online", "email": "r.ifypqovuzl@rcvfhg.ir", "createdTime": 1500078008 }, { "id": "450000199109254165", "name": "蔡明", "age": 22, "address": "其它區", "status": "online", "email": "z.rbq@uqadfyx.ee", "createdTime": 1260078008 }, { "id": "440000198912192761", "name": "曹明", "age": 25, "address": "其它區", "status": "online", "email": "h.mkmqo@dcj.ee", "createdTime": 1260078008 }, { "id": "310000198807038763", "name": "侯靜", "age": 21, "address": "萊城區", "status": "offline", "email": "u.xlkda@ckoicbhk.br", "createdTime": 1560078008 }, { "id": "310000198406163029", "name": "譚濤", "age": 29, "address": "閘北區", "status": "offline", "email": "r.wyr@hmqqlafes.no", "createdTime": 1500078008 }, { "id": "220000199605161598", "name": "羅秀蘭", "age": 27, "address": "其它區", "status": "online", "email": "d.eggqvlbol@crqodjvdys.nu", "createdTime": 1560078008 }, { "id": "37000019810301051X", "name": "黎敏", "age": 27, "address": "其它區", "status": "online", "email": "s.unumfugq@qgeufl.om", "createdTime": 1560078008 }, { "id": "440000201411267619", "name": "黃強", "age": 24, "address": "合浦縣", "status": "offline", "email": "q.ufollx@kdvgtb.al", "createdTime": 1560218008 }, { "id": "440000200504038626", "name": "葉豔", "age": 25, "address": "大渡口區", "status": "offline", "email": "h.trtiurcut@vnypp.sm", "createdTime": 1560078008 } ] that.tableData.isMulti = true that.tableData.pageInfo = { page: 1, size: 10, total: 135 } that.loading = false }, 500) // 模擬請求延時 } } } </script>
複製代碼
demo寫完就發現,不少功能都麼得啊,功能非常單一,發現問題:
有問題了,一個一個來0.0
第一個:時間那一項須要格式化,在表格中這一列都須要按照一樣的方法進行格式化處理,那麼咱們能夠把配置信息放在表頭中,而後再表格組件中解析處理,定義formatFn:
{
prop: 'createdTime',
label: '添加時間',
minWidth: '128',
formatFn: 'timeFormat'
}
複製代碼
組件內添加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>
複製代碼
添加utils類,編寫格式化數據方法,並註冊全局
// 格式化方法文件(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)
}
}
}
複製代碼
這樣若是有其餘格式化規則的也能夠經過自定義格式化方法,而後再表頭中定義須要調用的方法就能夠了,同時這個格式化方法不僅是能夠用在表格中,其餘任何你想要進行格式化的地方均可以在這個文件中定義,而後直接使用就能夠了,而不用再引入方法再使用,是否是很方便(づ ̄3 ̄)づ
看到這裏應該就明白這個table組件的核心其實仍是這個格式化方法的使用
繼續第二個問題,狀態那一列,須要的不僅是數據的變化,更是須要將相應的狀態數據轉換成對應的標籤,這就須要擴展一下table組件,添加新的判斷邏輯
{
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>
複製代碼
我本意是想着format成element-ui的標籤,如
可是在寫format方法的時候發現直接返回el-tag標籤,而後通過v-html解析爲html標籤可是後面發現並不能按照預期的那樣解析成element的標籤,思考一番沒有發現什麼好的方法,沒辦法只能本身返回原生標籤,定義class而後本身書寫樣式修改成標籤的樣子定義狀態關係表,添加format方法
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>`
}
複製代碼
這樣通常的樣式format是能夠知足了,可是一些比較複雜的需求本身手寫樣式就比較不方便了,可是我本身一時也沒想到好的解決辦法,再加上這樣比較難搞的需求比較少,因此就一直放着了(可能這就是我技術成長有限的緣由吧0.0),各位看官若是有什麼好的建議方法的話歡迎來提啊
繼續第三項,這個需求在後臺管理平臺很容易遇到,點擊名稱進入詳情,原本我仍是想使用format,format出一個a標籤,而後href是想要跳往的地址,可是功能雖然是實現了,可是使用a標籤有個很大問題就是頁面跳出感太強,只能放棄這個方法,後來沒什麼好的辦法,就想着在表頭上作文章,新定義一種formatType => 'link',可選參數linkUrl定義跳轉連接,修改table組件的template
<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;
}
}
複製代碼
需求知足了,可是隻是個妥協之計,該怎麼在format返回的標籤字符串上面綁定方法呢,若有想法,不勝感激,解決了這個問題能讓這組件功能提升一大步,由於這個詳情功能還算通用,能夠在組件兼容,可是若是是其餘點擊方法呢?總不能一點點的都兼容,這樣就失去了封裝組件的意義,由於兼容是兼容不完的。
繼續繼續,操做項在後臺管理平臺是不可缺乏的,主要的問題就在於怎麼傳遞、拋出點擊方法,我這邊是這麼實現的
先修改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>
複製代碼
設置操做項數據信息
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']
}
}
}
}
}
複製代碼
把一些通用的屬性、方法抽離出來放在tableMixins裏面,減小每次調用的書寫
// 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
}
}
}
複製代碼
這樣一套組合拳下來,管理平臺經常使用的表格功能基本都實現了,貼一下成果圖(樣式沒怎麼寫,這個就根據本身的項目風格進行調整吧)
我把這個組件的源碼上傳到了個人GitHub上了,同時寫的有demo,有興趣的朋友能夠clone到本地琢磨琢磨,同時歡迎提出代碼的不足或者bug,不勝感激0.0,貼上 源碼地址組件寫完了,文章也水完了,反思一些不足,一個是怎麼渲染出UI組件的標籤,另外一個是怎麼在format出來的標籤上面綁定方法,誠然組件開發過程當中有不少妥協,這些妥協就是自身水平或者視野沒能達到致使的惡果,把組件拋出來就是爲了集思廣益,這個文章對你有幫助,能幫助你提升那我會很高興,若是你幫忙解決了我遇到的困難,幫助到個人提升那我會更加高興,我爲的不就是這個嘛,因此歡迎你們幫忙想解決方案
固然組件開發過程當中我仍是作了一些很人性化的設計,一個是提供了多衝篩選條件同時做用的方法,就是維護一個searchCondition,每次篩選起做用就添加到searchCondition中,而後把searchCondition傳遞給數據查詢方法,這個方法能夠在demo中嘗試,控制檯會有輸出,另外一個小功能就不得不吐槽一下不少開源組件的一個反人類設計,富文本編輯器不少人都用過吧,會有一個menu的配置項,若是這個配置項你什麼也不穿他就默認展現所有的菜單項,可是若是你不想要其中一個菜單項,你在menu的配置中把這一項置爲false.......而後全部的菜單全沒了,我看過一些源碼是直接使用傳遞進去的menu覆蓋默認的menu配置項,那麼我只是想取消其中一項,我就得把所有的菜單項所有配置一遍且所有置爲true,這...不坑爹呢嘛,因此我在向組件內部傳參的時候添加了一個方法,而是使用解構賦值,維護一個computed,代碼以下:
computed: {
propConfig () {
// defaultConfig是默認配置項,tableConfig是父組件傳遞進來的配置項
return { ...this.defaultConfig, ...this.tableConfig }
}
}
複製代碼
這樣默認配置項只會在父組件某一項有修改的時候進行變動,其餘不變
就這些吧,但願會對你們有所幫助