实现虚拟表格
思路
通过插入表格中wrapEl撑起总数据的表格高度,实现滚动条。innerEl中包括需要动态显示的数据行,通过translateY来实现动态显示数据。
核心代码:
js
// 创建wrapEl、innerEl
if (!el.wrapEl) {
const wrapEl = document.createElement('div')
const innerEl = document.createElement('div')
wrapEl.appendChild(innerEl)
innerEl.appendChild(el.children[0])
el.insertBefore(wrapEl, el.firstChild)
el.wrapEl = wrapEl
el.innerEl = innerEl
// 修复 fixed 动态切换时,固定列不显示(scrollTop位置和非固定列滚动位置不一致导致的,需要同步scrollTop的值)
if (index > 0 && tableWrapEl) {
this.$nextTick(() => {
el.scrollTop = tableWrapEl.scrollTop
})
}
}
index === 0 && (tableWrapEl = el) // 记录非固定列的dom
if (el.wrapEl) {
// 设置高度
el.wrapEl.style.height = `${wrapHeight }px`
// 设置transform撑起高度
el.innerEl.style.transform = `translateY(${offsetTop}px)`
// 设置paddingTop撑起高度
// el.innerEl.style.paddingTop = `${offsetTop}px`
}
简化思路案例:
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.container {
width: 500px;
height: 500px;
background-color: #f39494;
overflow: scroll;
}
.box{
border: 1px solid #000;
width: 100px;
height: 1000px;
}
.inner{
background-color: aqua;
}
</style>
</head>
<body>
<div class="container">
<div class="box">
<div class="inner">
<div>Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quod.</div>
</div>
</div>
</div>
<script>
const container = document.querySelector('.container');
const box = document.querySelector('.box');
const inner = document.querySelector('.inner');
let scrollTop = 0;
container.addEventListener('scroll', () => {
console.log('container scroll');
scrollTop = container.scrollTop;
inner.style.transform = `translateY(${scrollTop}px)`;
});
</script>
</body>
</html>
组件实现
vue
<template>
<div
class="el-table-virtual-scroll"
:class="[
isExpanding ? 'is-expanding' : '',
isHideAppend ? 'hide-append' : '',
scrollPosition ? `is-scrolling-${scrollPosition}` : '',
hasFixedRight ? 'has-custom-fixed-right' : '']">
<slot v-bind="{ headerCellFixedStyle, cellFixedStyle }" />
</div>
</template>
<script>
import throttle from 'lodash/throttle'
import {
isScroller,
getParentScroller,
getScrollTop,
getOffsetHeight,
scrollToY,
isEmpty,
setMousewheelSlow,
orderBy,
getColumnById,
} from './utils.js'
// 表格body class名称
const TableBodyClassNames = [
'.el-table__body-wrapper', // 主表格容器
'.el-table__fixed-right .el-table__fixed-body-wrapper', // 右固定表格容器
'.el-table__fixed .el-table__fixed-body-wrapper', // 左固定表格容器
]
export default {
name: 'VirtualTableScroll',
props: {
// 总数据
data: {
type: Array,
required: true,
},
// 每一行的预估高度
itemSize: {
type: Number,
default: 60,
},
// 指定滚动容器
scrollBox: {
type: String,
},
// 顶部和底部缓冲区域,值越大显示表格的行数越多
buffer: {
type: Number,
default: 200,
},
// key值,data数据中的唯一id
keyProp: {
type: String,
default: 'id',
},
// 滚动事件的节流时间
throttleTime: {
type: Number,
default: 16,
},
// 是否获取表格行动态高度
dynamic: {
type: Boolean,
default: true,
},
// 是否开启虚拟滚动
virtualized: {
type: Boolean,
default: true,
},
// 表格行合并时,合并在一起的行返回相同的key值
rowSpanKey: {
type: Function,
},
warn: {
type: Boolean,
default: true,
},
// 禁用虚拟滚动
disabled: {
type: Boolean,
default: false,
},
// 支持自定义选中数据的排序规则,传入false则可保留列表的排序规则,默认是按照选中顺序排序
selectionSort: {
type: [Function, Boolean],
default: true,
},
// 获取el-table组件,默认 virtual-scroll 组件的第一个子组件
getElTable: {
type: Function,
default () {
return this.$children[0]
},
},
keepScroll: {
type: Boolean,
default: true,
},
},
provide () {
return {
virtualScroll: this,
}
},
data () {
return {
sizes: {}, // 尺寸映射(依赖响应式)
start: 0, // 渲染列表开始索引
end: undefined, // 渲染列表结束索引
curRow: null, // 表格单选:选中的行
oldSelection: [], // 表格多选:选中的行
isExpanding: false, // 列是否正在展开
columnVms: [], // virtual-column 组件实例
isHideAppend: false, // 是否隐藏append
scrollPosition: '', // x轴滚动位置(左、中、右)
hasFixedRight: false, // 是否有固定右边的列
listData: [], // 未筛选为data源数据,筛选后则为筛选后的数据
isTree: false, // 是否自定义树形表格
}
},
computed: {
// 计算出每个item(的key值)到滚动容器顶部的距离
offsetMap ({ keyProp, itemSize, sizes, listData }) {
if (!this.dynamic) return {}
const res = {}
let total = 0
for (let i = 0; i < listData.length; i++) {
const key = listData[i][keyProp]
if (typeof key === 'undefined') {
this.warn && console.warn(`data[${i}][${keyProp}] 为 undefined,请确保 keyProp 对应的值不为undefined`)
}
res[key] = total
const curSize = sizes[key]
const size = typeof curSize === 'number' ? curSize : itemSize
total += size
}
return res
},
// 树节点的 children 映射,通过响应式关联起来,那么children中添加、删除节点会触发treeMap computed,从而监听treeMap更新视图【注:children 添加删除不会触发data watch,data只是浅监听】
treeMap ({ data, keyProp, treeProps, isTree }) {
if (!isTree || !treeProps) return
const res = {}
const { children } = treeProps
const traverse = (nodes) => {
nodes.forEach((node) => {
const key = node[keyProp]
if (typeof key !== 'undefined' && node[children]) {
res[key] = node[children]
traverse(node[children])
}
})
}
// 开始遍历树结构
traverse(data)
return res
},
},
watch: {
data (data, oldData) {
this.listData = data
if (this.list && data !== oldData) {
this.list = data
}
// 筛选数据后会调用update更新视图
this.onFilterChange && this.onFilterChange()
if (this.isReserveSelection()) {
// 保留旧的选中值
this.updateSelectionByRowKey(data, oldData)
} else {
// 对比新旧值,移除删除的选中值
this.updateSelectionData(data, oldData)
}
},
virtualized: {
immediate: true,
handler () {
// 筛选数据后会调用update更新视图
this.onFilterChange && this.onFilterChange()
},
},
disabled () {
this.doUpdate()
},
treeMap () {
this.update()
},
},
created () {
this.$nextTick(() => {
this.initData()
})
},
activated () {
this.isDeactivated = false
this.elTable?.fit === false && this.restoreScroll()
},
deactivated () {
this.isDeactivated = true
},
beforeDestroy () {
this.destory()
},
methods: {
// 初始化数据
initData () {
this.destory() // 销毁,防止多次调用
// 可视范围内显示数据
this.renderData = []
// 页面可视范围顶端、底部
this.top = undefined
this.bottom = undefined
// 截取页面可视范围内显示数据的开始和结尾索引
this.start = 0
this.end = undefined
// 是否是表格内部滚动
this.isInnerScroll = false
// 高亮的行
this.highlightRow = null
// 滚动位置
this.scrollPos = [0, 0]
// 触发scroll
this.triggleScroll = false
// 多选:记录多选选项的顺序
this.checkOrder = 0
// 验证ElTable组件
this.elTable = this.getElTable()
// if (!this.elTable || this.elTable.$options.name !== 'ElTable' || this.elTable.$options.name !== 'PureTable') {
// throw new Error('未找到 <el-table> 组件. 请确保 <el-table> 组件在虚拟组件内,且 getElTable 方法能获取到正确的 <el-table> 组件!')
// }
if (!this.elTable.rowKey) {
this.warn && console.warn('[el-table-virtual-scroll]: 建议设置 <el-table> 组件的 rowKey 属性')
}
this.scroller = this.getScroller()
this.observeElTable()
// 监听事件
this.onScroll = !this.throttleTime ? this.handleScroll : throttle(this.handleScroll, this.throttleTime)
this.scroller.addEventListener('scroll', this.onScroll)
window.addEventListener('resize', this.onScroll)
// 兼容
this.hackTableExpand() // 兼容表格展开行
this.hackTableHeaderDrag() // 兼容表格头拖拽
this.hackTableSort() // 兼容表格排序
this.hackTableFilter() // 兼容表格筛选
this.hackRowHighlight() // 兼容单选
this.hackSelection() // 兼容多选
this.hackCustomTree() // 兼容树形表格
this.bindTableDestory() // 绑定表格销毁事件
console.log(this.elTable )
// 设置listData,首次updateTreeData会在node上添加$v_tree属性,触发data watch,从而触发 onSortChange,所以defaultSort就无需再次触发
this.treeProps = this.elTable.treeProps || { children: 'children', hasChildren: 'hasChildren' }
// 此处兼容 default-sort 属性
if (this.elTable.defaultSort) {
this.$nextTick(() => { // 此处使用nextTick是因为 el-tale的sortingColumn排序数据还没设置好,得等一会
this.onSortChange() // onSortChange 会触发updateTreeData
})
} else {
this.updateTreeData()
}
// 初次执行 (固定高度的表格布局好后,会触发 bodyHeight 更改(已手动监听,位于 unWatch2代码处),从而触发 onScroll,所以无需手动执行onScroll)
setTimeout(() => {
!this.triggleScroll && this.onScroll()
}, 100)
},
// 滚轮滚动速度减缓,减少快速滚动白屏
// slowNum - 减速的值,值越大,滚动越慢
slowOnMousewheel (slowNum = 1, scroller = this.scroller) {
this.removeMousewheelEvent && this.removeMousewheelEvent()
this.removeMousewheelEvent = null
if (!slowNum) return
this.removeMousewheelEvent = setMousewheelSlow(scroller, slowNum)
},
// 获取滚动元素
getScroller () {
let el
if (this.scrollBox) {
if (this.scrollBox === 'window' || this.scrollBox === window) return window
el = document.querySelector(this.scrollBox)
if (!el) throw new Error(` scrollBox prop: '${this.scrollBox}' is not a valid selector`)
if (!isScroller(el)) console.warn(`Warning! scrollBox prop: '${this.scrollBox}' is not a scroll element`)
return el
}
// 如果表格是固定高度,则获取表格内的滚动节点,否则获取父层滚动节点
if (
this.elTable &&
(this.elTable.height || this.elTable.maxHeight || this.elTable.height === 0 || this.elTable.maxHeight === 0)
) {
this.isInnerScroll = true
return this.$el.querySelector('.el-table__body-wrapper')
}
return getParentScroller(this.$el)
},
// 设置表格到滚动容器的距离
getToTop () {
if (this.isInnerScroll) {
return 0
}
return this.$el.getBoundingClientRect().top - (this.scroller === window
? 0 : this.scroller.getBoundingClientRect().top) + getScrollTop(this.scroller)
},
// 处理滚动事件
handleScroll (shouldUpdate = true) {
if (this.disabled) return
if (!this.scroller) return
this.triggleScroll = true
// 【修复】如果使用v-show 进行切换表格会特别卡顿 #30;
// 【原因】v-show为false时,表格内滚动容器的高度为auto,没有滚动条限制,虚拟滚动计算渲染全部内容
if (this.isInnerScroll && !this.scroller.style.height && !this.scroller.style.maxHeight) return
// 如果组件失活,则不再执行handleScroll;否则外部容器滚动情况下记录的scrollTop会是0
if (this.isDeactivated) return
// 记录scrollPos
// 需要判断表格没有隐藏(修复表格隐藏状态下更新绑定数组长度,显示后滚动条位置异常 #67)
if (this.isInnerScroll && this.elTable.layout.bodyHeight) {
this.scrollPos[0] = this.scroller.scrollTop
this.scrollPos[1] = this.scroller.scrollLeft
}
if (!this.virtualized) return
this.removeHoverRows()
// 更新当前尺寸(高度)
this.updateSizes()
// 计算renderData
this.calcRenderData()
// 计算位置
this.calcPosition()
shouldUpdate && this.updatePosition()
// 触发事件
this.$emit('change', this.renderData, this.start, this.end)
// 同步表格行高亮
this.syncRowsHighlight()
},
// 移除多个hover-row
removeHoverRows () {
const hoverRows = this.$el.querySelectorAll('.el-table__row.hover-row')
if (hoverRows.length > 1) {
Array.from(hoverRows).forEach((row) => {
row.classList.remove('hover-row')
})
}
},
// 更新尺寸(高度)
updateSizes () {
if (!this.dynamic) return
let rows = this.$el.querySelectorAll('.el-table__body > tbody > .el-table__row')
// 处理树形表格(修复树结构懒加载 如果有hasChildren=false的行 行可视区域高度异常 #45)
const isTree = this.elTable.lazy
const isVTree = this.isTree // 自定义树(非el-table的树)
const noFirstLevelReg = /el-table__row--level-[1-9]\d*/ // 匹配树形表格非一级行
if (!isVTree && isTree) {
// 筛选出树形表格的一级行,一级行className含有el-table__row--level-0或者不存在层级className
rows = Array.from(this.$el.querySelectorAll('.el-table__body > tbody > .el-table__row')).filter((row) => {
return !noFirstLevelReg.test(row.className)
})
}
Array.from(rows).forEach((row, index) => {
const item = this.renderData[index]
if (!item) return
// 计算表格行的高度
let offsetHeight = row.offsetHeight
// 表格行如果有扩展行,需要加上扩展内容的高度
if (!isTree && !isVTree && row.classList.contains('expanded')) {
offsetHeight += row.nextSibling.offsetHeight
}
// 表格行如果有子孙节点,需要加上子孙节点的高度
if (isTree) {
let next = row.nextSibling
while (next && next.tagName === 'TR' && noFirstLevelReg.test(next.className)) {
offsetHeight += next.offsetHeight
next = next.nextSibling
}
}
const key = item[this.keyProp]
if (offsetHeight && this.sizes[key] !== offsetHeight) {
this.$set(this.sizes, key, offsetHeight)
}
})
},
// 获取某条数据offsetTop
getItemOffsetTop (index) {
if (!this.dynamic) {
return this.itemSize * index
}
const item = this.listData[index]
if (item) {
return this.offsetMap[item[this.keyProp]] || 0
}
return 0
},
// 获取某条数据的尺寸
getItemSize (index) {
if (index <= -1) return 0
const item = this.listData[index]
if (item) {
const key = item[this.keyProp]
return this.sizes[key] || this.itemSize
}
return this.itemSize
},
// 计算只在视图上渲染的数据
calcRenderData () {
const { scroller, listData, buffer } = this
// 计算可视范围顶部、底部
const toTop = this.getToTop() // 表格到滚动容器的距离
const top = getScrollTop(scroller) - buffer - toTop
const bottom = getScrollTop(scroller) + getOffsetHeight(scroller) + buffer - toTop
let start
let end
if (!this.dynamic) {
start = top <= 0 ? 0 : Math.floor(top / this.itemSize)
end = bottom <= 0 ? 0 : Math.ceil(bottom / this.itemSize)
} else {
// 二分法计算可视范围内的开始的第一个内容
let l = 0
let r = listData.length - 1
let mid = 0
while (l <= r) {
mid = Math.floor((l + r) / 2)
const midVal = this.getItemOffsetTop(mid)
if (midVal < top) {
const midNextVal = this.getItemOffsetTop(mid + 1)
if (midNextVal > top) break
l = mid + 1
} else {
r = mid - 1
}
}
start = mid
// 二分法计算可视范围内的结束的最后一个内容
l = start
r = listData.length - 1
mid = 0
while (l <= r) {
mid = Math.floor((l + r) / 2)
const midVal = this.getItemOffsetTop(mid)
if (midVal >= bottom) {
const midNextVal = this.getItemOffsetTop(mid - 1)
if (midNextVal < bottom) break
r = mid - 1
} else {
l = mid + 1
}
}
end = mid
}
if (this.isRowSpan()) {
// 计算包含合并行的开始结束区间(⚠️注意:合并行不支持使用斑马纹,因为不能100%确定合并行的开始行是偶数,可能会向上找一直到第一行,导致渲染非常多行,浪费性能)
[start, end] = this.calcRenderSpanData(start, end)
} else {
// 开始索引始终保持偶数,如果为奇数,则加1使其保持偶数【确保表格行的偶数数一致,不会导致斑马纹乱序显示】
if (start % 2) start = start - 1
}
this.top = top
this.bottom = bottom
this.start = start
this.end = end
this.renderData = listData.slice(start, end + 1)
if (this.start === 0 && this.end > 30 && this.end === this.listData.length - 1) {
this.warn && console.warn(`[el-table-virtual-scroll] 表格数据全部渲染,渲染数量为:${ this.listData.length}`)
}
},
// 是否是合并行
isRowSpan () {
return typeof this.rowSpanKey === 'function'
},
// 如果存在合并行的情况,渲染的数据范围扩大到包含合并行
calcRenderSpanData (start, end) {
// 从开始节点向上查找是否有合并行
let prevKey
while (start > 0) {
const curRow = this.listData[start]
const curkey = this.rowSpanKey(curRow, start)
// 如果不存在key,说明当前行不属于合并行
if (isEmpty(curkey)) break
// 如果当前行与后面一行的key不相同,说明则当前行不属于合并行,从后一行开始截断
if (!isEmpty(prevKey) && prevKey !== curkey) {
start++
break
}
prevKey = curkey
start--
}
// 从末端节点向下查找是否有合并行
const len = this.listData.length
prevKey = undefined
while (end < len) {
const curRow = this.listData[end]
const curkey = this.rowSpanKey(curRow, end)
// 如果不存在key,说明当前行不属于合并行
if (!curkey) break
// 如果当前行与前面一行的key不相同,说明则当前行不属于合并行,从前一行开始截断
if (prevKey && prevKey !== curkey) {
end--
break
}
prevKey = curkey
end++
}
return [start, end]
},
// 计算位置
calcPosition () {
const last = this.listData.length - 1
// 计算内容总高度
const wrapHeight = this.getItemOffsetTop(last) + this.getItemSize(last)
// 计算当前滚动位置需要撑起的高度
const offsetTop = this.getItemOffsetTop(this.start)
let tableWrapEl
// 设置dom位置
TableBodyClassNames.forEach((className, index) => {
const el = this.$el.querySelector(className)
if (!el) return
// 创建wrapEl、innerEl
if (!el.wrapEl) {
const wrapEl = document.createElement('div')
const innerEl = document.createElement('div')
wrapEl.appendChild(innerEl)
innerEl.appendChild(el.children[0])
el.insertBefore(wrapEl, el.firstChild)
el.wrapEl = wrapEl
el.innerEl = innerEl
// 修复 fixed 动态切换时,固定列不显示(scrollTop位置和非固定列滚动位置不一致导致的,需要同步scrollTop的值)
if (index > 0 && tableWrapEl) {
this.$nextTick(() => {
el.scrollTop = tableWrapEl.scrollTop
})
}
}
index === 0 && (tableWrapEl = el) // 记录非固定列的dom
if (el.wrapEl) {
// 设置高度
el.wrapEl.style.height = `${wrapHeight }px`
// 设置transform撑起高度
el.innerEl.style.transform = `translateY(${offsetTop}px)`
// 设置paddingTop撑起高度
// el.innerEl.style.paddingTop = `${offsetTop}px`
}
})
},
// 监听el-table
observeElTable () {
// 监听滚动位置
const unWatch1 = this.$watch(
() => [this.elTable.scrollPosition, this.elTable.layout.scrollX],
([pos, scrollX], [oldPos, oldScrollX] = []) => {
// 修复自定义固定列 所有列宽总宽度小于表格宽度时 固定列样式有问题 #65
this.scrollPosition = this.elTable.layout.scrollX ? pos : 'none'
// 修复element-ui原有bug:当窗口缩放时,x轴滚动条从无到到有,且x轴已滚动到最右侧,右侧固定列
if (scrollX && !oldScrollX) {
this.elTable.syncPostion && this.elTable.syncPostion()
}
}, { immediate: true },
)
// 监听表格滚动高度变化(切换v-show时更新)
const unWatch2 = this.$watch(() => this.elTable.layout.bodyHeight, (val) => {
val > 0 && this.restoreScroll()
val > 0 && this.onScroll()
})
this.unWatchs = [unWatch1, unWatch2]
},
// 执行update方法更新虚拟滚动,且每次nextTick只能执行一次【在数据大于100条开启虚拟滚动时,由于监听了data、virtualized会连续触发两次update方法:第一次update时,(updateSize)计算尺寸里的渲染数据(renderData)与表格行的dom是一一对应,之后会改变渲染数据(renderData)的值;而第二次执行update时,renderData改变了,而表格行dom未改变,导致renderData与dom不一一对应,从而位置计算错误,最终渲染的数据对应不上。因此使用每次nextTick只能执行一次来避免bug发生】
doUpdate () {
if (this.hasDoUpdate) return // nextTick内已经执行过一次就不执行
if (!this.scroller) return // scroller不存在说明未初始化完成,不执行
// 启动虚拟滚动的瞬间,需要暂时隐藏el-table__append-wrapper里的内容,不然会导致滚动位置一直到append的内容处
this.isHideAppend = true
this.onScroll()
this.hasDoUpdate = true
this.$nextTick(() => {
this.hasDoUpdate = false
this.isHideAppend = false
})
},
// 空闲时更新位置(触发时间:滚动停止后等待10ms执行)
// 场景:固定表格fixed变化时、扩展行展开滑动时需要更新
updatePosition () {
this.timer && clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.timer && clearTimeout(this.timer)
// 传入false,避免一直循环调用
this.handleScroll(false)
}, this.throttleTime + 10)
},
// 渲染全部数据
renderAllData () {
this.renderData = this.listData
this.$emit('change', this.listData, 0, this.listData.length - 1)
console.log('renderAllData', this.listData)
this.$nextTick(() => {
// 清除撑起的高度和位置
TableBodyClassNames.forEach((className) => {
const el = this.$el.querySelector(className)
if (!el) return
if (el.wrapEl) {
// 设置高度
el.wrapEl.style.height = 'auto'
// 设置transform撑起高度
el.innerEl.style.transform = `translateY(${0}px)`
}
})
})
},
// 恢复滚动位置(仅支持表格内部滚动)
restoreScroll () {
if (!this.scroller || !this.isInnerScroll) return
this.scroller.scrollLeft = this.keepScroll ? this.scrollPos[1] : 0
this.scroller.scrollTop = this.keepScroll ? this.scrollPos[0] : 0
},
// 【外部调用】更新
update () {
if (this.isTree) {
this.onFilterChange && this.onFilterChange()
}
// console.log('update')
this.handleScroll()
},
// 【外部调用】滚动到第几行
// (不太精确:滚动到第n行时,如果周围的表格行计算出真实高度后会更新高度,导致内容坍塌或撑起)
// offsetY - 偏移量
scrollTo (index, offsetY = 0, stop = false) {
const item = this.listData[index]
if (item && this.scroller) {
this.updateSizes()
this.calcRenderData()
this.$nextTick(() => {
const offsetTop = this.getItemOffsetTop(index) - offsetY
scrollToY(this.scroller, offsetTop)
// 调用两次scrollTo,第一次滚动时,如果表格行初次渲染高度发生变化时,会导致滚动位置有偏差,此时需要第二次执行滚动,确保滚动位置无误
if (!stop) {
setTimeout(() => {
this.scrollTo(index, offsetY, true)
}, 50)
}
})
}
},
// 【外部调用】滚动到对应的行
scrollToRow (row, offsetY = 0) {
const index = this.listData.findIndex((item) => item === row || item[this.keyProp] === row[this.keyProp])
this.scrollTo(index, offsetY)
},
// 【外部调用】重置 (没用废弃)
reset () {
this.sizes = {}
this.scrollTo(0, 0, false)
},
// 销毁
destory () {
if (this.scroller) {
this.scroller.removeEventListener('scroll', this.onScroll)
window.removeEventListener('resize', this.onScroll)
}
if (this.unWatchs) {
this.unWatchs.forEach((unWatch) => unWatch())
}
if (this.removeMousewheelEvent) {
this.removeMousewheelEvent()
}
this.oldSelection = []
this.elTable = null
this.scroller = null
this.unWatchs = []
},
// 【VirtualColumn调用】获取列表全部数据】
// origin - 源数据,非筛选后的数据
getData (origin = true) {
return this.list || (origin ? this.data : this.listData)
},
// 【VirtualColumn调用】添加virtual-column实例
addColumn (vm) {
this.columnVms.push(vm)
},
// 【VirtualColumn调用】移除virtual-column实例
removeColumn (vm) {
this.columnVms = this.columnVms.filter((item) => item !== vm)
},
// 【多选】选中所有列
checkAll (val, rows = this.listData, byUser = false) {
const removedRows = []
rows.forEach((row) => {
if (row.$v_checked) {
removedRows.push(row)
}
if (row.$v_checked !== val) {
this.$set(row, '$v_checked', val)
this.$set(row, '$v_checkedOrder', val ? this.checkOrder++ : undefined)
}
})
const selection = this.emitSelectionChange(removedRows)
if (byUser) { // 当用户手动勾选全选 Checkbox 时触发的事件
this.$emit('select-all', selection, val)
this.elTable.$emit('select-all', selection, val)
}
if (val === false) {
this.checkOrder = 0 // 取消全选,则重置checkOrder
}
},
// 【多选】选中某一列
checkRow (row, val, emit = true, byUser = false) {
if (row.$v_checked === val) return
this.$set(row, '$v_checked', val)
this.$set(row, '$v_checkedOrder', val ? this.checkOrder++ : undefined)
if (emit) {
const selection = this.emitSelectionChange(val ? [] : [row])
if (byUser) { // 当用户手动勾选数据行的 Checkbox 时触发的事件
this.$emit('select', selection, row, val)
this.elTable.$emit('select', selection, row, val)
}
}
},
// 【多选】兼容表格clearSelection方法
clearSelection () {
// 清除旧的选中项
this.oldSelection.forEach((row) => {
this.$set(row, '$v_checked', false)
})
this.oldSelection = []
// 清除所有选中项
this.checkAll(false)
this.columnVms.forEach((vm) => vm.syncCheckStatus())
},
// 【多选】兼容表格toggleRowSelection方法
toggleRowSelection (row, selected) {
if (!Array.isArray(row)) {
row = [row]
}
this.toggleRowsSelection(row, selected)
},
// 【扩展多选】表格切换多个row选中状态
toggleRowsSelection (rows, selected) {
// reserve-selection 模式用到的变量
const oldSelectedMap = {} // 保留值map(旧的选中值)
const curSelectedMap = {} // 当前选中值map
let toDeleteMap = null // 需删除值map
const isReserve = this.isReserveSelection()
if (isReserve) {
this.oldSelection.forEach((row) => {
oldSelectedMap[row[this.keyProp]] = true
})
this.data.forEach((row) => {
curSelectedMap[row[this.keyProp]] = true
})
}
const removedRows = []
rows.forEach((row) => {
const val = typeof selected === 'boolean' ? selected : !row.$v_checked
!val && removedRows.push(row)
this.$set(row, '$v_checked', val)
this.$set(row, '$v_checkedOrder', val ? this.checkOrder++ : undefined)
if (!isReserve) return
/* 处理reserve-selection 模式 */
// 如果row在保留值oldSelection里,且取消选中,则需要将它移除
const key = row[this.keyProp]
if (key in oldSelectedMap && !val) {
if (!toDeleteMap) toDeleteMap = {}
toDeleteMap[key] = true
}
// 如果row不在当前列表里,且为选中,则需要添加在保留值oldSelection里
if (!(key in curSelectedMap) && val) {
this.oldSelection.push(row)
}
})
if (toDeleteMap) {
this.oldSelection = this.oldSelection.filter(
(row) => !(row[this.keyProp] in toDeleteMap),
)
}
this.emitSelectionChange(removedRows)
this.columnVms.forEach((vm) => vm.syncCheckStatus())
},
// 【多选】兼容表格selection-change事件
emitSelectionChange (removedRows) {
const isReserve = this.isReserveSelection() // 是否保留旧的值
const selection = isReserve ? [...this.oldSelection] : []
this.data.forEach((row) => {
if (row.$v_checked) {
selection.push(row)
}
})
this.sortSelection(selection)
this.$emit('selection-change', selection, removedRows)
this.elTable.$emit('selection-change', selection, removedRows)
// 对于 reserve-selection 模式,oldSelection始终保留旧的选中值,不保留当前选中值
// 对于 非 reserve-selection 模式,oldSelection始终保留当前选中值
if (!isReserve) {
this.oldSelection = [...selection]
}
return selection
},
// 【多选】兼容表格 reserve-selection,存储上次选中的值
updateSelectionByRowKey (data, oldData = []) {
if (!this.elTable) return
// 将旧的选中值存储到oldSelection中
oldData.forEach((row) => {
if (row.$v_checked) {
this.oldSelection.push(row)
}
})
const selectedMap = {}
this.oldSelection.forEach((row) => {
selectedMap[row[this.keyProp]] = true
})
const usedMap = {} // 当前选中值的map
data.forEach((row) => {
const key = row[this.keyProp]
if (key in selectedMap) {
this.$set(row, '$v_checked', true)
usedMap[key] = true
}
})
// 从oldSelection中移除当前选中的值
this.oldSelection = this.oldSelection.filter((row) => !(row[this.keyProp] in usedMap))
},
// 【多选】多选列是否设置了 reserve-selection
isReserveSelection () {
return this.columnVms.some((vm) => vm.reserveSelection && vm.isSelection())
},
// 获取选中值
getSelection () {
if (this.isReserveSelection()) {
const curSelection = this.data.filter((row) => row.$v_checked)
return [...this.oldSelection, ...curSelection]
}
return this.oldSelection
},
// 【多选】更新多选的值
updateSelectionData (data, oldData) {
this.syncSelectionStatus()
if (data !== oldData) {
if (this.oldSelection.length > 0) {
// 修复多选select-change事件在表格数据更新后未触发,导致旧数据未清除 #100
this.$emit('selection-change', [], [...this.oldSelection])
this.elTable.$emit('selection-change', [], [...this.oldSelection])
this.oldSelection = []
}
return
}
// 新的选中项
const selection = this.data.filter((row) => row.$v_checked)
this.sortSelection(selection)
// 新的选中项key map
const selectionKeyMap = selection.reduce((map, dataItem) => {
map[dataItem[this.keyProp]] = true
return map
}, {})
// 移除的项
const removedRows = this.oldSelection.reduce((rows, row) => {
if (!(row[this.keyProp] in selectionKeyMap)) rows.push(row)
return rows
}, [])
// 手动删除选中项、新旧项不一致(正常不会发生),触发selection-change事件
if (removedRows.length || selection.length !== this.oldSelection.length) {
this.$emit('selection-change', selection, removedRows)
this.elTable.$emit('selection-change', selection, removedRows)
this.oldSelection = [...selection]
}
},
// 【多选】多选排序
sortSelection (selection) {
if (!this.selectionSort) return
if (typeof this.selectionSort === 'function') {
selection.sort((a, b) => this.selectionSort(a, b))
} else {
selection.sort((a, b) => a.$v_checkedOrder - b.$v_checkedOrder)
}
},
// 【多选】同步多选状态
syncSelectionStatus () {
const selectionVm = this.columnVms.find((vm) => vm.isSelection())
if (selectionVm) {
selectionVm.syncCheckStatus()
}
},
// 【radio单选】设置选中行
setCurrentRow (row) {
this.curRow = row
this.$emit('current-change', row)
this.elTable.$emit('current-change', row)
},
// 【单选高亮】兼容行高亮
hackRowHighlight () {
// 兼容el-table的setCurrentRow:重写setCurrentRow方法
if (this.elTable.setCurrentRow.virtual) {
const originSetCurrentRow = this.elTable.setCurrentRow.bind(this.elTable) // 原方法
const setCurrentRow = (row) => { // 重写setCurrentRow
this.elTable.store.states.currentRow = this.highlightRow // 同步表格行高亮的值
if (this.highlightRow !== row) this.highlightRow = row // 同步highlightRow的值
originSetCurrentRow(row) // 执行原方法
}
this.elTable.setCurrentRow = setCurrentRow
setCurrentRow.virtual = true
}
// 兼容el-table的currentRowKey属性
const unWatch = this.$watch(() => this.elTable.currentRowKey, (val) => {
if (this.elTable.rowKey) {
const targetRow = this.listData.find((row) => val === row[this.elTable.rowKey])
this.highlightRow = targetRow
}
}, { immediate: true })
this.unWatchs.push(unWatch)
// 监听高亮的事件
const onCurrentChange = (row) => {
this.highlightRow = row
}
this.elTable.$on('current-change', onCurrentChange)
this.unWatchs.push(() => {
this.elTable.$off('current-change', onCurrentChange)
})
},
// 【单选高亮】同步表格行高亮的值
syncRowsHighlight () {
if (!this.elTable.highlightCurrentRow) return
// 必须使用nextTick,不然值同步不上
this.$nextTick(() => {
this.elTable.store.states.currentRow = this.highlightRow
})
},
// 使用自定义的树形表格;需禁用原来的树(将children字段改为空)
useCustomSelection () {
this.isSelection = true
},
// 【多选高亮】兼容多选
hackSelection () {
// 兼容选中行高亮
const onFixedColumnsChange = () => {
if (!this.elTable) return
const tableBodyVm = this.elTable.$children.filter((vm) => {
return vm.$options.name === 'ElTableBody'
})
tableBodyVm.forEach((vm) => {
if (vm.getRowClass.virtual) return
// 重写 table-body组件的 getRowClass 方法
// 支持多选选中时高亮
const originGetRowClass = vm.getRowClass.bind(this.elTable)
vm.getRowClass = (row, rowIndex) => {
const classes = originGetRowClass(row, rowIndex)
if (this.elTable.highlightSelectionRow && row.$v_checked) {
classes.push('selection-row')
}
return classes
}
vm.getRowClass.virtual = true
})
}
const unWatch = this.$watch(
() => [this.elTable.fixedColumns, this.elTable.rightFixedColumns],
onFixedColumnsChange,
{ immediate: true },
)
this.unWatchs.push(unWatch)
// 兼容多选 toggleRowSelection 方法
this.elTable.toggleRowSelection = (row, selected) => {
this.toggleRowSelection(row, selected)
}
this.elTable.clearSelection = () => {
this.clearSelection()
}
// 【多选】兼容表格 toggleAllSelection 方法
this.elTable.toggleAllSelection = () => {
const selectionVm = this.columnVms.find((vm) => vm.isSelection())
if (selectionVm) {
selectionVm.onCheckAllRows(!selectionVm.isCheckedAll)
}
}
},
// 监听表格header-dragend事件
hackTableHeaderDrag () {
const onHeaderDragend = () => {
// 设置状态,用于自定义固定列
this.hasHeadDrag = true
// #50 修复el-table原bug: 刷新布局,列放大缩小让高度变大,导致布局错乱
this.elTable.doLayout()
// 修复某一行内容很多时,将该行宽度拖拽成很宽,内容坍塌导致空白行(需要立即更新,因为要获取新行变化的高度)
this.update()
}
this.elTable.$on('header-dragend', onHeaderDragend)
this.unWatchs.push(() => {
this.elTable.$off('header-dragend', onHeaderDragend)
})
},
// 【展开行】使用扩展行
useExpandTable () {
this.isExpandType = true
},
// 【展开行】监听表格expand-change事件
hackTableExpand () {
if (!this.isExpandType) return
const { store } = this.elTable
// 判断表格行是否渲染扩展行
store.isRowExpanded = (row) => {
return row.$v_expanded
}
// expandRowKeys 变化时候,设置展开的行
store.setExpandRowKeys = () => {
const { expandRowKeys } = this.elTable
this.listData.forEach((row) => {
this.$set(row, '$v_expanded', expandRowKeys.includes(row[this.elTable.rowKey]))
})
}
// 表格data变化时,更新展开的行
store.updateExpandRows = () => {
const { defaultExpandAll } = this.elTable
if (defaultExpandAll) {
// 当设置了默认展开所有行时,直接设置所有行展开
this.listData.forEach((row) => {
this.$set(row, '$v_expanded', true)
})
}
}
// 外部调动方法 toggleRowExpansion
store.toggleRowExpansion = (row, expanded) => {
const val = typeof expanded === 'boolean' ? expanded : !row.$v_expanded
if (val === Boolean(row.$v_expanded)) return
this.$set(row, '$v_expanded', val)
// 暂时不做排序,没必要
this.elTable.$emit('expand-change', row, this.listData.filter((row) => row.$v_expanded))
if (this.updateExpandeding) return
this.updateExpandeding = true
this.$nextTick(() => {
this.updateExpandeding = false
this.update()
})
}
// 重写expandRows的indexOf,使其能获取正确的展开状态
store.states.expandRows.indexOf = (row) => {
if (row) {
return row.$v_expanded || -1
}
return -1
}
},
// 【展开行/树】切换某一行的展开状态
toggleRowExpansion (row, expanded) {
this.elTable.toggleRowExpansion(row, expanded)
},
// 【自定义固定列】设置固定左右样式
headerCellFixedStyle (data) {
return this.cellFixedStyle(data, true)
},
// 【自定义固定列】设置固定左右样式
cellFixedStyle ({ column }, isHeader = false) {
const elTable = this.getElTable()
if (!elTable) return
// 右边固定列头部需要加上滚动条宽度-gutterWidth
const { gutterWidth: _gutterWidth, scrollY, bodyWidth } = elTable.layout
const gutterWidth = isHeader && scrollY ? _gutterWidth : 0
// 计算固定样式(当列宽度变化时重新计算,其余直接使用缓存值fixedMap)
if (!this.fixedMap || this._isScrollY !== scrollY || this._bodyWidth !== bodyWidth || this.hasHeadDrag) {
if (this.hasHeadDrag) this.hasHeadDrag = false
this._isScrollY = scrollY
this._bodyWidth = bodyWidth
this.fixedMap = {}
this.totalLeft = 0 // 左边固定定位累加值
this.totalRight = 0 // 右边固定定位累加值
const columns = elTable.columns
const rightColumns = [] // 右边固定列集合
let lastLeftColumn // 左边固定列的最后一列
let firstRightColumn // 右边固定列的第一列
for (let i = 0; i < columns.length; i++) {
const column = columns[i]
const isLeft = column.className && column.className.includes('virtual-column__fixed-left')
const isRight = column.className && column.className.includes('virtual-column__fixed-right')
if (!isLeft && !isRight) continue
// 设置左边固定列定位样式
if (isLeft) {
lastLeftColumn = column
this.fixedMap[column.id] = {
left: this.totalLeft,
}
this.totalLeft += column.realWidth || column.width
}
// 收集右边固定列
if (isRight) {
if (!firstRightColumn) firstRightColumn = column
rightColumns.push(column)
}
}
// 设置固定列阴影classname
const leftClass = ' is-last-column'
const rightClass = ' is-first-column'
// 设置左边、右边固定列class
if (lastLeftColumn && !lastLeftColumn.className.includes(leftClass)) lastLeftColumn.className += leftClass
if (firstRightColumn &&
!firstRightColumn.className.includes(rightClass)) firstRightColumn.className += rightClass
// 设置右边固定列定位样式(从右往左依次)
this.hasFixedRight = rightColumns.length > 0
rightColumns.reverse().forEach((column) => {
this.fixedMap[column.id] = {
right: this.totalRight,
}
this.totalRight += column.realWidth || column.width
})
}
const style = this.fixedMap[column.id]
if (!style) return
const isFixedRight = 'right' in style
const curStyle = isFixedRight ? { right: `${style.right + gutterWidth }px` } : { left: `${style.left }px` }
// 修复 #89 使用 <virtual-column> vfixed, 表尾合计行对应的列没有固定
if (elTable.showSummary) {
this.$nextTick(() => {
const footTh = this.$el.querySelector(`.el-table__footer-wrapper .${column.id}`)
if (footTh) {
if (curStyle.left) footTh.style.left = curStyle.left
if (curStyle.right) footTh.style.right = curStyle.right
}
})
}
return curStyle
},
// 【自定义固定列】更新表头布局
doHeaderLayout () {
if (!this.elTable) return
this.fixedMap = null
this.elTable.$refs.tableHeader.$forceUpdate()
},
// 绑定排序事件
hackTableSort () {
this.onSortChange = () => {
if (!this.elTable) return
const states = this.elTable.store.states
const { sortingColumn } = states
const data = this.filterData || this.data // 优先使用过滤后的数据进行排序
if (!sortingColumn || typeof sortingColumn.sortable === 'string') {
this.updateTreeData(data)
} else {
const listData = orderBy(data,
states.sortProp, states.sortOrder, sortingColumn.sortMethod, sortingColumn.sortBy)
this.updateTreeData(listData)
}
// 触发更新
this.doUpdate()
if (!this.virtualized) {
this.renderAllData()
}
this.$nextTick(() => {
this.syncSelectionStatus()
})
}
this.elTable.$on('sort-change', this.onSortChange)
this.unWatchs.push(() => {
this.elTable.$off('sort-change', this.onSortChange)
})
// clearSort不会触发sort-change,需要重写clearSort方法,手动触发
const originClearSort = this.elTable.clearSort.bind(this.elTable)
this.elTable.clearSort = (...rest) => {
originClearSort(...rest)
this.onSortChange()
}
},
// 绑定筛选事件
hackTableFilter () {
// 拦截el-table内部对树的筛选
if (this.isTree) {
this.$nextTick(() => {
this.elTable.store.execQuery = () => {
this.elTable.store.states.data = [...this.elTable.data]
}
})
}
this.onFilterChange = () => {
if (!this.elTable) return
const states = this.elTable.store.states
const { filters } = states
// 使用原数据进行数据过滤
let data = this.data
Object.keys(filters).forEach((columnId) => {
const values = states.filters[columnId]
if (!values || values.length === 0) return
const column = getColumnById(states, columnId)
if (column && column.filterMethod) {
data = data.filter((row) => {
return values.some((value) => column.filterMethod.call(null, value, row, column))
})
}
})
// 过滤完之后,手动执行排序
const hasFilter = this.data !== data
this.filterData = hasFilter ? data : null
this.onSortChange()
}
this.elTable.$on('filter-change', this.onFilterChange)
this.unWatchs.push(() => {
this.elTable.$off('filter-change', this.onFilterChange)
})
// clearFilter不会触发 filter-change,需要重写 clearFilter 方法,手动触发
const originClearFilter = this.elTable.clearFilter.bind(this.elTable)
this.elTable.clearFilter = (...rest) => {
originClearFilter(...rest)
this.onFilterChange()
}
// init filters
// 此处兼容列的 filtered-value 属性
const states = this.elTable.store.states
states.columns.forEach((column) => {
if (column.filteredValue && column.filteredValue.length) {
this.onFilterChange()
}
})
},
// 使用自定义的树形表格;需禁用原来的树(将children字段改为空)
// 场景:row包含children会被当做的树结构,与 virtual-column 的树结构有冲突,所以需要禁用原来的
useCustomTree (isTry = true) {
this.isTree = true
const elTable = this.getElTable()
if (elTable) {
const states = elTable.store.states
states.childrenColumnName = '' // 拦截原来树形表格
states.lazyColumnIdentifier = '' // 拦截原来树形懒加载表格
elTable.store.updateTreeData = () => {}
} else if (isTry) {
this.$nextTick(() => {
this.useCustomTree(false)
})
}
},
// 兼容自定义树形表格
hackCustomTree () {
if (!this.isTree) return
// 兼容 el-table 的 expand-row-keys属性
const unWatch = this.$watch(() => this.elTable.expandRowKeys, () => {
this.expandRowKeysChanged = true
this.update()
this.expandRowKeysChanged = false
})
this.unWatchs.push(unWatch)
// 兼容 el-table 的 toggleRowExpansion
this.elTable.toggleRowExpansion = (row, expanded) => {
const treeState = row.$v_tree
if (!treeState) return
// 展开状态取反
if (typeof expanded === 'undefined') {
expanded = !treeState.expanded
}
// 状态一致返回
if (treeState.expanded === expanded) return
treeState.expanded = expanded
// 防止多次触发update
if (this.togglingRowExpansion) return
this.togglingRowExpansion = true
this.$nextTick(() => {
this.togglingRowExpansion = false
this.update()
this.columnVms.forEach((vm) => vm.isTree && vm.renderTreeNode())
})
}
},
// 更新树:将树结构平铺,将展开的节点筛出来,同时对新节点插入$v_tree 记录树节点状态数据
updateTreeData (data = this.data) {
if (!this.isTree || !this.treeProps) {
this.listData = data
return
}
const res = []
const { children = 'children' } = this.treeProps
const { defaultExpandAll, expandRowKeys, rowKey, indent } = this.elTable
const getExpanded = (key) => {
if (!key) return false
return defaultExpandAll || (expandRowKeys && expandRowKeys.indexOf(key) !== -1) || false
}
const traverse = (nodes, parent, level = 0, display = true) => {
nodes.forEach((node) => {
if (display) {
res.push(node)
}
let treeState = node.$v_tree || {}
if (!node.$v_tree) {
// 如果是新节点,添加 $v_tree($v_tree不设置成响应式,$v_tree值的变更可能会触发data变化,data watch会触发,这是不可控的,会导致updateTreeData多触发一次)
// eslint-disable-next-line no-multi-assign
node.$v_tree = treeState = {
parent,
level,
expanded: getExpanded(node[rowKey]),
loaded: false,
loading: false,
indent: (level - 1) * indent,
}
} else {
// 如果是旧节点
// expandRowKeys更改时,旧的节点数据的 expanded(是否展开)以getExpanded方法为准
if (this.expandRowKeysChanged) {
treeState.expanded = getExpanded(node[rowKey])
}
}
if (Array.isArray(node[children])) {
// 父节点显示且展开状态时,才显示子节点
const childDisplay = display && treeState.expanded
traverse(node[children], node, level + 1, childDisplay)
}
})
}
// 开始遍历树结构
traverse(data)
this.listData = res
// console.log('this.isTree', this.listData, this.listData.map(i => i.id))
},
/*
* 【扩展树形表格方法】获取子节点
*/
getChildNodes (row) {
const { children = 'children' } = this.treeProps
return row[children] || []
},
// 【扩展树形表格方法】获取父节点
getParentNode (row) {
const treeState = row.$v_tree
return treeState && treeState.parent
},
// 【扩展树形表格方法】获取父层所有节点
getParentNodes (row) {
const res = []
let curRow = row
while (curRow) {
const treeState = curRow.$v_tree
if (!treeState || !treeState.parent) break
res.unshift(treeState.parent)
curRow = treeState.parent
}
return res
},
// 【扩展树形表格方法】重新加载节点
// 删除原来子节点,并触发load函数重新加载
async reloadNode (row) {
const treeState = row.$v_tree
if (!treeState) return
const { children = 'children', hasChildren = 'hasChildren' } = this.treeProps
row[children] = null
row[hasChildren] = true
treeState.loaded = false
this.update()
const vm = this.columnVms.find((vm) => vm.isTree)
vm && await vm.loadChildNodes(row, true)
},
// 【扩展树形表格方法】收起所有树节点
unexpandAllNodes () {
const { children } = this.treeProps
// 遍历树节点,展开或收起
const traverse = (rows) => {
rows.forEach((row) => {
this.toggleRowExpansion(row, false)
if (Array.isArray(row[children])) traverse(row[children])
})
}
traverse(this.listData)
},
// 【扩展树形表格方法】展开所有树节点
expandAllNodes () {
// 遍历树节点,展开或收起
const { children, hasChildren = 'hasChildren' } = this.treeProps
const traverse = (rows) => {
rows.forEach((row) => {
const treeState = row.$v_tree
// 懒加载节点如果未加载则不展开
if (row[hasChildren] && !treeState.loaded) return
this.toggleRowExpansion(row, true)
if (Array.isArray(row[children])) traverse(row[children])
})
}
traverse(this.listData)
},
// 表格销毁事件
bindTableDestory () {
const onTableDestory = () => {
this.warn && console.warn('<el-table> 组件销毁时,建议将 <el-table-virtual-scroll> 组件一同销毁')
this.destory()
this.$nextTick(() => {
this.initData()
})
}
// 防止el-table绑定key时,重新渲染表格但没有重新初始化<virtual-scroll>组件
this.elTable.$on('hook:beforeDestory', onTableDestory)
this.unWatchs.push(() => {
this.elTable.$off('hook:beforeDestory', onTableDestory)
})
},
},
}
</script>
<style lang="scss">
.el-table-virtual-scroll {
&.has-custom-fixed-right {
.el-table__cell.gutter {
position: sticky;
right: 0;
}
}
}
.el-table-virtual-scroll {
.el-table--border {
border-radius: 0;
}
.el-table {
border-radius: 0;
.caret-wrapper {
margin-left: 6px;
}
.el-table__header th.is-sortable {
.cell:first-child > div {
display: inline-flex;
// height: 36px;
align-items: center;
}
}
.el-table__body tr.hilight-row > td {
background: $--cs-interactive_bg;
}
.el-table__body tr.warn-row > td {
background: $--cs-warning_bg;
}
.el-table__body tr.error-row > td {
background: $--cs-error_bg;
}
}
.el-table--mini td,
.el-table--mini th {
padding: 6px;
}
.data-search-empty {
margin: 84px 0 80px;
p {
margin: 20px 0 0;
font-size: 14px;
font-weight: 400;
color: $--cs-gray3;
}
}
.radio-table .el-table__header .el-table-column--selection .el-checkbox {
display: none;
}
}
</style>
<style lang='scss' scoped>
.is-expanding {
::v-deep .el-table__expand-icon {
transition: none;
}
}
.hide-append {
::v-deep .el-table__append-wrapper {
display: none;
}
}
</style>
js
import normalizeWheel from 'normalize-wheel'
import { getValueByPath } from 'element-ui/src/utils/util'
import throttle from 'lodash/throttle'
// 判断是否是滚动容器
export function isScroller (el) {
const style = window.getComputedStyle(el, null)
const scrollValues = ['auto', 'scroll']
return scrollValues.includes(style.overflow) || scrollValues.includes(style['overflow-y'])
}
// 获取父层滚动容器
export function getParentScroller (el) {
let parent = el
while (parent) {
if ([window, document, document.documentElement].includes(parent)) {
return window
}
if (isScroller(parent)) {
return parent
}
parent = parent.parentNode
}
return parent || window
}
// 获取容器滚动位置
export function getScrollTop (el) {
return el === window ? window.pageYOffset : el.scrollTop
}
// 获取容器高度
export function getOffsetHeight (el) {
return el === window ? window.innerHeight : el.offsetHeight
}
// 滚动到某个位置
export function scrollToY (el, y) {
if (el === window) {
window.scroll(0, y)
} else {
el.scrollTop = y
}
}
// 是否为空 undefine or null
export function isEmpty (val) {
return typeof val === 'undefined' || val === null
}
export const isFirefox = typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().indexOf('firefox') > -1
// 设置滚轮速度(完全参考 element-ui > table > handleFixedMousewheel方法)
export function setMousewheelSlow (scroller, slow) {
function handler (event) {
const data = normalizeWheel(event)
if (Math.abs(data.spinY) > 0) {
const currentScrollTop = scroller.scrollTop
if (data.pixelY < 0 && currentScrollTop !== 0) {
event.preventDefault()
}
if (data.pixelY > 0 && scroller.scrollHeight - scroller.clientHeight > currentScrollTop) {
event.preventDefault()
}
scroller.scrollTop += Math.ceil(data.pixelY / slow)
}
}
const throttleHandler = throttle(handler, 0)
scroller.addEventListener(isFirefox ? 'DOMMouseScroll' : 'mousewheel', throttleHandler, { passive: false })
return function destory () {
scroller.removeEventListener(isFirefox ? 'DOMMouseScroll' : 'mousewheel', throttleHandler)
}
}
const isObject = function (obj) {
return obj !== null && typeof obj === 'object'
}
// 排序(来源:element-ui/table/util的orderBy方法)
export const orderBy = function (array, sortKey, reverse, sortMethod, sortBy) {
// eslint-disable-next-line no-mixed-operators
if (!sortKey && !sortMethod && (!sortBy || Array.isArray(sortBy) && !sortBy.length)) {
return array
}
if (typeof reverse === 'string') {
reverse = reverse === 'descending' ? -1 : 1
} else {
reverse = (reverse && reverse < 0) ? -1 : 1
}
const getKey = sortMethod ? null : function (value, index) {
if (sortBy) {
if (!Array.isArray(sortBy)) {
sortBy = [sortBy]
}
return sortBy.map(function (by) {
if (typeof by === 'string') {
return getValueByPath(value, by)
}
return by(value, index, array)
})
}
if (sortKey !== '$key') {
if (isObject(value) && '$value' in value) value = value.$value
}
return [isObject(value) ? getValueByPath(value, sortKey) : value]
}
const compare = function (a, b) {
if (sortMethod) {
return sortMethod(a.value, b.value)
}
for (let i = 0, len = a.key.length; i < len; i++) {
if (a.key[i] < b.key[i]) {
return -1
}
if (a.key[i] > b.key[i]) {
return 1
}
}
return 0
}
return array.map(function (value, index) {
return {
value: value,
index: index,
key: getKey ? getKey(value, index) : null,
}
}).sort(function (a, b) {
let order = compare(a, b)
if (!order) {
// make stable https://en.wikipedia.org/wiki/Sorting_algorithm#Stability
order = a.index - b.index
}
return order * reverse
}).map((item) => item.value)
}
export const getColumnById = function (table, columnId) {
let column = null
table.columns.forEach(function (item) {
if (item.id === columnId) {
column = item
}
})
return column
}