说明:最近几个月负责公司部门vue项目的前端架构任务,该组件的设计实现,主要考虑提高业务开发效率,实现表格的动态化配置,对项目中常见的业务页面,能以较少的代码量,通过简单的json配置实现表格和表单的渲染,实现代码的开发任务。要点有三:
1、表格的动态化配置,动态展现表格内容、配置化显示操作按钮,可扩展操作按钮并提供表格中自定义列模板,使用vue的render原理实现。使用到组件BaseCrud
2、新增、修改的操作弹窗,表单内容动态配置,与表格内操作实现联动。使用组件BaseDialogForm
3、表格列表的数据获取,增删改查的异步请求,都依赖封装的apiService方法,方法中抽象了list、create、update、delete、detail方法,具体代码和实现思路在另外一篇文章使用axios进行apiService的封装中讲解。
注意:crud中的表格自定义列模板依赖于cell组件,源码为下方的expand.js,需要在BaseCrud中引入使用。使用方法和iview框架的表格中自定义方法完全一致(直接使用的iview中的实现源码,哈哈~~~)。
一、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主体内容区,展示表格内容-->
<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>
<!--扩展按钮-->
<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的分页组件-->
<div class="crud-pagination">
<!--如果不是异步请求展示数据,需要隐藏分页-->
<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按钮触发的表单弹窗-->
<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: [
// 表单标题,例如用户、角色
'formTitle',
// 表单配置
'formConfig',
// 表单的model数据
'formData',
// 表格配置
'gridConfig',
// 表格按钮配置
'gridBtnConfig',
// 表格死数据
'gridData',
// 数据接口
'apiService',
// 判断是否是异步数据
'isAsync',
// 表格编辑区域宽度
'gridEditWidth',
// 是否隐藏表格操作
'hideEditArea',
],
data() {
return {
// 新增修改模态框title
dialogTitle: '',
// 展示的表格数据,数据来源可能是父组件传递的固定数据,可能是接口请求数据
showGridData: [],
// 当前页码
currentPage: 1,
// 每页显示数量
currentPageSize: 10,
// 列表数据总数
dataTotal: 0,
// 表单数据
formModel: {},
// 表格加载状态
listLoading: false
}
},
mounted() {
this.getData();
},
methods: {
// 获取列表数据
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();
// 新增时,模态框数据需要拷贝基础定义的数据模型,修改时,直接拷贝当前行数据
this.formModel = item ? Object.assign({}, item) : Object.assign({}, this.formData) ;
this.dialogTitle = (item ? '修改' : '新增') + this.formTitle;
this.$refs.dialogForm.showDialog();
},
// 处理相应父组件的事件方法
handleEmit(emitName,row) {
this.$emit(emitName, row);
},
handleCurrentChange(page) {
this.currentPage = page;
this.getData();
},
handleSizeChange(size) {
this.currentPageSize = size;
this.getData();
},
// 模态框数据提交
dialogSubmit(data) {
this.apiService[data.userId ? 'update' : 'create'](data).then(res => {
this.getData();
this.$message.success(this.dialogTitle + '成功!');
})
},
remove(data) {
// 处理删除逻辑
},
view(data){
// 处理查看详情逻辑
}
},
watch: {
// 防止表格预置数据不成功,涉及生命周期问题
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>
该组件主要提供了表格的字段动态配置,操作按钮的自定义配置,包括是否显示新增、修改、删除、查看按钮。操作区域使用属性配置,是因为一般右侧操作区域是固定的,而且如果使用宽度自适应的话,会出现宽度不够,操作按钮换行的难看效果,所以使用gridEditWidth可配置。操作区域可隐藏,部分页面中使用可能只是查询列表。show-overflow-tooltip可以在文字过长时,用省略号显示,我这里默认每个表格都使用该属性。可根据自己项目中的需求,修改代码为该属性可配置。需要注意的是,如果表格字段为空,鼠标hover时该格子有个小黑点的效果。可以考虑在代码中,判断字段为空时显示‘--’进行标识,也防止小黑点的出现。
提供了获取列表数据的方法,新增和修改的方法,依赖于BaseDialogForm组件的应用,新增和修改时会复用同一个模态框,但是通过formTitle展示不一样的标题,最后保存时通过特殊字段id进行业务逻辑的区分。扩展的操作按钮,使用handleEmit方法进行统一处理,使用事件映射完成父子组件的通信。在具体使用时,可在父组件中使用this.$refs.crud.getData()在进行业务操作后,刷新列表数据。
上诉组件源码只是提供了基本的实现思路,我在项目实际使用中,结合公司的定制化业务,实际还结合了很多组件的应用,包括查看详情模态框是根据新增和修改的动态表单进行默认生成展示,配置列表上方动态生成的高级搜索表单进行表格数据查询操作等等。具体的扩展大家可以根据实际业务进行定制化修改。
具体的使用实例会在最下方代码展示
二、表格自定义列模板,依赖的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"
>
<!--输入框表单类型-->
<el-input v-if="item.type ==='text'" v-model="formData[item.prop]"
:placeholder="item.placeholder?item.placeholder:'请输入'"></el-input>
<!--文本域表单类型-->
<el-input v-if="item.type === 'textarea'" type="textarea" v-model="formData[item.prop]"
:placeholder="item.placeholder?item.placeholder:'请输入'"></el-input>
<!--checkbox表单类型-->
<el-checkbox-group v-if="item.type === 'checkbox'" v-model="formData[item.prop]"
:placeholder="item.placeholder?item.placeholder:'请选择'">
<el-checkbox v-for="option in item.data" :label="option.id" :key="option.id">{{option.name}}</el-checkbox>
</el-checkbox-group>
<!--radio表单类型-->
<el-radio-group v-if="item.type === 'radio'" v-model="formData[item.prop]"
:placeholder="item.placeholder?item.placeholder:'请选择'">
<el-radio v-for="option in item.data" :label="option.id" :key="option.id">{{option.name}}</el-radio>
</el-radio-group>
<!--下拉选择类型-->
<el-select v-if="item.type === 'select'" v-model="formData[item.prop]"
:placeholder="item.placeholder?item.placeholder:'请选择'">
<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:'请选择日期'">
</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() {
// 将组件上的属性赋值给当前组件内变量,因为props只能单向绑定,还需要监听属性值变化进行父子组件间交互
this.formModel = this.formData;
this.dialogVisible = this.visible;
this.dialogTitle = this.title;
},
methods: {
// 提交表单数据
submitForm() {
this.$refs.configForm.validate((valid) => {
if (valid) {
// 让父组件接收到响应数据
this.$emit('submit', this.formModel);
// 关闭模态框
this.dialogVisible = false;
} else {
console.log('error submit!!');
return false;
}
});
},
// 重置表单状态
resetForm() {
if (this.$refs.configForm) {
this.$refs.configForm.resetFields();
}
},
// 展示模态框
showDialog() {
this.dialogVisible = true;
}
},
watch: {
/*实现表单数据的绑定,实时接收父组件的数据变化*/
formData() {
this.formModel = this.formData;
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.el-input{
width: 100% !important;
}
.el-select{
width: 100% !important;
}
</style>
上诉代码大家应该都能看懂,就是通过简单的config数组提供需要动态生成的表单配置,具体包含表单控件类型、对应的字段、验证规则、placeholder文字等等所需的配置,可根据自身情况进行扩展,此处只做简单的示例
需要注意的是,在使用重置表单状态的方法中,需要预先判断组件实例是否存在,因为el-dialog的底层代码实现用了v-if,在第一次还未显示时,dom实际不存在,如果不进行判断直接使用的话,代码会报错。
四、使用示例之用户列表渲染
页面代码:
<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="用户" :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: '扩展信息一',
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('点击了下载按钮',row);
}
}
}
</script>
config配置:
export const USER_CONFIG = {
gridConfig: [
{label: '用户ID', prop: 'id', width: '100'},
{label: '手机号(登录账号)', prop: 'tel'},
{label: '邮箱', prop: 'email', width: '100'},
{label: '中文名', prop: 'name'},
{
label: '状态', 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: '创建时间', prop: 'create_time'},
{label: '扩展信息', prop: 'expand'}
],
// crud的模态框表单配置,可配置表单类型,验证规则,是否必填,col-span布局可通过span参数配置
formConfig: [
{span: 12, label: '手机号', 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: '请选择角色', trigger: 'change' }
},
{
span: 12, label: '状态',
prop: 'status',
type: 'radio',
data: [{name: '正常', id: 1}, {name: '禁用', id: 0}],
rules: {required: true, message: '请选择角色状态', trigger: 'change'}
},
{span: 24, label: '备注', prop: 'expand', type: 'textarea'}
],
// crud的操作按钮配置,基础按钮有添加、修改、删除、查看,还可以配置扩展按钮
gridBtnConfig: {
create: true, update: true, delete: true, view: false,
expands: [
{ name: '下载按钮', emitName: 'download', type: 'primary' }
]
},
// 表单基础数据类型,需要预先赋值
formModel: {
id: '',
tel: '',
name: '',
email: '',
status: '',
create_time: '',
expand: '',
roleIdList: []
}
};
以上是用户列表的基础配置使用示例,数据使用的是测试数据。在实际使用中应该是通过apiService从服务器端请求数据。下列截图展示了基础的配置使用效果展示。
五、总结
上诉示例是基础业务的实现代码,由于实际业务中代码量稍大,逻辑稍微复杂,并未在此处进行完全展示。此处代码为删减版,可能有一些问题,可以留言进行探讨解决。表格配置实际使用中,应该会有使用过滤器的情况,还有我在项目中实际使用时,有表格多选的情况,表格内操作按钮,和表格外按钮的配置,以及涉及批量操作的如批量删除等业务。若后期大家实际需要,我会陆续分享实现方案。
如果各位大牛有更好的实现方案,也希望不吝赐教,大家共同进步。
最后,希望走过路过的朋友能个给个赞呗,谢谢~~