列表滚动

148 阅读1分钟
  <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>