<div class="dv-scroll-board" ref="domRef" v-resize="onResize">
<div class="header" v-if="header.length && mergedConfig" :style="`background-color: ${mergedConfig.headerBGC};`">
<div
class="header-item"
v-for="(headerItem, i) in header"
:key="`${headerItem}${i}`"
:style="`
height: ${mergedConfig.headerHeight}px;
line-height: ${mergedConfig.headerHeight}px;
width: ${widths[i]}px;
`"
:align="aligns[i]"
v-html="headerItem"
></div>
</div>
<div></div>
<div class="rows-wrap" :style="`height: ${height - (header.length ? mergedConfig.headerHeight : 0)}px;`">
<div v-if="mergedConfig" class="rows" :style="`transfrom: ${boxStyleTransform}`">
<div
class="row-item"
v-for="(row, ri) in rows"
:key="`${row.toString()}${row.scroll}`"
:style="`
height: ${heights[ri]}px;
line-height: ${heights[ri]}px;
background-color: ${mergedConfig[row.rowIndex % 2 === 0 ? 'evenRowBGC' : 'oddRowBGC']};
`"
>
<div
class="ceil"
v-for="(ceil, ci) in row.ceils"
:key="`${ceil}${ri}${ci}`"
:style="`width: ${widths[ci]}px;`"
:align="aligns[ci]"
v-html="ceil"
@click="emitEvent('click', ri, ci, row, ceil)"
@mouseenter="handleHover(true, ri, ci, row, ceil)"
@mouseleave="handleHover(false)"
></div>
</div>
</div>
</div>
</div>
</template>
<script setup>
// import autoResize from '../../../mixin/autoResize'
// import { deepMerge } from '@jiaminghi/charts/lib/util/index'
// import { deepClone } from '@jiaminghi/c-render/lib/plugin/util'
import { merge, cloneDeep } from 'lodash-es'
const props = defineProps({
config: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['click', 'mouseover'])
// 固定变量
// const ref = ref('scroll-board') // 容器dom名
const defaultConfig = {
// 默认配置值
/**
* header: 表头数据
* header: [
* {
* prop: 'orderNum',
* label: '格口',
* align: 'left'
* },
* {
* prop: 'order',
* label: '订单号'
* },
* {
* prop: 'checkStatus',
* label: '复合状态',
* align: 'left'
* }
* ]
*/
header: [], // 表头数据
data: [], // 表格数据
rowNum: 20, // 首次展示条数
headerBGC: '#00BAFF', // 表头颜色
oddRowBGC: 'transparent', // old行颜色#003B51
evenRowBGC: 'transparent', // even行颜色#0A2732
waitTime: 500, // 滚动等待时长,单位 ms
headerHeight: 35, // 表头高度
columnWidth: [], // 列宽度
align: [], // 每列对齐方式
index: false, // 是否需要排序列,
indexHeader: '#', // 序号列的表头内容
carousel: 'single', // 滚动方式: 单挑(single)/自由(move)/翻页(page)
hoverPause: true // 悬浮(mouse hovered)是否暂停
}
// 响应式变量
const domRef = ref(null)
const height = ref(null)
const width = ref(null)
const mergedConfig = ref(null) // 合并后的配置项
// 表头
const header = ref([])
const rowsData = ref([]) // 表行源数据
const rows = ref([]) // 表行渲染数据
const widths = ref([]) // 每列列宽
const heights = ref([]) // 行高
const avgHeight = ref(0) // 平均行高
const aligns = ref([]) // 每列对齐方式
const animationIndex = ref(0) // 动画列的序号
const animationHandler = ref('') // 动画函数
const updater = ref(0) // 滚动长度
const needCalc = ref(false) // 是否重新更新dom
const scrollTop = ref(null)
const translateY = ref(null)
const boxStyleTransform = computed(() => {
return `translate(0, ${translateY.value}px )`
})
const animationFrame = ref(null)
const moveAnimation = (start = false) => {
stopMoveAnimation()
animationFrame.value = requestAnimationFrame(() => {
if (rows.value.length > 0) {
translateY.value = height.value[rows.value[0]['scroll']]
}
moveAnimation()
})
}
const stopMoveAnimation = () => {
if (animationFrame.value) {
cancelAnimationFrame(animationFrame.value)
}
}
// 合并配置项
const getMergeConfig = () => {
mergedConfig.value = merge(cloneDeep(defaultConfig, true), props.config || {})
}
// mouse over事件:鼠标悬浮事件
const handleHover = (enter, ri, ci, row, ceil) => {
if (enter) emitEvent('mouseover', ri, ci, row, ceil)
if (!mergedConfig.value.hoverPause) return
if (mergedConfig.value.carousel !== 'move') {
if (enter) {
stopAnimation()
scrollTop.value = document.getElementsByClassName('rows')[0].scrollTop
} else {
animation(true)
}
}
}
// emit事件:鼠标悬浮事件/鼠标点击事件
const emitEvent = (type, ri, ci, row, ceil) => {
const { ceils, rowIndex } = row
emit(type, {
row: ceils,
ceil,
rowIndex,
columnIndex: ci
})
}
// 计算表头数据
const calcHeaderData = () => {
let { header: headerList, index, indexHeader } = mergedConfig.value
if (!headerList || !headerList.length) {
headerList = []
return
}
headerList = [...headerList]
if (index) headerList.unshift(indexHeader)
header.value = headerList
}
// 计算表格body行的数据
const calcRowsData = () => {
let { data, index, headerBGC, rowNum } = mergedConfig.value
if (index) {
data = data.map((row, i) => {
row = [...row]
const indexTag = `<span class="index" style="background-color: ${headerBGC};">${i + 1}</span>`
row.unshift(indexTag)
return row
})
}
data = data.map((ceils, i) => ({ ceils, rowIndex: i }))
const rowLength = data.length
if (rowLength > rowNum && rowLength < 2 * rowNum) {
data = [...data, ...data]
heights.value = [...heights.value, ...heights.value]
}
data = data.map((d, i) => ({ ...d, scroll: i }))
rowsData.value = data
rows.value = data
}
// 计算表格列宽度
const calcWidths = () => {
const { columnWidth, header } = mergedConfig.value
const usedWidth = columnWidth.reduce((all, w) => all + w, 0)
let columnNum = 0
const rowsDataOne = rowsData.value[0]
if (rowsDataOne) {
columnNum = rowsDataOne.ceils.length
} else if (header.length) {
columnNum = header.length
}
const avgWidth = (width.value - usedWidth) / (columnNum - columnWidth.length)
const widthList = new Array(columnNum).fill(avgWidth)
widths.value = merge(widthList, columnWidth)
}
// 计算表格行高度
const calcHeights = (onresize = false) => {
const { headerHeight, rowNum, data } = mergedConfig.value
let allHeight = height.value
if (header.value.length) allHeight -= headerHeight
const avgHeightList = allHeight / rowNum
avgHeight.value = avgHeightList
if (!onresize) heights.value = new Array(data.length).fill(avgHeightList)
}
// 计算表格列的对齐方式
const calcAligns = () => {
const columnNum = header.value.length
let alignList = new Array(columnNum).fill('left')
const { align } = mergedConfig.value
aligns.value = merge(alignList, align)
}
// 翻页的动画
const animation = async (start = false) => {
if (needCalc.value) {
calcRowsData()
calcHeights()
needCalc.value = false
}
const updaterOld = updater.value
const { waitTime, carousel, rowNum } = mergedConfig.value
const rowLength = rowsData.value.length
if (rowNum >= rowLength) return
if (start) {
await new Promise((resolve) => setTimeout(resolve, waitTime))
if (updaterOld !== updater.value) return
}
const animationNum = carousel === 'single' ? 1 : rowNum
if (scrollTop.value) {
const scrollNum = Math.ceil(scrollTop.value / avgHeight.value)
animationIndex.value = animationIndex.value + scrollNum
document.getElementsByClassName('rows')[0].scrollTop = 0
scrollTop.value = 0
}
let rowList = rowsData.value.slice(animationIndex.value)
rowList.push(...rowsData.value.slice(0, animationIndex.value))
// rows.value = rowList.slice(0, carousel === 'page' ? rowNum * 2 : rowNum + 1)
rows.value = rowList.slice(0, mergedConfig.value.data.length + 1)
heights.value = new Array(rowLength).fill(avgHeight.value)
// await new Promise((resolve) => setTimeout(resolve, 300))
if (updaterOld !== updater.value) return
heights.value.splice(0, animationNum, ...new Array(animationNum).fill(0))
animationIndex.value += animationNum
const back = animationIndex.value - rowLength
if (back >= 0) animationIndex.value = back
animationHandler.value = setTimeout(animation, waitTime)
}
// 停止动画
const stopAnimation = () => {
updater.value = (updater.value + 1) % 999999
if (!animationHandler.value) return
clearTimeout(animationHandler.value)
}
// 更新行数据
const updateRows = (rows, animationNum) => {
mergedConfig.value = {
...mergedConfig.value,
data: [...rows]
}
needCalc.value = true
if (typeof animationNum === 'number') animationIndex.value = animationNum
if (!animationHandler.value) {
mergedConfig.value.carousel !== 'move' && animation(true)
}
}
// 计算表格body高度
const getDomHeight = () => {
nextTick(() => {
const getBoundingClientRect = domRef.value ? domRef.value.getBoundingClientRect() : {}
height.value = getBoundingClientRect.height || 0
width.value = getBoundingClientRect.width || 0
calcWidths()
calcHeights()
})
}
const calcData = () => {
getMergeConfig()
calcHeaderData()
calcRowsData()
// calcWidths()
// calcHeights()
calcAligns()
mergedConfig.value.carousel !== 'move' && animation(true)
mergedConfig.value.carousel === 'move' && moveAnimation(true)
}
const onResize = () => {
if (!mergedConfig.value) return
getDomHeight()
}
watch(
() => props.config,
() => {
stopAnimation()
animationIndex.value = 0
calcData()
}
)
onMounted(() => {
getDomHeight()
calcData()
})
onUnmounted(() => {})
</script>
<style lang="scss" scoped>
.dv-scroll-board {
position: relative;
width: 100%;
height: 100%;
color: #fff;
.text {
padding: 0 10px;
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.header {
display: flex;
flex-direction: row;
font-size: 15px;
.header-item {
@extend.text;
transition: all 0.3s;
}
}
.rows-wrap {
overflow: hidden;
position: relative;
.rows {
position: absolute;
top: 0;
left: 0;
height: 100%;
overflow-y: auto;
}
.row-item {
display: flex;
font-size: 14px;
transition: all 0.3s;
}
.ceil {
@extend.text;
}
.index {
border-radius: 3px;
padding: 0px 3px;
}
}
}
</style>