el-table动态渲染列、可编辑单元格、虚拟无缝滚动
针对日常开发的组件二次封装、方案设计实现。包括对el-table的动态渲染、单元格编辑;对于无缝滚动的实现,优化大数据量下的页面卡顿问题。
1. el-table
实现动态渲染列
常规使用el-table
<template>
<el-table
ref="multipleTable"
:data="data"
>
<el-table-column prop="family_name" label="姓名" align="center">
</el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button
@click="handleEdit(scope.row)"
type="text"
size="small"
>用户信息</el-button
>
</template>
</el-table-column>
</el-table>
</template>
<script>
export default {
data(){
return {
data:[]
}
},
methods:{
handleEdit(){}
}
}
</script>
表格比较长时,需要些好多的el-table-column
;所以想通过动态渲染的方式循环渲染出列项。
官方给出的formatter
格式化列项输出的方法只能格式化文本。无法渲染VNode。
尝试通过v-html
绑定,报错h is not a function
// ...
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<div v-html="item.render(scope)"></div>
</template>
</el-table-column>
解决办法,通过render
方法提供CreateElement
函数。新创建一个组件RenderColumn
RenderColumn.vue
<script>
export default {
props:{
render:{
type:Function,
default:()=>()=>{}
},
scope:{
type:Object,
default:()=>{}
}
},
render(h){
const { row, column, $index } = this.scope
return this.render(h,row,column,$index)
}
}
</script>
在渲染表格时调用,主要在于需要给render
方法传入CreateElement
方法。
<template>
<el-table
ref="multipleTable"
:data="data"
>
<el-table-column v-for="item in columns" :label="item.lable" :prop="item.prop">
<template slot-scope="scope">
<render-column v-if="item.render" :render="item.render" :scope="scope" />
<span v-else>{{scope.row[item.prop]}}</span>
</template>
</el-table-column>
</el-table>
</template>
<script>
export default {
data(){
let $this = this
return {
data:[],
columns:[
{
label:'姓名',
prop:'name'
},
{
label:'操作',
render(h,row,column){
return <el-button
onClick={$this.handleEdit(row)}
type="text"
size="small"
>用户信息</el-button>
}
}
]
}
},
methods:{
handleEdit(){}
}
}
</script>
vue-cli
脚手架已经继承了JSX的语法,可以直接书写。
2. el-table
实现单元格的编辑
实现单元格的编辑,实现编辑组件EditCell.vue
逻辑的核心点:
-
非编辑状态下,展示当前列项值,通过点击事件,单元格进入可编辑状态。并可通过
this.$refs.input.focus()
聚焦 -
数据
el-input
主要在于处理完成输入、enter键后完成编辑状态。 -
当完成编辑时,如果传入了校验函数。则需调用函数进行校验。并通过
el-popover
展示。
<template>
<div class="edit-cell">
<el-popover :value="validateMsg !== ''" trigger="manual">
<div slot="reference">
<span v-if="!editable" @click="handleEditable"
>{{ editValue }}
<i class="el-icon-edit"></i>
</span>
<el-input
ref="input"
autofocus
v-else
v-model="editValue"
@change="handleEditable"
@blur="handleEditable"
/>
</div>
<span style="color: #f5222d">{{ validateMsg }}</span>
</el-popover>
</div>
</template>
<script>
export default {
name: "edit-cell",
props: {
value: String,
validator: {
type: Function,
default: () => null
}
},
data() {
return {
editValue: "",
editable: false,
validateMsg: ""
};
},
mounted() {
this.editValue = this.value;
},
methods: {
handleEditable() {
if (this.editable && typeof this.validator === "function") {
const err = this.validator(this.editValue);
if (err) {
this.validateMsg = err;
return;
}
this.validateMsg = "";
}
this.editable = !this.editable;
if (this.editable) {
this.$nextTick(() => {
this.$refs.input.focus();
});
}
this.$emit("change", this.editValue);
}
}
};
</script>
如果要实现整行row
的编辑,可给每一行数据追加属性editable
,整合编辑时更改属性,切换为编辑状态。
切入编辑状态el-input
本来想通过autofocus
获取焦点的。但没有用,使用了ref组件内部的方法。
3. 实现虚拟无缝滚动seamlessScroll
使用过vue-seamless-scroll
,可实现数据的无缝滚动。但当数据量超过大几千时,页面就会变的很卡。通过看源代码实现,加入5000的数据量,需要渲染10000个DOM节点。
通过之前虚拟列表的思想,实现一个虚拟无缝滚动组件
实现滚动的主要API
-
transform:translate(0px,0px)
,在水平、垂直方向上进行平移数据列表 -
window.requestAnimationFrame(()=>{})
在浏览器下次重绘时调用回调函数,通常为60次/s
实现的主要逻辑:
-
组件挂载或者数据
data
变化时进行数据初始化init()
-
init
方法用于调用数据切割滚动方法。其中一个参数virtual
用于显示控制如果数据量不大时,就没必要虚拟滚动了。 -
在
move
方法中,通过每一帧的渲染更新,回调函数处理this.translateY -= this.speed
平移数据列表。 -
在
splitData
中则处理数据切割,判断如果不需要虚拟滚动时,则加载展示所有的数据。 -
随后监听了
translateY
的变化,用于处理虚拟列表的滚动分页逻辑/** * 如果平移的距离大于分页*每项的长度,进行数据滚动重置 **/ handleDataScorll() { if ( Math.abs(this.translateY) < this.pageOptions.pageSize * this.itemWidth ) { return; } // this.stop(); // 第一页已滚动完成 if (this.virtual) { this.splitData(); } this.translateY = 0; },
核心的JS逻辑,实现的相关方法。
export default {
// ...
mounted() {
// 复制数据,数据仓
this.copyData = [...this.data];
// 切割数据
this.init();
},
computed: {
boxStyle() {
return {
transform: `translate(0, ${this.translateY}px )`,
};
},
total() {
return this.data.length;
},
},
watch: {
data(newData) {
this.copyData = [...newData];
this.init();
},
translateY() {
this.handleDataScorll();
},
},
methods: {
init() {
if (!this.virtual) {
// 非虚拟列表管滚动,则直接展示所有数据
this.pageOptions.pageSize = this.total;
}
if (this.total > 0) {
this.splitData();
this.move();
}
},
move() {
this.stop();
this.animationFrame = requestAnimationFrame(() => {
if (this.total > 0) {
this.translateY -= this.speed;
}
this.move();
});
},
splitData() {
if (!this.virtual) {
this.preData = [...this.copyData];
this.nextData = [...this.copyData];
return;
}
// 只有在虚拟列表时,才调换数据位置
this.copyData = [...this.copyData, ...this.preData];
// pre
this.preData = this.copyData.splice(0, this.pageOptions.pageSize);
// next
this.nextData = this.copyData.slice(0, this.pageOptions.pageSize);
},
/**
* 监听滚动的距离
*/
handleDataScorll() {
if (
Math.abs(this.translateY) <
this.pageOptions.pageSize * this.itemWidth
) {
return;
}
// this.stop();
// 第一页已滚动完成
if (this.virtual) {
this.splitData();
}
this.translateY = 0;
},
stop() {
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
}
}
},
};
示例中仅实现竖向滚动,横向滚动后续会追加props属性mode
进行逻辑处理。
4. 通过el-select
实现联级选择
Element提供的Cascader,但设计师可能需要的是并排的多个下拉,进行控制。
主要的实现逻辑:
-
通过level指定联动选择的层级数量。通过循环渲染出
el-select
, -
还有最关键的实现分级数据, 从data中分级出每一级level数据。视图中则通过
optionsData[index]
获取数据
optionsData: function () {
let arr = [[...this.data]]
for (let id of this.selectedData) {
if (!id) {
arr.push([])
break
}
let data = arr[arr.length - 1].find((item) => item.id === id)
if (!data) {
arr.push([])
break
}
arr.push(data.children || [])
}
return arr
}
-
最重要的是保证
selectedData
为层级深度长度的数组,这样才能渲染出正确数量的el-select -
每一层级的事件
change
通过index来更新选中的数据selelctData
<template>
<div class="cascade-select-city">
<el-select
placeholder="请选择"
v-for="(val, index) in selectedData"
:key="index"
:value="selectedData[index]"
@change="handleSelect($event, index)"
>
<el-option value="">请选择</el-option>
<el-option
v-for="item in optionsData[index]"
:key="item.id"
:label="item.name"
:value="item.name"
/>
</el-select>
</div>
</template>
<script>
export default {
name: 'cascade-select',
props: {
/**
* 用于自定义级联数据
*/
data: {
type: Array,
default: () => []
},
/**
* 联动层级数量
*/
level: {
type: Number,
default: 1
},
/**
* 绑定数据
*/
value: {
type: Array,
default: () => []
}
},
data () {
return {
selectedData: new Array(this.level).fill('')
}
},
mounted () {
},
watch: {
value (val, oldVal) {
if (JSON.stringify([val]) !== JSON.stringify([this.selectedData])) {
//
this.selectedData = [...val]
}
}
},
computed: {
/**
* 处理层级数据
*/
optionsData: function () {
let arr = [[...this.data]]
for (let id of this.selectedData) {
if (!id) {
arr.push([])
break
}
let data = arr[arr.length - 1].find((item) => item.AreaId === id)
if (!data) {
arr.push([])
break
}
arr.push(data.children || [])
}
return arr
}
},
methods: {
/**
* 处理联动的select
*/
handleSelect (selected, level) {
// 更新值
this.selectedData = this.selectedData.map((val, index) => {
if (index < level) {
return val
}
return index === level ? selected : ''
})
this.$emit('change', this.selectedData)
}
}
}
</script>
组件仅实现了data为静态数据时的逻辑处理,如果数据是动态的呢,比如异步加载联动数据。
后续会开放一些日常开发封装的公共组件。地址暂时不可访问Fun-UI