<template> <div class="dv-scroll-board" :ref="ref"> <!--表头--> <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; font-size:${mergedConfig.headerFontSize}; color:${mergedConfig.headerColor}; width: ${widths[i]}px; `" :align="aligns[i]" v-html="headerItem" /> </div> <!-- 表数据 --> <div v-if="mergedConfig" class="rows" :style="`height: ${height - (header.length ? mergedConfig.headerHeight : 0)}px;`" > <div class="row-item" :class="mergedConfig.hoverColor?'hoverRow':''" v-for="(row, ri) in rows" :key="`${row.toString()}${row.scroll}`" :style="` height: ${heights[ri]}px; line-height: ${heights[ri]}px; font-size:${mergedConfig.rowFontSize}; color:${mergedConfig.rowColor}; 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> </template> <script> import autoResize from '@/mixin/autoResize' import { deepMerge } from '@/utils/index' export default { name: 'ScrollBoard', mixins: [autoResize], props: { config: { type: Object, default: () => ({}) } }, data () { return { ref: 'scroll-board', defaultConfig: { header: [], // 表头header = ['column1', 'column2', 'column3'] data: [], // 数据 textAlign: 'left', // 文本样式居中还是居左 rowNum: 5, // 显示行数 headerBGC: '#00BAFF', // 表头背景颜色 headerColor: '#FFFFFF', // 表头文字颜色 oddRowBGC: '#003B51', // 偶数行背景颜色 evenRowBGC: '#0A2732', // 奇数行背景颜色 rowColor: '#FFFFFF', // 行文字颜色 headerFontSize: '0.06rem', // 表头字体大小 rowFontSize: '0.06rem', // 行字体大小 waitTime: 2000, // 滚动间隔时间 headerHeight: 35, // 表头高度 columnWidth: [], // 每列宽度 align: [], // 每对齐方式['left', 'center', 'right'] index: false, // 是否显示行号 indexHeader: '#', // 行号列表头 carousel: 'single', // 轮播方式 ,single一行行滚动|page整页滚动 hoverPause: true, // 悬浮暂停轮播 hoverColor: false // 悬停是否需要背景颜色 }, mergedConfig: null, // 混合后的配置 header: [], // 表头数据 rowsData: [], // 行数据 rows: [], // 行,ceils中是数据,rowIndex,scroll widths: [], // 宽度列表 heights: [], // 每行高度列表 avgHeight: 0, // 平均高度 aligns: [], // 对齐方式 animationIndex: 0, // 动画当前位置 animationHandler: '', updater: 0 } }, watch: { config () { // 配置改变后,停止动画,重新计算数据 const { stopAnimation, calcData } = this stopAnimation() this.animationIndex = 0 calcData() } }, methods: { // 处理鼠标hover handleHover (enter, ri, ci, row, ceil) { const { mergedConfig, emitEvent, stopAnimation, animation } = this // 如果放上去要暂停动画 if (enter) emitEvent('mouseover', ri, ci, row, ceil) if (!mergedConfig.hoverPause) return if (enter) { stopAnimation() } else { animation(true) } }, // 处理autoRize afterAutoResizeMixinInit () { const { calcData } = this calcData() }, // 重新计算大小 onResize () { const { mergedConfig, calcWidths, calcHeights } = this if (!mergedConfig) return calcWidths() calcHeights() }, calcData () { const { mergeConfig, calcHeaderData, calcRowsData } = this mergeConfig() // 合并配置 calcHeaderData() // 计算表头 calcRowsData() // 计算表数据 const { calcWidths, calcHeights, calcAligns } = this calcWidths() // 计算宽 calcHeights() // 计算高 calcAligns() // 计算对齐方式 const { animation } = this animation(true) // 打开动画 }, // 合并配置项 mergeConfig () { let { config, defaultConfig } = this // this.mergedConfig = deepMerge(deepClone(defaultConfig, true), config || {}) this.mergedConfig = Object.assign(defaultConfig, config || {}) }, // 计算表头数据 calcHeaderData () { let {header, index, indexHeader} = this.mergedConfig if (!header.length) { this.header = [] return } header = [...header] // 如果有序号要在header加上序号列 if (index) header.unshift(indexHeader) this.header = header }, // 计算行数据 calcRowsData () { let { data, index, rowNum } = this.mergedConfig // 如果要显示行号,添加行号列 if (index) { // 遍历数据 data = data.map((row, i) => { row = [...row] const indexTag = `<span class="board-index">${i + 1}</span>` row.unshift(indexTag) return row }) } // 创建一个新数组,将ceils赋为当前数据,rowIndex行号 data = data.map((ceils, i) => ({ ceils, rowIndex: i })) const rowLength = data.length // 如果数据行数大于显示行数并小于2倍的要显示行数,复制出来一组数据 if (rowLength > rowNum && rowLength < 2 * rowNum) { data = [...data, ...data] } // 创建一个新数组,给每个数组项新增一个scroll属性为行号 data = data.map((d, i) => ({ ...d, scroll: i })) this.rowsData = data this.rows = data }, // 计算宽度 calcWidths () { const { width, mergedConfig, rowsData } = this const { columnWidth, header } = mergedConfig // 把cloumnWidth中值累加,计算要使用的总宽度 const usedWidth = columnWidth.reduce((all, w) => all + w, 0) let columnNum = 0 if (rowsData[0]) { columnNum = rowsData[0].ceils.length // 获取列宽度 } else if (header.length) { columnNum = header.length } // 剩下空间平分 const avgWidth = (width - usedWidth) / (columnNum - columnWidth.length) // 平均宽度数据 const widths = new Array(columnNum).fill(avgWidth) // 混合平均宽度数组和列宽数组 this.widths = deepMerge(widths, columnWidth) }, // 计算高度 calcHeights (onresize = false) { const { height, mergedConfig, header } = this const { headerHeight, rowNum, data } = mergedConfig let allHeight = height // 总高度 if (header.length) allHeight -= headerHeight // 总高度减表头高度 // 计算每行的平均高度 const avgHeight = allHeight / rowNum this.avgHeight = avgHeight // 将平均高度填入heights数组中 if (!onresize) this.heights = new Array(data.length).fill(avgHeight) }, // 计算对齐方式 calcAligns () { const { header, mergedConfig } = this const columnNum = header.length let aligns = new Array(columnNum).fill('left') const { align } = mergedConfig this.aligns = deepMerge(aligns, align) }, // 同步函数,动画 async animation (start = false) { let { avgHeight, animationIndex, mergedConfig, rowsData, animation, updater } = this const { waitTime, carousel, rowNum } = mergedConfig const rowLength = rowsData.length // 如果数据少于要显示行数, 不用执行动画 if (rowNum >= rowLength) return // 如果要开始动画 if (start) { await new Promise(resolve => setTimeout(resolve, waitTime)) if (updater !== this.updater) return } // 动画时一行行滚动还是一页页滚动,计算一次上移个数 const animationNum = carousel === 'single' ? 1 : rowNum // 获取从动画开始位往后的数据 // slice(开始截取位置,结束截取位置) let rows = rowsData.slice(animationIndex) // 把前面的数据补到后面 rows.push(...rowsData.slice(0, animationIndex)) // 截取多一行或多一页数据 this.rows = rows.slice(0, carousel === 'page' ? rowNum * 2 : rowNum + 1) this.heights = new Array(rowLength).fill(avgHeight) // 等待300ms await new Promise(resolve => setTimeout(resolve, 300)) // 如果暂停了,不再执行动画 if (updater !== this.updater) return // 根据一次滚动一条还是一页,计算新高度,将要消失掉的数据的高度改为0 // splice(添加/删除item的位置, 删除的item数量,添加的新item) this.heights.splice(0, animationNum, ...new Array(animationNum).fill(0)) // 更新animationIndex animationIndex += animationNum // 当前动画位置减去行数,如果结果大于0,表示超出滚动范围,则重新计算动画开始位置 const back = animationIndex - rowLength if (back >= 0) animationIndex = back this.animationIndex = animationIndex // 重新倒计时 this.animationHandler = setTimeout(animation, waitTime - 300) }, // 停止动画 stopAnimation () { const { animationHandler, updater } = this // 每次停止动画时,updater都+1 this.updater = (updater + 1) % 999999 // 并清除动画定时器 if (!animationHandler) return clearTimeout(animationHandler) }, emitEvent (type, ri, ci, row, ceil) { const { ceils, rowIndex } = row this.$emit(type, { row: ceils, ceil, rowIndex, columnIndex: ci }) }, /** * 更新行 * @param {string[][]} rows 更新后的行数据 * @param {number} index 下次滚动的起始行 (可缺省) */ updateRows (rows, animationIndex) { const { mergedConfig, calcRowsData, calcHeights, animationHandler, animation } = this this.mergedConfig = { ...mergedConfig, data: [...rows] } calcRowsData() // 重新计算行数据 calcHeights() // 计算行高 if (typeof animationIndex === 'number') this.animationIndex = animationIndex if (!animationHandler) animation(true) } }, // 销毁时,停止动画 destroyed () { this.stopAnimation() } } </script> <style rel="stylesheet/scss" lang="scss" scoped> @import '../../assets/css/variable.scss'; .dv-scroll-board { position: relative; width: 100%; height: 100%; color: #fff; .header { display: flex; flex-direction: row; font-size: 0.01rem; .header-item { transition: all 0.3s; padding: 0 10px; box-sizing: border-box; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } } .rows { overflow: hidden; .row-item { width:100%; display: flex; font-size: 16px; transition: all 0.3s; .ceil { background: transparent; padding: 0 10px; box-sizing: border-box; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; /*索引*/ .board-index { background-color:red; border-radius: 3px; padding: 0px 3px; } } } .hoverRow:hover{ background:$gradientListHover; /*background:red;*/ } } } </style>