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').offsetHeightinterval.value = setInterval(() => {if (myTable.value) {const oldScrollTop = myTable.value.$el.scrollTopmyTable.value.$el.scrollTop += scrollSpeedif(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.dataawait nextTick()updateHeaderWidth()startScroll()}
)watch(() => props.data, async (newData) => {myTable.value.$el.scrollTop = 0clearInterval(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
来模拟数据刷新查看更新数据的效果。