LazyContainer.vue 2.29 KB
<script lang="ts" setup>
import { onMounted, reactive, ref, toRef } from 'vue'
import { Skeleton } from 'ant-design-vue'
import { useTimeoutFn } from '@vueuse/core'
import { basicProps } from './LazyContainer.props'
import { useIntersectionObserver } from '@/hooks/event/useIntersectionObserver'

interface State {
  isInit: boolean
  loading: boolean
  intersectionObserverInstance: IntersectionObserver | null
}
const props = defineProps(basicProps)

const emit = defineEmits(['init'])

const elRef = ref()
const state = reactive<State>({
  isInit: false,
  loading: false,
  intersectionObserverInstance: null,
})

onMounted(() => {
  immediateInit()
  initIntersectionObserver()
})

// If there is a set delay time, it will be executed immediately
function immediateInit() {
  const { timeout } = props
  timeout
          && useTimeoutFn(() => {
            init()
          }, timeout)
}

function init() {
  state.loading = true

  useTimeoutFn(() => {
    if (state.isInit) return
    state.isInit = true
    emit('init')
  }, props.maxWaitingTime || 80)
}

function initIntersectionObserver() {
  const { timeout, direction, threshold } = props
  if (timeout) return
  // According to the scrolling direction to construct the viewport margin, used to load in advance
  let rootMargin = '0px'
  switch (direction) {
    case 'vertical':
      rootMargin = `${threshold} 0px`
      break
    case 'horizontal':
      rootMargin = `0px ${threshold}`
      break
  }

  try {
    const { stop, observer } = useIntersectionObserver({
      rootMargin,
      target: toRef(elRef.value, '$el'),
      onIntersect: (entries: any[]) => {
        const isIntersecting = entries[0].isIntersecting || entries[0].intersectionRatio
        if (isIntersecting) {
          init()
          if (observer)
            stop()
        }
      },
      root: toRef(props, 'viewport'),
    })
  }
  catch (e) {
    init()
  }
}
</script>

<template>
  <transition-group
    v-bind="$attrs"
    ref="elRef"
    class="h-full w-full box-border"
    :name="transitionName"
    :tag="tag"
    mode="out-in"
  >
    <div v-if="state.isInit" key="component">
      <slot :loading="state.loading" />
    </div>
    <div v-else key="skeleton">
      <slot v-if="$slots.skeleton" name="skeleton" />
      <Skeleton v-else />
    </div>
  </transition-group>
</template>