uniapp vue3 虚拟下拉滚动 升级版 (包括ios 安卓虚拟滚动时候的兼容问题 最终版)
//注释 一进入页面就获取全部数据,然后将数据剪切成显示n个 :itemSize="88" 代表默认是每一个列表高度都是88px 最后的结果 永远只会显示对应的几个dom数据列表
1. 创建一个index.vue
1. 创建一个index.vue
<template>
<view class="wrap">
<view class="wrapTab">
<view class="title">所有健康任务</view>
<scroll-view class="scroll-X" scroll-x="true" v-if="state.isShow">
<view class="tabList">
<view
:class="['item', state.active == index ? 'on' : '']"
v-for="(item, index) in healthTaskDetailList"
:key="index"
@tap="tabFun(index)"
>
{{ item.type }}
</view>
</view>
</scroll-view>
<view class="taskList" v-if="state.isShow">
<VirtualList :dateIndex="dateIndex" :listData="dateList" :itemSize="88"></VirtualList>
</view>
<view class="nodate" v-if="!state.isShow">
<img class="img" src="https://ainengli.hzjrsw.com/jkhx/noData.png" alt="" />
<view class="nodate_text">
<text class="name">暂无健康任务</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { taskAllDate } from '@/api/hmm'
import { getPdf } from '@/api/report'
import VirtualList from './VirtualList.vue'
const state = reactive({
userInfo: {},
getUserId: '',
list: [],
active: 0,
heighted: '',
isShow: false
})
const healthTaskDetailList = ref([])
const dateList = ref([])
const dateIndex = ref(0)
onShow(() => {
state.userInfo = uni.getStorageSync('userInfo')
? JSON.parse(uni.getStorageSync('userInfo')).patientInfo
: {}
state.getUserId = uni.getStorageSync('userInfo')
? JSON.parse(uni.getStorageSync('userInfo')).userId
: ''
init()
})
const init = () => {
healthTaskDetailList.value = []
dateList.value = []
state.active = 0
state.isShow = false
uni.showLoading({
title: '加载中'
})
let param = state.userInfo.empi + '/' + state.getUserId
taskAllDate(param).then((res) => {
uni.hideLoading()
if (res.code == 0) {
healthTaskDetailList.value = res.data && res.data.length > 0 ? res.data : []
dateList.value =
res.data &&
res.data.length > 0 &&
res.data[0] &&
res.data[0].hepTaskDetailVO &&
res.data[0].hepTaskDetailVO.length > 0
? res.data[0].hepTaskDetailVO
: []
state.isShow = true
}
})
}
const tabFun = (index) => {
state.active = index
dateIndex.value = index
dateList.value =
healthTaskDetailList.value[index] &&
healthTaskDetailList.value[index].hepTaskDetailVO &&
healthTaskDetailList.value[index].hepTaskDetailVO.length > 0
? healthTaskDetailList.value[index].hepTaskDetailVO
: []
}
const toQuestionaire = () => {
if (state.userInfo && state.userInfo.questionState == false) {
uni.navigateTo({ url: '/packA/pages/questionaire/enter' })
} else {
uni.navigateTo({
url: '/packA/pages/questionaire/detail?index=0'
})
}
}
const healthReport = () => {
getPdf(state.userInfo.empi).then((res) => {
if (res.code == 0) {
uni.downloadFile({
url: res.data,
success: function (res) {
if (res.statusCode === 200) {
// 调用wx.openDocument打开文件
uni.openDocument({
filePath: res.tempFilePath,
success: function (res) {
console.log('打开文档成功')
},
fail: function (err) {
console.log('打开文档失败', err)
}
})
}
},
fail: function (err) {
console.log('下载文件失败', err)
}
})
}
})
}
</script>
<style lang="scss" scoped>
.cf {
zoom: 1;
}
.cf::after {
display: block;
height: 0;
clear: both;
font-size: 0;
content: '.';
visibility: hidden;
}
.wrap {
height: 100vh;
overflow: hidden;
background: #fff;
.wrapTab {
margin: 24rpx 32rpx 0;
padding-bottom: 64rpx;
.title {
font-size: 32rpx;
color: #222;
font-weight: 700;
}
.scroll-X {
margin-top: 24rpx;
.tabList {
white-space: nowrap;
width: auto;
.item {
display: inline-block;
vertical-align: middle;
padding: 0 24rpx;
height: 56rpx;
line-height: 56rpx;
font-size: 28rpx;
color: #666;
&:nth-last-child(1) {
margin-right: 0;
}
&.on {
background: #00d1b6;
border-radius: 28rpx;
color: #fff;
}
}
}
}
.taskList {
height: calc(100vh - 590rpx);
overflow: auto;
border-radius: 12rpx;
margin-top: 24rpx;
}
.nodate {
text-align: center;
.img {
display: inline-block;
width: 320rpx;
height: 240rpx;
margin-top: 120rpx;
}
.nodate_text {
.name {
display: block;
font-size: 28rpx;
color: #666;
text-align: center;
margin-top: 10rpx;
}
}
}
}
}
</style>
2.组件 VirtualList.vue 页面 逻辑都在这个页面
<template>
<view class="content">
<!-- 使用 scroll-view 组件来实现滚动 -->
<scroll-view
ref="list"
scroll-y="true"
class="list-container"
@scroll="handleScroll"
:scroll-top="state.startOffset"
>
<view class="list-phantom" :style="{ height: listHeight + 'px' }"></view>
<view class="taskWrap" :style="{ transform: getTransform }">
<view class="item" v-for="(item, index) in visibleData" :key="item.id">
<view class="top">
<img
class="image"
v-if="item.templateType == '健康监测'"
src="https://ainengli.hzjrsw.com/jkhx/healthmMonitor.png"
alt=""
/>
<img
class="image"
v-else-if="item.templateType == '健康处方'"
src="https://ainengli.hzjrsw.com/jkhx/prescription.png"
alt=""
/>
<img
class="image"
v-else-if="item.templateType == '饮食方案'"
src="@/static/image/raozg/food.png"
alt=""
/>
<img
class="image"
v-else-if="item.templateType == '运动方案'"
src="@/static/image/raozg/sports.png"
alt=""
/>
<img
class="image"
v-else-if="item.templateType == '健康宣教'"
src="https://ainengli.hzjrsw.com/jkhx/propaganda.png"
alt=""
/>
<img
class="image"
v-else-if="item.templateType == '健康随访'"
src="https://ainengli.hzjrsw.com/jkhx/followUp.png"
alt=""
/>
<img
class="image"
v-else-if="item.templateType == '用药提醒'"
src="https://ainengli.hzjrsw.com/jkhx/remind.png"
alt=""
/>
<img class="image" v-else src="https://ainengli.hzjrsw.com/jkhx/visit.png" alt="" />
<!-- <text class="name">{{ item.templateType }}+{{ item.id }}</text> -->
<text class="name">{{ item.templateType }}</text>
<text class="time">{{
item.planTime
? transformTimeTwo(
new Date(item.planTime.replace(/\-/g, '/')).getTime(),
'minutesdd'
).replace(/\//g, '-')
: ''
}}</text>
</view>
<view class="min">
<view class="contents">{{ item.typeContent }}</view>
<view
:class="[
'operation',
item.isRead == false && item.dateStatus == 0
? 'unfinished'
: item.isRead == false && item.dateStatus == 1
? ''
: item.isRead == false && item.dateStatus == 2
? 'noStart'
: 'on'
]"
@tap="targetType(item)"
>
{{
item.isRead == false && item.dateStatus == 0
? '未完成'
: item.isRead == false && item.dateStatus == 1
? '去完成'
: item.isRead == false && item.dateStatus == 2
? '未开始'
: '已完成'
}}
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { reactive, watch, computed } from 'vue'
import { debounce } from '@/utils/tools'
import { transformTimeTwo } from '@/utils/hmm'
import store from '@/store'
const state = reactive({
listHeight: 0,
screenHeight: 0, // 屏幕高度即可视区域高度
startOffset: 0, // 顶部偏移量
startIndex: 0, // 可视化区域的数据开始下标
endIndex: 0 // 可视化区域的数据结束下标
})
const props = defineProps({
listData: {
type: Array,
default: () => []
},
//每项高度
itemSize: {
type: Number,
default: 91
},
dateIndex: {
type: Number,
default: 0
},
defaultShow: {
type: Boolean,
default: true
}
})
watch(
() => props.dateIndex,
(newValue, oldValue) => {
uni.getSystemInfo({
success: function (res) {
state.screenHeight = res.screenHeight - 306
}
})
let visibleCounts = Math.ceil(state.screenHeight / props.itemSize) || 0
state.startOffset = 0
state.startIndex = 0
state.endIndex = 0
for (let i = 0; i < props.listData.length; i++) {
if (props.listData[i].today) {
state.startIndex = i
if (props.listData.length >= visibleCounts) {
state.startOffset = state.startIndex * props.itemSize
}
state.endIndex = state.startIndex + visibleCounts
} else {
state.startOffset = state.startIndex * props.itemSize
}
}
},
{
immediate: true,
deep: true
}
)
const listHeight = computed(() => {
let num = props.listData.length
return num * props.itemSize
})
const visibleCount = computed(() => {
uni.getSystemInfo({
success: function (res) {
state.screenHeight = res.screenHeight - 306
}
})
return Math.ceil(state.screenHeight / (props.itemSize + 12)) || 0
})
const getTransform = computed(() => {
return `translate3d(0, ${isNaN(state.startOffset) ? 0 : state.startOffset}px, 0)`
})
const visibleData = computed(() => {
const end = Math.min(state.endIndex, props.listData.length)
const start = Math.max(state.startIndex, 0)
const data = props.listData.slice(start, end)
return data
// let minNum = Math.min(state.endIndex, props.listData.length)
// if (minNum - state.startIndex < visibleCount.value) {
// if (minNum - visibleCount.value >= 0) {
// return props.listData.slice(
// minNum - visibleCount.value,
// Math.min(state.endIndex, props.listData.length)
// )
// } else {
// // state.startOffset = 0
// return props.listData.slice(0, Math.min(state.endIndex, props.listData.length))
// }
// } else {
// return props.listData.slice(state.startIndex, Math.min(state.endIndex, props.listData.length))
// }
})
// 获取屏幕高度即可视化区域高度
// const getScreenHeight = () => {
// uni.getSystemInfo({
// success: function (res) {
// state.screenHeight = res.screenHeight
// }
// })
// }
const handleScroll = debounce(
(e) => {
const scrollTop = Math.max(e.detail.scrollTop, 0)
const maxScrollTop = listHeight.value - state.screenHeight
if (scrollTop >= maxScrollTop && maxScrollTop > 0) {
// 已经滚动到底部,不再更新索引
state.startIndex = props.listData.length - visibleCount.value
state.endIndex = props.listData.length
state.startOffset = maxScrollTop
} else {
state.startIndex = Math.floor(scrollTop / props.itemSize)
state.endIndex = state.startIndex + visibleCount.value
// 确保 state.startOffset 计算时的 scrollTop 和 props.itemSize 都是有效数值
state.startOffset =
!isNaN(scrollTop) && !isNaN(props.itemSize) ? scrollTop - (scrollTop % props.itemSize) : 0
}
},
10,
''
)
const targetType = (item) => {
if (item.dateStatus == 2) {
uni.showToast({
title: '该健康任务暂未开始',
icon: 'none'
})
return false
} else {
if (item.templateType == '健康监测') {
uni.navigateTo({ url: '/packA/pages/healthPortrait/indexTwo' })
} else if (item.templateType == '健康随访') {
store.commit('SET_FOLLOW', item)
if (item.isRead) {
uni.navigateTo({
url: '/packA/pages/follows/detail?name=' + item.itemName + '&type=' + 'read'
})
} else if (item.dateStatus == 0) {
uni.navigateTo({
url: '/packA/pages/follows/detail?name=' + item.itemName + '&type=' + 'timeout'
})
} else {
uni.navigateTo({
url: '/packA/pages/follows/fillin?name=' + item.itemName
})
}
} else if (item.templateType == '健康宣教') {
if (item.itemContent && JSON.parse(item.itemContent).id) {
uni.navigateTo({
url:
'/packA/pages/popularScience/detail?id=' +
JSON.parse(item.itemContent).id +
'&isreadId=' +
item.id +
'&dateStatus=' +
item.dateStatus
})
}
} else if (item.templateType == '饮食方案') {
// if (item.itemContent && !item.isRead) {
uni.navigateTo({
url: '/packA/pages/dailyFood/foodDetail?id=' + item.id
})
// }
} else if (item.templateType == '运动方案') {
// if (item.itemContent && !item.isRead) {
uni.navigateTo({
url: '/packA/pages/sportsInformation/sportsDetail?id=' + item.id
})
// }
} else {
uni.navigateTo({
url: '/packA/pages/healthManage/detail?id=' + item.id + '&dateStatus=' + item.dateStatus
})
}
}
}
</script>
<style lang="scss" scoped>
.content {
height: 100%;
.list-container {
height: 100%;
overflow: auto;
position: relative;
.list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.list {
left: 0;
right: 0;
top: 0;
position: absolute;
.list-item {
text-align: center;
border-bottom: 1px solid #ccc;
}
}
.taskWrap {
.item {
background: #fafafa;
border-radius: 12rpx;
padding: 24rpx;
margin-top: 24rpx;
height: 176rpx;
&:nth-child(1) {
margin-top: 0;
}
.top {
position: relative;
margin-top: 5rpx;
.image {
display: inline-block;
vertical-align: middle;
width: 40rpx;
height: 40rpx;
}
.name {
display: inline-block;
vertical-align: middle;
margin-left: 16rpx;
font-size: 32rpx;
color: #222;
font-weight: 700;
}
.time {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
font-size: 28rpx;
color: #666;
}
}
.min {
position: relative;
margin-top: 21rpx;
.contents {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 24rpx;
color: #666;
padding: 8rpx 16rpx;
background: #fff;
border-radius: 4rpx;
display: inline-block;
}
.operation {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
padding: 6rpx 23rpx;
background: #00d1b6;
border: 1rpx solid #00d1b6;
font-size: 24rpx;
color: #fff;
border-radius: 38rpx;
&.on {
background: #fff;
border-color: #00d1b6;
color: #00d1b6;
}
&.unfinished {
background: #fff;
border-color: #cfcfcf;
color: #666;
}
&.noStart {
opacity: 0.5;
}
}
}
}
}
}
}
</style>