电视机上要以列表展示数据,并且数据会实时更新,电视机不能点击,所以考虑自动播放的一个效果。展示方案有两种: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
来模拟数据刷新查看更新数据的效果。