Newer
Older
CloudBrainNew / src / components / board / ScrollBoard.vue
StephanieGitHub on 4 Feb 2021 11 KB first commit
<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>