数据大屏六:组件开发思路__Vue.js__API
发布于 2 个月前 作者 banyungong 149 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

概要:本文主要记录数据大屏中,容器组件,变换分类组件,面板条目滚动组件的开发思路以及用到的技术。

容器组件

  • 1、resize事件调整屏幕宽高比。放大缩小,或者是屏幕宽度变化的时候,容器中的内容保持同样的宽高比。
  • 2、利用transform:scale(.5),突破chrome浏览器12像素的限制。chrome字体小于12px的时候,再缩小不起作用。scale字体小了,但是容器也缩小了。需要样式控制容器的缩放位置。 保持宽高比的思路:

scale可以接收两个参数,x y轴的缩放比,如果写一个表示x=y的缩放比相同

容器的尺寸: 用户传入或者根据dom.clientWidth/dom.clientHeight计算出来,dom没有计算出来则取电脑屏幕的一个尺寸screen.width/screen.height。

用户可见视口的宽高 window.body.clientWidth/window.body.clientHeight随着屏幕的缩放改变的宽高

缩放比的计算 = 用户可见视口的宽高/容器的尺寸

组件开发相关的优化点:

  • 用节流debounce方法,来控制resize频繁触发。
  • 保证方法的调用顺序 为了保证initsize容器尺寸初始化是在dom更新后初始化的,加上nextTickt。 为了保证initSize在updateSize之前执行,将initSize方法改成promise来做。
  • MutationObserver监听容器组件,当容器组件的style发生变化的时候,重新做屏幕适配缩放自适应。
<template>
  <div id="container" :ref="refName">
    <template v-if="ready">
      <slot></slot>
    </template>
  </div>
</template>

<script>
  import { ref, getCurrentInstance, onMounted, onUnmounted, nextTick } from 'vue'
  import { debounce } from '../../utils'

  export default {
    name: 'container',
    props: {
      options: Object
    },
    setup(ctx) {
      const refName = 'Container'
      const width = ref(0)
      const height = ref(0)
      const originalWidth = ref(0)
      const originalHeight = ref(0)
      const ready = ref(false)
      let context, dom, observer

      const initSize = () => {
        return new Promise((resolve) => {
          nextTick(() => {
            // console.log('打印---', context, document.getElementById('imooc-container'));
            dom = document.getElementById('container')
            // 获取大屏的真实尺寸
            if (ctx.options && ctx.options.width && ctx.options.height) {
              width.value = ctx.options.width
              height.value = ctx.options.height
            } else {
              width.value = dom.clientWidth
              height.value = dom.clientHeight
            }
            // 获取画布尺寸
            if (!originalWidth.value || !originalHeight.value) {
              originalWidth.value = window.screen.width
              originalHeight.value = window.screen.height
            }
            resolve()
          })
        })
      }

      const updateSize = () => {
        if (width.value && height.value) {
          dom.style.width = `${width.value}px`
          dom.style.height = `${height.value}px`
        } else {
          dom.style.width = `${originalWidth.value}px`
          dom.style.height = `${originalHeight.value}px`
        }
      }

      const updateScale = () => {
        // 获取真实的视口尺寸
        const currentWidth = document.body.clientWidth
        const currentHeight = document.body.clientHeight
        // 获取大屏最终的宽高
        const realWidth = width.value || originalWidth.value
        const realHeight = height.value || originalHeight.value
        // console.log(currentWidth, currentHeight)
        const widthScale = currentWidth / realWidth
        const heightScale = currentHeight / realHeight
        dom && (dom.style.transform = `scale(${widthScale}, ${heightScale})`)
      }

      const onResize = async (e) => {
        await initSize()
        updateScale()
      }

      const initMutationObserver = () => {
        const MutationObserver = window.MutationObserver
        observer = new MutationObserver(onResize)
        observer.observe(dom, {
          attributes: true,
          attributeFilter: ['style'],
          attributeOldValue: true
        })
      }

      const removeMutationObserver = () => {
        if (observer) {
          observer.disconnect()
          observer.takeRecords()
          observer = null
        }
      }
      onMounted(async () => {
        ready.value = false
        context = getCurrentInstance().ctx
        await initSize()
        updateSize()
        updateScale()
        window.addEventListener('resize', debounce(100, onResize))
        initMutationObserver()
        ready.value = true
      })

      onUnmounted(() => {
        window.removeEventListener('resize', onResize)
        removeMutationObserver()
      })

      return {
        refName,
        ready
      }
    }
  }
</script>

<style lang="scss">
  #container {
    position: fixed;
    top: 0;
    left: 0;
    overflow: hidden;
    transform-origin: left top;
    z-index: 999;
  }
</style>

效果如下动图: 放大缩小的时候都是自适应的 GIF 2021-7-27 16-34-27.gif

变换分类组件

实现思路:

  • 将用户传入的分类数据数组循环排列,设置selected hover两个参数,通过和循环数组中对应的index是否相等,控制鼠标选中以及鼠标移上去的颜色变化。
  • setInteval定时切换选中的状态。通过setInterval定时器进行定时的变化selected指针位置,并且判断当selected指向最后一个类别的时候,从头开始继续依次位移。
  • 通过鼠标的事件来控制相关的颜色变化。
<template>
  <div class="country-category">
    <div
      class="category"
      v-for="(item, index) in data" :key="item"
      @click="onClick(index)"
      @mouseenter="onMouseEnter(index)"
      @mouseleave="onMouseLeave(index)"
      @mousemove="onMouseEnter(index)">
      <div class="selected" :style="{background: color[0]}" v-if="index === selected">{{item}}</div>
      <div class="hovered" :style="{background: color[1]}" v-else-if="index===hover">{{item}}</div>
      <div v-else>{{item}}</div>
    </div>
  </div>
</template>

<script>
import { ref, onMounted, onUnmounted } from 'vue'

export default {
  name: 'TransformCategory',
  props: {
    data: Array,
    color: {
      type: Array,
      default() {
        return ['rgb(140, 160, 173)', 'rgb(80, 80, 80)']
      }
    }
  },
  setup(props) {
    const selected = ref(0)
    const hover = ref(-1)
    let task
    const update = () => {
      task && clearInterval(task)
      task = setInterval(() => {
        if (selected.value + 1 > props.data.length - 1) {
          selected.value = 0
        }
        selected.value += 1
      }, 2000)
    }

    onMounted(update)

    onUnmounted(() => {
      task && clearInterval(task)
    })

    const onClick = (index) => {
      selected.value = index
    }

    const onMouseEnter = (index) => {
      hover.value = index
    }

    const onMouseLeave = () => {
      hover.value = -1
    }

    return {
      selected,
      hover,
      onClick,
      onMouseEnter,
      onMouseLeave
    }
  }
}
</script>

<style lang="scss" scoped>
  .country-category {
    display: flex;
    width: 100%;
    height: 100%;

    .category {
      flex: 1;
      background: rgb(53, 57, 65);
      font-size: 24px;
      color: rgb(144, 160, 174);

      .hovered {
        background: rgb(80, 80, 80);
      }

      .selected {
        background: rgb(140, 160, 173);
        color: #fff;
      }

      div {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 100%;
        height: 100%;
      }
    }
  }
</style>

GIF 2021-7-27 16-47-08.gif

面板条目滚动组件

实现思路:

  • 头部实现:flex弹性布局,溢出可以自动换行。动态的计算头部dom的宽/标题个数=平均宽度,设置每个标题的宽。可以用户自定义的设置单的的设置每个标题列的样式:字体 颜色 背景色等。
  • 内容条目实现:内容区域flex布局,溢出可以自动换行,并且设置每一项的宽度跟头部的的每项宽度一样。
  • 滚动内容区域:设置滚动区域的整体高度,height-头部区域的高度。设置每一行的内容的高度,将(height-头部区域的高度)/实际元素个数(页面显示最大个数)。可以定制单双数背景颜色,以及文字颜色,文字大小。
  • 过渡动画:拿到整个数据的行数,以及页面显示的行数。页面可以展示行数,小于,展示数据行数才去执行动画。在每一个行数条目上加上过渡动画,让动画有一个过渡的效果

定义一个动画指针index指向当前动画的行数,定义一个步长数一次滚动执行几行动画moveNum。

通过指针行数控制循环播放,判断当前指针是不是到达了最后一行:指针长度-数组总的长度>=0表示循环完一圈,然后将指针重置 =(指针长度-数组总的长度)

将第一个或者多个行元素的高度变成0,然后间隔几秒重新渲染,并且通过指针指向元素,将循环的数组进行首尾相连,一直循环下去形成的动画。

为了解决,页面展示行数刚好等于数据展示长度执行动画不连续或者不执行动画的问题,判断数据的长度<=展示长度的时候,将展示的数据拷贝变成两份执行动画。

<template>
  <div class="base-scroll-list" :id="id">
    <div
      class="base-scroll-list-header"
      :style="{
      backgroundColor: actualConfig.headerBg,
      height: `${actualConfig.headerHeight}px`,
      fontSize: `${actualConfig.headerFontSize}px`,
      color: actualConfig.headerColor}">
      <div
        class="header-item base-scroll-list-text"
        v-for="(headerItem, i) in headerData"
        :key="`${headerItem}${i}`"
        :style="{width: `${columnWidths[i]}px`,...headerStyle[i]}"
        v-html="headerItem"
        :align="aligns[i]">
      </div>
    </div>
    <div
      class="base-scroll-list-rows-wrapper"
      :style="{
        height: `${height-actualConfig.headerHeight}px`}">
        <!-- 循环的给每一项加上背景颜色 -->
      <div
        class="base-scroll-list-rows"
        v-for="(rowData, index) in currentRowsData"
        :key="rowData.rowIndex"
        :style="{
          height: `${rowHeights[index]}px`,
          lineHeight: `${rowHeights[index]}px`,
          backgroundColor: rowData.rowIndex % 2 === 0 ? rowBg[1]: rowBg[0],
          fontSize: `${actualConfig.rowFontSize}px`,
          color: actualConfig.rowColor
        }"
      >
        <div
          class="base-scroll-list-columns base-scroll-list-text"
          v-for="(colData, colIndex) in rowData.data"
          :key="`${colData}${colIndex}`"
          v-html="colData"
          :style="{
            width: `${columnWidths[colIndex]}px`,
            ...rowStyle[colIndex]
          }"
          :align="aligns[colIndex]"
        >
        </div>
      </div>
    </div>
  </div>
</template>

<script type="text/ecmascript-6">
import { watch, ref } from 'vue'
import { v4 as uuidv4 } from 'uuid'
// 在onmounted中获取头部整体的宽和高
import useScreen from '../../hooks/useScreen'
import cloneDeep from 'lodash/cloneDeep'
import assign from 'lodash/assign'

const defaultConfig = {
  // 标题数据
  headerData: [],
  // 标题样式
  headerStyle: [],
  // 标题背景
  headerBg: 'rgb(90,90,90)',
  // 标题高度
  headerHeight: '35',
  // 标题是否展示序号
  headerIndex: false,
  // 展示的序号内容
  headerIndexContent: '#',
  // 序号内容的样式
  headerIndexStyle: {
    width: '50px'
  },
  // 序号列数据内容
  headerIndexData: [],
  // 数据项,二维数组
  data: [],
  // 每页显示的数据条数
  rowNum: 10,
  // 行样式
  rowStyle: [],
  // 行序号内容的样式
  rowIndexStyle: {
    width: '50px'
  },
  // 行背景
  rowBg: [],
  // 内容居中方式
  aligns: [],
  headerFontSize: 28,
  rowFontSize: 28,
  headerColor: '#fff',
  rowColor: '#000',
  moveNum: 1, // 每次移动几条数据的位置
  duration: 2000 // 动画间隔
}

export default {
  name: 'BaseScrollList',

  props: {
    config: {
      type: Object,
      default: () => {
        return {}
      }
    }
  },

  setup(props) {
    const id = `base-scroll-list-${uuidv4()}`
    // 把头部区域的id传递进去 获取宽高
    const { width, height } = useScreen(id)
    const headerData = ref([])
    const headerStyle = ref([])
    const rowStyle = ref([])
    const rowBg = ref([])
    const actualConfig = ref([])
    const columnWidths = ref([])
    const rowHeights = ref([])
    const rowsData = ref([])
    const currentRowsData = ref([])
    const currentIndex = ref(0) // 动画指针
    const rowNum = ref(defaultConfig.rowNum)
    const aligns = ref([])
    const isAnimationStart = ref(true)

    let avgHeight
    const handleHeader = (config) => {
      const _headerData = cloneDeep(config.headerData)
      const _headerStyle = cloneDeep(config.headerStyle)
      const _rowStyle = cloneDeep(config.rowStyle)
      const _rowsData = cloneDeep(config.data)
      const _aligns = cloneDeep(config.aligns)

      if (_headerData.length === 0) {
        return
      }

      if (config.headerIndex) {
        _headerData.unshift(config.headerIndexContent)
        _headerStyle.unshift(config.headerIndexStyle)
        _rowStyle.unshift(config.rowIndexStyle)
        _rowsData.forEach((rows, index) => {
          // 处理序号列数据
          if (config.headerIndexData && config.headerIndexData.length > 0 && config.headerIndexData[index]) {
            rows.unshift(config.headerIndexData[index])
          } else {
            rows.unshift(index + 1)
          }
        })
        _aligns.unshift('center')
      }

      // 动态计算header中每一列的宽度
      let usedWidth = 0
      let usedColumnNum = 0
      // 判断是否存在自定义width
      _headerStyle.forEach(style => {
        if (style.width) {
          usedWidth += Number(style.width.replace('px', ''))
          usedColumnNum++
        }
      })
      // 动态计算列宽时,使用剩余未定义的宽度除以剩余的列数
      const avgWidth = (width.value - usedWidth) / (_headerData.length - usedColumnNum)
      // 给每个标题填充为宽度avgWidth的一个数组
      const _columnWidths = new Array(_headerData.length).fill(avgWidth)
      _headerStyle.forEach((style, index) => {
        if (style.width) {
          const headerWidth = Number(style.width.replace('px', ''))
          _columnWidths[index] = headerWidth
        }
      })

      columnWidths.value = _columnWidths
      headerData.value = _headerData
      headerStyle.value = _headerStyle
      rowStyle.value = _rowStyle
      aligns.value = _aligns
      const { rowNum } = config
      if (_rowsData.length >= rowNum && _rowsData.length < rowNum * 2) {
        const newRowData = [..._rowsData, ..._rowsData]
        rowsData.value = newRowData.map((item, index) => ({
          data: item,
          rowIndex: index
        }))
      } else {
        rowsData.value = _rowsData.map((item, index) => ({
          data: item,
          rowIndex: index
        }))
      }

    }

    const handleRows = (config) => {
      // 动态计算每行数据的高度
      const { headerHeight } = config
      const unusedHeight = height.value - headerHeight
      rowNum.value = config.rowNum

      // 如果rowNum大于实际数据长度,则以实际数据长度为准
      if (rowNum.value > rowsData.value.length) {
        rowNum.value = rowsData.value.length
      }
      avgHeight = unusedHeight / rowNum.value
      rowHeights.value = new Array(rowNum.value).fill(avgHeight)

      // 获取行背景色
      if (config.rowBg) {
        rowBg.value = config.rowBg
      }
    }

    const startAnimation = async () => {
      if (!isAnimationStart.value) {
        return
      }
      const config = actualConfig.value
      const { rowNum, moveNum, duration } = config
      const totalLength = rowsData.value.length
      // 数组的长度小于屏幕可以显示的长度 不动画
      if (totalLength < rowNum) {
        return
      }
      const index = currentIndex.value // 动画指针 初始值是0
      const _rowsData = cloneDeep(rowsData.value)
      // 将数据重新收尾拼接 删除完了再追加到数组末尾
      // [a,b,c,d,e,f,g]
      //    index = 1
      // [b,c,d,e,f,g,a]
      const rows = _rowsData.slice(index)
      rows.push(..._rowsData.slice(0, index))
      currentRowsData.value = rows
      // 先将所有行的高度还原
      rowHeights.value = new Array(totalLength).fill(avgHeight)
      // 加一个时间间隔
      const waitTime = 500
      if (!isAnimationStart.value) {
        return
      }
      await new Promise(resolve => setTimeout(resolve, waitTime))
      // 将moveNum的行高度设置0
      // 这里splice将指定元素删除并替换
      rowHeights.value.splice(0, moveNum, ...new Array(moveNum).fill(0))

      currentIndex.value += moveNum
      // 判断是否到达最后一组数据
      const isLast = currentIndex.value - totalLength
      if (isLast >= 0) {
        currentIndex.value = isLast
      }
      // 让线程sleep
      if (!isAnimationStart.value) {
        return
      }
      await new Promise(resolve => setTimeout(resolve, duration - waitTime))
      await startAnimation()
    }

    const stopAnimation = () => {
      isAnimationStart.value = false
    }

    const update = () => {
      stopAnimation()
      const _actualConfig = assign(defaultConfig, props.config)
      rowsData.value = _actualConfig.data || []

      handleHeader(_actualConfig)
      handleRows(_actualConfig)

      actualConfig.value = _actualConfig
      // 展示动画
      isAnimationStart.value = true
      startAnimation()
    }

    watch(() => props.config, () => {
      update()
    })

    return {
      id,
      headerData,
      headerStyle,
      actualConfig,
      columnWidths,
      rowsData,
      rowHeights,
      rowStyle,
      rowBg,
      aligns,
      currentRowsData,
      height
    }
  }
}
</script>

<style lang="scss" scoped>
.base-scroll-list {
  height: 100%;
  .base-scroll-list-text {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    box-sizing: border-box;
  }
  .base-scroll-list-header {
    display: flex;
    font-size: 15px;
    align-items: center;
  }

  .base-scroll-list-rows-wrapper {
    overflow: hidden;
    .base-scroll-list-rows {
      display: flex;
      align-items: center;
      transition: all 0.3s linear;
      .base-scroll-list-columns {
        height: 100%;
      }
    }
  }
}
</style>

GIF 2021-7-27 17-01-52.gif

vueCountTo兼容vue3.0的方法

找到vueCountTo的源码,将里面的核心代码移植到自己的项目中,然后根据提示,修改错误报错。

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: success202107888 原文链接:https://juejin.im/post/6989529331760365604

回到顶部