vue3+ts封装一个uniapp的自动滚动列表,实现看板效果

电视机上要以列表展示数据,并且数据会实时更新,电视机不能点击,所以考虑自动播放的一个效果。展示方案有两种:1、列表上下自动滚动实现轮播效果。(此时具体滚动的高度由用户自己决定,每次滚动几条数据)2、列表以"页"的形式做成轮播图的翻页效果。

由于项目的电视机是有任务提示作用的,最后考虑做成第一种方案,用户能更清晰了解任务安排和数据的更新。

搜索之后了解到vue-seamless-scroll支持列表的自动滚动效果,但是一般是vue2使用,所以考虑自己封装一个组件。 一开始是使用uni-app的组件uni-table进行封装,然后发现其实有很多注意事项,可能需要对uni-table进行深度改造,最后决定自己使用原生table来封装。

uni-table的第一次封装如下

<template>
    <view class="fullscreen-table">
      <uni-table ref="table" :data="tableData">
        <uni-tr>
		    <uni-th align="center">title</uni-th>
		    <uni-th align="center">date</uni-th>
	    </uni-tr>
	    <uni-tr v-for="(item,index) in tableData" :key="index">
	        <uni-td>{{ item.title }}</uni-td>
	        <uni-td>{{ item.date }}</uni-td>
	    </uni-tr>
      </uni-table>
    </view>
  </template>
  
  <script setup lang="ts">
  import { defineComponent, onMounted, ref } from 'vue';
    const table = ref<any>(null)
    const tableData = ref<any[]>([
        {
          'title': '无缝滚动第一行无缝滚动第一行',
          'date': '2017-12-16'
        }, {
          'title': '无缝滚动第二行无缝滚动第二行',
          'date': '2017-12-16'
        }, {
          'title': '无缝滚动第三行无缝滚动第三行',
          'date': '2017-12-16'
        }, {
          'title': '无缝滚动第四行无缝滚动第四行',
          'date': '2017-12-16'
        }, {
          'title': '无缝滚动第五行无缝滚动第五行',
          'date': '2017-12-16'
        }, {
          'title': '无缝滚动第六行无缝滚动第六行',
          'date': '2017-12-16'
        }, {
          'title': '无缝滚动第七行无缝滚动第七行',
          'date': '2017-12-16'
        }, {
          'title': '无缝滚动第八行无缝滚动第八行',
          'date': '2017-12-16'
        }, {
          'title': '无缝滚动第九行无缝滚动第九行',
          'date': '2017-12-16'
        }
      ]);
  
      onMounted(() => {
        const tableElement = table.value.$el;
        const scrollHeight = tableElement.scrollHeight;
        const viewportHeight = tableElement.clientHeight;
        let scrollPosition = 0;
        const speed = 1; // 调整滚动速度
  
        function autoScroll() {
          if (scrollPosition + viewportHeight >= scrollHeight) {
            scrollPosition = 0;
          } else {
            scrollPosition += speed;
          }
          tableElement.scrollTop = scrollPosition;
          requestAnimationFrame(autoScroll);
        }
  
        autoScroll();
      })
  </script>
  
  <style scoped>
  .fullscreen-table {
    height: 100vh;
    overflow: hidden; /* 防止表格外部滚动 */
  }
  
  .uni-table {
    overflow-y: auto; /* 允许表格内部滚动 */
  }

  </style>

最后封装代码如下:(第一次自己封装组件,可能有很多没考虑进去并且可以优化的地方,只是分享一个简单的半成品)

<template>
    <view class="table-container">
      <view class="table-header">
        <table>
          <thead>
            <tr class="header" align="left" :style="{height: headerHeight+'px', fontSize: headerFontSize+'px', lineHeight: headerHeight+'px'}">
              <th :class="'headTh'+index" v-for="(header, index) in headers" :key="index">{{ Object.values(header)[0] }}</th>
            </tr>
          </thead>
        </table>
      </view>
      <view ref="myTable" class="table-body">
        <view v-if="!(data.length>0)" style="width: 100%; height: 100%; font-size: 50px;  display: flex; justify-content: center; align-items: center;">暂无数据</view>
        <view v-else>
          <table>
          <tbody>
            <tr v-for="(row, rowIndex) in Data" class="cell" :key="rowIndex" :style="{height: bodyHeight+'px', fontSize: bodyFontSize+'px'}">
                <td v-for="(header, headerIndex) in headers" :key="headerIndex" :style="computedStyle(header)" :class="'cellTd'+headerIndex">
                  {{ row[Object.keys(header)[0]] }}
                </td>
            </tr>
          </tbody>
        </table>
        </view>
      </view>
    </view>
  </template>
  
  <script lang="ts" setup>
  import { defineProps, nextTick, onMounted, ref, watch, computed, onUpdated } from 'vue'
  export interface TableProps {
    headers: any[]
    data: any[]
    headerFontSize?: Number,
    bodyFontSize?: Number,
    headerHeight?: Number,
    bodyHeight?: Number,
    intervalTime?: Number
  }
  
  const props = withDefaults(defineProps<TableProps>(), {
    headers: () => [],
    data: () => [],
    headerFontSize:  () => 20,
    bodyFontSize:  () => 20,
    headerHeight:  () => 80,
    bodyHeight:  () => 80,
    intervalTime: () => 3000
})
const Data: Ref<any[]> = ref([])
const myTable = ref(null)
let scrollSpeed = 0
const interval = ref<NodeJS.Timeout | null>(null)
const oldData = ref(props.data)
// 根据header传的style值,动态设置表格的style
const computedStyle = function(header: Object) {
      const keys = Object.keys(header)
      const values = Object.values(header)
      let styles = ''
      if(keys.length>1){
        for(let i=1; i<keys.length; i++){
          styles += (keys[i]+':'+values[i])
          if(i!=keys.length-1) styles+=','
        }
      }
      const styleObject: { [key: string]: string } = {};
      styles.split(',').forEach(style => {
      const [key, value] = style.split(':')
        styleObject[key] = value
      })
      return styleObject
    }
// 开始滚动
const startScroll = () => {
  // 设置滚动速度为每行的高度
  scrollSpeed = props.bodyHeight ? props.bodyHeight : document.querySelector('.cell').offsetHeight
  interval.value = setInterval(() => {
    if (myTable.value) {
      const oldScrollTop = myTable.value.$el.scrollTop
      myTable.value.$el.scrollTop += scrollSpeed
      if(myTable.value.$el.scrollTop===oldScrollTop) {
        myTable.value.$el.scrollTop=0
      }
      if (myTable.value.$el.scrollTop >= myTable.value.$el.scrollHeight / 2) {
        myTable.value.$el.scrollTop = 0
      }
    }
  }, props.intervalTime)
}
// 设置表头和表格共同列宽
function updateHeaderWidth() {
  if (props.data.length>0) {
      for(let index=0; index<props.headers.length; index++)
      {
        document.querySelector(`.headTh${index}`).style.width = document.querySelector(`.cellTd${index}`)?.offsetWidth + 'px'
      }
    }
      
}

onMounted( async () => {
    Data.value = props.data
    await nextTick()
    updateHeaderWidth()
    startScroll()
  }
)

watch(() => props.data, async (newData) => {
  myTable.value.$el.scrollTop = 0
  clearInterval(interval.value)
  updateData(newData)
})

onUpdated(() => {
  updateHeaderWidth()
  startScroll()
})

const updateData = (newData: any[]) => {
      const oldDataArray = oldData.value.slice()
      oldData.value = oldDataArray.filter(item => {
        return newData.some(newItem => {
          return JSON.stringify(newItem) === JSON.stringify(item)
        })
      })
      newData.forEach(item => {
        if (!oldDataArray.some(oldItem => JSON.stringify(oldItem) === JSON.stringify(item))) {
          oldData.value.unshift(item)
        }
      })
      Data.value = oldData.value
    }
  </script>
  
  <style scoped>
  .table-container {
    display: flex;
    flex-direction: column;
    height: 100vh; 
    padding: 20px;
  }
  
  .table-header {
    position: sticky;
    top: 0;
    z-index: 10; 
    margin-bottom: 20px;
  }
  
  .table-header table {
    width: 100%;
    border-collapse: collapse;
  }
  
  .table-body {
    overflow-y: auto;
    flex: 1;    
    padding: 20px;
  }
  
  .table-body table {
    width: 100%;
    border-collapse: collapse;
  }

  .header th{
    font-weight: bolder;
    white-space: nowrap;
    padding: 20px;
    box-sizing: border-box;
  }

  .cell td{
    padding: 20px;
    box-sizing: border-box;
  }

.table-body::-webkit-scrollbar {
  width: 0;
  height: 0;
}

.table-body {
  -ms-overflow-style: none; 
  scrollbar-width: none;
}
  </style>

封装过程中发现因为表头和内容是两个分开的table,所以会存在表头和表格不能上下对齐的情况,这时候考虑代码中的updateHeaderWidth函数,在组件挂载完的时候将头部表格和内容表格的宽度一一对应。

通过 headers: any[]接收父组件传的表头展示数据。
data: any[]接收父组件传的表格内容数据

都为必传内容,但是也有默认赋值。

通过
headerFontSize?: Number,表头的文字大小(一般表头会更醒目一些)
bodyFontSize?: Number,表格的文字大小
headerHeight?: Number,表头高度(默认根据传值让表头内容在单元格中居中)
bodyHeight?: Number,表格高度
intervalTime?: Number滚动间隙(多少毫秒滚动一次)

组件挂载完的时候,通过startScroll来开始滚动,组件默认将bodyHeight表格高度设置为滚动速度scrollSpeed,实现每次滚动底部刷新出最新一条数据的效果。

对于数据的刷新,本来计划新数据和旧数据进行一个简单的diff比较差异,然后将没有的数据加入到旧数据的最后面,保存更新数据前的滚动高度scrollTop,更新数据之后继续从该高度开始滚动。但是后面又意识到不仅仅有增加,还会有删除,这个时候滚动高度scrollTop不适配了,数据也不一定会接着更新数据前的内容展示。

这个时候考虑先将旧数据中在新数据中仍存在的值过滤出来,然后将没有的数据加入到旧数据的最前面(数据data要求是一个数组,因此加数据的时候采用unshift),每次更新数据都将滚动高度scrollTop置0开始重新滚动,用户每次都会看到最新的数据。

后面发现更新数据后的总的滚动高度scrollHeight 获取不正常,滚动会停止,以为是数据更新之后还没重新渲染完就获取了导致的,但是在onUpdated中获取的也是同样的值,后面排查也没发现具体原因,因此直接写死代码,让出现异常的时候判断出来重新将滚动高度scrollTop置0。

监听数据变化完之后,表格会重新渲染,此时表头由于数据没有更新会保持原宽度不变,所以在onUpdated中再次调用updateHeaderWidth函数并且重新启动自动滚动startScroll

考虑到对于表格的展示,用户可能有不同的要求,比如时间的字段实际太长了,需要将字体调小来实现一行展示。因此提供computedStyle方法来实现动态style。此时的传值会在headers中,因为data实际都是接口获取值,不会说每个数据字段都告诉你要什么样式。

调用代码如下:

<template>
    <view class="body">
      <ScrollTable :headers="headers" :data="tableData" :body-font-size="30" :body-height="110" :header-font-size="50" :header-height="120" :interval-time="2000" />
    </view>
</template>
  
<script setup lang="ts">
import { ref } from 'vue';
import ScrollTable from '@/components/ScrollTable/ScrollTable.vue';
const headers = ref<any[]>([{'type': '异常类型', 'whiteSpace': 'nowrap'}, {'reason': '异常原因'}, {'dateTime': '异常时间', 'fontSize': '25px', 'whiteSpace': 'nowrap'}])
cosnt const tableData = ref<any[]>([
        {
          'type': 'wcs扫监管码异常',
          'reason': '订单XXXXXXXXXXXXXXX出现WCS扫监管码异常,我试试原因很长的时候会不会自动换行',
          'dateTime': '2024-08-30 09:17:10'
        }, {
          'type': '视觉扫描异常',
          'reason': '订单XXXXXXXXXXXXXXX出现视觉扫描异常',
          'dateTime': '2024-08-30 10:17:10'
        }, {
          'type': '未识别托盘码',
          'reason': '订单XXXXXXXXXXXXXXX未能识别托盘码',
          'dateTime': '2024-08-30 11:17:10'
        }, {
          'type': '遗漏未入库物料',
          'reason': '订单XXXXXXXXXXXXXXX存在有物料未入库',
          'dateTime': '2024-08-30 12:17:10'
        }, {
          'type': '外包装异常',
          'reason': '订单XXXXXXXXXXXXXXX存在有物料未入库',
          'dateTime': '2024-08-30 13:17:10'
        },
        {
          'type': 'wcs扫监管码异常',
          'reason': '订单XXXXXXXXXXXXXXX出现WCS扫监管码异常1',
          'dateTime': '2024-08-30 13:27:10'
        }, {
          'type': '视觉扫描异常',
          'reason': '订单XXXXXXXXXXXXXXX出现视觉扫描异常2',
          'dateTime': '2024-08-30 13:37:10'
        }, {
          'type': '未识别托盘码',
          'reason': '订单XXXXXXXXXXXXXXX未能识别托盘码3',
          'dateTime': '2024-08-30 13:47:10'
        }, {
          'type': '遗漏未入库物料',
          'reason': '订单XXXXXXXXXXXXXXX存在有物料未入库',
          'dateTime': '2024-08-30 13:57:10'
        }, {
          'type': '外包装异常',
          'reason': '订单XXXXXXXXXXXXXXX存在有物料未入库',
          'dateTime': '2024-08-30 14:17:10'
        }
    ])

最后可以使用setTimeOut来模拟数据刷新查看更新数据的效果。

相关推荐
GIS程序媛—椰子36 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_00142 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端1 小时前
Content Security Policy (CSP)
前端·javascript·面试
木舟10091 小时前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤43911 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
黎金安1 小时前
前端第二次作业
前端·css·css3
啦啦右一1 小时前
前端 | MYTED单篇TED词汇学习功能优化
前端·学习
半开半落1 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt