說(shuō)明:最近幾個(gè)月負(fù)責(zé)公司部門(mén)vue項(xiàng)目的前端架構(gòu)任務(wù)稽莉,該組件的設(shè)計(jì)實(shí)現(xiàn),主要考慮提高業(yè)務(wù)開(kāi)發(fā)效率诱担,實(shí)現(xiàn)表格的動(dòng)態(tài)化配置定血,對(duì)項(xiàng)目中常見(jiàn)的業(yè)務(wù)頁(yè)面,能以較少的代碼量须鼎,通過(guò)簡(jiǎn)單的json配置實(shí)現(xiàn)表格和表單的渲染鲸伴,實(shí)現(xiàn)代碼的開(kāi)發(fā)任務(wù)府蔗。要點(diǎn)有三:
1、表格的動(dòng)態(tài)化配置汞窗,動(dòng)態(tài)展現(xiàn)表格內(nèi)容姓赤、配置化顯示操作按鈕,可擴(kuò)展操作按鈕并提供表格中自定義列模板仲吏,使用vue的render原理實(shí)現(xiàn)不铆。使用到組件BaseCrud
2、新增裹唆、修改的操作彈窗誓斥,表單內(nèi)容動(dòng)態(tài)配置,與表格內(nèi)操作實(shí)現(xiàn)聯(lián)動(dòng)许帐。使用組件BaseDialogForm
3劳坑、表格列表的數(shù)據(jù)獲取,增刪改查的異步請(qǐng)求成畦,都依賴(lài)封裝的apiService方法距芬,方法中抽象了list、create循帐、update框仔、delete、detail方法拄养,具體代碼和實(shí)現(xiàn)思路在另外一篇文章使用axios進(jìn)行apiService的封裝中講解离斩。
注意:crud中的表格自定義列模板依賴(lài)于cell組件,源碼為下方的expand.js瘪匿,需要在BaseCrud中引入使用捐腿。使用方法和iview框架的表格中自定義方法完全一致(直接使用的iview中的實(shí)現(xiàn)源碼,哈哈~~~)柿顶。
一茄袖、BaseCrud的組件源碼
<template>
<div class="crud">
<!--crud頭部,包含可操作按鈕-->
<el-row class="crud-header">
<el-button type="primary" size="mini" v-if="gridBtnConfig.create" @click="createOrUpdate(null)">新增
</el-button>
</el-row>
<!--crud主體內(nèi)容區(qū)嘁锯,展示表格內(nèi)容-->
<el-table
:data="showGridData"
border
v-loading="listLoading"
style="width: 100%">
<el-table-column
v-for="(item,index) in gridConfig"
:key="index"
:prop="item.prop"
:label="item.label"
show-overflow-tooltip
:width="item.width?item.width:''">
<template slot-scope="scope">
<Cell
v-if="item.render"
:row="scope.row"
:column="item"
:index="scope.$index"
:render="item.render"></Cell>
<span v-else>{{scope.row[item.prop]}}</span>
</template>
</el-table-column>
<el-table-column fixed="right" v-if="!hideEditArea" label="操作" :width="gridEditWidth?gridEditWidth:200">
<template slot-scope="scope">
<el-button size="mini" v-if="gridBtnConfig.update" type="primary"
@click="createOrUpdate(scope.row)">修改
</el-button>
<el-button size="mini" v-if="gridBtnConfig.delete" type="danger" @click="remove(scope.row)">刪除</el-button>
<el-button size="mini" v-if="gridBtnConfig.view" type="primary" @click="view(scope.row)">查看</el-button>
<!--擴(kuò)展按鈕-->
<el-button size="mini" @click="handleEmit(item.emitName, scope.row)"
v-if="gridBtnConfig.expands && gridBtnConfig.expands.length>0"
v-for="(item,index) in gridBtnConfig.expands" :key="index" :type="item.type?item.type:'primary'">
{{item.name}}
</el-button>
</template>
</el-table-column>
</el-table>
<!--crud的分頁(yè)組件-->
<div class="crud-pagination">
<!--如果不是異步請(qǐng)求展示數(shù)據(jù)宪祥,需要隱藏分頁(yè)-->
<el-pagination
v-if="isAsync"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 30, 40]"
:page-size="currentPageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="dataTotal">
</el-pagination>
</div>
<!--crud按鈕觸發(fā)的表單彈窗-->
<BaseDialogForm :title="dialogTitle" ref="dialogForm" :config="formConfig" :form-data="formModel"
@submit="dialogSubmit"></BaseDialogForm>
</div>
</template>
<script>
import BaseDialogForm from '@/components/BaseDialogForm/index.vue'
import Cell from './expand';
export default {
name: "base-crud",
components: {
BaseDialogForm,
Cell
},
props: [
// 表單標(biāo)題,例如用戶(hù)家乘、角色
'formTitle',
// 表單配置
'formConfig',
// 表單的model數(shù)據(jù)
'formData',
// 表格配置
'gridConfig',
// 表格按鈕配置
'gridBtnConfig',
// 表格死數(shù)據(jù)
'gridData',
// 數(shù)據(jù)接口
'apiService',
// 判斷是否是異步數(shù)據(jù)
'isAsync',
// 表格編輯區(qū)域?qū)挾? 'gridEditWidth',
// 是否隱藏表格操作
'hideEditArea',
],
data() {
return {
// 新增修改模態(tài)框title
dialogTitle: '',
// 展示的表格數(shù)據(jù)蝗羊,數(shù)據(jù)來(lái)源可能是父組件傳遞的固定數(shù)據(jù),可能是接口請(qǐng)求數(shù)據(jù)
showGridData: [],
// 當(dāng)前頁(yè)碼
currentPage: 1,
// 每頁(yè)顯示數(shù)量
currentPageSize: 10,
// 列表數(shù)據(jù)總數(shù)
dataTotal: 0,
// 表單數(shù)據(jù)
formModel: {},
// 表格加載狀態(tài)
listLoading: false
}
},
mounted() {
this.getData();
},
methods: {
// 獲取列表數(shù)據(jù)
getData() {
this.listLoading = true;
let params = {
page: this.currentPage,
limit: this.currentPageSize
};
this.apiService.list(params).then(res => {
this.showGridData = res.data.list;
this.dataTotal = res.data.total;
this.listLoading = false;
}, err => {
this.listLoading = false;
});
},
createOrUpdate(item) {
this.$refs.dialogForm.resetForm();
// 新增時(shí)仁锯,模態(tài)框數(shù)據(jù)需要拷貝基礎(chǔ)定義的數(shù)據(jù)模型耀找,修改時(shí),直接拷貝當(dāng)前行數(shù)據(jù)
this.formModel = item ? Object.assign({}, item) : Object.assign({}, this.formData) ;
this.dialogTitle = (item ? '修改' : '新增') + this.formTitle;
this.$refs.dialogForm.showDialog();
},
// 處理相應(yīng)父組件的事件方法
handleEmit(emitName,row) {
this.$emit(emitName, row);
},
handleCurrentChange(page) {
this.currentPage = page;
this.getData();
},
handleSizeChange(size) {
this.currentPageSize = size;
this.getData();
},
// 模態(tài)框數(shù)據(jù)提交
dialogSubmit(data) {
this.apiService[data.userId ? 'update' : 'create'](data).then(res => {
this.getData();
this.$message.success(this.dialogTitle + '成功!');
})
},
remove(data) {
// 處理刪除邏輯
},
view(data){
// 處理查看詳情邏輯
}
},
watch: {
// 防止表格預(yù)置數(shù)據(jù)不成功野芒,涉及生命周期問(wèn)題
gridData() {
this.showGridData = this.gridData;
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.crud {
.crud-header {
margin-bottom: 10px;
line-height: 40px;
}
.crud-pagination {
text-align: right;
margin-top: 10px;
}
}
</style>
該組件主要提供了表格的字段動(dòng)態(tài)配置蓄愁,操作按鈕的自定義配置,包括是否顯示新增狞悲、修改撮抓、刪除、查看按鈕摇锋。操作區(qū)域使用屬性配置丹拯,是因?yàn)橐话阌覀?cè)操作區(qū)域是固定的,而且如果使用寬度自適應(yīng)的話荸恕,會(huì)出現(xiàn)寬度不夠乖酬,操作按鈕換行的難看效果,所以使用gridEditWidth可配置融求。操作區(qū)域可隱藏咬像,部分頁(yè)面中使用可能只是查詢(xún)列表。show-overflow-tooltip可以在文字過(guò)長(zhǎng)時(shí)双肤,用省略號(hào)顯示,我這里默認(rèn)每個(gè)表格都使用該屬性钮惠∶┟樱可根據(jù)自己項(xiàng)目中的需求,修改代碼為該屬性可配置素挽。需要注意的是蔑赘,如果表格字段為空,鼠標(biāo)hover時(shí)該格子有個(gè)小黑點(diǎn)的效果预明∷跞可以考慮在代碼中,判斷字段為空時(shí)顯示‘--’進(jìn)行標(biāo)識(shí)撰糠,也防止小黑點(diǎn)的出現(xiàn)酥馍。
提供了獲取列表數(shù)據(jù)的方法,新增和修改的方法阅酪,依賴(lài)于BaseDialogForm組件的應(yīng)用旨袒,新增和修改時(shí)會(huì)復(fù)用同一個(gè)模態(tài)框,但是通過(guò)formTitle展示不一樣的標(biāo)題术辐,最后保存時(shí)通過(guò)特殊字段id進(jìn)行業(yè)務(wù)邏輯的區(qū)分砚尽。擴(kuò)展的操作按鈕,使用handleEmit方法進(jìn)行統(tǒng)一處理辉词,使用事件映射完成父子組件的通信必孤。在具體使用時(shí),可在父組件中使用this.$refs.crud.getData()在進(jìn)行業(yè)務(wù)操作后瑞躺,刷新列表數(shù)據(jù)敷搪。
上訴組件源碼只是提供了基本的實(shí)現(xiàn)思路兴想,我在項(xiàng)目實(shí)際使用中,結(jié)合公司的定制化業(yè)務(wù)购啄,實(shí)際還結(jié)合了很多組件的應(yīng)用襟企,包括查看詳情模態(tài)框是根據(jù)新增和修改的動(dòng)態(tài)表單進(jìn)行默認(rèn)生成展示,配置列表上方動(dòng)態(tài)生成的高級(jí)搜索表單進(jìn)行表格數(shù)據(jù)查詢(xún)操作等等狮含。具體的擴(kuò)展大家可以根據(jù)實(shí)際業(yè)務(wù)進(jìn)行定制化修改顽悼。
具體的使用實(shí)例會(huì)在最下方代碼展示
二、表格自定義列模板几迄,依賴(lài)的expand.js的源碼
export default {
name: 'TableExpand',
functional: true,
props: {
row: Object,
render: Function,
index: Number,
column: {
type: Object,
default: null
}
},
render: (h, ctx) => {
const params = {
row: ctx.props.row,
index: ctx.props.index
};
if (ctx.props.column) params.column = ctx.props.column;
return ctx.props.render(h, params);
}
};
三蔚龙、BaseDialogForm組件的源碼
<template>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
:width="width?width:'80%'">
<el-form :model="formModel" ref="configForm" label-width="100px">
<el-row :gutter="16">
<el-col :span="item.span?item.span:8" v-for="(item,index) in config" :key="index">
<el-form-item
:prop="item.prop"
:rules="item.rules"
:label="item.label"
>
<!--輸入框表單類(lèi)型-->
<el-input v-if="item.type ==='text'" v-model="formData[item.prop]"
:placeholder="item.placeholder?item.placeholder:'請(qǐng)輸入'"></el-input>
<!--文本域表單類(lèi)型-->
<el-input v-if="item.type === 'textarea'" type="textarea" v-model="formData[item.prop]"
:placeholder="item.placeholder?item.placeholder:'請(qǐng)輸入'"></el-input>
<!--checkbox表單類(lèi)型-->
<el-checkbox-group v-if="item.type === 'checkbox'" v-model="formData[item.prop]"
:placeholder="item.placeholder?item.placeholder:'請(qǐng)選擇'">
<el-checkbox v-for="option in item.data" :label="option.id" :key="option.id">{{option.name}}</el-checkbox>
</el-checkbox-group>
<!--radio表單類(lèi)型-->
<el-radio-group v-if="item.type === 'radio'" v-model="formData[item.prop]"
:placeholder="item.placeholder?item.placeholder:'請(qǐng)選擇'">
<el-radio v-for="option in item.data" :label="option.id" :key="option.id">{{option.name}}</el-radio>
</el-radio-group>
<!--下拉選擇類(lèi)型-->
<el-select v-if="item.type === 'select'" v-model="formData[item.prop]"
:placeholder="item.placeholder?item.placeholder:'請(qǐng)選擇'">
<el-option
v-for="option in item.data"
:key="option.id"
:label="option.name"
:value="option.id">
</el-option>
</el-select>
<el-date-picker
v-if="item.type === 'datepicker'"
v-model="formData[item.prop]"
type="date"
:placeholder="item.placeholder?item.placeholder:'請(qǐng)選擇日期'">
</el-date-picker>
</el-form-item>
</el-col>
</el-row>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitForm">確 定</el-button>
</span>
</el-dialog>
</template>
<script>
export default {
name: "base-dialog-form",
props: [
'title',
'width',
'visible',
'config',
'formData'
],
data() {
return {
formModel: {},
dialogVisible: false,
dialogTitle: '',
}
},
mounted() {
// 將組件上的屬性賦值給當(dāng)前組件內(nèi)變量,因?yàn)閜rops只能單向綁定,還需要監(jiān)聽(tīng)屬性值變化進(jìn)行父子組件間交互
this.formModel = this.formData;
this.dialogVisible = this.visible;
this.dialogTitle = this.title;
},
methods: {
// 提交表單數(shù)據(jù)
submitForm() {
this.$refs.configForm.validate((valid) => {
if (valid) {
// 讓父組件接收到響應(yīng)數(shù)據(jù)
this.$emit('submit', this.formModel);
// 關(guān)閉模態(tài)框
this.dialogVisible = false;
} else {
console.log('error submit!!');
return false;
}
});
},
// 重置表單狀態(tài)
resetForm() {
if (this.$refs.configForm) {
this.$refs.configForm.resetFields();
}
},
// 展示模態(tài)框
showDialog() {
this.dialogVisible = true;
}
},
watch: {
/*實(shí)現(xiàn)表單數(shù)據(jù)的綁定映胁,實(shí)時(shí)接收父組件的數(shù)據(jù)變化*/
formData() {
this.formModel = this.formData;
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.el-input{
width: 100% !important;
}
.el-select{
width: 100% !important;
}
</style>
上訴代碼大家應(yīng)該都能看懂木羹,就是通過(guò)簡(jiǎn)單的config數(shù)組提供需要?jiǎng)討B(tài)生成的表單配置,具體包含表單控件類(lèi)型解孙、對(duì)應(yīng)的字段坑填、驗(yàn)證規(guī)則、placeholder文字等等所需的配置弛姜,可根據(jù)自身情況進(jìn)行擴(kuò)展脐瑰,此處只做簡(jiǎn)單的示例
需要注意的是,在使用重置表單狀態(tài)的方法中廷臼,需要預(yù)先判斷組件實(shí)例是否存在苍在,因?yàn)閑l-dialog的底層代碼實(shí)現(xiàn)用了v-if,在第一次還未顯示時(shí)荠商,dom實(shí)際不存在寂恬,如果不進(jìn)行判斷直接使用的話,代碼會(huì)報(bào)錯(cuò)莱没。
四初肉、使用示例之用戶(hù)列表渲染
頁(yè)面代碼:
<template>
<div>
<BaseCrud @download="download" :apiService="userService" :grid-config="configData.gridConfig" :grid-btn-config="configData.gridBtnConfig"
:grid-data="testData"
:form-config="configData.formConfig" :form-data="configData.formModel" :grid-edit-width="200"
form-title="用戶(hù)" :is-async="true">
</BaseCrud>
</div>
</template>
<script>
import BaseCrud from '@/components/BaseCrud/index.vue'
import {USER_CONFIG} from './config'
import {userService} from '@/api/user.js'
export default {
components: {
BaseCrud
},
data () {
return {
testData: [],
configData: USER_CONFIG,
userService: userService
}
},
name: 'user-list',
mounted () {
this.testData = [
{
id: '1',
tel: '15184318420',
name: '小白',
email: '412412@qq.com',
status: '1',
create_time: '2018-04-20',
expand: '擴(kuò)展信息一',
role: ['2']
},
{
id: '2',
tel: '13777369283',
name: '小紅',
email: '456465@qq.com',
status: '0',
create_time: '2018-03-23',
expand: 'hashashashas',
role: ['1']
}
];
},
methods: {
download(row){
console.log('點(diǎn)擊了下載按鈕',row);
}
}
}
</script>
config配置:
export const USER_CONFIG = {
gridConfig: [
{label: '用戶(hù)ID', prop: 'id', width: '100'},
{label: '手機(jī)號(hào)(登錄賬號(hào))', prop: 'tel'},
{label: '郵箱', prop: 'email', width: '100'},
{label: '中文名', prop: 'name'},
{
label: '狀態(tài)', prop: 'status', render: (h, params) => {
if(params.row.status === '0'){
return h('el-tag', {
props:{
size:'mini',
type:'warning'
}
},'正常');
}else {
return h('el-tag', {
props:{
size:'mini',
type:'success'
}
},'禁用');
}
}
},
{label: '創(chuàng)建時(shí)間', prop: 'create_time'},
{label: '擴(kuò)展信息', prop: 'expand'}
],
// crud的模態(tài)框表單配置,可配置表單類(lèi)型饰躲,驗(yàn)證規(guī)則朴译,是否必填,col-span布局可通過(guò)span參數(shù)配置
formConfig: [
{span: 12, label: '手機(jī)號(hào)', prop: 'tel', type: 'text'},
{span: 12, label: '中文名', prop: 'name', type: 'text'},
{span: 12, label: '郵箱', prop: 'email', type: 'text'},
{
span: 12, label: '角色',
prop: 'roleIdList',
type: 'checkbox',
data: [{name: '角色一', id: '1'}, {name: '角色二', id: '2'}],
rules: { type: 'array', required: true, message: '請(qǐng)選擇角色', trigger: 'change' }
},
{
span: 12, label: '狀態(tài)',
prop: 'status',
type: 'radio',
data: [{name: '正常', id: 1}, {name: '禁用', id: 0}],
rules: {required: true, message: '請(qǐng)選擇角色狀態(tài)', trigger: 'change'}
},
{span: 24, label: '備注', prop: 'expand', type: 'textarea'}
],
// crud的操作按鈕配置,基礎(chǔ)按鈕有添加属铁、修改眠寿、刪除、查看焦蘑,還可以配置擴(kuò)展按鈕
gridBtnConfig: {
create: true, update: true, delete: true, view: false,
expands: [
{ name: '下載按鈕', emitName: 'download', type: 'primary' }
]
},
// 表單基礎(chǔ)數(shù)據(jù)類(lèi)型盯拱,需要預(yù)先賦值
formModel: {
id: '',
tel: '',
name: '',
email: '',
status: '',
create_time: '',
expand: '',
roleIdList: []
}
};
以上是用戶(hù)列表的基礎(chǔ)配置使用示例,數(shù)據(jù)使用的是測(cè)試數(shù)據(jù)。在實(shí)際使用中應(yīng)該是通過(guò)apiService從服務(wù)器端請(qǐng)求數(shù)據(jù)狡逢。下列截圖展示了基礎(chǔ)的配置使用效果展示宁舰。
五、總結(jié)
上訴示例是基礎(chǔ)業(yè)務(wù)的實(shí)現(xiàn)代碼奢浑,由于實(shí)際業(yè)務(wù)中代碼量稍大蛮艰,邏輯稍微復(fù)雜,并未在此處進(jìn)行完全展示雀彼。此處代碼為刪減版壤蚜,可能有一些問(wèn)題,可以留言進(jìn)行探討解決徊哑。表格配置實(shí)際使用中袜刷,應(yīng)該會(huì)有使用過(guò)濾器的情況,還有我在項(xiàng)目中實(shí)際使用時(shí)莺丑,有表格多選的情況著蟹,表格內(nèi)操作按鈕,和表格外按鈕的配置梢莽,以及涉及批量操作的如批量刪除等業(yè)務(wù)萧豆。若后期大家實(shí)際需要,我會(huì)陸續(xù)分享實(shí)現(xiàn)方案昏名。
如果各位大牛有更好的實(shí)現(xiàn)方案涮雷,也希望不吝賜教,大家共同進(jìn)步葡粒。
最后份殿,希望走過(guò)路過(guò)的朋友能個(gè)給個(gè)贊唄膜钓,謝謝~~