一般做后台管理系统的项目, 用到最多的 UI
组件应该就是表格和表单吧.
一个最基础的例子就是上面一排操作按钮: 增删改查; 下面一个表格, 展示数据; 增改的时候弹出表单; 而且大部分情况下表格和表单的字段都是一样的, 但却要写两套代码, 作为一个崇尚简洁的程序员, 越简洁越好(才不是懒得写呢)
所以就想封装一下这两个组件, 最好一行代码就搞定了
分析现有代码
table
先看 table
例子:
1 2 3 4 5
| <el-table :data="tableData" style="width: 100%"> <el-table-column prop="date" label="日期" width="180"> </el-table-column> <el-table-column prop="name" label="姓名" width="180"> </el-table-column> <el-table-column prop="address" label="地址"> </el-table-column> </el-table>
|
我们可以把 column
这部分抽出一个数组, 数组中包含所有属性; 和 data
似的传入封装好的组件中, 组件自己遍历生成 el-table-column
, 那咱们调用组件的时候是不是就一行代码就搞定了呀:
1
| <base-table :data="tableData" :columns="columns" style="width: 100%"></base-table>
|
再看 form
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| <el-form ref="form" :model="form" label-width="80px"> <el-form-item label="活动名称"> <el-input v-model="form.name"></el-input> </el-form-item> <el-form-item label="活动区域"> <el-select v-model="form.region" placeholder="请选择活动区域"> <el-option label="区域一" value="shanghai"></el-option> <el-option label="区域二" value="beijing"></el-option> </el-select> </el-form-item> <el-form-item label="活动时间"> <el-col :span="11"> <el-date-picker type="date" placeholder="选择日期" v-model="form.date1" style="width: 100%;" ></el-date-picker> </el-col> <el-col class="line" :span="2">-</el-col> <el-col :span="11"> <el-time-picker placeholder="选择时间" v-model="form.date2" style="width: 100%;" ></el-time-picker> </el-col> </el-form-item> <el-form-item label="即时配送"> <el-switch v-model="form.delivery"></el-switch> </el-form-item> <el-form-item label="活动性质"> <el-checkbox-group v-model="form.type"> <el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox> <el-checkbox label="地推活动" name="type"></el-checkbox> <el-checkbox label="线下主题活动" name="type"></el-checkbox> <el-checkbox label="单纯品牌曝光" name="type"></el-checkbox> </el-checkbox-group> </el-form-item> <el-form-item label="特殊资源"> <el-radio-group v-model="form.resource"> <el-radio label="线上品牌商赞助"></el-radio> <el-radio label="线下场地免费"></el-radio> </el-radio-group> </el-form-item> <el-form-item label="活动形式"> <el-input type="textarea" v-model="form.desc"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="onSubmit">立即创建</el-button> <el-button>取消</el-button> </el-form-item> </el-form>
|
form
就比较难一些了: 不仅要判断表单类型( input
, select
, textarea
等等), select
等表单还需要 option
数组
为了实现一行代码就搞定, 所以我们最终调用的方式应该是这样的:
1
| <base-form ref="form" :model="form" :form-items="formItems" label-width="80px"></base-form>
|
那就需要在 formItems
中做处理了: 首先需要区分表单类型; 对于 select
等这类表单, 还需要传入 option
选择数组
初稿
table
table
就比较简单了
组件源码:
1 2 3 4 5 6 7 8
| <el-table v-bind="$attrs"> <el-table-column v-for="column in columns" :key="column.prop" v-bind="column" > </el-table-column> </el-table>
|
1 2 3 4 5 6 7 8 9
| name: 'BaseTable', props: { columns: { type: Array, default () { return [] } } }
|
调用:
1
| <base-table :data="tableData" :columns="columns" style="width: 100%"></base-table>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| data() { return { tableData: [ { date: '2016-05-02', name: '王小虎', address: '上海市普陀区金沙江路 1518 弄' } ], columns: [ { label: '日期', prop: 'date', width: 180 }, { label: '姓名', prop: 'name', width: 180 }, { label: '地址', prop: 'address' } ] } }
|
form
需要做的事情比 table
多一些
组件源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <el-form v-bind="$attrs" :model="model"> <el-form-item v-for="item in formItems" :key="item.prop"> <el-input v-if="item.type === 'input'" v-model="model[item.prop]" v-bind="item" > </el-input> <el-select v-if="item.type === 'select'" v-model="model[item.prop]" v-bind="item"> <el-option v-for="option in item.select" :key="option.value" :label="option.label" :value="option.value"> </el-option> </el-select> <el-switch v-if="item.type === 'switch'" v-model="model[item.prop]" v-bind="item" > </el-switch> </el-form-item> </el-form>
|
其余的表单就不写了, 都差不多的样子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| name: 'BaseForm', props: { modle: { type: Object, default() { return {} } }, formItems: { type: Array, default() { return [] } } }
|
调用:
1
| <base-form ref="form" :model="form" :form-items="formItems" label-width="80px"></base-form>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| data() { return { form: { name: '', region: '' }, formItems: [ { type: 'input', label: '活动名称', prop: 'name' }, { type: 'select', label: '活动区域', prop: 'region', select: [ { label: '区域一', value: 'shanghai' }, { label: '区域二', value: 'beijing' } ] } ] } }
|
优化
目前使用我们封装好的组件, 确实可以实现一行代码就搞定的效果, 而且如果 form
和 table
字段一样的话, formItems
和 columns
完全可以合并为一个数组. 但还有几个问题:
BaseTable
功能过于单一, 只能展示数据, 我们实际工作中还有可编辑的表格
BaseForm
封装的组件重复代码太多, 判断太多, 很多时候只是组件名字不同, 里面内容完全一致, 比如 el-input
和 el-switch
; 现在把 ElementUI
所有表单元素都写进去了, 但实际上就 el-input
用的比较多, 而且可扩展性不好, 想要再加表单, 就需要修改封装的组件的源码
综合以上两个问题我们不难看出目前的主要问题就是这些可编辑的表单元素, 那我们解决了这个问题就可以了呀
只是组件名字不同, 里面内容完全一致
从这句话想到了什么吗? 对! 就是这个 Vue
内置组件: component
我们完全可以去掉那些 v-if
判断, 而只使用 component
即可, 这样可扩展性问题也解决了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <el-form v-bind="$attrs" :model="model"> <el-form-item v-for="item in formItems" :key="item.prop"> <component :is="item.type" v-model="model[item.prop]" v-bind="item"> <el-option v-for="option in item.select" :key="option.value" :label="option.label" :value="option.value" > </el-option> <el-radio v-for="radio in item.radio" :key="radio[item.value]" :label="radio[item.value]" > {{ radio[item.label] }} </el-radio> </component> </el-form-item> </el-form>
|
是不是一下就简单多了?
那么现在 item.type
就是组件的名字了, 比如 el-input
; 关于 el-select
这类还需要有选择数据的组件(el-option
, el-radio
等), 我目前是全部放到 component
里面的, 一来是不传这些选择数组的时候, 就不会渲染, 二来是就算渲染了, 组件内部 slot
如果不接收, 也不会显示出来, 三呢就是这些组件已经在项目中使用了, 改动的话有点麻烦, 所以如果是新项目, 这些也可以使用 component
那 form
的改完了, table
是不是也可以使用这个呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <el-table v-bind="$attrs" v-on="$listeners"> <template v-for="(column, index) in columns"> <el-table-column v-if="column.editable" :key="column.prop" v-bind="column"> <component slot-scope="{ row }" :is="column.type" v-model="row[column.prop]" v-bind="column" > <el-option v-for="option in column.select" :key="option.value" :label="option.label" :value="option.value" > </el-option> <el-radio v-for="radio in column.radio" :key="radio[column.value]" :label="radio[column.value]" > {{ radio[column.label] }} </el-radio> </component> </el-table-column> <el-table-column v-else :key="column.prop" v-bind="column"> </el-table-column> </template> </el-table>
|
一想到这个组件可能被很多地方使用, 那咱们是不是也需要将这个可编辑的组件也封装为一个组件呢?
封装可编辑组件
这个就没啥好说的了, 就是把上面的拿下来即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <component :is="item.type" v-model="model[item.prop]" v-bind="item"> <el-option v-for="option in item.select" :key="option.value" :label="option.label" :value="option.value" > </el-option> <el-radio v-for="radio in item.radio" :key="radio[item.value]" :label="radio[item.value]" > {{ radio[item.label] }} </el-radio> </component>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| name: 'EditableElements', props: { item: { type: Object, default() { return {} } }, model: { type: Object, default() { return {} } } }
|
优化一下:
上面只有 radio
, 其实 checkbox
也是类似的, 所以对于这类组件应该也使用 component
处理, 而且 value
和 label
也需要做一下映射:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
| <template> <component :is="item.component" v-model="model[item.prop]" :key="item.prop" v-bind="item" v-focus="item.focus" :placeholder="item.placeholder || `${handlePlaceholder(item.type)}${item.label}`" v-on="{ ...$listeners, ...item.events }" > <text-ellipsis v-if="item.type === 'info'" :content="model[item.prop]"></text-ellipsis>
<template v-if="item.type === 'select'"> <el-option v-for="option in item.options || item.select" :key="option[listProps.value]" :value="option[listProps.value]" :label="option[listProps.label]" :disabled="option.disabled" ></el-option> </template> <template v-if="list.includes(item.type)"> <component :is="`el-${item.type}`" v-for="option in item.options || item[item.type]" :key="option[listProps.value]" :label="option[listProps.value]" :disabled="option.disabled" > {{ ele[listProps.label] }} </component> </template> <slot v-for="(value, key) in item.slots" :name="key" :slot="key">{{ value }}</slot> </component> </template>
<script> import { handlePlaceholder } from 'utils'
export default { name: 'EditableElements', inheritAttrs: false, props: { item: { type: Object, default() { return {} } }, model: { type: Object, default() { return {} } } }, data() { return { list: ['radio', 'checkbox'] } }, computed: { listProps() { const props = this.item.props || this.item[`${this.item.type}Props`] if (!props) return { label: 'label', value: 'value' } const { label = 'label', value = 'value', ...rest } = props return { label, value, ...rest } } }, methods: { handlePlaceholder }, directives: { focus: { update(el, { value, oldValue }) { if (value && value !== oldValue) { const dom = el.querySelector('input') || el.querySelector('textarea') dom && dom.focus() } }, inserted(el, { value }) { if (value) { const dom = el.querySelector('input') || el.querySelector('textarea') dom && dom.focus() } } } } } </script>
<style lang="less" scoped> .editable-elements { .el-select { width: 100%; } } </style>
|
终稿
到这里本次封装之旅差不多就结束了, 想想还有点小激动呢!
闲话不多说, 加了亿点点小细节, enjoy
这里是源码: BaseForm, BaseTable, EditableElements
以下为简略代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <el-form v-bind="$attrs" :model="model" ref="elForm" class="base-form"> <slot name="prev"></slot> <el-form-item v-for="item in items" :key="item.prop" v-bind="item" :rules="[ { required: !item.noRequired, message: item.ruleMessage || `${handlePlaceholder(item.type)}${item.label}`, trigger: item.type === 'select' ? 'change' : 'blur' }, ...(rules[item.prop] || []) ]" > <editable-elements :model="model" :item="item" v-on="$listeners"></editable-elements> </el-form-item> <slot></slot> </el-form>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| props: { keyProps: { type: Object, default() { return null } }, model: { type: Object, default() { return {} } }, formItems: { type: Array, default() { return [] } }, rules: { type: Object, default() { return {} } } }, computed: { items() { return this.keyProps ? this.formItems.map(item => ({ ...item, prop: item[this.keyProps.prop || 'prop'], label: item[this.keyProps.label || 'label'] })) : this.formItems } }
|
table
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <el-table ref="elTable" class="base-table" v-bind="$attrs" v-on="$listeners"> <slot name="prev"></slot> <template v-for="(column, index) in cols"> <el-table-column v-if="column.editable" :key="column.prop" v-bind="column"> <editable-elements slot-scope="{ row, $index }" :model="row" :item="{ ...column, focus: index === focusCol && $index === focusRow }" @change="change(row, $event, column)" ></editable-elements> </el-table-column> <el-table-column v-else :key="column.prop" v-bind="column"> </el-table-column> </template> <slot></slot> </el-table>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| props: { keyProps: { type: Object, default() { return null } }, columns: { type: Array, default() { return [] } }, focusRow: { type: Number, default: 0 }, focusCol: { type: Number, default: 0 } }, computed: { cols() { return this.keyProps ? this.columns.map(column => ({ ...column, prop: column[this.keyProps.prop || 'prop'], label: column[this.keyProps.label || 'label'] })) : this.columns } }
|
editable-elements
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <template> <component :is="item.component" v-model="model[item.prop]" :key="item.prop" v-bind="item" v-focus="item.focus" :placeholder="item.placeholder || `${handlePlaceholder(item.type)}${item.label}`" v-on="{ ...$listeners, ...item.events }" > <text-ellipsis v-if="item.type === 'info'" :content="model[item.prop]"></text-ellipsis> <template v-if="item.type === 'select'"> <el-option v-for="option in item.options || item.select" :key="option[listProps.value]" :value="option[listProps.value]" :label="option[listProps.label]" :disabled="option.disabled" ></el-option> </template> <template v-if="list.includes(item.type)"> <component :is="`el-${item.type}`" v-for="option in item.options || item[item.type]" :key="option[listProps.value]" :label="option[listProps.value]" :disabled="option.disabled" > {{ option[listProps.label] }} </component> </template> <slot v-for="(value, key) in item.slots" :name="key" :slot="key">{{ value }}</slot> </component> </template>
|
增加功能
BaseTable 增加 defaultCheckedKeys 和 currentNodeKey
最近做项目的时候遇到 table
初始化的时候要有默认选中和默认高亮行的需求, 那就参照 el-tree
的方式做一下
本以为和 el-tree
一样, 传入 key
后, el-table
内部会自己处理, 结果 el-table
只能通过传入的 row
来实现默认选中和默认高亮行, 这也不能理解, 毕竟 el-table
中很少用到 row-key
那咱们就通过传入的 key
遍历找到对应的 row
, 当然这种方式就必须传入 row-key
啦:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| props: { data: { type: Array, default() { return [] } }, defaultCheckedKeys: { type: Array, default() { return [] } }, currentNodeKey: { type: [String, Number], default: '' } }, watch: { data: { handler() { this.setDefaultCheckedKeys() this.setCurrentNodeKey() }, immediate: true }, rowKey: { type: [String, Function], default: 'id' }, defaultCheckedKeys: { handler: 'setDefaultCheckedKeys', immediate: true }, currentNodeKey: { handler: 'setCurrentNodeKey', immediate: true } }, methods: { setDefaultCheckedKeys() { this.$nextTick(() => { if (this.defaultCheckedKeys.length) { const rows = this.data.filter(item => this.defaultCheckedKeys.includes(item[this.rowKey])) rows.forEach(row => { this.$refs.elTable.toggleRowSelection(row, true) }) } else { this.$refs.elTable.clearSelection() } }) }, setCurrentNodeKey() { this.$nextTick(() => { if (this.currentNodeKey) { const row = this.data.find(item => this.currentNodeKey === item[this.rowKey]) if (row) { this.$refs.elTable.setCurrentRow(row) } } else { this.$refs.elTable.setCurrentRow(null) } }) } }
|
实际工作中发现的问题
改变 editable
后页面没变化
最近项目增加了权限控制, 一些可编辑的表格需要先判断权限, 有权限才可以编辑.
所以需要动态修改 editable
的值, 初始为 false
, 后续根据权限改变值.
但发现明明有权限, 却还是不可编辑状态, 而且跟踪代码也发现值确实改变了, 但页面却没有改变
只有触发页面重绘后才会变成可编辑状态, 就好像是状态其实已经改变了, 就差最后的绘制了
这时候突然想到是不是表格复用了, 仍然用的是不可编辑状态的表格: 利用 v-if 动态渲染表格时, 在 el-table-column 中添加 key 属性防止表格复用
查看代码发现果然是复用了: 可编辑和不可编辑用的相同的 key
1 2 3 4 5 6 7 8 9 10
| <el-table-column v-if="column.editable" :key="column.prop" v-bind="column"> </el-table-column> <el-table-column v-else :key="column.prop" v-bind="column"> </el-table-column>
|
当初封装组件的时候想着他俩肯定只能存在一个, 用相同的 key
应该没什么问题, 却忘了切换 editable
后, 相同的 key
会复用的问题了
那稍微修改一下吧:
1 2 3 4 5 6 7 8 9 10
| <el-table-column v-if="column.editable" :key="`${column.prop}-edit`" v-bind="column"> </el-table-column> <el-table-column v-else :key="column.prop" v-bind="column"> </el-table-column>
|
所以以后尽量还是用不同的 key
吧, 即使是 if else
.
可编辑 table 有时候没有滚动条
引起这个问题主要有两个条件:
table
中有可编辑组件
- 内容高度刚好超出设置高度一点点
当有可编辑组件时, tr
的高度比不可编辑时高了一点, 而 el-table
还是按不可编辑状态计算高度, 从而认为当前内容并没有超出设置高度, 所以没有出现滚动条
所以解决方法有两种:
要么将 tr
的高度设死, 这样可编辑和不可编辑高度一样, 就可以了; 不过样式可能不算美观
要么让 el-table
重新计算一下高度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| computed: { isEditable() { return this.columns.some(item => item.editable || item.editableMethod) } }, watch: { data: { handler() { this.setDefaultCheckedKeys() this.setCurrentNodeKey() this.refreshLayout() }, immediate: true } }, methods: { refreshLayout() { if (!this.isEditable) return this.$nextTick(() => { setTimeout(() => { this.$refs.elTable.doLayout() }, 200) }) } }
|