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来模拟数据刷新查看更新数据的效果。

相关推荐
小羊没烦恼!13 分钟前
T-SQL查询高级—SQL Server索引中的碎片和填充因子
开发语言·前端·javascript·数据库·后端·python·web安全
Liu.77432 分钟前
Vue3 中如何使用 GET 请求传递复杂参数
vue.js·vue3
定栓40 分钟前
Webpack迁到Vite第一节:模块系统迁移:require → import
运维·前端·webpack
小熊在忙fpga1 小时前
N32G430芯片的flash表示
前端·javascript·html
Data analyse4561 小时前
App埋点版本兼容的字段迁移校验
大数据·运维·服务器·前端·数据库·数据分析
todoitbo1 小时前
本来只想给内网开个口子,Natapp 却多给了一道防护
前端·javascript·vue.js·内网穿透·网络防护·natapp
会灭火的程序员1 小时前
SuperMap AgentX Server智能体接入WEB应用技术文档
前端·supermap·agentx·agentx server
CRMEB1 小时前
商品 SKU 设计,从简单规格到多规格踩过什么
java·前端·数据库·开源
Chloe.Zz1 小时前
一篇入门Agent
开发语言·前端·javascript
java1234_小锋1 小时前
【技术专题】HTML5 网页技术 - HTML5 表格标签
前端·html·html5