uniapp vue3 虚拟下拉滚动 升级版 (包括ios 安卓虚拟滚动时候的兼容问题 最终版)

//注释   一进入页面就获取全部数据,然后将数据剪切成显示n个    :itemSize="88"  代表默认是每一个列表高度都是88px   最后的结果 永远只会显示对应的几个dom数据列表

 

 


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>
posted @ 2024-07-11 14:48  风雪中de冲破  阅读(45)  评论(0编辑  收藏  举报